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 a stable module graph reuses one isolate are
documented once in Platform efficiency
(platform_efficiency:guide). 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
packageGetGitRemotewithcreate: trueand a newkody_idto register a stub saved package and mint a short-lived authenticated remote in one call (for existing packages, omitcreate). 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 (package_apps:guide).
This section is the mount-prefix recipe those pages share.
Production-hosted package apps live at
https://{username}.kody.run/packages/<kody-id>/<path> (the username is in the
hostname; the path mount is /packages/<kody-id>). Confirmed non-production
runtimes may serve inline on the app origin at
/@username/packages/<kody-id>/<path> instead. The app receives only /<path>
in its fetch request in both cases. 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:
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/<kody-id>on a subdomain,/@username/packages/<kody-id>when inline).
Kody derives both fields from the package's current serving username and
kody.id, 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.json#kody.id is optional. If present it must match the package name
leaf (the URL slug). Prefer omitting it and letting the leaf be the slug.
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 still 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: "heavy_work_offload:guide" }).
Secret-using packages
When a package will use user-scoped secrets ({{secret:name}} placeholders or
kody.secretMounts):
- Ensure each secret exists (open
search({ entity: "connect_secret:guide" })/search({ entity: "secret_backed_integration:guide" })). - 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. Secret mounts bind in the package's own surfaces (jobs, apps, subscriptions, HTTP invocation). - 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 —
packageAppFetch({ kody_id })with 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 (package_apps:guide). - Subscriptions —
packageSubscriptionDispatch({ kody_id, topic, … })with exactly one ofparams(fixture) oremail_message_id(stored-mail replay) for each declared topic. 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
Public packages should include one root icon.svg, icon.png, icon.webp,
icon.jpg, or icon.jpeg. community-icon.* is also accepted. Prefer a square
visual with a simple silhouette that remains legible at 56 pixels. Keep it under
2 MiB and 16 megapixels. Kody stores a 256-pixel WebP derivative of that source
(or a generated package-name swirl when the repository has no icon).
Publishing the package refreshes the catalog listing icon automatically. The
candidate paths win in the order icon.* then community-icon.* (svg, png,
webp, jpg, jpeg in each group), so 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.