@eleboucher/pi-memini 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +97 -48
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28,9 +28,72 @@ function validateNamespace(ns) {
28
28
  return null;
29
29
  }
30
30
 
31
+ // ../../../packages/memini-client/src/capture.ts
32
+ var TRUNCATION_MARKER = "\n[...truncated]";
33
+ function truncateForCapture(s, max) {
34
+ if (typeof max !== "number" || !Number.isFinite(max) || max <= 0) return s;
35
+ const cap = Math.floor(max);
36
+ if (s.length <= cap) return s;
37
+ let i = 0;
38
+ for (let n = 0; i < s.length && n < cap; n++) {
39
+ i += s.codePointAt(i) > 65535 ? 2 : 1;
40
+ }
41
+ if (i >= s.length) return s;
42
+ return s.slice(0, i) + TRUNCATION_MARKER;
43
+ }
44
+ function buildTurnCapture(userText, assistantText, userMax, assistantMax) {
45
+ return `${truncateForCapture(userText, userMax)}
46
+
47
+ ${truncateForCapture(assistantText, assistantMax)}`;
48
+ }
49
+
31
50
  // ../../../packages/memini-client/src/session.ts
32
51
  var SESSION_CWD_TTL_MS = 6 * 60 * 60 * 1e3;
33
52
 
