@eleboucher/pi-memini 0.6.12 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +45 -37
  2. package/dist/index.js +542 -545
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,168 +1,8 @@
1
1
  // src/index.ts
2
2
  import { Type } from "typebox";
3
-
4
- // ../../../packages/namespace-resolver/src/index.ts
5
- import { execSync } from "node:child_process";
6
- import { basename } from "node:path";
7
- import { homedir } from "node:os";
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- var DEFAULT_TEMPLATE = "{tenant}/{project}/{agent}";
11
- function defaultConfigPath() {
12
- const xdg = process.env["XDG_CONFIG_HOME"];
13
- const base = xdg && xdg.trim() ? xdg : path.join(homedir(), ".config");
14
- return path.join(base, "memini", "config.json");
15
- }
16
- function readConfig(configPath) {
17
- const p = configPath || defaultConfigPath();
18
- try {
19
- const raw = fs.readFileSync(p, "utf8");
20
- const parsed = JSON.parse(raw);
21
- return {
22
- tenantRoots: Array.isArray(parsed.tenantRoots) ? parsed.tenantRoots : [],
23
- template: typeof parsed.template === "string" ? parsed.template : DEFAULT_TEMPLATE,
24
- overrides: parsed.overrides && typeof parsed.overrides === "object" ? parsed.overrides : {},
25
- found: true
26
- };
27
- } catch {
28
- return { tenantRoots: [], template: DEFAULT_TEMPLATE, overrides: {}, found: false };
29
- }
30
- }
31
- function gitOut(args, dir) {
32
- try {
33
- return execSync(`git ${args}`, {
34
- cwd: dir,
35
- stdio: ["ignore", "pipe", "ignore"],
36
- timeout: 500
37
- }).toString().trim();
38
- } catch {
39
- return "";
40
- }
41
- }
42
- function repoNameFromRemote(url) {
43
- if (typeof url !== "string" || !url) return null;
44
- const cleaned = url.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
45
- if (!cleaned) return null;
46
- const scpMatch = cleaned.match(/^[^/:]+:[^/]/);
47
- const p = scpMatch ? cleaned.slice(scpMatch[0].indexOf(":") + 1) : cleaned;
48
- const segs = p.split("/").filter(Boolean);
49
- return segs.length ? segs[segs.length - 1] : null;
50
- }
51
- function repoSlugFromRemote(url) {
52
- if (typeof url !== "string" || !url) return null;
53
- const cleaned = url.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
54
- if (!cleaned) return null;
55
- const scpMatch = cleaned.match(/^[^/:]+:[^/]/);
56
- const p = scpMatch ? cleaned.slice(scpMatch[0].indexOf(":") + 1) : cleaned;
57
- const segs = p.split("/").filter(Boolean);
58
- if (!segs.length) return null;
59
- if (segs.length === 1) return segs[0];
60
- const owner = segs[segs.length - 2].replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
61
- const repo = segs[segs.length - 1];
62
- return owner ? `${owner}-${repo}` : repo;
63
- }
64
- function expandTilde(p) {
65
- if (p === "~") return homedir();
66
- if (p.startsWith("~/")) return path.join(homedir(), p.slice(2));
67
- return p;
68
- }
69
- function sanitizeSegment(s) {
70
- return s.replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "");
71
- }
72
- function resolveTenant(cwd, config) {
73
- const resolvedCwd = path.resolve(cwd);
74
- for (const root of config.tenantRoots) {
75
- if (!root || typeof root !== "object") continue;
76
- if (typeof root.path !== "string" || !root.path) continue;
77
- const rootPath = path.resolve(expandTilde(root.path));
78
- if (resolvedCwd === rootPath || resolvedCwd.startsWith(rootPath + path.sep)) {
79
- const t = sanitizeSegment(String(root.tenant || ""));
80
- if (t) return t;
81
- }
82
- }
83
- return void 0;
84
- }
85
- function resolveProject(cwd, gitRemoteUrl, env) {
86
- const ownerRepo = (env?.["MEMINI_NAMESPACE_SCOPE"] || "").trim() === "owner-repo";
87
- let remote = gitRemoteUrl;
88
- if (!remote) {
89
- remote = gitOut("remote get-url origin", cwd);
90
- }
91
- if (remote) {
92
- const name = ownerRepo ? repoSlugFromRemote(remote) : repoNameFromRemote(remote);
93
- if (name) return sanitizeSegment(name);
94
- }
95
- const toplevel = gitOut("rev-parse --show-toplevel", cwd);
96
- if (toplevel) return sanitizeSegment(basename(toplevel));
97
- const b = basename(cwd);
98
- return sanitizeSegment(b) || "default";
99
- }
100
- function resolveAgent(opts) {
101
- const agent = (opts.agentId || opts.env?.["MEMINI_AGENT"] || "").trim();
102
- if (!agent) return void 0;
103
- return sanitizeSegment(agent);
104
- }
105
- function resolveHome(env) {
106
- const home = (env["MEMINI_HOME"] || "").trim();
107
- return home || void 0;
108
- }
109
- function applyTemplate(template, segments) {
110
- const all = {
111
- tenant: segments.tenant,
112
- project: segments.project,
113
- agent: segments.agent,
114
- namespace: segments.namespace
115
- };
116
- let result = template.replace(/\{(tenant|project|agent|namespace)\}/g, (_, key) => {
117
- return all[key] ?? "";
118
- });
119
- result = result.replace(/\/{2,}/g, "/");
120
- result = result.replace(/^\/+|\/+$/g, "");
121
- return result;
122
- }
123
- function resolveNamespace(opts) {
124
- const env = opts.env || process.env;
125
- const cwd = opts.cwd && opts.cwd.trim() ? opts.cwd : process.cwd();
126
- const home = resolveHome(env);
127
- const homeSource = home ? "env" : void 0;
128
- const nsEnv = (env["MEMINI_NAMESPACE"] || "").trim();
129
- if (nsEnv) {
130
- return { namespace: nsEnv, segments: {}, source: "env", home, homeSource };
131
- }
132
- const config = readConfig(opts.configPath);
133
- if (!config.found) {
134
- const project2 = sanitizeSegment(basename(cwd));
135
- return {
136
- namespace: project2,
137
- segments: project2 ? { project: project2 } : {},
138
- source: "cwd",
139
- home,
140
- homeSource
141
- };
142
- }
143
- const tenant = resolveTenant(cwd, config);
144
- const project = resolveProject(cwd, opts.gitRemoteUrl, env);
145
- const agent = resolveAgent(opts);
146
- const override = opts.integration ? config.overrides[opts.integration] : void 0;
147
- const baseNamespace = override?.namespace;
148
- const segments = { tenant, project, agent, namespace: baseNamespace };
149
- const template = override?.template || config.template;
150
- let namespace = applyTemplate(template, segments);
151
- if (!namespace) {
152
- namespace = project || "default";
153
- }
154
- let source = "default";
155
- if (tenant) source = "config";
156
- else if (opts.gitRemoteUrl || gitOut("remote get-url origin", cwd)) source = "git";
157
- else source = "cwd";
158
- return { namespace, segments, source, home, homeSource };
159
- }
3
+ import { readFileSync } from "node:fs";
160
4
 
161
5
  // ../../../packages/memini-client/src/redact.ts
