total-recall-brain 3.40.7 → 3.41.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 (172) hide show
  1. package/bin/total-recall-memory.mjs +0 -0
  2. package/bin/total-recall.mjs +9 -40
  3. package/docs/reference/cli-reference.md +65 -1
  4. package/frontend/dist/assets/{index-AGUes-VG.css → index-BG9NkVUy.css} +1 -1
  5. package/frontend/dist/assets/{index-Bt5iVPZj.js → index-BtuiZqwP.js} +20 -7
  6. package/frontend/dist/index.html +2 -2
  7. package/frontend/src/components/plugins/PluginPanel.tsx +31 -5
  8. package/frontend/src/components/plugins/card/CardEngine.tsx +15 -23
  9. package/frontend/src/components/plugins/card/CardScenes.tsx +52 -23
  10. package/frontend/src/components/plugins/card/card.css +2 -5
  11. package/frontend/src/components/plugins/card/contract.ts +13 -19
  12. package/frontend/src/components/plugins/card/format.ts +8 -1
  13. package/frontend/src/components/plugins/card/widgets.tsx +4 -35
  14. package/frontend/src/components/plugins/plugin-frame.ts +30 -3
  15. package/frontend/src/pages/PluginsPage.tsx +3 -2
  16. package/package.json +1 -1
  17. package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
  18. package/scaffold/.agent/skills/plugin/SKILL.md +110 -0
  19. package/scaffold/.agent/skills/plugin/evals/README.md +53 -0
  20. package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +42 -0
  21. package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +39 -0
  22. package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +32 -0
  23. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +46 -0
  24. package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +55 -0
  25. package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +92 -0
  26. package/scaffold/.agent/skills/plugin/evals/run.mjs +118 -0
  27. package/scaffold/.agent/skills/plugin/prompts/01-function.md +29 -0
  28. package/scaffold/.agent/skills/plugin/prompts/02-surfaces.md +43 -0
  29. package/scaffold/.agent/skills/plugin/prompts/03-scaffold.md +31 -0
  30. package/scaffold/.agent/skills/plugin/prompts/04-cli.md +49 -0
  31. package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +41 -0
  32. package/scaffold/.agent/skills/plugin/prompts/06-card.md +46 -0
  33. package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +52 -0
  34. package/scaffold/.agent/skills/plugin/prompts/08-verify.md +43 -0
  35. package/scaffold/.agent/skills/plugin/references/anti-patterns.md +123 -0
  36. package/scaffold/.agent/skills/plugin/references/card.md +90 -0
  37. package/scaffold/.agent/skills/plugin/references/scaffold.md +120 -0
  38. package/scaffold/.agent/skills/plugin/references/surfaces.md +206 -0
  39. package/scaffold/.agent/skills/plugin/scripts/record.mjs +360 -0
  40. package/scaffold/.agent/skills/plugin/subagents/card-designer.md +60 -0
  41. package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +55 -0
  42. package/scaffold/.agent/skills/plugin/subagents/scaffold-extractor.md +71 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +71 -1441
  44. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -5
  45. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +3 -44
  46. package/scaffold/.agent/skills/total-recall/plugins/auth/scripts/agent-login-pty.py +185 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
  48. package/scaffold/.agent/skills/total-recall/plugins/auth/src/host-secrets-store.mjs +15 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +6 -4
  50. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -24
  52. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  53. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +0 -21
  54. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  55. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +0 -32
  56. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  57. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +11 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +5 -2
  59. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
  60. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -28
  61. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +95 -13
  62. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +553 -340
  63. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +0 -36
  64. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +383 -400
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4 -4
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +0 -1
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +0 -28
  68. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
  69. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +0 -32
  70. package/scaffold/.agent/skills/total-recall/references/auth.md +3 -1
  71. package/scaffold/.agent/skills/total-recall/references/memory.md +4 -0
  72. package/scaffold/.agent/skills/total-recall/references/openwiki.md +2 -0
  73. package/scaffold/.agent/skills/total-recall/references/operations.md +12 -0
  74. package/src/cli/brain.mjs +72 -2
  75. package/src/cli/connect.mjs +10 -39
  76. package/src/cli/context-stats.mjs +104 -0
  77. package/src/cli/context.mjs +8 -0
  78. package/src/cli/daemon.mjs +10 -1
  79. package/src/cli/export.mjs +3 -2
  80. package/src/cli/ingest-okf.mjs +12 -15
  81. package/src/cli/init.mjs +0 -85
  82. package/src/cli/lint.mjs +3 -2
  83. package/src/cli/plugin/copies.mjs +68 -0
  84. package/src/cli/plugin/create.mjs +22 -24
  85. package/src/cli/plugin/index.mjs +49 -3
  86. package/src/cli/plugin/search.mjs +19 -2
  87. package/src/cli/rebuild.mjs +2 -0
  88. package/src/cli/remember.mjs +23 -0
  89. package/src/cli/status.mjs +10 -3
  90. package/src/cli/update.mjs +3 -2
  91. package/src/core/action-predictor.mjs +161 -0
  92. package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +20 -3
  93. package/src/core/app-deploy/source.mjs +16 -9
  94. package/src/core/automatic-brain-updates.mjs +6 -3
  95. package/src/core/brain-registry.mjs +19 -2
  96. package/src/core/brain-state.mjs +23 -0
  97. package/src/core/cli-commands.mjs +1 -0
  98. package/src/core/cli-maintenance.mjs +1 -1
  99. package/src/core/cli-semantic-index.mjs +1 -1
  100. package/src/core/command-surface.mjs +3 -3
  101. package/src/core/composed-command-resolution.mjs +21 -0
  102. package/src/core/config.mjs +84 -0
  103. package/src/core/confined-memory.mjs +93 -0
  104. package/src/core/context-telemetry.mjs +214 -0
  105. package/src/core/daemon-control.mjs +14 -43
  106. package/src/core/fast-recall.mjs +1 -1
  107. package/src/core/memory-brain.mjs +2 -0
  108. package/src/core/plugin-activation.mjs +38 -6
  109. package/src/core/plugin-bundle.mjs +54 -0
  110. package/src/core/plugin-catalog.mjs +1 -1
  111. package/src/core/plugin-context.mjs +1 -1
  112. package/src/core/plugin-contracts/card.mjs +69 -37
  113. package/src/core/plugin-loader.mjs +5 -6
  114. package/src/core/plugin-runner.mjs +13 -6
  115. package/src/core/plugin-runtime.mjs +5 -4
  116. package/src/core/plugin-setup.mjs +5 -3
  117. package/src/core/plugin-state-store.mjs +12 -1
  118. package/src/core/plugin-state.mjs +46 -5
  119. package/src/core/plugin-store.mjs +78 -3
  120. package/src/core/plugin-tasks.mjs +3 -1
  121. package/src/core/plugin-updates.mjs +117 -0
  122. package/src/core/plugin-vendor.mjs +10 -4
  123. package/src/core/project-brain.mjs +14 -41
  124. package/src/core/project-id.mjs +4 -1
  125. package/src/core/required-plugins.mjs +151 -13
  126. package/src/core/skills-registry.mjs +27 -3
  127. package/src/core/ssss-kernel-bridge.mjs +3 -1
  128. package/src/core/startup-health.mjs +66 -16
  129. package/src/core/surface.mjs +53 -46
  130. package/src/core/testing/plugin-fixture.mjs +69 -0
  131. package/src/core/vault-files.mjs +13 -0
  132. package/src/core/vault.mjs +4 -30
  133. package/src/server/index.mjs +13 -1
  134. package/src/server/invoke-decision-child.mjs +49 -0
  135. package/src/server/invoke-execution.mjs +87 -0
  136. package/src/server/invoke-policy.mjs +64 -0
  137. package/src/server/invoke-read-child.mjs +26 -0
  138. package/src/server/invoke-targets.mjs +49 -0
  139. package/src/server/invoke-write-child.mjs +19 -0
  140. package/src/server/routes/invoke.mjs +314 -0
  141. package/src/server/routes/plugins.mjs +1 -1
  142. package/scaffold/.agent/skills/plugins/SKILL.md +0 -57
  143. package/scaffold/.agent/skills/plugins/references/card-design.md +0 -55
  144. package/scaffold/.agent/skills/plugins/references/distribution.md +0 -23
  145. package/scaffold/.agent/skills/plugins/references/manifest-contract.md +0 -90
  146. package/scaffold/.agent/skills/plugins/references/optimized/index.md +0 -9
  147. package/scaffold/.agent/skills/plugins/references/optimized/manifest.json +0 -35
  148. package/scaffold/.agent/skills/plugins/references/optimized/original.md +0 -58
  149. package/scaffold/.agent/skills/plugins/references/optimized/section-01.md +0 -5
  150. package/scaffold/.agent/skills/plugins/references/optimized/section-02.md +0 -25
  151. package/scaffold/.agent/skills/plugins/references/optimized/section-03.md +0 -12
  152. package/scaffold/.agent/skills/plugins/references/optimized/section-04.md +0 -7
  153. package/scaffold/.agent/skills/plugins/references/optimized/section-05.md +0 -5
  154. package/scaffold/.agent/skills/total-recall/openwiki/README.md +0 -10
  155. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +0 -5
  156. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +0 -10
  157. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +0 -11
  158. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +0 -7
  159. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +0 -7
  160. package/scaffold/openwiki/README.md +0 -10
  161. package/scaffold/openwiki/architecture.md +0 -5
  162. package/scaffold/openwiki/ide.md +0 -10
  163. package/scaffold/openwiki/memory.md +0 -11
  164. package/scaffold/openwiki/secrets.md +0 -7
  165. package/scaffold/openwiki/skills.md +0 -7
  166. package/src/core/okf-adapter.mjs +0 -559
  167. package/templates/openwiki/README.md +0 -10
  168. package/templates/openwiki/architecture.md +0 -5
  169. package/templates/openwiki/ide.md +0 -10
  170. package/templates/openwiki/memory.md +0 -11
  171. package/templates/openwiki/secrets.md +0 -7
  172. package/templates/openwiki/skills.md +0 -7
