@jbarket/loomai 0.5.0 → 0.7.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 +129 -1
- package/README.md +115 -0
- package/dist/backends/dedup.d.ts +23 -0
- package/dist/backends/dedup.js +100 -0
- package/dist/backends/embedding-runtime.js +38 -9
- package/dist/backends/episodes.js +1 -1
- package/dist/backends/fastembed.js +30 -4
- package/dist/backends/migrations.js +200 -0
- package/dist/backends/mmr.d.ts +8 -0
- package/dist/backends/mmr.js +18 -4
- package/dist/backends/people.d.ts +192 -0
- package/dist/backends/people.js +824 -0
- package/dist/backends/proposals.js +1 -1
- package/dist/backends/recorder-health.d.ts +34 -0
- package/dist/backends/recorder-health.js +117 -0
- package/dist/backends/salience.d.ts +39 -0
- package/dist/backends/salience.js +52 -4
- package/dist/backends/scope.d.ts +56 -0
- package/dist/backends/scope.js +75 -0
- package/dist/backends/sqlite-knowledge.d.ts +24 -4
- package/dist/backends/sqlite-knowledge.js +253 -91
- package/dist/backends/sqlite-vec.d.ts +32 -1
- package/dist/backends/sqlite-vec.js +394 -75
- package/dist/backends/types.d.ts +203 -3
- package/dist/categories.d.ts +2 -0
- package/dist/categories.js +2 -0
- package/dist/cli/args.d.ts +16 -1
- package/dist/cli/args.js +45 -1
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +81 -3
- package/dist/cli/knowledge.js +4 -1
- package/dist/cli/memory.js +2 -2
- package/dist/cli/person.d.ts +2 -0
- package/dist/cli/person.js +116 -0
- package/dist/cli/pieces.d.ts +24 -0
- package/dist/cli/pieces.js +139 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +10 -0
- package/dist/cli/remote.d.ts +36 -0
- package/dist/cli/remote.js +173 -0
- package/dist/cli/serve.d.ts +7 -0
- package/dist/cli/serve.js +94 -5
- package/dist/cli/subcommands.d.ts +1 -1
- package/dist/cli/subcommands.js +2 -2
- package/dist/cli/update.js +30 -11
- package/dist/clients.js +7 -4
- package/dist/config.d.ts +14 -0
- package/dist/config.js +25 -2
- package/dist/index.js +28 -8
- package/dist/injection/harnesses.js +9 -3
- package/dist/install/harnesses.js +7 -1
- package/dist/server.d.ts +8 -1
- package/dist/server.js +461 -374
- package/dist/tools/dossier.js +5 -1
- package/dist/tools/eurorack-import.js +3 -0
- package/dist/tools/identity.d.ts +2 -0
- package/dist/tools/identity.js +35 -2
- package/dist/tools/knowledge-history.d.ts +6 -1
- package/dist/tools/knowledge-history.js +26 -8
- package/dist/tools/knowledge-maintain.d.ts +5 -0
- package/dist/tools/knowledge-maintain.js +92 -5
- package/dist/tools/knowledge-recall.d.ts +7 -1
- package/dist/tools/knowledge-recall.js +53 -3
- package/dist/tools/knowledge-verify.d.ts +11 -1
- package/dist/tools/knowledge-verify.js +17 -4
- package/dist/tools/knowledge-write.d.ts +8 -1
- package/dist/tools/knowledge-write.js +27 -9
- package/dist/tools/memory-history.d.ts +5 -0
- package/dist/tools/memory-history.js +52 -0
- package/dist/tools/memory-list.js +2 -1
- package/dist/tools/memory-merge.d.ts +2 -0
- package/dist/tools/memory-merge.js +40 -0
- package/dist/tools/memory-revision-restore.d.ts +5 -0
- package/dist/tools/memory-revision-restore.js +33 -0
- package/dist/tools/person.d.ts +32 -0
- package/dist/tools/person.js +111 -0
- package/dist/tools/prune.js +1 -1
- package/dist/tools/recall.js +7 -1
- package/dist/tools/remember.js +18 -2
- package/dist/tools/update.js +5 -1
- package/dist/transport/cli-endpoint.d.ts +13 -0
- package/dist/transport/cli-endpoint.js +97 -0
- package/dist/transport/guards.d.ts +14 -0
- package/dist/transport/guards.js +26 -0
- package/dist/transport/http-server.d.ts +28 -1
- package/dist/transport/http-server.js +156 -18
- package/dist/transport/knowledge-server.d.ts +11 -0
- package/dist/transport/knowledge-server.js +30 -0
- package/dist/transport/knowledge-tools.d.ts +50 -0
- package/dist/transport/knowledge-tools.js +290 -0
- package/dist/transport/mcp-app-hello.d.ts +1 -1
- package/dist/widgets/display-modes.html +62 -56
- package/dist/widgets/hello.html +62 -56
- package/dist/widgets/proposals.html +62 -56
- package/dist/widgets/top-of-mind.html +60 -54
- package/package.json +7 -5
package/dist/cli/serve.js
CHANGED
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
* optional bearer token. Host/port/token come from flags or env:
|
|
5
5
|
* --http --host <h> --port <n> (LOOM_HTTP_HOST, LOOM_HTTP_PORT, LOOM_BEARER_TOKEN)
|
|
6
6
|
* Bind-safety refuses a public/0.0.0.0 host; Tailscale is the access control.
|
|
7
|
+
*
|
|
8
|
+
* `--scope knowledge` (t-677) starts the standalone knowledge-only service
|
|
9
|
+
* instead of the full server — same transport, tailscale-bind-safety, and
|
|
10
|
+
* bearer-gating pattern, but the MCP tool surface is knowledge_* only and
|
|
11
|
+
* auth is per-identity (LOOM_KNOWLEDGE_BEARER_TOKENS, "identity:token,...")
|
|
12
|
+
* so the acting identity is derivable server-side for attribution. It is
|
|
13
|
+
* NOT started by default and ships disabled — nothing in this repo enables
|
|
14
|
+
* or deploys it; standing it up is a separate, deliberate operational step.
|
|
7
15
|
*/
|
|
8
16
|
import { parseArgs } from 'node:util';
|
|
9
|
-
import { StdioServerTransport } from '@modelcontextprotocol/
|
|
17
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
10
18
|
import { createLoomServer } from '../server.js';
|
|
19
|
+
import { createKnowledgeOnlyServer } from '../transport/knowledge-server.js';
|
|
11
20
|
import { startHttpServer } from '../transport/http-server.js';
|
|
12
21
|
import { resolveContextDir } from '../config.js';
|
|
13
22
|
const USAGE = `Usage: loom serve [options]
|
|
@@ -17,12 +26,49 @@ mesh-reachable HTTP daemon instead.
|
|
|
17
26
|
|
|
18
27
|
Options:
|
|
19
28
|
--http Serve over HTTP instead of stdio
|
|
29
|
+
--scope <full|knowledge> Tool surface for --http (default full);
|
|
30
|
+
knowledge without --http is rejected
|
|
20
31
|
--host <h> HTTP bind host (env LOOM_HTTP_HOST, default 127.0.0.1)
|
|
21
|
-
--port <n> HTTP port (env LOOM_HTTP_PORT,
|
|
32
|
+
--port <n> HTTP port (env LOOM_HTTP_PORT, or LOOM_KNOWLEDGE_HTTP_PORT
|
|
33
|
+
for --scope knowledge, default 8787 / 8788)
|
|
22
34
|
--help, -h Show this help
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
--scope full (default): bearer token from LOOM_BEARER_TOKEN.
|
|
37
|
+
--scope knowledge: per-identity bearer from LOOM_KNOWLEDGE_BEARER_TOKENS,
|
|
38
|
+
format "identity:token,identity:token" — e.g. "art:abc123,mark:def456".
|
|
39
|
+
Required and non-empty; the knowledge-only service refuses to boot without
|
|
40
|
+
at least one identity configured. Registers knowledge_* tools only — no
|
|
41
|
+
identity/memory/dossier tools on this surface.
|
|
42
|
+
|
|
43
|
+
Bind-safety refuses a public host in both scopes.
|
|
25
44
|
`;
|
|
45
|
+
/**
|
|
46
|
+
* Parse "identity:token,identity:token" into a token → identity map ready
|
|
47
|
+
* for startHttpServer's `tokens` option (checkBearerMulti). Throws with a
|
|
48
|
+
* message naming the offending pair on malformed input — fail loud at CLI
|
|
49
|
+
* startup rather than silently dropping an identity.
|
|
50
|
+
*/
|
|
51
|
+
export function parseIdentityTokens(raw) {
|
|
52
|
+
const tokens = {};
|
|
53
|
+
const pairs = raw.split(',');
|
|
54
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
55
|
+
const trimmed = pairs[i].trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
continue;
|
|
58
|
+
const idx = trimmed.indexOf(':');
|
|
59
|
+
const identity = idx > 0 ? trimmed.slice(0, idx).trim() : '';
|
|
60
|
+
const token = idx > 0 ? trimmed.slice(idx + 1).trim() : '';
|
|
61
|
+
if (!identity || !token) {
|
|
62
|
+
// Never echo `trimmed` — a malformed pair is exactly the shape a
|
|
63
|
+
// pasted-without-colon secret takes, and this error can land in
|
|
64
|
+
// stderr/journal/CI logs. Report position only, never content.
|
|
65
|
+
throw new Error(`malformed identity:token pair at position ${i + 1} — expected "identity:token", ` +
|
|
66
|
+
`got ${identity ? 'an identity with no token' : 'no identity'} (value withheld from this error)`);
|
|
67
|
+
}
|
|
68
|
+
tokens[identity] = token;
|
|
69
|
+
}
|
|
70
|
+
return tokens;
|
|
71
|
+
}
|
|
26
72
|
export async function run(argv, io) {
|
|
27
73
|
let values;
|
|
28
74
|
try {
|
|
@@ -30,6 +76,7 @@ export async function run(argv, io) {
|
|
|
30
76
|
args: argv,
|
|
31
77
|
options: {
|
|
32
78
|
http: { type: 'boolean', default: false },
|
|
79
|
+
scope: { type: 'string', default: 'full' },
|
|
33
80
|
host: { type: 'string' },
|
|
34
81
|
port: { type: 'string' },
|
|
35
82
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -46,19 +93,61 @@ export async function run(argv, io) {
|
|
|
46
93
|
io.stdout(USAGE);
|
|
47
94
|
return 0;
|
|
48
95
|
}
|
|
96
|
+
if (values.scope !== 'full' && values.scope !== 'knowledge') {
|
|
97
|
+
io.stderr(`Unknown --scope "${values.scope}" — expected "full" or "knowledge".\n${USAGE}`);
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
// The knowledge-only surface exists only as an HTTP service. Without
|
|
101
|
+
// --http the stdio branch below would open the FULL server — every
|
|
102
|
+
// identity/memory/dossier tool — under a flag that asked for less
|
|
103
|
+
// (PR #124 review). Refuse before any transport opens.
|
|
104
|
+
if (values.scope === 'knowledge' && !values.http) {
|
|
105
|
+
io.stderr(`--scope knowledge requires --http — the knowledge-only surface is HTTP-only; ` +
|
|
106
|
+
`refusing to start the full stdio server instead.\n${USAGE}`);
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
49
109
|
const contextDir = resolveContextDir();
|
|
110
|
+
// Optional: gaps directory for recall-miss logging (LOOM_GAPS_DIR env var).
|
|
111
|
+
// When set, knowledge_recall misses are appended to ${gapsDir}/recall-miss.jsonl
|
|
112
|
+
// so the knowledge-mine script can surface them as expansion candidates.
|
|
113
|
+
const gapsDir = process.env.LOOM_GAPS_DIR || undefined;
|
|
114
|
+
if (values.http && values.scope === 'knowledge') {
|
|
115
|
+
const host = values.host ?? process.env.LOOM_HTTP_HOST ?? '127.0.0.1';
|
|
116
|
+
const port = Number(values.port ?? process.env.LOOM_KNOWLEDGE_HTTP_PORT ?? 8788);
|
|
117
|
+
const rawTokens = process.env.LOOM_KNOWLEDGE_BEARER_TOKENS ?? '';
|
|
118
|
+
let tokens;
|
|
119
|
+
try {
|
|
120
|
+
tokens = parseIdentityTokens(rawTokens);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
io.stderr(`${err.message}\n${USAGE}`);
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
if (Object.keys(tokens).length === 0) {
|
|
127
|
+
io.stderr(`--scope knowledge requires LOOM_KNOWLEDGE_BEARER_TOKENS ` +
|
|
128
|
+
`("identity:token,identity:token") — refusing to start unauthenticated.\n${USAGE}`);
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
const handle = await startHttpServer({
|
|
132
|
+
contextDir, host, port, tokens, gapsDir, createServer: createKnowledgeOnlyServer,
|
|
133
|
+
});
|
|
134
|
+
io.stderr(`loom: knowledge-only HTTP MCP daemon on http://${handle.host}:${handle.port} ` +
|
|
135
|
+
`(per-identity bearer: ${Object.keys(tokens).join(', ')})\n`);
|
|
136
|
+
await new Promise(() => { });
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
50
139
|
if (values.http) {
|
|
51
140
|
const host = values.host ?? process.env.LOOM_HTTP_HOST ?? '127.0.0.1';
|
|
52
141
|
const port = Number(values.port ?? process.env.LOOM_HTTP_PORT ?? 8787);
|
|
53
142
|
const token = process.env.LOOM_BEARER_TOKEN || undefined;
|
|
54
|
-
const handle = await startHttpServer({ contextDir, host, port, token });
|
|
143
|
+
const handle = await startHttpServer({ contextDir, host, port, token, gapsDir });
|
|
55
144
|
io.stderr(`loom: HTTP MCP daemon on http://${handle.host}:${handle.port} ` +
|
|
56
145
|
`(${token ? 'bearer-gated' : 'open — network is the boundary'})\n`);
|
|
57
146
|
// Hold open until the process is signalled.
|
|
58
147
|
await new Promise(() => { });
|
|
59
148
|
return 0;
|
|
60
149
|
}
|
|
61
|
-
const { server } = createLoomServer({ contextDir });
|
|
150
|
+
const { server } = createLoomServer({ contextDir, gapsDir });
|
|
62
151
|
const transport = new StdioServerTransport();
|
|
63
152
|
await server.connect(transport);
|
|
64
153
|
// connect() resolves when the stdio transport STARTS, not when it closes.
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* (src/cli/index.ts) and the CLI-vs-MCP routing gate (src/index.ts) so
|
|
4
4
|
* the set never drifts between entry points.
|
|
5
5
|
*/
|
|
6
|
-
export declare const SUBCOMMANDS: readonly ['wake', 'recall', 'remember', 'forget', 'update', 'memory', 'knowledge', 'update-identity', 'bootstrap', 'serve', 'inject', 'harness', 'install', 'doctor', 'migrate'];
|
|
6
|
+
export declare const SUBCOMMANDS: readonly ['wake', 'recall', 'remember', 'forget', 'update', 'memory', 'knowledge', 'person', 'update-identity', 'bootstrap', 'serve', 'inject', 'harness', 'install', 'doctor', 'migrate', 'pieces'];
|
|
7
7
|
export type Subcommand = typeof SUBCOMMANDS[number];
|
package/dist/cli/subcommands.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const SUBCOMMANDS = [
|
|
7
7
|
'wake', 'recall', 'remember', 'forget', 'update',
|
|
8
|
-
'memory', 'knowledge', 'update-identity', 'bootstrap', 'serve',
|
|
8
|
+
'memory', 'knowledge', 'person', 'update-identity', 'bootstrap', 'serve',
|
|
9
9
|
'inject', 'harness',
|
|
10
|
-
'install', 'doctor', 'migrate',
|
|
10
|
+
'install', 'doctor', 'migrate', 'pieces',
|
|
11
11
|
];
|
package/dist/cli/update.js
CHANGED
|
@@ -11,11 +11,14 @@ const USAGE = `Usage: loom update <ref> [options]
|
|
|
11
11
|
loom update --category <cat> --title <exact> [options]
|
|
12
12
|
|
|
13
13
|
Updates content (from stdin or \$EDITOR) and/or metadata on an existing
|
|
14
|
-
memory.
|
|
14
|
+
memory. --confirm/--contradict record an observation against a feedback
|
|
15
|
+
memory's confidence instead — no body required for those.
|
|
15
16
|
|
|
16
17
|
Options:
|
|
17
18
|
--category <name> Identify by category (with --title)
|
|
18
19
|
--title <exact> Identify by title (with --category)
|
|
20
|
+
--confirm Feedback memories only: raise confidence (+0.05)
|
|
21
|
+
--contradict Feedback memories only: lower confidence (-0.1)
|
|
19
22
|
--json Emit UpdateResult
|
|
20
23
|
--context-dir <path> Agent context dir
|
|
21
24
|
--help, -h Show this help
|
|
@@ -29,6 +32,8 @@ export async function run(argv, io) {
|
|
|
29
32
|
options: {
|
|
30
33
|
category: { type: 'string' },
|
|
31
34
|
title: { type: 'string' },
|
|
35
|
+
confirm: { type: 'boolean' },
|
|
36
|
+
contradict: { type: 'boolean' },
|
|
32
37
|
help: { type: 'boolean', short: 'h' },
|
|
33
38
|
},
|
|
34
39
|
strict: true,
|
|
@@ -49,6 +54,15 @@ export async function run(argv, io) {
|
|
|
49
54
|
io.stderr(`Provide a <ref> or --category+--title.\n${USAGE}`);
|
|
50
55
|
return 2;
|
|
51
56
|
}
|
|
57
|
+
if (parsed.values.confirm && parsed.values.contradict) {
|
|
58
|
+
io.stderr(`--confirm and --contradict are mutually exclusive.\n${USAGE}`);
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
const observation = parsed.values.confirm
|
|
62
|
+
? 'confirm'
|
|
63
|
+
: parsed.values.contradict
|
|
64
|
+
? 'contradict'
|
|
65
|
+
: undefined;
|
|
52
66
|
const env = resolveEnv(global, io.env);
|
|
53
67
|
try {
|
|
54
68
|
assertStackVersionCompatible(env.contextDir);
|
|
@@ -57,23 +71,28 @@ export async function run(argv, io) {
|
|
|
57
71
|
io.stderr(`${err.message}\n`);
|
|
58
72
|
return 1;
|
|
59
73
|
}
|
|
74
|
+
// A pure observation needs no new body — --confirm/--contradict alone is a
|
|
75
|
+
// complete update. Any other call still requires one (existing behavior).
|
|
60
76
|
let body;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
if (!observation) {
|
|
78
|
+
try {
|
|
79
|
+
body = await readBody(io, 'update');
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
io.stderr(`${err.message}\n`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
if (!body) {
|
|
86
|
+
io.stderr(`body cannot be empty\n`);
|
|
87
|
+
return 2;
|
|
88
|
+
}
|
|
71
89
|
}
|
|
72
90
|
const input = {
|
|
73
91
|
ref,
|
|
74
92
|
category: parsed.values.category,
|
|
75
93
|
title: parsed.values.title,
|
|
76
94
|
content: body,
|
|
95
|
+
observation,
|
|
77
96
|
};
|
|
78
97
|
if (env.json) {
|
|
79
98
|
const backend = createBackend(env.contextDir);
|
package/dist/clients.js
CHANGED
|
@@ -14,17 +14,20 @@
|
|
|
14
14
|
import { readFile } from 'node:fs/promises';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { assertSafePathSegment } from './path-safety.js';
|
|
17
|
+
import { INSTALL_TARGETS } from './install/harnesses.js';
|
|
17
18
|
// ─── Tool list ────────────────────────────────────────────────────────────────
|
|
18
19
|
// Canonical tool names — used to generate the prefix-specific list per client
|
|
19
20
|
export const TOOLS = [
|
|
20
21
|
'identity', 'dossier', 'remember', 'recall', 'update', 'forget',
|
|
21
22
|
'memory_list', 'memory_prune', 'find_similar', 'memory_audit', 'episodes',
|
|
22
|
-
'memory_archive', 'memory_restore',
|
|
23
|
+
'memory_archive', 'memory_restore', 'memory_merge', 'memory_history', 'memory_revision_restore',
|
|
23
24
|
'memory_propose', 'memory_proposals', 'memory_ratify', 'memory_reject',
|
|
24
25
|
'update_identity', 'bootstrap', 'harness_init', 'harness_describe',
|
|
25
26
|
'knowledge_write', 'knowledge_recall', 'knowledge_maintain',
|
|
26
27
|
'knowledge_archive', 'knowledge_restore', 'knowledge_supersede', 'knowledge_move', 'knowledge_merge',
|
|
27
28
|
'knowledge_purge', 'knowledge_verify', 'knowledge_history',
|
|
29
|
+
'person_write', 'person_verify', 'person_archive', 'person_restore',
|
|
30
|
+
'person', 'person_list', 'person_history', 'person_revision_restore',
|
|
28
31
|
];
|
|
29
32
|
function toolList(prefix) {
|
|
30
33
|
return TOOLS.map((t) => `\`${prefix}${t}\``).join(', ');
|
|
@@ -34,11 +37,11 @@ const ADAPTERS = {
|
|
|
34
37
|
'claude-code': `## Runtime: Claude Code
|
|
35
38
|
|
|
36
39
|
You are running in Claude Code (Anthropic CLI). Loom tools use double-underscore prefix:
|
|
37
|
-
${toolList('
|
|
40
|
+
${toolList(INSTALL_TARGETS['claude-code'].toolPrefix)}`,
|
|
38
41
|
'gemini-cli': `## Runtime: Gemini CLI
|
|
39
42
|
|
|
40
|
-
You are running in Gemini CLI (Google). Loom tools use
|
|
41
|
-
${toolList('
|
|
43
|
+
You are running in Gemini CLI (Google). Loom tools use single-underscore prefix:
|
|
44
|
+
${toolList(INSTALL_TARGETS['gemini-cli'].toolPrefix)}`,
|
|
42
45
|
};
|
|
43
46
|
// ─── Loader ──────────────────────────────────────────────────────────────────
|
|
44
47
|
export function getBuiltInAdapter(client) {
|
package/dist/config.d.ts
CHANGED
|
@@ -25,6 +25,20 @@ export declare function assertContextBootable(contextDir: string, defaultPath?:
|
|
|
25
25
|
export declare function resolveRepoRoot(): string;
|
|
26
26
|
export declare function resolveSqliteDbPath(contextDir: string): string;
|
|
27
27
|
export declare function resolveKnowledgeDbPath(contextDir: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the acting identity's name for actor attribution (knowledge
|
|
30
|
+
* pages.author / page_revisions.actor / verifications.verifier). This is
|
|
31
|
+
* deliberately NOT a tool argument — an LLM-supplied free-text identity is
|
|
32
|
+
* spoofable (any caller could claim to be any identity), so it is derived
|
|
33
|
+
* server-side from the connection instead.
|
|
34
|
+
*
|
|
35
|
+
* LOOM_IDENTITY overrides explicitly (useful for the multi-identity
|
|
36
|
+
* knowledge service, where each bearer token maps to a known identity).
|
|
37
|
+
* Otherwise derived from the context dir's real path basename — this
|
|
38
|
+
* resolves symlink aliases (e.g. `~/.config/loom/default` -> the real
|
|
39
|
+
* `.../loom/art` dir) to the actual identity name rather than the alias.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveIdentityName(contextDir: string): string;
|
|
28
42
|
/**
|
|
29
43
|
* Local-only observation logs (e.g. `telemetry/recall.jsonl`). Lives inside
|
|
30
44
|
* the context dir so it travels with the agent and never leaves the machine.
|
package/dist/config.js
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* 2. --context-dir CLI argument
|
|
15
15
|
* 3. ~/.config/loom/default (fallback)
|
|
16
16
|
*/
|
|
17
|
-
import { resolve, dirname } from 'node:path';
|
|
17
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
18
18
|
import { homedir } from 'node:os';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
|
-
import { existsSync, readFileSync, writeFileSync, statSync } from 'node:fs';
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync, statSync, realpathSync } from 'node:fs';
|
|
21
21
|
/**
|
|
22
22
|
* The single canonical helper for the default context path. Every call
|
|
23
23
|
* site that needs `~/.config/loom/default` must go through this —
|
|
@@ -70,6 +70,29 @@ export function resolveSqliteDbPath(contextDir) {
|
|
|
70
70
|
export function resolveKnowledgeDbPath(contextDir) {
|
|
71
71
|
return process.env.LOOM_KNOWLEDGE_DB_PATH ?? resolve(contextDir, 'knowledge.db');
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the acting identity's name for actor attribution (knowledge
|
|
75
|
+
* pages.author / page_revisions.actor / verifications.verifier). This is
|
|
76
|
+
* deliberately NOT a tool argument — an LLM-supplied free-text identity is
|
|
77
|
+
* spoofable (any caller could claim to be any identity), so it is derived
|
|
78
|
+
* server-side from the connection instead.
|
|
79
|
+
*
|
|
80
|
+
* LOOM_IDENTITY overrides explicitly (useful for the multi-identity
|
|
81
|
+
* knowledge service, where each bearer token maps to a known identity).
|
|
82
|
+
* Otherwise derived from the context dir's real path basename — this
|
|
83
|
+
* resolves symlink aliases (e.g. `~/.config/loom/default` -> the real
|
|
84
|
+
* `.../loom/art` dir) to the actual identity name rather than the alias.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveIdentityName(contextDir) {
|
|
87
|
+
if (process.env.LOOM_IDENTITY)
|
|
88
|
+
return process.env.LOOM_IDENTITY;
|
|
89
|
+
try {
|
|
90
|
+
return basename(realpathSync(contextDir));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return basename(contextDir);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
73
96
|
/**
|
|
74
97
|
* Local-only observation logs (e.g. `telemetry/recall.jsonl`). Lives inside
|
|
75
98
|
* the context dir so it travels with the agent and never leaves the machine.
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Loom — CLI + stdio MCP entry point.
|
|
4
4
|
*
|
|
5
|
-
* When
|
|
6
|
-
* src/cli/index.ts. Otherwise (or if argv is
|
|
7
|
-
* through to the MCP stdio server so existing
|
|
8
|
-
* working.
|
|
5
|
+
* When the first non-global token is a known CLI subcommand or
|
|
6
|
+
* --help/--version, routes to src/cli/index.ts. Otherwise (or if argv is
|
|
7
|
+
* empty / only flags), falls through to the MCP stdio server so existing
|
|
8
|
+
* .mcp.json configs keep working.
|
|
9
|
+
*
|
|
10
|
+
* "First non-global token", not argv[2]: --context-dir and friends are
|
|
11
|
+
* documented as global flags, so `loom --context-dir DIR knowledge write`
|
|
12
|
+
* has to reach the CLI. Routing on argv[2] alone sent it to the stdio
|
|
13
|
+
* server instead, which read an empty stdin and exited 0 — a write verb
|
|
14
|
+
* reporting success having never entered the write path (t-665).
|
|
9
15
|
*
|
|
10
16
|
* Configure via environment variables:
|
|
11
17
|
* LOOM_CONTEXT_DIR — path to identity/memory directory (required)
|
|
@@ -16,26 +22,40 @@
|
|
|
16
22
|
*/
|
|
17
23
|
import { realpathSync } from 'node:fs';
|
|
18
24
|
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import { StdioServerTransport } from '@modelcontextprotocol/
|
|
25
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
20
26
|
import { createLoomServer } from './server.js';
|
|
21
27
|
import { resolveContextDir } from './config.js';
|
|
22
28
|
import { SUBCOMMANDS } from './cli/subcommands.js';
|
|
29
|
+
import { firstNonGlobalIndex } from './cli/args.js';
|
|
30
|
+
import { findPiece } from './cli/pieces.js';
|
|
23
31
|
const CLI_KEYWORDS = new Set(SUBCOMMANDS);
|
|
24
32
|
function isCliInvocation(argv) {
|
|
25
|
-
const
|
|
33
|
+
const args = argv.slice(2);
|
|
34
|
+
const first = args[firstNonGlobalIndex(args)];
|
|
26
35
|
if (first === undefined)
|
|
27
36
|
return false;
|
|
28
37
|
if (first === '--help' || first === '-h')
|
|
29
38
|
return true;
|
|
30
39
|
if (first === '--version' || first === '-V')
|
|
31
40
|
return true;
|
|
32
|
-
|
|
41
|
+
if (CLI_KEYWORDS.has(first))
|
|
42
|
+
return true;
|
|
43
|
+
// An installed piece (loom-<first> on PATH) is a CLI call too. Anything else
|
|
44
|
+
// still falls through to the stdio server, exactly as before.
|
|
45
|
+
return findPiece(first, process.env) !== undefined;
|
|
33
46
|
}
|
|
34
47
|
export { isCliInvocation };
|
|
35
48
|
async function main() {
|
|
36
49
|
if (isCliInvocation(process.argv)) {
|
|
37
50
|
const { runCli } = await import('./cli/index.js');
|
|
38
|
-
|
|
51
|
+
const code = await runCli(process.argv.slice(2));
|
|
52
|
+
// Drain stdout before exiting — without this, large outputs (e.g.
|
|
53
|
+
// `loom memory list --json` on a big store) truncate at 64 KiB when
|
|
54
|
+
// piped because process.exit() tears down the stream before the OS
|
|
55
|
+
// buffer drains (t-198). process.stdout.write('', cb) fires after all
|
|
56
|
+
// prior writes have been handed to the OS.
|
|
57
|
+
process.stdout.write('', () => process.exit(code));
|
|
58
|
+
return;
|
|
39
59
|
}
|
|
40
60
|
const contextDir = resolveContextDir();
|
|
41
61
|
const { server } = createLoomServer({ contextDir });
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
* Harness preset table for `loom inject`. Each entry names a target
|
|
3
3
|
* harness, its canonical default path, and the MCP tool prefix to emit
|
|
4
4
|
* in the injected instruction block.
|
|
5
|
+
*
|
|
6
|
+
* toolPrefix is NOT redeclared here — it's read from
|
|
7
|
+
* `src/install/harnesses.ts`'s INSTALL_TARGETS, which is the single
|
|
8
|
+
* source of truth for the prefix per harness (t-397: the two tables used
|
|
9
|
+
* to carry independent, disagreeing hardcoded values for codex).
|
|
5
10
|
*/
|
|
6
11
|
import { homedir } from 'node:os';
|
|
7
12
|
import { join } from 'node:path';
|
|
13
|
+
import { INSTALL_TARGETS } from '../install/harnesses.js';
|
|
8
14
|
export const HARNESS_KEYS = [
|
|
9
15
|
'claude-code',
|
|
10
16
|
'codex',
|
|
@@ -15,19 +21,19 @@ export const HARNESSES = {
|
|
|
15
21
|
key: 'claude-code',
|
|
16
22
|
display: 'Claude Code',
|
|
17
23
|
defaultPath: join(homedir(), '.claude', 'CLAUDE.md'),
|
|
18
|
-
toolPrefix: '
|
|
24
|
+
toolPrefix: INSTALL_TARGETS['claude-code'].toolPrefix,
|
|
19
25
|
},
|
|
20
26
|
'codex': {
|
|
21
27
|
key: 'codex',
|
|
22
28
|
display: 'Codex',
|
|
23
29
|
defaultPath: join(homedir(), '.codex', 'AGENTS.md'),
|
|
24
|
-
toolPrefix: '
|
|
30
|
+
toolPrefix: INSTALL_TARGETS['codex'].toolPrefix,
|
|
25
31
|
},
|
|
26
32
|
'gemini-cli': {
|
|
27
33
|
key: 'gemini-cli',
|
|
28
34
|
display: 'Gemini CLI',
|
|
29
35
|
defaultPath: join(homedir(), '.gemini', 'GEMINI.md'),
|
|
30
|
-
toolPrefix: '
|
|
36
|
+
toolPrefix: INSTALL_TARGETS['gemini-cli'].toolPrefix,
|
|
31
37
|
},
|
|
32
38
|
};
|
|
33
39
|
export function isHarnessKey(s) {
|
|
@@ -27,7 +27,9 @@ export const INSTALL_TARGETS = {
|
|
|
27
27
|
mcpConfigHint: '~/.codex/config.toml',
|
|
28
28
|
invoke: 'use the loom-setup skill',
|
|
29
29
|
restart: 'restart the Codex session',
|
|
30
|
-
|
|
30
|
+
// mcp__loom__, not mcp_loom_: a live Codex session's own tool catalog
|
|
31
|
+
// names it mcp__loom__identity (t-397, verified 2026-09-05 by mark).
|
|
32
|
+
toolPrefix: 'mcp__loom__',
|
|
31
33
|
},
|
|
32
34
|
'gemini-cli': {
|
|
33
35
|
key: 'gemini-cli',
|
|
@@ -36,6 +38,10 @@ export const INSTALL_TARGETS = {
|
|
|
36
38
|
mcpConfigHint: '~/.gemini/settings.json',
|
|
37
39
|
invoke: 'use the loom-setup skill',
|
|
38
40
|
restart: 'exit and restart Gemini CLI',
|
|
41
|
+
// mcp_loom_, single underscores: gemini-cli joins server + '_' + tool
|
|
42
|
+
// and prepends 'mcp_' (MCP_QUALIFIED_NAME_SEPARATOR / generateValidName in
|
|
43
|
+
// packages/core/src/tools/mcp-tool.ts @ b25669d), so the model sees
|
|
44
|
+
// mcp_loom_identity (t-397, caught by mark in review of #122).
|
|
39
45
|
toolPrefix: 'mcp_loom_',
|
|
40
46
|
},
|
|
41
47
|
'opencode': {
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
export interface LoomServerConfig {
|
|
3
3
|
contextDir: string;
|
|
4
|
+
/**
|
|
5
|
+
* Optional path to the knowledge-gaps directory (e.g. `~/Art/.knowledge-gaps`).
|
|
6
|
+
* When set, zero-result knowledge_recall calls append a miss entry to
|
|
7
|
+
* `${gapsDir}/recall-miss.jsonl`, feeding the expansion engine's second
|
|
8
|
+
* signal channel. Set via LOOM_GAPS_DIR env var in production.
|
|
9
|
+
*/
|
|
10
|
+
gapsDir?: string;
|
|
4
11
|
}
|
|
5
12
|
export interface LoomServerInstance {
|
|
6
13
|
server: McpServer;
|