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.
- package/bin/total-recall.mjs +20 -2
- package/package.json +2 -2
- package/scaffold/.agent/skills/plugins/SKILL.md +5 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +33 -3
- package/src/cli/api.mjs +133 -0
- package/src/cli/app/index.mjs +181 -1
- package/src/cli/brief.mjs +195 -0
- package/src/cli/command.mjs +269 -5
- package/src/cli/config.mjs +25 -7
- package/src/cli/connect.mjs +1 -1
- package/src/cli/daemon.mjs +3 -3
- package/src/cli/group.mjs +151 -0
- package/src/cli/init.mjs +54 -0
- package/src/cli/plugin/create.mjs +20 -10
- package/src/cli/project-id.mjs +43 -0
- package/src/cli/repo-expert-generate.mjs +37 -3
- package/src/cli/secret.mjs +36 -3
- package/src/cli/skill-config.mjs +211 -0
- package/src/cli/skill.mjs +37 -2
- package/src/core/api-routes.mjs +99 -0
- package/src/core/app-deploy/adapter-contract.mjs +176 -0
- package/src/core/app-deploy/design-tokens.mjs +148 -108
- package/src/core/app-deploy/plan.mjs +2 -2
- package/src/core/app-deploy/resolve.mjs +30 -2
- package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +211 -0
- package/src/core/app-deploy/runtime-cli/python.py.tmpl +178 -0
- package/src/core/app-deploy/runtime-cli.mjs +254 -0
- package/src/core/app-deploy/ui-elements.mjs +473 -0
- package/src/core/app-deploy/verify.mjs +18 -0
- package/src/core/command-surface.mjs +129 -0
- package/src/core/daemon-control.mjs +31 -12
- package/src/core/daemon-loop.mjs +14 -7
- package/src/core/plugin-loader.mjs +12 -12
- package/src/core/project-id.mjs +72 -0
- package/src/core/provider-catalog.mjs +1 -1
- package/src/core/schema.mjs +72 -13
- package/src/core/secrets-env-export.mjs +3 -4
- package/src/core/secrets-store.mjs +30 -29
- package/src/core/skill-config.mjs +376 -0
- package/src/core/skills-registry.mjs +147 -31
- package/src/core/source-adapters.mjs +2 -2
- package/src/core/surface.mjs +109 -16
- package/src/server/routes/help.mjs +1 -1
- package/src/server/routes/system.mjs +1 -1
- package/plugins/code-quality/README.md +0 -25
- package/plugins/code-quality/cli.mjs +0 -25
- package/plugins/code-quality/plugin.json +0 -17
package/bin/total-recall.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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#
|
|
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**:
|
|
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
|
|
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**:
|
|
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 |
|
package/src/cli/api.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/cli/app/index.mjs
CHANGED
|
@@ -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
|
|
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;
|