total-recall-brain 3.28.2 → 3.30.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 (134) hide show
  1. package/bin/total-recall.mjs +24 -1
  2. package/docs/ARCHITECTURE.md +26 -18
  3. package/extension/background.js +259 -84
  4. package/extension/content-script.css +7 -13
  5. package/extension/content-script.js +252 -477
  6. package/extension/icons/icon-128.png +0 -0
  7. package/extension/icons/icon-16.png +0 -0
  8. package/extension/icons/icon-32.png +0 -0
  9. package/extension/icons/icon-48.png +0 -0
  10. package/extension/icons/mark.svg +6 -0
  11. package/extension/lib/brain-client.js +160 -73
  12. package/extension/lib/tokens.css +187 -0
  13. package/extension/lib/ui-helpers.js +229 -0
  14. package/extension/lib/ui-helpers.spec.mjs +94 -0
  15. package/extension/manifest.json +20 -5
  16. package/extension/options/options.css +64 -218
  17. package/extension/options/options.html +91 -43
  18. package/extension/options/options.js +208 -85
  19. package/extension/sidepanel/sidepanel.css +268 -970
  20. package/extension/sidepanel/sidepanel.html +200 -166
  21. package/extension/sidepanel/sidepanel.js +788 -942
  22. package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
  23. package/frontend/dist/index.html +1 -1
  24. package/frontend/src/api/plugins.ts +124 -92
  25. package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
  26. package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
  27. package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
  28. package/frontend/src/pages/PluginsPage.tsx +454 -1218
  29. package/package.json +2 -1
  30. package/plugins/code-quality/README.md +25 -0
  31. package/plugins/code-quality/cli.mjs +25 -0
  32. package/plugins/code-quality/plugin.json +17 -0
  33. package/plugins/git-sentinel/README.md +27 -0
  34. package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
  35. package/plugins/git-sentinel/plugin.json +18 -0
  36. package/plugins/system-monitor/README.md +26 -0
  37. package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
  38. package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
  39. package/plugins/system-monitor/plugin.json +29 -0
  40. package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
  41. package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
  42. package/scaffold/.agent/skills/total-recall/SKILL.md +22 -6
  43. package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
  44. package/src/cli/agent.mjs +1 -1
  45. package/src/cli/app/index.mjs +493 -0
  46. package/src/cli/brain.mjs +33 -3
  47. package/src/cli/command.mjs +80 -41
  48. package/src/cli/connect.mjs +10 -1
  49. package/src/cli/harness.mjs +1 -1
  50. package/src/cli/init.mjs +3 -1
  51. package/src/cli/mesh.mjs +118 -12
  52. package/src/cli/plugin/create.mjs +144 -25
  53. package/src/cli/plugin/index.mjs +83 -53
  54. package/src/cli/plugin/install.mjs +10 -127
  55. package/src/cli/plugin/list.mjs +44 -44
  56. package/src/cli/plugin/remove.mjs +23 -30
  57. package/src/cli/plugin/scaffold-cmd.mjs +30 -0
  58. package/src/cli/plugin/search.mjs +108 -23
  59. package/src/cli/rebuild.mjs +37 -1
  60. package/src/cli/recall.mjs +17 -2
  61. package/src/cli/research.mjs +4 -6
  62. package/src/cli/share.mjs +2 -0
  63. package/src/core/app-deploy/apply.mjs +386 -0
  64. package/src/core/app-deploy/design-tokens.mjs +217 -0
  65. package/src/core/app-deploy/plan.mjs +249 -0
  66. package/src/core/app-deploy/resolve.mjs +295 -0
  67. package/src/core/app-deploy/source.mjs +337 -0
  68. package/src/core/app-deploy/standalone.mjs +248 -0
  69. package/src/core/app-deploy/upgrade.mjs +256 -0
  70. package/src/core/app-deploy/verify.mjs +213 -0
  71. package/src/core/bound-hosts.mjs +1 -1
  72. package/src/core/brain-registry.mjs +146 -0
  73. package/src/core/brain-state.json +1 -0
  74. package/src/core/daemon-loop.mjs +30 -26
  75. package/src/core/dream.mjs +5 -17
  76. package/src/core/fact-seeker.mjs +53 -368
  77. package/src/core/lan-discovery.mjs +3 -3
  78. package/src/core/mesh-access-sync.mjs +134 -0
  79. package/src/core/mesh-access.mjs +18 -3
  80. package/src/core/mesh-discover.mjs +173 -0
  81. package/src/core/mesh.mjs +131 -32
  82. package/src/core/optimizer.mjs +4 -41
  83. package/src/core/plugin-bundle.mjs +167 -0
  84. package/src/core/plugin-context.mjs +8 -13
  85. package/src/core/plugin-loader.mjs +272 -68
  86. package/src/core/plugin-peers.mjs +163 -0
  87. package/src/core/plugin-public.mjs +85 -0
  88. package/src/core/plugin-runner-child.mjs +24 -0
  89. package/src/core/plugin-runner.mjs +102 -0
  90. package/src/core/plugin-store.mjs +384 -0
  91. package/src/core/plugin-tasks.mjs +212 -0
  92. package/src/core/post-mortem.mjs +40 -9
  93. package/src/core/research-gate.mjs +182 -0
  94. package/src/core/research-queue.mjs +47 -7
  95. package/src/core/research-surface.mjs +211 -0
  96. package/src/core/research.mjs +25 -32
  97. package/src/core/scheduler.mjs +67 -153
  98. package/src/core/schema.mjs +129 -0
  99. package/src/core/search.mjs +12 -1
  100. package/src/core/secret-integration-research.mjs +12 -8
  101. package/src/core/secrets-store.mjs +76 -2
  102. package/src/core/session-watcher.mjs +6 -0
  103. package/src/core/surface.mjs +64 -3
  104. package/src/core/task-executors.mjs +8 -45
  105. package/src/core/vault-cache.mjs +70 -4
  106. package/src/server/api.mjs +21 -0
  107. package/src/server/rest.mjs +2 -0
  108. package/src/server/route-manifest.json +21 -5
  109. package/src/server/routes/collab.mjs +38 -11
  110. package/src/server/routes/extension.mjs +3 -2
  111. package/src/server/routes/memory.mjs +24 -2
  112. package/src/server/routes/mesh.mjs +7 -7
  113. package/src/server/routes/plugins-mesh.mjs +49 -0
  114. package/src/server/routes/plugins.mjs +91 -426
  115. package/src/server/routes/research.mjs +5 -2
  116. package/src/server/routes/share.mjs +2 -0
  117. package/src/server/tools.mjs +29 -2
  118. package/extension/icons/README.md +0 -27
  119. package/extension/lib/preconfigured.js +0 -5
  120. package/extension/popup/popup.css +0 -227
  121. package/extension/popup/popup.html +0 -50
  122. package/extension/popup/popup.js +0 -175
  123. package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
  124. package/scaffold/.agent/plugins/.gitkeep +0 -0
  125. package/scaffold/.agent/plugins/code-quality/README.md +0 -16
  126. package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
  127. package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
  128. package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
  129. package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
  130. package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
  131. package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
  132. package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
  133. package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
  134. package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Capability Deployment — Source Resolver & Integrity Verifier
