@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,142 @@
1
+ /**
2
+ * Harness manifest reader.
3
+ *
4
+ * Each harness an agent has ever sleeved into gets one manifest at
5
+ * `<contextDir>/harnesses/<client>.md`. The manifest describes the
6
+ * harness independently of the model running inside it — tool prefixes,
7
+ * delegation primitive, scheduling, session search, known gotchas.
8
+ *
9
+ * Contract: stack spec v1 §4.7.
10
+ */
11
+ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
12
+ import { existsSync } from 'node:fs';
13
+ import { resolve } from 'node:path';
14
+ import { parseFrontmatter } from './types.js';
15
+ import { assertSafePathSegment } from '../path-safety.js';
16
+ const DIR = 'harnesses';
17
+ export async function read(contextDir, key) {
18
+ assertSafePathSegment(key, 'harness name');
19
+ const path = resolve(contextDir, DIR, `${key}.md`);
20
+ if (!existsSync(path))
21
+ return null;
22
+ const raw = await readFile(path, 'utf-8');
23
+ if (raw.trim() === '')
24
+ return null;
25
+ const { frontmatter, body } = parseFrontmatter(raw);
26
+ return { key, frontmatter, body: body.trim(), path };
27
+ }
28
+ export async function list(contextDir) {
29
+ const path = resolve(contextDir, DIR);
30
+ if (!existsSync(path))
31
+ return [];
32
+ const entries = await readdir(path);
33
+ return entries
34
+ .filter((name) => name.endsWith('.md'))
35
+ .map((name) => name.slice(0, -'.md'.length))
36
+ .sort();
37
+ }
38
+ /**
39
+ * Normalize an MCP peer's clientInfo.name to a harness-filename-shaped key.
40
+ *
41
+ * Proxies annotate the name with a trailing parenthetical, e.g.
42
+ * "claude-ai (via mcp-remote 0.1.37)" — strip that first, then lowercase and
43
+ * collapse spaces/underscores to hyphens so "Claude Desktop" and "claude_desktop"
44
+ * both land on "claude-desktop".
45
+ */
46
+ export function normalizePeer(peerName) {
47
+ const base = peerName.split(/\s*\(/, 1)[0];
48
+ return base.trim().toLowerCase().replace(/[\s_]+/g, '-');
49
+ }
50
+ /**
51
+ * Resolve a connected MCP peer to a harness manifest key, DATA-DRIVEN from the
52
+ * manifest files on disk (not a hardcoded code map). A harness is recognized
53
+ * when the normalized peer matches either the manifest's filename OR one of the
54
+ * comma-separated values in its `answersTo` frontmatter (each normalized the
55
+ * same way). A brand-new harness is recognized by dropping a file — no code
56
+ * change. Returns undefined when nothing matches.
57
+ */
58
+ export async function resolvePeerToHarness(contextDir, peerName) {
59
+ if (!peerName)
60
+ return undefined;
61
+ const normalized = normalizePeer(peerName);
62
+ if (!normalized)
63
+ return undefined;
64
+ const keys = await list(contextDir);
65
+ // Filename match is the cheapest and most direct signal.
66
+ if (keys.includes(normalized))
67
+ return normalized;
68
+ // Otherwise scan each manifest's answersTo frontmatter for the peer.
69
+ for (const key of keys) {
70
+ const block = await read(contextDir, key);
71
+ if (!block)
72
+ continue;
73
+ const answersTo = block.frontmatter.answersTo;
74
+ if (!answersTo)
75
+ continue;
76
+ const aliases = answersTo
77
+ .split(',')
78
+ .map((alias) => normalizePeer(alias))
79
+ .filter(Boolean);
80
+ if (aliases.includes(normalized))
81
+ return key;
82
+ }
83
+ return undefined;
84
+ }
85
+ /**
86
+ * Write (or overwrite) a harness manifest from a connected harness describing
87
+ * itself. Lands strictly under `<contextDir>/harnesses/<targetKey>.md` with
88
+ * stamped frontmatter (harness/version/updated) followed by the provided body.
89
+ * Re-runnable — overwrites. assertSafePathSegment guards the key so the creed
90
+ * and other tiers are structurally unreachable.
91
+ */
92
+ export async function describeHarness(contextDir, targetKey, content, opts = {}) {
93
+ assertSafePathSegment(targetKey, 'harness name');
94
+ const dir = resolve(contextDir, DIR);
95
+ await mkdir(dir, { recursive: true });
96
+ const path = resolve(dir, `${targetKey}.md`);
97
+ const exists = existsSync(path);
98
+ const version = opts.version ?? '0.1';
99
+ const body = content.trim();
100
+ const manifest = `---\n` +
101
+ `harness: ${targetKey}\n` +
102
+ `version: ${version}\n` +
103
+ `updated: ${new Date().toISOString()}\n` +
104
+ `---\n\n` +
105
+ `${body}\n`;
106
+ await writeFile(path, manifest, 'utf-8');
107
+ return { key: targetKey, path, action: exists ? 'overwritten' : 'created' };
108
+ }
109
+ export function template(key) {
110
+ return `---
111
+ harness: ${key}
112
+ version: 0.4
113
+ ---
114
+
115
+ ## Tool prefixes
116
+ <tool-prefix list — see stack spec §4.7>
117
+
118
+ ## Delegation primitive
119
+ <primary sub-agent mechanism>
120
+
121
+ ## Cron / scheduling
122
+ <scheduling primitive if any, and local-vs-UTC note>
123
+
124
+ ## Session search
125
+ <how transcripts are searched>
126
+
127
+ ## Gotchas
128
+ <known quirks>
129
+ `;
130
+ }
131
+ export async function initHarness(contextDir, name, opts = {}) {
132
+ assertSafePathSegment(name, 'harness name');
133
+ const dir = resolve(contextDir, DIR);
134
+ await mkdir(dir, { recursive: true });
135
+ const path = resolve(dir, `${name}.md`);
136
+ const exists = existsSync(path);
137
+ if (exists && !opts.overwrite) {
138
+ return { name, path, action: 'skipped-exists' };
139
+ }
140
+ await writeFile(path, template(name), 'utf-8');
141
+ return { name, path, action: exists ? 'overwritten' : 'created' };
142
+ }
@@ -0,0 +1,4 @@
1
+ import { type Block } from './types.js';
2
+ export declare function read(contextDir: string, key: string): Promise<Block | null>;
3
+ export declare function list(contextDir: string): Promise<string[]>;
4
+ export declare function template(key: string): string;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Model manifest reader.
3
+ *
4
+ * Each model family the agent has ever sleeved into gets one manifest at
5
+ * `<contextDir>/models/<key>.md`. Describes capability notes, workarounds,
6
+ * and when-to-use / when-not-to-use guidance — independent of harness.
7
+ *
8
+ * Contract: stack spec v1 §4.8.
9
+ */
10
+ import { readFile, readdir } from 'node:fs/promises';
11
+ import { existsSync } from 'node:fs';
12
+ import { resolve } from 'node:path';
13
+ import { parseFrontmatter } from './types.js';
14
+ import { assertSafePathSegment } from '../path-safety.js';
15
+ const DIR = 'models';
16
+ export async function read(contextDir, key) {
17
+ assertSafePathSegment(key, 'model name');
18
+ const path = resolve(contextDir, DIR, `${key}.md`);
19
+ if (!existsSync(path))
20
+ return null;
21
+ const raw = await readFile(path, 'utf-8');
22
+ if (raw.trim() === '')
23
+ return null;
24
+ const { frontmatter, body } = parseFrontmatter(raw);
25
+ return { key, frontmatter, body: body.trim(), path };
26
+ }
27
+ export async function list(contextDir) {
28
+ const path = resolve(contextDir, DIR);
29
+ if (!existsSync(path))
30
+ return [];
31
+ const entries = await readdir(path);
32
+ return entries
33
+ .filter((name) => name.endsWith('.md'))
34
+ .map((name) => name.slice(0, -'.md'.length))
35
+ .sort();
36
+ }
37
+ export function template(key) {
38
+ return `---
39
+ model: ${key}
40
+ family: <family name>
41
+ size: <size hint if meaningful>
42
+ ---
43
+
44
+ ## Capability notes
45
+ <strengths and weaknesses that matter operationally>
46
+
47
+ ## Workarounds
48
+ <known-good compensation patterns — or "None required.">
49
+
50
+ ## When to use
51
+ <concrete situations this model is the right choice>
52
+
53
+ ## When not to use
54
+ <concrete situations where a different model is better>
55
+ `;
56
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared block types for loom stack blocks.
3
+ *
4
+ * A "block" is a markdown file in the stack that an adapter loads during
5
+ * the wake sequence — a harness manifest or a model manifest.
6
+ * This module defines the common shape and a tiny frontmatter parser.
7
+ * We intentionally avoid a YAML dep: frontmatter here is key: value lines,
8
+ * small and strict.
9
+ */
10
+ /** A single block read from disk. */
11
+ export interface Block {
12
+ /** Filename without `.md`. Also the harness / model name. */
13
+ key: string;
14
+ /** Parsed frontmatter as flat key→value. Empty object when absent or malformed. */
15
+ frontmatter: Record<string, string>;
16
+ /** Markdown body after the frontmatter fences. Not trimmed — callers trim as needed. */
17
+ body: string;
18
+ /** Absolute path this block was read from. */
19
+ path: string;
20
+ }
21
+ /** The common reader surface harness and model both implement. */
22
+ export interface BlockReader {
23
+ /** Read a single block by key. Returns null when the file is missing or empty. */
24
+ read(contextDir: string, key: string): Promise<Block | null>;
25
+ /** Sorted list of keys present in this block's directory. */
26
+ list(contextDir: string): Promise<string[]>;
27
+ /** A blank template for this block type, parameterized by key. */
28
+ template(key: string): string;
29
+ }
30
+ /**
31
+ * Split `---` frontmatter from the markdown body. Missing or malformed
32
+ * fences yield `{}` frontmatter + the original text as body. Individual
33
+ * lines that aren't `key: value` are ignored.
34
+ *
35
+ * Normalizes CRLF → LF before parsing so files authored on Windows parse correctly.
36
+ */
37
+ export declare function parseFrontmatter(text: string): {
38
+ frontmatter: Record<string, string>;
39
+ body: string;
40
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared block types for loom stack blocks.
3
+ *
4
+ * A "block" is a markdown file in the stack that an adapter loads during
5
+ * the wake sequence — a harness manifest or a model manifest.
6
+ * This module defines the common shape and a tiny frontmatter parser.
7
+ * We intentionally avoid a YAML dep: frontmatter here is key: value lines,
8
+ * small and strict.
9
+ */
10
+ /**
11
+ * Split `---` frontmatter from the markdown body. Missing or malformed
12
+ * fences yield `{}` frontmatter + the original text as body. Individual
13
+ * lines that aren't `key: value` are ignored.
14
+ *
15
+ * Normalizes CRLF → LF before parsing so files authored on Windows parse correctly.
16
+ */
17
+ export function parseFrontmatter(text) {
18
+ // Normalize CRLF → LF so the regex works cross-platform.
19
+ const normalized = text.replace(/\r\n/g, '\n');
20
+ const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
21
+ if (!match)
22
+ return { frontmatter: {}, body: text };
23
+ const frontmatter = {};
24
+ for (const line of match[1].split('\n')) {
25
+ const colon = line.indexOf(':');
26
+ if (colon === -1)
27
+ continue;
28
+ const key = line.slice(0, colon).trim();
29
+ const value = line.slice(colon + 1).trim();
30
+ if (key)
31
+ frontmatter[key] = value;
32
+ }
33
+ return { frontmatter, body: match[2] };
34
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared memory category vocabulary — the single source of truth for
3
+ * write-side category enforcement across MCP schemas and the CLI
4
+ * (store-convergence spec, "One boundary discipline").
5
+ *
6
+ * Reads (recall / memory_list / forget) intentionally accept arbitrary
7
+ * strings so memories written before the vocabulary existed (e.g.
8
+ * legacy "general") stay reachable. Only writes are enforced.
9
+ */
10
+ export declare const MEMORY_CATEGORIES: readonly ['user', 'project', 'self', 'feedback', 'reference', 'pursuit', 'episode'];
11
+ /** The category that carries the short-term cross-body tape. */
12
+ export declare const EPISODE_CATEGORY = "episode";
13
+ /** Episodes expire by default — they are a tape, not the stack. */
14
+ export declare const EPISODE_DEFAULT_TTL = "48h";
15
+ export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number];
16
+ export declare function isMemoryCategory(value: string): value is MemoryCategory;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared memory category vocabulary — the single source of truth for
3
+ * write-side category enforcement across MCP schemas and the CLI
4
+ * (store-convergence spec, "One boundary discipline").
5
+ *
6
+ * Reads (recall / memory_list / forget) intentionally accept arbitrary
7
+ * strings so memories written before the vocabulary existed (e.g.
8
+ * legacy "general") stay reachable. Only writes are enforced.
9
+ */
10
+ export const MEMORY_CATEGORIES = [
11
+ 'user',
12
+ 'project',
13
+ 'self',
14
+ 'feedback',
15
+ 'reference',
16
+ 'pursuit',
17
+ // Short-term cross-body tier (t-142): a 3-line "where / what / open" note a
18
+ // sleeve leaves so the NEXT sleeve knows what just happened across all of
19
+ // Art. Defaults to a 48h TTL on write; injected at boot as a time-ordered
20
+ // tape (never salience-ranked) and excluded from the Top-of-Mind digest.
21
+ 'episode',
22
+ ];
23
+ /** The category that carries the short-term cross-body tape. */
24
+ export const EPISODE_CATEGORY = 'episode';
25
+ /** Episodes expire by default — they are a tape, not the stack. */
26
+ export const EPISODE_DEFAULT_TTL = '48h';
27
+ export function isMemoryCategory(value) {
28
+ return MEMORY_CATEGORIES.includes(value);
29
+ }
@@ -0,0 +1,21 @@
1
+ export interface ResolvedEnv {
2
+ contextDir: string;
3
+ client?: string;
4
+ model?: string;
5
+ json: boolean;
6
+ }
7
+ export interface RawGlobalFlags {
8
+ contextDir?: string;
9
+ client?: string;
10
+ model?: string;
11
+ json?: boolean;
12
+ }
13
+ export declare function resolveEnv(flags: RawGlobalFlags, processEnv: NodeJS.ProcessEnv): ResolvedEnv;
14
+ /**
15
+ * Extracts global flags from an argv slice, returning the remaining argv.
16
+ * Recognizes: --context-dir, --client, --model, --json.
17
+ */
18
+ export declare function extractGlobalFlags(argv: string[]): {
19
+ flags: RawGlobalFlags;
20
+ rest: string[];
21
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared argv helpers — global flag resolution + context-dir/env
3
+ * precedence. Individual commands parse their own subcommand flags
4
+ * via node:util parseArgs.
5
+ */
6
+ import { resolve } from 'node:path';
7
+ import { resolveDefaultContextPath } from '../config.js';
8
+ export function resolveEnv(flags, processEnv) {
9
+ const contextDir = flags.contextDir ??
10
+ processEnv.LOOM_CONTEXT_DIR ??
11
+ resolveDefaultContextPath(processEnv.HOME);
12
+ return {
13
+ contextDir: resolve(contextDir),
14
+ client: flags.client ?? processEnv.LOOM_CLIENT,
15
+ model: flags.model ?? processEnv.LOOM_MODEL,
16
+ json: Boolean(flags.json),
17
+ };
18
+ }
19
+ /**
20
+ * Extracts global flags from an argv slice, returning the remaining argv.
21
+ * Recognizes: --context-dir, --client, --model, --json.
22
+ */
23
+ export function extractGlobalFlags(argv) {
24
+ const flags = {};
25
+ const rest = [];
26
+ for (let i = 0; i < argv.length; i++) {
27
+ const a = argv[i];
28
+ if (a === '--context-dir') {
29
+ flags.contextDir = argv[++i];
30
+ continue;
31
+ }
32
+ if (a === '--client') {
33
+ flags.client = argv[++i];
34
+ continue;
35
+ }
36
+ if (a === '--model') {
37
+ flags.model = argv[++i];
38
+ continue;
39
+ }
40
+ if (a === '--json') {
41
+ flags.json = true;
42
+ continue;
43
+ }
44
+ rest.push(a);
45
+ }
46
+ return { flags, rest };
47
+ }
@@ -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,214 @@
1
+ /**
2
+ * loom bootstrap — initialize a fresh agent.
3
+ *
4
+ * Param sources (precedence):
5
+ * 1. flags (--user, --name, --purpose, --voice, --preferences, --clients)
6
+ * 2. piped JSON on stdin
7
+ * 3. interactive readline prompts (only when stdin is a TTY)
8
+ */
9
+ import { parseArgs } from 'node:util';
10
+ import { join, dirname } from 'node:path';
11
+ import { createInterface } from 'node:readline/promises';
12
+ import { lstat, symlink } from 'node:fs/promises';
13
+ import { bootstrap } from '../tools/bootstrap.js';
14
+ import { assertStackVersionCompatible, resolveDefaultContextPath } from '../config.js';
15
+ import { extractGlobalFlags, resolveEnv } from './args.js';
16
+ import { readStdin, renderJson, stderrWritable } from './io.js';
17
+ import { validateAgentName } from '../install/names.js';
18
+ const USAGE = `Usage: loom bootstrap [options]
19
+
20
+ Initializes IDENTITY.md, preferences.md, and self-model.md
21
+ in the context directory.
22
+
23
+ Param sources (first match wins):
24
+ 1. Flags (--name / --purpose / --voice)
25
+ 2. Piped JSON on stdin: {"name","purpose","voice","user"?,"clients"?}
26
+ 3. Interactive prompts when stdin is a TTY and nothing else is set
27
+
28
+ Options:
29
+ --user <str> The human this agent works with (their name)
30
+ --name <str> Agent name (required)
31
+ --purpose <str> One-line purpose
32
+ --voice <str> Short voice descriptor
33
+ --preferences <str> Optional preferences preamble
34
+ --clients <csv> Comma-separated client adapters (e.g. claude-code)
35
+ --force Overwrite an existing IDENTITY.md
36
+ --json Emit {contextDir, wrote: string[]}
37
+ --context-dir <path> Agent context dir
38
+ --help, -h Show this help
39
+ `;
40
+ function checkName(name, io) {
41
+ const r = validateAgentName(name);
42
+ if (r.ok)
43
+ return null;
44
+ io.stderr(`Invalid --name: ${r.reason}\n`);
45
+ return 2;
46
+ }
47
+ async function promptInteractive(io) {
48
+ const rl = createInterface({ input: io.stdin, output: stderrWritable(io) });
49
+ // Race each question against the readline 'close' event (fires when stdin
50
+ // ends before the user answers — e.g. piped /dev/null or empty test streams).
51
+ let closed = false;
52
+ const closedPromise = new Promise((resolve) => {
53
+ rl.once('close', () => { closed = true; resolve(null); });
54
+ });
55
+ const ask = (q) => Promise.race([
56
+ rl.question(q).then((s) => s),
57
+ closedPromise,
58
+ ]);
59
+ try {
60
+ if (closed)
61
+ return null;
62
+ const user = ((await ask('Your name (the human): ')) ?? '').trim();
63
+ if (closed)
64
+ return null;
65
+ const name = ((await ask('Agent name: ')) ?? '').trim();
66
+ if (!name)
67
+ return null;
68
+ const purpose = ((await ask('Purpose (one line): ')) ?? '').trim();
69
+ if (!purpose)
70
+ return null;
71
+ const voice = ((await ask('Voice (short descriptor): ')) ?? '').trim();
72
+ if (!voice)
73
+ return null;
74
+ const clientsRaw = ((await ask('Clients (comma-separated, e.g. claude-code): ')) ?? '').trim();
75
+ const clients = clientsRaw ? clientsRaw.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
76
+ return { user: user || undefined, name, purpose, voice, clients };
77
+ }
78
+ finally {
79
+ rl.close();
80
+ }
81
+ }
82
+ export async function run(argv, io) {
83
+ const { flags: global, rest } = extractGlobalFlags(argv);
84
+ let parsed;
85
+ try {
86
+ parsed = parseArgs({
87
+ args: rest,
88
+ options: {
89
+ user: { type: 'string' },
90
+ name: { type: 'string' },
91
+ purpose: { type: 'string' },
92
+ voice: { type: 'string' },
93
+ preferences: { type: 'string' },
94
+ clients: { type: 'string' },
95
+ force: { type: 'boolean' },
96
+ help: { type: 'boolean', short: 'h' },
97
+ },
98
+ strict: true,
99
+ allowPositionals: false,
100
+ });
101
+ }
102
+ catch (err) {
103
+ io.stderr(`${err.message}\n${USAGE}`);
104
+ return 2;
105
+ }
106
+ if (parsed.values.help) {
107
+ io.stdout(USAGE);
108
+ return 0;
109
+ }
110
+ const env = resolveEnv(global, io.env);
111
+ try {
112
+ assertStackVersionCompatible(env.contextDir);
113
+ }
114
+ catch (err) {
115
+ io.stderr(`${err.message}\n`);
116
+ return 1;
117
+ }
118
+ let params = null;
119
+ if (parsed.values.name || parsed.values.purpose || parsed.values.voice || parsed.values.user) {
120
+ if (!parsed.values.name || !parsed.values.purpose || !parsed.values.voice) {
121
+ io.stderr(`When using flags, --name, --purpose, and --voice are all required.\n`);
122
+ return 2;
123
+ }
124
+ const clientsCsv = parsed.values.clients;
125
+ params = {
126
+ user: parsed.values.user,
127
+ name: parsed.values.name,
128
+ purpose: parsed.values.purpose,
129
+ voice: parsed.values.voice,
130
+ preferences: parsed.values.preferences,
131
+ clients: clientsCsv ? clientsCsv.split(',').map((s) => s.trim()).filter(Boolean) : undefined,
132
+ force: Boolean(parsed.values.force),
133
+ };
134
+ }
135
+ else if (!io.stdinIsTTY) {
136
+ const raw = await readStdin(io.stdin);
137
+ const trimmed = raw.trim();
138
+ if (!trimmed) {
139
+ io.stderr(`No bootstrap params supplied (no flags, no stdin, no TTY).\n${USAGE}`);
140
+ return 2;
141
+ }
142
+ try {
143
+ const body = JSON.parse(trimmed);
144
+ if (!body.name || !body.purpose || !body.voice) {
145
+ io.stderr(`Piped JSON must include name, purpose, and voice.\n`);
146
+ return 2;
147
+ }
148
+ params = {
149
+ user: body.user,
150
+ name: body.name, purpose: body.purpose, voice: body.voice,
151
+ preferences: body.preferences, clients: body.clients,
152
+ force: Boolean(parsed.values.force),
153
+ };
154
+ }
155
+ catch (err) {
156
+ io.stderr(`Could not parse stdin as JSON: ${err.message}\n`);
157
+ return 2;
158
+ }
159
+ }
160
+ else {
161
+ const prompted = await promptInteractive(io);
162
+ if (!prompted || !prompted.name || !prompted.purpose || !prompted.voice) {
163
+ io.stderr(`name, purpose, and voice are all required.\n`);
164
+ return 2;
165
+ }
166
+ params = { ...prompted, force: Boolean(parsed.values.force) };
167
+ }
168
+ const nameCode = checkName(params.name, io);
169
+ if (nameCode !== null)
170
+ return nameCode;
171
+ try {
172
+ // params is always set by this point — all null paths return early above.
173
+ const text = await bootstrap(env.contextDir, params);
174
+ // Provision the default symlink if the new agent dir is directly under
175
+ // the loom config root and no default pointer exists yet.
176
+ const home = io.env.HOME ?? process.env.HOME;
177
+ const defaultLink = resolveDefaultContextPath(home);
178
+ if (dirname(env.contextDir) === dirname(defaultLink) && env.contextDir !== defaultLink) {
179
+ try {
180
+ await lstat(defaultLink); // throws if default doesn't exist at all
181
+ }
182
+ catch {
183
+ // default doesn't exist — create symlink
184
+ try {
185
+ await symlink(env.contextDir, defaultLink);
186
+ if (!env.json) {
187
+ io.stdout(`Created default pointer: ${defaultLink} -> ${env.contextDir}\n`);
188
+ }
189
+ }
190
+ catch {
191
+ // Non-fatal: symlink creation may fail in sandboxed or read-only environments
192
+ }
193
+ }
194
+ }
195
+ if (env.json) {
196
+ renderJson(io, {
197
+ contextDir: env.contextDir,
198
+ wrote: [
199
+ join(env.contextDir, 'IDENTITY.md'),
200
+ join(env.contextDir, 'preferences.md'),
201
+ join(env.contextDir, 'self-model.md'),
202
+ ],
203
+ });
204
+ }
205
+ else {
206
+ io.stdout(text.endsWith('\n') ? text : text + '\n');
207
+ }
208
+ return 0;
209
+ }
210
+ catch (err) {
211
+ io.stderr(`${err.message}\n`);
212
+ return 1;
213
+ }
214
+ }
@@ -0,0 +1,2 @@
1
+ import type { IOStreams } from './io.js';
2
+ export declare function run(argv: string[], io: IOStreams): Promise<number>;