@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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom migrate — report and apply pending schema migrations to memories.db.
|
|
3
|
+
*
|
|
4
|
+
* Exits 0 when all migrations applied or already up to date.
|
|
5
|
+
* Exits 1 when any migration fails (and prints the error).
|
|
6
|
+
* Use --dry-run to report pending migrations without applying them.
|
|
7
|
+
*/
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
11
|
+
import { runMigrations, pendingMigrations } from '../backends/migrations.js';
|
|
12
|
+
import { resolveSqliteDbPath } from '../config.js';
|
|
13
|
+
import { extractGlobalFlags, resolveEnv } from './args.js';
|
|
14
|
+
import { renderJson } from './io.js';
|
|
15
|
+
const USAGE = `Usage: loom migrate [options]
|
|
16
|
+
|
|
17
|
+
Report and apply pending schema migrations to memories.db.
|
|
18
|
+
Exits non-zero if any migration fails.
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--dry-run Show pending migrations without applying them
|
|
22
|
+
--json Machine-readable output
|
|
23
|
+
--context-dir <p> Agent context dir (default: $LOOM_CONTEXT_DIR or ~/.config/loom/default)
|
|
24
|
+
--help, -h Show this help
|
|
25
|
+
`;
|
|
26
|
+
export async function run(argv, io) {
|
|
27
|
+
const { flags: global, rest } = extractGlobalFlags(argv);
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = parseArgs({
|
|
31
|
+
args: rest,
|
|
32
|
+
options: {
|
|
33
|
+
'dry-run': { type: 'boolean' },
|
|
34
|
+
help: { type: 'boolean', short: 'h' },
|
|
35
|
+
},
|
|
36
|
+
strict: true,
|
|
37
|
+
allowPositionals: false,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
io.stderr(`${err.message}\n${USAGE}`);
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
if (parsed.values.help) {
|
|
45
|
+
io.stdout(USAGE);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
const env = resolveEnv(global, io.env);
|
|
49
|
+
const dbPath = resolveSqliteDbPath(env.contextDir);
|
|
50
|
+
const json = env.json || Boolean(global.json);
|
|
51
|
+
const dryRun = Boolean(parsed.values['dry-run']);
|
|
52
|
+
if (!existsSync(dbPath)) {
|
|
53
|
+
if (json) {
|
|
54
|
+
renderJson(io, { dbPath, status: 'no_db', migrations: [] });
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
io.stdout(`No memories.db found at ${dbPath} — nothing to migrate.\n`);
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
const db = new BetterSqlite3(dbPath);
|
|
62
|
+
db.pragma('journal_mode = WAL');
|
|
63
|
+
try {
|
|
64
|
+
if (dryRun) {
|
|
65
|
+
const pending = pendingMigrations(db);
|
|
66
|
+
if (json) {
|
|
67
|
+
renderJson(io, {
|
|
68
|
+
dbPath,
|
|
69
|
+
status: 'dry_run',
|
|
70
|
+
pending: pending.map((m) => ({ id: m.id, description: m.description })),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (pending.length === 0) {
|
|
74
|
+
io.stdout(`memories.db is up to date. No pending migrations.\n`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
io.stdout(`Pending migrations (${pending.length}):\n`);
|
|
78
|
+
for (const m of pending) {
|
|
79
|
+
io.stdout(` - ${m.id}: ${m.description}\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
const results = runMigrations(db, { strict: false });
|
|
85
|
+
const applied = results.filter((r) => r.status === 'applied');
|
|
86
|
+
const failed = results.filter((r) => r.status === 'failed');
|
|
87
|
+
if (json) {
|
|
88
|
+
renderJson(io, {
|
|
89
|
+
dbPath,
|
|
90
|
+
status: failed.length > 0 ? 'error' : 'ok',
|
|
91
|
+
migrations: results,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (failed.length > 0) {
|
|
95
|
+
for (const r of failed) {
|
|
96
|
+
io.stderr(`Migration '${r.id}' failed: ${r.error}\n`);
|
|
97
|
+
}
|
|
98
|
+
if (applied.length > 0) {
|
|
99
|
+
io.stdout(`Applied ${applied.length} migration${applied.length === 1 ? '' : 's'} before failure.\n`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (applied.length === 0) {
|
|
103
|
+
io.stdout(`memories.db is already up to date.\n`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
io.stdout(`Applied ${applied.length} migration${applied.length === 1 ? '' : 's'}:\n`);
|
|
107
|
+
for (const r of applied) {
|
|
108
|
+
io.stdout(` - ${r.id}: ${r.description}\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return failed.length > 0 ? 1 : 0;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
db.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom recall — semantic memory search.
|
|
3
|
+
*/
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { recall } from '../tools/recall.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: loom recall <query> [options]
|
|
11
|
+
|
|
12
|
+
Search memories semantically.
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--category <name> Filter by category
|
|
16
|
+
--project <name> Filter by project
|
|
17
|
+
--limit <n> Max results (default backend-specific)
|
|
18
|
+
--diversity <f> MMR diversity 0..1 (default 0.3); 0 = pure relevance order
|
|
19
|
+
--json Emit MemoryMatch[] as JSON
|
|
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
|
+
project: { type: 'string' },
|
|
32
|
+
limit: { type: 'string' },
|
|
33
|
+
diversity: { type: 'string' },
|
|
34
|
+
help: { type: 'boolean', short: 'h' },
|
|
35
|
+
},
|
|
36
|
+
strict: true,
|
|
37
|
+
allowPositionals: true,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
io.stderr(`${err.message}\n${USAGE}`);
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
if (parsed.values.help) {
|
|
45
|
+
io.stdout(USAGE);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
const query = parsed.positionals[0];
|
|
49
|
+
if (!query) {
|
|
50
|
+
io.stderr(`Missing query.\n${USAGE}`);
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
const env = resolveEnv(global, io.env);
|
|
54
|
+
try {
|
|
55
|
+
assertStackVersionCompatible(env.contextDir);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
io.stderr(`${err.message}\n`);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
const limit = parsed.values.limit !== undefined
|
|
62
|
+
? Number.parseInt(parsed.values.limit, 10)
|
|
63
|
+
: undefined;
|
|
64
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
|
|
65
|
+
io.stderr(`--limit must be a positive integer.\n`);
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
const diversity = parsed.values.diversity !== undefined
|
|
69
|
+
? Number.parseFloat(parsed.values.diversity)
|
|
70
|
+
: undefined;
|
|
71
|
+
if (diversity !== undefined && (Number.isNaN(diversity) || diversity < 0 || diversity > 1)) {
|
|
72
|
+
io.stderr(`--diversity must be between 0 and 1.\n`);
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
const input = {
|
|
76
|
+
query,
|
|
77
|
+
category: parsed.values.category,
|
|
78
|
+
project: parsed.values.project,
|
|
79
|
+
limit,
|
|
80
|
+
diversity,
|
|
81
|
+
};
|
|
82
|
+
if (env.json) {
|
|
83
|
+
const backend = createBackend(env.contextDir);
|
|
84
|
+
const matches = await backend.recall(input);
|
|
85
|
+
renderJson(io, matches);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const text = await recall(env.contextDir, input);
|
|
89
|
+
io.stdout(text.endsWith('\n') ? text : text + '\n');
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom remember — save a new memory. Body from stdin or $EDITOR.
|
|
3
|
+
*/
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { remember } from '../tools/remember.js';
|
|
6
|
+
import { MEMORY_CATEGORIES, isMemoryCategory } from '../categories.js';
|
|
7
|
+
import { assertStackVersionCompatible } from '../config.js';
|
|
8
|
+
import { extractGlobalFlags, resolveEnv } from './args.js';
|
|
9
|
+
import { readBody, renderJson } from './io.js';
|
|
10
|
+
const USAGE = `Usage: loom remember <title> [options]
|
|
11
|
+
|
|
12
|
+
Body is read from stdin (when piped) or $EDITOR (when interactive).
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--category <name> Category: user|project|self|feedback|reference|pursuit (default: reference)
|
|
16
|
+
--project <name> Project tag
|
|
17
|
+
--ttl <dur> TTL like "7d", "30d", or "permanent"
|
|
18
|
+
--refs <csv> Comma-separated reference refs stored in metadata
|
|
19
|
+
--meta <json> JSON object merged into metadata (e.g. '{"where":"voice"}')
|
|
20
|
+
--json Emit MemoryRef
|
|
21
|
+
--context-dir <path> Agent context dir
|
|
22
|
+
--help, -h Show this help
|
|
23
|
+
`;
|
|
24
|
+
export async function run(argv, io) {
|
|
25
|
+
const { flags: global, rest } = extractGlobalFlags(argv);
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = parseArgs({
|
|
29
|
+
args: rest,
|
|
30
|
+
options: {
|
|
31
|
+
category: { type: 'string' },
|
|
32
|
+
project: { type: 'string' },
|
|
33
|
+
ttl: { type: 'string' },
|
|
34
|
+
refs: { type: 'string' },
|
|
35
|
+
meta: { type: 'string' },
|
|
36
|
+
help: { type: 'boolean', short: 'h' },
|
|
37
|
+
},
|
|
38
|
+
strict: true,
|
|
39
|
+
allowPositionals: true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
io.stderr(`${err.message}\n${USAGE}`);
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
if (parsed.values.help) {
|
|
47
|
+
io.stdout(USAGE);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
const title = parsed.positionals[0];
|
|
51
|
+
if (!title) {
|
|
52
|
+
io.stderr(`Missing title.\n${USAGE}`);
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
const category = parsed.values.category ?? 'reference';
|
|
56
|
+
if (!isMemoryCategory(category)) {
|
|
57
|
+
io.stderr(`Unknown category "${category}". Valid categories: ${MEMORY_CATEGORIES.join(', ')}.\n`);
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
const env = resolveEnv(global, io.env);
|
|
61
|
+
try {
|
|
62
|
+
assertStackVersionCompatible(env.contextDir);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
io.stderr(`${err.message}\n`);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
let body;
|
|
69
|
+
try {
|
|
70
|
+
body = await readBody(io, 'remember');
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
io.stderr(`${err.message}\n`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (!body) {
|
|
77
|
+
io.stderr(`body cannot be empty\n`);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
const refsList = parsed.values.refs
|
|
81
|
+
? parsed.values.refs.split(',').map((s) => s.trim()).filter(Boolean)
|
|
82
|
+
: undefined;
|
|
83
|
+
let meta;
|
|
84
|
+
if (parsed.values.meta) {
|
|
85
|
+
try {
|
|
86
|
+
const parsedMeta = JSON.parse(parsed.values.meta);
|
|
87
|
+
if (!parsedMeta || typeof parsedMeta !== 'object' || Array.isArray(parsedMeta))
|
|
88
|
+
throw new Error('not an object');
|
|
89
|
+
meta = parsedMeta;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
io.stderr(`--meta must be a JSON object: ${err.message}\n`);
|
|
93
|
+
return 2;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const metadata = refsList || meta ? { ...(meta ?? {}), ...(refsList ? { refs: refsList } : {}) } : undefined;
|
|
97
|
+
const ref = await remember(env.contextDir, {
|
|
98
|
+
category,
|
|
99
|
+
title,
|
|
100
|
+
content: body,
|
|
101
|
+
project: parsed.values.project,
|
|
102
|
+
ttl: parsed.values.ttl,
|
|
103
|
+
metadata,
|
|
104
|
+
});
|
|
105
|
+
if (env.json) {
|
|
106
|
+
renderJson(io, ref);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
io.stdout(`Remembered: ${ref.ref} — ${ref.title}\n`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom serve — MCP startup. Default is stdio; `--http` starts the mesh-reachable
|
|
3
|
+
* HTTP daemon (c-loom-transport) bound to a loopback/mesh interface with an
|
|
4
|
+
* optional bearer token. Host/port/token come from flags or env:
|
|
5
|
+
* --http --host <h> --port <n> (LOOM_HTTP_HOST, LOOM_HTTP_PORT, LOOM_BEARER_TOKEN)
|
|
6
|
+
* Bind-safety refuses a public/0.0.0.0 host; Tailscale is the access control.
|
|
7
|
+
*/
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { createLoomServer } from '../server.js';
|
|
11
|
+
import { startHttpServer } from '../transport/http-server.js';
|
|
12
|
+
import { resolveContextDir } from '../config.js';
|
|
13
|
+
const USAGE = `Usage: loom serve [options]
|
|
14
|
+
|
|
15
|
+
Start the MCP server. Default transport is stdio; --http starts the
|
|
16
|
+
mesh-reachable HTTP daemon instead.
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--http Serve over HTTP instead of stdio
|
|
20
|
+
--host <h> HTTP bind host (env LOOM_HTTP_HOST, default 127.0.0.1)
|
|
21
|
+
--port <n> HTTP port (env LOOM_HTTP_PORT, default 8787)
|
|
22
|
+
--help, -h Show this help
|
|
23
|
+
|
|
24
|
+
Bearer token comes from LOOM_BEARER_TOKEN. Bind-safety refuses a public host.
|
|
25
|
+
`;
|
|
26
|
+
export async function run(argv, io) {
|
|
27
|
+
let values;
|
|
28
|
+
try {
|
|
29
|
+
({ values } = parseArgs({
|
|
30
|
+
args: argv,
|
|
31
|
+
options: {
|
|
32
|
+
http: { type: 'boolean', default: false },
|
|
33
|
+
host: { type: 'string' },
|
|
34
|
+
port: { type: 'string' },
|
|
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 (values.help) {
|
|
46
|
+
io.stdout(USAGE);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
const contextDir = resolveContextDir();
|
|
50
|
+
if (values.http) {
|
|
51
|
+
const host = values.host ?? process.env.LOOM_HTTP_HOST ?? '127.0.0.1';
|
|
52
|
+
const port = Number(values.port ?? process.env.LOOM_HTTP_PORT ?? 8787);
|
|
53
|
+
const token = process.env.LOOM_BEARER_TOKEN || undefined;
|
|
54
|
+
const handle = await startHttpServer({ contextDir, host, port, token });
|
|
55
|
+
io.stderr(`loom: HTTP MCP daemon on http://${handle.host}:${handle.port} ` +
|
|
56
|
+
`(${token ? 'bearer-gated' : 'open — network is the boundary'})\n`);
|
|
57
|
+
// Hold open until the process is signalled.
|
|
58
|
+
await new Promise(() => { });
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
const { server } = createLoomServer({ contextDir });
|
|
62
|
+
const transport = new StdioServerTransport();
|
|
63
|
+
await server.connect(transport);
|
|
64
|
+
// connect() resolves when the stdio transport STARTS, not when it closes.
|
|
65
|
+
// Returning here would let the CLI dispatcher process.exit(0) and kill the
|
|
66
|
+
// server the instant it came up. Hold until the client hangs up.
|
|
67
|
+
await new Promise((resolve) => {
|
|
68
|
+
transport.onclose = () => resolve();
|
|
69
|
+
});
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical list of loom CLI subcommands. Shared by the CLI dispatcher
|
|
3
|
+
* (src/cli/index.ts) and the CLI-vs-MCP routing gate (src/index.ts) so
|
|
4
|
+
* the set never drifts between entry points.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SUBCOMMANDS: readonly ['wake', 'recall', 'remember', 'forget', 'update', 'memory', 'knowledge', 'update-identity', 'bootstrap', 'serve', 'inject', 'harness', 'install', 'doctor', 'migrate'];
|
|
7
|
+
export type Subcommand = typeof SUBCOMMANDS[number];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical list of loom CLI subcommands. Shared by the CLI dispatcher
|
|
3
|
+
* (src/cli/index.ts) and the CLI-vs-MCP routing gate (src/index.ts) so
|
|
4
|
+
* the set never drifts between entry points.
|
|
5
|
+
*/
|
|
6
|
+
export const SUBCOMMANDS = [
|
|
7
|
+
'wake', 'recall', 'remember', 'forget', 'update',
|
|
8
|
+
'memory', 'knowledge', 'update-identity', 'bootstrap', 'serve',
|
|
9
|
+
'inject', 'harness',
|
|
10
|
+
'install', 'doctor', 'migrate',
|
|
11
|
+
];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface MultiSelectItem<T> {
|
|
2
|
+
value: T;
|
|
3
|
+
label: string;
|
|
4
|
+
detail?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MultiSelectState<T> {
|
|
7
|
+
cursor: number;
|
|
8
|
+
selected: Set<T>;
|
|
9
|
+
}
|
|
10
|
+
export type MultiSelectEvent = {
|
|
11
|
+
kind: 'up';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'down';
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'toggle';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'confirm';
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'cancel';
|
|
20
|
+
};
|
|
21
|
+
export type MultiSelectResult<T> = {
|
|
22
|
+
status: 'running';
|
|
23
|
+
state: MultiSelectState<T>;
|
|
24
|
+
} | {
|
|
25
|
+
status: 'confirmed';
|
|
26
|
+
selected: Set<T>;
|
|
27
|
+
} | {
|
|
28
|
+
status: 'cancelled';
|
|
29
|
+
};
|
|
30
|
+
export interface ReduceOpts {
|
|
31
|
+
single?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface MultiSelectOpts<T> {
|
|
34
|
+
title: string;
|
|
35
|
+
items: ReadonlyArray<MultiSelectItem<T>>;
|
|
36
|
+
initialSelected?: ReadonlySet<T>;
|
|
37
|
+
single?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare function initialState<T>(_items: ReadonlyArray<MultiSelectItem<T>>, initialSelected?: ReadonlySet<T>): MultiSelectState<T>;
|
|
40
|
+
export declare function reduce<T>(state: MultiSelectState<T>, event: MultiSelectEvent, items: ReadonlyArray<MultiSelectItem<T>>, opts?: ReduceOpts): MultiSelectResult<T>;
|
|
41
|
+
interface KeypressAdapterDeps {
|
|
42
|
+
stdin: NodeJS.ReadStream;
|
|
43
|
+
stdout: {
|
|
44
|
+
write: (s: string) => void;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* TTY wrapper around `reduce`. Returns the confirmed selection or null
|
|
49
|
+
* on cancel. Throws if stdin is not a TTY — callers must check
|
|
50
|
+
* beforehand.
|
|
51
|
+
*/
|
|
52
|
+
export declare function multiSelect<T>(opts: MultiSelectOpts<T>, deps?: KeypressAdapterDeps): Promise<ReadonlySet<T> | null>;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
/**
|
|
3
|
+
* Reusable keyboard-nav multi-select primitive. The reducer is pure and
|
|
4
|
+
* fully unit-tested; the TTY adapter (`multiSelect`) is a thin
|
|
5
|
+
* stdin+render wrapper exercised by the inject integration test and
|
|
6
|
+
* manual verification only. Raw-mode stdin + ANSI cursor codes.
|
|
7
|
+
*/
|
|
8
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
9
|
+
export function initialState(_items, initialSelected) {
|
|
10
|
+
return {
|
|
11
|
+
cursor: 0,
|
|
12
|
+
selected: new Set(initialSelected ?? []),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function reduce(state, event, items, opts = {}) {
|
|
16
|
+
switch (event.kind) {
|
|
17
|
+
case 'up': {
|
|
18
|
+
if (items.length === 0)
|
|
19
|
+
return { status: 'running', state };
|
|
20
|
+
const cursor = (state.cursor - 1 + items.length) % items.length;
|
|
21
|
+
return { status: 'running', state: { ...state, cursor } };
|
|
22
|
+
}
|
|
23
|
+
case 'down': {
|
|
24
|
+
if (items.length === 0)
|
|
25
|
+
return { status: 'running', state };
|
|
26
|
+
const cursor = (state.cursor + 1) % items.length;
|
|
27
|
+
return { status: 'running', state: { ...state, cursor } };
|
|
28
|
+
}
|
|
29
|
+
case 'toggle': {
|
|
30
|
+
if (items.length === 0)
|
|
31
|
+
return { status: 'running', state };
|
|
32
|
+
const v = items[state.cursor].value;
|
|
33
|
+
const has = state.selected.has(v);
|
|
34
|
+
let next;
|
|
35
|
+
if (opts.single) {
|
|
36
|
+
next = has ? new Set() : new Set([v]);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
next = new Set(state.selected);
|
|
40
|
+
if (has)
|
|
41
|
+
next.delete(v);
|
|
42
|
+
else
|
|
43
|
+
next.add(v);
|
|
44
|
+
}
|
|
45
|
+
return { status: 'running', state: { cursor: state.cursor, selected: next } };
|
|
46
|
+
}
|
|
47
|
+
case 'confirm':
|
|
48
|
+
return { status: 'confirmed', selected: new Set(state.selected) };
|
|
49
|
+
case 'cancel':
|
|
50
|
+
return { status: 'cancelled' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ─── TTY adapter (manually verified) ────────────────────────────────────────
|
|
54
|
+
function renderFrame(opts, state, write) {
|
|
55
|
+
// Clear screen + move home; keep scrollback.
|
|
56
|
+
write('\x1b[2J\x1b[H');
|
|
57
|
+
write(`${opts.title}\n\n`);
|
|
58
|
+
opts.items.forEach((item, i) => {
|
|
59
|
+
const marker = state.selected.has(item.value) ? '[x]' : '[ ]';
|
|
60
|
+
const pointer = i === state.cursor ? '›' : ' ';
|
|
61
|
+
const detail = item.detail ? ` ${item.detail}` : '';
|
|
62
|
+
write(` ${pointer} ${marker} ${item.label}${detail}\n`);
|
|
63
|
+
});
|
|
64
|
+
const hint = opts.single
|
|
65
|
+
? '\n ↑/↓ move space select enter confirm esc/q cancel\n'
|
|
66
|
+
: '\n ↑/↓ move space toggle enter confirm esc/q cancel\n';
|
|
67
|
+
write(hint);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* TTY wrapper around `reduce`. Returns the confirmed selection or null
|
|
71
|
+
* on cancel. Throws if stdin is not a TTY — callers must check
|
|
72
|
+
* beforehand.
|
|
73
|
+
*/
|
|
74
|
+
export async function multiSelect(opts, deps = { stdin: process.stdin, stdout: process.stdout }) {
|
|
75
|
+
const { stdin, stdout } = deps;
|
|
76
|
+
if (!stdin.isTTY) {
|
|
77
|
+
throw new Error('multiSelect requires a TTY stdin');
|
|
78
|
+
}
|
|
79
|
+
emitKeypressEvents(stdin);
|
|
80
|
+
stdin.setRawMode(true);
|
|
81
|
+
stdin.resume();
|
|
82
|
+
let state = initialState(opts.items, opts.initialSelected);
|
|
83
|
+
const write = (s) => stdout.write(s);
|
|
84
|
+
renderFrame(opts, state, write);
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const onKey = (_str, key) => {
|
|
87
|
+
let event = null;
|
|
88
|
+
if (key.ctrl && key.name === 'c')
|
|
89
|
+
event = { kind: 'cancel' };
|
|
90
|
+
else if (key.name === 'escape' || key.name === 'q')
|
|
91
|
+
event = { kind: 'cancel' };
|
|
92
|
+
else if (key.name === 'up')
|
|
93
|
+
event = { kind: 'up' };
|
|
94
|
+
else if (key.name === 'down')
|
|
95
|
+
event = { kind: 'down' };
|
|
96
|
+
else if (key.name === 'space')
|
|
97
|
+
event = { kind: 'toggle' };
|
|
98
|
+
else if (key.name === 'return')
|
|
99
|
+
event = { kind: 'confirm' };
|
|
100
|
+
if (!event)
|
|
101
|
+
return;
|
|
102
|
+
const result = reduce(state, event, opts.items, { single: opts.single });
|
|
103
|
+
if (result.status === 'running') {
|
|
104
|
+
state = result.state;
|
|
105
|
+
renderFrame(opts, state, write);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
stdin.off('keypress', onKey);
|
|
109
|
+
stdin.setRawMode(false);
|
|
110
|
+
stdin.pause();
|
|
111
|
+
write('\n');
|
|
112
|
+
resolve(result.status === 'confirmed' ? result.selected : null);
|
|
113
|
+
};
|
|
114
|
+
stdin.on('keypress', onKey);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom update-identity — list sections or replace/append section body.
|
|
3
|
+
*/
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { listSections, updateIdentity } from '../tools/update-identity.js';
|
|
6
|
+
import { assertStackVersionCompatible } from '../config.js';
|
|
7
|
+
import { extractGlobalFlags, resolveEnv } from './args.js';
|
|
8
|
+
import { readBody, renderJson } from './io.js';
|
|
9
|
+
const USAGE = `Usage:
|
|
10
|
+
loom update-identity <file> # list sections (read-only)
|
|
11
|
+
loom update-identity <file> <section> # replace section body
|
|
12
|
+
loom update-identity <file> <section> --append # add as new section
|
|
13
|
+
|
|
14
|
+
<file> is "preferences" or "self-model". IDENTITY.md is immutable.
|
|
15
|
+
|
|
16
|
+
Body (for replace/append) is read from stdin or \$EDITOR.
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--append Add a new H2 section at end instead of replacing
|
|
20
|
+
--json Emit {file, section, mode} on success
|
|
21
|
+
--context-dir <path> Agent context dir
|
|
22
|
+
--help, -h Show this help
|
|
23
|
+
`;
|
|
24
|
+
const EDITABLE = new Set(['preferences', 'self-model']);
|
|
25
|
+
export async function run(argv, io) {
|
|
26
|
+
const { flags: global, rest } = extractGlobalFlags(argv);
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = parseArgs({
|
|
30
|
+
args: rest,
|
|
31
|
+
options: {
|
|
32
|
+
append: { type: 'boolean' },
|
|
33
|
+
help: { type: 'boolean', short: 'h' },
|
|
34
|
+
},
|
|
35
|
+
strict: true,
|
|
36
|
+
allowPositionals: true,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
io.stderr(`${err.message}\n${USAGE}`);
|
|
41
|
+
return 2;
|
|
42
|
+
}
|
|
43
|
+
if (parsed.values.help) {
|
|
44
|
+
io.stdout(USAGE);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
const file = parsed.positionals[0];
|
|
48
|
+
const section = parsed.positionals[1];
|
|
49
|
+
if (!file) {
|
|
50
|
+
io.stderr(`Missing <file>.\n${USAGE}`);
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
if (!EDITABLE.has(file)) {
|
|
54
|
+
io.stderr(`Unknown file "${file}". Editable: preferences, self-model.\n`);
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
const env = resolveEnv(global, io.env);
|
|
58
|
+
try {
|
|
59
|
+
assertStackVersionCompatible(env.contextDir);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
io.stderr(`${err.message}\n`);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
if (!section) {
|
|
66
|
+
const text = await listSections(env.contextDir, file);
|
|
67
|
+
io.stdout(text.endsWith('\n') ? text : text + '\n');
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
let body;
|
|
71
|
+
try {
|
|
72
|
+
body = await readBody(io, 'update-identity');
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
io.stderr(`${err.message}\n`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
if (!body) {
|
|
79
|
+
io.stderr(`body cannot be empty\n`);
|
|
80
|
+
return 2;
|
|
81
|
+
}
|
|
82
|
+
const mode = parsed.values.append ? 'append' : 'replace';
|
|
83
|
+
await updateIdentity(env.contextDir, { file, section, content: body, mode });
|
|
84
|
+
if (env.json) {
|
|
85
|
+
renderJson(io, { file, section, mode });
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
io.stdout(`${mode === 'append' ? 'Appended' : 'Replaced'} ${file}:${section}\n`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|