@eleboucher/pi-memini 0.5.11 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +58 -8
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare function floatEnv(name: string, def: number): number;
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function labelsEnv(name?: string): Set<string>;
|
|
33
33
|
export declare function sanitizeNamespace(s: string): string;
|
|
34
|
+
export declare function sanitizeNamespacePath(s: string): string;
|
|
34
35
|
export declare function deriveNamespace(cwd: string | undefined): string;
|
|
35
36
|
export interface ResolvedConfig {
|
|
36
37
|
base_url: string;
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* MEMINI_API_KEY stay in the environment. See ../README.md for the table.
|
|
19
19
|
*/
|
|
20
20
|
import { Type } from "typebox";
|
|
21
|
+
import { resolveNamespace } from "@memini/namespace-resolver";
|
|
21
22
|
const DEFAULT_BASE_URL = "http://localhost:8080";
|
|
22
23
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
23
24
|
const DEFAULT_RECALL_LIMIT = 3;
|
|
@@ -72,6 +73,17 @@ export function sanitizeNamespace(s) {
|
|
|
72
73
|
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
73
74
|
.replace(/^-+|-+$/g, "");
|
|
74
75
|
}
|
|
76
|
+
// sanitizeNamespacePath sanitizes a hierarchical namespace per segment,
|
|
77
|
+
// preserving the "/" separators the resolver's tenant paths carry
|
|
78
|
+
// (work/memini must not flatten to work-memini — the other integrations keep
|
|
79
|
+
// the separator, and flattening would split memory across integrations).
|
|
80
|
+
export function sanitizeNamespacePath(s) {
|
|
81
|
+
return String(s)
|
|
82
|
+
.split("/")
|
|
83
|
+
.map(sanitizeNamespace)
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join("/");
|
|
86
|
+
}
|
|
75
87
|
// deriveNamespace scopes memory to the project: the basename of the working
|
|
76
88
|
// directory, the same scheme memini auto-resolves from a git repo. Returns ""
|
|
77
89
|
// when no path is given.
|
|
@@ -86,14 +98,37 @@ export function deriveNamespace(cwd) {
|
|
|
86
98
|
// testing.
|
|
87
99
|
export function resolveConfig(env, cwd) {
|
|
88
100
|
const e = env || {};
|
|
89
|
-
const
|
|
101
|
+
const nsEnv = (e.MEMINI_NAMESPACE || "").trim();
|
|
102
|
+
let namespace;
|
|
103
|
+
if (nsEnv) {
|
|
104
|
+
// MEMINI_NAMESPACE wins and is used raw-trimmed (the server validates the
|
|
105
|
+
// header). Routing it through sanitizeNamespacePath would alter an explicit
|
|
106
|
+
// value — the canonical resolver returns it untouched.
|
|
107
|
+
namespace = nsEnv;
|
|
108
|
+
}
|
|
109
|
+
else if (cwd) {
|
|
110
|
+
const { namespace: resolvedNs } = resolveNamespace({
|
|
111
|
+
cwd,
|
|
112
|
+
env: e,
|
|
113
|
+
integration: "pi",
|
|
114
|
+
});
|
|
115
|
+
// Per-segment sanitize: resolver output may be a tenant path (work/memini).
|
|
116
|
+
namespace = sanitizeNamespacePath(resolvedNs) || DEFAULT_NAMESPACE;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// No explicit namespace and no cwd to resolve against.
|
|
120
|
+
namespace = DEFAULT_NAMESPACE;
|
|
121
|
+
}
|
|
90
122
|
const recall_limit = (() => {
|
|
91
123
|
const n = Number(e.MEMINI_RECALL_LIMIT);
|
|
92
124
|
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RECALL_LIMIT;
|
|
93
125
|
})();
|
|
94
126
|
return {
|
|
95
127
|
base_url: e.MEMINI_BASE_URL || e.MEMINI_URL || DEFAULT_BASE_URL,
|
|
96
|
-
namespace
|
|
128
|
+
// namespace is already resolved above (raw-trimmed on the env path,
|
|
129
|
+
// per-segment sanitized on the resolver path); re-sanitizing here would
|
|
130
|
+
// flatten tenant separators.
|
|
131
|
+
namespace: namespace || DEFAULT_NAMESPACE,
|
|
97
132
|
recall: envBool(e.MEMINI_RECALL, true),
|
|
98
133
|
capture: envBool(e.MEMINI_CAPTURE, true),
|
|
99
134
|
recall_limit,
|
|
@@ -417,6 +452,12 @@ export default function meminiExtension(pi) {
|
|
|
417
452
|
"current workspace state and the user's instructions):",
|
|
418
453
|
...fit.items,
|
|
419
454
|
];
|
|
455
|
+
// /v1/search sets `degraded: "keyword_only"` (plus a `note`) when the query
|
|
456
|
+
// embed was unavailable and it fell back to keyword-only matching; both are
|
|
457
|
+
// already on `result`, so surfacing them is a one-line addition.
|
|
458
|
+
if (result?.degraded) {
|
|
459
|
+
lines.push(`[memini: ${result.note || "semantic search unavailable — results are keyword-only and may be incomplete"}]`);
|
|
460
|
+
}
|
|
420
461
|
if (fit.dropped > 0)
|
|
421
462
|
lines.push(`[... ${fit.dropped} item(s) truncated by token budget]`);
|
|
422
463
|
return {
|
|
@@ -447,7 +488,6 @@ export default function meminiExtension(pi) {
|
|
|
447
488
|
metadata.session_id = sid;
|
|
448
489
|
const stored = await client.postJson("/v1/memories", {
|
|
449
490
|
content: buildTurnContent(userText, assistantText),
|
|
450
|
-
tier: "episodic",
|
|
451
491
|
tags: ["pi"],
|
|
452
492
|
metadata,
|
|
453
493
|
});
|
|
@@ -467,7 +507,11 @@ export default function meminiExtension(pi) {
|
|
|
467
507
|
pi.registerTool({
|
|
468
508
|
name: "memory_recall",
|
|
469
509
|
label: "Recall memory",
|
|
470
|
-
description: "Recall relevant memories from long-term memory (memini) via hybrid (semantic + keyword) search."
|
|
510
|
+
description: "Recall relevant memories from long-term memory (memini) via hybrid (semantic + keyword) search. " +
|
|
511
|
+
"Call before starting work that may have history: editing an unfamiliar file, debugging a recurring " +
|
|
512
|
+
"issue, or when asked what's known about something. A degraded:\"keyword_only\" field in the result " +
|
|
513
|
+
"means semantic search was unavailable and results came from keyword matching alone — treat as " +
|
|
514
|
+
"incomplete, not exhaustive.",
|
|
471
515
|
parameters: Type.Object({
|
|
472
516
|
query: Type.String({ description: "What to search for" }),
|
|
473
517
|
limit: Type.Optional(Type.Number({ description: "Max results (default 3)" })),
|
|
@@ -488,7 +532,9 @@ export default function meminiExtension(pi) {
|
|
|
488
532
|
tier: r?.memory?.tier || "",
|
|
489
533
|
score: typeof r?.score === "number" ? r.score : 0,
|
|
490
534
|
}));
|
|
491
|
-
|
|
535
|
+
// /v1/search already carries `degraded`/`note` on `res`; pass them through
|
|
536
|
+
// rather than dropping them silently.
|
|
537
|
+
return text(res?.degraded ? { results, degraded: res.degraded, note: res.note } : { results });
|
|
492
538
|
},
|
|
493
539
|
});
|
|
494
540
|
pi.registerTool({
|
|
@@ -519,7 +565,9 @@ export default function meminiExtension(pi) {
|
|
|
519
565
|
pi.registerTool({
|
|
520
566
|
name: "memory_remember",
|
|
521
567
|
label: "Remember",
|
|
522
|
-
description: "Store a durable fact, decision, or preference in long-term memory (memini)."
|
|
568
|
+
description: "Store a durable fact, decision, or preference in long-term memory (memini). Call proactively when " +
|
|
569
|
+
"the user says 'remember this', after an architectural decision (capture the why), or after " +
|
|
570
|
+
"discovering a non-obvious bug or convention. Keep memories atomic — one self-contained fact per call.",
|
|
523
571
|
parameters: Type.Object({
|
|
524
572
|
content: Type.String({ description: "The fact to remember" }),
|
|
525
573
|
tier: Type.Optional(Type.String({
|
|
@@ -548,8 +596,10 @@ export default function meminiExtension(pi) {
|
|
|
548
596
|
pi.registerTool({
|
|
549
597
|
name: "memory_forget",
|
|
550
598
|
label: "Forget",
|
|
551
|
-
description: "
|
|
552
|
-
"outdated, or poisoned. Get the id from memory_recall or memory_list.
|
|
599
|
+
description: "Permanently delete a memory from long-term memory (memini) by its id — use when a recalled memory " +
|
|
600
|
+
"is wrong, outdated, or poisoned. Get the id from memory_recall or memory_list. To correct a fact, " +
|
|
601
|
+
"forget the stale one and remember the corrected version — this integration talks to memini over " +
|
|
602
|
+
"REST, which has no partial-update endpoint.",
|
|
553
603
|
parameters: Type.Object({
|
|
554
604
|
id: Type.String({ description: "The id of the memory to forget (from memory_recall / memory_list)." }),
|
|
555
605
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eleboucher/pi-memini",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Shared cross-session memory for the Pi coding agent, backed by a memini service.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"typecheck": "tsc -p tsconfig.test.json"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"@memini/namespace-resolver": "workspace:*",
|
|
15
16
|
"typebox": "^1.1.38"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|