@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.
- package/CHANGELOG.md +475 -0
- package/LICENSE +661 -0
- package/README.md +702 -0
- package/assets/demo.cast +396 -0
- package/assets/skill/SKILL.md +192 -0
- package/dist/backends/embedding-runtime.d.ts +39 -0
- package/dist/backends/embedding-runtime.js +265 -0
- package/dist/backends/episodes.d.ts +36 -0
- package/dist/backends/episodes.js +126 -0
- package/dist/backends/fastembed.d.ts +18 -0
- package/dist/backends/fastembed.js +95 -0
- package/dist/backends/glob.d.ts +10 -0
- package/dist/backends/glob.js +16 -0
- package/dist/backends/index.d.ts +6 -0
- package/dist/backends/index.js +78 -0
- package/dist/backends/migrations.d.ts +38 -0
- package/dist/backends/migrations.js +157 -0
- package/dist/backends/mmr.d.ts +45 -0
- package/dist/backends/mmr.js +82 -0
- package/dist/backends/proposals.d.ts +65 -0
- package/dist/backends/proposals.js +182 -0
- package/dist/backends/recall-log.d.ts +42 -0
- package/dist/backends/recall-log.js +143 -0
- package/dist/backends/retry.d.ts +29 -0
- package/dist/backends/retry.js +60 -0
- package/dist/backends/salience.d.ts +57 -0
- package/dist/backends/salience.js +186 -0
- package/dist/backends/sqlite-knowledge.d.ts +37 -0
- package/dist/backends/sqlite-knowledge.js +788 -0
- package/dist/backends/sqlite-vec.d.ts +64 -0
- package/dist/backends/sqlite-vec.js +737 -0
- package/dist/backends/ttl.d.ts +24 -0
- package/dist/backends/ttl.js +50 -0
- package/dist/backends/types.d.ts +562 -0
- package/dist/backends/types.js +8 -0
- package/dist/blocks/harness.d.ts +45 -0
- package/dist/blocks/harness.js +142 -0
- package/dist/blocks/model.d.ts +4 -0
- package/dist/blocks/model.js +56 -0
- package/dist/blocks/types.d.ts +40 -0
- package/dist/blocks/types.js +34 -0
- package/dist/categories.d.ts +16 -0
- package/dist/categories.js +29 -0
- package/dist/cli/args.d.ts +21 -0
- package/dist/cli/args.js +47 -0
- package/dist/cli/bootstrap.d.ts +2 -0
- package/dist/cli/bootstrap.js +214 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +206 -0
- package/dist/cli/forget.d.ts +2 -0
- package/dist/cli/forget.js +97 -0
- package/dist/cli/harness.d.ts +2 -0
- package/dist/cli/harness.js +91 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +127 -0
- package/dist/cli/inject.d.ts +2 -0
- package/dist/cli/inject.js +298 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.js +137 -0
- package/dist/cli/io.d.ts +14 -0
- package/dist/cli/io.js +63 -0
- package/dist/cli/knowledge.d.ts +2 -0
- package/dist/cli/knowledge.js +216 -0
- package/dist/cli/memory.d.ts +2 -0
- package/dist/cli/memory.js +608 -0
- package/dist/cli/migrate.d.ts +2 -0
- package/dist/cli/migrate.js +116 -0
- package/dist/cli/recall.d.ts +2 -0
- package/dist/cli/recall.js +91 -0
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +111 -0
- package/dist/cli/serve.d.ts +2 -0
- package/dist/cli/serve.js +71 -0
- package/dist/cli/subcommands.d.ts +7 -0
- package/dist/cli/subcommands.js +11 -0
- package/dist/cli/tui/multi-select.d.ts +53 -0
- package/dist/cli/tui/multi-select.js +116 -0
- package/dist/cli/update-identity.d.ts +2 -0
- package/dist/cli/update-identity.js +90 -0
- package/dist/cli/update.d.ts +2 -0
- package/dist/cli/update.js +87 -0
- package/dist/cli/wake.d.ts +2 -0
- package/dist/cli/wake.js +53 -0
- package/dist/clients.d.ts +8 -0
- package/dist/clients.js +63 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +160 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +75 -0
- package/dist/injection/harnesses.d.ts +17 -0
- package/dist/injection/harnesses.js +51 -0
- package/dist/injection/render.d.ts +17 -0
- package/dist/injection/render.js +50 -0
- package/dist/injection/writer.d.ts +25 -0
- package/dist/injection/writer.js +118 -0
- package/dist/install/harnesses.d.ts +26 -0
- package/dist/install/harnesses.js +90 -0
- package/dist/install/names.d.ts +16 -0
- package/dist/install/names.js +36 -0
- package/dist/install/render.d.ts +18 -0
- package/dist/install/render.js +42 -0
- package/dist/install/skill-source.d.ts +1 -0
- package/dist/install/skill-source.js +11 -0
- package/dist/path-safety.d.ts +19 -0
- package/dist/path-safety.js +62 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +571 -0
- package/dist/tools/append-maintenance-log.d.ts +52 -0
- package/dist/tools/append-maintenance-log.js +103 -0
- package/dist/tools/archive.d.ts +2 -0
- package/dist/tools/archive.js +21 -0
- package/dist/tools/bootstrap.d.ts +11 -0
- package/dist/tools/bootstrap.js +161 -0
- package/dist/tools/dossier.d.ts +1 -0
- package/dist/tools/dossier.js +102 -0
- package/dist/tools/eurorack-import.d.ts +47 -0
- package/dist/tools/eurorack-import.js +199 -0
- package/dist/tools/find-similar.d.ts +3 -0
- package/dist/tools/find-similar.js +26 -0
- package/dist/tools/forget.d.ts +6 -0
- package/dist/tools/forget.js +70 -0
- package/dist/tools/harness.d.ts +18 -0
- package/dist/tools/harness.js +54 -0
- package/dist/tools/identity-scaffold.d.ts +22 -0
- package/dist/tools/identity-scaffold.js +142 -0
- package/dist/tools/identity.d.ts +4 -0
- package/dist/tools/identity.js +226 -0
- package/dist/tools/knowledge-archive.d.ts +5 -0
- package/dist/tools/knowledge-archive.js +24 -0
- package/dist/tools/knowledge-history.d.ts +6 -0
- package/dist/tools/knowledge-history.js +55 -0
- package/dist/tools/knowledge-maintain.d.ts +9 -0
- package/dist/tools/knowledge-maintain.js +113 -0
- package/dist/tools/knowledge-merge.d.ts +8 -0
- package/dist/tools/knowledge-merge.js +40 -0
- package/dist/tools/knowledge-move.d.ts +10 -0
- package/dist/tools/knowledge-move.js +48 -0
- package/dist/tools/knowledge-purge.d.ts +5 -0
- package/dist/tools/knowledge-purge.js +20 -0
- package/dist/tools/knowledge-recall.d.ts +20 -0
- package/dist/tools/knowledge-recall.js +142 -0
- package/dist/tools/knowledge-restore.d.ts +4 -0
- package/dist/tools/knowledge-restore.js +24 -0
- package/dist/tools/knowledge-supersede.d.ts +6 -0
- package/dist/tools/knowledge-supersede.js +36 -0
- package/dist/tools/knowledge-verify.d.ts +8 -0
- package/dist/tools/knowledge-verify.js +39 -0
- package/dist/tools/knowledge-write.d.ts +25 -0
- package/dist/tools/knowledge-write.js +87 -0
- package/dist/tools/memory-audit.d.ts +3 -0
- package/dist/tools/memory-audit.js +47 -0
- package/dist/tools/memory-list.d.ts +4 -0
- package/dist/tools/memory-list.js +23 -0
- package/dist/tools/prune.d.ts +4 -0
- package/dist/tools/prune.js +28 -0
- package/dist/tools/recall.d.ts +4 -0
- package/dist/tools/recall.js +24 -0
- package/dist/tools/remember.d.ts +10 -0
- package/dist/tools/remember.js +43 -0
- package/dist/tools/restore.d.ts +2 -0
- package/dist/tools/restore.js +20 -0
- package/dist/tools/update-identity.d.ts +34 -0
- package/dist/tools/update-identity.js +194 -0
- package/dist/tools/update.d.ts +2 -0
- package/dist/tools/update.js +16 -0
- package/dist/transport/guards.d.ts +35 -0
- package/dist/transport/guards.js +107 -0
- package/dist/transport/http-server.d.ts +17 -0
- package/dist/transport/http-server.js +198 -0
- package/dist/transport/mcp-app-hello.d.ts +3 -0
- package/dist/transport/mcp-app-hello.js +66 -0
- package/dist/widgets/display-modes.html +131 -0
- package/dist/widgets/hello.html +138 -0
- package/dist/widgets/proposals.html +131 -0
- package/dist/widgets/top-of-mind.html +131 -0
- package/package.json +73 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom inject — write a marker-bounded managed section into each
|
|
3
|
+
* selected harness's dotfile. Composes with user content; idempotent.
|
|
4
|
+
*
|
|
5
|
+
* This file only wires the flag-driven (non-interactive) path; the
|
|
6
|
+
* interactive wizard is added in Task 7.
|
|
7
|
+
*/
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { createInterface } from 'node:readline/promises';
|
|
10
|
+
import { assertStackVersionCompatible } from '../config.js';
|
|
11
|
+
import { extractGlobalFlags, resolveEnv } from './args.js';
|
|
12
|
+
import { renderJson, stderrWritable } from './io.js';
|
|
13
|
+
import { multiSelect } from './tui/multi-select.js';
|
|
14
|
+
import { HARNESSES, HARNESS_KEYS, isHarnessKey, resolveHarnessPath, } from '../injection/harnesses.js';
|
|
15
|
+
import { renderBlock } from '../injection/render.js';
|
|
16
|
+
import { writeManagedBlock, MalformedMarkersError, findMarkers, buildContent, normalizeLF, } from '../injection/writer.js';
|
|
17
|
+
import { readFile } from 'node:fs/promises';
|
|
18
|
+
const USAGE = `Usage: loom inject [options]
|
|
19
|
+
|
|
20
|
+
Writes a managed section into each selected harness's dotfile telling
|
|
21
|
+
the agent how to load identity via loom. Re-running is idempotent;
|
|
22
|
+
content outside the <!-- loom:start / loom:end --> markers is preserved.
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--harness <keys> Comma-separated subset of: ${HARNESS_KEYS.join(', ')}
|
|
26
|
+
--all Inject into all default harnesses (exclusive with --harness)
|
|
27
|
+
--to <path> Override target path (valid only when exactly one harness is selected)
|
|
28
|
+
--dry-run Print unified diff; write nothing
|
|
29
|
+
--json Machine-readable output
|
|
30
|
+
--context-dir <path> Agent context dir (default: $LOOM_CONTEXT_DIR)
|
|
31
|
+
--help, -h Show this help
|
|
32
|
+
|
|
33
|
+
With no harness flags, runs the interactive wizard on a TTY; exits 2
|
|
34
|
+
on non-TTY stdin.
|
|
35
|
+
`;
|
|
36
|
+
const wizardOverrides = new Map();
|
|
37
|
+
/** Minimal line-level diff (Myers-lite via LCS table). Good enough for
|
|
38
|
+
* a managed-block preview; avoids pulling in a dependency. */
|
|
39
|
+
function lineDiff(oldText, newText) {
|
|
40
|
+
const A = oldText.split('\n');
|
|
41
|
+
const B = newText.split('\n');
|
|
42
|
+
const n = A.length, m = B.length;
|
|
43
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
44
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
45
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
46
|
+
if (A[i] === B[j])
|
|
47
|
+
lcs[i][j] = lcs[i + 1][j + 1] + 1;
|
|
48
|
+
else
|
|
49
|
+
lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const out = [];
|
|
53
|
+
let i = 0, j = 0;
|
|
54
|
+
while (i < n && j < m) {
|
|
55
|
+
if (A[i] === B[j]) {
|
|
56
|
+
out.push(` ${A[i]}`);
|
|
57
|
+
i++;
|
|
58
|
+
j++;
|
|
59
|
+
}
|
|
60
|
+
else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
61
|
+
out.push(`-${A[i]}`);
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
out.push(`+${B[j]}`);
|
|
66
|
+
j++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
while (i < n) {
|
|
70
|
+
out.push(`-${A[i]}`);
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
while (j < m) {
|
|
74
|
+
out.push(`+${B[j]}`);
|
|
75
|
+
j++;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function makeDiff(existing, next, path) {
|
|
80
|
+
const header = `--- ${path}\n+++ ${path}\n`;
|
|
81
|
+
const hunk = lineDiff(existing, next);
|
|
82
|
+
return header + hunk.join('\n') + (hunk.length ? '\n' : '');
|
|
83
|
+
}
|
|
84
|
+
async function planTargets(harnesses, toOverride, envHome) {
|
|
85
|
+
if (toOverride !== undefined && harnesses.length !== 1) {
|
|
86
|
+
return {
|
|
87
|
+
error: 'loom inject: --to requires exactly a single --harness value',
|
|
88
|
+
code: 2,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const targets = harnesses.map((key) => {
|
|
92
|
+
const override = toOverride ?? wizardOverrides.get(key);
|
|
93
|
+
return {
|
|
94
|
+
harness: HARNESSES[key],
|
|
95
|
+
path: override ?? resolveHarnessPath(HARNESSES[key], envHome),
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
wizardOverrides.clear();
|
|
99
|
+
return targets;
|
|
100
|
+
}
|
|
101
|
+
async function executeTargets(targets, contextDir, opts) {
|
|
102
|
+
const rows = [];
|
|
103
|
+
let hadError = false;
|
|
104
|
+
let lastError;
|
|
105
|
+
for (const t of targets) {
|
|
106
|
+
const block = renderBlock(t.harness, contextDir);
|
|
107
|
+
try {
|
|
108
|
+
if (opts.dryRun) {
|
|
109
|
+
const existing = await readFile(t.path, 'utf-8').catch((e) => {
|
|
110
|
+
if (e.code === 'ENOENT')
|
|
111
|
+
return null;
|
|
112
|
+
throw e;
|
|
113
|
+
});
|
|
114
|
+
const norm = existing === null ? null : normalizeLF(existing);
|
|
115
|
+
const markers = norm === null ? null : findMarkers(norm, t.path);
|
|
116
|
+
const { next, action } = buildContent(norm, block, markers);
|
|
117
|
+
rows.push({
|
|
118
|
+
harness: t.harness.key,
|
|
119
|
+
path: t.path,
|
|
120
|
+
action,
|
|
121
|
+
bytesWritten: 0,
|
|
122
|
+
diff: makeDiff(norm ?? '', next, t.path),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const res = await writeManagedBlock(t.path, block);
|
|
127
|
+
rows.push({
|
|
128
|
+
harness: t.harness.key,
|
|
129
|
+
path: res.path,
|
|
130
|
+
action: res.action,
|
|
131
|
+
bytesWritten: res.bytesWritten,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
hadError = true;
|
|
137
|
+
lastError = err;
|
|
138
|
+
rows.push({
|
|
139
|
+
harness: t.harness.key,
|
|
140
|
+
path: t.path,
|
|
141
|
+
action: 'no-change',
|
|
142
|
+
bytesWritten: 0,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { rows, hadError, lastError };
|
|
147
|
+
}
|
|
148
|
+
function humanActionLabel(action) {
|
|
149
|
+
switch (action) {
|
|
150
|
+
case 'created': return 'created';
|
|
151
|
+
case 'appended': return 'appended';
|
|
152
|
+
case 'updated': return 'updated';
|
|
153
|
+
case 'no-change': return 'no change';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function writeHumanReport(rows, io) {
|
|
157
|
+
for (const r of rows) {
|
|
158
|
+
io.stdout(`${r.harness}: ${r.path} (${humanActionLabel(r.action)})\n`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function writeDiffReport(rows, io) {
|
|
162
|
+
for (const r of rows) {
|
|
163
|
+
if (r.diff)
|
|
164
|
+
io.stdout(r.diff);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export async function run(argv, io) {
|
|
168
|
+
const { flags: global, rest } = extractGlobalFlags(argv);
|
|
169
|
+
let parsed;
|
|
170
|
+
try {
|
|
171
|
+
parsed = parseArgs({
|
|
172
|
+
args: rest,
|
|
173
|
+
options: {
|
|
174
|
+
harness: { type: 'string' },
|
|
175
|
+
all: { type: 'boolean' },
|
|
176
|
+
to: { type: 'string' },
|
|
177
|
+
'dry-run': { type: 'boolean' },
|
|
178
|
+
help: { type: 'boolean', short: 'h' },
|
|
179
|
+
},
|
|
180
|
+
strict: true,
|
|
181
|
+
allowPositionals: false,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
io.stderr(`${err.message}\n${USAGE}`);
|
|
186
|
+
return 2;
|
|
187
|
+
}
|
|
188
|
+
if (parsed.values.help) {
|
|
189
|
+
io.stdout(USAGE);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
if (parsed.values.harness !== undefined && parsed.values.all === true) {
|
|
193
|
+
io.stderr('loom inject: --harness and --all are mutually exclusive\n');
|
|
194
|
+
return 2;
|
|
195
|
+
}
|
|
196
|
+
let harnesses = null;
|
|
197
|
+
if (parsed.values.all === true) {
|
|
198
|
+
harnesses = [...HARNESS_KEYS];
|
|
199
|
+
}
|
|
200
|
+
else if (parsed.values.harness !== undefined) {
|
|
201
|
+
const parts = parsed.values.harness.split(',').map((s) => s.trim()).filter(Boolean);
|
|
202
|
+
for (const p of parts) {
|
|
203
|
+
if (!isHarnessKey(p)) {
|
|
204
|
+
io.stderr(`loom inject: unknown harness '${p}' (valid: ${HARNESS_KEYS.join(', ')})\n`);
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
harnesses = parts;
|
|
209
|
+
}
|
|
210
|
+
if (harnesses === null) {
|
|
211
|
+
if (!io.stdinIsTTY) {
|
|
212
|
+
io.stderr('loom inject: --harness or --all required when stdin is not a TTY\n');
|
|
213
|
+
return 2;
|
|
214
|
+
}
|
|
215
|
+
const chosen = await multiSelect({
|
|
216
|
+
title: 'Select harnesses to inject loom into:',
|
|
217
|
+
items: HARNESS_KEYS.map((k) => {
|
|
218
|
+
const home = io.env.HOME ?? '';
|
|
219
|
+
const defaultPath = resolveHarnessPath(HARNESSES[k], io.env.HOME);
|
|
220
|
+
const display = home ? defaultPath.replace(new RegExp(`^${home}`), '~') : defaultPath;
|
|
221
|
+
return {
|
|
222
|
+
value: k,
|
|
223
|
+
label: HARNESSES[k].display,
|
|
224
|
+
detail: display,
|
|
225
|
+
};
|
|
226
|
+
}),
|
|
227
|
+
initialSelected: new Set(HARNESS_KEYS),
|
|
228
|
+
});
|
|
229
|
+
if (chosen === null) {
|
|
230
|
+
io.stderr('loom inject: cancelled\n');
|
|
231
|
+
return 130;
|
|
232
|
+
}
|
|
233
|
+
harnesses = [...chosen];
|
|
234
|
+
if (harnesses.length === 0) {
|
|
235
|
+
io.stderr('loom inject: no harnesses selected\n');
|
|
236
|
+
return 2;
|
|
237
|
+
}
|
|
238
|
+
const rl = createInterface({ input: io.stdin, output: stderrWritable(io) });
|
|
239
|
+
try {
|
|
240
|
+
io.stderr('\nPress enter to accept defaults, or type a path:\n');
|
|
241
|
+
for (const k of harnesses) {
|
|
242
|
+
const defaultPath = resolveHarnessPath(HARNESSES[k], io.env.HOME);
|
|
243
|
+
const ans = (await rl.question(` ${HARNESSES[k].display} [${defaultPath}]: `)).trim();
|
|
244
|
+
if (ans.length > 0)
|
|
245
|
+
wizardOverrides.set(k, ans);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
rl.close();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const envR = resolveEnv(global, io.env);
|
|
253
|
+
try {
|
|
254
|
+
assertStackVersionCompatible(envR.contextDir);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
io.stderr(`${err.message}\n`);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
const plan = await planTargets(harnesses, parsed.values.to, io.env.HOME);
|
|
261
|
+
if ('error' in plan) {
|
|
262
|
+
io.stderr(`${plan.error}\n`);
|
|
263
|
+
return plan.code;
|
|
264
|
+
}
|
|
265
|
+
const dryRun = parsed.values['dry-run'] === true;
|
|
266
|
+
const json = global.json === true;
|
|
267
|
+
let report;
|
|
268
|
+
try {
|
|
269
|
+
report = await executeTargets(plan, envR.contextDir, { dryRun });
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
if (err instanceof MalformedMarkersError) {
|
|
273
|
+
io.stderr(`${err.message}\n`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
io.stderr(`loom inject: ${err.message}\n`);
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
if (json) {
|
|
280
|
+
renderJson(io, report.rows);
|
|
281
|
+
}
|
|
282
|
+
else if (dryRun) {
|
|
283
|
+
writeDiffReport(report.rows, io);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
writeHumanReport(report.rows, io);
|
|
287
|
+
}
|
|
288
|
+
if (report.hadError) {
|
|
289
|
+
if (report.lastError instanceof MalformedMarkersError) {
|
|
290
|
+
io.stderr(`${report.lastError.message}\n`);
|
|
291
|
+
}
|
|
292
|
+
else if (report.lastError) {
|
|
293
|
+
io.stderr(`loom inject: ${report.lastError.message}\n`);
|
|
294
|
+
}
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom install — write the bundled SKILL.md into a target harness's
|
|
3
|
+
* skills directory. Flag-driven for scripting; single-select TUI on a
|
|
4
|
+
* TTY when no --harness is given. See stack spec v1 §11 (Adapters).
|
|
5
|
+
*/
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { resolve as pathResolve } from 'node:path';
|
|
8
|
+
import { extractGlobalFlags } from './args.js';
|
|
9
|
+
import { renderJson } from './io.js';
|
|
10
|
+
import { INSTALL_TARGETS, INSTALL_TARGET_KEYS, isInstallTargetKey, resolveSkillPath, } from '../install/harnesses.js';
|
|
11
|
+
import { writeSkill } from '../install/render.js';
|
|
12
|
+
import { multiSelect } from './tui/multi-select.js';
|
|
13
|
+
const USAGE = `Usage: loom install [options]
|
|
14
|
+
|
|
15
|
+
Writes the loom-setup skill into a target harness's skills directory.
|
|
16
|
+
On a TTY with no --harness flag, runs a single-select picker.
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--harness <key> One of: ${INSTALL_TARGET_KEYS.join(', ')}
|
|
20
|
+
--to <path> Override destination path (requires --harness)
|
|
21
|
+
--force Overwrite an existing skill file
|
|
22
|
+
--dry-run Report action without writing
|
|
23
|
+
--json Emit { target, path, action } and suppress prose
|
|
24
|
+
--help, -h Show this help
|
|
25
|
+
|
|
26
|
+
The "other" target writes ./loom-setup-skill.md in the current directory
|
|
27
|
+
unless --to overrides.
|
|
28
|
+
`;
|
|
29
|
+
async function pickHarnessInteractive(_io) {
|
|
30
|
+
const items = INSTALL_TARGET_KEYS.map((k) => ({
|
|
31
|
+
value: k,
|
|
32
|
+
label: INSTALL_TARGETS[k].label,
|
|
33
|
+
detail: INSTALL_TARGETS[k].skillDir ?? '(writes to current directory)',
|
|
34
|
+
}));
|
|
35
|
+
const picked = await multiSelect({
|
|
36
|
+
title: 'Install loom-setup skill into which harness?',
|
|
37
|
+
items,
|
|
38
|
+
single: true,
|
|
39
|
+
});
|
|
40
|
+
if (!picked)
|
|
41
|
+
return null;
|
|
42
|
+
const arr = [...picked];
|
|
43
|
+
if (arr.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
return arr[0];
|
|
46
|
+
}
|
|
47
|
+
export async function run(argv, io) {
|
|
48
|
+
const { flags: global, rest } = extractGlobalFlags(argv);
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = parseArgs({
|
|
52
|
+
args: rest,
|
|
53
|
+
options: {
|
|
54
|
+
harness: { type: 'string' },
|
|
55
|
+
to: { type: 'string' },
|
|
56
|
+
force: { type: 'boolean' },
|
|
57
|
+
'dry-run': { type: 'boolean' },
|
|
58
|
+
json: { type: 'boolean' },
|
|
59
|
+
help: { type: 'boolean', short: 'h' },
|
|
60
|
+
},
|
|
61
|
+
strict: true,
|
|
62
|
+
allowPositionals: false,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
io.stderr(`${err.message}\n${USAGE}`);
|
|
67
|
+
return 2;
|
|
68
|
+
}
|
|
69
|
+
if (parsed.values.help) {
|
|
70
|
+
io.stdout(USAGE);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
const force = Boolean(parsed.values.force);
|
|
74
|
+
const dryRun = Boolean(parsed.values['dry-run']);
|
|
75
|
+
const json = Boolean(parsed.values.json) || Boolean(global.json);
|
|
76
|
+
let key;
|
|
77
|
+
if (parsed.values.harness !== undefined) {
|
|
78
|
+
if (!isInstallTargetKey(parsed.values.harness)) {
|
|
79
|
+
io.stderr(`Unknown harness: ${parsed.values.harness}. Choose one of: ${INSTALL_TARGET_KEYS.join(', ')}.\n`);
|
|
80
|
+
return 2;
|
|
81
|
+
}
|
|
82
|
+
key = parsed.values.harness;
|
|
83
|
+
}
|
|
84
|
+
else if (io.stdinIsTTY) {
|
|
85
|
+
const picked = await pickHarnessInteractive(io);
|
|
86
|
+
if (!picked) {
|
|
87
|
+
io.stderr('Cancelled.\n');
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
key = picked;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
io.stderr(`--harness is required when stdin is not a TTY.\n${USAGE}`);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
const target = INSTALL_TARGETS[key];
|
|
97
|
+
let dest;
|
|
98
|
+
if (parsed.values.to !== undefined) {
|
|
99
|
+
dest = pathResolve(parsed.values.to);
|
|
100
|
+
}
|
|
101
|
+
else if (key === 'other') {
|
|
102
|
+
dest = pathResolve(process.cwd(), 'loom-setup-skill.md');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const p = resolveSkillPath(target);
|
|
106
|
+
if (p === null) {
|
|
107
|
+
io.stderr(`Internal error: target ${key} has no skillDir and --to was not provided.\n`);
|
|
108
|
+
return 2;
|
|
109
|
+
}
|
|
110
|
+
dest = p;
|
|
111
|
+
}
|
|
112
|
+
const res = await writeSkill(dest, { force, dryRun });
|
|
113
|
+
if (json) {
|
|
114
|
+
renderJson(io, { target: key, path: res.path, action: res.action });
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
const verb = actionVerb(res.action);
|
|
118
|
+
const lines = [
|
|
119
|
+
`${verb} ${res.path}`,
|
|
120
|
+
'',
|
|
121
|
+
`Next: open ${target.label}. ${firstLetterUpper(target.invoke)}.`,
|
|
122
|
+
`After the skill finishes, ${target.restart}.`,
|
|
123
|
+
];
|
|
124
|
+
io.stdout(lines.join('\n') + '\n');
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
function actionVerb(a) {
|
|
128
|
+
switch (a) {
|
|
129
|
+
case 'created': return 'Wrote';
|
|
130
|
+
case 'skipped-exists': return 'Already up to date at';
|
|
131
|
+
case 'skipped-stale': return 'Skill file is outdated (re-run with --force to overwrite):';
|
|
132
|
+
case 'overwritten': return 'Overwrote';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function firstLetterUpper(s) {
|
|
136
|
+
return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
|
|
137
|
+
}
|
package/dist/cli/io.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
export interface IOStreams {
|
|
3
|
+
stdout: (s: string) => void;
|
|
4
|
+
stderr: (s: string) => void;
|
|
5
|
+
stdin: NodeJS.ReadableStream;
|
|
6
|
+
stdinIsTTY: boolean;
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
}
|
|
9
|
+
export declare function realStreams(): IOStreams;
|
|
10
|
+
export declare function readStdin(stdin: NodeJS.ReadableStream): Promise<string>;
|
|
11
|
+
export declare function openEditor(env: NodeJS.ProcessEnv, subcommand: string, initial?: string): Promise<string>;
|
|
12
|
+
export declare function readBody(io: IOStreams, subcommand: string): Promise<string>;
|
|
13
|
+
export declare function renderJson(io: IOStreams, value: unknown): void;
|
|
14
|
+
export declare function stderrWritable(io: IOStreams): Writable;
|
package/dist/cli/io.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI I/O — stream writers, body reader (stdin → $EDITOR fallback),
|
|
3
|
+
* and the --json vs human render dispatcher.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, writeFile, unlink } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { Writable } from 'node:stream';
|
|
10
|
+
export function realStreams() {
|
|
11
|
+
return {
|
|
12
|
+
stdout: (s) => { process.stdout.write(s); },
|
|
13
|
+
stderr: (s) => { process.stderr.write(s); },
|
|
14
|
+
stdin: process.stdin,
|
|
15
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
16
|
+
env: process.env,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export async function readStdin(stdin) {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
for await (const chunk of stdin) {
|
|
22
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
23
|
+
}
|
|
24
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
25
|
+
}
|
|
26
|
+
export async function openEditor(env, subcommand, initial = '') {
|
|
27
|
+
// Spec §"Body input for write commands": $VISUAL ?? $EDITOR ?? 'vi'.
|
|
28
|
+
const editor = env.VISUAL ?? env.EDITOR ?? 'vi';
|
|
29
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
30
|
+
const tempPath = join(tmpdir(), `loom-${subcommand}-${process.pid}-${randomSuffix}.md`);
|
|
31
|
+
await writeFile(tempPath, initial, 'utf-8');
|
|
32
|
+
await new Promise((resolve, reject) => {
|
|
33
|
+
const child = spawn(editor, [tempPath], { stdio: 'inherit' });
|
|
34
|
+
child.on('exit', (code) => {
|
|
35
|
+
if (code === 0)
|
|
36
|
+
resolve();
|
|
37
|
+
else
|
|
38
|
+
reject(new Error(`editor exited with code ${code}; temp file at ${tempPath}`));
|
|
39
|
+
});
|
|
40
|
+
child.on('error', reject);
|
|
41
|
+
});
|
|
42
|
+
const body = await readFile(tempPath, 'utf-8');
|
|
43
|
+
await unlink(tempPath).catch(() => { });
|
|
44
|
+
return body;
|
|
45
|
+
}
|
|
46
|
+
export async function readBody(io, subcommand) {
|
|
47
|
+
if (!io.stdinIsTTY) {
|
|
48
|
+
return (await readStdin(io.stdin)).trimEnd();
|
|
49
|
+
}
|
|
50
|
+
const body = await openEditor(io.env, subcommand);
|
|
51
|
+
return body.trimEnd();
|
|
52
|
+
}
|
|
53
|
+
export function renderJson(io, value) {
|
|
54
|
+
io.stdout(JSON.stringify(value, null, 2) + '\n');
|
|
55
|
+
}
|
|
56
|
+
export function stderrWritable(io) {
|
|
57
|
+
return new Writable({
|
|
58
|
+
write(chunk, _encoding, callback) {
|
|
59
|
+
io.stderr(Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk));
|
|
60
|
+
callback();
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|