@eleboucher/pi-memini 0.6.7 → 0.6.9
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 +57 -22
- package/dist/index.js +568 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ What it wires:
|
|
|
17
17
|
- **`agent_end`** — once the agent finishes a prompt, stores the completed
|
|
18
18
|
user/assistant turn back into memini (episodic, tagged `pi`, with the session
|
|
19
19
|
id) so it can be recalled later.
|
|
20
|
-
- **Explicit tools** — the
|
|
21
|
-
registered natively via `pi.registerTool`: `memory_recall`, `memory_list`,
|
|
20
|
+
- **Explicit tools** — modeled on the tool set Claude Code gets from memini's
|
|
21
|
+
MCP server, registered natively via `pi.registerTool`: `memory_recall`, `memory_list`,
|
|
22
22
|
`memory_remember`, `memory_forget`. The model can call them on demand even
|
|
23
23
|
though the automatic loop already runs.
|
|
24
24
|
|
|
@@ -53,33 +53,68 @@ pi -e ./integrations/pi/plugin/dist/index.js
|
|
|
53
53
|
All config is via environment variables in the shell that launches Pi (secrets
|
|
54
54
|
stay out of any file):
|
|
55
55
|
|
|
56
|
-
| Env var | Default
|
|
57
|
-
| -------------------------------- |
|
|
58
|
-
| `MEMINI_BASE_URL` | `http://localhost:8080`
|
|
59
|
-
| `MEMINI_NAMESPACE` | cwd basename
|
|
60
|
-
| `MEMINI_HOME` | unset
|
|
61
|
-
| `MEMINI_RECALL` | on
|
|
62
|
-
| `MEMINI_CAPTURE` | on
|
|
63
|
-
| `MEMINI_RECALL_LIMIT` | `3`
|
|
64
|
-
| `MEMINI_INJECT_RECALL_MAX_TOK` | `0`
|
|
65
|
-
| `MEMINI_INJECT_RECALL_MIN_SCORE` | `0`
|
|
66
|
-
| `MEMINI_INJECT_LABELS` | —
|
|
67
|
-
| `MEMINI_TIMEOUT_MS` | `30000`
|
|
68
|
-
| `MEMINI_FALLBACK` | on
|
|
69
|
-
| `MEMINI_API_KEY` | —
|
|
70
|
-
| `MEMINI_REQUIRE_HTTPS` | —
|
|
71
|
-
|
|
72
|
-
Unset, the namespace is derived from the
|
|
73
|
-
|
|
56
|
+
| Env var | Default | Purpose |
|
|
57
|
+
| -------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `MEMINI_BASE_URL` | `http://localhost:8080` | memini REST base URL (alias: `MEMINI_URL`) |
|
|
59
|
+
| `MEMINI_NAMESPACE` | git repo name, else cwd basename | project the memory is scoped to (`X-Memini-Namespace`) |
|
|
60
|
+
| `MEMINI_HOME` | unset | caller's personal namespace, sent as `X-Memini-Home`; unset = no home leg |
|
|
61
|
+
| `MEMINI_RECALL` | on | `0`/`false` disables recall-before-turn |
|
|
62
|
+
| `MEMINI_CAPTURE` | on | `0`/`false` disables capture-after-turn |
|
|
63
|
+
| `MEMINI_RECALL_LIMIT` | `3` | max memories injected per turn |
|
|
64
|
+
| `MEMINI_INJECT_RECALL_MAX_TOK` | `0` | hard ceiling on recall-block tokens (`0` = unbounded); the tail is dropped with a footer |
|
|
65
|
+
| `MEMINI_INJECT_RECALL_MIN_SCORE` | `0` | fused-score floor (>=) sent as `min_score` to `/v1/search` |
|
|
66
|
+
| `MEMINI_INJECT_LABELS` | — | comma-separated bullet labels: `tier`, `confidence`, `age` |
|
|
67
|
+
| `MEMINI_TIMEOUT_MS` | `30000` | per-request timeout |
|
|
68
|
+
| `MEMINI_FALLBACK` | on | `0`/`false` surfaces errors instead of degrading silently |
|
|
69
|
+
| `MEMINI_API_KEY` | — | bearer token, if memini needs auth (sent as `Authorization: Bearer …`; alias: `MEMINI_TOKEN`) |
|
|
70
|
+
| `MEMINI_REQUIRE_HTTPS` | — | `1` refuses to send the token over plaintext HTTP |
|
|
71
|
+
|
|
72
|
+
Unset, the namespace is derived from the git repo (remote name, then toplevel
|
|
73
|
+
basename), falling back to the working-directory basename, and sent as the
|
|
74
|
+
`X-Memini-Namespace` header — set it to share one memory pool with your
|
|
74
75
|
other agents (Claude Code, opencode, …).
|
|
75
76
|
|
|
77
|
+
### Commands
|
|
78
|
+
|
|
79
|
+
| Command | What it does |
|
|
80
|
+
| ------------------ | ------------------------------------------------------------------------------- |
|
|
81
|
+
| `memini:status` | Effective settings, the resolved namespace **and where it came from**, warnings |
|
|
82
|
+
| `memini:namespace` | Show, set, or clear the namespace override for this project |
|
|
83
|
+
|
|
84
|
+
`memini:status` exists because a list of values is not enough to debug a namespace
|
|
85
|
+
problem. It shows provenance (`<- env` vs `(default)`), so a `MEMINI_NAMESPACE`
|
|
86
|
+
exported once from a shell profile — which pins _every_ repo on the machine to one
|
|
87
|
+
namespace — shows up as a warning rather than as a mystery. Secrets are redacted.
|
|
88
|
+
|
|
89
|
+
### The namespace override
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
memini:namespace # show the namespace and where it came from
|
|
93
|
+
memini:namespace acme/api # override it for this project
|
|
94
|
+
memini:namespace --clear # back to automatic resolution
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Precedence is **override > `MEMINI_NAMESPACE` > config file > git > cwd**. The
|
|
98
|
+
override deliberately beats the environment: a globally exported
|
|
99
|
+
`MEMINI_NAMESPACE` is exactly the problem an override exists to solve, so if the
|
|
100
|
+
environment won, the command would silently do nothing on the machines that need
|
|
101
|
+
it.
|
|
102
|
+
|
|
103
|
+
The override is stored in `~/.config/memini/overrides.json` and shared with every
|
|
104
|
+
other memini client, so one set in Claude Code applies here too, and `memini
|
|
105
|
+
doctor` reports the same value. See
|
|
106
|
+
[env-vars](../../docs/reference/env-vars.md#the-overrides-file) for the format.
|
|
107
|
+
|
|
108
|
+
Unlike the Claude Code plugin, setting an override here takes effect immediately —
|
|
109
|
+
the namespace is re-read per request, so there is no reconnect to wait for.
|
|
110
|
+
|
|
76
111
|
### Build & test
|
|
77
112
|
|
|
78
113
|
```sh
|
|
79
114
|
cd integrations/pi/plugin
|
|
80
115
|
npm install
|
|
81
|
-
npm run build #
|
|
82
|
-
npm test # pure-helper unit tests (tsx --test)
|
|
116
|
+
npm run build # esbuild bundle -> dist/index.js
|
|
117
|
+
npm test # bundle test (node --test) + pure-helper unit tests (tsx --test)
|
|
83
118
|
```
|
|
84
119
|
|
|
85
120
|
## Alternative: MCP wire
|
package/dist/index.js
CHANGED
|
@@ -158,11 +158,245 @@ function resolveNamespace(opts) {
|
|
|
158
158
|
return { namespace, segments, source, home, homeSource };
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
// ../../../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
|
+
function redactValue(value) {
|
|
167
|
+
if (!value) return "";
|
|
168
|
+
if (value.length <= 12) return "***";
|
|
169
|
+
return `${value.slice(0, 3)}\u2026${value.slice(-4)}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ../../../packages/memini-client/src/namespace-validate.ts
|
|
173
|
+
var MAX_NAMESPACE_BYTES = 256;
|
|
174
|
+
function normalizeNamespace(ns) {
|
|
175
|
+
let out = String(ns ?? "").trim();
|
|
176
|
+
out = out.replace(/^\/+|\/+$/g, "");
|
|
177
|
+
while (out.includes("//")) out = out.replace(/\/\/+/g, "/");
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
function validateNamespace(ns) {
|
|
181
|
+
if (!ns) return "namespace is empty";
|
|
182
|
+
if (Buffer.byteLength(ns, "utf8") > MAX_NAMESPACE_BYTES) {
|
|
183
|
+
return `namespace exceeds ${MAX_NAMESPACE_BYTES} bytes`;
|
|
184
|
+
}
|
|
185
|
+
if (/[\r\n]/.test(ns)) return "namespace contains a newline";
|
|
186
|
+
if (/[\x00-\x1F\x7F]/.test(ns)) return "namespace contains a control character";
|
|
187
|
+
if (/[^\x20-\x7E]/.test(ns)) return "namespace contains a non-ASCII character";
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
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");
|
|
202
|
+
}
|
|
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);
|
|
215
|
+
}
|
|
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: {} };
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
version: typeof parsed.version === "number" ? parsed.version : OVERRIDES_VERSION,
|
|
225
|
+
overrides: parsed.overrides || {}
|
|
226
|
+
};
|
|
227
|
+
} catch {
|
|
228
|
+
return { ...EMPTY, overrides: {} };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
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;
|
|
238
|
+
}
|
|
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()
|
|
248
|
+
};
|
|
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
|
+
}
|
|
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];
|
|
261
|
+
try {
|
|
262
|
+
fs2.mkdirSync(path2.dirname(p), { recursive: true });
|
|
263
|
+
fs2.writeFileSync(p, JSON.stringify(file, null, 2) + "\n");
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
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;
|
|
314
|
+
}
|
|
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
|
|
323
|
+
};
|
|
324
|
+
}
|
|
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;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
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
|
+
});
|
|
354
|
+
}
|
|
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
|
+
});
|
|
363
|
+
}
|
|
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
|
+
});
|
|
371
|
+
}
|
|
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."
|
|
380
|
+
});
|
|
381
|
+
}
|
|
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
|
+
}
|
|
393
|
+
|
|
161
394
|
// src/index.ts
|
|
162
395
|
var DEFAULT_BASE_URL = "http://localhost:8080";
|
|
163
396
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
164
397
|
var DEFAULT_RECALL_LIMIT = 3;
|
|
165
398
|
var DEFAULT_NAMESPACE = "pi";
|
|
399
|
+
var STATUS_TIMEOUT_MS = 4e3;
|
|
166
400
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
167
401
|
function envBool(value, fallback) {
|
|
168
402
|
if (value === void 0 || value === null || value === "") return fallback;
|
|
@@ -200,22 +434,33 @@ function deriveNamespace(cwd) {
|
|
|
200
434
|
const base = cwd.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || "";
|
|
201
435
|
return sanitizeNamespace(base);
|
|
202
436
|
}
|
|
203
|
-
function
|
|
437
|
+
function resolverSource(source) {
|
|
438
|
+
return source === "git" ? "git-remote" : source;
|
|
439
|
+
}
|
|
440
|
+
function resolveProjectNamespace(env, cwd, opts = {}) {
|
|
204
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
|
+
}
|
|
205
446
|
const nsEnv = (e.MEMINI_NAMESPACE || "").trim();
|
|
206
|
-
let namespace;
|
|
207
447
|
if (nsEnv) {
|
|
208
|
-
namespace
|
|
209
|
-
}
|
|
210
|
-
|
|
448
|
+
return { namespace: nsEnv, source: "env" };
|
|
449
|
+
}
|
|
450
|
+
if (cwd) {
|
|
451
|
+
const { namespace: resolvedNs, source } = resolveNamespace({
|
|
211
452
|
cwd,
|
|
212
453
|
env: e,
|
|
213
454
|
integration: "pi"
|
|
214
455
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
namespace = DEFAULT_NAMESPACE;
|
|
456
|
+
const sanitized = sanitizeNamespacePath(resolvedNs);
|
|
457
|
+
if (sanitized) return { namespace: sanitized, source: resolverSource(source) };
|
|
218
458
|
}
|
|
459
|
+
return { namespace: DEFAULT_NAMESPACE, source: "default" };
|
|
460
|
+
}
|
|
461
|
+
function resolveConfig(env, cwd) {
|
|
462
|
+
const e = env || {};
|
|
463
|
+
const { namespace } = resolveProjectNamespace(e, cwd);
|
|
219
464
|
const recall_limit = (() => {
|
|
220
465
|
const n = Number(e.MEMINI_RECALL_LIMIT);
|
|
221
466
|
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RECALL_LIMIT;
|
|
@@ -223,7 +468,7 @@ function resolveConfig(env, cwd) {
|
|
|
223
468
|
const homeEnv = (e.MEMINI_HOME || "").trim();
|
|
224
469
|
return {
|
|
225
470
|
base_url: e.MEMINI_BASE_URL || e.MEMINI_URL || DEFAULT_BASE_URL,
|
|
226
|
-
// namespace is already resolved above (
|
|
471
|
+
// namespace is already resolved above (verbatim on the override/env paths,
|
|
227
472
|
// per-segment sanitized on the resolver path); re-sanitizing here would
|
|
228
473
|
// flatten tenant separators.
|
|
229
474
|
namespace: namespace || DEFAULT_NAMESPACE,
|
|
@@ -328,10 +573,10 @@ function createClient(cfg, warn) {
|
|
|
328
573
|
if (cfg.home) h["X-Memini-Home"] = cfg.home;
|
|
329
574
|
return h;
|
|
330
575
|
}
|
|
331
|
-
async function request(method,
|
|
576
|
+
async function request(method, path3, body) {
|
|
332
577
|
guard(baseUrl, secret);
|
|
333
578
|
try {
|
|
334
|
-
const res = await fetch(`${baseUrl}${
|
|
579
|
+
const res = await fetch(`${baseUrl}${path3}`, {
|
|
335
580
|
method,
|
|
336
581
|
headers: headers(body ? { "Content-Type": "application/json" } : void 0),
|
|
337
582
|
body: body ? JSON.stringify(body) : void 0,
|
|
@@ -339,11 +584,11 @@ function createClient(cfg, warn) {
|
|
|
339
584
|
});
|
|
340
585
|
if (!res.ok) {
|
|
341
586
|
if (cfg.fallback_on_error) {
|
|
342
|
-
warn(`memini ${method} ${
|
|
587
|
+
warn(`memini ${method} ${path3} failed: ${res.status}`);
|
|
343
588
|
return null;
|
|
344
589
|
}
|
|
345
590
|
const text = await res.text().catch(() => "");
|
|
346
|
-
throw new Error(`memini ${method} ${
|
|
591
|
+
throw new Error(`memini ${method} ${path3} failed: ${res.status} ${text}`);
|
|
347
592
|
}
|
|
348
593
|
return await res.json().catch(() => ({ ok: true }));
|
|
349
594
|
} catch (error) {
|
|
@@ -352,10 +597,31 @@ function createClient(cfg, warn) {
|
|
|
352
597
|
return null;
|
|
353
598
|
}
|
|
354
599
|
}
|
|
600
|
+
async function requestResult(method, path3, body) {
|
|
601
|
+
try {
|
|
602
|
+
guard(baseUrl, secret);
|
|
603
|
+
const res = await fetch(`${baseUrl}${path3}`, {
|
|
604
|
+
method,
|
|
605
|
+
headers: headers(body ? { "Content-Type": "application/json" } : void 0),
|
|
606
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
607
|
+
signal: AbortSignal.timeout(cfg.timeout_ms)
|
|
608
|
+
});
|
|
609
|
+
if (!res.ok) {
|
|
610
|
+
const detail = (await res.text().catch(() => "")).trim();
|
|
611
|
+
warn(`memini ${method} ${path3} failed: ${res.status} ${detail}`);
|
|
612
|
+
return { ok: false, error: detail || `HTTP ${res.status}` };
|
|
613
|
+
}
|
|
614
|
+
return { ok: true, data: await res.json().catch(() => ({})) };
|
|
615
|
+
} catch (error) {
|
|
616
|
+
warn(`memini: ${String(error)}`);
|
|
617
|
+
return { ok: false, error: String(error) };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
355
620
|
return {
|
|
356
|
-
postJson: (
|
|
357
|
-
getJson: (
|
|
358
|
-
deleteJson: (
|
|
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)
|
|
359
625
|
};
|
|
360
626
|
}
|
|
361
627
|
function meminiListPath(args) {
|
|
@@ -394,8 +660,223 @@ function buildTurnContent(userText, assistantText) {
|
|
|
394
660
|
|
|
395
661
|
${String(assistantText).slice(0, 3e3)}`;
|
|
396
662
|
}
|
|
397
|
-
|
|
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;
|
|
666
|
+
const headers = { "X-Memini-Namespace": namespace };
|
|
667
|
+
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
668
|
+
if (cfg.home) headers["X-Memini-Home"] = cfg.home;
|
|
669
|
+
try {
|
|
670
|
+
const res = await fetch(`${baseUrl}${path3}`, {
|
|
671
|
+
method: "GET",
|
|
672
|
+
headers,
|
|
673
|
+
signal: AbortSignal.timeout(STATUS_TIMEOUT_MS)
|
|
674
|
+
});
|
|
675
|
+
if (!res.ok) {
|
|
676
|
+
if (!quiet) warn(`GET ${path3} -> ${res.status}`);
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
return await res.json();
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (!quiet) warn(`GET ${path3} failed: ${String(error)}`);
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async function fetchServer(cfg, namespace, warn) {
|
|
686
|
+
const started = Date.now();
|
|
687
|
+
const readSet = await statusGet(cfg, namespace, "/v1/namespaces/read-set", warn);
|
|
688
|
+
const out = {
|
|
689
|
+
reachable: readSet != null,
|
|
690
|
+
latencyMs: Date.now() - started,
|
|
691
|
+
readSet
|
|
692
|
+
};
|
|
693
|
+
const health = await statusGet(cfg, namespace, "/healthz?verbose=1", warn, true);
|
|
694
|
+
if (health) {
|
|
695
|
+
out.version = health.version;
|
|
696
|
+
out.status = health.status;
|
|
697
|
+
out.deps = health.deps;
|
|
698
|
+
} else {
|
|
699
|
+
out.healthExposed = false;
|
|
700
|
+
}
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
703
|
+
var pad = (s, n) => String(s).padEnd(n);
|
|
704
|
+
function renderStatus(settings, cfg, server) {
|
|
705
|
+
const ns = settings.namespace;
|
|
706
|
+
const L = [];
|
|
707
|
+
L.push(`memini \u2014 effective settings (pi)`);
|
|
708
|
+
L.push(`cwd: ${settings.cwd}`);
|
|
709
|
+
L.push("");
|
|
710
|
+
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}`);
|
|
719
|
+
}
|
|
720
|
+
L.push(` ${pad("home (personal)", 28)} ${ns.home || "(unset)"}`);
|
|
721
|
+
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("");
|
|
735
|
+
}
|
|
736
|
+
const secret = process.env.MEMINI_API_KEY || process.env.MEMINI_TOKEN || "";
|
|
737
|
+
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}`);
|
|
742
|
+
L.push(` ${pad("bearer", 28)} ${secret ? redactValue(secret) : "(none)"}`);
|
|
743
|
+
L.push("");
|
|
744
|
+
L.push(`SERVER`);
|
|
745
|
+
if (!server.reachable) {
|
|
746
|
+
L.push(` ${pad("reachable", 28)} NO \u2014 could not reach ${cfg.base_url}`);
|
|
747
|
+
} else {
|
|
748
|
+
const ver = server.version ? `, ${server.version}` : "";
|
|
749
|
+
L.push(` ${pad("reachable", 28)} yes (${server.latencyMs}ms${ver})`);
|
|
750
|
+
const d = server.deps || {};
|
|
751
|
+
if (d.store) L.push(` ${pad("store", 28)} ${d.store.ok ? "ok" : `FAILING \u2014 ${d.store.last_error || "?"}`}`);
|
|
752
|
+
if (d.embedder) {
|
|
753
|
+
L.push(` ${pad("embedder", 28)} ${d.embedder.ok ? "ok" : `FAILING \u2014 ${d.embedder.last_error || "?"}`}`);
|
|
754
|
+
}
|
|
755
|
+
if (server.healthExposed === false) {
|
|
756
|
+
L.push(` ${pad("dependency detail", 28)} unavailable (/healthz not routed \u2014 normal behind an ingress)`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
L.push("");
|
|
760
|
+
if (server.readSet?.entries?.length) {
|
|
761
|
+
L.push(`READ SET for "${ns.effective}" \u2014 where a plain recall looks`);
|
|
762
|
+
L.push(` ${pad("NAMESPACE", 34)} ${pad("ORIGIN", 12)} TIERS`);
|
|
763
|
+
for (const e of server.readSet.entries) {
|
|
764
|
+
const tiers = Array.isArray(e.tiers) && e.tiers.length ? e.tiers.join(",") : "all";
|
|
765
|
+
L.push(` ${pad(e.namespace, 34)} ${pad(e.origin, 12)} ${tiers}`);
|
|
766
|
+
}
|
|
767
|
+
L.push("");
|
|
768
|
+
}
|
|
769
|
+
L.push(`PATHS`);
|
|
770
|
+
L.push(` ${pad("overrides", 28)} ${settings.paths.overrides}`);
|
|
771
|
+
L.push("");
|
|
772
|
+
if (settings.warnings.length) {
|
|
773
|
+
L.push(`WARNINGS`);
|
|
774
|
+
for (const w of settings.warnings) {
|
|
775
|
+
L.push(` [${w.level === "warn" ? "!" : "i"}] ${w.code}: ${w.message}`);
|
|
776
|
+
if (w.fix) L.push(` fix: ${w.fix}`);
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
L.push(`No problems detected.`);
|
|
780
|
+
}
|
|
781
|
+
return L.join("\n");
|
|
782
|
+
}
|
|
783
|
+
function registerMeminiCommands(pi, cfg, warn) {
|
|
784
|
+
const show = (content) => {
|
|
785
|
+
pi.sendMessage({ customType: "memini-status", content, display: true });
|
|
786
|
+
};
|
|
787
|
+
pi.registerCommand("memini:status", {
|
|
788
|
+
description: "Show memini's effective settings: namespace + provenance, connection, server read set",
|
|
789
|
+
handler: async (_args, ctx) => {
|
|
790
|
+
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));
|
|
803
|
+
} catch (error) {
|
|
804
|
+
ctx.ui.notify(`memini: status failed: ${String(error)}`, "error");
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
pi.registerCommand("memini:namespace", {
|
|
809
|
+
description: "Show, set, or --clear the memini namespace override for this project",
|
|
810
|
+
handler: async (args, ctx) => {
|
|
811
|
+
try {
|
|
812
|
+
const cwd = ctx.cwd || process.cwd();
|
|
813
|
+
const arg = String(args || "").trim();
|
|
814
|
+
const before = resolveProjectNamespace(process.env, cwd);
|
|
815
|
+
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>`);
|
|
828
|
+
}
|
|
829
|
+
out.push(`Overrides file: ${defaultOverridesPath(process.env)}`);
|
|
830
|
+
show(out.join("\n"));
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
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.`);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const after2 = resolveProjectNamespace(process.env, cwd);
|
|
840
|
+
cfg.namespace = after2.namespace;
|
|
841
|
+
show(
|
|
842
|
+
[
|
|
843
|
+
`namespace override cleared: ${before.namespace} -> ${after2.namespace} (source: ${after2.source})`,
|
|
844
|
+
``,
|
|
845
|
+
`Recall and capture use the new namespace from the next turn.`
|
|
846
|
+
].join("\n")
|
|
847
|
+
);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const ns = normalizeNamespace(arg);
|
|
851
|
+
const bad = validateNamespace(ns);
|
|
852
|
+
if (bad) {
|
|
853
|
+
ctx.ui.notify(`memini: invalid namespace ${JSON.stringify(arg)}: ${bad}`, "error");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
writeOverride(cwd, ns, { env: process.env });
|
|
857
|
+
const after = resolveProjectNamespace(process.env, cwd);
|
|
858
|
+
cfg.namespace = after.namespace;
|
|
859
|
+
show(
|
|
860
|
+
[
|
|
861
|
+
`namespace override set: ${before.namespace} -> ${after.namespace}`,
|
|
862
|
+
`project: ${overrideKey(cwd)}`,
|
|
863
|
+
``,
|
|
864
|
+
`The override wins over MEMINI_NAMESPACE. Recall and capture use it from the next turn.`
|
|
865
|
+
].join("\n")
|
|
866
|
+
);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
ctx.ui.notify(`memini: namespace failed: ${String(error)}`, "error");
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
var TOOL_NAMES = ["memory_recall", "memory_briefing", "memory_list", "memory_remember", "memory_forget"];
|
|
398
874
|
var VALID_TIERS = ["working", "episodic", "semantic", "procedural"];
|
|
875
|
+
var VALID_SCOPES = ["project", "full", "everywhere"];
|
|
876
|
+
function briefingPath(args) {
|
|
877
|
+
const scope = String(args?.scope || "").trim();
|
|
878
|
+
return VALID_SCOPES.includes(scope) ? `/v1/namespaces/briefing?scope=${encodeURIComponent(scope)}` : "/v1/namespaces/briefing";
|
|
879
|
+
}
|
|
399
880
|
function sessionIdOf(ctx) {
|
|
400
881
|
try {
|
|
401
882
|
return String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
@@ -419,6 +900,11 @@ function meminiExtension(pi) {
|
|
|
419
900
|
};
|
|
420
901
|
const cfg = resolveConfig(process.env, process.cwd());
|
|
421
902
|
const client = createClient(cfg, warn);
|
|
903
|
+
try {
|
|
904
|
+
if (typeof pi.registerCommand === "function") registerMeminiCommands(pi, cfg, warn);
|
|
905
|
+
} catch (error) {
|
|
906
|
+
warn(`command registration skipped: ${String(error)}`);
|
|
907
|
+
}
|
|
422
908
|
const pendingUser = /* @__PURE__ */ new Map();
|
|
423
909
|
const captured = /* @__PURE__ */ new Set();
|
|
424
910
|
const injectedBySession = /* @__PURE__ */ new Map();
|
|
@@ -506,31 +992,70 @@ function meminiExtension(pi) {
|
|
|
506
992
|
description: 'Match memories whose top-level metadata contains each key=value pair, e.g. {"category":"bug_fixes"}.'
|
|
507
993
|
})
|
|
508
994
|
);
|
|
995
|
+
const Scope = Type.Optional(
|
|
996
|
+
Type.String({
|
|
997
|
+
enum: VALID_SCOPES,
|
|
998
|
+
description: "How wide to read: 'project' = just this project's own memories; 'full' (default) = project plus inherited context (ancestors, your personal namespace, links); 'everywhere' = full plus nested sub-projects."
|
|
999
|
+
})
|
|
1000
|
+
);
|
|
509
1001
|
pi.registerTool({
|
|
510
1002
|
name: "memory_recall",
|
|
511
1003
|
label: "Recall memory",
|
|
512
|
-
description: `
|
|
1004
|
+
description: `Search prior context in long-term memory (memini) via hybrid (semantic + keyword) retrieval, ranked by relevance, recency, and corroboration. Call BEFORE starting work that may have history: editing an unfamiliar file, debugging a recurring issue, making a non-obvious decision, or when asked what's known about something. Prefer a short descriptive query ('JWT auth setup'). scope picks how wide to read: 'project' (just this project), 'full' (default: project plus inherited ancestor/personal/link context), or 'everywhere' (full plus nested sub-projects). Each result's namespace/from fields are provenance, not a choice \u2014 an absent 'from' means this project's own memory, otherwise it names the ancestor or personal namespace the memory came from; read them to learn where knowledge lives, never construct a namespace path. Empty results mean nothing is known \u2014 proceed from first principles, never invent a remembered fact. A degraded:"keyword_only" field in the result means semantic search was unavailable and results came from keyword matching alone \u2014 treat as incomplete, not exhaustive.`,
|
|
513
1005
|
parameters: Type.Object({
|
|
514
1006
|
query: Type.String({ description: "What to search for" }),
|
|
515
1007
|
limit: Type.Optional(Type.Number({ description: "Max results (default 3)" })),
|
|
516
1008
|
tags: Tags,
|
|
517
|
-
metadata: Metadata
|
|
1009
|
+
metadata: Metadata,
|
|
1010
|
+
scope: Scope
|
|
518
1011
|
}),
|
|
519
1012
|
async execute(_toolCallId, params) {
|
|
520
1013
|
const body = { query: params.query, limit: params.limit || DEFAULT_RECALL_LIMIT };
|
|
521
1014
|
if (params.tags?.length) body.tags = params.tags;
|
|
522
1015
|
if (params.metadata && Object.keys(params.metadata).length) body.metadata = params.metadata;
|
|
1016
|
+
if (VALID_SCOPES.includes(params.scope)) body.scope = params.scope;
|
|
523
1017
|
const res = await client.postJson("/v1/search", body);
|
|
524
|
-
const results = (res?.results || []).map((r) =>
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1018
|
+
const results = (res?.results || []).map((r) => {
|
|
1019
|
+
const mem = r?.memory || {};
|
|
1020
|
+
const out = {
|
|
1021
|
+
id: mem.id || "",
|
|
1022
|
+
content: mem.content || "",
|
|
1023
|
+
summary: mem.summary || "",
|
|
1024
|
+
tier: mem.tier || "",
|
|
1025
|
+
score: typeof r?.score === "number" ? r.score : 0
|
|
1026
|
+
};
|
|
1027
|
+
if (mem.namespace) out.namespace = mem.namespace;
|
|
1028
|
+
if (r?.from) out.from = r.from;
|
|
1029
|
+
return out;
|
|
1030
|
+
});
|
|
531
1031
|
return text(res?.degraded ? { results, degraded: res.degraded, note: res.note } : { results });
|
|
532
1032
|
}
|
|
533
1033
|
});
|
|
1034
|
+
pi.registerTool({
|
|
1035
|
+
name: "memory_briefing",
|
|
1036
|
+
label: "Session briefing",
|
|
1037
|
+
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.",
|
|
1038
|
+
parameters: Type.Object({ scope: Scope }),
|
|
1039
|
+
async execute(_toolCallId, params) {
|
|
1040
|
+
const res = await client.getJson(briefingPath(params));
|
|
1041
|
+
if (!res) return text({ briefing: null, error: "memini unavailable" });
|
|
1042
|
+
const section = (items) => (items || []).map((b) => {
|
|
1043
|
+
const mem = b?.memory || {};
|
|
1044
|
+
const out = { id: mem.id || "", content: mem.content || "", tier: mem.tier || "" };
|
|
1045
|
+
if (mem.namespace) out.namespace = mem.namespace;
|
|
1046
|
+
if (b?.from) out.from = b.from;
|
|
1047
|
+
return out;
|
|
1048
|
+
});
|
|
1049
|
+
return text({
|
|
1050
|
+
namespace: res.namespace || "",
|
|
1051
|
+
scope_header: res.scope_header || "",
|
|
1052
|
+
pinned: section(res.pinned),
|
|
1053
|
+
facts: section(res.facts),
|
|
1054
|
+
procedures: section(res.procedures),
|
|
1055
|
+
recent: section(res.recent)
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
534
1059
|
pi.registerTool({
|
|
535
1060
|
name: "memory_list",
|
|
536
1061
|
label: "List memory",
|
|
@@ -560,7 +1085,7 @@ function meminiExtension(pi) {
|
|
|
560
1085
|
pi.registerTool({
|
|
561
1086
|
name: "memory_remember",
|
|
562
1087
|
label: "Remember",
|
|
563
|
-
description: "Store a durable fact, decision, or preference in long-term memory (memini). Call proactively when the user says 'remember this', after an architectural decision (capture the why), or after discovering a non-obvious bug or convention. Keep memories atomic \u2014 one self-contained fact per call. Don't store what's already in project docs or trivially recoverable from code. To correct an existing memory, pass its id \u2014 the write updates it in place.",
|
|
1088
|
+
description: "Store a durable fact, decision, or preference in long-term memory (memini). Call proactively when the user says 'remember this', after an architectural decision (capture the why), or after discovering a non-obvious bug or convention. Keep memories atomic \u2014 one self-contained fact per call. Don't store what's already in project docs or trivially recoverable from code. To correct an existing memory, pass its id \u2014 the write updates it in place. visibility decides who should know: 'project' (default) keeps it here; 'personal' follows the user everywhere; or name an ancestor from the memory_briefing Scope line to share it up that chain. reinforced=true in the result means the fact was ALREADY KNOWN: no new memory was created, the existing one was strengthened, and `id` names that pre-existing memory rather than anything you just wrote \u2014 do not report it to the user as a new save.",
|
|
564
1089
|
parameters: Type.Object({
|
|
565
1090
|
content: Type.String({ description: "The fact to remember \u2014 atomic and self-contained." }),
|
|
566
1091
|
id: Type.Optional(
|
|
@@ -582,6 +1107,11 @@ function meminiExtension(pi) {
|
|
|
582
1107
|
Type.String({
|
|
583
1108
|
description: "Optional topic bucket stored as metadata.category (e.g. bug_fixes, architecture_decisions) for browsing by subject later."
|
|
584
1109
|
})
|
|
1110
|
+
),
|
|
1111
|
+
visibility: Type.Optional(
|
|
1112
|
+
Type.String({
|
|
1113
|
+
description: "Who should remember this: 'project' (default, this project only), 'personal' (about the user, follows them everywhere), or an ancestor namespace name read off the memory_briefing Scope line (e.g. the team or org level) to share it up that chain. On a durable write an unrecognized name errors listing the valid options. Episodic/working writes always stay in the project regardless."
|
|
1114
|
+
})
|
|
585
1115
|
)
|
|
586
1116
|
}),
|
|
587
1117
|
async execute(_toolCallId, params) {
|
|
@@ -590,8 +1120,13 @@ function meminiExtension(pi) {
|
|
|
590
1120
|
if (params.tier && VALID_TIERS.includes(params.tier)) body.tier = params.tier;
|
|
591
1121
|
if (params.tags?.length) body.tags = params.tags;
|
|
592
1122
|
if (params.category) body.metadata = { category: params.category };
|
|
593
|
-
const
|
|
594
|
-
|
|
1123
|
+
const visibility = String(params.visibility || "").trim();
|
|
1124
|
+
if (visibility) body.visibility = visibility;
|
|
1125
|
+
const res = await client.postJsonResult("/v1/memories", body);
|
|
1126
|
+
if (!res.ok) return text({ id: null, success: false, error: res.error });
|
|
1127
|
+
const out = { id: res.data?.id || null, success: true };
|
|
1128
|
+
if (res.data?.reinforced) out.reinforced = true;
|
|
1129
|
+
return text(out);
|
|
595
1130
|
}
|
|
596
1131
|
});
|
|
597
1132
|
pi.registerTool({
|
|
@@ -611,6 +1146,7 @@ function meminiExtension(pi) {
|
|
|
611
1146
|
}
|
|
612
1147
|
export {
|
|
613
1148
|
approxTokens,
|
|
1149
|
+
briefingPath,
|
|
614
1150
|
buildTurnContent,
|
|
615
1151
|
createPlaintextBearerAuthGuard,
|
|
616
1152
|
meminiExtension as default,
|
|
@@ -623,7 +1159,10 @@ export {
|
|
|
623
1159
|
intEnv,
|
|
624
1160
|
labelsEnv,
|
|
625
1161
|
meminiListPath,
|
|
1162
|
+
registerMeminiCommands,
|
|
1163
|
+
renderStatus,
|
|
626
1164
|
resolveConfig,
|
|
1165
|
+
resolveProjectNamespace,
|
|
627
1166
|
sanitizeNamespace,
|
|
628
1167
|
sanitizeNamespacePath,
|
|
629
1168
|
truncate
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eleboucher/pi-memini",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
4
4
|
"description": "Shared cross-session memory for the Pi coding agent, backed by a memini service.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
7
7
|
],
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --external:typebox --external:@earendil-works/pi-coding-agent --alias:@memini/namespace-resolver=../../../packages/namespace-resolver/src/index.ts --outfile=dist/index.js",
|
|
10
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --external:typebox --external:@earendil-works/pi-coding-agent --alias:@memini/namespace-resolver=../../../packages/namespace-resolver/src/index.ts --alias:@memini/client=../../../packages/memini-client/src/index.ts --outfile=dist/index.js",
|
|
11
11
|
"test": "node --test test/bundle.test.mjs && npx tsx --test test/helpers.test.ts",
|
|
12
12
|
"typecheck": "tsc -p tsconfig.test.json"
|
|
13
13
|
},
|