@@ -0,0 +1,314 @@
1
+ /** Optional developer API. Explicit grants precede every owner execution. */
2
+ import { Router } from 'express';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { requireAuth } from '../auth.mjs';
6
+ import { loadKeys, keyHasScope } from '../keys.mjs';
7
+ import { brainDir, globalBrainDir } from '../../core/config.mjs';
8
+ import { logger } from '../../core/logger.mjs';
9
+ import crypto from 'node:crypto';
10
+ import { appendVfsEvent } from '../../core/ssss-operation-service.mjs';
11
+ import { CLI_COMMANDS } from '../../core/cli-commands.mjs';
12
+ import { buildPluginCatalog } from '../../core/plugin-catalog.mjs';
13
+ import { listSurfaceCommands } from '../../core/command-surface.mjs';
14
+ import { composedCommandDirectories } from '../../core/composed-command-resolution.mjs';
15
+ import { invokeAuthority, authorizeInvoke, operationAllowed } from '../invoke-policy.mjs';
16
+ import { restrictedBrainTargets } from '../invoke-targets.mjs';
17
+ import { INVOKE_LIMITS, InvokeError, executeInvokeChild, requireSuccessfulRun, invokeEnvironment, absoluteTarget } from '../invoke-execution.mjs';
18
+
19
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
20
+ // Share across router instances in this server; do not race projection refreshes.
21
+ const mutatingBrains = new Set();
22
+ export const OPERATIONS = Object.freeze(Object.fromEntries(Object.entries({
23
+ capabilities: { scope: null, family: 'introspection' },
24
+ 'cli.run': { scope: 'sandbox:run', family: 'cli' },
25
+ 'ssss.read': { scope: 'ssss:read', family: 'ssss' },
26
+ 'ssss.write': { scope: 'ssss:write', family: 'ssss' },
27
+ 'composable.list': { scope: 'config:read', family: 'composable' },
28
+ 'composable.run': { scope: 'sandbox:run', family: 'composable' },
29
+ 'decision.run': { scope: 'memory:read', family: 'decision' },
30
+ 'decision.predict': { scope: 'memory:read', family: 'decision' },
31
+ }).map(([name, def]) => [name, Object.freeze(def)])));
32
+
33
+ const fail = (res, status, operation, code, message, extra = {}) => res.status(status).json({
34
+ ok: false, operation, result: null, error: { code, message }, ...extra,
35
+ });
36
+
37
+ /** Only scopes that can execute are advertised. */
38
+ function capabilitiesFor(authority, limits = INVOKE_LIMITS, targets = ['server']) {
39
+ const families = {};
40
+ for (const [name, def] of Object.entries(OPERATIONS)) {
41
+ if (!operationAllowed(authority, name, def.scope, targets)) continue;
42
+ (families[def.family] ||= []).push({ operation: name, scope: def.scope });
43
+ }
44
+ return { operations: families, families: Object.keys(families), mode: authority.mode,
45
+ targets: authority.mode === 'full' ? 'host' : targets,
46
+ credentials: authority.mode === 'full' ? 'host-auth-owner' : [],
47
+ provider_spending: authority.mode === 'full' ? 'trusted-host-authority' : 0,
48
+ limits };
49
+ }
50
+
51
+ /** Injectable dependencies allow real HTTP tests with disposable auth-owned keys. */
52
+ export function createInvokeRouter({ auth = requireAuth, store = { loadKeys, keyHasScope },
53
+ execute = executeInvokeChild, audit,
54
+ cwd = process.cwd(), serverBrain = brainDir, registryBrain = globalBrainDir, limits = INVOKE_LIMITS,
55
+ commandScopes = composedCommandDirectories } = {}) {
56
+ const router = Router();
57
+ const resolveTargets = authority => authority.mode === 'restricted' ?
58
+ restrictedBrainTargets(authority, { serverBrain, registryBrain }) : new Map();
59
+ const recordAudit = audit || (event => appendVfsEvent(`invoke/${crypto.randomUUID()}`, event, {
60
+ vaultRoot: path.join(serverBrain, 'memory-vault'), workspaceId: 'invoke', actorRole: 'system', intent: 'Record developer invoke outcome',
61
+ }));
62
+ const active = new Map();
63
+ let total = 0;
64
+ router.get('/v1/invoke/capabilities', auth, (req, res) => {
65
+ try {
66
+ const authority = invokeAuthority(req, store);
67
+ res.json({ ok: true, operation: 'capabilities', result: capabilitiesFor(authority, limits, [...resolveTargets(authority).keys()]), error: null });
68
+ } catch (err) { fail(res, err.status || 503, 'capabilities', err.code || 'authority-unavailable', err.code ? err.message : 'Authority validation unavailable.'); }
69
+ });
70
+ router.post('/v1/invoke', auth, async (req, res) => {
71
+ const started = Date.now();
72
+ const { operation, params = {} } = req.body || {};
73
+ if (typeof operation !== 'string' || !operation) return fail(res, 400, null, 'missing-operation', 'Body must include { operation, params }.');
74
+ if (!Object.hasOwn(OPERATIONS, operation)) return fail(res, 404, operation, 'unknown-operation', 'Unknown operation.');
75
+ const def = OPERATIONS[operation];
76
+ let authority, timer, controller, disconnected, counted = false, outcome = 'denied', target = null, mutationBrain, mutationResult;
77
+ try {
78
+ if (!params || typeof params !== 'object' || Array.isArray(params)) throw new InvokeError(400, 'invalid-params', 'params must be an object.');
79
+ if (Buffer.byteLength(JSON.stringify(params)) > 16384) throw new InvokeError(413, 'input-limit', 'Invoke parameters exceed 16 KiB.');
80
+ authority = invokeAuthority(req, store);
81
+ const targets = resolveTargets(authority), targetNames = [...targets.keys()];
82
+ authorizeInvoke(authority, operation, def.scope, params, targetNames);
83
+ target = authority.mode === 'restricted' ? (operation === 'capabilities' ? null : params.target ?? 'server') : absoluteTarget(params.target, cwd);
84
+ const selectedBrain = authority.mode === 'restricted' && target ? targets.get(target) : serverBrain;
85
+ if (authority.mode === 'restricted' && operation === 'ssss.write') {
86
+ const brain = selectedBrain;
87
+ if (mutatingBrains.has(brain)) throw new InvokeError(429, 'brain-write-busy', 'A confined write is already active for this brain.');
88
+ mutatingBrains.add(brain); mutationBrain = brain;
89
+ }
90
+ if (total >= limits.concurrency || (active.get(authority.id) || 0) >= limits.perKey) throw new InvokeError(429, 'concurrency-limit', 'Execution concurrency limit reached.');
91
+ total++; active.set(authority.id, (active.get(authority.id) || 0) + 1); counted = true;
92
+ try { await recordAudit({ caller: authority.id, mode: authority.mode, operation, target, outcome: 'authorized' }); }
93
+ catch { throw new InvokeError(503, 'audit-unavailable', 'Execution audit is unavailable; no work was started.'); }
94
+ controller = new AbortController();
95
+ disconnected = () => { if (!res.writableEnded) controller.abort(); };
96
+ req.once('aborted', disconnected); res.once('close', disconnected);
97
+ const revalidate = () => {
98
+ try {
99
+ const current = invokeAuthority(req, store);
100
+ if (current.mode !== authority.mode) throw new Error('mode changed');
101
+ const currentTargets = resolveTargets(current);
102
+ authorizeInvoke(current, operation, def.scope, params, [...currentTargets.keys()]);
103
+ if (current.mode === 'restricted' && target && currentTargets.get(target) !== selectedBrain) throw new Error('target changed');
104
+ } catch { controller.abort(); }
105
+ };
106
+ revalidate();
107
+ timer = setInterval(revalidate, 250); timer.unref?.();
108
+ const context = { authority, target, serverBrain, selectedBrain, signal: controller.signal, limits, commandScopes,
109
+ run: options => {
110
+ revalidate();
111
+ return execute({ ...options, signal: controller.signal, limits });
112
+ } };
113
+ let result;
114
+ if (operation === 'capabilities') result = capabilitiesFor(authority, limits, targetNames);
115
+ else result = await HANDLERS[operation](params, context);
116
+ if (operation === 'ssss.write' && authority.mode === 'restricted') mutationResult = result;
117
+ revalidate();
118
+ if (controller.signal.aborted) throw new InvokeError(403, 'authority-ended', 'Caller authority ended during execution.');
119
+ outcome = 'success';
120
+ await recordAudit({ caller: authority.id, mode: authority.mode, operation, target, outcome, latency_ms: Date.now() - started });
121
+ return res.json({ ok: true, operation, result, error: null, scope: def.scope, latency_ms: Date.now() - started });
122
+ } catch (err) {
123
+ outcome = err instanceof InvokeError ? err.code : 'operation-failed';
124
+ // Error strings from owner code can contain secrets. Only curated policy
125
+ // messages and explicitly requested full-mode command output are returned.
126
+ try { await recordAudit({ caller: authority?.id || req.auth?.id || 'unresolved', operation, target, outcome, latency_ms: Date.now() - started }); }
127
+ catch {
128
+ logger.error('invoke', 'Execution outcome audit unavailable', { operation, caller: authority?.id || 'unresolved' });
129
+ return fail(res, 503, operation, 'audit-unavailable', 'Execution audit could not be recorded.', mutationResult ? { result: mutationResult } : {});
130
+ }
131
+ return fail(res, err.status || 400, operation, outcome, err instanceof InvokeError ? err.message : 'Operation failed.',
132
+ { scope: def.scope, latency_ms: Date.now() - started, ...(err.result || mutationResult ? { result: err.result || mutationResult } : {}) });
133
+ } finally {
134
+ clearInterval(timer);
135
+ if (mutationBrain) mutatingBrains.delete(mutationBrain);
136
+ if (disconnected) { req.removeListener('aborted', disconnected); res.removeListener('close', disconnected); }
137
+ if (counted) { total--; const n = active.get(authority.id) - 1; if (n) active.set(authority.id, n); else active.delete(authority.id); }
138
+ }
139
+ });
140
+ return router;
141
+ }
142
+
143
+ export const invokeRouter = createInvokeRouter();
144
+
145
+ /** The categories `total-recall remember` documents. Singular, and closed. */
146
+ const MEMORY_CATEGORIES = new Set(['invariant', 'preference', 'correction', 'fact', 'concept', 'pattern', 'anti-pattern', 'decision', 'lore']);
147
+
148
+ /** Static projection of the shared CLI execution scopes. */
149
+ function composedCommands(context) {
150
+ return listSurfaceCommands(context.commandScopes(context.target));
151
+ }
152
+
153
+ async function runCliVerb({ verb, args = [], timeoutMs }, context) {
154
+ if (typeof verb !== 'string' || !verb.trim()) throw new InvokeError(400, 'invalid-verb', 'cli.run needs a registered verb.');
155
+ if (!Array.isArray(args) || args.some(a => typeof a !== 'string' || a.includes('\0'))) throw new InvokeError(400, 'invalid-args', 'args must be an array of strings.');
156
+ const parts = verb.trim().split(/\s+/);
157
+ const [command] = parts;
158
+ const catalog = buildPluginCatalog(context.target);
159
+ const plugin = catalog.plugins.find(p => p.namespace === command && p.callable && p.activation.enabled);
160
+ if (!Object.hasOwn(CLI_COMMANDS, command) && !plugin && !composedCommands(context).some(c => c.name === command)) {
161
+ throw new InvokeError(404, 'unregistered-command', 'Command is not registered in the authorized target.');
162
+ }
163
+ const env = invokeEnvironment(true);
164
+ env.TR_INVOKE_CONTEXT = JSON.stringify({ caller: context.authority.id, mode: 'full', target: context.target });
165
+ return requireSuccessfulRun(await context.run({ entry: path.join(root, 'bin/total-recall.mjs'),
166
+ args: [...parts, ...args], cwd: context.target, env, timeoutMs }));
167
+ }
168
+
169
+ const HANDLERS = {
170
+ async 'cli.run'(params, context) { return await runCliVerb(params, context); },
171
+
172
+ async 'ssss.read'({ query, category, limit = 5, timeoutMs }, context) {
173
+ if (typeof query !== 'string' || !query.trim() || query.startsWith('-')) throw new InvokeError(400, 'invalid-query', 'query must be nonempty text, not a CLI option.');
174
+ if (category !== undefined && (typeof category !== 'string' || !/^[a-z][a-z-]{0,63}$/.test(category))) throw new InvokeError(400, 'invalid-category', 'Invalid category.');
175
+ if (context.authority.mode === 'restricted') {
176
+ const brain = context.selectedBrain;
177
+ const run = requireSuccessfulRun(await context.run({ entry: path.join(root, 'src/server/invoke-read-child.mjs'),
178
+ cwd: root, env: invokeEnvironment(false), timeoutMs, nodeArgs: ['--max-old-space-size=128', '--permission',
179
+ `--allow-fs-read=${path.join(root, 'src')}`, `--allow-fs-read=${path.join(root, 'node_modules')}`,
180
+ `--allow-fs-read=${path.join(root, 'package.json')}`, `--allow-fs-read=${brain}`],
181
+ input: JSON.stringify({ brain, query, category, limit: Math.max(1, Math.min(20, Number(limit) || 5)) }) }));
182
+ try { return JSON.parse(run.stdout); } catch { throw new InvokeError(502, 'invalid-owner-output', 'Retrieval returned invalid output.'); }
183
+ }
184
+ // Retrieval belongs to the owning CLI. It searches metadata and indexed body
185
+ // text with ranking and session chunks; a substring scan here would return
186
+ // different, worse results than `recall` for the same question.
187
+ const k = Math.max(1, Math.min(20, Number(limit) || 5));
188
+ const args = ['--local', '--format', 'json', '-k', String(k), '--no-sessions'];
189
+ if (category) args.push('--category', String(category));
190
+ // The query is ONE argv element, passed through `args` so it is never
191
+ // re-split on whitespace.
192
+ const run = await runCliVerb({ verb: 'recall', args: [query, ...args], timeoutMs }, context);
193
+ if (run.exit_code !== 0) throw new Error(run.stderr?.trim() || 'recall failed');
194
+ // The CLI pretty-prints ONE JSON array across many lines, so it cannot be
195
+ // read line-by-line: a line-based reader finds only `[` and fails to parse,
196
+ // which surfaces as a successful call returning zero results. Slice from the
197
+ // first `[` to the last `]` to tolerate leading log lines.
198
+ const stdout = String(run.stdout || '');
199
+ const start = stdout.indexOf('[');
200
+ const end = stdout.lastIndexOf(']');
201
+ let parsed = null;
202
+ if (start !== -1 && end > start) {
203
+ try { parsed = JSON.parse(stdout.slice(start, end + 1)); } catch { /* unparseable */ }
204
+ }
205
+ if (!Array.isArray(parsed)) throw new InvokeError(502, 'invalid-owner-output', 'Recall did not return a valid result array.');
206
+ const nodes = parsed.map(n => ({
207
+ slug: n.slug, category: n.category, title: n.title,
208
+ status: n.status, score: n.score, snippet: String(n.body || n.content || '').slice(0, 240),
209
+ }));
210
+ return { count: nodes.length, nodes };
211
+ },
212
+
213
+ async 'ssss.write'(params, context) {
214
+ const { category, title, content, tags, scope, timeoutMs } = params;
215
+ if (context.authority.mode === 'restricted') {
216
+ const brain = context.selectedBrain;
217
+ const run = await context.run({ entry: path.join(root, 'src/server/invoke-write-child.mjs'),
218
+ cwd: root, env: invokeEnvironment(false), timeoutMs, nodeArgs: ['--max-old-space-size=128', '--permission',
219
+ `--allow-fs-read=${path.join(root, 'src')}`, `--allow-fs-read=${path.join(root, 'node_modules')}`,
220
+ `--allow-fs-read=${path.join(root, 'package.json')}`, `--allow-fs-read=${brain}`,
221
+ `--allow-fs-write=${path.join(brain, 'memory-vault')}`,
222
+ `--allow-fs-write=${path.join(brain, 'memory-vault', '*')}`,
223
+ `--allow-fs-write=${path.join(brain, 'memory-derived')}`,
224
+ `--allow-fs-write=${path.join(brain, 'memory-derived', '*')}`],
225
+ input: JSON.stringify({ brain, params: { ...params, target: 'server' }, caller: context.authority.id }) });
226
+ let result;
227
+ try { result = JSON.parse(run.stdout); } catch { /* interruption may precede output */ }
228
+ if (run.exit_code !== 0 || run.termination_reason || run.truncated) {
229
+ throw new InvokeError(run.termination_reason === 'timeout' ? 504 : run.termination_reason === 'authority-ended' ? 403 : 422,
230
+ run.termination_reason || 'mutation-failed', 'Fact creation did not complete; check committed/indexed status before retry.',
231
+ result || { committed: 'unknown', indexed: false });
232
+ }
233
+ if (!result?.committed || !result?.indexed) throw new InvokeError(502, 'invalid-owner-output', 'Mutation returned invalid output.', { committed: 'unknown', indexed: false });
234
+ return result;
235
+ }
236
+ if (!content || typeof content !== 'string') throw new Error('ssss.write needs content.');
237
+ // Canonical memory is written by its owning CLI, never reimplemented here.
238
+ // `remember` runs the validated write path and recompiles the derived
239
+ // surfaces; a hand-rolled vault write would bypass both and could leave the
240
+ // indexes stale while reporting success.
241
+ const cat = typeof category === 'string' && category.trim() ? category.trim() : 'fact';
242
+ // These are the categories `remember` documents. They are singular, and a
243
+ // node TYPE (`facts`) is not one of them — passing it produced a confusing
244
+ // failure from deep inside the write path.
245
+ if (!MEMORY_CATEGORIES.has(cat)) {
246
+ throw new Error(`Unknown memory category '${cat}'. Valid: ${[...MEMORY_CATEGORIES].join(', ')}.`);
247
+ }
248
+ const text = title ? `${title}\n\n${content}` : content;
249
+ const args = ['--tags', Array.isArray(tags) && tags.length ? tags.join(',') : 'invoke'];
250
+ if (scope === 'global') args.push('--global');
251
+ else args.push('--project');
252
+ // Same rule as ssss.read: the memory text is one argv element, never split.
253
+ const run = await runCliVerb({ verb: `remember ${cat}`, args: [text, ...args], timeoutMs }, context);
254
+ if (run.exit_code !== 0) throw new Error(run.stderr?.trim() || run.stdout?.trim() || 'remember failed');
255
+ const created = /vault:\s*(\S+)/.exec(run.stdout || '')?.[1] || null;
256
+ return { category: cat, path: created, exit_code: run.exit_code };
257
+ },
258
+
259
+ async 'composable.list'(_params, context) {
260
+ const catalog = buildPluginCatalog(context.target);
261
+ const commands = composedCommands(context).filter(c => !Object.hasOwn(CLI_COMMANDS, c.name) &&
262
+ !catalog.plugins.some(p => p.callable && p.activation.enabled && p.namespace === c.name));
263
+ return { count: commands.length, commands };
264
+ },
265
+
266
+ async 'composable.run'({ name, args = [], timeoutMs }, context) {
267
+ if (typeof name !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(name)) throw new InvokeError(400, 'invalid-name', 'composable.run needs a kebab-case command name.');
268
+ if (Object.hasOwn(CLI_COMMANDS, name) || !composedCommands(context).some(c => c.name === name)) throw new InvokeError(404, 'unregistered-command', 'Composed name is not executable in the authorized target.');
269
+ const catalog = buildPluginCatalog(context.target);
270
+ if (catalog.plugins.some(p => p.callable && p.activation.enabled && p.namespace === name)) throw new InvokeError(409, 'shadowed-command', 'A plugin shadows this composed name.');
271
+ return await runCliVerb({ verb: name, args, timeoutMs }, context);
272
+ },
273
+
274
+ async 'decision.run'(params, context) {
275
+ if (!params.questions || typeof params.questions !== 'object' || Array.isArray(params.questions)) {
276
+ throw new InvokeError(400, 'invalid-questions', 'decision.run needs a question object.');
277
+ }
278
+ return runDecision('decision.run', params, context);
279
+ },
280
+
281
+ async 'decision.predict'(params, context) {
282
+ return runDecision('decision.predict', params, context);
283
+ },
284
+ };
285
+
286
+ async function runDecision(operation, params, context) {
287
+ const requested = params.timeoutMs === undefined ? context.limits.timeoutMs : Number(params.timeoutMs);
288
+ if (!Number.isFinite(requested) || requested <= 0) throw new InvokeError(400, 'invalid-timeout', 'timeoutMs must be positive and finite.');
289
+ const timeoutMs = Math.min(requested, context.limits.timeoutMs);
290
+ const env = invokeEnvironment(true);
291
+ env.TR_INVOKE_CONTEXT = JSON.stringify({ caller: context.authority.id, mode: 'full', target: context.target });
292
+ const run = await context.run({ entry: path.join(root, 'src/server/invoke-decision-child.mjs'),
293
+ cwd: context.target, env, timeoutMs,
294
+ input: JSON.stringify({ operation, params, brain: context.serverBrain, timeoutMs }) });
295
+ // Provider/auth diagnostics may contain credentials. Unlike requested cli.run
296
+ // output, these pipes are never exposed, even on timeout or malformed output.
297
+ if (run.termination_reason || run.truncated) {
298
+ const code = run.termination_reason || 'output-limit';
299
+ throw new InvokeError(code === 'timeout' ? 504 : code === 'authority-ended' ? 403 : 422,
300
+ code, 'Decision execution did not complete; remote work may already have been accepted.');
301
+ }
302
+ let envelope;
303
+ try { envelope = JSON.parse(run.stdout); } catch { /* denied below */ }
304
+ if (run.exit_code !== 0) throw new InvokeError(502, 'decision-failed', 'Decision owner failed.');
305
+ if (envelope?.ok === false) {
306
+ const credential = envelope.error === 'credential-unavailable';
307
+ throw new InvokeError(credential ? 503 : 502, credential ? 'credential-unavailable' : 'provider-failed',
308
+ credential ? 'Decision credential is unavailable.' : 'Decision provider failed.');
309
+ }
310
+ if (envelope?.ok !== true || !envelope.result || typeof envelope.result !== 'object' || Array.isArray(envelope.result)) {
311
+ throw new InvokeError(502, 'invalid-owner-output', 'Decision owner returned invalid output.');
312
+ }
313
+ return envelope.result;
314
+ }
@@ -323,7 +323,7 @@ router.post("/api/plugins/:id/run", requireAuth, requireScope("config:write"), a
323
323
 
324
324
  if (execution) {
325
325
  if (stdin !== undefined) return badRequest(res, 'Credential operations require direct local execution');
326
- if (!pluginActivation(plugin).enabled && !['on', 'off', 'status', 'health', 'jobs', '--help', 'help'].includes(subcommand)) return badRequest(res, `Plugin ${plugin.id} is inactive`);
326
+ if (!pluginActivation(plugin, { projectRoot: projectRoot() }).enabled && !['on', 'off', 'status', 'health', 'jobs', '--help', 'help'].includes(subcommand)) return badRequest(res, `Plugin ${plugin.id} is inactive`);
327
327
  const { loadPluginModule } = await import('../../core/plugin-runtime.mjs');
328
328
  const owner = await loadPluginModule('cli-agents', 'command-executor', { activeRequired: true });
329
329
  const result = await owner.executeCommand({ plugin, subcommand, args, execution, projectRoot: projectRoot() });
@@ -1,57 +0,0 @@
1
- ---
2
- name: plugins
3
- description: Create, scaffold, design, test and distribute Total Recall plugins that load correctly and render a functional, uniquely designed card. Use when an agent builds, changes or reviews a tr-plugin-*.
4
- repo_scoped: false
5
- ---
6
-
7
- # Total Recall plugins
8
-
9
- Run `total-recall plugin --help` first. A plugin is a standalone repository (`tr-plugin-<id>`) with a `plugin.json`, a CLI handler, tests, and optionally a live panel. Core Total Recall holds portable memory and generic loading; every capability lives in a plugin.
10
-
11
- ## Decide how the plugin will be used
12
-
13
- 1. **Brick in a project (default for apps).** `total-recall plugin vendor <id | git-url | path> [dir]` copies the source into `bricks/<id>` and records it in `tr-bricks.json`. The project owns the copy and ships with no runtime dependency on the plugin or Total Recall. Plugins exist so nobody starts from zero.
14
- 2. **Direct use.** `total-recall plugin install <source>` then `total-recall <id> ...` for your own machines and workflow.
15
- 3. **New project.** Prefer adding bricks to starting from an empty folder. Run `total-recall plugin bricks` to see what a project already has.
16
-
17
- Declare how yours is distributed: `distribution: { "tier": "core|official|personal|community", "license": "open|closed" }`. Closed means an operated service behind a thin client; see [distribution](references/distribution.md). Never rely on a local license check for protection.
18
-
19
- ## Build order
20
-
21
- 1. `total-recall plugin create <id> --name "<neutral name>" --description "<one concrete sentence>" --with-cli`.
22
- 2. Write the CLI first. Every subcommand prints JSON with `--json` and returns a non-zero exit on failure. Required subcommands where relevant: `status --json`, `config get|set --json`, `secrets status --json`. Details: [manifest contract](references/manifest-contract.md).
23
- 3. Declare every command in `plugin.json` with typed arguments and an effect (`read`, `write`, `destructive`, `send`). The card builds its forms from these, so an undeclared command is invisible to users and agents.
24
- 4. Declare `configuration.fields` for every setting and `secrets[]` for every credential. Credentials go through `total-recall auth secret`, never ambient environment variables, argv, files or logs.
25
- 5. Design the card: pick a template and widgets that fit the function, set brand tokens, write `ui/DESIGN.md`. Details: [card design](references/card-design.md).
26
- 6. Test (see below), then run the validator: `total-recall plugin info <id>` must report no errors.
27
-
28
- ## Rules that make a plugin compatible
29
-
30
- - **Real data only.** A value appears on the card only if a command or the host returned it. Never hard-code metrics such as "ONLINE" or sample numbers. If a source fails, show the failure and the time of the last attempt.
31
- - **Functional first.** The card must show what the plugin can do (all declared commands), all of its settings, all of its credentials, and its real status. Decoration comes after, never instead.
32
- - **Unique design per plugin.** Layout and brand follow the plugin's function. Two plugins with different jobs must not look alike. No decorative animation, no emoji as icons, no skins that imitate hardware.
33
- - **Neutral, descriptive name.** No invented brand. The name says what the plugin does.
34
- - **Tokens only.** No hard-coded colors in plugin UI code; use the brand tokens and the design tokens in `ui/DESIGN.md`.
35
- - **Accessible and touch ready.** Controls are at least 44 px, keyboard reachable, with visible focus, contrast-checked state colors, and reduced motion respected.
36
- - **Secrets stay secret.** Set from the card over stdin for declared secret arguments only. Show present or missing, never the value.
37
- - **State through the vault contracts.** No unmanaged raw file writes for records; use SSSS operations.
38
- - **Fail closed.** Missing credentials, failed providers and unavailable peers produce explicit errors, never a success badge.
39
- - **Copy.** No filler words, and no sentence ends with a preposition.
40
-
41
- ## Test before you share
42
-
43
- - Unit tests for each subcommand, including failure paths and `--json` shape.
44
- - A manifest test that loads `plugin.json` with the validator and checks that every `card_layout` source command and every `status.command` is a declared subcommand.
45
- - A secrets test proving no secret value appears in argv, stdout, logs or the card state.
46
- - Run suites in the background on the configured test node and read the real exit status. A passing health probe is not a test result.
47
-
48
- ## Distribution
49
-
50
- - Open plugins are vendorable and can be shared with `plugin share`. Closed service-backed plugins list price and usage in the manifest `monetization` block. See [distribution](references/distribution.md).
51
- - Third parties publish with the same manifest and conformance tests. The store lists signed, immutable versions and never runs plugin code.
52
-
53
- ## References
54
-
55
- - [Manifest contract](references/manifest-contract.md): every field, with a complete example.
56
- - [Card design](references/card-design.md): templates, widgets, brand tokens, the design process, anti-patterns.
57
- - [Distribution](references/distribution.md): bricks, tiers, license class, monetization, store, third-party authoring.
@@ -1,55 +0,0 @@
1
- # Card design
2
-
3
- The host runs one engine for behavior: actions with typed forms, settings, credentials, status, activity. You choose the design: a layout template, the widgets inside it, and brand tokens. Every plugin gets its own look, tied to what it does.
4
-
5
- ## Pick a template by function
6
- | Template | Use it when the job is | Arrangement |
7
- | --- | --- | --- |
8
- | `console` | running a task and reading the result | primary form beside the output |
9
- | `ledger` | reviewing records and acting on one | table first, action rail at the side |
10
- | `monitor` | watching live health | gauges or status tiles above a live log |
11
- | `inbox` | working through items | list and detail |
12
- | `dashboard` | an overview then a few actions | status tiles, then actions |
13
- | `workbench` | several related tools | tabs, one tool per tab |
14
- | `vault` | managing stored records | record list with per-row actions |
15
-
16
- ## Widgets
17
- `status`, `actions`, `table`, `detail`, `log`, `form`, `keypad`, `gauge`, `list`, `settings`, `credentials`, `activity`, `live`. Widgets that read data (`table`, `detail`, `list`, `gauge`, `log`) need `source: { "command": "<declared read command>", "path": "<json path>" }`. `table` needs `columns` with `key`, `label` and optional `format` (`text number money date datetime bytes percent status`). `form` needs `command`. `actions` may list `commands` to feature; the rest stay reachable under "All commands". `live` mounts the plugin's own sandboxed panel.
18
-
19
- ```json
20
- "card_layout": { "template": "ledger", "regions": [
21
- { "widget": "status", "variant": "strip" },
22
- { "widget": "table", "source": { "command": "list", "path": "payments" },
23
- "columns": [ { "key": "id", "label": "Payment" }, { "key": "amount", "label": "Amount", "format": "money" }, { "key": "state", "label": "State", "format": "status" } ] },
24
- { "widget": "actions", "commands": ["refund"] },
25
- { "widget": "settings" }, { "widget": "credentials" } ] }
26
- ```
27
- Always include `settings` and `credentials` (or let the engine append them) so every setting and credential is visible.
28
-
29
- ## Name, logo and brand
30
- - **A wrapper of someone else's API keeps that provider's brand.** Name it for the provider (`Stripe`, `Google Maps`, `Telnyx Voice`) and set `branding.provider`. Do not invent a brand for a wrapper.
31
- - **First-party plugins** use a plain descriptive name.
32
- - **Monetized, operated services** (you run the instance or hold the provider accounts) have their own brand and logo, distinct from the software and providers behind them. Credit the underlying software in the about text.
33
- - **Real logos only.** `branding.logo` is an `.svg` or `.png` shipped in the plugin. A provider's logo comes from the provider's brand kit and is used only to identify the integration; record its source in `ui/LOGO.md`. First-party and monetized plugins ship an original mark. Never use an emoji or a generic icon as the brand. `branding.icon` is deprecated and warns.
34
-
35
- ## Drawer
36
- The card face stays compact: logo, real status, and the two or three primary actions. The host's slide-out drawer carries Settings, Credentials, Usage and Projects (see [distribution](distribution.md)); declare `configuration`, `secrets` and a `status` command and the drawer fills itself. Do not build your own settings or usage screens.
37
-
38
- ## Brand tokens
39
- ```json
40
- "branding": { "color": "#635bff", "accent": "#0a2540", "font": "ibm-plex",
41
- "density": "compact", "radius": "sm", "header": "ledger-rule" }
42
- ```
43
- - `color`, `accent`: `#rrggbb`. The engine derives surfaces and state colors and checks contrast.
44
- - `font`: `system inter ibm-plex space-grotesk jetbrains-mono dm-sans outfit fraunces newsreader chivo-mono`.
45
- - `density`: `compact comfortable spacious`. `radius`: `none sm md lg pill`. `header`: `plain ledger-rule band tab-strip badge-left`.
46
- Choose tokens from the plugin's function and audience, not from a default: a payments ledger reads as compact and exact, a messaging inbox as roomy and conversational, a security monitor as high-contrast and dense.
47
-
48
- ## Design process for an agent
49
- 1. Write down in `ui/DESIGN.md`: who uses the card, the two or three actions they take most, what they need to see without clicking, and the reasons for the template and tokens.
50
- 2. Declare `card_layout` and `branding`. Bind every data widget to a real command.
51
- 3. Render at 360, 768 and 1280 px wide and look at it. Check light and dark, keyboard focus order, 44 px targets and long values (wrapping, truncation with a title, empty and error states).
52
- 4. Compare against other plugins' cards. If it looks like another plugin's, change the template, widget order or tokens.
53
-
54
- ## Anti-patterns the validator or review rejects
55
- Fixed metrics; sample data; a made-up brand on a wrapper; an emoji or generic icon in place of a logo; decorative canvas or CSS animation; emoji as icons; hardware-skin imitations; raw hex colors in plugin code; one template used for every plugin; hiding settings or credentials; showing a secret value; claiming "connected" without a returned status.
@@ -1,23 +0,0 @@
1
- # Distribution, monetization and third-party plugins
2
-
3
- ## Open plugins as bricks
4
- `total-recall plugin vendor <source> [dir]` copies a plugin into `bricks/<id>` and records version, source and content hash in `tr-bricks.json`. `total-recall plugin bricks` reports each brick as unchanged, modified or missing. Edits are expected; the project owns the copy. Install the dependencies the command prints and add the brick's tests to the project gates. Do not edit the plugin repository to satisfy one project.
5
-
6
- ## Tiers and license class
7
- - `core`: ships with Total Recall (memory, auth, mesh, project tooling).
8
- - `official`: published by the maintainer, installed on demand.
9
- - `personal`: one owner's own variant; keep operator-specific names and infrastructure here.
10
- - `community`: published by a third party.
11
- Open plugins carry their source. Closed plugins are services.
12
-
13
- ## Monetizable (closed) plugins
14
- Source that runs on a user's machine cannot be protected by a local license check. A closed plugin is a thin client plus a hosted service that holds provider accounts, meters usage, enforces plan and credits, and bills. The client reads the license key from `total-recall auth secret`, sends signed requests, and reports a clear error when the license is missing, lapsed or out of credit. Show price and usage on the card. Typical candidates are wrappers over third-party APIs such as telephony, messaging, domains and mail, and metered AI access.
15
-
16
- ## Installations and usage
17
- Adding a plugin to a project records the installation locally: plugin, version, content hash, project path, date. The host lists these in the drawer's Projects tab with status (unchanged, modified, missing). Each brick appends usage entries to `<project>/.tr/usage/<plugin>.jsonl`: time, command, exit state, duration, units and cost for metered calls, never payloads or secrets. Closed services meter on the server with one key per project. Nothing is sent anywhere unless the owner explicitly opts in. A brick's `usage` helper must not write request or response bodies.
18
-
19
- ## Third-party authoring
20
- Anyone can build a plugin with `plugin create`, the manifest contract and the conformance checks, then submit it to the store. The store lists signed, immutable, hash-pinned versions, pays out to the manifest `monetization.recipient`, and never executes plugin code. API wrappers can be generated from an OpenAPI document with `plugin from-openapi` once it ships; the output is open and follows this contract.
21
-
22
- ## Status of these features
23
- Implemented: `plugin vendor`, `plugin bricks`, the manifest validator for arguments, effects, status, layout, branding and distribution. In progress under the plugin-split project: the shared card engine, the slide-out drawer, installation and usage ledgers, logo rendering and templates, the host `stdin` channel for secrets, `new`, `plugin test`, `plugin from-openapi`, license verification and the store. Declare the contract fields now; they validate today and render as the engine lands.
@@ -1,90 +0,0 @@
1
- # Manifest contract
2
-
3
- `plugin.json` is validated by Total Recall (`total-recall plugin info <id>` shows errors and warnings). Fields marked optional change what the card can show; omitting them still yields a card that lists commands and secrets.
4
-
5
- ## Identity and distribution
6
- ```json
7
- {
8
- "id": "payments",
9
- "name": "Payments",
10
- "version": "0.1.0",
11
- "description": "One concrete sentence about the job this plugin performs.",
12
- "license": "MIT",
13
- "use_cases": ["commerce"],
14
- "distribution": { "tier": "official", "license": "open" }
15
- }
16
- ```
17
- `id` is lowercase kebab-case. `name` is neutral and descriptive. `tier` is `core`, `official`, `personal` or `community`. `license` is `open` (source can be vendored) or `closed` (operated service).
18
-
19
- ## Commands
20
- ```json
21
- "cli": {
22
- "command": "payments",
23
- "handler": "./cli.mjs",
24
- "subcommands": [
25
- { "name": "status", "description": "Connection and credential state", "effect": "read", "output": "json" },
26
- { "name": "refund", "description": "Refund a payment", "effect": "destructive", "output": "json", "timeout_ms": 30000,
27
- "args": [
28
- { "name": "paymentId", "flag": "--payment", "label": "Payment", "type": "string", "required": true },
29
- { "name": "reason", "flag": "--reason", "label": "Reason", "type": "choice", "options": ["duplicate", "fraud", "requested"] },
30
- { "name": "amount", "flag": "--amount", "label": "Amount", "type": "number", "description": "Leave empty for a full refund" }
31
- ] },
32
- { "name": "token-set", "description": "Store the API key", "effect": "write",
33
- "args": [{ "name": "value", "label": "API key", "type": "string", "secret": true, "positional": true, "required": true }] }
34
- ]
35
- }
36
- ```
37
- - **Argument types:** `string`, `number`, `boolean`, `choice` (requires `options`). Each needs `name` (camelCase), `label`, and either `flag` (`--kebab-case`) or `positional: true`.
38
- - **Effects:** `read` (default), `write`, `destructive`, `send`. The card asks for confirmation before `destructive` and `send`, and never auto-runs anything but `read`.
39
- - **Secret arguments** are read from stdin by the CLI, never from a flag. A secret argument must not declare a `flag`.
40
- - Duplicate command or argument names are rejected.
41
-
42
- ## Status
43
- ```json
44
- "status": { "command": "status", "refresh_seconds": 30 }
45
- ```
46
- The command must be declared and must be a `read` command. It prints:
47
- ```json
48
- { "ok": true, "summary": "Connected, live mode", "items": [
49
- { "label": "Account", "value": "acct_123", "state": "ok" },
50
- { "label": "Webhook secret", "value": "missing", "state": "warn" } ] }
51
- ```
52
- States are `ok`, `warn`, `error`, `unknown`. Do not use `card_face.metrics`: fixed text cannot reflect state, and the validator warns about it.
53
-
54
- Only an explicit `effect: "read"` permits execution from a card. Missing effects default to reference-only; names such as `config`, `list` or `check` do not establish that a command is read-only. Grouped commands that accept a write action must not be labeled read.
55
-
56
- ## Secret input through the owner interface
57
-
58
- Declare exactly one argument with `type: "string"`, `secret: true` and no `flag`. A secret does not require positional metadata. The isolated owner interface can call `host.run('save-key', [], { stdin: value })`. The host authenticates and checks write scope, the server validates the manifest declaration and the 65,536-byte UTF-8 limit, and the child receives the value through stdin with `--stdin`. Empty, undeclared and ambiguous secret input is rejected. Child stdout/stderr is suppressed for this operation, and the owner interface receives only success or failure. Re-read nonsecret credential status through a separate command; never echo the submitted value.
59
-
60
- ## Workspaces
61
-
62
- `workspace.component` identifies an owner-supplied `ui.elements` id or tag. The host mounts that interface through the same isolated panel boundary; core contains no plugin-specific workspace implementation. `workspace.permissions` may list `microphone`, `camera` or `notifications`, but a declaration does not grant device access. Device-backed workspaces remain explicitly unavailable until a safe host capability bridge exists. Never add `allow-same-origin` to the scripts-only iframe to bypass this limitation.
63
-
64
- ## Settings and credentials
65
- ```json
66
- "configuration": { "fields": [
67
- { "name": "mode", "type": "string", "label": "Mode", "options": ["test", "live"], "default": "test", "required": true },
68
- { "name": "currency", "type": "string", "label": "Default currency", "default": "usd" },
69
- { "name": "sendReceipts", "type": "boolean", "label": "Email receipts", "default": true } ] },
70
- "secrets": [
71
- { "key": "PAYMENTS_SECRET_KEY", "description": "Secret key for payment processing", "required": true },
72
- { "key": "PAYMENTS_WEBHOOK_SECRET", "description": "Signs incoming webhooks", "required": false } ]
73
- ```
74
- Field names are camelCase and may not contain password, secret, apiKey or token; those belong in `secrets` with uppercase keys. Declare every setting and credential the plugin really reads, so the card shows all of them.
75
-
76
- ## Layout and branding
77
- See [card design](card-design.md) for `card_layout` and `branding`. Branding also carries the real logo and, for wrappers, the provider:
78
- ```json
79
- "branding": { "logo": "ui/logo.svg", "provider": { "name": "Stripe", "url": "https://stripe.com" }, "color": "#635bff" }
80
- ```
81
-
82
- ## Monetization (optional)
83
- ```json
84
- "monetization": { "model": "metered", "price_cents": 2, "currency": "usd", "period": "per-use",
85
- "license_key_secret": "PAYMENTS_LICENSE_KEY", "checkout_url": "https://example.com/plugins/payments" }
86
- ```
87
- Models: `free`, `one-time`, `subscription`, `metered`, `take-rate` (with `take_rate_basis_points`). Enforcement belongs to the operated service; see [distribution](distribution.md).
88
-
89
- ## Runtime, tasks and UI elements
90
- `tasks` run a declared subcommand on a cron schedule; each needs `intent`, `schedule`, `command`. `ui.elements` lists the optional live panel (`kind: "panel"` or `"card-face"`, `tag`, `module`). A plugin does not need `ui` at all for the standard card.
@@ -1,9 +0,0 @@
1
- # Task references
2
-
3
- Read only sections needed for the operation, including their prerequisites and constraints. Commands and plain paths assume the skill root. This index selects documentation, not permission or rule applicability.
4
-
5
- - [Overview](section-01.md)
6
- - [Where to work](section-02.md)
7
- - [Implement a capability](section-03.md)
8
- - [Validate the actual job](section-04.md)
9
- - [Share with another user](section-05.md)
@@ -1,35 +0,0 @@
1
- {
2
- "version": 1,
3
- "source_sha256": "d5470c2686d2e6d8aeb35977acad1bbd46a3018759b9753843e78005f2a9edf0",
4
- "entrypoint_sha256": "3d5ec8c164079c3f17b197bdeeef1dc2a49f27e5e1de8d53e6148627fb0dd6d5",
5
- "sections": [
6
- {
7
- "path": "references/optimized/section-01.md",
8
- "sha256": "6fd88392d0552c027241b6b3494ca17dc00aa7029d4bf061de199d4a31f04124"
9
- },
10
- {
11
- "path": "references/optimized/section-02.md",
12
- "sha256": "d5651cfeeddb7c12545acc96771e85c486069161c111a4576e4af34b089a6c8e"
13
- },
14
- {
15
- "path": "references/optimized/section-03.md",
16
- "sha256": "325c0da34e876b557a215c7b43d486f9447434af0c6188678eed347a21be4d79"
17
- },
18
- {
19
- "path": "references/optimized/section-04.md",
20
- "sha256": "b6ff62769477d9e88b703feaa88bef37ba2314b2a3de96d87778e21b25e94dd3"
21
- },
22
- {
23
- "path": "references/optimized/section-05.md",
24
- "sha256": "34681996b33ab68fdd479cfadec9e9ddf059fc511a81a5fa6778adaca78ab815"
25
- },
26
- {
27
- "path": "references/optimized/index.md",
28
- "sha256": "410f8b54e7645ee566baea54d62ea05d9869e5bd0566652bb69e42a0138d075a"
29
- },
30
- {
31
- "path": "references/optimized/original.md",
32
- "sha256": "d5470c2686d2e6d8aeb35977acad1bbd46a3018759b9753843e78005f2a9edf0"
33
- }
34
- ]
35
- }