@membank/cli 0.11.2 → 0.13.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.
Files changed (3) hide show
  1. package/README.md +19 -7
  2. package/dist/index.mjs +263 -231
  3. package/package.json +5 -6
package/README.md CHANGED
@@ -35,6 +35,16 @@ Options:
35
35
 
36
36
  Supported harnesses: `claude-code`, `copilot`, `codex`, `opencode` (see `membank setup` for harness-specific setup instructions)
37
37
 
38
+ ### `membank setup upgrade`
39
+
40
+ Migrate existing harness configs from the old `npx @membank/cli --mcp` pattern to the standalone `npx @membank/mcp` binary:
41
+
42
+ ```bash
43
+ membank setup upgrade
44
+ ```
45
+
46
+ Run this once after upgrading to align all configured harnesses with the new standalone MCP package.
47
+
38
48
  ## Commands
39
49
 
40
50
  ### `membank query <text>`
@@ -167,31 +177,33 @@ membank inject --harness claude-code --event session-stop
167
177
 
168
178
  Options: `--harness <name>` (claude-code|copilot-cli|codex|opencode), `--event <event>` (session-start|user-prompt-submit|session-stop)
169
179
 
170
- ### `membank dashboard`
180
+ ### `membank dashboard` (deprecated)
171
181
 
172
- Start the web dashboard for browsing and managing memories.
182
+ The dashboard is now a standalone package. Run it directly:
173
183
 
174
184
  ```bash
175
- membank dashboard
185
+ npx @membank/dashboard
176
186
  ```
177
187
 
178
- Opens http://localhost:3847 by default. Features: full-text search, filtering by type/scope/pin status, edit memory metadata, view dedup reviews, and storage statistics.
188
+ See [`@membank/dashboard`](../dashboard/README.md) for options.
179
189
 
180
190
  ## Global flags
181
191
 
182
192
  ```
183
193
  --json Output machine-readable JSON
184
194
  --yes, -y Skip confirmation prompts
185
- --mcp Start MCP stdio server (used by harness config)
195
+ --mcp Start MCP stdio server (deprecated use npx @membank/mcp)
186
196
  ```
187
197
 
188
198
  ## MCP server mode
189
199
 
200
+ The preferred way to run the MCP server is via the standalone package:
201
+
190
202
  ```bash
191
- membank --mcp
203
+ npx @membank/mcp
192
204
  ```
193
205
 
194
- Starts the stdio MCP server. This is what harnesses connect to `setup` writes this command into harness configs automatically.
206
+ `membank setup` writes this command into harness configs automatically. The legacy `membank --mcp` flag still works but emits a deprecation warning. Run `membank setup upgrade` to migrate existing harness configs.
195
207
 
196
208
  ## Session hooks
197
209
 
package/dist/index.mjs CHANGED
@@ -1,20 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { cancel, confirm, intro, isCancel, multiselect, note, outro } from "@clack/prompts";
3
- import { DatabaseManager, EmbeddingService, MIGRATIONS, MemoryRepository, MemoryTypeSchema, MemoryTypeSchema as MemoryTypeSchema$1, PIN_BUDGET_THRESHOLD, ProjectRepository, QueryEngine, SessionContextBuilder, SynthesisRepository, TagsJsonSchema as TagsRowSchema, resolveProject, runScopeToProjectsMigration } from "@membank/core";
4
- import { runSynthesis, startServer } from "@membank/mcp";
3
+ import { DatabaseManager, EmbeddingService, MIGRATIONS, MODEL_NAME, MemoryTypeSchema, MemoryTypeSchema as MemoryTypeSchema$1, ModelDownloader, PIN_BUDGET_THRESHOLD, QueryEngine, SessionContextBuilder, createMemoryRepository, createProjectRepository, createSynthesisRepository, resolveProject, runScopeToProjectsMigration, saveMemory } from "@membank/core";
4
+ import { runExtraction, runSynthesis, startServer } from "@membank/mcp";
5
5
  import chalk from "chalk";
6
6
  import { Command } from "commander";
7
7
  import ora from "ora";
8
8
  import { z } from "zod";
9
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
9
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
10
  import { homedir, tmpdir } from "node:os";
11
11
  import { dirname, join } from "node:path";
12
- import { startDashboard } from "@membank/dashboard";
13
12
  import Table from "cli-table3";
14
13
  import { execFile } from "node:child_process";
15
14
  import { promisify } from "node:util";
16
- import { EventEmitter } from "node:events";
17
- import { pipeline } from "@huggingface/transformers";
18
15
  import { createInterface } from "node:readline";
19
16
  //#region src/schemas.ts
