diff --git a/examples/hello/module.js b/examples/hello/module.js index e69b9a5..ee088a3 100644 --- a/examples/hello/module.js +++ b/examples/hello/module.js @@ -1,17 +1,15 @@ export const manifest = { + $schema: 'https://charm.ing/schema/app-manifest/2026-07-31.json', id: 'charming-cli-hello', - version: '0.0.1', - displayName: 'Charming CLI Hello', - capabilities: { - imports: [], - exports: ['hello'], - }, + meta: { name: 'Charming CLI Hello', icon: { emoji: '๐Ÿ‘‹', bg: '#0ea5e9' } }, + capabilities: { imports: [] }, }; export const routes = [ { op: 'hello', - readOnly: true, + method: 'GET', + annotations: { readOnlyHint: true }, handler: async (input) => ({ message: `Hello, ${typeof input?.name === 'string' ? input.name : 'world'}!`, }), diff --git a/package.json b/package.json index 7ad74a1..9a3ac3f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "bin", "dist", "examples", + "templates", "provenance.json", "UPSTREAM.json", "skills" @@ -26,7 +27,14 @@ "publishConfig": { "provenance": true }, - "keywords": ["charming", "cli", "oclif", "mcp", "agent", "personal-software"], + "keywords": [ + "charming", + "cli", + "oclif", + "mcp", + "agent", + "personal-software" + ], "scripts": { "build": "rm -rf dist && bun node_modules/typescript/bin/tsc -p tsconfig.build.json", "build:test": "rm -rf dist && bun node_modules/typescript/bin/tsc -p tsconfig.build.json --sourceMap false --declarationMap false", diff --git a/skills/charming/SKILL.md b/skills/charming/SKILL.md index 0304fb0..fc17fa5 100644 --- a/skills/charming/SKILL.md +++ b/skills/charming/SKILL.md @@ -1,6 +1,6 @@ --- name: charming -description: Build, inspect, update, and call hosted personal apps with the Charming CLI. Use when a user wants to create or manage an interactive personal app hosted by Charming. +description: Build, inspect, update, and call hosted personal apps with the Charming CLI. Covers the app contract (manifest, capabilities, env.storage, routes, window.charming.api), the sandbox rules, and the CLI workflow. Use when a user wants to create or manage an interactive personal app hosted by Charming. license: MIT compatibility: Requires the charming CLI, Node.js 22 or newer, and network access to charm.ing. metadata: @@ -19,32 +19,138 @@ metadata: Charming is a hosting platform for personal apps you and your agent build, update, and use together. Learn more at [usecharming.com](https://usecharming.com). Install: `npm install -g usecharming`. -Use `charming` to create and manage apps hosted by Charming. It saves credentials, prevents stale updates, and prints JSON errors. +Charming hosts small single-purpose personal apps. You write two files, push them with `charming`, and the user gets a real URL with real storage that any agent โ€” including this one, later โ€” can keep editing. + +An app is a directory: + +- `module.js` โ€” required. The backend: a `manifest` and a `routes` array. +- `ui.js` โ€” optional. One JavaScript program that fills `#app`. +- `styles.css` โ€” optional. Overrides only. + +Do not free-hand the skeleton. Copy `templates/crud`, then change the data model. + +> > > > > > > 111dce1 (Teach the skill to build apps, not just drive the CLI) ## Start -1. Run `charming auth status`. -2. If it reports `"authenticated": false`, run `charming auth login --no-open`. Give the approval URL and code to the user. -3. Run `charming apps create --dry-run`. The directory must contain `module.js`; it may contain `ui.js` and `styles.css`. -4. A signed-in create can replace an app with the same manifest ID. Ask before running `charming apps create --yes`. -5. Return the `url` from the JSON result. +1. `charming auth status`. If `"authenticated": false`, run `charming auth login --no-open` and give the user the approval URL and code. +2. Copy the CRUD template into a working directory: `cp -r "$(npm root -g)/usecharming/templates/crud" ./my-app` Change `manifest.id`, `manifest.meta.name`, the storage key, and the `window.charming.api("")` argument in `ui.js` to match. +3. `charming apps create ./my-app --description "" --dry-run` +4. `charming apps create ./my-app --description "" --yes` +5. Smoke-test the backend before you claim it works: `charming apps call list --input '{}'` +6. Return the `url` from the JSON result. + +`--yes` is required on a signed-in create because a create whose `manifest.id` already exists **replaces that app in place**. Ask the user before running it, and keep `manifest.id` stable and unique per app. + +Without a saved token, `charming apps create` still works: it pairs the new app to this machine and stores an app-scoped credential locally, so later `update` and `call` on that app work from the same machine without `charming auth login`. ## Iterate -- List: `charming apps list` -- Inspect operations and input schemas: `charming apps describe ` -- Export or update source: `charming apps source --out ` or `charming apps update ` -- Call an app operation: `charming apps call --input '{"key":"value"}'` -- Delete: `charming apps delete --yes` +The CLI has no partial-edit mode. The loop is export, edit, push: + +``` +charming apps source --out ./my-app # writes module.js / ui.js / styles.css +# edit the files +charming apps update ./my-app --dry-run +charming apps update ./my-app +``` + +`apps update` reads the current source, matches its ETag, and fails loudly on a conflicting concurrent write instead of clobbering it โ€” there's no revision number to track. A field missing locally is left as-is on the server: a directory with no `ui.js` updates the module and keeps the deployed UI. + +Other commands: `charming apps list`, `charming apps describe ` (the public descriptor โ€” ops and input schemas), `charming apps rename `, `charming apps delete --yes`. Run mutations with `--dry-run` first. Signed-in creates and deletions require `--yes`; pass it only after the user approves that action. +Read JSON results from stdout. Login instructions and JSON errors use stderr. Branch on `error.kind`. Follow `error.recovery` when present. Never print tokens or `device_code` values. Show `user_code` only during login. + `charming auth logout` removes the user credential and every app credential for the selected origin. Unclaimed apps that relied on those app credentials become unreachable. -Read JSON results from stdout. Login instructions and JSON errors use stderr. Branch on `error.kind`. Follow `error.recovery` when present. Never print tokens or `device_code` values. Show `user_code` only during login. +## The module + +```js +export const manifest = { + $schema: 'https://charm.ing/schema/app-manifest/2026-07-31.json', + id: 'water-log', // stable, unique, never changes + meta: { name: 'Water Log', icon: { emoji: '๐Ÿ’ง', bg: '#0ea5e9' } }, + capabilities: { imports: ['charming:storage/kv@1.0'] }, +}; + +export const routes = [ + { + op: 'list', + method: 'GET', + annotations: { readOnlyHint: true }, + handler: async (_input, { env }) => (await env.storage.get('items_v1')) ?? [], + }, +]; +``` + +- `manifest` is parsed statically. Keep it a plain literal โ€” no computed values. +- Unknown top-level keys are rejected. The keys are `$schema`, `id`, `meta`, `capabilities`, `permissions`. There is no `version` or `displayName` key โ€” the display name is `meta.name`. +- `meta.icon` is one emoji plus a hex background, not an array of image URLs. +- Declare the capabilities you use, nothing more. There is no capability `exports` array โ€” `routes` is the only way to expose an operation. +- `routes` is an array of route objects, never an object keyed by op name. `op` values must be unique. +- Charming does **not** infer `readOnlyHint` from `method: 'GET'`. Set it explicitly on every read, or the operation publishes as a mutation. +- Handlers receive `(input, { env, ctx, request })` and return a JSON-compatible value; Charming wraps it as `{ ok: true, value }` on the wire (or `{ ok: false, error: { kind, message } }` on failure). +- Only WinterTC globals (Request, Response, URL, crypto, TextEncoder, โ€ฆ). No Node APIs, no DOM. +- Omit `default.fetch` unless the app needs custom fallback HTTP; Charming supplies a 404 handler when it's absent. + +### Storage + +`env.storage` (`get` / `put` / `delete` / `list`) stores JSON values **directly**. Never `JSON.stringify` on the way in or `JSON.parse` on the way out. Storage is per-app and survives updates, so iterate freely. + +`env.storage` works everywhere the app runs โ€” the web app and inside Claude/ChatGPT. `localStorage` / `sessionStorage` / `IndexedDB` need the claim-gated `charming:browser/storage@1.0` capability _and_ are empty inside chat hosts. Anything the user expects to keep belongs in `env.storage`. + +Keep user data in `env.storage`, never in module constants โ€” a shared or templated copy of the app then starts empty instead of leaking the author's data. + +Full capability list, `permissions.server.fetch` rules, secrets, and external images: [resources/contract.md](./resources/contract.md). + +## The UI + +`ui.js` is one JavaScript program โ€” not HTML, not a module. Charming injects `
`, a Tailwind-compatible runtime, and a default theme. + +```js +const api = window.charming.api('water-log'); // manifest.id, NOT the URL's UUID +const items = await api.list({}); // the value itself; throws on failure +``` + +`window.charming.api(id).(input)` returns the value directly and throws `CharmingOperationError` on failure. It is not `api.operation(name, params)`, and it is never a raw `fetch()`. + +Sandbox rules that silently break apps: + +- `alert`, `confirm`, and `prompt` do nothing. Build inline UI instead. +- No external `