@jbarket/loomai 0.5.0

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 (176) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/LICENSE +661 -0
  3. package/README.md +702 -0
  4. package/assets/demo.cast +396 -0
  5. package/assets/skill/SKILL.md +192 -0
  6. package/dist/backends/embedding-runtime.d.ts +39 -0
  7. package/dist/backends/embedding-runtime.js +265 -0
  8. package/dist/backends/episodes.d.ts +36 -0
  9. package/dist/backends/episodes.js +126 -0
  10. package/dist/backends/fastembed.d.ts +18 -0
  11. package/dist/backends/fastembed.js +95 -0
  12. package/dist/backends/glob.d.ts +10 -0
  13. package/dist/backends/glob.js +16 -0
  14. package/dist/backends/index.d.ts +6 -0
  15. package/dist/backends/index.js +78 -0
  16. package/dist/backends/migrations.d.ts +38 -0
  17. package/dist/backends/migrations.js +157 -0
  18. package/dist/backends/mmr.d.ts +45 -0
  19. package/dist/backends/mmr.js +82 -0
  20. package/dist/backends/proposals.d.ts +65 -0
  21. package/dist/backends/proposals.js +182 -0
  22. package/dist/backends/recall-log.d.ts +42 -0
  23. package/dist/backends/recall-log.js +143 -0
  24. package/dist/backends/retry.d.ts +29 -0
  25. package/dist/backends/retry.js +60 -0
  26. package/dist/backends/salience.d.ts +57 -0
  27. package/dist/backends/salience.js +186 -0
  28. package/dist/backends/sqlite-knowledge.d.ts +37 -0
  29. package/dist/backends/sqlite-knowledge.js +788 -0
  30. package/dist/backends/sqlite-vec.d.ts +64 -0
  31. package/dist/backends/sqlite-vec.js +737 -0
  32. package/dist/backends/ttl.d.ts +24 -0
  33. package/dist/backends/ttl.js +50 -0
  34. package/dist/backends/types.d.ts +562 -0
  35. package/dist/backends/types.js +8 -0
  36. package/dist/blocks/harness.d.ts +45 -0
  37. package/dist/blocks/harness.js +142 -0
  38. package/dist/blocks/model.d.ts +4 -0
  39. package/dist/blocks/model.js +56 -0
  40. package/dist/blocks/types.d.ts +40 -0
  41. package/dist/blocks/types.js +34 -0
  42. package/dist/categories.d.ts +16 -0
  43. package/dist/categories.js +29 -0
  44. package/dist/cli/args.d.ts +21 -0
  45. package/dist/cli/args.js +47 -0
  46. package/dist/cli/bootstrap.d.ts +2 -0
  47. package/dist/cli/bootstrap.js +214 -0
  48. package/dist/cli/doctor.d.ts +2 -0
  49. package/dist/cli/doctor.js +206 -0
  50. package/dist/cli/forget.d.ts +2 -0
  51. package/dist/cli/forget.js +97 -0
  52. package/dist/cli/harness.d.ts +2 -0
  53. package/dist/cli/harness.js +91 -0
  54. package/dist/cli/index.d.ts +2 -0
  55. package/dist/cli/index.js +127 -0
  56. package/dist/cli/inject.d.ts +2 -0
  57. package/dist/cli/inject.js +298 -0
  58. package/dist/cli/install.d.ts +2 -0
  59. package/dist/cli/install.js +137 -0
  60. package/dist/cli/io.d.ts +14 -0
  61. package/dist/cli/io.js +63 -0
  62. package/dist/cli/knowledge.d.ts +2 -0
  63. package/dist/cli/knowledge.js +216 -0
  64. package/dist/cli/memory.d.ts +2 -0
  65. package/dist/cli/memory.js +608 -0
  66. package/dist/cli/migrate.d.ts +2 -0
  67. package/dist/cli/migrate.js +116 -0
  68. package/dist/cli/recall.d.ts +2 -0
  69. package/dist/cli/recall.js +91 -0
  70. package/dist/cli/remember.d.ts +2 -0
  71. package/dist/cli/remember.js +111 -0
  72. package/dist/cli/serve.d.ts +2 -0
  73. package/dist/cli/serve.js +71 -0
  74. package/dist/cli/subcommands.d.ts +7 -0
  75. package/dist/cli/subcommands.js +11 -0
  76. package/dist/cli/tui/multi-select.d.ts +53 -0
  77. package/dist/cli/tui/multi-select.js +116 -0
  78. package/dist/cli/update-identity.d.ts +2 -0
  79. package/dist/cli/update-identity.js +90 -0
  80. package/dist/cli/update.d.ts +2 -0
  81. package/dist/cli/update.js +87 -0
  82. package/dist/cli/wake.d.ts +2 -0
  83. package/dist/cli/wake.js +53 -0
  84. package/dist/clients.d.ts +8 -0
  85. package/dist/clients.js +63 -0
  86. package/dist/config.d.ts +59 -0
  87. package/dist/config.js +160 -0
  88. package/dist/index.d.ts +15 -0
  89. package/dist/index.js +75 -0
  90. package/dist/injection/harnesses.d.ts +17 -0
  91. package/dist/injection/harnesses.js +51 -0
  92. package/dist/injection/render.d.ts +17 -0
  93. package/dist/injection/render.js +50 -0
  94. package/dist/injection/writer.d.ts +25 -0
  95. package/dist/injection/writer.js +118 -0
  96. package/dist/install/harnesses.d.ts +26 -0
  97. package/dist/install/harnesses.js +90 -0
  98. package/dist/install/names.d.ts +16 -0
  99. package/dist/install/names.js +36 -0
  100. package/dist/install/render.d.ts +18 -0
  101. package/dist/install/render.js +42 -0
  102. package/dist/install/skill-source.d.ts +1 -0
  103. package/dist/install/skill-source.js +11 -0
  104. package/dist/path-safety.d.ts +19 -0
  105. package/dist/path-safety.js +62 -0
  106. package/dist/server.d.ts +8 -0
  107. package/dist/server.js +571 -0
  108. package/dist/tools/append-maintenance-log.d.ts +52 -0
  109. package/dist/tools/append-maintenance-log.js +103 -0
  110. package/dist/tools/archive.d.ts +2 -0
  111. package/dist/tools/archive.js +21 -0
  112. package/dist/tools/bootstrap.d.ts +11 -0
  113. package/dist/tools/bootstrap.js +161 -0
  114. package/dist/tools/dossier.d.ts +1 -0
  115. package/dist/tools/dossier.js +102 -0
  116. package/dist/tools/eurorack-import.d.ts +47 -0
  117. package/dist/tools/eurorack-import.js +199 -0
  118. package/dist/tools/find-similar.d.ts +3 -0
  119. package/dist/tools/find-similar.js +26 -0
  120. package/dist/tools/forget.d.ts +6 -0
  121. package/dist/tools/forget.js +70 -0
  122. package/dist/tools/harness.d.ts +18 -0
  123. package/dist/tools/harness.js +54 -0
  124. package/dist/tools/identity-scaffold.d.ts +22 -0
  125. package/dist/tools/identity-scaffold.js +142 -0
  126. package/dist/tools/identity.d.ts +4 -0
  127. package/dist/tools/identity.js +226 -0
  128. package/dist/tools/knowledge-archive.d.ts +5 -0
  129. package/dist/tools/knowledge-archive.js +24 -0
  130. package/dist/tools/knowledge-history.d.ts +6 -0
  131. package/dist/tools/knowledge-history.js +55 -0
  132. package/dist/tools/knowledge-maintain.d.ts +9 -0
  133. package/dist/tools/knowledge-maintain.js +113 -0
  134. package/dist/tools/knowledge-merge.d.ts +8 -0
  135. package/dist/tools/knowledge-merge.js +40 -0
  136. package/dist/tools/knowledge-move.d.ts +10 -0
  137. package/dist/tools/knowledge-move.js +48 -0
  138. package/dist/tools/knowledge-purge.d.ts +5 -0
  139. package/dist/tools/knowledge-purge.js +20 -0
  140. package/dist/tools/knowledge-recall.d.ts +20 -0
  141. package/dist/tools/knowledge-recall.js +142 -0
  142. package/dist/tools/knowledge-restore.d.ts +4 -0
  143. package/dist/tools/knowledge-restore.js +24 -0
  144. package/dist/tools/knowledge-supersede.d.ts +6 -0
  145. package/dist/tools/knowledge-supersede.js +36 -0
  146. package/dist/tools/knowledge-verify.d.ts +8 -0
  147. package/dist/tools/knowledge-verify.js +39 -0
  148. package/dist/tools/knowledge-write.d.ts +25 -0
  149. package/dist/tools/knowledge-write.js +87 -0
  150. package/dist/tools/memory-audit.d.ts +3 -0
  151. package/dist/tools/memory-audit.js +47 -0
  152. package/dist/tools/memory-list.d.ts +4 -0
  153. package/dist/tools/memory-list.js +23 -0
  154. package/dist/tools/prune.d.ts +4 -0
  155. package/dist/tools/prune.js +28 -0
  156. package/dist/tools/recall.d.ts +4 -0
  157. package/dist/tools/recall.js +24 -0
  158. package/dist/tools/remember.d.ts +10 -0
  159. package/dist/tools/remember.js +43 -0
  160. package/dist/tools/restore.d.ts +2 -0
  161. package/dist/tools/restore.js +20 -0
  162. package/dist/tools/update-identity.d.ts +34 -0
  163. package/dist/tools/update-identity.js +194 -0
  164. package/dist/tools/update.d.ts +2 -0
  165. package/dist/tools/update.js +16 -0
  166. package/dist/transport/guards.d.ts +35 -0
  167. package/dist/transport/guards.js +107 -0
  168. package/dist/transport/http-server.d.ts +17 -0
  169. package/dist/transport/http-server.js +198 -0
  170. package/dist/transport/mcp-app-hello.d.ts +3 -0
  171. package/dist/transport/mcp-app-hello.js +66 -0
  172. package/dist/widgets/display-modes.html +131 -0
  173. package/dist/widgets/hello.html +138 -0
  174. package/dist/widgets/proposals.html +131 -0
  175. package/dist/widgets/top-of-mind.html +131 -0
  176. package/package.json +73 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * loom doctor — read-only environment probe. Reports node version