162
- var SENSITIVE = /(^|_)(KEY|TOKEN|SECRET|PASSWORD|PASS|BEARER|DSN|CREDENTIALS?)$/i;
163
- function isSensitive(name) {
164
- return SENSITIVE.test(name);
165
- }
166
6
  function redactValue(value) {
167
7
  if (!value) return "";
168
8
  if (value.length <= 12) return "***";
@@ -188,216 +28,261 @@ function validateNamespace(ns) {
188
28
  return null;
189
29
  }
190
30
 
191
- // ../../../packages/memini-client/src/override.ts
192
- import { execSync as execSync2 } from "node:child_process";
193
- import { homedir as homedir2 } from "node:os";
194
- import fs2 from "node:fs";
195
- import path2 from "node:path";
196
- var OVERRIDES_VERSION = 1;
197
- var EMPTY = { version: OVERRIDES_VERSION, overrides: {} };
198
- function defaultOverridesPath(env = process.env) {
199
- const xdg = env["XDG_CONFIG_HOME"];
200
- const base = xdg && xdg.trim() ? xdg : path2.join(homedir2(), ".config");
201
- return path2.join(base, "memini", "overrides.json");
31
+ // ../../../packages/memini-client/src/session.ts
32
+ var SESSION_CWD_TTL_MS = 6 * 60 * 60 * 1e3;
33
+
34
+ // ../../../packages/memini-client/src/settings.ts
35
+ var BEHAVIOR_KNOBS = [
36
+ { envName: "MEMINI_CAPTURE_TURNS", wireKey: "capture_turns", kind: "bool", default: true },
37
+ { envName: "MEMINI_SESSION_DIGEST", wireKey: "session_digest", kind: "bool", default: true },
38
+ { envName: "MEMINI_INLINE_EXTRACT", wireKey: "inline_extract", kind: "bool", default: true },
39
+ { envName: "MEMINI_AUTO_SAVE", wireKey: "auto_save", kind: "bool", default: true },
40
+ { envName: "MEMINI_AUTO_SAVE_INTERVAL", wireKey: "auto_save_interval", kind: "int", default: 10 },
41
+ { envName: "MEMINI_INJECT_BRIEFING_PINNED", wireKey: "inject_briefing_pinned", kind: "int", default: 5 },
42
+ { envName: "MEMINI_INJECT_BRIEFING_FACTS", wireKey: "inject_briefing_facts", kind: "int", default: 5 },
43
+ { envName: "MEMINI_INJECT_BRIEFING_PROCEDURES", wireKey: "inject_briefing_procedures", kind: "int", default: 5 },
44
+ { envName: "MEMINI_INJECT_BRIEFING_RECENT", wireKey: "inject_briefing_recent", kind: "int", default: 3 },
45
+ { envName: "MEMINI_INJECT_BRIEFING_MAX_TOK", wireKey: "inject_briefing_max_tok", kind: "int", default: 0 },
46
+ { envName: "MEMINI_INJECT_PRETOOL_ITEMS", wireKey: "inject_pretool_items", kind: "int", default: 3 },
47
+ { envName: "MEMINI_INJECT_PRETOOL_MAX_TOK", wireKey: "inject_pretool_max_tok", kind: "int", default: 0 },
48
+ { envName: "MEMINI_INJECT_PRETOOL_MIN_SCORE", wireKey: "inject_pretool_min_score", kind: "float", default: 0 },
49
+ {
50
+ envName: "MEMINI_INJECT_PRETOOL_TOOLS",
51
+ wireKey: "inject_pretool_tools",
52
+ kind: "list",
53
+ default: ["Read", "Write", "Edit", "Glob", "Grep"]
54
+ },
55
+ { envName: "MEMINI_INJECT_DEDUPE", wireKey: "inject_dedupe", kind: "bool", default: true },
56
+ { envName: "MEMINI_INJECT_LABELS", wireKey: "inject_labels", kind: "list", default: [] },
57
+ { envName: "MEMINI_RECALL", wireKey: "recall", kind: "bool", default: true },
58
+ { envName: "MEMINI_CAPTURE", wireKey: "capture", kind: "bool", default: true },
59
+ { envName: "MEMINI_RECALL_LIMIT", wireKey: "recall_limit", kind: "int", default: 3 },
60
+ { envName: "MEMINI_INJECT_RECALL_MAX_TOK", wireKey: "inject_recall_max_tok", kind: "int", default: 0 },
61
+ { envName: "MEMINI_INJECT_RECALL_MIN_SCORE", wireKey: "inject_recall_min_score", kind: "float", default: 0 },
62
+ { envName: "MEMINI_MIN_CAPTURE_CHARS", wireKey: "min_capture_chars", kind: "int", default: 0 }
63
+ ];
64
+ function parseIntKnob(raw, fallback) {
65
+ const n = Number.parseInt(raw, 10);
66
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
202
67
  }
203
- function overrideKey(cwd) {
204
- const dir = cwd && cwd.trim() ? cwd : process.cwd();
205
- try {
206
- const top = execSync2("git rev-parse --show-toplevel", {
207
- cwd: dir,
208
- stdio: ["ignore", "pipe", "ignore"],
209
- timeout: 500
210
- }).toString().trim();
211
- if (top) return path2.resolve(top);
212
- } catch {
213
- }
214
- return path2.resolve(dir);
68
+ function parseFloatKnob(raw, fallback) {
69
+ const n = Number.parseFloat(raw);
70
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
215
71
  }
216
- function readOverrides(opts = {}) {
217
- const p = opts.overridesPath || defaultOverridesPath(opts.env);
218
- try {
219
- const parsed = JSON.parse(fs2.readFileSync(p, "utf8"));
220
- if (!parsed || typeof parsed !== "object" || typeof parsed.overrides !== "object") {
221
- return { ...EMPTY, overrides: {} };
72
+ function parseListKnob(raw) {
73
+ return raw.split(/[|,]/).map((s) => s.trim().toLowerCase()).filter(Boolean);
74
+ }
75
+ function effectiveSetting(knob2, server, env = process.env) {
76
+ const raw = env[knob2.envName];
77
+ if (raw != null && raw !== "") {
78
+ let value;
79
+ switch (knob2.kind) {
80
+ case "bool":
81
+ value = !/^(0|false|no|off)$/i.test(raw.trim());
82
+ break;
83
+ case "int":
84
+ value = parseIntKnob(raw, knob2.default);
85
+ break;
86
+ case "float":
87
+ value = parseFloatKnob(raw, knob2.default);
88
+ break;
89
+ case "list":
90
+ value = parseListKnob(raw);
91
+ break;
222
92
  }
223
- return {
224
- version: typeof parsed.version === "number" ? parsed.version : OVERRIDES_VERSION,
225
- overrides: parsed.overrides || {}
226
- };
227
- } catch {
228
- return { ...EMPTY, overrides: {} };
93
+ return { value, source: "env-override" };
94
+ }
95
+ if (server && Object.prototype.hasOwnProperty.call(server, knob2.wireKey)) {
96
+ return { value: server[knob2.wireKey], source: "server" };
229
97
  }
98
+ return { value: knob2.default, source: "default" };
230
99
  }
231
- function readOverride(cwd, opts = {}) {
232
- const file = readOverrides(opts);
233
- const keys = Object.keys(file.overrides);
234
- if (keys.length === 0) return void 0;
235
- const entry = file.overrides[overrideKey(cwd)];
236
- if (!entry || typeof entry.namespace !== "string" || !entry.namespace.trim()) return void 0;
237
- return entry;
100
+
101
+ // ../../../packages/memini-client/src/bootstrap.ts
102
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
103
+ function envEnabled(raw, defaultOn) {
104
+ if (raw == null || raw === "") return defaultOn;
105
+ return !/^(0|false|no|off)$/i.test(raw.trim());
238
106
  }
239
- function writeOverride(cwd, namespace, opts = {}) {
240
- const ns = normalizeNamespace(namespace);
241
- const bad = validateNamespace(ns);
242
- if (bad) throw new Error(`invalid namespace ${JSON.stringify(namespace)}: ${bad}`);
243
- const p = opts.overridesPath || defaultOverridesPath(opts.env);
244
- const file = readOverrides(opts);
245
- const entry = {
246
- namespace: ns,
247
- setAt: (opts.now ? opts.now() : /* @__PURE__ */ new Date()).toISOString()
107
+ function readBootstrap(env = process.env) {
108
+ return {
109
+ baseUrl: env["MEMINI_BASE_URL"] || "http://localhost:8080",
110
+ apiKey: env["MEMINI_API_KEY"] || "",
111
+ requireHttps: envEnabled(env["MEMINI_REQUIRE_HTTPS"], false),
112
+ debug: envEnabled(env["MEMINI_DEBUG"], false),
113
+ agent: env["MEMINI_AGENT"] || "",
114
+ namespaceEnv: (env["MEMINI_NAMESPACE"] || "").trim(),
115
+ namespacePrefixEnv: (env["MEMINI_NAMESPACE_PREFIX"] || "").trim(),
116
+ homeEnv: (env["MEMINI_HOME"] || "").trim()
248
117
  };
249
- file.version = OVERRIDES_VERSION;
250
- file.overrides[overrideKey(cwd)] = entry;
251
- fs2.mkdirSync(path2.dirname(p), { recursive: true });
252
- fs2.writeFileSync(p, JSON.stringify(file, null, 2) + "\n");
253
- return entry;
254
118
  }
255
- function clearOverride(cwd, opts = {}) {
256
- const p = opts.overridesPath || defaultOverridesPath(opts.env);
257
- const file = readOverrides(opts);
258
- const key = overrideKey(cwd);
259
- if (!(key in file.overrides)) return false;
260
- delete file.overrides[key];
119
+ function isPlaintextBearerUnsafe(baseUrl, secret) {
120
+ if (!secret) return false;
261
121
  try {
262
- fs2.mkdirSync(path2.dirname(p), { recursive: true });
263
- fs2.writeFileSync(p, JSON.stringify(file, null, 2) + "\n");
122
+ const u = new URL(baseUrl);
123
+ return u.protocol === "http:" && !LOOPBACK_HOSTS.has(u.hostname.replace(/^\[|\]$/g, "").toLowerCase());
264
124
  } catch {
265
125
  return false;
266
126
  }
267
- return true;
127
+ }
128
+ function assertBearerTransportSafe(baseUrl, secret, env = process.env) {
129
+ if (!isPlaintextBearerUnsafe(baseUrl, secret)) return;
130
+ if (!envEnabled(env["MEMINI_REQUIRE_HTTPS"], false)) return;
131
+ throw new Error(
132
+ `memini: a bearer token is configured for plaintext HTTP to ${baseUrl}. The token and memory payloads can be observed on the network; use HTTPS or an SSH tunnel.`
133
+ );
268
134
  }
269
135
 
270
- // ../../../packages/memini-client/src/session.ts
271
- var SESSION_CWD_TTL_MS = 6 * 60 * 60 * 1e3;
272
-
273
- // ../../../packages/memini-client/src/settings.ts
274
- var CLIENT_KNOBS = [
275
- // Connection
276
- { name: "MEMINI_BASE_URL", kind: "string", default: "http://localhost:8080", usedBy: "hooks + MCP", description: "memini base URL (alias: MEMINI_URL)" },
277
- { name: "MEMINI_MCP_URL", kind: "string", default: "${MEMINI_BASE_URL}/mcp", usedBy: "MCP", description: "MCP endpoint; derived from the base URL unless set" },
278
- { name: "MEMINI_API_KEY", kind: "string", default: "", usedBy: "hooks + MCP", description: "bearer token sent to the server (alias: MEMINI_TOKEN)" },
279
- { name: "MEMINI_REQUIRE_HTTPS", kind: "bool", default: "0", usedBy: "hooks + MCP", description: "refuse to send a bearer token over plaintext HTTP" },
280
- // Namespace
281
- { name: "MEMINI_NAMESPACE", kind: "string", default: "(auto: git/cwd)", usedBy: "hooks + MCP", description: "pin the namespace; overrides git and directory detection" },
282
- { name: "MEMINI_NAMESPACE_SCOPE", kind: "string", default: "repo", usedBy: "hooks", description: "owner-repo derives owner-repo slugs from the git remote" },
283
- { name: "MEMINI_AGENT", kind: "string", default: "", usedBy: "hooks + MCP", description: "nest the namespace under a per-agent segment" },
284
- { name: "MEMINI_HOME", kind: "string", default: "", usedBy: "hooks + MCP", description: 'personal namespace; required for visibility:"personal" writes' },
285
- // Capture
286
- { name: "MEMINI_CAPTURE_TURNS", kind: "bool", default: "on", usedBy: "hooks", description: "capture each user\u2192assistant turn as episodic memory" },
287
- { name: "MEMINI_SESSION_DIGEST", kind: "bool", default: "on", usedBy: "hooks", description: "record session digests (files edited, commands run); 0 to keep memory to durable facts only" },
288
- { name: "MEMINI_INLINE_EXTRACT", kind: "bool", default: "on", usedBy: "hooks", description: "inject the memory-save directive at SessionStart" },
289
- { name: "MEMINI_AUTO_SAVE", kind: "bool", default: "on", usedBy: "hooks", description: "periodic auto-save nudge on Stop" },
290
- { name: "MEMINI_AUTO_SAVE_INTERVAL", kind: "int", default: "10", usedBy: "hooks", description: "user messages between auto-save nudges" },
291
- // Injection budgets
292
- { name: "MEMINI_INJECT_BRIEFING_PINNED", kind: "int", default: "5", usedBy: "hooks", description: "max pinned memories at SessionStart (0 disables)" },
293
- { name: "MEMINI_INJECT_BRIEFING_FACTS", kind: "int", default: "5", usedBy: "hooks", description: "max durable facts at SessionStart (0 disables)" },
294
- { name: "MEMINI_INJECT_BRIEFING_PROCEDURES", kind: "int", default: "5", usedBy: "hooks", description: "max procedural how-tos at SessionStart (0 disables)" },
295
- { name: "MEMINI_INJECT_BRIEFING_RECENT", kind: "int", default: "3", usedBy: "hooks", description: "max recent episodic entries at SessionStart (0 disables)" },
296
- { name: "MEMINI_INJECT_BRIEFING_MAX_TOK", kind: "int", default: "uncapped", usedBy: "hooks", description: "token ceiling on the SessionStart briefing" },
297
- { name: "MEMINI_INJECT_PRETOOL_ITEMS", kind: "int", default: "3", usedBy: "hooks", description: "max hits surfaced per file on PreToolUse" },
298
- { name: "MEMINI_INJECT_PRETOOL_MAX_TOK", kind: "int", default: "uncapped", usedBy: "hooks", description: "token ceiling per file on PreToolUse" },
299
- { name: "MEMINI_INJECT_PRETOOL_MIN_SCORE", kind: "float", default: "0", usedBy: "hooks", description: "relevance floor for PreToolUse hits" },
300
- { name: "MEMINI_INJECT_PRETOOL_TOOLS", kind: "list", default: "Read|Write|Edit|Glob|Grep", usedBy: "hooks", description: "tool allowlist for PreToolUse recall" },
301
- { name: "MEMINI_INJECT_LABELS", kind: "list", default: "", usedBy: "hooks", description: "annotate injected bullets: tier, confidence, age, reason" },
302
- // Diagnostics
303
- { name: "MEMINI_DEBUG", kind: "bool", default: "0", usedBy: "hooks + MCP", description: "verbose hook logging to stderr" }
304
- ];
305
- function describeKnob(spec, env) {
306
- const raw = env[spec.name];
307
- const set = raw != null && raw !== "";
308
- const sensitive = isSensitive(spec.name);
309
- let value;
310
- if (set) {
311
- value = sensitive ? redactValue(raw) : raw;
312
- } else {
313
- value = spec.default === "" ? "(unset)" : spec.default;
136
+ // ../../../packages/memini-client/src/facts.ts
137
+ import { execFileSync } from "node:child_process";
138
+ import path from "node:path";
139
+ function gitOut(args, dir) {
140
+ try {
141
+ const out = execFileSync("git", args, {
142
+ cwd: dir,
143
+ stdio: ["ignore", "pipe", "ignore"],
144
+ timeout: 500
145
+ }).toString().trim();
146
+ return out || void 0;
147
+ } catch {
148
+ return void 0;
314
149
  }
315
- return {
316
- name: spec.name,
317
- value,
318
- source: set ? "env" : "default",
319
- isDefault: !set,
320
- sensitive,
321
- usedBy: spec.usedBy,
322
- description: spec.description
150
+ }
151
+ function gatherFacts(cwd, env = process.env) {
152
+ const dir = cwd && cwd.trim() ? cwd : process.cwd();
153
+ const facts = {
154
+ cwd_basename: path.basename(dir)
323
155
  };
156
+ const remote = gitOut(["remote", "get-url", "origin"], dir);
157
+ if (remote) facts.remote_url = remote;
158
+ const toplevel = gitOut(["rev-parse", "--show-toplevel"], dir);
159
+ if (toplevel) {
160
+ facts.toplevel_path = toplevel;
161
+ facts.toplevel_basename = path.basename(toplevel);
162
+ }
163
+ const agent = env["MEMINI_AGENT"];
164
+ if (agent) facts.agent = agent;
165
+ const ns = (env["MEMINI_NAMESPACE"] || "").trim();
166
+ if (ns) facts.env_namespace = ns;
167
+ const prefix = (env["MEMINI_NAMESPACE_PREFIX"] || "").trim();
168
+ if (prefix) facts.env_namespace_prefix = prefix;
169
+ return facts;
324
170
  }
325
- var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
326
- function isPlaintextToNonLoopback(baseUrl) {
327
- try {
328
- const u = new URL(baseUrl);
329
- return u.protocol === "http:" && !LOOPBACK.has(u.hostname.replace(/^\[|\]$/g, "").toLowerCase());
330
- } catch {
331
- return false;
171
+
172
+ // ../../../packages/memini-client/src/resolve.ts
173
+ function remotePathSegments(url) {
174
+ if (typeof url !== "string" || !url) return [];
175
+ const cleaned = url.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
176
+ if (!cleaned) return [];
177
+ const scpMatch = cleaned.match(/^[^/:]+:[^/]/);
178
+ const p = scpMatch ? cleaned.slice(scpMatch[0].indexOf(":") + 1) : cleaned;
179
+ return p.split("/").filter(Boolean);
180
+ }
181
+ function repoNameFromRemote(url) {
182
+ const segs = remotePathSegments(url);
183
+ return segs.length ? segs[segs.length - 1] : void 0;
184
+ }
185
+ function repoSlugFromRemote(url) {
186
+ const segs = remotePathSegments(url);
187
+ if (!segs.length) return void 0;
188
+ if (segs.length === 1) return segs[0];
189
+ const owner = segs[segs.length - 2].replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
190
+ const repo = segs[segs.length - 1];
191
+ return owner ? `${owner}-${repo}` : repo;
192
+ }
193
+ function withAgent(ns, agent) {
194
+ const trimmed = (agent || "").trim();
195
+ if (!trimmed) return ns;
196
+ const seg = trimmed.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
197
+ return seg ? `${ns}/${seg}` : ns;
198
+ }
199
+ function deriveLocalNamespace(f, scope = "repo") {
200
+ if (f.declared_namespace) {
201
+ return { namespace: f.declared_namespace, source: "declared" };
202
+ }
203
+ let base;
204
+ let source = "default";
205
+ if (f.remote_url) {
206
+ const name = scope === "owner_repo" ? repoSlugFromRemote(f.remote_url) : repoNameFromRemote(f.remote_url);
207
+ if (name) {
208
+ base = name;
209
+ source = "remote";
210
+ }
211
+ }
212
+ if (!base && f.toplevel_basename) {
213
+ base = f.toplevel_basename;
214
+ source = "toplevel";
215
+ }
216
+ if (!base && f.cwd_basename) {
217
+ base = f.cwd_basename;
218
+ source = "cwd";
219
+ }
220
+ if (!base) {
221
+ return { namespace: "default", source: "default" };
332
222
  }
223
+ return { namespace: withAgent(base, f.agent), source };
333
224
  }
334
- function describeSettings(opts) {
335
- const env = opts.env || process.env;
336
- const cwd = opts.cwd;
337
- const override = readOverride(cwd, { env, overridesPath: opts.overridesPath });
338
- const withoutOverride = opts.resolve(env, { ignoreOverride: true });
339
- const envSansPin = { ...env };
340
- delete envSansPin["MEMINI_NAMESPACE"];
341
- const derived = opts.resolve(envSansPin, { ignoreOverride: true });
342
- const effective = override ? override.namespace : withoutOverride.namespace;
343
- const source = override ? "override" : withoutOverride.source;
344
- const home = (env["MEMINI_HOME"] || "").trim() || void 0;
345
- const settings = CLIENT_KNOBS.map((k) => describeKnob(k, env));
346
- const warnings = [];
347
- if (override) {
348
- warnings.push({
349
- level: "note",
350
- code: "override-active",
351
- message: `namespace is overridden to "${override.namespace}" for this project (set ${override.setAt}); without it this project would use "${withoutOverride.namespace}".`,
352
- fix: "Run the namespace command with --clear to return to automatic resolution."
353
- });
225
+ function resolveNamespace(boot, facts, hs) {
226
+ if (hs) {
227
+ return { namespace: hs.namespace, source: `server:${hs.namespace_source}`, degraded: false };
354
228
  }
355
- const pin = (env["MEMINI_NAMESPACE"] || "").trim();
356
- if (pin && !override && derived.namespace && derived.namespace !== pin) {
357
- warnings.push({
358
- level: "warn",
359
- code: "global-namespace-pin",
360
- message: `MEMINI_NAMESPACE is set to "${pin}", which pins EVERY project on this machine to one namespace. This project would otherwise resolve to "${derived.namespace}". If this variable is exported from a shell rc (or a fish universal variable), every repo you work in is sharing one memory pool.`,
361
- fix: `Unset MEMINI_NAMESPACE and let each repo resolve on its own, or set a per-project override instead.`
362
- });
229
+ if (boot.namespaceEnv) {
230
+ return { namespace: boot.namespaceEnv, source: "env", degraded: true };
363
231
  }
364
- if (!home) {
365
- warnings.push({
366
- level: "warn",
367
- code: "home-unset",
368
- message: 'MEMINI_HOME is unset: there is no personal namespace, so visibility:"personal" writes will error and no personal leg merges into recall.',
369
- fix: "Export MEMINI_HOME=personal/<you>."
370
- });
232
+ const { namespace, source } = deriveLocalNamespace(facts);
233
+ const prefix = boot.namespacePrefixEnv;
234
+ if (prefix && (source === "remote" || source === "toplevel" || source === "cwd")) {
235
+ return { namespace: `${prefix}/${namespace}`, source: `local-${source}`, degraded: true };
371
236
  }
372
- const baseUrl = env["MEMINI_BASE_URL"] || env["MEMINI_URL"] || "http://localhost:8080";
373
- const token = env["MEMINI_API_KEY"] || env["MEMINI_TOKEN"] || "";
374
- if (token && isPlaintextToNonLoopback(baseUrl)) {
375
- warnings.push({
376
- level: "warn",
377
- code: "plaintext-bearer",
378
- message: `a bearer token is configured for plaintext HTTP to ${baseUrl}; the token and your memory payloads can be observed on the network.`,
379
- fix: "Use HTTPS, or tunnel over SSH. Set MEMINI_REQUIRE_HTTPS=1 to make this an error."
237
+ return { namespace, source: `local-${source}`, degraded: true };
238
+ }
239
+
240
+ // ../../../packages/memini-client/src/handshake.ts
241
+ var HANDSHAKE_TTL_MS = 10 * 60 * 1e3;
242
+ async function performHandshake(boot, facts, opts = {}) {
243
+ assertBearerTransportSafe(boot.baseUrl, boot.apiKey, {
244
+ MEMINI_REQUIRE_HTTPS: boot.requireHttps ? "1" : "0"
245
+ });
246
+ const controller = new AbortController();
247
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2500);
248
+ try {
249
+ const headers = { "Content-Type": "application/json" };
250
+ if (boot.apiKey) headers["Authorization"] = `Bearer ${boot.apiKey}`;
251
+ if (boot.homeEnv) headers["X-Memini-Home"] = boot.homeEnv;
252
+ const body = JSON.stringify({
253
+ project: facts,
254
+ client: { name: opts.clientName, version: opts.clientVersion }
255
+ });
256
+ const res = await fetch(`${boot.baseUrl}/v1/handshake`, {
257
+ method: "POST",
258
+ headers,
259
+ body,
260
+ signal: controller.signal
380
261
  });
262
+ if (!res.ok) return void 0;
263
+ return await res.json();
264
+ } catch {
265
+ return void 0;
266
+ } finally {
267
+ clearTimeout(timer);
381
268
  }
382
- return {
383
- cwd,
384
- namespace: { effective, source, override, withoutOverride, derived, home },
385
- settings,
386
- paths: {
387
- overrides: opts.overridesPath || defaultOverridesPath(env),
388
- cache: opts.cacheDir
389
- },
390
- warnings
391
- };
392
269
  }
393
270
 
394
271
  // src/index.ts
395
- var DEFAULT_BASE_URL = "http://localhost:8080";
396
272
  var DEFAULT_TIMEOUT_MS = 3e4;
397
273
  var DEFAULT_RECALL_LIMIT = 3;
398
- var DEFAULT_NAMESPACE = "pi";
399
274
  var STATUS_TIMEOUT_MS = 4e3;
400
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
275
+ var HANDSHAKE_TIMEOUT_MS = 2500;
276
+ var CLIENT_NAME = "pi-memini";
277
+ function readPluginVersion() {
278
+ try {
279
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
280
+ return typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
281
+ } catch {
282
+ return "0.0.0";
283
+ }
284
+ }
285
+ var CLIENT_VERSION = readPluginVersion();
401
286
  function envBool(value, fallback) {
402
287
  if (value === void 0 || value === null || value === "") return fallback;
403
288
  return !/^(0|false|no|off)$/i.test(String(value).trim());
@@ -423,65 +308,74 @@ function labelsEnv(name = "MEMINI_INJECT_LABELS") {
423
308
  raw.split(/[|,]/).map((s) => s.trim().toLowerCase()).filter(Boolean)
424
309
  );
425
310
  }
426
- function sanitizeNamespace(s) {
427
- return String(s).trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
428
- }
429
- function sanitizeNamespacePath(s) {
430
- return String(s).split("/").map(sanitizeNamespace).filter(Boolean).join("/");
431
- }
432
- function deriveNamespace(cwd) {
433
- if (typeof cwd !== "string" || !cwd.trim()) return "";
434
- const base = cwd.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || "";
435
- return sanitizeNamespace(base);
436
- }
437
- function resolverSource(source) {
438
- return source === "git" ? "git-remote" : source;
311
+ function memoizeAsync(fn, ttlMs, now = Date.now) {
312
+ let cached = null;
313
+ return {
314
+ async get() {
315
+ const t = now();
316
+ if (!cached || t >= cached.expiresAt) {
317
+ const value = await fn();
318
+ cached = { value, expiresAt: t + ttlMs };
319
+ }
320
+ return cached.value;
321
+ },
322
+ invalidate() {
323
+ cached = null;
324
+ }
325
+ };
439
326
  }
440
- function resolveProjectNamespace(env, cwd, opts = {}) {
441
- const e = env || {};
442
- if (!opts.ignoreOverride && cwd) {
443
- const override = readOverride(cwd, { env: e });
444
- if (override) return { namespace: override.namespace, source: "override" };
445
- }
446
- const nsEnv = (e.MEMINI_NAMESPACE || "").trim();
447
- if (nsEnv) {
448
- return { namespace: nsEnv, source: "env" };
449
- }
450
- if (cwd) {
451
- const { namespace: resolvedNs, source } = resolveNamespace({
452
- cwd,
453
- env: e,
454
- integration: "pi"
327
+ async function attemptHandshake(boot, facts, fallbackOnError) {
328
+ try {
329
+ return await performHandshake(boot, facts, {
330
+ timeoutMs: HANDSHAKE_TIMEOUT_MS,
331
+ clientName: CLIENT_NAME,
332
+ clientVersion: CLIENT_VERSION
455
333
  });
456
- const sanitized = sanitizeNamespacePath(resolvedNs);
457
- if (sanitized) return { namespace: sanitized, source: resolverSource(source) };
334
+ } catch (error) {
335
+ if (!fallbackOnError) throw error;
336
+ return void 0;
458
337
  }
459
- return { namespace: DEFAULT_NAMESPACE, source: "default" };
460
338
  }
461
- function resolveConfig(env, cwd) {
339
+ function createSessionContext(cwd, env = process.env, now = Date.now) {
340
+ const e = env;
341
+ const boot = readBootstrap(e);
342
+ const facts = gatherFacts(cwd, e);
343
+ const fallbackOnError = envBool(e.MEMINI_FALLBACK, true);
344
+ const memo = memoizeAsync(() => attemptHandshake(boot, facts, fallbackOnError), HANDSHAKE_TTL_MS, now);
345
+ return { boot, facts, memo };
346
+ }
347
+ function resolveStaticConfig(env = process.env) {
462
348
  const e = env || {};
463
- const { namespace } = resolveProjectNamespace(e, cwd);
464
- const recall_limit = (() => {
465
- const n = Number(e.MEMINI_RECALL_LIMIT);
466
- return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RECALL_LIMIT;
467
- })();
468
349
  const homeEnv = (e.MEMINI_HOME || "").trim();
469
350
  return {
470
- base_url: e.MEMINI_BASE_URL || e.MEMINI_URL || DEFAULT_BASE_URL,
471
- // namespace is already resolved above (verbatim on the override/env paths,
472
- // per-segment sanitized on the resolver path); re-sanitizing here would
473
- // flatten tenant separators.
474
- namespace: namespace || DEFAULT_NAMESPACE,
475
351
  home: homeEnv || void 0,
476
- recall: envBool(e.MEMINI_RECALL, true),
477
- capture: envBool(e.MEMINI_CAPTURE, true),
478
- recall_limit,
479
- recall_max_tokens: intEnv("MEMINI_INJECT_RECALL_MAX_TOK", 0),
480
- recall_min_score: floatEnv("MEMINI_INJECT_RECALL_MIN_SCORE", 0),
481
352
  timeout_ms: Number(e.MEMINI_TIMEOUT_MS || DEFAULT_TIMEOUT_MS),
482
353
  fallback_on_error: envBool(e.MEMINI_FALLBACK, true)
483
354
  };
484
355
  }
356
+ function knob(wireKey) {
357
+ const k = BEHAVIOR_KNOBS.find((b) => b.wireKey === wireKey);
358
+ if (!k) throw new Error(`pi-memini: unknown behavior knob "${wireKey}"`);
359
+ return k;
360
+ }
361
+ function resolveLiveConfig(boot, facts, hs, env = process.env) {
362
+ const resolved = resolveNamespace(boot, facts, hs);
363
+ const server = hs?.settings;
364
+ return {
365
+ namespace: resolved.namespace,
366
+ namespace_source: resolved.source,
367
+ degraded: resolved.degraded,
368
+ recall: effectiveSetting(knob("recall"), server, env).value,
369
+ capture: effectiveSetting(knob("capture"), server, env).value,
370
+ recall_limit: effectiveSetting(knob("recall_limit"), server, env).value,
371
+ recall_max_tokens: effectiveSetting(knob("inject_recall_max_tok"), server, env).value,
372
+ recall_min_score: effectiveSetting(knob("inject_recall_min_score"), server, env).value
373
+ };
374
+ }
375
+ async function sessionLive(ctx, env = process.env) {
376
+ const hs = await ctx.memo.get();
377
+ return resolveLiveConfig(ctx.boot, ctx.facts, hs, env);
378
+ }
485
379
  function approxTokens(text) {
486
380
  if (!text) return 0;
487
381
  const words = String(text).trim().split(/\s+/).filter(Boolean).length;
@@ -535,25 +429,13 @@ function formatResults(results, limit, labels) {
535
429
  return `[${tagParts.join(" \xB7 ")}] ${text}`;
536
430
  }).filter((x) => Boolean(x));
537
431
  }
538
- function normalizedHostname(hostname) {
539
- return hostname.replace(/^\[|\]$/g, "").toLowerCase();
540
- }
541
- function usesPlaintextBearerAuth(baseUrl, secret) {
542
- if (!secret) return false;
543
- try {
544
- const parsed = new URL(baseUrl);
545
- return parsed.protocol === "http:" && !LOOPBACK_HOSTS.has(normalizedHostname(parsed.hostname));
546
- } catch {
547
- return false;
548
- }
549
- }
550
432
  function plaintextBearerAuthMessage(baseUrl) {
551
433
  return `memini: MEMINI_API_KEY is configured for plaintext HTTP to ${baseUrl}. Bearer tokens and memory payloads can be observed on the network; use HTTPS or an SSH tunnel.`;
552
434
  }
553
435
  function createPlaintextBearerAuthGuard(warn, env) {
554
436
  let warned = false;
555
437
  return function guardPlaintextBearerAuth(baseUrl, secret) {
556
- if (!usesPlaintextBearerAuth(baseUrl, secret)) return;
438
+ if (!isPlaintextBearerUnsafe(baseUrl, secret || "")) return;
557
439
  const message = plaintextBearerAuthMessage(baseUrl);
558
440
  if ((env || process.env).MEMINI_REQUIRE_HTTPS === "1") throw new Error(message);
559
441
  if (!warned) {
@@ -562,53 +444,53 @@ function createPlaintextBearerAuthGuard(warn, env) {
562
444
  }
563
445
  };
564
446
  }
565
- function createClient(cfg, warn) {
566
- const baseUrl = String(cfg.base_url).replace(/\/+$/, "");
567
- const secret = process.env.MEMINI_API_KEY || process.env.MEMINI_TOKEN;
447
+ function createClient(staticCfg, boot, warn) {
448
+ const baseUrl = String(boot.baseUrl).replace(/\/+$/, "");
449
+ const secret = boot.apiKey;
568
450
  const guard = createPlaintextBearerAuthGuard(warn);
569
- if (process.env.MEMINI_REQUIRE_HTTPS === "1") guard(baseUrl, secret);
570
- function headers(extra) {
571
- const h = { "X-Memini-Namespace": cfg.namespace, ...extra || {} };
451
+ if (boot.requireHttps) guard(baseUrl, secret);
452
+ function headers(namespace, extra) {
453
+ const h = { "X-Memini-Namespace": namespace, ...extra || {} };
572
454
  if (secret) h.Authorization = `Bearer ${secret}`;
573
- if (cfg.home) h["X-Memini-Home"] = cfg.home;
455
+ if (staticCfg.home) h["X-Memini-Home"] = staticCfg.home;
574
456
  return h;
575
457
  }
576
- async function request(method, path3, body) {
458
+ async function request(method, path2, namespace, body) {
577
459
  guard(baseUrl, secret);
578
460
  try {
579
- const res = await fetch(`${baseUrl}${path3}`, {
461
+ const res = await fetch(`${baseUrl}${path2}`, {
580
462
  method,
581
- headers: headers(body ? { "Content-Type": "application/json" } : void 0),
463
+ headers: headers(namespace, body ? { "Content-Type": "application/json" } : void 0),
582
464
  body: body ? JSON.stringify(body) : void 0,
583
- signal: AbortSignal.timeout(cfg.timeout_ms)
465
+ signal: AbortSignal.timeout(staticCfg.timeout_ms)
584
466
  });
585
467
  if (!res.ok) {
586
- if (cfg.fallback_on_error) {
587
- warn(`memini ${method} ${path3} failed: ${res.status}`);
468
+ if (staticCfg.fallback_on_error) {
469
+ warn(`memini ${method} ${path2} failed: ${res.status}`);
588
470
  return null;
589
471
  }
590
472
  const text = await res.text().catch(() => "");
591
- throw new Error(`memini ${method} ${path3} failed: ${res.status} ${text}`);
473
+ throw new Error(`memini ${method} ${path2} failed: ${res.status} ${text}`);
592
474
  }
593
475
  return await res.json().catch(() => ({ ok: true }));
594
476
  } catch (error) {
595
- if (!cfg.fallback_on_error) throw error;
477
+ if (!staticCfg.fallback_on_error) throw error;
596
478
  warn(`memini: ${String(error)}`);
597
479
  return null;
598
480
  }
599
481
  }
600
- async function requestResult(method, path3, body) {
482
+ async function requestResult(method, path2, namespace, body) {
601
483
  try {
602
484
  guard(baseUrl, secret);
603
- const res = await fetch(`${baseUrl}${path3}`, {
485
+ const res = await fetch(`${baseUrl}${path2}`, {
604
486
  method,
605
- headers: headers(body ? { "Content-Type": "application/json" } : void 0),
487
+ headers: headers(namespace, body ? { "Content-Type": "application/json" } : void 0),
606
488
  body: body ? JSON.stringify(body) : void 0,
607
- signal: AbortSignal.timeout(cfg.timeout_ms)
489
+ signal: AbortSignal.timeout(staticCfg.timeout_ms)
608
490
  });
609
491
  if (!res.ok) {
610
492
  const detail = (await res.text().catch(() => "")).trim();
611
- warn(`memini ${method} ${path3} failed: ${res.status} ${detail}`);
493
+ warn(`memini ${method} ${path2} failed: ${res.status} ${detail}`);
612
494
  return { ok: false, error: detail || `HTTP ${res.status}` };
613
495
  }
614
496
  return { ok: true, data: await res.json().catch(() => ({})) };
@@ -618,10 +500,10 @@ function createClient(cfg, warn) {
618
500
  }
619
501
  }
620
502
  return {
621
- postJson: (path3, payload) => request("POST", path3, payload),
622
- getJson: (path3) => request("GET", path3),
623
- deleteJson: (path3) => request("DELETE", path3),
624
- postJsonResult: (path3, payload) => requestResult("POST", path3, payload)
503
+ postJson: (path2, payload, namespace) => request("POST", path2, namespace, payload),
504
+ getJson: (path2, namespace) => request("GET", path2, namespace),
505
+ deleteJson: (path2, namespace) => request("DELETE", path2, namespace),
506
+ postJsonResult: (path2, payload, namespace) => requestResult("POST", path2, namespace, payload)
625
507
  };
626
508
  }
627
509
  function meminiListPath(args) {
@@ -660,37 +542,69 @@ function buildTurnContent(userText, assistantText) {
660
542
 
661
543
  ${String(assistantText).slice(0, 3e3)}`;
662
544
  }
663
- async function statusGet(cfg, namespace, path3, warn, quiet = false) {
664
- const baseUrl = String(cfg.base_url).replace(/\/+$/, "");
665
- const secret = process.env.MEMINI_API_KEY || process.env.MEMINI_TOKEN;
545
+ function pinKeyFacts(facts) {
546
+ const out = {};
547
+ if (facts.remote_url) out.remote_url = facts.remote_url;
548
+ if (facts.toplevel_path) out.toplevel_path = facts.toplevel_path;
549
+ return out;
550
+ }
551
+ async function pinsRequest(boot, method, body) {
552
+ assertBearerTransportSafe(boot.baseUrl, boot.apiKey);
553
+ const headers = { "Content-Type": "application/json" };
554
+ if (boot.apiKey) headers.Authorization = `Bearer ${boot.apiKey}`;
555
+ if (boot.homeEnv) headers["X-Memini-Home"] = boot.homeEnv;
556
+ const res = await fetch(`${boot.baseUrl}/v1/pins`, {
557
+ method,
558
+ headers,
559
+ body: JSON.stringify(body),
560
+ signal: AbortSignal.timeout(5e3)
561
+ });
562
+ let parsed = null;
563
+ try {
564
+ parsed = await res.json();
565
+ } catch {
566
+ }
567
+ return { ok: res.ok, status: res.status, body: parsed };
568
+ }
569
+ function pinErrorMessage(res) {
570
+ return res.body?.error || res.body?.message || `HTTP ${res.status}`;
571
+ }
572
+ function offlineMessage(boot, error) {
573
+ const detail = String(error?.message || error);
574
+ return `${detail}
575
+
576
+ Could not reach the memini server at ${boot.baseUrl}. Pins live on the server, so setting one needs it reachable. For an offline, machine-local override instead, export MEMINI_NAMESPACE=<namespace>.`;
577
+ }
578
+ async function statusGet(boot, namespace, path2, warn, quiet = false) {
579
+ const baseUrl = String(boot.baseUrl).replace(/\/+$/, "");
666
580
  const headers = { "X-Memini-Namespace": namespace };
667
- if (secret) headers.Authorization = `Bearer ${secret}`;
668
- if (cfg.home) headers["X-Memini-Home"] = cfg.home;
581
+ if (boot.apiKey) headers.Authorization = `Bearer ${boot.apiKey}`;
582
+ if (boot.homeEnv) headers["X-Memini-Home"] = boot.homeEnv;
669
583
  try {
670
- const res = await fetch(`${baseUrl}${path3}`, {
584
+ const res = await fetch(`${baseUrl}${path2}`, {
671
585
  method: "GET",
672
586
  headers,
673
587
  signal: AbortSignal.timeout(STATUS_TIMEOUT_MS)
674
588
  });
675
589
  if (!res.ok) {
676
- if (!quiet) warn(`GET ${path3} -> ${res.status}`);
590
+ if (!quiet) warn(`GET ${path2} -> ${res.status}`);
677
591
  return null;
678
592
  }
679
593
  return await res.json();
680
594
  } catch (error) {
681
- if (!quiet) warn(`GET ${path3} failed: ${String(error)}`);
595
+ if (!quiet) warn(`GET ${path2} failed: ${String(error)}`);
682
596
  return null;
683
597
  }
684
598
  }
685
- async function fetchServer(cfg, namespace, warn) {
599
+ async function fetchServer(boot, namespace, warn) {
686
600
  const started = Date.now();
687
- const readSet = await statusGet(cfg, namespace, "/v1/namespaces/read-set", warn);
601
+ const readSet = await statusGet(boot, namespace, "/v1/namespaces/readset", warn);
688
602
  const out = {
689
603
  reachable: readSet != null,
690
604
  latencyMs: Date.now() - started,
691
605
  readSet
692
606
  };
693
- const health = await statusGet(cfg, namespace, "/healthz?verbose=1", warn, true);
607
+ const health = await statusGet(boot, namespace, "/healthz?verbose=1", warn, true);
694
608
  if (health) {
695
609
  out.version = health.version;
696
610
  out.status = health.status;
@@ -701,49 +615,75 @@ async function fetchServer(cfg, namespace, warn) {
701
615
  return out;
702
616
  }
703
617
  var pad = (s, n) => String(s).padEnd(n);
704
- function renderStatus(settings, cfg, server) {
705
- const ns = settings.namespace;
618
+ function buildWarnings(ctx, live, hs) {
619
+ const warnings = [];
620
+ if (live.degraded) {
621
+ warnings.push({
622
+ level: "warn",
623
+ code: "degraded-mode",
624
+ message: `could not reach the memini server at ${ctx.boot.baseUrl}: the namespace is local-derived and every setting is a built-in default, not what the server would return.`,
625
+ fix: "Check MEMINI_BASE_URL and that the server is running; recall and capture are both failing until it is reachable."
626
+ });
627
+ }
628
+ if (ctx.boot.namespaceEnv && hs?.namespace_source !== "pin") {
629
+ warnings.push({
630
+ level: "warn",
631
+ code: "global-namespace-pin",
632
+ message: `MEMINI_NAMESPACE is set to "${ctx.boot.namespaceEnv}", which pins EVERY project on this machine to one namespace (unless this repo has a stronger server-side pin). If it is exported from a shell rc (or a fish universal variable), every repo you work in is sharing one memory pool.`,
633
+ fix: "Set a pin instead: /memini:namespace <ns> (a pin beats the environment)."
634
+ });
635
+ }
636
+ if (!ctx.boot.homeEnv) {
637
+ warnings.push({
638
+ level: "warn",
639
+ code: "home-unset",
640
+ message: 'MEMINI_HOME is unset: there is no personal namespace, so visibility:"personal" writes will error and no personal leg merges into recall.',
641
+ fix: "Export MEMINI_HOME=personal/<you>."
642
+ });
643
+ }
644
+ if (isPlaintextBearerUnsafe(ctx.boot.baseUrl, ctx.boot.apiKey)) {
645
+ warnings.push({
646
+ level: "warn",
647
+ code: "plaintext-bearer",
648
+ message: `a bearer token is configured for plaintext HTTP to ${ctx.boot.baseUrl}; the token and your memory payloads can be observed on the network.`,
649
+ fix: "Use HTTPS, or tunnel over SSH. Set MEMINI_REQUIRE_HTTPS=1 to make this an error."
650
+ });
651
+ }
652
+ return warnings;
653
+ }
654
+ function renderStatus(ctx, staticCfg, live, hs, server) {
706
655
  const L = [];
707
656
  L.push(`memini \u2014 effective settings (pi)`);
708
- L.push(`cwd: ${settings.cwd}`);
657
+ L.push(`cwd: ${ctx.facts.toplevel_path || process.cwd()}`);
709
658
  L.push("");
710
659
  L.push(`NAMESPACE`);
711
- L.push(` ${pad("effective", 28)} ${pad(ns.effective, 34)} <- ${ns.source}`);
712
- if (ns.override) {
713
- L.push(
714
- ` ${pad("without the override", 28)} ${pad(ns.withoutOverride.namespace, 34)} <- ${ns.withoutOverride.source}`
715
- );
716
- }
717
- if (ns.derived.namespace !== ns.effective) {
718
- L.push(` ${pad("git/cwd would give", 28)} ${pad(ns.derived.namespace, 34)} <- ${ns.derived.source}`);
660
+ L.push(` ${pad("effective", 28)} ${pad(live.namespace, 34)} <- ${live.namespace_source}`);
661
+ if (live.degraded) {
662
+ const local = deriveLocalNamespace(ctx.facts);
663
+ if (local.namespace !== live.namespace) {
664
+ L.push(` ${pad("git/cwd would give", 28)} ${pad(local.namespace, 34)} <- local-${local.source}`);
665
+ }
719
666
  }
720
- L.push(` ${pad("home (personal)", 28)} ${ns.home || "(unset)"}`);
667
+ L.push(` ${pad("home (personal)", 28)} ${ctx.boot.homeEnv || "(unset)"}`);
721
668
  L.push("");
722
- const groups = [
723
- ["CONNECTION", ["MEMINI_BASE_URL", "MEMINI_API_KEY", "MEMINI_REQUIRE_HTTPS"]],
724
- ["NAMESPACE INPUTS", ["MEMINI_NAMESPACE", "MEMINI_NAMESPACE_SCOPE", "MEMINI_AGENT", "MEMINI_HOME"]]
725
- ];
726
- for (const [group, names] of groups) {
727
- const rows = (settings.settings || []).filter((s) => names.includes(s.name));
728
- if (!rows.length) continue;
729
- L.push(group);
730
- for (const r of rows) {
731
- const origin = r.source === "env" ? `<- env` : `(default)`;
732
- L.push(` ${pad(r.name.replace(/^MEMINI_/, "").toLowerCase(), 28)} ${pad(r.value, 34)} ${origin}`);
733
- }
734
- L.push("");
669
+ L.push(`SETTINGS`);
670
+ const wireKeys = ["recall", "capture", "recall_limit", "inject_recall_max_tok", "inject_recall_min_score"];
671
+ for (const wireKey of wireKeys) {
672
+ const k = knob(wireKey);
673
+ const { value, source } = effectiveSetting(k, hs?.settings, process.env);
674
+ const origin = source === "env-override" ? "<- env" : source === "server" ? "<- server" : "(default)";
675
+ L.push(` ${pad(k.envName.replace(/^MEMINI_/, "").toLowerCase(), 28)} ${pad(String(value), 22)} ${origin}`);
735
676
  }
736
- const secret = process.env.MEMINI_API_KEY || process.env.MEMINI_TOKEN || "";
677
+ L.push("");
678
+ const secret = ctx.boot.apiKey;
737
679
  L.push(`EXTENSION`);
738
- L.push(` ${pad("recall", 28)} ${cfg.recall ? "on" : "off"}`);
739
- L.push(` ${pad("capture", 28)} ${cfg.capture ? "on" : "off"}`);
740
- L.push(` ${pad("recall_limit", 28)} ${cfg.recall_limit}`);
741
- L.push(` ${pad("timeout_ms", 28)} ${cfg.timeout_ms}`);
680
+ L.push(` ${pad("base_url", 28)} ${ctx.boot.baseUrl}`);
681
+ L.push(` ${pad("timeout_ms", 28)} ${staticCfg.timeout_ms}`);
742
682
  L.push(` ${pad("bearer", 28)} ${secret ? redactValue(secret) : "(none)"}`);
743
683
  L.push("");
744
684
  L.push(`SERVER`);
745
685
  if (!server.reachable) {
746
- L.push(` ${pad("reachable", 28)} NO \u2014 could not reach ${cfg.base_url}`);
686
+ L.push(` ${pad("reachable", 28)} NO \u2014 could not reach ${ctx.boot.baseUrl}`);
747
687
  } else {
748
688
  const ver = server.version ? `, ${server.version}` : "";
749
689
  L.push(` ${pad("reachable", 28)} yes (${server.latencyMs}ms${ver})`);
@@ -758,7 +698,7 @@ function renderStatus(settings, cfg, server) {
758
698
  }
759
699
  L.push("");
760
700
  if (server.readSet?.entries?.length) {
761
- L.push(`READ SET for "${ns.effective}" \u2014 where a plain recall looks`);
701
+ L.push(`READ SET for "${live.namespace}" \u2014 where a plain recall looks`);
762
702
  L.push(` ${pad("NAMESPACE", 34)} ${pad("ORIGIN", 12)} TIERS`);
763
703
  for (const e of server.readSet.entries) {
764
704
  const tiers = Array.isArray(e.tiers) && e.tiers.length ? e.tiers.join(",") : "all";
@@ -766,12 +706,10 @@ function renderStatus(settings, cfg, server) {
766
706
  }
767
707
  L.push("");
768
708
  }
769
- L.push(`PATHS`);
770
- L.push(` ${pad("overrides", 28)} ${settings.paths.overrides}`);
771
- L.push("");
772
- if (settings.warnings.length) {
709
+ const warnings = buildWarnings(ctx, live, hs);
710
+ if (warnings.length) {
773
711
  L.push(`WARNINGS`);
774
- for (const w of settings.warnings) {
712
+ for (const w of warnings) {
775
713
  L.push(` [${w.level === "warn" ? "!" : "i"}] ${w.code}: ${w.message}`);
776
714
  if (w.fix) L.push(` fix: ${w.fix}`);
777
715
  }
@@ -780,69 +718,94 @@ function renderStatus(settings, cfg, server) {
780
718
  }
781
719
  return L.join("\n");
782
720
  }
783
- function registerMeminiCommands(pi, cfg, warn) {
721
+ function registerMeminiCommands(pi, ctx, staticCfg, warn) {
784
722
  const show = (content) => {
785
723
  pi.sendMessage({ customType: "memini-status", content, display: true });
786
724
  };
787
725
  pi.registerCommand("memini:status", {
788
726
  description: "Show memini's effective settings: namespace + provenance, connection, server read set",
789
- handler: async (_args, ctx) => {
727
+ handler: async (_args, cmdCtx) => {
790
728
  try {
791
- const cwd = ctx.cwd || process.cwd();
792
- const settings = describeSettings({
793
- cwd,
794
- env: process.env,
795
- // Hand describeSettings THIS harness's resolver, so what it reports is
796
- // what the extension actually does. The opts pass-through carries
797
- // ignoreOverride, which is how the counterfactual lines see past an
798
- // override (it lives in a file, so no env-doctoring would remove it).
799
- resolve: (env, o) => resolveProjectNamespace(env, cwd, o)
800
- });
801
- const server = await fetchServer(cfg, settings.namespace.effective, warn);
802
- show(renderStatus(settings, cfg, server));
729
+ const hs = await ctx.memo.get();
730
+ const live = resolveLiveConfig(ctx.boot, ctx.facts, hs, process.env);
731
+ const server = await fetchServer(ctx.boot, live.namespace, warn);
732
+ show(renderStatus(ctx, staticCfg, live, hs, server));
803
733
  } catch (error) {
804
- ctx.ui.notify(`memini: status failed: ${String(error)}`, "error");
734
+ cmdCtx.ui.notify(`memini: status failed: ${String(error)}`, "error");
805
735
  }
806
736
  }
807
737
  });
808
738
  pi.registerCommand("memini:namespace", {
809
- description: "Show, set, or --clear the memini namespace override for this project",
810
- handler: async (args, ctx) => {
739
+ description: "Show, set, or --clear the memini namespace pin for this project (server-side)",
740
+ handler: async (args, cmdCtx) => {
811
741
  try {
812
- const cwd = ctx.cwd || process.cwd();
813
742
  const arg = String(args || "").trim();
814
- const before = resolveProjectNamespace(process.env, cwd);
743
+ const { boot, facts } = ctx;
815
744
  if (!arg) {
816
- const current = readOverride(cwd, { env: process.env });
817
- const out = [
818
- `namespace: ${before.namespace} (source: ${before.source})`,
819
- `project: ${overrideKey(cwd)}`,
820
- ``
821
- ];
822
- if (current) {
823
- out.push(`An override is active (set ${current.setAt}).`);
824
- out.push(`Clear it with: /memini:namespace --clear`);
825
- } else {
826
- out.push(`No override \u2014 resolving automatically.`);
827
- out.push(`Set one with: /memini:namespace <namespace>`);
745
+ const hs = await ctx.memo.get();
746
+ const live = resolveLiveConfig(boot, facts, hs, process.env);
747
+ const out = [];
748
+ if (!hs) {
749
+ out.push(`namespace: ${live.namespace} (${live.namespace_source} \u2014 server unreachable)`);
750
+ out.push("");
751
+ out.push(`Could not reach ${boot.baseUrl}, so this is a local guess, not the server's authority.`);
752
+ out.push(`A pin (if any) can only be read from the server.`);
753
+ show(out.join("\n"));
754
+ return;
755
+ }
756
+ out.push(`namespace: ${hs.namespace} (source: ${hs.namespace_source})`);
757
+ if (hs.namespace_source === "pin" && hs.pin) {
758
+ out.push(`pin: key ${hs.pin.key}`);
759
+ if (hs.pin.created_by) out.push(` set by ${hs.pin.created_by}`);
760
+ if (hs.pin.updated_at) out.push(` updated ${hs.pin.updated_at}`);
761
+ if (hs.pin.note) out.push(` note: ${hs.pin.note}`);
762
+ if (boot.namespaceEnv) {
763
+ out.push("");
764
+ out.push(`MEMINI_NAMESPACE is set to "${boot.namespaceEnv}", but the pin wins \u2014 a pin`);
765
+ out.push(`beats the environment on purpose.`);
766
+ }
767
+ } else if (hs.namespace_source === "env") {
768
+ out.push("");
769
+ out.push(`This comes from MEMINI_NAMESPACE, which pins EVERY project on this machine to`);
770
+ out.push(`one namespace. To scope just this project, set a pin: /memini:namespace <ns>`);
771
+ out.push(`(a pin beats the environment).`);
828
772
  }
829
- out.push(`Overrides file: ${defaultOverridesPath(process.env)}`);
773
+ out.push("");
774
+ out.push(`Set a pin with: /memini:namespace <namespace>`);
775
+ out.push(`Clear it with: /memini:namespace --clear`);
830
776
  show(out.join("\n"));
831
777
  return;
832
778
  }
833
779
  if (arg === "--clear" || arg === "clear") {
834
- const removed = clearOverride(cwd, { env: process.env });
835
- if (!removed) {
836
- show(`No override was set for ${overrideKey(cwd)} \u2014 nothing to clear.`);
780
+ const keyFacts2 = pinKeyFacts(facts);
781
+ if (!keyFacts2.remote_url && !keyFacts2.toplevel_path) {
782
+ cmdCtx.ui.notify(
783
+ `memini: this project has no git remote or toplevel, so it cannot have a pin to clear.`,
784
+ "error"
785
+ );
837
786
  return;
838
787
  }
839
- const after2 = resolveProjectNamespace(process.env, cwd);
840
- cfg.namespace = after2.namespace;
788
+ let res2;
789
+ try {
790
+ res2 = await pinsRequest(boot, "DELETE", keyFacts2);
791
+ } catch (error) {
792
+ cmdCtx.ui.notify(`memini: ${offlineMessage(boot, error)}`, "error");
793
+ return;
794
+ }
795
+ if (res2.status === 404) {
796
+ show(`No pin was set for this project \u2014 nothing to clear.`);
797
+ return;
798
+ }
799
+ if (!res2.ok) {
800
+ cmdCtx.ui.notify(`memini: could not clear the pin: ${pinErrorMessage(res2)}`, "error");
801
+ return;
802
+ }
803
+ ctx.memo.invalidate();
841
804
  show(
842
805
  [
843
- `namespace override cleared: ${before.namespace} -> ${after2.namespace} (source: ${after2.source})`,
806
+ `namespace pin cleared \u2014 this project resolves automatically again.`,
844
807
  ``,
845
- `Recall and capture use the new namespace from the next turn.`
808
+ `Recall and capture use the new resolution from the next turn.`
846
809
  ].join("\n")
847
810
  );
848
811
  return;
@@ -850,22 +813,42 @@ function registerMeminiCommands(pi, cfg, warn) {
850
813
  const ns = normalizeNamespace(arg);
851
814
  const bad = validateNamespace(ns);
852
815
  if (bad) {
853
- ctx.ui.notify(`memini: invalid namespace ${JSON.stringify(arg)}: ${bad}`, "error");
816
+ cmdCtx.ui.notify(`memini: invalid namespace ${JSON.stringify(arg)}: ${bad}`, "error");
854
817
  return;
855
818
  }
856
- writeOverride(cwd, ns, { env: process.env });
857
- const after = resolveProjectNamespace(process.env, cwd);
858
- cfg.namespace = after.namespace;
819
+ const keyFacts = pinKeyFacts(facts);
820
+ if (!keyFacts.remote_url && !keyFacts.toplevel_path) {
821
+ cmdCtx.ui.notify(
822
+ `memini: this project has no git remote or toplevel to pin a namespace to. A pin is keyed by the project's git identity; run inside a git repository, or export MEMINI_NAMESPACE=${ns} for a machine-local override.`,
823
+ "error"
824
+ );
825
+ return;
826
+ }
827
+ let res;
828
+ try {
829
+ res = await pinsRequest(boot, "PUT", { namespace: ns, ...keyFacts });
830
+ } catch (error) {
831
+ cmdCtx.ui.notify(`memini: ${offlineMessage(boot, error)}`, "error");
832
+ return;
833
+ }
834
+ if (!res.ok) {
835
+ cmdCtx.ui.notify(`memini: could not set the pin: ${pinErrorMessage(res)}`, "error");
836
+ return;
837
+ }
838
+ ctx.memo.invalidate();
839
+ const entry = res.body || {};
859
840
  show(
860
841
  [
861
- `namespace override set: ${before.namespace} -> ${after.namespace}`,
862
- `project: ${overrideKey(cwd)}`,
842
+ `namespace pinned: ${entry.namespace || ns}`,
843
+ `project key: ${entry.key || keyFacts.remote_url || keyFacts.toplevel_path}`,
863
844
  ``,
864
- `The override wins over MEMINI_NAMESPACE. Recall and capture use it from the next turn.`
845
+ `Recall and capture use it from the next turn. The pin lives on the memini server, so it`,
846
+ `follows you across machines and every client resolves the same namespace. It beats`,
847
+ `MEMINI_NAMESPACE.`
865
848
  ].join("\n")
866
849
  );
867
850
  } catch (error) {
868
- ctx.ui.notify(`memini: namespace failed: ${String(error)}`, "error");
851
+ cmdCtx.ui.notify(`memini: namespace failed: ${String(error)}`, "error");
869
852
  }
870
853
  }
871
854
  });
@@ -898,10 +881,11 @@ function meminiExtension(pi) {
898
881
  } catch {
899
882
  }
900
883
  };
901
- const cfg = resolveConfig(process.env, process.cwd());
902
- const client = createClient(cfg, warn);
884
+ const sessionCtx = createSessionContext(process.cwd(), process.env);
885
+ const staticCfg = resolveStaticConfig(process.env);
886
+ const client = createClient(staticCfg, sessionCtx.boot, warn);
903
887
  try {
904
- if (typeof pi.registerCommand === "function") registerMeminiCommands(pi, cfg, warn);
888
+ if (typeof pi.registerCommand === "function") registerMeminiCommands(pi, sessionCtx, staticCfg, warn);
905
889
  } catch (error) {
906
890
  warn(`command registration skipped: ${String(error)}`);
907
891
  }
@@ -966,22 +950,23 @@ function meminiExtension(pi) {
966
950
  const sid = sessionIdOf(ctx);
967
951
  const query = String(event?.prompt || "").trim();
968
952
  if (query && sid) rememberPendingUser(sid, query);
969
- if (!cfg.recall || !query) return;
970
- const body = { query, limit: cfg.recall_limit };
953
+ const live = await sessionLive(sessionCtx);
954
+ if (!live.recall || !query) return;
955
+ const body = { query, limit: live.recall_limit };
971
956
  if (sid) body.exclude_metadata = { session_id: sid };
972
- if (cfg.recall_min_score > 0) body.min_score = cfg.recall_min_score;
957
+ if (live.recall_min_score > 0) body.min_score = live.recall_min_score;
973
958
  const excludeIds = sid ? [...injectedBySession.get(sid) ?? []] : [];
974
959
  const result = await searchExcluding(body, excludeIds);
975
- const floor = cfg.recall_min_score > 0 ? cfg.recall_min_score : 0;
960
+ const floor = live.recall_min_score > 0 ? live.recall_min_score : 0;
976
961
  let rawHits = Array.isArray(result?.results) ? result.results : [];
977
962
  if (sid) {
978
963
  const seen = injectedBySession.get(sid);
979
964
  if (seen?.size) rawHits = rawHits.filter((r) => !seen.has(r?.memory?.id));
980
965
  }
981
966
  const filtered = floor > 0 ? rawHits.filter((r) => (typeof r?.score === "number" ? r.score : 0) >= floor) : rawHits;
982
- const hits = formatResults(filtered, cfg.recall_limit, labelsEnv());
967
+ const hits = formatResults(filtered, live.recall_limit, labelsEnv());
983
968
  if (hits.length === 0) return;
984
- const fit = fitByTokens(hits, cfg.recall_max_tokens);
969
+ const fit = fitByTokens(hits, live.recall_max_tokens);
985
970
  if (fit.items.length === 0) return;
986
971
  if (sid) {
987
972
  rememberInjected(sid, filtered.map((r) => r?.memory?.id).filter(Boolean));
@@ -1005,7 +990,8 @@ function meminiExtension(pi) {
1005
990
  };
1006
991
  });
1007
992
  pi.on("agent_end", async (event, ctx) => {
1008
- if (!cfg.capture) return;
993
+ const live = await sessionLive(sessionCtx);
994
+ if (!live.capture) return;
1009
995
  const sid = sessionIdOf(ctx);
1010
996
  const userText = sid && pendingUser.get(sid) || "";
1011
997
  const assistantText = extractLastAssistantText(event?.messages);
@@ -1014,11 +1000,15 @@ function meminiExtension(pi) {
1014
1000
  if (dedupKey && captured.has(dedupKey)) return;
1015
1001
  const metadata = { source: "pi", format: "turn" };
1016
1002
  if (sid) metadata.session_id = sid;
1017
- const stored = await client.postJson("/v1/memories", {
1018
- content: buildTurnContent(userText, assistantText),
1019
- tags: ["pi"],
1020
- metadata
1021
- });
1003
+ const stored = await client.postJson(
1004
+ "/v1/memories",
1005
+ {
1006
+ content: buildTurnContent(userText, assistantText),
1007
+ tags: ["pi"],
1008
+ metadata
1009
+ },
1010
+ live.namespace
1011
+ );
1022
1012
  if (stored !== null) {
1023
1013
  if (dedupKey) rememberCaptured(dedupKey);
1024
1014
  if (sid) pendingUser.delete(sid);
@@ -1051,11 +1041,12 @@ function meminiExtension(pi) {
1051
1041
  scope: Scope
1052
1042
  }),
1053
1043
  async execute(_toolCallId, params) {
1044
+ const live = await sessionLive(sessionCtx);
1054
1045
  const body = { query: params.query, limit: params.limit || DEFAULT_RECALL_LIMIT };
1055
1046
  if (params.tags?.length) body.tags = params.tags;
1056
1047
  if (params.metadata && Object.keys(params.metadata).length) body.metadata = params.metadata;
1057
1048
  if (VALID_SCOPES.includes(params.scope)) body.scope = params.scope;
1058
- const res = await client.postJson("/v1/search", body);
1049
+ const res = await client.postJson("/v1/search", body, live.namespace);
1059
1050
  const results = (res?.results || []).map((r) => {
1060
1051
  const mem = r?.memory || {};
1061
1052
  const out = {
@@ -1078,7 +1069,8 @@ function meminiExtension(pi) {
1078
1069
  description: "Layered session-start briefing for this project from long-term memory (memini) \u2014 pinned context, durable facts, how-to procedures, and recent activity \u2014 in one query-less call. Call it when a session opens to orient yourself; prefer it over broad recall queries at session start. The scope_header line ('Scope: acme/phoenix/api \u2190 acme/phoenix(3) \u2190 acme(4) \u2190 personal(2)') spells out the ancestor chain you inherit from \u2014 read it instead of guessing namespace paths, and name one of those ancestors as memory_remember's visibility to share a fact up that chain. scope='everywhere' also briefs nested sub-projects.",
1079
1070
  parameters: Type.Object({ scope: Scope }),
1080
1071
  async execute(_toolCallId, params) {
1081
- const res = await client.getJson(briefingPath(params));
1072
+ const live = await sessionLive(sessionCtx);
1073
+ const res = await client.getJson(briefingPath(params), live.namespace);
1082
1074
  if (!res) return text({ briefing: null, error: "memini unavailable" });
1083
1075
  const section = (items) => (items || []).map((b) => {
1084
1076
  const mem = b?.memory || {};
@@ -1110,8 +1102,9 @@ function meminiExtension(pi) {
1110
1102
  limit: Type.Optional(Type.Number({ description: "Max results (0 = all, default 20)" }))
1111
1103
  }),
1112
1104
  async execute(_toolCallId, params) {
1105
+ const live = await sessionLive(sessionCtx);
1113
1106
  const args = { ...params, limit: params.limit ?? 20 };
1114
- const res = await client.getJson(meminiListPath(args));
1107
+ const res = await client.getJson(meminiListPath(args), live.namespace);
1115
1108
  const memories = (res?.memories || []).map((m) => ({
1116
1109
  id: m.id || "",
1117
1110
  content: m.content || "",
@@ -1156,6 +1149,7 @@ function meminiExtension(pi) {
1156
1149
  )
1157
1150
  }),
1158
1151
  async execute(_toolCallId, params) {
1152
+ const live = await sessionLive(sessionCtx);
1159
1153
  const body = { content: params.content };
1160
1154
  if (params.id) body.id = params.id;
1161
1155
  if (params.tier && VALID_TIERS.includes(params.tier)) body.tier = params.tier;
@@ -1163,7 +1157,7 @@ function meminiExtension(pi) {
1163
1157
  if (params.category) body.metadata = { category: params.category };
1164
1158
  const visibility = String(params.visibility || "").trim();
1165
1159
  if (visibility) body.visibility = visibility;
1166
- const res = await client.postJsonResult("/v1/memories", body);
1160
+ const res = await client.postJsonResult("/v1/memories", body, live.namespace);
1167
1161
  if (!res.ok) return text({ id: null, success: false, error: res.error });
1168
1162
  const out = { id: res.data?.id || null, success: true };
1169
1163
  if (res.data?.reinforced) out.reinforced = true;
@@ -1178,8 +1172,9 @@ function meminiExtension(pi) {
1178
1172
  id: Type.String({ description: "The id of the memory to forget (from memory_recall / memory_list)." })
1179
1173
  }),
1180
1174
  async execute(_toolCallId, params) {
1175
+ const live = await sessionLive(sessionCtx);
1181
1176
  if (!params.id) return text({ forgotten: false, error: "id is required" });
1182
- const res = await client.deleteJson(`/v1/memories/${encodeURIComponent(params.id)}`);
1177
+ const res = await client.deleteJson(`/v1/memories/${encodeURIComponent(params.id)}`, live.namespace);
1183
1178
  return text({ forgotten: res != null });
1184
1179
  }
1185
1180
  });
@@ -1189,9 +1184,10 @@ export {
1189
1184
  approxTokens,
1190
1185
  briefingPath,
1191
1186
  buildTurnContent,
1187
+ buildWarnings,
1192
1188
  createPlaintextBearerAuthGuard,
1189
+ createSessionContext,
1193
1190
  meminiExtension as default,
1194
- deriveNamespace,
1195
1191
  extractLastAssistantText,
1196
1192
  extractMessageText,
1197
1193
  fitByTokens,
@@ -1200,11 +1196,12 @@ export {
1200
1196
  intEnv,
1201
1197
  labelsEnv,
1202
1198
  meminiListPath,
1199
+ memoizeAsync,
1200
+ pinKeyFacts,
1203
1201
  registerMeminiCommands,
1204
1202
  renderStatus,
1205
- resolveConfig,
1206
- resolveProjectNamespace,
1207
- sanitizeNamespace,
1208
- sanitizeNamespacePath,
1203
+ resolveLiveConfig,
1204
+ resolveStaticConfig,
1205
+ sessionLive,
1209
1206
  truncate
1210
1207
  };