@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.
- package/README.md +45 -37
- package/dist/index.js +542 -545
- 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/
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
204
|
-
const
|
|
205
|
-
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
256
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
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
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
457
|
-
if (
|
|
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
|
|
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 (!
|
|
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(
|
|
566
|
-
const baseUrl = String(
|
|
567
|
-
const secret =
|
|
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 (
|
|
570
|
-
function headers(extra) {
|
|
571
|
-
const h = { "X-Memini-Namespace":
|
|
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 (
|
|
455
|
+
if (staticCfg.home) h["X-Memini-Home"] = staticCfg.home;
|
|
574
456
|
return h;
|
|
575
457
|
}
|
|
576
|
-
async function request(method,
|
|
458
|
+
async function request(method, path2, namespace, body) {
|
|
577
459
|
guard(baseUrl, secret);
|
|
578
460
|
try {
|
|
579
|
-
const res = await fetch(`${baseUrl}${
|
|
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(
|
|
465
|
+
signal: AbortSignal.timeout(staticCfg.timeout_ms)
|
|
584
466
|
});
|
|
585
467
|
if (!res.ok) {
|
|
586
|
-
if (
|
|
587
|
-
warn(`memini ${method} ${
|
|
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} ${
|
|
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 (!
|
|
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,
|
|
482
|
+
async function requestResult(method, path2, namespace, body) {
|
|
601
483
|
try {
|
|
602
484
|
guard(baseUrl, secret);
|
|
603
|
-
const res = await fetch(`${baseUrl}${
|
|
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(
|
|
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} ${
|
|
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: (
|
|
622
|
-
getJson: (
|
|
623
|
-
deleteJson: (
|
|
624
|
-
postJsonResult: (
|
|
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
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
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 (
|
|
668
|
-
if (
|
|
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}${
|
|
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 ${
|
|
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 ${
|
|
595
|
+
if (!quiet) warn(`GET ${path2} failed: ${String(error)}`);
|
|
682
596
|
return null;
|
|
683
597
|
}
|
|
684
598
|
}
|
|
685
|
-
async function fetchServer(
|
|
599
|
+
async function fetchServer(boot, namespace, warn) {
|
|
686
600
|
const started = Date.now();
|
|
687
|
-
const readSet = await statusGet(
|
|
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(
|
|
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
|
|
705
|
-
const
|
|
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: ${
|
|
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(
|
|
712
|
-
if (
|
|
713
|
-
|
|
714
|
-
|
|
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)} ${
|
|
667
|
+
L.push(` ${pad("home (personal)", 28)} ${ctx.boot.homeEnv || "(unset)"}`);
|
|
721
668
|
L.push("");
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
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
|
-
|
|
677
|
+
L.push("");
|
|
678
|
+
const secret = ctx.boot.apiKey;
|
|
737
679
|
L.push(`EXTENSION`);
|
|
738
|
-
L.push(` ${pad("
|
|
739
|
-
L.push(` ${pad("
|
|
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 ${
|
|
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 "${
|
|
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
|
-
|
|
770
|
-
|
|
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
|
|
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,
|
|
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,
|
|
727
|
+
handler: async (_args, cmdCtx) => {
|
|
790
728
|
try {
|
|
791
|
-
const
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
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
|
|
810
|
-
handler: async (args,
|
|
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
|
|
743
|
+
const { boot, facts } = ctx;
|
|
815
744
|
if (!arg) {
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
out.push(`
|
|
824
|
-
out.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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(
|
|
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
|
|
835
|
-
if (!
|
|
836
|
-
|
|
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
|
-
|
|
840
|
-
|
|
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
|
|
806
|
+
`namespace pin cleared \u2014 this project resolves automatically again.`,
|
|
844
807
|
``,
|
|
845
|
-
`Recall and capture use the new
|
|
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
|
-
|
|
816
|
+
cmdCtx.ui.notify(`memini: invalid namespace ${JSON.stringify(arg)}: ${bad}`, "error");
|
|
854
817
|
return;
|
|
855
818
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
|
862
|
-
`project:
|
|
842
|
+
`namespace pinned: ${entry.namespace || ns}`,
|
|
843
|
+
`project key: ${entry.key || keyFacts.remote_url || keyFacts.toplevel_path}`,
|
|
863
844
|
``,
|
|
864
|
-
`
|
|
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
|
-
|
|
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
|
|
902
|
-
const
|
|
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,
|
|
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
|
-
|
|
970
|
-
|
|
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 (
|
|
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 =
|
|
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,
|
|
967
|
+
const hits = formatResults(filtered, live.recall_limit, labelsEnv());
|
|
983
968
|
if (hits.length === 0) return;
|
|
984
|
-
const fit = fitByTokens(hits,
|
|
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
|
-
|
|
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(
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
|
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
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
sanitizeNamespacePath,
|
|
1203
|
+
resolveLiveConfig,
|
|
1204
|
+
resolveStaticConfig,
|
|
1205
|
+
sessionLive,
|
|
1209
1206
|
truncate
|
|
1210
1207
|
};
|