3
+ * compatibility, stack version, existing agents under
4
+ * ~/.config/loom/*, and forward-looking git fields per agent. Never
5
+ * writes. Exit 0 regardless of findings; health is the output, not
6
+ * the exit code.
7
+ */
8
+ import { parseArgs } from 'node:util';
9
+ import { readdir, readFile, stat, lstat, readlink } from 'node:fs/promises';
10
+ import { join } from 'node:path';
11
+ import { extractGlobalFlags } from './args.js';
12
+ import { renderJson } from './io.js';
13
+ import { CURRENT_STACK_VERSION, resolveDefaultContextPath } from '../config.js';
14
+ const USAGE = `Usage: loom doctor [options]
15
+
16
+ Probes the loom environment. Read-only; exits 0 regardless of findings.
17
+
18
+ Options:
19
+ --json Machine-readable output
20
+ --help Show this help
21
+ `;
22
+ async function fileExists(p) {
23
+ try {
24
+ await stat(p);
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ async function probeGit(agentDir) {
32
+ const dotGit = join(agentDir, '.git');
33
+ const initialized = await fileExists(dotGit);
34
+ const gitignorePresent = await fileExists(join(agentDir, '.gitignore'));
35
+ return {
36
+ initialized,
37
+ hasRemote: false,
38
+ dirty: false,
39
+ gitignorePresent,
40
+ };
41
+ }
42
+ async function probeDefault(root) {
43
+ const defaultPath = join(root, 'default');
44
+ let s;
45
+ try {
46
+ s = await lstat(defaultPath);
47
+ }
48
+ catch {
49
+ return { exists: false, isSymlink: false };
50
+ }
51
+ if (s.isSymbolicLink()) {
52
+ const target = await readlink(defaultPath);
53
+ try {
54
+ await stat(defaultPath); // follows the symlink; throws if dangling
55
+ const empty = !(await fileExists(join(defaultPath, 'IDENTITY.md')));
56
+ return { exists: true, isSymlink: true, target, dangling: false, empty };
57
+ }
58
+ catch {
59
+ return { exists: true, isSymlink: true, target, dangling: true };
60
+ }
61
+ }
62
+ // A real directory named "default" — should be a symlink per the spec
63
+ const empty = !(await fileExists(join(defaultPath, 'IDENTITY.md')));
64
+ return { exists: true, isSymlink: false, empty };
65
+ }
66
+ async function probeAgents(home) {
67
+ const root = join(home, '.config', 'loom');
68
+ const agents = [];
69
+ let entries = [];
70
+ try {
71
+ entries = await readdir(root);
72
+ }
73
+ catch {
74
+ const defaultStatus = await probeDefault(root);
75
+ return { root, agents, defaultStatus };
76
+ }
77
+ for (const name of entries.sort()) {
78
+ if (name === 'default')
79
+ continue; // handled via probeDefault
80
+ const p = join(root, name);
81
+ let s;
82
+ try {
83
+ s = await stat(p);
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ if (!s.isDirectory())
89
+ continue;
90
+ agents.push({
91
+ name,
92
+ path: p,
93
+ hasIdentity: await fileExists(join(p, 'IDENTITY.md')),
94
+ hasMemoriesDb: await fileExists(join(p, 'memories.db')),
95
+ git: await probeGit(p),
96
+ });
97
+ }
98
+ const defaultStatus = await probeDefault(root);
99
+ return { root, agents, defaultStatus };
100
+ }
101
+ function defaultIsOk(ds) {
102
+ if (!ds.exists)
103
+ return false;
104
+ if (!ds.isSymlink)
105
+ return false; // real dir is not ok — must be a symlink
106
+ if (ds.dangling)
107
+ return false;
108
+ if (ds.empty)
109
+ return false;
110
+ return true;
111
+ }
112
+ function nodeOk(version) {
113
+ const m = version.match(/^v(\d+)\./);
114
+ return m !== null && Number(m[1]) >= 20;
115
+ }
116
+ async function probeStackVersion(contextDir) {
117
+ const file = join(contextDir, 'LOOM_STACK_VERSION');
118
+ try {
119
+ const body = (await readFile(file, 'utf-8')).trim();
120
+ const v = Number(body);
121
+ return Number.isInteger(v) && v <= CURRENT_STACK_VERSION;
122
+ }
123
+ catch {
124
+ return true;
125
+ }
126
+ }
127
+ export async function run(argv, io) {
128
+ const { flags: global, rest } = extractGlobalFlags(argv);
129
+ let parsed;
130
+ try {
131
+ parsed = parseArgs({
132
+ args: rest,
133
+ options: {
134
+ json: { type: 'boolean' },
135
+ help: { type: 'boolean', short: 'h' },
136
+ },
137
+ strict: true,
138
+ allowPositionals: false,
139
+ });
140
+ }
141
+ catch (err) {
142
+ io.stderr(`${err.message}\n${USAGE}`);
143
+ return 2;
144
+ }
145
+ if (parsed.values.help) {
146
+ io.stdout(USAGE);
147
+ return 0;
148
+ }
149
+ const home = io.env.HOME ?? process.env.HOME ?? '';
150
+ const contextDir = global.contextDir ?? io.env.LOOM_CONTEXT_DIR ?? resolveDefaultContextPath(home);
151
+ const { root, agents, defaultStatus } = await probeAgents(home);
152
+ const report = {
153
+ nodeOk: nodeOk(process.version),
154
+ nodeVersion: process.version,
155
+ stackVersionOk: await probeStackVersion(contextDir),
156
+ contextDirResolved: contextDir,
157
+ agentsRoot: root,
158
+ existingAgents: agents,
159
+ defaultStatus,
160
+ defaultOk: defaultIsOk(defaultStatus),
161
+ };
162
+ const json = Boolean(parsed.values.json) || Boolean(global.json);
163
+ if (json) {
164
+ renderJson(io, report);
165
+ return 0;
166
+ }
167
+ const lines = [];
168
+ lines.push(`node: ${report.nodeVersion}${report.nodeOk ? '' : ' (unsupported — need ≥ 20)'}`);
169
+ lines.push(`stack: ${report.stackVersionOk ? 'compatible' : 'incompatible'}`);
170
+ lines.push(`context dir: ${report.contextDirResolved}`);
171
+ lines.push(`agents root: ${report.agentsRoot}`);
172
+ const ds = report.defaultStatus;
173
+ if (!ds.exists) {
174
+ lines.push('default: missing [FAIL — run `loom bootstrap` to create an agent and set up the default pointer]');
175
+ }
176
+ else if (!ds.isSymlink) {
177
+ lines.push('default: real directory (should be a symlink) [FAIL]');
178
+ }
179
+ else if (ds.dangling) {
180
+ lines.push(`default: ${ds.target} (dangling — target does not exist) [FAIL]`);
181
+ }
182
+ else if (ds.empty) {
183
+ lines.push(`default: -> ${ds.target} (no IDENTITY.md) [FAIL]`);
184
+ }
185
+ else {
186
+ lines.push(`default: -> ${ds.target} [ok]`);
187
+ }
188
+ if (report.existingAgents.length === 0) {
189
+ lines.push('agents: (none)');
190
+ }
191
+ else {
192
+ lines.push(`agents: ${report.existingAgents.length}`);
193
+ for (const a of report.existingAgents) {
194
+ const flags = [];
195
+ if (a.hasIdentity)
196
+ flags.push('identity');
197
+ if (a.hasMemoriesDb)
198
+ flags.push('memories.db');
199
+ if (a.git.initialized)
200
+ flags.push('git');
201
+ lines.push(` - ${a.name} (${flags.join(', ') || 'empty'})`);
202
+ }
203
+ }
204
+ io.stdout(lines.join('\n') + '\n');
205
+ return 0;
206
+ }
@@ -0,0 +1,2 @@
1
+ import type { IOStreams } from './io.js';
2
+ export declare function run(argv: string[], io: IOStreams): Promise<number>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * loom forget — remove memories by ref, category+title, or title pattern.
3
+ */
4
+ import { parseArgs } from 'node:util';
5
+ import { forget } from '../tools/forget.js';
6
+ import { createBackend } from '../backends/index.js';
7
+ import { assertStackVersionCompatible } from '../config.js';
8
+ import { extractGlobalFlags, resolveEnv } from './args.js';
9
+ import { renderJson } from './io.js';
10
+ const USAGE = `Usage:
11
+ loom forget <ref>
12
+ loom forget --category <cat> --title <exact>
13
+ loom forget --title-pattern <glob> (--category <cat> | --project <name>)
14
+
15
+ Options:
16
+ --confirm Required for scope deletion (category/project/pattern
17
+ without an exact target). Without it, prints a preview
18
+ of what would be deleted and deletes nothing.
19
+ --json Emit ForgetResult
20
+ --context-dir <path> Agent context dir
21
+ --help, -h Show this help
22
+ `;
23
+ export async function run(argv, io) {
24
+ const { flags: global, rest } = extractGlobalFlags(argv);
25
+ let parsed;
26
+ try {
27
+ parsed = parseArgs({
28
+ args: rest,
29
+ options: {
30
+ category: { type: 'string' },
31
+ title: { type: 'string' },
32
+ project: { type: 'string' },
33
+ 'title-pattern': { type: 'string' },
34
+ confirm: { type: 'boolean' },
35
+ help: { type: 'boolean', short: 'h' },
36
+ },
37
+ strict: true,
38
+ allowPositionals: true,
39
+ });
40
+ }
41
+ catch (err) {
42
+ io.stderr(`${err.message}\n${USAGE}`);
43
+ return 2;
44
+ }
45
+ if (parsed.values.help) {
46
+ io.stdout(USAGE);
47
+ return 0;
48
+ }
49
+ const input = {
50
+ ref: parsed.positionals[0],
51
+ category: parsed.values.category,
52
+ title: parsed.values.title,
53
+ project: parsed.values.project,
54
+ title_pattern: parsed.values['title-pattern'],
55
+ };
56
+ const confirm = parsed.values.confirm === true;
57
+ const isSingleTarget = Boolean(input.ref) || Boolean(input.category && input.title);
58
+ if (input.title_pattern && !input.category && !input.project) {
59
+ io.stderr(`--title-pattern requires --category or --project as a scope guard.\n`);
60
+ return 2;
61
+ }
62
+ const hasAny = input.ref || input.category || input.project || input.title_pattern;
63
+ if (!hasAny) {
64
+ io.stderr(`Nothing to forget.\n${USAGE}`);
65
+ return 2;
66
+ }
67
+ const env = resolveEnv(global, io.env);
68
+ try {
69
+ assertStackVersionCompatible(env.contextDir);
70
+ }
71
+ catch (err) {
72
+ io.stderr(`${err.message}\n`);
73
+ return 1;
74
+ }
75
+ // Scope deletion ceremony holds at every surface (store-convergence
76
+ // §"One guard grammar") — the --json fast path must not bypass the gate.
77
+ if (!isSingleTarget && !confirm) {
78
+ const preview = await forget(env.contextDir, { ...input, confirm: false });
79
+ if (env.json) {
80
+ renderJson(io, { deleted: [], preview, confirmRequired: true });
81
+ }
82
+ else {
83
+ io.stdout(preview.endsWith('\n') ? preview : preview + '\n');
84
+ io.stdout('Re-run with --confirm to delete.\n');
85
+ }
86
+ return 2;
87
+ }
88
+ if (env.json) {
89
+ const backend = createBackend(env.contextDir);
90
+ const result = await backend.forget(input);
91
+ renderJson(io, result);
92
+ return result.deleted.length === 0 ? 3 : 0;
93
+ }
94
+ const text = await forget(env.contextDir, { ...input, confirm });
95
+ io.stdout(text.endsWith('\n') ? text : text + '\n');
96
+ return /not found|No memories matched/i.test(text) ? 3 : 0;
97
+ }
@@ -0,0 +1,2 @@
1
+ import type { IOStreams } from './io.js';
2
+ export declare function run(argv: string[], io: IOStreams): Promise<number>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ /**
3
+ * loom harness — manifest lifecycle for harness adapters.
4
+ *
5
+ * The only subcommand is `init`: writes a manifest template to
6
+ * <contextDir>/harnesses/<name>.md. Reading is implicit — identity()
7
+ * already composes the manifest from disk.
8
+ */
9
+ import { parseArgs } from 'node:util';
10
+ import { assertStackVersionCompatible } from '../config.js';
11
+ import { extractGlobalFlags, resolveEnv } from './args.js';
12
+ import { renderJson } from './io.js';
13
+ import { initHarness } from '../blocks/harness.js';
14
+ const USAGE = `Usage: loom harness <subcommand> [options]
15
+
16
+ Subcommands:
17
+ init [<name>] Write a manifest template for <name>
18
+
19
+ Options (init):
20
+ --force Overwrite existing manifest
21
+ --json Emit InitResult as JSON
22
+
23
+ <name> falls back to --client, then $LOOM_CLIENT.
24
+
25
+ Global: --context-dir, --client, --json, --help
26
+ `;
27
+ export async function run(argv, io) {
28
+ const { flags: global, rest } = extractGlobalFlags(argv);
29
+ const sub = rest[0];
30
+ const subRest = rest.slice(1);
31
+ if (!sub || sub === '--help' || sub === '-h') {
32
+ io.stdout(USAGE);
33
+ return sub ? 0 : 2;
34
+ }
35
+ if (sub !== 'init') {
36
+ io.stderr(`Unknown harness subcommand: ${sub}\n${USAGE}`);
37
+ return 2;
38
+ }
39
+ const env = resolveEnv(global, io.env);
40
+ try {
41
+ assertStackVersionCompatible(env.contextDir);
42
+ }
43
+ catch (err) {
44
+ io.stderr(`${err.message}\n`);
45
+ return 1;
46
+ }
47
+ let parsed;
48
+ try {
49
+ parsed = parseArgs({
50
+ args: subRest,
51
+ options: {
52
+ force: { type: 'boolean' },
53
+ help: { type: 'boolean', short: 'h' },
54
+ },
55
+ strict: true,
56
+ allowPositionals: true,
57
+ });
58
+ }
59
+ catch (err) {
60
+ io.stderr(`${err.message}\n${USAGE}`);
61
+ return 2;
62
+ }
63
+ if (parsed.values.help) {
64
+ io.stdout(USAGE);
65
+ return 0;
66
+ }
67
+ const name = parsed.positionals[0] ?? env.client;
68
+ if (!name) {
69
+ io.stderr('loom harness init: <name> required (or pass --client / set $LOOM_CLIENT)\n');
70
+ return 2;
71
+ }
72
+ try {
73
+ const result = await initHarness(env.contextDir, name, {
74
+ overwrite: parsed.values.force === true,
75
+ });
76
+ if (env.json) {
77
+ renderJson(io, result);
78
+ return 0;
79
+ }
80
+ io.stdout(`${result.name}: ${result.path} (${result.action})\n`);
81
+ return 0;
82
+ }
83
+ catch (err) {
84
+ if (err.message.startsWith('Invalid harness name')) {
85
+ io.stderr(`${err.message}\n`);
86
+ return 2;
87
+ }
88
+ io.stderr(`loom harness init: ${err.message}\n`);
89
+ return 1;
90
+ }
91
+ }
@@ -0,0 +1,2 @@
1
+ import type { IOStreams } from './io.js';
2
+ export declare function runCli(argv: string[], io?: IOStreams): Promise<number>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * CLI dispatcher — top-level --help / --version plus subcommand routing.
3
+ * Individual subcommand files land in Tasks 2–10.
4
+ */
5
+ import { readFile } from 'node:fs/promises';
6
+ import { join } from 'node:path';
7
+ import { resolveRepoRoot } from '../config.js';
8
+ import { realStreams } from './io.js';
9
+ import { SUBCOMMANDS } from './subcommands.js';
10
+ const TOP_HELP = `Usage: loom <command> [options]
11
+
12
+ Commands:
13
+ wake Print agent identity to stdout
14
+ recall <query> Search memories
15
+ remember <title> Save a new memory (body via stdin/$EDITOR)
16
+ update <ref> Modify an existing memory
17
+ forget <ref|scope> Remove memories
18
+ memory list|prune|similar|audit
19
+ Browse / clean / find duplicates / health report
20
+ knowledge write|recall|maintain
21
+ Write / search / health-report the knowledge store
22
+ update-identity Edit preferences.md / self-model.md sections
23
+ bootstrap Initialize a fresh agent
24
+ serve Explicit MCP stdio startup (same as no args)
25
+ inject Write loom identity pointer into harness dotfiles
26
+ harness init Scaffold a harness manifest from template
27
+ install Write loom-setup skill into a harness's skills dir
28
+ doctor Probe the loom environment (read-only)
29
+ migrate Apply pending schema migrations to memories.db
30
+
31
+ Global flags:
32
+ --context-dir <path> Agent context dir (default: $LOOM_CONTEXT_DIR or ~/.config/loom/default)
33
+ --client <name> Harness adapter hint (default: $LOOM_CLIENT)
34
+ --model <name> Model manifest hint (default: $LOOM_MODEL)
35
+ --json Machine-readable output
36
+ --help, -h Show help
37
+ --version, -V Print loom version
38
+
39
+ Run 'loom <command> --help' for per-command usage.
40
+ `;
41
+ async function readVersion() {
42
+ const pkg = JSON.parse(await readFile(join(resolveRepoRoot(), 'package.json'), 'utf-8'));
43
+ return pkg.version;
44
+ }
45
+ export async function runCli(argv, io = realStreams()) {
46
+ const first = argv[0];
47
+ if (first === '--help' || first === '-h' || first === undefined) {
48
+ io.stdout(TOP_HELP);
49
+ return 0;
50
+ }
51
+ if (first === '--version' || first === '-V') {
52
+ io.stdout(`loom v${await readVersion()}\n`);
53
+ return 0;
54
+ }
55
+ if (!SUBCOMMANDS.includes(first)) {
56
+ io.stderr(`Unknown subcommand: ${first}\n`);
57
+ io.stderr(TOP_HELP);
58
+ return 2;
59
+ }
60
+ const sub = first;
61
+ const rest = argv.slice(1);
62
+ switch (sub) {
63
+ case 'wake': {
64
+ const { run } = await import('./wake.js');
65
+ return run(rest, io);
66
+ }
67
+ case 'recall': {
68
+ const { run } = await import('./recall.js');
69
+ return run(rest, io);
70
+ }
71
+ case 'memory': {
72
+ const { run } = await import('./memory.js');
73
+ return run(rest, io);
74
+ }
75
+ case 'knowledge': {
76
+ const { run } = await import('./knowledge.js');
77
+ return run(rest, io);
78
+ }
79
+ case 'forget': {
80
+ const { run } = await import('./forget.js');
81
+ return run(rest, io);
82
+ }
83
+ case 'remember': {
84
+ const { run } = await import('./remember.js');
85
+ return run(rest, io);
86
+ }
87
+ case 'update': {
88
+ const { run } = await import('./update.js');
89
+ return run(rest, io);
90
+ }
91
+ case 'bootstrap': {
92
+ const { run } = await import('./bootstrap.js');
93
+ return run(rest, io);
94
+ }
95
+ case 'serve': {
96
+ const { run } = await import('./serve.js');
97
+ return run(rest, io);
98
+ }
99
+ case 'update-identity': {
100
+ const { run } = await import('./update-identity.js');
101
+ return run(rest, io);
102
+ }
103
+ case 'inject': {
104
+ const { run } = await import('./inject.js');
105
+ return run(rest, io);
106
+ }
107
+ case 'harness': {
108
+ const { run } = await import('./harness.js');
109
+ return run(rest, io);
110
+ }
111
+ case 'install': {
112
+ const { run } = await import('./install.js');
113
+ return run(rest, io);
114
+ }
115
+ case 'doctor': {
116
+ const { run } = await import('./doctor.js');
117
+ return run(rest, io);
118
+ }
119
+ case 'migrate': {
120
+ const { run } = await import('./migrate.js');
121
+ return run(rest, io);
122
+ }
123
+ default:
124
+ io.stderr(`Subcommand not implemented yet: ${sub}\n`);
125
+ return 2;
126
+ }
127
+ }
@@ -0,0 +1,2 @@
1
+ import type { IOStreams } from './io.js';
2
+ export declare function run(argv: string[], io: IOStreams): Promise<number>;