20
17
  const SETUP_HARNESS_VALUES = [
@@ -32,7 +29,7 @@ const InjectionHarnessSchema = z.enum([
32
29
  ]);
33
30
  const MigrateModeSchema = z.enum(["list", "run"]);
34
31
  const LimitSchema = z.coerce.number().int().positive();
35
- const PortSchema = z.coerce.number().int().min(1).max(65535);
32
+ z.coerce.number().int().min(1).max(65535);
36
33
  const OptionalNumberSchema = z.number().optional().catch(void 0);
37
34
  const MutableJsonObjectSchema = z.record(z.string(), z.unknown());
38
35
  const MaybeJsonObjectSchema = z.record(z.string(), z.unknown()).optional().catch(void 0);
@@ -60,15 +57,19 @@ async function addCommand(content, options, formatter, db, embeddingService) {
60
57
  const ownDb = db === void 0;
61
58
  const resolvedDb = db ?? DatabaseManager.open();
62
59
  try {
63
- const repo = new MemoryRepository(resolvedDb, embeddingService ?? new EmbeddingService(), new ProjectRepository(resolvedDb));
60
+ const embedder = embeddingService ?? new EmbeddingService();
61
+ const repo = createMemoryRepository(resolvedDb, createProjectRepository(resolvedDb));
64
62
  const tags = options.tags !== void 0 ? options.tags.split(",").map((t) => t.trim()) : [];
65
63
  const projectScope = options.global ? void 0 : await resolveProject();
66
64
  const spinner = formatter.isJson ? null : ora("Saving memory…").start();
67
- const memory = await repo.save({
65
+ const memory = await saveMemory({
68
66
  content,
69
67
  type: MemoryTypeSchema$1.parse(options.type),
70
68
  tags,
71
69
  projectScope
70
+ }, {
71
+ repo,
72
+ embedder
72
73
  });
73
74
  spinner?.succeed("Memory saved");
74
75
  formatter.outputMemory(memory);
@@ -151,35 +152,31 @@ function configShowCommand(formatter) {
151
152
  process.stdout.write(`${JSON.stringify(config, null, formatter.isJson ? 0 : 2)}\n`);
152
153
  }
153
154
  //#endregion
154
- //#region src/commands/dashboard.ts
155
- async function dashboardCommand(opts) {
156
- await startDashboard({ port: opts.port !== void 0 ? PortSchema.parse(opts.port) : void 0 });
157
- }
158
- //#endregion
159
155
  //#region src/commands/delete.ts
160
156
  async function deleteCommand(id, db, formatter, prompt) {
161
- if (db.db.prepare(`SELECT id FROM memories WHERE id = ?`).get(id) === void 0) {
157
+ const repo = createMemoryRepository(db, createProjectRepository(db));
158
+ if (repo.findById(id) === void 0) {
162
159
  formatter.error(`Memory not found: ${id}`);
163
160
  process.exit(1);
164
161
  }
165
162
  if (!await prompt.confirm(`Delete memory ${id}?`)) return;
166
- await new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db)).delete(id);
163
+ repo.delete(id);
167
164
  process.stdout.write(`${chalk.green("✓")} Deleted memory: ${chalk.dim(id)}\n`);
168
165
  }
169
166
  //#endregion
170
167
  //#region src/commands/export.ts
171
168
  function exportCommand(db, formatter, opts) {
172
- const memories = db.db.prepare(`SELECT m.*, e.embedding FROM memories m LEFT JOIN embeddings e ON e.rowid = m.rowid ORDER BY m.created_at DESC`).all().map((row) => ({
173
- id: row.id,
174
- content: row.content,
175
- type: row.type,
176
- tags: TagsRowSchema.parse(JSON.parse(row.tags)),
177
- sourceHarness: row.source,
178
- accessCount: row.access_count,
179
- pinned: row.pinned !== 0,
180
- createdAt: row.created_at,
181
- updatedAt: row.updated_at,
182
- embedding: row.embedding !== null ? Buffer.from(row.embedding).toString("base64") : null
169
+ const memories = createMemoryRepository(db, createProjectRepository(db)).exportAll().map((rec) => ({
170
+ id: rec.id,
171
+ content: rec.content,
172
+ type: rec.type,
173
+ tags: rec.tags,
174
+ sourceHarness: rec.sourceHarness,
175
+ accessCount: rec.accessCount,
176
+ pinned: rec.pinned,
177
+ createdAt: rec.createdAt,
178
+ updatedAt: rec.updatedAt,
179
+ embedding: rec.embedding !== null ? Buffer.from(rec.embedding.buffer, rec.embedding.byteOffset, rec.embedding.byteLength).toString("base64") : null
183
180
  }));
184
181
  const data = {
185
182
  version: 1,
@@ -195,6 +192,95 @@ function exportCommand(db, formatter, opts) {
195
192
  else process.stdout.write(`Exported ${memories.length} memories to ${outputPath}\n`);
196
193
  }
197
194
  //#endregion
195
+ //#region src/commands/extract.ts
196
+ const ExtractionHarnessSchema = z.enum(["claude-code"]);
197
+ const ClaudeCodeStopInputSchema = z.object({
198
+ session_id: z.string(),
199
+ transcript_path: z.string(),
200
+ cwd: z.string().optional(),
201
+ hook_event_name: z.string().optional(),
202
+ stop_hook_active: z.boolean().optional()
203
+ });
204
+ function parseHookPayload(harness, raw) {
205
+ let parsedJson;
206
+ try {
207
+ parsedJson = JSON.parse(raw);
208
+ } catch {
209
+ return {
210
+ ok: false,
211
+ reason: "stdin is not valid JSON"
212
+ };
213
+ }
214
+ if (harness === "claude-code") {
215
+ const parsed = ClaudeCodeStopInputSchema.safeParse(parsedJson);
216
+ if (!parsed.success) return {
217
+ ok: false,
218
+ reason: `invalid hook payload: ${parsed.error.message}`
219
+ };
220
+ return {
221
+ ok: true,
222
+ value: {
223
+ sessionId: parsed.data.session_id,
224
+ transcriptPath: parsed.data.transcript_path,
225
+ stopHookActive: parsed.data.stop_hook_active === true
226
+ }
227
+ };
228
+ }
229
+ return {
230
+ ok: false,
231
+ reason: `unsupported harness: ${harness}`
232
+ };
233
+ }
234
+ async function readStdin() {
235
+ if (process.stdin.isTTY) return "";
236
+ const chunks = [];
237
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
238
+ return Buffer.concat(chunks).toString("utf8");
239
+ }
240
+ async function extractCommand(opts) {
241
+ const harnessResult = ExtractionHarnessSchema.safeParse(opts.harness ?? "claude-code");
242
+ if (!harnessResult.success) {
243
+ process.stderr.write(`membank extract: unsupported harness "${opts.harness}". Supported: ${ExtractionHarnessSchema.options.join(", ")}\n`);
244
+ return;
245
+ }
246
+ const harness = harnessResult.data;
247
+ let sessionId = opts.sessionId;
248
+ let transcriptPath = opts.transcript;
249
+ let stopHookActive = false;
250
+ if (sessionId === void 0 || transcriptPath === void 0) {
251
+ const raw = await readStdin();
252
+ if (raw.trim().length > 0) {
253
+ const parsed = parseHookPayload(harness, raw);
254
+ if (!parsed.ok) {
255
+ process.stderr.write(`membank extract: ${parsed.reason}; skipping.\n`);
256
+ return;
257
+ }
258
+ sessionId = sessionId ?? parsed.value.sessionId;
259
+ transcriptPath = transcriptPath ?? parsed.value.transcriptPath;
260
+ stopHookActive = parsed.value.stopHookActive;
261
+ }
262
+ }
263
+ if (sessionId === void 0 || transcriptPath === void 0) {
264
+ process.stderr.write("membank extract: missing session_id or transcript_path (provide via stdin or --session/--transcript).\n");
265
+ return;
266
+ }
267
+ if (stopHookActive) {
268
+ process.stderr.write("membank extract: stop_hook_active=true; skipping to avoid recursion.\n");
269
+ return;
270
+ }
271
+ try {
272
+ const result = await runExtraction({
273
+ sessionId,
274
+ transcriptPath
275
+ });
276
+ if (result.status === "skipped") process.stderr.write(`membank extract: skipped (${result.reason})\n`);
277
+ else if (result.status === "failed") process.stderr.write(`membank extract: failed: ${result.error}\n`);
278
+ } catch (err) {
279
+ const msg = err instanceof Error ? err.message : String(err);
280
+ process.stderr.write(`membank extract: ${msg}\n`);
281
+ }
282
+ }
283
+ //#endregion
198
284
  //#region src/commands/import.ts
199
285
  async function importCommand(filePath, db, formatter, prompt) {
200
286
  let raw;
@@ -220,18 +306,23 @@ async function importCommand(filePath, db, formatter, prompt) {
220
306
  if (formatter.isJson) process.stdout.write(`${JSON.stringify({ found: count })}\n`);
221
307
  else process.stdout.write(`Found ${count} memories to import.\n`);
222
308
  if (!await prompt.confirm("Import?")) return;
223
- const insertMemory = db.db.prepare(`INSERT OR REPLACE INTO memories (id, content, type, tags, source, access_count, pinned, created_at, updated_at)
224
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
225
- const insertEmbedding = db.db.prepare(`INSERT OR REPLACE INTO embeddings (rowid, embedding) SELECT m.rowid, ? FROM memories m WHERE m.id = ?`);
226
- db.db.transaction(() => {
227
- for (const rec of parseResult.data.memories) {
228
- insertMemory.run(rec.id, rec.content, rec.type, JSON.stringify(rec.tags ?? []), rec.sourceHarness ?? null, rec.accessCount ?? 0, rec.pinned ? 1 : 0, rec.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), rec.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString());
229
- if (rec.embedding !== null && rec.embedding !== void 0) {
230
- const buf = Buffer.from(rec.embedding, "base64");
231
- insertEmbedding.run(buf, rec.id);
232
- }
233
- }
234
- })();
309
+ const records = parseResult.data.memories.map((rec) => ({
310
+ id: rec.id,
311
+ content: rec.content,
312
+ type: rec.type,
313
+ tags: rec.tags,
314
+ sourceHarness: rec.sourceHarness ?? null,
315
+ accessCount: rec.accessCount ?? 0,
316
+ pinned: rec.pinned ?? false,
317
+ createdAt: rec.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
318
+ updatedAt: rec.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
319
+ embedding: (() => {
320
+ if (rec.embedding == null) return null;
321
+ const buf = Buffer.from(rec.embedding, "base64");
322
+ return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
323
+ })()
324
+ }));
325
+ createMemoryRepository(db, createProjectRepository(db)).importAll(records);
235
326
  if (formatter.isJson) process.stdout.write(`${JSON.stringify({ imported: count })}\n`);
236
327
  else process.stdout.write(`Imported ${count} memories.\n`);
237
328
  }
@@ -277,8 +368,8 @@ async function buildText() {
277
368
  const resolved = await resolveProject();
278
369
  const db = DatabaseManager.open();
279
370
  try {
280
- const builder = new SessionContextBuilder(db);
281
- const synthRepo = new SynthesisRepository(db);
371
+ const builder = new SessionContextBuilder(createMemoryRepository(db, createProjectRepository(db)));
372
+ const synthRepo = createSynthesisRepository(db);
282
373
  const globalRow = synthRepo.getSynthesis("global");
283
374
  const projectRow = synthRepo.getSynthesis(resolved.hash);
284
375
  const synthesis = pickBestSynthesis(globalRow?.inFlightSince === null ? globalRow.content : void 0, projectRow?.inFlightSince === null ? projectRow.content : void 0);
@@ -288,10 +379,6 @@ async function buildText() {
288
379
  }
289
380
  }
290
381
  async function handleEvent(harness, eventName) {
291
- if (harness === "claude-code" && eventName === "Stop") {
292
- process.stdout.write("{}");
293
- return;
294
- }
295
382
  const text = await buildText().catch((err) => {
296
383
  const msg = err instanceof Error ? err.message : String(err);
297
384
  process.stderr.write(`membank inject: ${msg}\n`);
@@ -311,10 +398,6 @@ async function injectCommand(opts) {
311
398
  await handleEvent(harness, "UserPromptSubmit");
312
399
  return;
313
400
  }
314
- if (opts.event === "session-stop" || opts.event === "stop") {
315
- await handleEvent(harness, "Stop");
316
- return;
317
- }
318
401
  process.exit(0);
319
402
  }
320
403
  //#endregion
@@ -322,7 +405,7 @@ async function injectCommand(opts) {
322
405
  async function listCommand(options, formatter) {
323
406
  const db = DatabaseManager.open();
324
407
  try {
325
- const memories = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db)).list({
408
+ const memories = createMemoryRepository(db, createProjectRepository(db)).list({
326
409
  type: options.type !== void 0 ? MemoryTypeSchema$1.parse(options.type) : void 0,
327
410
  pinned: options.pinned
328
411
  });
@@ -353,7 +436,7 @@ async function migrateCommand(mode, name, formatter) {
353
436
  const db = DatabaseManager.open();
354
437
  try {
355
438
  if (name === "scope-to-projects") {
356
- const result = await runScopeToProjectsMigration(new ProjectRepository(db));
439
+ const result = await runScopeToProjectsMigration(createProjectRepository(db));
357
440
  if (result === null) {
358
441
  formatter.error("No project found for current directory.");
359
442
  return;
@@ -377,7 +460,7 @@ function pinCommand(id, db) {
377
460
  const ownDb = db === void 0;
378
461
  const resolvedDb = db ?? DatabaseManager.open();
379
462
  try {
380
- new MemoryRepository(resolvedDb, new EmbeddingService(), new ProjectRepository(resolvedDb)).setPin(id, true);
463
+ createMemoryRepository(resolvedDb, createProjectRepository(resolvedDb)).setPin(id, true);
381
464
  process.stdout.write(`${chalk.green("✓")} Pinned: ${chalk.dim(id)}\n`);
382
465
  } finally {
383
466
  if (ownDb) resolvedDb.close();
@@ -388,8 +471,7 @@ function pinCommand(id, db) {
388
471
  async function queryCommand(queryText, options, formatter) {
389
472
  const db = DatabaseManager.open();
390
473
  try {
391
- const embedding = new EmbeddingService();
392
- const engine = new QueryEngine(db, embedding, new MemoryRepository(db, embedding, new ProjectRepository(db)));
474
+ const engine = new QueryEngine(db, new EmbeddingService(), createMemoryRepository(db, createProjectRepository(db)));
393
475
  const limit = options.limit !== void 0 ? LimitSchema.parse(options.limit) : 10;
394
476
  const spinner = formatter.isJson ? null : ora("Searching memories…").start();
395
477
  const results = await engine.query({
@@ -409,7 +491,7 @@ async function queryCommand(queryText, options, formatter) {
409
491
  async function reviewCommand(opts, formatter) {
410
492
  const db = DatabaseManager.open();
411
493
  try {
412
- const repo = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db));
494
+ const repo = createMemoryRepository(db, createProjectRepository(db));
413
495
  if (opts.resolve !== void 0) {
414
496
  repo.resolveReviewEvents(opts.resolve);
415
497
  if (!formatter.isJson) process.stdout.write(`Resolved review events for memory ${opts.resolve}\n`);
@@ -427,7 +509,7 @@ async function reviewCommand(opts, formatter) {
427
509
  async function statsCommand(formatter) {
428
510
  const db = DatabaseManager.open();
429
511
  try {
430
- const stats = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db)).stats();
512
+ const stats = createMemoryRepository(db, createProjectRepository(db)).stats();
431
513
  formatter.outputStats(stats);
432
514
  } finally {
433
515
  db.close();
@@ -435,9 +517,6 @@ async function statsCommand(formatter) {
435
517
  }
436
518
  //#endregion
437
519
  //#region src/commands/synthesize.ts
438
- function hasSynthesesTable(db) {
439
- return db.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='syntheses'").get() !== void 0;
440
- }
441
520
  async function synthesizeRunCommand(opts, formatter) {
442
521
  const scope = opts.scope ?? "global";
443
522
  if (!formatter.isJson) process.stdout.write(`Running synthesis for scope: ${scope}\n`);
@@ -451,30 +530,25 @@ async function synthesizeRunCommand(opts, formatter) {
451
530
  function synthesizeShowCommand(opts, formatter) {
452
531
  const db = DatabaseManager.open();
453
532
  try {
454
- if (!hasSynthesesTable(db)) {
455
- if (formatter.isJson) process.stdout.write(`${JSON.stringify(null)}\n`);
456
- else process.stdout.write("No synthesis data available.\n");
457
- return;
458
- }
459
533
  const scope = opts.scope ?? "global";
460
534
  let resolvedScope = scope;
461
535
  if (scope !== "global" && !/^[0-9a-f]{16}$/.test(scope)) {
462
- const project = new ProjectRepository(db).getByName(scope);
536
+ const project = createProjectRepository(db).getByName(scope);
463
537
  if (project !== void 0) resolvedScope = project.scopeHash;
464
538
  }
465
- const row = db.db.prepare("SELECT * FROM syntheses WHERE scope = ? ORDER BY synthesized_at DESC LIMIT 1").get(resolvedScope);
466
- if (row === void 0) {
539
+ const synthesis = createSynthesisRepository(db).getSynthesis(resolvedScope);
540
+ if (synthesis === void 0) {
467
541
  if (formatter.isJson) process.stdout.write(`${JSON.stringify(null)}\n`);
468
542
  else process.stdout.write(`No synthesis found for scope: ${scope}\n`);
469
543
  return;
470
544
  }
471
- if (formatter.isJson) process.stdout.write(`${JSON.stringify(row)}\n`);
545
+ if (formatter.isJson) process.stdout.write(`${JSON.stringify(synthesis)}\n`);
472
546
  else {
473
- process.stdout.write(`\nScope: ${row.scope}\n`);
474
- if (row.synthesized_at !== null) process.stdout.write(`Synthesized: ${new Date(row.synthesized_at).toLocaleString()}\n`);
475
- if (row.expires_at !== null) process.stdout.write(`Expires: ${new Date(row.expires_at).toLocaleString()}\n`);
476
- if (row.in_flight_since !== null) process.stdout.write(`In-flight since: ${new Date(row.in_flight_since).toLocaleString()}\n`);
477
- process.stdout.write(`\n${row.content}\n\n`);
547
+ process.stdout.write(`\nScope: ${synthesis.scope}\n`);
548
+ process.stdout.write(`Synthesized: ${new Date(synthesis.synthesizedAt).toLocaleString()}\n`);
549
+ process.stdout.write(`Expires: ${new Date(synthesis.expiresAt).toLocaleString()}\n`);
550
+ if (synthesis.inFlightSince !== null) process.stdout.write(`In-flight since: ${new Date(synthesis.inFlightSince).toLocaleString()}\n`);
551
+ process.stdout.write(`\n${synthesis.content}\n\n`);
478
552
  }
479
553
  } finally {
480
554
  db.close();
@@ -483,29 +557,22 @@ function synthesizeShowCommand(opts, formatter) {
483
557
  function synthesizeStatusCommand(formatter) {
484
558
  const db = DatabaseManager.open();
485
559
  try {
486
- if (!hasSynthesesTable(db)) {
487
- if (formatter.isJson) process.stdout.write(`${JSON.stringify([])}\n`);
488
- else process.stdout.write("No synthesis data available.\n");
489
- return;
490
- }
491
- const rows = db.db.prepare(`SELECT s.*, p.name AS project_name
492
- FROM syntheses s
493
- LEFT JOIN projects p ON p.scope_hash = s.scope
494
- ORDER BY COALESCE(p.name, s.scope)`).all();
560
+ const syntheses = createSynthesisRepository(db).listAll();
561
+ const projectRepo = createProjectRepository(db);
495
562
  if (formatter.isJson) {
496
- process.stdout.write(`${JSON.stringify(rows)}\n`);
563
+ process.stdout.write(`${JSON.stringify(syntheses)}\n`);
497
564
  return;
498
565
  }
499
- if (rows.length === 0) {
566
+ if (syntheses.length === 0) {
500
567
  process.stdout.write("No syntheses found.\n");
501
568
  return;
502
569
  }
503
570
  process.stdout.write("\n");
504
- for (const row of rows) {
505
- const displayScope = row.project_name ?? row.scope;
506
- const inFlight = row.in_flight_since !== null ? " [in-flight]" : "";
507
- const synthesized = row.synthesized_at !== null ? new Date(row.synthesized_at).toLocaleString() : "(never)";
508
- const expires = row.expires_at !== null ? new Date(row.expires_at).toLocaleString() : "(none)";
571
+ for (const s of syntheses) {
572
+ const displayScope = (s.scope !== "global" ? projectRepo.getByHash(s.scope) : void 0)?.name ?? s.scope;
573
+ const inFlight = s.inFlightSince !== null ? " [in-flight]" : "";
574
+ const synthesized = new Date(s.synthesizedAt).toLocaleString();
575
+ const expires = new Date(s.expiresAt).toLocaleString();
509
576
  process.stdout.write(` ${displayScope}${inFlight}\n`);
510
577
  process.stdout.write(` synthesized_at: ${synthesized}\n`);
511
578
  process.stdout.write(` expires_at: ${expires}\n`);
@@ -521,7 +588,7 @@ function unpinCommand(id, db) {
521
588
  const ownDb = db === void 0;
522
589
  const resolvedDb = db ?? DatabaseManager.open();
523
590
  try {
524
- new MemoryRepository(resolvedDb, new EmbeddingService(), new ProjectRepository(resolvedDb)).setPin(id, false);
591
+ createMemoryRepository(resolvedDb, createProjectRepository(resolvedDb)).setPin(id, false);
525
592
  process.stdout.write(`${chalk.green("✓")} Unpinned: ${chalk.dim(id)}\n`);
526
593
  } finally {
527
594
  if (ownDb) resolvedDb.close();
@@ -777,18 +844,20 @@ function hasKey(container, key) {
777
844
  function assertCliFound(result, cli, command) {
778
845
  if (result.exitCode === 127) throw new CommandError(`${cli} CLI not found — install ${cli} first`, command);
779
846
  }
847
+ function jsonContainsStalePattern(value) {
848
+ return JSON.stringify(value).includes("@membank/cli");
849
+ }
780
850
  const MEMBANK_NPX_ARGS = [
781
851
  "npx",
782
852
  "-y",
783
- "@membank/cli",
784
- "--mcp"
853
+ "@membank/mcp"
785
854
  ];
786
855
  const writers$1 = {
787
856
  "claude-code": {
788
857
  preview(resolver) {
789
858
  return {
790
859
  configPath: join(resolver.home(), ".claude.json"),
791
- cliCommand: "claude mcp add --scope user membank -- npx -y @membank/cli --mcp"
860
+ cliCommand: "claude mcp add --scope user membank -- npx -y @membank/mcp"
792
861
  };
793
862
  },
794
863
  async write(resolver, run, { overwrite = false } = {}) {
@@ -821,6 +890,10 @@ const writers$1 = {
821
890
  assertCliFound(add, "claude", addCmd);
822
891
  if (add.exitCode !== 0) throw new CommandError(`claude mcp add failed: ${add.stderr || add.stdout}`, addCmd);
823
892
  return { status: "written" };
893
+ },
894
+ async isStale(resolver) {
895
+ const cfg = readJson(join(resolver.home(), ".claude.json"));
896
+ return hasKey(cfg.mcpServers, "membank") && jsonContainsStalePattern(cfg.mcpServers);
824
897
  }
825
898
  },
826
899
  copilot: {
@@ -840,22 +913,22 @@ const writers$1 = {
840
913
  ...MaybeJsonObjectSchema.parse(cfg.mcpServers),
841
914
  membank: {
842
915
  command: "npx",
843
- args: [
844
- "-y",
845
- "@membank/cli",
846
- "--mcp"
847
- ]
916
+ args: ["-y", "@membank/mcp"]
848
917
  }
849
918
  }
850
919
  });
851
920
  return { status: "written" };
921
+ },
922
+ async isStale(resolver) {
923
+ const cfg = readJson(join(resolver.home(), ".copilot", "mcp-config.json"));
924
+ return hasKey(cfg.mcpServers, "membank") && jsonContainsStalePattern(cfg.mcpServers);
852
925
  }
853
926
  },
854
927
  codex: {
855
928
  preview(_resolver) {
856
929
  return {
857
930
  configPath: null,
858
- cliCommand: "codex mcp add membank -- npx -y @membank/cli --mcp"
931
+ cliCommand: "codex mcp add membank -- npx -y @membank/mcp"
859
932
  };
860
933
  },
861
934
  async write(_resolver, run, { overwrite = false } = {}) {
@@ -887,6 +960,10 @@ const writers$1 = {
887
960
  assertCliFound(add, "codex", addCmd);
888
961
  if (add.exitCode !== 0) throw new CommandError(`codex mcp add failed: ${add.stderr || add.stdout}`, addCmd);
889
962
  return { status: "written" };
963
+ },
964
+ async isStale(_resolver, run) {
965
+ const list = await run("codex", ["mcp", "list"]);
966
+ return list.exitCode === 0 && list.stdout.includes("membank") && list.stdout.includes("@membank/cli");
890
967
  }
891
968
  },
892
969
  opencode: {
@@ -909,13 +986,16 @@ const writers$1 = {
909
986
  command: [
910
987
  "npx",
911
988
  "-y",
912
- "@membank/cli",
913
- "--mcp"
989
+ "@membank/mcp"
914
990
  ]
915
991
  }
916
992
  }
917
993
  });
918
994
  return { status: "written" };
995
+ },
996
+ async isStale(resolver) {
997
+ const cfg = readJson(join(resolver.home(), ".config", "opencode", "opencode.json"));
998
+ return hasKey(cfg.mcp, "membank") && jsonContainsStalePattern(cfg.mcp);
919
999
  }
920
1000
  }
921
1001
  };
@@ -937,6 +1017,15 @@ var HarnessConfigWriter = class {
937
1017
  if (!writer) throw new Error(`Unknown harness: ${harness}`);
938
1018
  return writer.write(this.#resolver, this.#run, { overwrite });
939
1019
  }
1020
+ async isStale(harness) {
1021
+ const writer = writers$1[harness];
1022
+ if (!writer) return false;
1023
+ try {
1024
+ return await writer.isStale(this.#resolver, this.#run);
1025
+ } catch {
1026
+ return false;
1027
+ }
1028
+ }
940
1029
  };
941
1030
  //#endregion
942
1031
  //#region src/setup/injection-hook-writer.ts
@@ -1009,7 +1098,7 @@ const writers = {
1009
1098
  },
1010
1099
  {
1011
1100
  event: "Stop",
1012
- command: "npx -y @membank/cli inject --harness claude-code --event session-stop",
1101
+ command: "npx -y @membank/cli extract --harness claude-code",
1013
1102
  existingCommand: extractInjectCommand(stopInner) || null
1014
1103
  }
1015
1104
  ]
@@ -1021,6 +1110,7 @@ const writers = {
1021
1110
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1022
1111
  const newHooks = { ...hooks };
1023
1112
  pruneNestedEvent(newHooks, "PostToolUseFailure");
1113
+ pruneNestedEvent(newHooks, "Stop");
1024
1114
  if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
1025
1115
  matcher: "",
1026
1116
  hooks: [{
@@ -1039,7 +1129,9 @@ const writers = {
1039
1129
  matcher: "",
1040
1130
  hooks: [{
1041
1131
  type: "command",
1042
- command: "npx -y @membank/cli inject --harness claude-code --event session-stop"
1132
+ command: "npx -y @membank/cli extract --harness claude-code",
1133
+ async: true,
1134
+ timeout: 600
1043
1135
  }]
1044
1136
  }];
1045
1137
  writeJsonAtomic(cfgPath, {
@@ -1056,27 +1148,18 @@ const writers = {
1056
1148
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1057
1149
  const sessionStart = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : [];
1058
1150
  const userPromptSubmitted = Array.isArray(hooks.userPromptSubmitted) ? hooks.userPromptSubmitted : [];
1059
- const sessionEnd = Array.isArray(hooks.sessionEnd) ? hooks.sessionEnd : [];
1060
1151
  return {
1061
1152
  status: "ready",
1062
1153
  configPath: cfgPath,
1063
- hooks: [
1064
- {
1065
- event: "sessionStart",
1066
- command: "npx -y @membank/cli inject --harness copilot-cli",
1067
- existingCommand: extractInjectCommand(sessionStart) || null
1068
- },
1069
- {
1070
- event: "userPromptSubmitted",
1071
- command: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
1072
- existingCommand: extractInjectCommand(userPromptSubmitted) || null
1073
- },
1074
- {
1075
- event: "sessionEnd",
1076
- command: "npx -y @membank/cli inject --harness copilot-cli --event session-stop",
1077
- existingCommand: extractInjectCommand(sessionEnd) || null
1078
- }
1079
- ]
1154
+ hooks: [{
1155
+ event: "sessionStart",
1156
+ command: "npx -y @membank/cli inject --harness copilot-cli",
1157
+ existingCommand: extractInjectCommand(sessionStart) || null
1158
+ }, {
1159
+ event: "userPromptSubmitted",
1160
+ command: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
1161
+ existingCommand: extractInjectCommand(userPromptSubmitted) || null
1162
+ }]
1080
1163
  };
1081
1164
  },
1082
1165
  write(resolver, events) {
@@ -1085,6 +1168,7 @@ const writers = {
1085
1168
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1086
1169
  const newHooks = { ...hooks };
1087
1170
  pruneFlatEvent(newHooks, "postToolUseFailure");
1171
+ pruneFlatEvent(newHooks, "sessionEnd");
1088
1172
  if (events.includes("sessionStart")) newHooks.sessionStart = [...filterOutMembankFlat(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []), {
1089
1173
  type: "command",
1090
1174
  bash: "npx -y @membank/cli inject --harness copilot-cli",
@@ -1095,11 +1179,6 @@ const writers = {
1095
1179
  bash: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
1096
1180
  timeoutSec: 30
1097
1181
  }];
1098
- if (events.includes("sessionEnd")) newHooks.sessionEnd = [...filterOutMembankFlat(Array.isArray(hooks.sessionEnd) ? hooks.sessionEnd : []), {
1099
- type: "command",
1100
- bash: "npx -y @membank/cli inject --harness copilot-cli --event session-stop",
1101
- timeoutSec: 30
1102
- }];
1103
1182
  writeJsonAtomic(cfgPath, {
1104
1183
  version: OptionalNumberSchema.parse(cfg.version) ?? 1,
1105
1184
  ...cfg,
@@ -1115,27 +1194,18 @@ const writers = {
1115
1194
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1116
1195
  const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
1117
1196
  const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
1118
- const stopInner = (Array.isArray(hooks.Stop) ? hooks.Stop : []).flatMap(getHooksArray);
1119
1197
  return {
1120
1198
  status: "ready",
1121
1199
  configPath: cfgPath,
1122
- hooks: [
1123
- {
1124
- event: "SessionStart",
1125
- command: "npx -y @membank/cli inject --harness codex",
1126
- existingCommand: extractInjectCommand(sessionStartInner) || null
1127
- },
1128
- {
1129
- event: "UserPromptSubmit",
1130
- command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
1131
- existingCommand: extractInjectCommand(userPromptSubmitInner) || null
1132
- },
1133
- {
1134
- event: "Stop",
1135
- command: "npx -y @membank/cli inject --harness codex --event session-stop",
1136
- existingCommand: extractInjectCommand(stopInner) || null
1137
- }
1138
- ]
1200
+ hooks: [{
1201
+ event: "SessionStart",
1202
+ command: "npx -y @membank/cli inject --harness codex",
1203
+ existingCommand: extractInjectCommand(sessionStartInner) || null
1204
+ }, {
1205
+ event: "UserPromptSubmit",
1206
+ command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
1207
+ existingCommand: extractInjectCommand(userPromptSubmitInner) || null
1208
+ }]
1139
1209
  };
1140
1210
  },
1141
1211
  write(resolver, events) {
@@ -1144,6 +1214,7 @@ const writers = {
1144
1214
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1145
1215
  const newHooks = { ...hooks };
1146
1216
  pruneNestedEvent(newHooks, "PostToolUse");
1217
+ pruneNestedEvent(newHooks, "Stop");
1147
1218
  if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
1148
1219
  matcher: "",
1149
1220
  hooks: [{
@@ -1160,14 +1231,6 @@ const writers = {
1160
1231
  timeout: 30
1161
1232
  }]
1162
1233
  }];
1163
- if (events.includes("Stop")) newHooks.Stop = [...filterOutMembank(Array.isArray(hooks.Stop) ? hooks.Stop : []), {
1164
- matcher: "",
1165
- hooks: [{
1166
- type: "command",
1167
- command: "npx -y @membank/cli inject --harness codex --event session-stop",
1168
- timeout: 30
1169
- }]
1170
- }];
1171
1234
  writeJsonAtomic(cfgPath, {
1172
1235
  ...cfg,
1173
1236
  hooks: newHooks
@@ -1229,76 +1292,6 @@ var InjectionHookWriter = class {
1229
1292
  }
1230
1293
  };
1231
1294
  //#endregion
1232
- //#region src/setup/model-downloader.ts
1233
- const MODEL_NAME = "Xenova/bge-small-en-v1.5";
1234
- var ModelDownloadError = class extends Error {
1235
- constructor(message, options) {
1236
- super(message, options);
1237
- this.name = "ModelDownloadError";
1238
- }
1239
- };
1240
- function defaultModelPath() {
1241
- return join(homedir(), ".membank", "models");
1242
- }
1243
- function isCached(modelPath) {
1244
- if (!existsSync(modelPath)) return false;
1245
- try {
1246
- return readdirSync(modelPath).length > 0;
1247
- } catch {
1248
- return false;
1249
- }
1250
- }
1251
- var ModelDownloader = class extends EventEmitter {
1252
- modelPath;
1253
- constructor(modelPath) {
1254
- super();
1255
- this.modelPath = modelPath ?? defaultModelPath();
1256
- }
1257
- isAlreadyCached() {
1258
- return isCached(this.modelPath);
1259
- }
1260
- get cachePath() {
1261
- return this.modelPath;
1262
- }
1263
- async download() {
1264
- if (isCached(this.modelPath)) return { skipped: true };
1265
- const startTime = Date.now();
1266
- let lastDownloadedBytes = 0;
1267
- let lastTimestamp = startTime;
1268
- try {
1269
- await pipeline("feature-extraction", MODEL_NAME, {
1270
- cache_dir: this.modelPath,
1271
- progress_callback: (event) => {
1272
- if (event.status !== "progress" || event.total == null || event.loaded == null) return;
1273
- const totalBytes = event.total;
1274
- const downloadedBytes = event.loaded;
1275
- const percentage = totalBytes > 0 ? downloadedBytes / totalBytes * 100 : 0;
1276
- const now = Date.now();
1277
- const elapsedSinceLastMs = now - lastTimestamp;
1278
- const bytesSinceLast = downloadedBytes - lastDownloadedBytes;
1279
- let estimatedSecondsRemaining = 0;
1280
- if (elapsedSinceLastMs > 0 && bytesSinceLast > 0) {
1281
- const bytesPerMs = bytesSinceLast / elapsedSinceLastMs;
1282
- estimatedSecondsRemaining = (totalBytes - downloadedBytes) / bytesPerMs / 1e3;
1283
- }
1284
- lastDownloadedBytes = downloadedBytes;
1285
- lastTimestamp = now;
1286
- const progress = {
1287
- totalBytes,
1288
- downloadedBytes,
1289
- percentage,
1290
- estimatedSecondsRemaining
1291
- };
1292
- this.emit("progress", progress);
1293
- }
1294
- });
1295
- } catch (err) {
1296
- throw new ModelDownloadError("Failed to download model", { cause: err });
1297
- }
1298
- return { skipped: false };
1299
- }
1300
- };
1301
- //#endregion
1302
1295
  //#region src/setup/harness-detector.ts
1303
1296
  const defaultResolver = { homeDir: homedir };
1304
1297
  function harnessConfigs(resolver) {
@@ -1586,7 +1579,10 @@ var SetupOrchestrator = class {
1586
1579
  };
1587
1580
  //#endregion
1588
1581
  //#region src/index.ts
1589
- if (process.argv.includes("--mcp")) await startServer();
1582
+ if (process.argv.includes("--mcp")) {
1583
+ process.stderr.write("[membank] Deprecation: `membank --mcp` is deprecated. Use: npx @membank/mcp\n");
1584
+ await startServer();
1585
+ }
1590
1586
  const program = new Command();
1591
1587
  program.name("membank").description("LLM memory management system").option("--json", "emit machine-readable JSON only").option("-y, --yes", "skip all confirmation prompts").option("--mcp", "start the MCP stdio server (for harness integration)");
1592
1588
  program.command("query <queryText>").description("search memories by semantic similarity").option("--type <type>", "filter by memory type (correction|preference|decision|learning|fact)").option("--limit <n>", "maximum number of results", "10").option("--include-pinned", "include pinned memories in results (excluded by default)").action(async (queryText, cmdOptions) => {
@@ -1698,7 +1694,47 @@ program.command("inject").description("output session context for harness inject
1698
1694
  process.exit(2);
1699
1695
  }
1700
1696
  });
1701
- program.command("setup").description("detect installed harnesses and write MCP config for each").option("--yes", "skip all confirmation prompts").option("--dry-run", "print planned changes without writing any file").option("--harness <name>", "target only the named harness (skip detection)").action(async (cmdOptions) => {
1697
+ program.command("extract").description("(internal) run session-end memory extraction; reads the harness's Stop hook payload from stdin").option("--harness <name>", "harness whose stop-hook payload is on stdin (only claude-code is supported today)", "claude-code").option("--session <id>", "session id (otherwise read from stdin)").option("--transcript <path>", "transcript JSONL path (otherwise read from stdin)").action(async (cmdOptions) => {
1698
+ try {
1699
+ await extractCommand({
1700
+ harness: cmdOptions.harness,
1701
+ sessionId: cmdOptions.session,
1702
+ transcript: cmdOptions.transcript
1703
+ });
1704
+ } catch (err) {
1705
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
1706
+ }
1707
+ process.exit(0);
1708
+ });
1709
+ const setupCmd = program.command("setup").description("detect installed harnesses and write MCP config for each").option("--yes", "skip all confirmation prompts").option("--dry-run", "print planned changes without writing any file").option("--harness <name>", "target only the named harness (skip detection)");
1710
+ setupCmd.command("upgrade").description("upgrade harness configs from membank --mcp to standalone membank-mcp").action(async () => {
1711
+ const isJson = program.opts().json === true;
1712
+ const writer = new HarnessConfigWriter();
1713
+ const stale = [];
1714
+ for (const harness of SUPPORTED_HARNESSES) if (await writer.isStale(harness)) stale.push(harness);
1715
+ if (stale.length === 0) {
1716
+ if (isJson) process.stdout.write(`${JSON.stringify({ upgraded: [] })}\n`);
1717
+ else process.stdout.write("All harness configs are already up to date.\n");
1718
+ return;
1719
+ }
1720
+ const upgraded = [];
1721
+ const errors = [];
1722
+ for (const harness of stale) try {
1723
+ await writer.write(harness, { overwrite: true });
1724
+ upgraded.push(harness);
1725
+ if (!isJson) process.stdout.write(` ${chalk.green("✓")} ${harness}\n`);
1726
+ } catch (err) {
1727
+ const msg = err instanceof Error ? err.message : String(err);
1728
+ errors.push(`${harness}: ${msg}`);
1729
+ if (!isJson) process.stderr.write(` ${chalk.red("✗")} ${harness}: ${msg}\n`);
1730
+ }
1731
+ if (isJson) process.stdout.write(`${JSON.stringify({
1732
+ upgraded,
1733
+ errors
1734
+ })}\n`);
1735
+ if (errors.length > 0) process.exit(1);
1736
+ });
1737
+ setupCmd.action(async (cmdOptions) => {
1702
1738
  const globalOpts = program.opts();
1703
1739
  const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
1704
1740
  const formatter = Formatter.create(globalOpts.json === true);
@@ -1792,13 +1828,9 @@ program.command("migrate <mode> [name]").description("list or run a named data m
1792
1828
  process.exit(2);
1793
1829
  }
1794
1830
  });
1795
- program.command("dashboard").description("open the memory management dashboard in the browser").option("--port <port>", "port to listen on (default: 3847, fallback to random)").action(async (cmdOptions) => {
1796
- try {
1797
- await dashboardCommand(cmdOptions);
1798
- } catch (err) {
1799
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
1800
- process.exit(2);
1801
- }
1831
+ program.command("dashboard").description("(deprecated) open the memory management dashboard").allowUnknownOption().action(() => {
1832
+ process.stderr.write("The dashboard is now a standalone package.\nRun: npx @membank/dashboard\n");
1833
+ process.exit(1);
1802
1834
  });
1803
1835
  const configCmd = program.command("config").description("manage membank configuration");
1804
1836
  configCmd.command("get <key>").description("print a config value as JSON").action((key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,19 +11,18 @@
11
11
  "membank": "./dist/index.mjs"
12
12
  },
13
13
  "files": [
14
- "dist"
14
+ "dist",
15
+ "!dist/**/*.map"
15
16
  ],
16
17
  "dependencies": {
17
18
  "@clack/prompts": "^1.3.0",
18
- "@huggingface/transformers": "^4.2.0",
19
19
  "chalk": "^5.6.2",
20
20
  "cli-table3": "^0.6.5",
21
21
  "commander": "^14.0.3",
22
22
  "ora": "^9.4.0",
23
23
  "zod": "^4.4.3",
24
- "@membank/core": "0.9.4",
25
- "@membank/dashboard": "0.5.5",
26
- "@membank/mcp": "0.12.2"
24
+ "@membank/core": "0.11.0",
25
+ "@membank/mcp": "0.14.0"
27
26
  },
28
27
  "devDependencies": {
29
28
  "@types/node": "^25.6.0",