chat-recall 0.5.32 → 0.6.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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=mcp-relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-relay.d.ts","sourceRoot":"","sources":["../src/mcp-relay.ts"],"names":[],"mappings":""}
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
3
+
4
+ // src/mcp-relay.ts
5
+ import { connect } from "node:net";
6
+ import { spawn } from "node:child_process";
7
+ import { openSync } from "node:fs";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ // src/mcp-socket.ts
11
+ import { createHash } from "node:crypto";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { accessSync, chmodSync, constants, mkdirSync, statSync } from "node:fs";
15
+ import { dirname } from "node:path";
16
+ var MAX_PATH = 100;
17
+ var fits = (dir) => dir.length + 1 + 16 + 5 <= MAX_PATH;
18
+ function socketDirCandidates() {
19
+ if (process.platform === "win32") {
20
+ const out2 = [];
21
+ if (process.env.LOCALAPPDATA) out2.push(join(process.env.LOCALAPPDATA, "chat-recall", "run"));
22
+ if (process.env.TEMP) out2.push(join(process.env.TEMP, "chat-recall-run"));
23
+ out2.push(join(tmpdir(), "chat-recall-run"));
24
+ return out2;
25
+ }
26
+ const out = [];
27
+ const xdg = process.env.XDG_RUNTIME_DIR;
28
+ if (xdg && xdg.length > 0) {
29
+ const p = join(xdg, "chat-recall");
30
+ if (fits(p)) out.push(p);
31
+ }
32
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
33
+ const named = join(tmpdir(), `chat-recall-${uid}`);
34
+ if (fits(named)) out.push(named);
35
+ const last = `/tmp/chat-recall-${uid}`;
36
+ if (!out.includes(last)) out.push(last);
37
+ return out;
38
+ }
39
+ var MAX_MISSING_LEVELS = 8;
40
+ function isDir(path) {
41
+ try {
42
+ return statSync(path).isDirectory();
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ function usableDir(dir) {
48
+ if (!isDir(dir)) {
49
+ const missing = [];
50
+ let cur = dir;
51
+ let base = false;
52
+ while (missing.length < MAX_MISSING_LEVELS) {
53
+ const parent = dirname(cur);
54
+ if (parent === cur) break;
55
+ missing.push(cur);
56
+ if (isDir(parent)) {
57
+ base = true;
58
+ break;
59
+ }
60
+ cur = parent;
61
+ }
62
+ if (!base) return false;
63
+ for (let i = missing.length - 1; i >= 0; i--) {
64
+ try {
65
+ mkdirSync(missing[i]);
66
+ } catch {
67
+ }
68
+ if (!isDir(missing[i])) return false;
69
+ }
70
+ }
71
+ try {
72
+ accessSync(dir, constants.W_OK);
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+ function privateDir(dir) {
79
+ if (process.platform === "win32") return true;
80
+ try {
81
+ chmodSync(dir, 448);
82
+ return (statSync(dir).mode & 63) === 0;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+ function ensureSocketDir() {
88
+ const candidates = socketDirCandidates();
89
+ for (const dir of candidates) {
90
+ if (!usableDir(dir)) continue;
91
+ if (!privateDir(dir)) continue;
92
+ return dir;
93
+ }
94
+ throw new Error(`no private runtime directory for the MCP daemon (tried ${candidates.join(", ")})`);
95
+ }
96
+ function socketSlug(version) {
97
+ const key = [
98
+ process.env.CHAT_RECALL_MCP_PROFILE ?? "",
99
+ process.env.CHAT_RECALL_SERVER ?? "",
100
+ version
101
+ ].join(" ");
102
+ return createHash("sha256").update(key).digest("hex").slice(0, 16);
103
+ }
104
+ function socketPath(dir, version) {
105
+ const slug = socketSlug(version);
106
+ if (process.platform === "win32") return "\\\\.\\pipe\\chat-recall-" + slug;
107
+ return join(dir, `${slug}.sock`);
108
+ }
109
+ function logPath(dir, version) {
110
+ return join(dir, `${socketSlug(version)}.log`);
111
+ }
112
+ var SOCKET_FLAG = "--socket";
113
+ function daemonEnabled() {
114
+ const v = process.env.CHAT_RECALL_NO_DAEMON;
115
+ if (v === void 0) return true;
116
+ const t = v.trim();
117
+ return t === "" || t === "0" || t === "false";
118
+ }
119
+
120
+ // src/mcp-relay.ts
121
+ var VERSION = true ? "0.6.0" : "0.0.0";
122
+ var SERVER_URL = new URL("./mcp.js", import.meta.url);
123
+ var SERVER_PATH = fileURLToPath(SERVER_URL);
124
+ var START_TIMEOUT_MS = 15e3;
125
+ var RETRY_MS = 100;
126
+ async function serveInProcess() {
127
+ const spec = SERVER_URL.href;
128
+ await import(spec);
129
+ }
130
+ function tryConnect(path) {
131
+ return new Promise((resolve) => {
132
+ const sock = connect(path);
133
+ const give_up = () => {
134
+ sock.destroy();
135
+ resolve(null);
136
+ };
137
+ sock.once("connect", () => {
138
+ sock.removeListener("error", give_up);
139
+ resolve(sock);
140
+ });
141
+ sock.once("error", give_up);
142
+ });
143
+ }
144
+ function startDaemon(dir, sock) {
145
+ try {
146
+ let err = "ignore";
147
+ try {
148
+ err = openSync(logPath(dir, VERSION), "a");
149
+ } catch {
150
+ }
151
+ const child = spawn(process.execPath, [SERVER_PATH, "--daemon", SOCKET_FLAG, sock], {
152
+ detached: true,
153
+ stdio: ["ignore", "ignore", err],
154
+ env: process.env
155
+ });
156
+ child.unref();
157
+ return child;
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+ function relay(sock) {
163
+ process.stdin.pipe(sock);
164
+ sock.pipe(process.stdout);
165
+ const done = (code) => {
166
+ try {
167
+ sock.destroy();
168
+ } catch {
169
+ }
170
+ process.exit(code);
171
+ };
172
+ process.stdin.on("end", () => sock.end());
173
+ sock.on("close", () => done(0));
174
+ sock.on("error", () => done(0));
175
+ }
176
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
177
+ async function main() {
178
+ if (!daemonEnabled()) {
179
+ await serveInProcess();
180
+ return;
181
+ }
182
+ let dir;
183
+ let path;
184
+ try {
185
+ dir = ensureSocketDir();
186
+ path = socketPath(dir, VERSION);
187
+ } catch {
188
+ await serveInProcess();
189
+ return;
190
+ }
191
+ const existing = await tryConnect(path);
192
+ if (existing) {
193
+ relay(existing);
194
+ return;
195
+ }
196
+ const child = startDaemon(dir, path);
197
+ if (!child) {
198
+ await serveInProcess();
199
+ return;
200
+ }
201
+ const deadline = Date.now() + START_TIMEOUT_MS;
202
+ while (Date.now() < deadline) {
203
+ await sleep(RETRY_MS);
204
+ const sock = await tryConnect(path);
205
+ if (sock) {
206
+ relay(sock);
207
+ return;
208
+ }
209
+ if (child.exitCode !== null || child.signalCode !== null) {
210
+ const sibling = await tryConnect(path);
211
+ if (sibling) {
212
+ relay(sibling);
213
+ return;
214
+ }
215
+ break;
216
+ }
217
+ }
218
+ console.error("[mcp] no daemon answered; serving this session in-process");
219
+ await serveInProcess();
220
+ }
221
+ main().catch(async (err) => {
222
+ console.error("[mcp] relay failed, serving in-process:", err?.message ?? err);
223
+ try {
224
+ await serveInProcess();
225
+ } catch (inner) {
226
+ console.error("[mcp] could not start the server at all:", inner);
227
+ process.exit(1);
228
+ }
229
+ });
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-relay.js","sourceRoot":"","sources":["../src/mcp-relay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Every directory the socket and the daemon log may live in, best first.
3
+ *
4
+ * XDG_RUNTIME_DIR first: it is per-user, mode 0700, and cleared on logout, so a
5
+ * socket never outlives the login session that made it. macOS never sets it,
6
+ * hence the per-uid directory under TMPDIR — which is also what keeps one
7
+ * user's socket out of another's reach on a shared /tmp. `/tmp` itself is the
8
+ * last resort: a TMPDIR long enough to break the path limit still leaves it.
9
+ *
10
+ * This is a LIST, not one answer, because every entry but the last comes from
11
+ * the environment. A sandbox, a read-only mount or a stale variable pointing at
12
+ * a directory this user cannot write makes the first choice fail, and one
13
+ * failed candidate must cost a session nothing: the next one is tried.
14
+ */
15
+ export declare function socketDirCandidates(): string[];
16
+ /**
17
+ * Create `dir` if it is missing, and report whether it is a directory this
18
+ * process can write afterwards. Never throws: a candidate that cannot be used
19
+ * is a reason to try the next one, not a failure.
20
+ *
21
+ * Deliberately NOT `mkdirSync(dir, { recursive: true })`. Node 23's recursive
22
+ * mkdir walks BACK to the parent whenever a component reports ENOENT and
23
+ * forward again when one exists — and a path whose parent exists while the
24
+ * child still reports ENOENT makes it oscillate between the two forever, at
25
+ * 100% of a core. `/proc/anything` does exactly that on Linux: `mkdir
26
+ * /proc/x` returns ENOENT, not EACCES. This path comes from XDG_RUNTIME_DIR or
27
+ * TMPDIR — the values this program does not control — so it must never be
28
+ * handed to that loop. (Verified on Node v23.9.0: the recursive call never
29
+ * returns; a single-level `mkdirSync` on the same path throws ENOENT at once.)
30
+ *
31
+ * This walks up to the deepest ancestor that already exists, bounded, then
32
+ * creates forward from there ONE level at a time. It never revisits a level,
33
+ * so it cannot spin.
34
+ */
35
+ export declare function usableDir(dir: string): boolean;
36
+ /**
37
+ * Make `dir` private to this user, and report whether that took effect.
38
+ *
39
+ * This is also the OWNERSHIP proof. `usableDir` says yes to anything writable,
40
+ * and on a shared /tmp that includes a directory another local user created
41
+ * first — our socket would then sit in their directory, theirs to swap out.
42
+ * chmod(2) fails with EPERM for anyone but the owner, so a chmod that
43
+ * succeeded means we own the directory; the mode check after it catches a
44
+ * filesystem that accepts chmod and ignores it (FAT, some FUSE mounts). No
45
+ * uid comparison, no window between a stat and a chmod. Windows has no POSIX
46
+ * modes; the per-user LOCALAPPDATA path is the protection there.
47
+ */
48
+ export declare function privateDir(dir: string): boolean;
49
+ /**
50
+ * The runtime directory for this user: the first candidate that can be used
51
+ * AND made private, created on the way. Throws only when NO candidate works,
52
+ * which the relay turns into serving the session in-process.
53
+ */
54
+ export declare function ensureSocketDir(): string;
55
+ /**
56
+ * Close the socket file itself to other users. connect(2) needs write
57
+ * permission on a Unix socket, so 0600 refuses every other uid even if the
58
+ * directory were traversable. Named pipes on Windows are not files.
59
+ */
60
+ export declare function restrictSocket(path: string): void;
61
+ /**
62
+ * What makes two sessions share a daemon.
63
+ *
64
+ * The profile decides WHICH TOOLS a server registers — "lean" is 27, "full" is
65
+ * all 61 — and that list is fixed when the server object is built. Two sessions
66
+ * that asked for different lists therefore cannot share one daemon; keying on it
67
+ * gives them one each. The server URL is in the key for the same reason: a
68
+ * daemon holds a connection to one backend.
69
+ *
70
+ * The version is in it because without it an upgraded package would attach to a
71
+ * daemon still running the old code — the handshake would succeed and every
72
+ * answer would come from the version the user believes they replaced.
73
+ */
74
+ export declare function socketSlug(version: string): string;
75
+ /**
76
+ * The address the daemon listens on, inside the runtime directory the caller
77
+ * resolved with `ensureSocketDir()`.
78
+ *
79
+ * Windows gets a named pipe rather than a file. Node's local-domain sockets ARE
80
+ * named pipes there, and a path outside the pipe namespace is not a valid one —
81
+ * so this is the native equivalent, not a workaround. It also needs no
82
+ * directory and leaves nothing behind when the daemon dies.
83
+ */
84
+ export declare function socketPath(dir: string, version: string): string;
85
+ /** Where a daemon's diagnostics go. Never a session's stderr: it outlives it. */
86
+ export declare function logPath(dir: string, version: string): string;
87
+ /**
88
+ * The flag the relay hands the daemon it starts. The relay already resolved a
89
+ * directory and is waiting on that exact path, so the daemon binds there rather
90
+ * than resolving again — a second resolution could land somewhere else if the
91
+ * environment shifted between the two processes.
92
+ */
93
+ export declare const SOCKET_FLAG = "--socket";
94
+ /** The path given with `--socket`, if the process was started with one. */
95
+ export declare function socketPathFromArgv(argv: readonly string[]): string | undefined;
96
+ /** True unless the user asked to keep everything inside their own session. */
97
+ export declare function daemonEnabled(): boolean;
98
+ //# sourceMappingURL=mcp-socket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-socket.d.ts","sourceRoot":"","sources":["../src/mcp-socket.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAoB9C;AAeD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAmC9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ/C;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAOjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAOlD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,aAAa,CAAC;AAEtC,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAK9E;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,IAAI,OAAO,CAKvC"}
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Where a chat-recall MCP daemon listens, and how a session reaches it.
3
+ *
4
+ * One daemon per (profile, server, version) — not one per session. Every MCP
5
+ * session on a machine used to be a full copy of this package: the whole bundle
6
+ * loaded, its own background sync loop, its own skills check. They now share
7
+ * one, and each session is a thin relay onto it.
8
+ *
9
+ * Imported by BOTH the relay and the server, so it may use Node builtins only.
10
+ * Anything heavier here lands in the relay bundle and undoes the point of it.
11
+ */
12
+ import { createHash } from 'node:crypto';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { accessSync, chmodSync, constants, mkdirSync, statSync } from 'node:fs';
16
+ import { dirname } from 'node:path';
17
+ /** POSIX allows 108 bytes for a socket path, macOS 104. Stay well inside. */
18
+ const MAX_PATH = 100;
19
+ /** Room a candidate directory must leave for `/<16-hex slug>.sock`. */
20
+ const fits = (dir) => dir.length + 1 + 16 + 5 <= MAX_PATH;
21
+ /**
22
+ * Every directory the socket and the daemon log may live in, best first.
23
+ *
24
+ * XDG_RUNTIME_DIR first: it is per-user, mode 0700, and cleared on logout, so a
25
+ * socket never outlives the login session that made it. macOS never sets it,
26
+ * hence the per-uid directory under TMPDIR — which is also what keeps one
27
+ * user's socket out of another's reach on a shared /tmp. `/tmp` itself is the
28
+ * last resort: a TMPDIR long enough to break the path limit still leaves it.
29
+ *
30
+ * This is a LIST, not one answer, because every entry but the last comes from
31
+ * the environment. A sandbox, a read-only mount or a stale variable pointing at
32
+ * a directory this user cannot write makes the first choice fail, and one
33
+ * failed candidate must cost a session nothing: the next one is tried.
34
+ */
35
+ export function socketDirCandidates() {
36
+ if (process.platform === 'win32') {
37
+ const out = [];
38
+ if (process.env.LOCALAPPDATA)
39
+ out.push(join(process.env.LOCALAPPDATA, 'chat-recall', 'run'));
40
+ if (process.env.TEMP)
41
+ out.push(join(process.env.TEMP, 'chat-recall-run'));
42
+ out.push(join(tmpdir(), 'chat-recall-run'));
43
+ return out;
44
+ }
45
+ const out = [];
46
+ const xdg = process.env.XDG_RUNTIME_DIR;
47
+ if (xdg && xdg.length > 0) {
48
+ const p = join(xdg, 'chat-recall');
49
+ if (fits(p))
50
+ out.push(p);
51
+ }
52
+ const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
53
+ const named = join(tmpdir(), `chat-recall-${uid}`);
54
+ if (fits(named))
55
+ out.push(named);
56
+ const last = `/tmp/chat-recall-${uid}`;
57
+ if (!out.includes(last))
58
+ out.push(last);
59
+ return out;
60
+ }
61
+ /** How many missing levels this will build before giving up. A runtime
62
+ * directory is one or two below something that already exists; deeper than
63
+ * this means the path is wrong, not a tree worth creating. */
64
+ const MAX_MISSING_LEVELS = 8;
65
+ function isDir(path) {
66
+ try {
67
+ return statSync(path).isDirectory();
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * Create `dir` if it is missing, and report whether it is a directory this
75
+ * process can write afterwards. Never throws: a candidate that cannot be used
76
+ * is a reason to try the next one, not a failure.
77
+ *
78
+ * Deliberately NOT `mkdirSync(dir, { recursive: true })`. Node 23's recursive
79
+ * mkdir walks BACK to the parent whenever a component reports ENOENT and
80
+ * forward again when one exists — and a path whose parent exists while the
81
+ * child still reports ENOENT makes it oscillate between the two forever, at
82
+ * 100% of a core. `/proc/anything` does exactly that on Linux: `mkdir
83
+ * /proc/x` returns ENOENT, not EACCES. This path comes from XDG_RUNTIME_DIR or
84
+ * TMPDIR — the values this program does not control — so it must never be
85
+ * handed to that loop. (Verified on Node v23.9.0: the recursive call never
86
+ * returns; a single-level `mkdirSync` on the same path throws ENOENT at once.)
87
+ *
88
+ * This walks up to the deepest ancestor that already exists, bounded, then
89
+ * creates forward from there ONE level at a time. It never revisits a level,
90
+ * so it cannot spin.
91
+ */
92
+ export function usableDir(dir) {
93
+ if (!isDir(dir)) {
94
+ // Collect missing components up to the first existing ancestor.
95
+ const missing = [];
96
+ let cur = dir;
97
+ let base = false;
98
+ while (missing.length < MAX_MISSING_LEVELS) {
99
+ const parent = dirname(cur);
100
+ if (parent === cur)
101
+ break; // reached the root
102
+ missing.push(cur);
103
+ if (isDir(parent)) {
104
+ base = true;
105
+ break;
106
+ }
107
+ cur = parent;
108
+ }
109
+ // No existing directory within the bound: the path is not somewhere this
110
+ // program should be building a tree.
111
+ if (!base)
112
+ return false;
113
+ for (let i = missing.length - 1; i >= 0; i--) {
114
+ try {
115
+ mkdirSync(missing[i]); // single level — throws ENOENT rather than spins
116
+ }
117
+ catch {
118
+ /* judged by what exists, not by what mkdir said (EEXIST is fine) */
119
+ }
120
+ if (!isDir(missing[i]))
121
+ return false;
122
+ }
123
+ }
124
+ try {
125
+ accessSync(dir, constants.W_OK);
126
+ return true;
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
132
+ /**
133
+ * Make `dir` private to this user, and report whether that took effect.
134
+ *
135
+ * This is also the OWNERSHIP proof. `usableDir` says yes to anything writable,
136
+ * and on a shared /tmp that includes a directory another local user created
137
+ * first — our socket would then sit in their directory, theirs to swap out.
138
+ * chmod(2) fails with EPERM for anyone but the owner, so a chmod that
139
+ * succeeded means we own the directory; the mode check after it catches a
140
+ * filesystem that accepts chmod and ignores it (FAT, some FUSE mounts). No
141
+ * uid comparison, no window between a stat and a chmod. Windows has no POSIX
142
+ * modes; the per-user LOCALAPPDATA path is the protection there.
143
+ */
144
+ export function privateDir(dir) {
145
+ if (process.platform === 'win32')
146
+ return true;
147
+ try {
148
+ chmodSync(dir, 0o700);
149
+ return (statSync(dir).mode & 0o077) === 0;
150
+ }
151
+ catch {
152
+ return false;
153
+ }
154
+ }
155
+ /**
156
+ * The runtime directory for this user: the first candidate that can be used
157
+ * AND made private, created on the way. Throws only when NO candidate works,
158
+ * which the relay turns into serving the session in-process.
159
+ */
160
+ export function ensureSocketDir() {
161
+ const candidates = socketDirCandidates();
162
+ for (const dir of candidates) {
163
+ if (!usableDir(dir))
164
+ continue;
165
+ if (!privateDir(dir))
166
+ continue;
167
+ return dir;
168
+ }
169
+ throw new Error(`no private runtime directory for the MCP daemon (tried ${candidates.join(', ')})`);
170
+ }
171
+ /**
172
+ * Close the socket file itself to other users. connect(2) needs write
173
+ * permission on a Unix socket, so 0600 refuses every other uid even if the
174
+ * directory were traversable. Named pipes on Windows are not files.
175
+ */
176
+ export function restrictSocket(path) {
177
+ if (process.platform === 'win32')
178
+ return;
179
+ try {
180
+ chmodSync(path, 0o600);
181
+ }
182
+ catch {
183
+ /* the directory is already 0700; this is the second lock, not the only one */
184
+ }
185
+ }
186
+ /**
187
+ * What makes two sessions share a daemon.
188
+ *
189
+ * The profile decides WHICH TOOLS a server registers — "lean" is 27, "full" is
190
+ * all 61 — and that list is fixed when the server object is built. Two sessions
191
+ * that asked for different lists therefore cannot share one daemon; keying on it
192
+ * gives them one each. The server URL is in the key for the same reason: a
193
+ * daemon holds a connection to one backend.
194
+ *
195
+ * The version is in it because without it an upgraded package would attach to a
196
+ * daemon still running the old code — the handshake would succeed and every
197
+ * answer would come from the version the user believes they replaced.
198
+ */
199
+ export function socketSlug(version) {
200
+ const key = [
201
+ process.env.CHAT_RECALL_MCP_PROFILE ?? '',
202
+ process.env.CHAT_RECALL_SERVER ?? '',
203
+ version,
204
+ ].join(' ');
205
+ return createHash('sha256').update(key).digest('hex').slice(0, 16);
206
+ }
207
+ /**
208
+ * The address the daemon listens on, inside the runtime directory the caller
209
+ * resolved with `ensureSocketDir()`.
210
+ *
211
+ * Windows gets a named pipe rather than a file. Node's local-domain sockets ARE
212
+ * named pipes there, and a path outside the pipe namespace is not a valid one —
213
+ * so this is the native equivalent, not a workaround. It also needs no
214
+ * directory and leaves nothing behind when the daemon dies.
215
+ */
216
+ export function socketPath(dir, version) {
217
+ const slug = socketSlug(version);
218
+ if (process.platform === 'win32')
219
+ return '\\\\.\\pipe\\chat-recall-' + slug;
220
+ return join(dir, `${slug}.sock`);
221
+ }
222
+ /** Where a daemon's diagnostics go. Never a session's stderr: it outlives it. */
223
+ export function logPath(dir, version) {
224
+ return join(dir, `${socketSlug(version)}.log`);
225
+ }
226
+ /**
227
+ * The flag the relay hands the daemon it starts. The relay already resolved a
228
+ * directory and is waiting on that exact path, so the daemon binds there rather
229
+ * than resolving again — a second resolution could land somewhere else if the
230
+ * environment shifted between the two processes.
231
+ */
232
+ export const SOCKET_FLAG = '--socket';
233
+ /** The path given with `--socket`, if the process was started with one. */
234
+ export function socketPathFromArgv(argv) {
235
+ const i = argv.indexOf(SOCKET_FLAG);
236
+ if (i === -1)
237
+ return undefined;
238
+ const v = argv[i + 1];
239
+ return v && !v.startsWith('--') ? v : undefined;
240
+ }
241
+ /** True unless the user asked to keep everything inside their own session. */
242
+ export function daemonEnabled() {
243
+ const v = process.env.CHAT_RECALL_NO_DAEMON;
244
+ if (v === undefined)
245
+ return true;
246
+ const t = v.trim();
247
+ return t === '' || t === '0' || t === 'false';
248
+ }
249
+ //# sourceMappingURL=mcp-socket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-socket.js","sourceRoot":"","sources":["../src/mcp-socket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,6EAA6E;AAC7E,MAAM,QAAQ,GAAG,GAAG,CAAC;AAErB,uEAAuE;AACvE,MAAM,IAAI,GAAG,CAAC,GAAW,EAAW,EAAE,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,QAAQ,CAAC;AAE3E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY;YAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC;QAC7F,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI;YAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC;QAC1E,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC;QAC5C,OAAO,GAAG,CAAC;IACb,CAAC;IACD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IACxC,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QACnC,IAAI,IAAI,CAAC,CAAC,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,eAAe,GAAG,EAAE,CAAC,CAAC;IACnD,IAAI,IAAI,CAAC,KAAK,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,oBAAoB,GAAG,EAAE,CAAC;IACvC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;+DAE+D;AAC/D,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAE7B,SAAS,KAAK,CAAC,IAAY;IACzB,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAChB,gEAAgE;QAChE,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,IAAI,GAAG,GAAG,GAAG,CAAC;QACd,IAAI,IAAI,GAAG,KAAK,CAAC;QACjB,OAAO,OAAO,CAAC,MAAM,GAAG,kBAAkB,EAAE,CAAC;YAC3C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,MAAM,KAAK,GAAG;gBAAE,MAAM,CAAC,mBAAmB;YAC9C,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClB,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClB,IAAI,GAAG,IAAI,CAAC;gBACZ,MAAM;YACR,CAAC;YACD,GAAG,GAAG,MAAM,CAAC;QACf,CAAC;QACD,yEAAyE;QACzE,qCAAqC;QACrC,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC;QAExB,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iDAAiD;YAC1E,CAAC;YAAC,MAAM,CAAC;gBACP,oEAAoE;YACtE,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;QACvC,CAAC;IACH,CAAC;IACD,IAAI,CAAC;QACH,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QAChC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAC9C,IAAI,CAAC;QACH,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,UAAU,GAAG,mBAAmB,EAAE,CAAC;IACzC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,SAAS;QAC9B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC/B,OAAO,GAAG,CAAC;IACb,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,0DAA0D,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACtG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO;IACzC,IAAI,CAAC;QACH,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,8EAA8E;IAChF,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,MAAM,GAAG,GAAG;QACV,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,EAAE;QACzC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE;QACpC,OAAO;KACR,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,OAAe;IACrD,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,2BAA2B,GAAG,IAAI,CAAC;IAC5E,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;AACnC,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,OAAe;IAClD,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AAEtC,2EAA2E;AAC3E,MAAM,UAAU,kBAAkB,CAAC,IAAuB;IACxD,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACpC,IAAI,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACtB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa;IAC3B,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;IAC5C,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACnB,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,OAAO,CAAC;AAChD,CAAC"}