total-recall-brain 3.31.2 → 3.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/bin/total-recall.mjs +20 -2
  2. package/package.json +2 -2
  3. package/scaffold/.agent/skills/plugins/SKILL.md +5 -0
  4. package/scaffold/.agent/skills/total-recall/SKILL.md +33 -3
  5. package/src/cli/api.mjs +133 -0
  6. package/src/cli/app/index.mjs +181 -1
  7. package/src/cli/brief.mjs +195 -0
  8. package/src/cli/command.mjs +269 -5
  9. package/src/cli/config.mjs +25 -7
  10. package/src/cli/connect.mjs +1 -1
  11. package/src/cli/daemon.mjs +3 -3
  12. package/src/cli/group.mjs +151 -0
  13. package/src/cli/init.mjs +54 -0
  14. package/src/cli/plugin/create.mjs +20 -10
  15. package/src/cli/project-id.mjs +43 -0
  16. package/src/cli/repo-expert-generate.mjs +37 -3
  17. package/src/cli/secret.mjs +36 -3
  18. package/src/cli/skill-config.mjs +211 -0
  19. package/src/cli/skill.mjs +37 -2
  20. package/src/core/api-routes.mjs +99 -0
  21. package/src/core/app-deploy/adapter-contract.mjs +176 -0
  22. package/src/core/app-deploy/design-tokens.mjs +148 -108
  23. package/src/core/app-deploy/plan.mjs +2 -2
  24. package/src/core/app-deploy/resolve.mjs +30 -2
  25. package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +211 -0
  26. package/src/core/app-deploy/runtime-cli/python.py.tmpl +178 -0
  27. package/src/core/app-deploy/runtime-cli.mjs +254 -0
  28. package/src/core/app-deploy/ui-elements.mjs +473 -0
  29. package/src/core/app-deploy/verify.mjs +18 -0
  30. package/src/core/command-surface.mjs +129 -0
  31. package/src/core/daemon-control.mjs +31 -12
  32. package/src/core/daemon-loop.mjs +14 -7
  33. package/src/core/plugin-loader.mjs +12 -12
  34. package/src/core/project-id.mjs +72 -0
  35. package/src/core/provider-catalog.mjs +1 -1
  36. package/src/core/schema.mjs +72 -13
  37. package/src/core/secrets-env-export.mjs +3 -4
  38. package/src/core/secrets-store.mjs +30 -29
  39. package/src/core/skill-config.mjs +376 -0
  40. package/src/core/skills-registry.mjs +147 -31
  41. package/src/core/source-adapters.mjs +2 -2
  42. package/src/core/surface.mjs +109 -16
  43. package/src/server/routes/help.mjs +1 -1
  44. package/src/server/routes/system.mjs +1 -1
  45. package/plugins/code-quality/README.md +0 -25
  46. package/plugins/code-quality/cli.mjs +0 -25
  47. package/plugins/code-quality/plugin.json +0 -17
@@ -88,6 +88,11 @@ const COMMANDS = {
88
88
  brain: 'brain.mjs',
89
89
  config: 'config.mjs',
90
90
  skill: 'skill.mjs',
91
+ 'project-id': 'project-id.mjs',
92
+ api: 'api.mjs',
93
+ brief: 'brief.mjs',
94
+ group: 'group.mjs',
95
+ groups: 'group.mjs',
91
96
  secret: 'secret.mjs',
92
97
  secrets: 'secret.mjs',
93
98
  help: 'help.mjs',
@@ -167,6 +172,19 @@ async function main() {
167
172
 
168
173
  const handlerFile = COMMANDS[command];
169
174
  if (!handlerFile) {
175
+ // Any HTTP API route group without a dedicated verb: `total-recall <group> <action> …`
176
+ try {
177
+ const { loadApiRoutes } = await import('../src/core/api-routes.mjs');
178
+ if ((await loadApiRoutes()).some((r) => r.group === command)) {
179
+ const { default: apiCli } = await import('../src/cli/api.mjs');
180
+ await apiCli(args);
181
+ process.exit(process.exitCode ?? 0);
182
+ }
183
+ } catch (err) {
184
+ console.error(`Error: ${err.message}`);
185
+ process.exit(1);
186
+ }
187
+
170
188
  // Check if it is a dynamic integration command configured in the VFS
171
189
  try {
172
190
  const { agentDir, resolveBrainLayer } = await import('../src/core/config.mjs');
@@ -188,7 +206,7 @@ async function main() {
188
206
  } else {
189
207
  await handler.default(process.argv.slice(3));
190
208
  }
191
- process.exit(0);
209
+ return;
192
210
  }
193
211
  }
194
212
 
@@ -208,7 +226,7 @@ async function main() {
208
226
  } else {
209
227
  await handler.default(process.argv.slice(3));
210
228
  }
211
- process.exit(0);
229
+ return;
212
230
  }
213
231
  }