53
+ // ../../../packages/memini-client/src/bootstrap.ts
54
+ var DEFAULT_TIMEOUT_MS = 3e4;
55
+ var MIN_TIMEOUT_MS = 100;
56
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
57
+ function envTimeoutMs(raw, fallback = DEFAULT_TIMEOUT_MS) {
58
+ if (raw == null || raw.trim() === "") return fallback;
59
+ const n = Number(raw.trim());
60
+ if (!Number.isFinite(n)) return fallback;
61
+ return Math.max(MIN_TIMEOUT_MS, Math.trunc(n));
62
+ }
63
+ function envEnabled(raw, defaultOn) {
64
+ if (raw == null || raw === "") return defaultOn;
65
+ return !/^(0|false|no|off)$/i.test(raw.trim());
66
+ }
67
+ function readBootstrap(env = process.env) {
68
+ return {
69
+ baseUrl: env["MEMINI_BASE_URL"] || "http://localhost:8080",
70
+ apiKey: env["MEMINI_API_KEY"] || "",
71
+ requireHttps: envEnabled(env["MEMINI_REQUIRE_HTTPS"], false),
72
+ debug: envEnabled(env["MEMINI_DEBUG"], false),
73
+ agent: env["MEMINI_AGENT"] || "",
74
+ namespaceEnv: (env["MEMINI_NAMESPACE"] || "").trim(),
75
+ namespacePrefixEnv: (env["MEMINI_NAMESPACE_PREFIX"] || "").trim(),
76
+ homeEnv: (env["MEMINI_HOME"] || "").trim(),
77
+ timeoutMs: envTimeoutMs(env["MEMINI_TIMEOUT_MS"])
78
+ };
79
+ }
80
+ function isPlaintextBearerUnsafe(baseUrl, secret) {
81
+ if (!secret) return false;
82
+ try {
83
+ const u = new URL(baseUrl);
84
+ return u.protocol === "http:" && !LOOPBACK_HOSTS.has(u.hostname.replace(/^\[|\]$/g, "").toLowerCase());
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+ function assertBearerTransportSafe(baseUrl, secret, env = process.env) {
90
+ if (!isPlaintextBearerUnsafe(baseUrl, secret)) return;
91
+ if (!envEnabled(env["MEMINI_REQUIRE_HTTPS"], false)) return;
92
+ throw new Error(
93
+ `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.`
94
+ );
95
+ }
96
+
34
97
  // ../../../packages/memini-client/src/settings.ts
35
98
  var BEHAVIOR_KNOBS = [
36
99
  { envName: "MEMINI_CAPTURE_TURNS", wireKey: "capture_turns", kind: "bool", default: true },
@@ -38,6 +101,7 @@ var BEHAVIOR_KNOBS = [
38
101
  { envName: "MEMINI_INLINE_EXTRACT", wireKey: "inline_extract", kind: "bool", default: true },
39
102
  { envName: "MEMINI_AUTO_SAVE", wireKey: "auto_save", kind: "bool", default: true },
40
103
  { envName: "MEMINI_AUTO_SAVE_INTERVAL", wireKey: "auto_save_interval", kind: "int", default: 10 },
104
+ { envName: "MEMINI_AUTO_SAVE_MIN_EVENTS", wireKey: "auto_save_min_events", kind: "int", default: 3 },
41
105
  { envName: "MEMINI_INJECT_BRIEFING_PINNED", wireKey: "inject_briefing_pinned", kind: "int", default: 5 },
42
106
  { envName: "MEMINI_INJECT_BRIEFING_FACTS", wireKey: "inject_briefing_facts", kind: "int", default: 5 },
43
107
  { envName: "MEMINI_INJECT_BRIEFING_PROCEDURES", wireKey: "inject_briefing_procedures", kind: "int", default: 5 },
@@ -59,11 +123,30 @@ var BEHAVIOR_KNOBS = [
59
123
  { envName: "MEMINI_RECALL_LIMIT", wireKey: "recall_limit", kind: "int", default: 3 },
60
124
  { envName: "MEMINI_INJECT_RECALL_MAX_TOK", wireKey: "inject_recall_max_tok", kind: "int", default: 0 },
61
125
  { 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 }
126
+ { envName: "MEMINI_MIN_CAPTURE_CHARS", wireKey: "min_capture_chars", kind: "int", default: 0 },
127
+ { envName: "MEMINI_CAPTURE_USER_MAX_CHARS", wireKey: "capture_user_max_chars", kind: "int", default: 1e3 },
128
+ {
129
+ envName: "MEMINI_CAPTURE_ASSISTANT_MAX_CHARS",
130
+ wireKey: "capture_assistant_max_chars",
131
+ kind: "int",
132
+ default: 3e3
133
+ },
134
+ // Also read at the transport layer (bootstrap.ts's timeoutMs), because a
135
+ // request timeout has to exist before the handshake that fetches settings.
136
+ // Both spellings share DEFAULT_TIMEOUT_MS/MIN_TIMEOUT_MS, so there is exactly
137
+ // one number: raising it here (server-side) or in the env both work.
138
+ {
139
+ envName: "MEMINI_TIMEOUT_MS",
140
+ wireKey: "request_timeout_ms",
141
+ kind: "int",
142
+ default: DEFAULT_TIMEOUT_MS,
143
+ min: MIN_TIMEOUT_MS
144
+ }
63
145
  ];
64
- function parseIntKnob(raw, fallback) {
146
+ function parseIntKnob(raw, fallback, min = 0) {
65
147
  const n = Number.parseInt(raw, 10);
66
- return Number.isFinite(n) && n >= 0 ? n : fallback;
148
+ if (!Number.isFinite(n) || n < 0) return fallback;
149
+ return Math.max(min, n);
67
150
  }
68
151
  function parseFloatKnob(raw, fallback) {
69
152
  const n = Number.parseFloat(raw);
@@ -81,7 +164,7 @@ function effectiveSetting(knob2, server, env = process.env) {
81
164
  value = !/^(0|false|no|off)$/i.test(raw.trim());
82
165
  break;
83
166
  case "int":
84
- value = parseIntKnob(raw, knob2.default);
167
+ value = parseIntKnob(raw, knob2.default, knob2.min);
85
168
  break;
86
169
  case "float":
87
170
  value = parseFloatKnob(raw, knob2.default);
@@ -98,41 +181,6 @@ function effectiveSetting(knob2, server, env = process.env) {
98
181
  return { value: knob2.default, source: "default" };
99
182
  }
100
183
 
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());
106
- }
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()
117
- };
118
- }
119
- function isPlaintextBearerUnsafe(baseUrl, secret) {
120
- if (!secret) return false;
121
- try {
122
- const u = new URL(baseUrl);
123
- return u.protocol === "http:" && !LOOPBACK_HOSTS.has(u.hostname.replace(/^\[|\]$/g, "").toLowerCase());
124
- } catch {
125
- return false;
126
- }
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
- );
134
- }
135
-
136
184
  // ../../../packages/memini-client/src/facts.ts
137
185
  import { execFileSync } from "node:child_process";
138
186
  import path from "node:path";
@@ -269,7 +317,7 @@ async function performHandshake(boot, facts, opts = {}) {
269
317
  }
270
318
 
271
319
  // src/index.ts
272
- var DEFAULT_TIMEOUT_MS = 3e4;
320
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
273
321
  var DEFAULT_RECALL_LIMIT = 3;
274
322
  var STATUS_TIMEOUT_MS = 4e3;
275
323
  var HANDSHAKE_TIMEOUT_MS = 2500;
@@ -349,7 +397,7 @@ function resolveStaticConfig(env = process.env) {
349
397
  const homeEnv = (e.MEMINI_HOME || "").trim();
350
398
  return {
351
399
  home: homeEnv || void 0,
352
- timeout_ms: Number(e.MEMINI_TIMEOUT_MS || DEFAULT_TIMEOUT_MS),
400
+ timeout_ms: Number(e.MEMINI_TIMEOUT_MS || DEFAULT_TIMEOUT_MS2),
353
401
  fallback_on_error: envBool(e.MEMINI_FALLBACK, true)
354
402
  };
355
403
  }
@@ -369,7 +417,9 @@ function resolveLiveConfig(boot, facts, hs, env = process.env) {
369
417
  capture: effectiveSetting(knob("capture"), server, env).value,
370
418
  recall_limit: effectiveSetting(knob("recall_limit"), server, env).value,
371
419
  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
420
+ recall_min_score: effectiveSetting(knob("inject_recall_min_score"), server, env).value,
421
+ capture_user_max_chars: effectiveSetting(knob("capture_user_max_chars"), server, env).value,
422
+ capture_assistant_max_chars: effectiveSetting(knob("capture_assistant_max_chars"), server, env).value
373
423
  };
374
424
  }
375
425
  async function sessionLive(ctx, env = process.env) {
@@ -537,10 +587,8 @@ function extractLastAssistantText(messages) {
537
587
  }
538
588
  return "";
539
589
  }
540
- function buildTurnContent(userText, assistantText) {
541
- return `${String(userText).slice(0, 1e3)}
542
-
543
- ${String(assistantText).slice(0, 3e3)}`;
590
+ function buildTurnContent(userText, assistantText, userMax, assistantMax) {
591
+ return buildTurnCapture(String(userText), String(assistantText), userMax, assistantMax);
544
592
  }
545
593
  function pinKeyFacts(facts) {
546
594
  const out = {};
@@ -1003,7 +1051,7 @@ function meminiExtension(pi) {
1003
1051
  const stored = await client.postJson(
1004
1052
  "/v1/memories",
1005
1053
  {
1006
- content: buildTurnContent(userText, assistantText),
1054
+ content: buildTurnContent(userText, assistantText, live.capture_user_max_chars, live.capture_assistant_max_chars),
1007
1055
  tags: ["pi"],
1008
1056
  metadata
1009
1057
  },
@@ -1119,7 +1167,8 @@ function meminiExtension(pi) {
1119
1167
  pi.registerTool({
1120
1168
  name: "memory_remember",
1121
1169
  label: "Remember",
1122
- 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.",
1170
+ // Save-policy invariants are canonical in internal/api/mcp/mcp.go serverInstructions.
1171
+ description: "Store a fact, decision, preference, or event for later recall. Do not wait to be asked \u2014 call this the moment you learn: a decision and why it was made, a bug's root cause, a project convention, a stated user preference, a correction from the user (a correction IS a durable preference), an environment or tool quirk, or a non-obvious command/workflow. When the user says 'remember this', 'note that', 'don't forget', 'going forward...', or corrects you, call this tool FIRST, then acknowledge \u2014 and on an explicit request save unconditionally, even if it seems trivial or already stored; secrets and credentials are the one exception. Keep memories atomic \u2014 one self-contained fact per call; search works better on small records. Do NOT store secrets or credentials, transient session state, task progress, or facts already in project docs/CLAUDE.md or trivially recoverable from code. To correct an existing memory, pass its id \u2014 the write updates it in place. If a stored memory proves wrong or outdated, fix it immediately: re-save the corrected fact with the existing id, or delete it with memory_forget if it should not exist \u2014 never leave a known-incorrect memory 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.",
1123
1172
  parameters: Type.Object({
1124
1173
  content: Type.String({ description: "The fact to remember \u2014 atomic and self-contained." }),
1125
1174
  id: Type.Optional(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eleboucher/pi-memini",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Shared cross-session memory for the Pi coding agent, backed by a memini service.",
5
5
  "files": [
6
6
  "dist"