3
+ *
4
+ * Implements Phase 2 of CAPABILITY_DEPLOYMENT_PLUGINS:
5
+ * - Pinned source/hash verification
6
+ * - Safe extraction and filesystem containment
7
+ * - Strict rejection of symlinks, parent directory traversals, and absolute paths
8
+ * - Prevention of secret and environment credential leakage (.env, *.key, *.pem, secrets.enc)
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import os from 'node:os';
14
+ import crypto from 'node:crypto';
15
+ import {
16
+ validatePluginManifest,
17
+ bundledPluginsDir,
18
+ getPlugin
19
+ } from '../plugin-loader.mjs';
20
+ import {
21
+ decodeBundle,
22
+ hashPluginDir
23
+ } from '../plugin-bundle.mjs';
24
+ import {
25
+ parsePublicPluginSource,
26
+ fetchPublicBundle
27
+ } from '../plugin-public.mjs';
28
+
29
+ export const PROHIBITED_SECRET_PATTERNS = [
30
+ /^\.env(\..+)?$/i,
31
+ /\.pem$/i,
32
+ /\.key$/i,
33
+ /\.pkcs12$/i,
34
+ /\.pfx$/i,
35
+ /^id_rsa(\..+)?$/i,
36
+ /^id_ed25519(\..+)?$/i,
37
+ /^secrets\.enc$/i,
38
+ /credentials\.json$/i,
39
+ /client_secret.*\.json$/i
40
+ ];
41
+
42
+ /**
43
+ * Checks if a relative file path matches known credential/secret patterns.
44
+ * @param {string} relPath
45
+ * @returns {boolean}
46
+ */
47
+ export function isProhibitedSecretFile(relPath) {
48
+ const basename = path.basename(relPath);
49
+ return PROHIBITED_SECRET_PATTERNS.some((pattern) => pattern.test(basename));
50
+ }
51
+
52
+ /**
53
+ * Validates that a path is safe and strictly relative.
54
+ * Rejects absolute paths, windows drive letters, null bytes, backslashes, and directory traversals.
55
+ * @param {string} p
56
+ */
57
+ export function assertSafeSourcePath(p) {
58
+ if (typeof p !== 'string' || p.length === 0 || p.length > 512) {
59
+ throw new Error(`Invalid source path: empty or exceeds 512 characters (${p})`);
60
+ }
61
+ if (p.includes('\\') || p.includes('\0')) {
62
+ throw new Error(`Security error: path '${p}' contains forbidden characters (backslash or null byte)`);
63
+ }
64
+ if (p.startsWith('/') || /^[A-Za-z]:/.test(p)) {
65
+ throw new Error(`Security error: path '${p}' must be relative, not absolute`);
66
+ }
67
+ const parts = p.split('/');
68
+ for (const part of parts) {
69
+ if (part === '' || part === '.' || part === '..') {
70
+ throw new Error(`Security error: path '${p}' contains illegal traversal component ('${part}')`);
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Canonical SHA-256 computation over entries sorted by path.
77
+ * Format: path \0 length \0 data
78
+ * @param {Array<{ path: string, data: Buffer }>} entries
79
+ * @returns {string} hex digest
80
+ */
81
+ export function computeEntriesHash(entries) {
82
+ const sorted = [...entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
83
+ const h = crypto.createHash('sha256');
84
+ for (const e of sorted) {
85
+ h.update(e.path);
86
+ h.update('\0');
87
+ h.update(String(e.data.length));
88
+ h.update('\0');
89
+ h.update(e.data);
90
+ }
91
+ return h.digest('hex');
92
+ }
93
+
94
+ /**
95
+ * Scans a source directory, strictly verifying safety and computing canonical hash.
96
+ * @param {string} dirPath
97
+ * @returns {{ sha256: string, entries: Array<{ path: string, abs: string, size: number, mode: number, data: Buffer }> }}
98
+ */
99
+ export function scanAndVerifyDirectory(dirPath) {
100
+ const root = fs.realpathSync(dirPath);
101
+ const entries = [];
102
+ const SKIP_DIRS = new Set(['.git', 'node_modules', '.agent']);
103
+
104
+ function walk(current) {
105
+ for (const item of fs.readdirSync(current, { withFileTypes: true })) {
106
+ if (item.name.startsWith('.') && item.name !== '.agent') {
107
+ // Skip hidden dotfiles, but flag prohibited .env files
108
+ if (isProhibitedSecretFile(item.name)) {
109
+ throw new Error(`Security error: Prohibited credential file '${item.name}' found in capability source`);
110
+ }
111
+ continue;
112
+ }
113
+ if (SKIP_DIRS.has(item.name)) continue;
114
+
115
+ const abs = path.join(current, item.name);
116
+ const rel = path.relative(root, abs).split(path.sep).join('/');
117
+
118
+ assertSafeSourcePath(rel);
119
+
120
+ if (isProhibitedSecretFile(rel)) {
121
+ throw new Error(`Security error: Prohibited credential file '${rel}' found in capability source`);
122
+ }
123
+
124
+ if (item.isSymbolicLink()) {
125
+ throw new Error(`Security error: Symbolic link detected at '${rel}'. Symlinks are forbidden in capability sources.`);
126
+ }
127
+
128
+ if (item.isDirectory()) {
129
+ walk(abs);
130
+ } else if (item.isFile()) {
131
+ const stat = fs.statSync(abs);
132
+ const data = fs.readFileSync(abs);
133
+ entries.push({
134
+ path: rel,
135
+ abs,
136
+ size: stat.size,
137
+ mode: stat.mode & 0o777,
138
+ data
139
+ });
140
+ }
141
+ }
142
+ }
143
+
144
+ walk(root);
145
+
146
+ if (entries.length === 0) {
147
+ throw new Error(`Capability source directory is empty: ${dirPath}`);
148
+ }
149
+
150
+ const manifestEntry = entries.find((e) => e.path === 'plugin.json');
151
+ if (!manifestEntry) {
152
+ throw new Error(`Capability source missing required manifest 'plugin.json' at root: ${dirPath}`);
153
+ }
154
+
155
+ const sha256 = computeEntriesHash(entries);
156
+ return { sha256, entries };
157
+ }
158
+
159
+ /**
160
+ * Safely extracts validated entries into a target destination.
161
+ * @param {Array<{ path: string, mode?: number, data: Buffer }>} entries
162
+ * @param {string} destDir
163
+ */
164
+ export function extractEntriesSafely(entries, destDir) {
165
+ if (!fs.existsSync(destDir)) {
166
+ fs.mkdirSync(destDir, { recursive: true });
167
+ }
168
+ const realDest = fs.realpathSync(destDir);
169
+
170
+ for (const entry of entries) {
171
+ assertSafeSourcePath(entry.path);
172
+ if (isProhibitedSecretFile(entry.path)) {
173
+ throw new Error(`Security error: cannot extract prohibited secret file '${entry.path}'`);
174
+ }
175
+
176
+ const targetAbs = path.join(realDest, entry.path);
177
+ // Ensure target doesn't escape destination directory
178
+ if (!targetAbs.startsWith(realDest + path.sep)) {
179
+ throw new Error(`Security error: extracted path '${entry.path}' escapes destination directory`);
180
+ }
181
+
182
+ fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
183
+ fs.writeFileSync(targetAbs, entry.data, { mode: entry.mode || 0o644 });
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Resolves, validates, and stages a capability plugin from a local path, bundled ID, public link, or bundle.
189
+ *
190
+ * @param {string|object} source - Local path, bundled id, public URL, or in-memory bundle
191
+ * @param {object} [options]
192
+ * @param {string} [options.expectedHash] - Expected SHA-256 hash
193
+ * @param {string} [options.projectRoot] - Target project root
194
+ * @param {string} [options.stagingDir] - Destination directory to stage files
195
+ * @param {object} [options.publicDeps] - Dependency injection for network operations (fetch)
196
+ * @returns {Promise<{
197
+ * id: string,
198
+ * version: string,
199
+ * manifest: object,
200
+ * sourceUri: string,
201
+ * sourceType: string,
202
+ * sha256: string,
203
+ * fileCount: number,
204
+ * sizeBytes: number,
205
+ * stagedDir: string,
206
+ * files: Array<{ path: string, size: number, sha256: string }>,
207
+ * cleanup: () => void
208
+ * }>}
209
+ */
210
+ export async function resolveCapabilitySource(source, options = {}) {
211
+ let expectedHash = options.expectedHash || null;
212
+ let sourceType = 'local';
213
+ let sourceUri = typeof source === 'string' ? source : 'in-memory-bundle';
214
+ let entries = [];
215
+ let computedSha = null;
216
+
217
+ // 1. In-memory bundle object
218
+ if (typeof source === 'object' && source !== null && source.files && source.format) {
219
+ sourceType = 'bundle';
220
+ const decoded = decodeBundle(source, { expectedSha256: expectedHash || undefined });
221
+ computedSha = decoded.sha256;
222
+ entries = decoded.entries;
223
+ }
224
+ // 2. Public hash-pinned URL
225
+ else if (typeof source === 'string' && (source.startsWith('http://') || source.startsWith('https://'))) {
226
+ sourceType = 'public';
227
+ const parsed = parsePublicPluginSource(source);
228
+ expectedHash = expectedHash || parsed.sha256;
229
+ const { bundle, sha256 } = await fetchPublicBundle(source, options.publicDeps);
230
+ const decoded = decodeBundle(bundle, { expectedSha256: expectedHash });
231
+ computedSha = sha256;
232
+ entries = decoded.entries;
233
+ }
234
+ // 3. Bundled or local path / ID
235
+ else if (typeof source === 'string') {
236
+ let candidateDir = null;
237
+
238
+ if (source.startsWith('bundled:')) {
239
+ sourceType = 'bundled';
240
+ const pluginId = source.replace('bundled:', '');
241
+ candidateDir = path.join(bundledPluginsDir(), pluginId);
242
+ } else if (fs.existsSync(source)) {
243
+ sourceType = 'local';
244
+ candidateDir = path.resolve(source);
245
+ } else {
246
+ // Check if it's an installed or bundled plugin by ID
247
+ const installed = getPlugin(source, options.projectRoot || process.cwd());
248
+ if (installed && fs.existsSync(installed.dir)) {
249
+ sourceType = installed.scope === 'bundled' ? 'bundled' : 'local';
250
+ candidateDir = installed.dir;
251
+ } else {
252
+ const bundledDir = path.join(bundledPluginsDir(), source);
253
+ if (fs.existsSync(bundledDir)) {
254
+ sourceType = 'bundled';
255
+ candidateDir = bundledDir;
256
+ }
257
+ }
258
+ }
259
+
260
+ if (!candidateDir || !fs.existsSync(candidateDir)) {
261
+ throw new Error(`Cannot resolve capability plugin source: '${source}' (path or plugin does not exist)`);
262
+ }
263
+
264
+ const scan = scanAndVerifyDirectory(candidateDir);
265
+ computedSha = scan.sha256;
266
+ entries = scan.entries;
267
+ } else {
268
+ throw new Error(`Invalid capability plugin source: ${typeof source}`);
269
+ }
270
+
271
+ // Verify hash pin if provided
272
+ if (expectedHash && computedSha !== expectedHash) {
273
+ const err = new Error(
274
+ `Capability source hash mismatch: expected '${expectedHash}', but computed '${computedSha}'`
275
+ );
276
+ err.name = 'SourceHashMismatchError';
277
+ err.expected = expectedHash;
278
+ err.actual = computedSha;
279
+ throw err;
280
+ }
281
+
282
+ // Parse and validate plugin manifest
283
+ const manifestEntry = entries.find((e) => e.path === 'plugin.json');
284
+ if (!manifestEntry) {
285
+ throw new Error(`Capability source is missing 'plugin.json' manifest.`);
286
+ }
287
+
288
+ let manifest;
289
+ try {
290
+ manifest = JSON.parse(manifestEntry.data.toString('utf8'));
291
+ } catch (parseErr) {
292
+ throw new Error(`Malformed plugin.json manifest: ${parseErr.message}`);
293
+ }
294
+
295
+ const validation = validatePluginManifest(manifest);
296
+ if (!validation.valid) {
297
+ throw new Error(`Invalid plugin manifest in source: ${validation.errors.join('; ')}`);
298
+ }
299
+
300
+ // Stage extracted files
301
+ let stagedDir = options.stagingDir;
302
+ let isTemp = false;
303
+ if (!stagedDir) {
304
+ stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), `tr-cap-source-${manifest.id}-`));
305
+ isTemp = true;
306
+ }
307
+
308
+ extractEntriesSafely(entries, stagedDir);
309
+
310
+ const fileDescriptors = entries.map((e) => ({
311
+ path: e.path,
312
+ size: e.size || e.data.length,
313
+ sha256: crypto.createHash('sha256').update(e.data).digest('hex')
314
+ }));
315
+
316
+ const totalBytes = fileDescriptors.reduce((sum, f) => sum + f.size, 0);
317
+
318
+ return {
319
+ id: manifest.id,
320
+ version: manifest.version,
321
+ manifest,
322
+ sourceUri,
323
+ sourceType,
324
+ sha256: computedSha,
325
+ fileCount: entries.length,
326
+ sizeBytes: totalBytes,
327
+ stagedDir,
328
+ files: fileDescriptors,
329
+ cleanup: () => {
330
+ if (isTemp && fs.existsSync(stagedDir)) {
331
+ try {
332
+ fs.rmSync(stagedDir, { recursive: true, force: true });
333
+ } catch {}
334
+ }
335
+ }
336
+ };
337
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Total Recall — Standalone Application Creator
3
+ *
4
+ * Implements Phase 3 of CAPABILITY_DEPLOYMENT_PLUGINS:
5
+ * - Scaffolds clean-room runnable standalone application shell
6
+ * - Initializes SSSS vault and project Total Recall brain
7
+ * - Deploys initial capability plugin in isolated staging
8
+ * - Verifies static gates before atomic publication to target directory
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import os from 'node:os';
14
+ import { applyDeploymentPlan, resolveAppVaultDir, formatSsssDocument } from './apply.mjs';
15
+ import { createDeploymentPlan } from './plan.mjs';
16
+
17
+ export class AppCreateError extends Error {
18
+ constructor(message, details = {}) {
19
+ super(message);
20
+ this.name = 'AppCreateError';
21
+ this.details = details;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Copies a directory recursively from src to dest.
27
+ * @param {string} src
28
+ * @param {string} dest
29
+ */
30
+ function copyDirSync(src, dest) {
31
+ fs.mkdirSync(dest, { recursive: true });
32
+ for (const item of fs.readdirSync(src, { withFileTypes: true })) {
33
+ const srcPath = path.join(src, item.name);
34
+ const destPath = path.join(dest, item.name);
35
+ if (item.isDirectory()) {
36
+ copyDirSync(srcPath, destPath);
37
+ } else {
38
+ fs.copyFileSync(srcPath, destPath);
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Scaffolds and publishes a new standalone application with an initial capability plugin.
45
+ *
46
+ * @param {string} targetDir - Destination directory for the new app
47
+ * @param {object} options
48
+ * @param {string} options.plugin - Source path or identifier for initial capability
49
+ * @param {string} [options.adapter] - Target adapter (default: 'ssss-app')
50
+ * @param {string} [options.name] - Application name
51
+ * @param {boolean} [options.force] - Allow overwriting non-empty target directory
52
+ * @param {boolean} [options.initBrain] - Initialize project Total Recall brain (default: true)
53
+ * @param {boolean} [options.runGates] - Run pre-publish verification gates (default: true)
54
+ * @returns {Promise<{
55
+ * success: boolean,
56
+ * targetDir: string,
57
+ * name: string,
58
+ * adapter: string,
59
+ * capabilities: string[],
60
+ * published_at: string
61
+ * }>}
62
+ */
63
+ export async function createStandaloneApp(targetDir, options = {}) {
64
+ const resolvedTarget = path.resolve(targetDir);
65
+ const appName = options.name || path.basename(resolvedTarget);
66
+ const adapter = options.adapter || 'ssss-app';
67
+ const initBrain = options.initBrain !== false;
68
+ const runGates = options.runGates !== false;
69
+
70
+ if (!options.plugin) {
71
+ throw new AppCreateError('Missing required capability plugin source (--plugin)');
72
+ }
73
+
74
+ // Guard against accidental overwrite
75
+ if (fs.existsSync(resolvedTarget) && !options.force) {
76
+ const items = fs.readdirSync(resolvedTarget);
77
+ if (items.length > 0) {
78
+ throw new AppCreateError(
79
+ `Target directory '${resolvedTarget}' is not empty. Use --force to overwrite.`
80
+ );
81
+ }
82
+ }
83
+
84
+ // 1. Staged creation in isolated directory
85
+ const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tr-app-create-'));
86
+
87
+ try {
88
+ // 2. Scaffold standard SSSS application shell
89
+ const packageJson = {
90
+ name: appName,
91
+ version: '0.1.0',
92
+ type: 'module',
93
+ scripts: {
94
+ start: 'node index.mjs',
95
+ check: 'node check.mjs'
96
+ },
97
+ ssss: {
98
+ spec_version: '0.9.6',
99
+ adapter
100
+ }
101
+ };
102
+ fs.writeFileSync(
103
+ path.join(stagingDir, 'package.json'),
104
+ JSON.stringify(packageJson, null, 2),
105
+ 'utf8'
106
+ );
107
+
108
+ // Standalone entrypoint runnable without Total Recall daemon
109
+ const indexJs = `/**
110
+ * ${appName} — Standalone SSSS Application Entrypoint
111
+ * Generated by Total Recall Capability Engine.
112
+ * Operates independently with zero external database dependencies.
113
+ */
114
+
115
+ import fs from 'node:fs';
116
+ import path from 'node:path';
117
+
118
+ export function getAppInfo() {
119
+ return {
120
+ name: '${appName}',
121
+ adapter: '${adapter}',
122
+ status: 'ready',
123
+ vault: path.resolve('vault')
124
+ };
125
+ }
126
+
127
+ if (import.meta.url === \`file://\${process.argv[1]}\`) {
128
+ console.log('🚀 ${appName} started in standalone mode');
129
+ console.log('📦 SSSS Adapter: ${adapter}');
130
+ console.log('📁 Vault Root: ', path.resolve('vault'));
131
+ }
132
+ `;
133
+ fs.writeFileSync(path.join(stagingDir, 'index.mjs'), indexJs, 'utf8');
134
+
135
+ // Standalone verification gate runner
136
+ const checkJs = `/**
137
+ * ${appName} — Verification Gate
138
+ */
139
+ import fs from 'node:fs';
140
+ import path from 'node:path';
141
+
142
+ export function runChecks() {
143
+ const vaultDir = path.resolve('vault');
144
+ if (!fs.existsSync(vaultDir)) {
145
+ throw new Error('Missing required SSSS vault directory');
146
+ }
147
+ return { passed: true, checks: ['vault_present', 'entrypoint_present'] };
148
+ }
149
+
150
+ if (import.meta.url === \`file://\${process.argv[1]}\`) {
151
+ const result = runChecks();
152
+ console.log('✔ All verification gates passed:', result.checks.join(', '));
153
+ }
154
+ `;
155
+ fs.writeFileSync(path.join(stagingDir, 'check.mjs'), checkJs, 'utf8');
156
+
157
+ // SSSS Vault structure
158
+ const vaultDir = path.join(stagingDir, 'vault');
159
+ fs.mkdirSync(path.join(vaultDir, 'system', 'capabilities'), { recursive: true });
160
+ fs.mkdirSync(path.join(vaultDir, 'system', 'grants'), { recursive: true });
161
+ fs.mkdirSync(path.join(vaultDir, 'system', 'events'), { recursive: true });
162
+
163
+ // 3. Initialize project Total Recall brain if requested
164
+ if (initBrain) {
165
+ const brainDir = path.join(stagingDir, '.agent', 'skills', 'total-recall');
166
+ fs.mkdirSync(path.join(brainDir, 'memory-vault'), { recursive: true });
167
+ fs.mkdirSync(path.join(stagingDir, '.agent', 'commands'), { recursive: true });
168
+ }
169
+
170
+ // 4. Deploy initial capability plugin into staging
171
+ const plan = await createDeploymentPlan(options.plugin, {
172
+ target: stagingDir,
173
+ adapter
174
+ });
175
+
176
+ if (!plan.valid) {
177
+ throw new AppCreateError(
178
+ `Capability plugin is invalid for target app: ${plan.conflicts?.length || 0} conflict(s)`
179
+ );
180
+ }
181
+
182
+ const applied = await applyDeploymentPlan(plan, {
183
+ target: stagingDir,
184
+ actor: 'total-recall/app-create'
185
+ });
186
+
187
+ // Write standard README.md if capability did not supply one
188
+ const readmePath = path.join(stagingDir, 'README.md');
189
+ if (!fs.existsSync(readmePath)) {
190
+ fs.writeFileSync(
191
+ readmePath,
192
+ `# ${appName}\n\nStandalone SSSS application powered by the Total Recall capability engine.\n`,
193
+ 'utf8'
194
+ );
195
+ }
196
+
197
+ // 5. Verification Gates
198
+ if (runGates) {
199
+ if (!fs.existsSync(path.join(stagingDir, 'package.json'))) {
200
+ throw new AppCreateError('Verification gate failed: package.json missing in staged app');
201
+ }
202
+ if (!fs.existsSync(path.join(stagingDir, 'index.mjs'))) {
203
+ throw new AppCreateError('Verification gate failed: entrypoint index.mjs missing');
204
+ }
205
+ for (const capId of applied.capabilities) {
206
+ const capRecord = path.join(stagingDir, 'vault', 'system', 'capabilities', `${capId}.md`);
207
+ if (!fs.existsSync(capRecord)) {
208
+ throw new AppCreateError(
209
+ `Verification gate failed: capability installation record missing for '${capId}'`
210
+ );
211
+ }
212
+ }
213
+ }
214
+
215
+ // 6. Atomic Publication to target directory
216
+ if (fs.existsSync(resolvedTarget) && options.force) {
217
+ fs.rmSync(resolvedTarget, { recursive: true, force: true });
218
+ }
219
+
220
+ fs.mkdirSync(path.dirname(resolvedTarget), { recursive: true });
221
+
222
+ try {
223
+ fs.renameSync(stagingDir, resolvedTarget);
224
+ } catch {
225
+ // Fallback if renaming crosses device boundaries
226
+ copyDirSync(stagingDir, resolvedTarget);
227
+ fs.rmSync(stagingDir, { recursive: true, force: true });
228
+ }
229
+
230
+ return {
231
+ success: true,
232
+ targetDir: resolvedTarget,
233
+ name: appName,
234
+ adapter,
235
+ capabilities: applied.capabilities,
236
+ published_at: new Date().toISOString()
237
+ };
238
+ } catch (err) {
239
+ if (fs.existsSync(stagingDir)) {
240
+ try {
241
+ fs.rmSync(stagingDir, { recursive: true, force: true });
242
+ } catch {}
243
+ }
244
+ throw new AppCreateError(`Failed to create standalone application: ${err.message}`, {
245
+ error: err
246
+ });
247
+ }
248
+ }