214
232
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.31.2",
3
+ "version": "3.32.1",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -64,7 +64,7 @@
64
64
  "homepage": "https://github.com/gregiteen/total-recall#readme",
65
65
  "dependencies": {
66
66
  "@simplewebauthn/server": "^13.3.2",
67
- "@ssss/cli": "github:gregiteen/ssss#main",
67
+ "@ssss/cli": "github:gregiteen/ssss#v0.10.1",
68
68
  "bcrypt": "^6.0.0",
69
69
  "better-sqlite3": "^12.11.1",
70
70
  "cookie-parser": "^1.4.7",
@@ -39,6 +39,11 @@ An ID is lowercase kebab case, starts with a letter, and is 2–64 characters. `
39
39
  - **Agent context:** add `compile: { "generator": "./generator.mjs", "auto": true }` and export `generateContext()` from that file. Only include information useful to the agent; avoid secrets and noisy telemetry.
40
40
  - **Memory category:** declare it under `ssss_schemas.categories` using the current manifest schema. Preserve SSSS Markdown and operation-contract writes; do not write vault documents directly.
41
41
 
42
+ - **Repo-customized skill:** a layered skill (plugin-owned `core/`, repo-owned files beside it) declares its repo config contract as `core/config.schema.json`, a JSON Schema whose root is an object. Annotate an array with `"x-collection": "<noun>"` (and optionally `"x-key"`, default `name`) to get `<noun> list|add|remove`. An optional `core/detect.mjs` exports `detect({ repoRoot, skillDir })` and returns a proposed config. Users customize the repo layer with `total-recall skill config <id> config set …`, `detect --apply`, or `init`, or with a repo command generated by `total-recall command create <name> --config-for-skill <id>`. Every write validates the whole config and commits a `skill_config` record to the repo's project vault; `config.json` is rebuilt from it. Never tell users to edit `config.json` by hand: `config validate` reports hand edits as drift, and `config import` adopts a pre-existing file. Mark gate-command string fields `"x-command": true`; `skill config <id> check`, `skill status <id>`, and `app verify` confirm each one resolves without running it.
43
+
44
+ - **App-runtime CLI:** declare `app_cli` in `plugin.json`: `name`, `api: { base_url_env, default_base_url }` (loopback only; operators override it with the env var or `--base-url`), optional `auth: { type: "bearer", token_env }`, and `commands: [{ name, method, path, args }]`. Args are typed (`string|integer|number|boolean`, `choices`, `default`, `required`, `positional`); `{name}` in `path` must be a required arg, other args go to the query (GET/DELETE) or JSON body. `total-recall app cli <plugin> --lang node|python` (or `--adapter flask|nextjs|react|ssss-app`) prints a self-contained file; `--out <file>` writes it. The file runs without Total Recall: exit 0 ok, 1 the app returned an error, 2 usage, 3 unreachable; `--json` prints `{ ok, status, data | error }`. Regenerate after changing the spec; never hand-edit the output.
45
+ - **UI elements:** author each element once as a self-contained custom element: an ES module under the plugin (e.g. `ui/<id>.js`) that imports nothing, calls `customElements.define('<tag>', …)` exactly once, and styles itself only with design-token variables (`var(--color-…)`, `--spacing-…`, `--radius-…`, `--typography-<style>-…`, `--font-family-…`; `--_name` for private ones). No literal colors, not even as `var()` fallbacks. Declare it in `plugin.json` `ui.elements[]`: `id`, `kind` (`panel|form|list|detail|settings|widget`), `tag`, `module`, optional `slot`, typed `props` (set as DOM properties), `events`, the `tokens` it requires and `optional_tokens` it uses with a token fallback; `ui.design_tokens` may point at a neutral default `DESIGN.md`. `total-recall app ui <plugin> --ui web-components|react` (or `--adapter flask|ssss-app|nextjs|react`) with `--tokens <app DESIGN.md>` checks the required tokens and generates `tokens.css`, `elements/<id>.js`, `index.js`, React wrappers (`--typescript` for `.tsx` plus `.d.ts`), and `ui-manifest.json`; `--out <dir>` writes `<dir>/<plugin-id>/`. An app restyles elements by editing its `DESIGN.md` and regenerating; the element files do not change.
46
+
42
47
  ## Validate the actual job
43
48
 
44
49
  1. Validate `plugin.json` against the installed schema and `total-recall plugin info <id>`.
@@ -28,7 +28,7 @@ You MUST actively reference and apply this skill under the following specific ru
28
28
 
29
29
  ### 1. When the user establishes a rule, preference, or correction
30
30
  * **Trigger**: Whenever the user corrects your behavior, shares a coding style, defines an architectural pattern, or establishes a preference you must remember.
31
- * **Action**: Autonomously create a new SSSS v2 memory node under `.agent/memory-vault/` and compile the vault.
31
+ * **Action**: Save the new rule with `npx total-recall remember` in the appropriate category and scope. The CLI validates the node and compiles the vault.
32
32
 
33
33
  ### 2. When referencing invariants, preferences, and corrections (Intrinsically Known Rules)
34
34
  * **⚠️ MANDATORY**: The compiled instruction files (like `INSTRUCTIONS.md`, `GEMINI.md`, `AGENTS.md`, etc.) contain the **entirety** of active invariants, preferences, and behavior corrections (rules/directives).
@@ -36,7 +36,7 @@ You MUST actively reference and apply this skill under the following specific ru
36
36
 
37
37
  ### 3. When searching for complex domain knowledge or general facts
38
38
  * **Trigger**: When you need to retrieve deep factual information (e.g. from the `facts/` category) or conceptual domain structures (e.g. from `concepts/`) that are not present in your active instruction files.
39
- * **Action**: Use the semantic/lexical hybrid search engine (via the local `recall` command, or `/api/memory/search/semantic` endpoint) along with regular text search (e.g., grep):
39
+ * **Action**: Use `npx total-recall recall "<query>"` to search memory. Do not read or grep memory-vault files directly.
40
40
  * **Hybrid Search**: Blends cosine-similarity vector search and lexical density TF-IDF keyword matching using Reciprocal Rank Fusion (RRF). This blends intent-based semantic matching with exact keyword accuracy.
41
41
  * **Hierarchical Parent-Child Search**: Memory node bodies are chunked and indexed as children. The vector store searches both the parent node and child chunks, returning matches based on maximum similarity.
42
42
  * **Regular Text Search**: Use grep to locate exact configurations, literal keys, or symbols.
@@ -58,7 +58,7 @@ You MUST actively reference and apply this skill under the following specific ru
58
58
 
59
59
  ### 5. When you modify, add, or delete files in the memory vault
60
60
  * **Trigger**: Immediately after performing any memory node writes.
61
- * **Action**: Rebuild the instruction shims. If using CLI commands (`npx total-recall remember` / `forget`), recompilation runs automatically in the background (asynchronously via detached subprocesses) to minimize latency. If editing vault files directly, trigger manual compilation by sending a POST request to `/api/vault/compile` or running `npx total-recall compile`.
61
+ * **Action**: The `remember` and `forget` CLI commands compile the instruction shims automatically. Use `npx total-recall compile` only when a manual rebuild is needed; never edit vault files directly.
62
62
 
63
63
  ### 5b. When handling ANY credential, API key, or password
64
64
  Never invent a new place to keep it. It goes in the encrypted secrets store —
@@ -74,6 +74,24 @@ See **TESTS**. The full suite and the quality gates run only on the Mac Mini —
74
74
  never on a laptop or the cloud droplet. A single spec file may run on any mesh node. Builds and installs are fine on any machine; build large apps
75
75
  on the Mac Mini.
76
76
 
77
+ ### 5e. When operating any third-party provider (DNS, registrar, hosting, mail, payments, telephony)
78
+ The user's providers are already integrated: their API keys live in the secrets
79
+ store. "Use the API" for a provider means exactly that.
80
+ 1. `npx total-recall secret catalog` — find the provider's key(s) by provider tag.
81
+ 2. Call the provider's REST API or CLI with the value read inside your process
82
+ (never echoed, never in argv). Check the provider's current API docs first.
83
+ 3. **Do not** fall back to browser automation when an API exists, and **do not**
84
+ ask the user for a key the catalog already has. Browser only for flows with
85
+ no API (e.g. a portal-only compliance form).
86
+ 4. Prefer targeted writes over bulk ones: edit one DNS record by id, never
87
+ replace a whole zone by default; read the current state first and verify after.
88
+ 5. If a key is missing, find the service that already uses it (its deployed
89
+ env), import it with `secret set <key> --stdin`, and record where it lives
90
+ with `remember fact`.
91
+ 6. If the agent host's own permission mode blocks an action category (e.g. DNS
92
+ changes), say so in one line with the exact setting that allows it. Do not
93
+ argue repeatedly and do not route around it through another tool.
94
+
77
95
  ### 6. When troubleshooting connections, port blocks, or sync errors
78
96
  * **Trigger**: If the REST server is unreachable, ports are blocked, or the upstream sync tool encounters errors.
79
97
  * **Action**: Refer to the active diagnostics manual within this skill to heal the local runtime.
@@ -338,6 +356,18 @@ curl -H "Authorization: Bearer <YOUR_PAT_TOKEN>" \
338
356
 
339
357
  ## 🧩 SKILLS MANAGEMENT (`skill` subcommand)
340
358
 
359
+ ### Initializing a new repository
360
+
361
+ After initializing a project brain, initialize that repository's own
362
+ `.agent/skills/repo-expert/` from its current code, manifests, tests, and
363
+ runtime configuration. `skill generate-expert --repo <repo>` is a starting
364
+ point; review and trim generated output before use, especially directory trees
365
+ that include runtime data. The local expert must declare `repo_scoped: true`
366
+ and pass an identity check against the current repository. Register it in the
367
+ project brain. The global `repo-expert` remains a repository-agnostic discovery
368
+ method; never deploy another project's expert or synchronize local experts
369
+ across repositories.
370
+
341
371
  Two distinct layers. Confusing them is the usual cause of "my edit disappeared".
342
372
 
343
373
  | Layer | What it is | Commands |
@@ -0,0 +1,133 @@
1
+ /**
2
+ * total-recall api — every HTTP API route as a CLI verb.
3
+ *
4
+ * total-recall api list [group] [--json]
5
+ * total-recall api <group> <action> [positional params…] [--data '<json>'] [--query k=v]… [--json]
6
+ * total-recall api call <METHOD> </api/path> [--data '<json>'] [--query k=v]…
7
+ *
8
+ * Also reachable as `total-recall <group> <action>` for any group that has no
9
+ * dedicated verb. Talks to the brain server named in config/brain.json.
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { loadApiRoutes } from '../core/api-routes.mjs';
14
+ import { detectProjectBrain, getGlobalBrainDir } from './agent-dir.mjs';
15
+
16
+ function brainConnection() {
17
+ const dirs = [detectProjectBrain(process.cwd())?.brainDir, getGlobalBrainDir()].filter(Boolean);
18
+ for (const d of dirs) {
19
+ try {
20
+ const c = JSON.parse(fs.readFileSync(path.join(d, 'config', 'brain.json'), 'utf8'));
21
+ if (c.url) return { url: process.env.TR_BRAIN || c.url, token: process.env.TR_PAT || c.token };
22
+ } catch {
23
+ /* try next */
24
+ }
25
+ }
26
+ return { url: process.env.TR_BRAIN || 'http://localhost:3000', token: process.env.TR_PAT };
27
+ }
28
+
29
+ function parse(argv) {
30
+ const out = { pos: [], query: {}, data: undefined, json: false, help: false };
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const a = argv[i];
33
+ if (a === '--json') out.json = true;
34
+ else if (a === '--help' || a === '-h') out.help = true;
35
+ else if (a === '--data') out.data = argv[++i];
36
+ else if (a === '--query') {
37
+ const [k, ...v] = String(argv[++i] ?? '').split('=');
38
+ if (k) out.query[k] = v.join('=');
39
+ } else out.pos.push(a);
40
+ }
41
+ return out;
42
+ }
43
+
44
+ async function call(method, routePath, { query, data }) {
45
+ const { url, token } = brainConnection();
46
+ const qs = new URLSearchParams(query).toString();
47
+ const res = await fetch(`${url.replace(/\/$/, '')}${routePath}${qs ? `?${qs}` : ''}`, {
48
+ method,
49
+ headers: {
50
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
51
+ ...(data !== undefined ? { 'Content-Type': 'application/json' } : {}),
52
+ },
53
+ body: data !== undefined ? data : undefined,
54
+ });
55
+ const text = await res.text();
56
+ let body;
57
+ try {
58
+ body = JSON.parse(text);
59
+ } catch {
60
+ body = text;
61
+ }
62
+ return { status: res.status, ok: res.ok, body };
63
+ }
64
+
65
+ function printRoutes(routes) {
66
+ const width = Math.max(...routes.map((r) => `${r.group} ${r.action}`.length), 10);
67
+ for (const r of routes) {
68
+ const params = r.params.map((p) => ` <${p}>`).join('');
69
+ console.log(` ${`${r.group} ${r.action}${params}`.padEnd(width + 12)} ${r.method.padEnd(6)} ${r.path}`);
70
+ }
71
+ }
72
+
73
+ export default async function apiCli(argv = []) {
74
+ const opts = parse(argv[0] === 'api' ? argv.slice(1) : argv);
75
+ const routes = await loadApiRoutes();
76
+ const [first, second, ...rest] = opts.pos;
77
+
78
+ if (opts.help || !first || first === 'list') {
79
+ const group = first === 'list' ? second : null;
80
+ const shown = group ? routes.filter((r) => r.group === group) : routes;
81
+ if (opts.json) return console.log(JSON.stringify(shown, null, 2));
82
+ if (!group && !opts.help && first !== 'list') {
83
+ console.log('Usage: total-recall api list [group] | api <group> <action> [params] [--data json] [--query k=v] | api call <METHOD> <path>\n');
84
+ }
85
+ if (!shown.length) return console.log(` No routes for group "${group}".`);
86
+ if (!group) {
87
+ const groups = [...new Set(routes.map((r) => r.group))];
88
+ console.log(` ${routes.length} routes in ${groups.length} groups: ${groups.join(', ')}\n`);
89
+ if (!opts.help && first !== 'list') return;
90
+ }
91
+ return printRoutes(shown);
92
+ }
93
+
94
+ if (first === 'call') {
95
+ const [method, routePath] = [String(second || '').toUpperCase(), rest[0]];
96
+ if (!method || !routePath) {
97
+ console.error('Usage: total-recall api call <METHOD> </api/path> [--data json] [--query k=v]');
98
+ process.exit(1);
99
+ }
100
+ return finish(await call(method, routePath, opts), opts);
101
+ }
102
+
103
+ const group = first;
104
+ const groupRoutes = routes.filter((r) => r.group === group);
105
+ if (!groupRoutes.length) {
106
+ console.error(`Unknown API group "${group}". Run: total-recall api list`);
107
+ process.exit(1);
108
+ }
109
+ if (!second) return printRoutes(groupRoutes);
110
+ const route = groupRoutes.find((r) => r.action === second);
111
+ if (!route) {
112
+ console.error(`No "${second}" action in ${group}. Actions:`);
113
+ printRoutes(groupRoutes);
114
+ process.exit(1);
115
+ }
116
+ if (rest.length < route.params.length) {
117
+ console.error(`Missing parameter(s): ${route.params.slice(rest.length).map((p) => `<${p}>`).join(' ')}`);
118
+ process.exit(1);
119
+ }
120
+ let p = route.path;
121
+ route.params.forEach((name, i) => {
122
+ p = p.replace(`:${name}`, encodeURIComponent(rest[i]));
123
+ });
124
+ return finish(await call(route.method, p, opts), opts);
125
+ }
126
+
127
+ function finish(r, opts) {
128
+ console.log(typeof r.body === 'string' ? r.body : JSON.stringify(r.body, null, opts.json ? 0 : 2));
129
+ if (!r.ok) {
130
+ console.error(`HTTP ${r.status}`);
131
+ process.exitCode = 1;
132
+ }
133
+ }
@@ -8,6 +8,8 @@
8
8
  * app create <dir> --plugin <source> [--adapter <id>] [--name <name>] [--force] [--json]
9
9
  * app upgrade <id> --to <source> [--target <dir>] [--adapter <id>] [--dry-run] [--json]
10
10
  * app verify [--target <dir>] [--json]
11
+ * app cli <source> [--lang node|python | --adapter <id>] [--out <file>] [--force] [--json]
12
+ * app ui <source> [--ui web-components|react | --adapter <id>] [--tokens <DESIGN.md>] [--typescript] [--out <dir>] [--force] [--json]
11
13
  */
12
14
 
13
15
  import path from 'node:path';
@@ -26,6 +28,10 @@ import {
26
28
  AppCreateError
27
29
  } from '../../core/app-deploy/standalone.mjs';
28
30
  import { verifyApplication } from '../../core/app-deploy/verify.mjs';
31
+ import { resolveCapabilitySource } from '../../core/app-deploy/source.mjs';
32
+ import { generateAppCli, languageForAdapter, APP_CLI_LANGUAGES } from '../../core/app-deploy/runtime-cli.mjs';
33
+ import { generateUiElements, writeUiElements, uiTargetForAdapter, UI_TARGETS } from '../../core/app-deploy/ui-elements.mjs';
34
+ import fs from 'node:fs';
29
35
  import {
30
36
  IncompatibleAdapterError,
31
37
  IncompatibleSsssVersionError,
@@ -73,6 +79,23 @@ Commands:
73
79
  --target <dir> Target application directory (default: cwd)
74
80
  --json Output verification report as JSON
75
81
 
82
+ cli <source> Generate the app's own runtime CLI from the plugin's app_cli spec.
83
+ The file needs no Total Recall install at run time.
84
+ --lang <lang> node (ESM script) or python (argparse module)
85
+ --adapter <id> Pick the language from the adapter (flask: python; else node)
86
+ --out <file> Write the file (mode 755) instead of printing it
87
+ --force Replace an existing --out file
88
+ --json Output { language, path, sha256, out } as JSON
89
+
90
+ ui <source> Generate the plugin's UI elements for the app, styled by its DESIGN.md.
91
+ --ui <target> web-components (Flask/Jinja, any HTML) or react (React/Next)
92
+ --adapter <id> Pick the target from the adapter (flask, ssss-app: web-components)
93
+ --tokens <file> The app's DESIGN.md (default: the plugin's own default tokens)
94
+ --typescript React wrappers as .tsx
95
+ --out <dir> Write <dir>/<plugin-id>/ (replaced whole) instead of listing files
96
+ --force Replace a previously generated <dir>/<plugin-id>/
97
+ --json Output { target, files, elements, tokens, warnings, out } as JSON
98
+
76
99
  Exit Codes:
77
100
  0 Success
78
101
  1 User/argument error or internal failure
@@ -418,7 +441,164 @@ export async function run(argv = []) {
418
441
  process.exit(0);
419
442
  }
420
443
 
421
- // 5. APP VERIFY
444
+ // 5. APP CLI
445
+ else if (sub === 'cli') {
446
+ let source = null;
447
+ let language = null;
448
+ let adapter = null;
449
+ let out = null;
450
+ let force = false;
451
+ let isJson = false;
452
+ for (let i = 0; i < rest.length; i++) {
453
+ const arg = rest[i];
454
+ if (arg === '--json') isJson = true;
455
+ else if (arg === '--force') force = true;
456
+ else if (arg === '--lang' && rest[i + 1]) language = rest[++i];
457
+ else if (arg === '--adapter' && rest[i + 1]) adapter = rest[++i];
458
+ else if (arg === '--out' && rest[i + 1]) out = rest[++i];
459
+ else if (!arg.startsWith('--') && !source) source = arg;
460
+ else {
461
+ console.error(`\n❌ Unknown or incomplete option: ${arg}\n`);
462
+ process.exit(1);
463
+ }
464
+ }
465
+ const report = (message, code) => {
466
+ if (isJson) console.error(JSON.stringify({ error: message }));
467
+ else console.error(`\n❌ ${message}\n`);
468
+ process.exit(code);
469
+ };
470
+ if (!source) report('Missing capability source. Usage: total-recall app cli <source> --lang node|python [--out <file>]', 1);
471
+ if (language && adapter) report('Pass --lang or --adapter, not both', 1);
472
+ if (!language && !adapter) report(`Pass --lang (${APP_CLI_LANGUAGES.join('|')}) or --adapter <id>`, 1);
473
+ if (adapter) {
474
+ try {
475
+ language = languageForAdapter(adapter);
476
+ } catch (err) {
477
+ report(err.message, 3);
478
+ }
479
+ }
480
+
481
+ let generated;
482
+ let resolved;
483
+ try {
484
+ resolved = await resolveCapabilitySource(source);
485
+ if (!resolved.manifest.app_cli) throw new Error(`Plugin '${resolved.id}' declares no app_cli`);
486
+ generated = generateAppCli(resolved.manifest.app_cli, { language, source: { id: resolved.id, version: resolved.version } });
487
+ } catch (err) {
488
+ if (err?.message?.startsWith('process.exit(')) throw err;
489
+ report(err.message, 1);
490
+ } finally {
491
+ resolved?.cleanup?.();
492
+ }
493
+
494
+ let written = null;
495
+ if (out) {
496
+ written = path.resolve(out);
497
+ if (fs.existsSync(written) && !force) report(`${written} exists; pass --force to replace it`, 1);
498
+ try {
499
+ fs.mkdirSync(path.dirname(written), { recursive: true });
500
+ const tmp = `${written}.${process.pid}.tmp`;
501
+ fs.writeFileSync(tmp, generated.content, { mode: generated.mode });
502
+ fs.chmodSync(tmp, generated.mode);
503
+ fs.renameSync(tmp, written);
504
+ } catch (err) {
505
+ report(`Could not write ${written}: ${err.message}`, 1);
506
+ }
507
+ }
508
+
509
+ if (isJson) {
510
+ console.log(JSON.stringify({ language: generated.language, path: generated.path, sha256: generated.sha256, out: written }));
511
+ } else if (written) {
512
+ console.log(`✔ Wrote ${generated.language} app CLI '${generated.spec.name}' to ${written} (suggested path: ${generated.path})`);
513
+ } else {
514
+ process.stdout.write(generated.content);
515
+ }
516
+ process.exit(0);
517
+ }
518
+
519
+ // 6. APP UI
520
+ else if (sub === 'ui') {
521
+ let source = null;
522
+ let target = null;
523
+ let adapter = null;
524
+ let tokens = null;
525
+ let out = null;
526
+ let typescript = false;
527
+ let force = false;
528
+ let isJson = false;
529
+ for (let i = 0; i < rest.length; i++) {
530
+ const arg = rest[i];
531
+ if (arg === '--json') isJson = true;
532
+ else if (arg === '--force') force = true;
533
+ else if (arg === '--typescript') typescript = true;
534
+ else if (arg === '--ui' && rest[i + 1]) target = rest[++i];
535
+ else if (arg === '--adapter' && rest[i + 1]) adapter = rest[++i];
536
+ else if (arg === '--tokens' && rest[i + 1]) tokens = rest[++i];
537
+ else if (arg === '--out' && rest[i + 1]) out = rest[++i];
538
+ else if (!arg.startsWith('--') && !source) source = arg;
539
+ else {
540
+ console.error(`\n❌ Unknown or incomplete option: ${arg}\n`);
541
+ process.exit(1);
542
+ }
543
+ }
544
+ const report = (message, code) => {
545
+ if (isJson) console.error(JSON.stringify({ error: message }));
546
+ else console.error(`\n❌ ${message}\n`);
547
+ process.exit(code);
548
+ };
549
+ if (!source) report('Missing capability source. Usage: total-recall app ui <source> --ui web-components|react [--tokens DESIGN.md] [--out <dir>]', 1);
550
+ if (target && adapter) report('Pass --ui or --adapter, not both', 1);
551
+ if (!target && !adapter) report(`Pass --ui (${UI_TARGETS.join('|')}) or --adapter <id>`, 1);
552
+ if (adapter) {
553
+ try {
554
+ target = uiTargetForAdapter(adapter);
555
+ } catch (err) {
556
+ report(err.message, 3);
557
+ }
558
+ }
559
+ if (!UI_TARGETS.includes(target)) report(`Unknown --ui '${target}' (known: ${UI_TARGETS.join(', ')})`, 1);
560
+ if (typescript && target !== 'react') report('--typescript applies to --ui react only', 1);
561
+ if (tokens && !fs.existsSync(tokens)) report(`Design tokens file not found: ${tokens}`, 1);
562
+
563
+ let generated;
564
+ let resolved;
565
+ let written = null;
566
+ try {
567
+ resolved = await resolveCapabilitySource(source);
568
+ generated = generateUiElements(resolved.manifest, { pluginDir: resolved.stagedDir, target, tokens, typescript });
569
+ if (out) written = writeUiElements(generated, out, { force });
570
+ } catch (err) {
571
+ if (err?.message?.startsWith('process.exit(')) throw err;
572
+ report(err.message, 1);
573
+ } finally {
574
+ resolved?.cleanup?.();
575
+ }
576
+
577
+ if (isJson) {
578
+ console.log(JSON.stringify({
579
+ target: generated.target,
580
+ plugin: generated.pluginId,
581
+ files: generated.files.map(({ path: p, sha256 }) => ({ path: p, sha256 })),
582
+ elements: generated.elements,
583
+ tokens: generated.tokens,
584
+ warnings: generated.warnings,
585
+ out: written
586
+ }));
587
+ } else {
588
+ console.log(written
589
+ ? `✔ Wrote ${generated.files.length} ${generated.target} file(s) for '${generated.pluginId}' to ${written}`
590
+ : `${generated.target} files for '${generated.pluginId}' (pass --out <dir> to write them):`);
591
+ for (const f of generated.files) console.log(` ${f.path} ${f.sha256.slice(0, 12)}`);
592
+ console.log(generated.tokens.source
593
+ ? ` tokens: ${generated.tokens.source === 'app' ? "the app's DESIGN.md" : "the plugin's default DESIGN.md"}`
594
+ : ' tokens: none — pass --tokens <DESIGN.md> to check required tokens and emit tokens.css');
595
+ for (const w of generated.warnings) console.log(` ⚠ ${w}`);
596
+ console.log(`\nUse it:\n${generated.usage.replace(/^/gm, ' ')}`);
597
+ }
598
+ process.exit(0);
599
+ }
600
+
601
+ // 7. APP VERIFY
422
602
  else if (sub === 'verify') {
423
603
  let target = process.cwd();
424
604
  let isJson = false;