Packages
Package authoring
Official Kody doc
Use this guide when creating a new Kody package or materially changing an existing one.
A package is not done until root README.md and AGENTS.md are both present
and non-empty, README ## Intent is current, every public export has JSDoc as
specified in Export JSDoc, and the smoke tests in
Verify your publish pass (or the user explicitly skips a
surface). Publish checks fail when either file is missing or empty.
Already-published packages keep running as published; the next author-driven
publish requires both files.
Unique Dynamic Worker days and how the acting user plus a stable module graph
reuse one isolate per UTC day are documented once in
Platform efficiency (guide:platform_efficiency).
Package README and AGENTS.md files do not repeat that cost model.
Choose an authoring lane
There are two lanes for writing package source. Pick based on whether you have local filesystem and git access:
- Git lane (coding agents — preferred). Call
packageGetGitRemote({ create: true, kody_id: '@owner/leaf', description })(or the name leaf in leftoverkody_id) to register a stub saved package and mint a short-lived authenticated remote in one call (for existing packages, omitcreateand pass the scoped name, orpackage_idwhen the name is not known). Run the returnedsetup_commandsto clone into a temporary directory — they set localgit config user.email/user.namefromgit_author(the signed-in Kody account). Do not invent or guess a git identity. Edit normally — binary assets, multi-file refactors, and local build/test loops all work — commit, push, then publish withpackagePublishExternalPush. If that tool returnslocked, open the returnedapproval_urlso the owner can promote the named commit. Do not treat HEAD as live untilpublished_commitmoves. When the OAuth token is coarser than the export (Gmail drafts without send), lock after the first publish — see locked-gmail-drafts.md. - Tool-only lane. Without local filesystem/git access, create with
packageSave(complete UTF-8 text file set; no binary files) and edit through repo sessions (repoOpenSession,repoEditFiles,repoCommit, thenrepoRunChecksbeforerepoPublishSession).
If a request needs binary assets, many-file changes, or local build/test loops and you are tool-only, tell the user the task fits a coding-capable agent better and confirm before proceeding.
Package docs (README.md + AGENTS.md)
Publish requires two non-empty root files. They are not interchangeable.
README.md— human-focused. What the package does, who it is for, prerequisites, setup, and how a person knows it is working. Include a concise## Intentsection. Do not treat this file as the agent runbook.AGENTS.md— agent-focused. Imports, smoke-test calls, edge cases, and other notes an agent needs to use or change the package. Do not dump that runbook intoREADME.md.
Repo checks (repoRunChecks, repoPublishSession,
packagePublishExternalPush) fail with a docs check when either file is
missing or empty. Community install and platform codemods do not apply this
gate, so existing listings stay forkable. The next author-driven publish of a
package must add both files.
New stubs from packageGetGitRemote (create: true) include placeholders for
both files. Replace those placeholders before the first real publish.
README Intent section
Package intent is human-authored guidance, not a Kody primitive. Keep it in the
root README.md so people (and search detail) see why the package exists.
When you create or materially change a package:
- Include or maintain a
## Intentsection inREADME.md. - Capture the user's goal in a few concrete sentences.
- Ask the user when the intent is unclear or underspecified.
- Update the intent only when you are confident the goal changed.
- If the user expands the package scope, update the section with the new scope.
Do not add a package manifest field, runtime object, saved value, or other Kody primitive solely to track intent.
README Intent is package-level. It does not replace per-export JSDoc. Search shows Intent and an Exports Purpose column side by side; Purpose comes from each export's JSDoc, not from this section.
Minimal shape
# Package Name
## Intent
This package exists to ...Keep the section concise. It should explain why the package exists and what success means for the user, not duplicate every implementation detail.
AGENTS.md is a separate root file. A typical stub:
# Package Name
## Imports
```ts
import main from 'kody:@scope/id'
```
## Smoke tests
Call the root export from `execute` after publish.
## Edge cases
…Export JSDoc
Search detail (entity: "package:…") shows an Exports table whose Purpose
column comes from each export's JSDoc. When JSDoc is missing, Purpose falls back
to the generic string Package export. Agents skim that column first when
choosing among sibling exports.
TypeScript types and the export name give call shape when present. They do not
say when or why to pick one export over another. README ## Intent is
package-scoped and often does not name every export. Neither replaces per-export
JSDoc.
Do not add a package manifest field, runtime object, saved value, or other Kody primitive solely to track export purpose. Put it in JSDoc on the exported function.
When you create or materially change a public export:
- Write JSDoc immediately above the exported function (or above
export defaultfor a local binding in the same file). - Start with one line that states what the export does and when to call it.
- Add
@paramfor each input. - Add
@returns. - Add
@examplethat importskody:@scope/id/exportand calls it. Do not lead withpackages.invoke.
If the export's package.json exports entry has a types condition, put the
JSDoc on that types file — search reads the types module when it exists. JSDoc
on an imported re-export (export default foo where foo is imported) is not
attributed; implement the function in the export file (or a local binding in
that file) so the comment sits on the exported symbol.
/**
* Format a Discord moderation report for a channel.
* Use when a human or job needs a readable summary of recent flags.
*
* @param input - Channel id and optional lookback window
* @returns Markdown report body
*
* @example
* import formatReport from 'kody:@scope/discord/format-report'
*
* const report = await formatReport({ channelId: '123' })
*/
export default async function formatReport(input: {
channelId: string
lookbackHours?: number
}): Promise<{ markdown: string }> {
return { markdown: '' }
}Treat missing or generic Purpose (Package export.) as unfinished work, the
same as a missing README ## Intent section.
Package app routing
Hosted-app session handoff, packageAppFetch smoke tests, asset URLs, and lean
forks are the Package apps playbook (guide:package_apps).
This section is the mount-prefix recipe those pages share.
Production-hosted package apps live at
https://{username}.kody.run/packages/<package-name>/<path> (the username is in
the hostname; the path mount is /packages/<package-name>). Confirmed
non-production runtimes may serve inline on the app origin at
/@username/packages/<package-name>/<path> instead.
Every package app receives only /<path> after the host strips the mount.
Root-relative links such as /audio/123 therefore escape the mount and are not
routed back to the package app. Import packageContext from kody:runtime and
build every in-app link, redirect, share/email URL, and OAuth callback against
its public base. A Remix recipe that prefixes its route contract remounts the
Request in the entry; see Package apps.
import { packageContext } from 'kody:runtime'
import { form, route } from 'remix/routes'
export const routes = route(packageContext?.appBasePath ?? '', {
home: '/',
notes: form('notes'),
})A fetch handler builds those URLs itself:
import { packageContext } from 'kody:runtime'
if (!packageContext?.hostedUrl) {
throw new Error('This module must run as a package app.')
}
const audioUrl = new URL(
`${packageContext.appBasePath}/audio/123`,
packageContext.hostedUrl,
)packageContext.hostedUrlis the full public URL of the app mount.packageContext.appBasePathis the origin-relative mount path (/packages/<package-name>on a subdomain,/@username/packages/<package-name>when inline).
Kody derives both fields from the package's current serving username and package name leaf, including after a rename or fork. Do not hard-code either path segment.
kody.description (short public tagline)
package.json#kody.description is a short public tagline, not a feature
dump. Aim for about 80–120 characters (hard max 200). Prefer outcome
phrasing such as “Send transactional email via Resend” over inventory lists of
exports, auth, or APIs.
Put feature lists, API surface, auth notes, and longer human guidance in
README.md (including ## Intent), agent runbooks in AGENTS.md,
kody.searchText, and export JSDoc — not in
kody.description. Community listings and Open Graph share cards reuse this
field, so keep it concise.
kody.category (community browse)
Public community listings browse by a closed category. Set
package.json#kody.category to one of integrations, examples,
productivity, apps, or utilities before making the package public. When
the field is omitted, Kody infers a category from well-known tags such as
github or zero-auth, or files the listing under Other. Tags stay freeform
search keywords; do not use kody.tags as a second category vocabulary.
Package visibility
New packages are always private. Visibility is a repo setting
(packageUpdate changes.visibility or repoUpdate), not
package.json#private. Ignore leftover "private" in manifests.
- Public means default-branch HEAD is world-readable and forkable and the
package appears on
/community. - Private is owner-only. Going private 404s public URLs; existing forks keep
their copies. Type the package slug to confirm (
confirm_namefor agents). - There are no MIT, logo, or README Intent platform gates to become public.
Agents run a personal-details hygiene pass before flipping public (below). The
Worker does not scan or block on that review. Publishing a version requires
non-empty root
README.mdandAGENTS.md(see Package docs).
Personal-details hygiene before going public
Before calling packageUpdate with changes.visibility: "public" or
communityPublish, review source, README, Intent, description, tags, examples,
and hardcoded identifiers for overly personal material.
Treat as personal or too household-specific:
- home addresses, private emails, phone numbers, family names
- personal Discord or channel IDs
- private calendar habits
- one-off personal automation that only makes sense for one household
- secrets, tokens, and internal-only URLs
If the package is clean, proceed.
If anything looks personal or hyper-specific to one person or household, do not publish yet. Tell the user what you found. Suggest how to generalize: parameterize identifiers, use secrets or integrations instead of hardcoded credentials, rename examples, strip PII, and keep a private fork for personal wiring. Wait for explicit go-ahead before flipping public.
Work that does not fit a Worker isolate
Package checks and publish rebuilds bundle every declared npm dependency inside
a short-lived isolate. A large graph (PDF.js / unpdf, native addons, browsers,
big WASM) can fail with an isolate memory or CPU reset even when the same import
works in ad hoc execute.
Do not vendor the library, switch to a dynamic import, or skip checks. Keep the
Kody package as a thin orchestrator and run the heavy work in a process the
owner operates. Open search({ entity: "guide:heavy_work_offload" }).
Secret-using packages
Password-manager items use custom secret providers
({{secret/<provider>:<ref>}} and kody.secretProvider); that path is separate
from user-secret grants.
When a package will use user-scoped secrets ({{secret:name}} placeholders or
kody.secretMounts):
- Ensure each secret exists (open
search({ entity: "guide:connect_secret" })/search({ entity: "guide:secret_backed_integration" })). - Self-authored packages and community forks adopted with
communityForkAdoptafter a real source review get automatic read/use access to user secrets (host approval still applies;secretSet/secretDeletestill need anallowed_packagesgrant). After save/publish, readpending_secret_package_approvalsfrom the tool result — it is non-null only for unadopted community forks. - When pending approvals are present, either review the fork source and call
communityForkAdoptwith areview_summary, or send the userbulk_approval_url/ eachapproval_url. - Wait for approval or adoption (when required), then smoke-test from
executewith a statickody:@scope/package/exportimport. Use a read-only export or a package-supported dry-run input that actually reads the approved secret (for example an authenticated read-only API call), so the smoke test proves secret access without external side effects. Dependents compose the same way: they statically import the export; the stamp uses the owning package's grants andkody.secretMountswithout adding the dependent toallowed_packages. - Only then treat the package as ready to run.
Host approval (from an earlier ad hoc execute smoke test) is separate from
package approval. Unadopted community-forked packages may need both;
self-authored and adopted packages still need host approval when outbound calls
require it.
Verify your publish
After publish succeeds — and after any required secret approvals — confirm every
export's search Purpose is real JSDoc (not Package export.; see
Export JSDoc), then run synthetic smoke tests for every
declared surface before calling the package complete. Synthetic invocations are
real-surface runs with real side effects; use a deliberately visible
irreversible-side-effect guard when a smoke test should stay safe.
- Read
test_hintson thepackagePublishExternalPushresult when present. It lists copy-pasteable calls for declared apps and subscription topics. - Exports and secret mounts — statically import
kody:@scope/package/exportfromexecuteagainst a read-only export or package-supported dry-run input that exercises approved secrets (see Secret-using packages above). - Package apps —
packageAppFetchwith the scoped name (orpackage_idwhen the name is not known), plus the path, method, and body your handler needs. Confirm{ status, headers, body, truncated }and anypackageStorage()side effects. See Package app fetch and the Package apps playbook (guide:package_apps). - Subscriptions — from interactive MCP,
packageSubscriptionDispatchwith the scoped name (orpackage_idwhen the name is not known),topic, and exactly one ofparams(fixture) oremail_message_id(stored-mail replay) for each declared topic. This verifies the handler you just published. Reuse another package with a statickody:@scope/package/exportimport. See Synthetic event dispatch and the package subscriptions guide. - Optional UI checks — open
hosted_app_urlwhen the publish response includes one; synthetic app fetches do not replace browser verification for layout, OAuth redirects, or websocket facets.
Only after README.md + AGENTS.md, README ## Intent, per-export JSDoc, and
these checks pass (or the user explicitly skips a surface) treat the package as
ready to run.
Package icon
Put the list/identity mark at .kody/icon.png (or .kody/icon.svg,
.kody/icon.webp, .kody/icon.jpg, .kody/icon.jpeg). Prefer a square visual
with a simple silhouette that remains legible at 56 pixels. Keep it under 2 MiB
and 16 megapixels. For a package that represents a product or service, use that
product's official logo. Kody stores a 256-pixel WebP derivative of that source
(or a generated package-name swirl when the repository has no icon).
.kody/ is platform metadata, like .github/. Keep app PWA icons
(icons/icon-192.png, public/) in the product tree — they are not the list
mark.
Publishing or pushing the package indexes the mark from the published commit.
The first existing file wins in this order: .kody/icon.*, root icon.*, root
community-icon.* (svg, png, webp, jpg, jpeg in each group), then
icons/icon-192.png for package apps. When replacing an icon with a different
format (for example svg → png), delete the superseded file in the same commit or
the earlier path in that list keeps winning. packageSave is text-only; binary
icons arrive through the Artifacts git / publish path.