@stackmemoryai/stackmemory 0.5.64 → 0.5.66

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.
@@ -13,7 +13,9 @@ import {
13
13
  AddAnchorSchema,
14
14
  CreateTaskSchema
15
15
  } from "./schemas.js";
16
- import { readFileSync, existsSync, mkdirSync } from "fs";
16
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
17
+ import { compactPlan } from "../../orchestrators/multimodal/utils.js";
18
+ import { filterPending } from "./pending-utils.js";
17
19
  import { join, dirname } from "path";
18
20
  import { execSync } from "child_process";
19
21
  import { FrameManager } from "../../core/context/index.js";
@@ -55,6 +57,7 @@ class LocalStackMemoryMCP {
55
57
  contextRetrieval;
56
58
  discoveryHandlers;
57
59
  diffMemHandlers;
60
+ pendingPlans = /* @__PURE__ */ new Map();
58
61
  constructor() {
59
62
  this.projectRoot = this.findProjectRoot();
60
63
  this.projectId = this.getProjectId();
@@ -97,6 +100,7 @@ class LocalStackMemoryMCP {
97
100
  this.diffMemHandlers = new DiffMemHandlers();
98
101
  this.setupHandlers();
99
102
  this.loadInitialContext();
103
+ this.loadPendingPlans();
100
104
  this.browserMCP.initialize(this.server).catch((error) => {
101
105
  logger.error("Failed to initialize Browser MCP", error);
102
106
  });
@@ -262,6 +266,199 @@ ${summary}...`, 0.8);
262
266
  }
263
267
  }
264
268
  },
269
+ {
270
+ name: "plan_and_code",
271
+ description: "Generate a plan (Claude), attempt implementation (Codex/Claude), and return JSON result. Quiet by default.",
272
+ inputSchema: {
273
+ type: "object",
274
+ properties: {
275
+ task: { type: "string", description: "Task description" },
276
+ implementer: {
277
+ type: "string",
278
+ enum: ["codex", "claude"],
279
+ default: "codex",
280
+ description: "Which agent implements code"
281
+ },
282
+ maxIters: {
283
+ type: "number",
284
+ default: 2,
285
+ description: "Retry loop iterations"
286
+ },
287
+ execute: {
288
+ type: "boolean",
289
+ default: false,
290
+ description: "Actually call implementer (otherwise dry-run)"
291
+ },
292
+ record: {
293
+ type: "boolean",
294
+ default: false,
295
+ description: "Record plan & critique into StackMemory context"
296
+ },
297
+ recordFrame: {
298
+ type: "boolean",
299
+ default: false,
300
+ description: "Record as real frame with anchors"
301
+ }
302
+ },
303
+ required: ["task"]
304
+ }
305
+ },
306
+ {
307
+ name: "plan_gate",
308
+ description: "Phase 1: Generate a plan and return an approvalId for later execution",
309
+ inputSchema: {
310
+ type: "object",
311
+ properties: {
312
+ task: { type: "string", description: "Task description" },
313
+ plannerModel: {
314
+ type: "string",
315
+ description: "Claude model (optional)"
316
+ }
317
+ },
318
+ required: ["task"]
319
+ }
320
+ },
321
+ {
322
+ name: "approve_plan",
323
+ description: "Phase 2: Execute a previously generated plan by approvalId (runs implement + critique)",
324
+ inputSchema: {
325
+ type: "object",
326
+ properties: {
327
+ approvalId: {
328
+ type: "string",
329
+ description: "Id from plan_gate"
330
+ },
331
+ implementer: {
332
+ type: "string",
333
+ enum: ["codex", "claude"],
334
+ default: "codex",
335
+ description: "Which agent implements code"
336
+ },
337
+ maxIters: { type: "number", default: 2 },
338
+ recordFrame: { type: "boolean", default: true },
339
+ execute: { type: "boolean", default: true }
340
+ },
341
+ required: ["approvalId"]
342
+ }
343
+ },
344
+ {
345
+ name: "pending_list",
346
+ description: "List pending approval-gated plans (supports filters)",
347
+ inputSchema: {
348
+ type: "object",
349
+ properties: {
350
+ taskContains: {
351
+ type: "string",
352
+ description: "Filter tasks containing this substring"
353
+ },
354
+ olderThanMs: {
355
+ type: "number",
356
+ description: "Only items older than this age (ms)"
357
+ },
358
+ newerThanMs: {
359
+ type: "number",
360
+ description: "Only items newer than this age (ms)"
361
+ },
362
+ sort: {
363
+ type: "string",
364
+ enum: ["asc", "desc"],
365
+ description: "Sort by createdAt"
366
+ },
367
+ limit: { type: "number", description: "Max items to return" }
368
+ }
369
+ }
370
+ },
371
+ {
372
+ name: "pending_clear",
373
+ description: "Clear pending approval-gated plans (by id, all, or olderThanMs)",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {
377
+ approvalId: {
378
+ type: "string",
379
+ description: "Clear a single approval by id"
380
+ },
381
+ all: {
382
+ type: "boolean",
383
+ description: "Clear all pending approvals",
384
+ default: false
385
+ },
386
+ olderThanMs: {
387
+ type: "number",
388
+ description: "Clear approvals older than this age (ms)"
389
+ }
390
+ }
391
+ }
392
+ },
393
+ {
394
+ name: "pending_show",
395
+ description: "Show a pending plan by approvalId",
396
+ inputSchema: {
397
+ type: "object",
398
+ properties: {
399
+ approvalId: {
400
+ type: "string",
401
+ description: "Approval id from plan_gate"
402
+ }
403
+ },
404
+ required: ["approvalId"]
405
+ }
406
+ },
407
+ {
408
+ name: "plan_only",
409
+ description: "Generate an implementation plan (Claude) and return JSON only",
410
+ inputSchema: {
411
+ type: "object",
412
+ properties: {
413
+ task: { type: "string", description: "Task description" },
414
+ plannerModel: {
415
+ type: "string",
416
+ description: "Claude model for planning (optional)"
417
+ }
418
+ },
419
+ required: ["task"]
420
+ }
421
+ },
422
+ {
423
+ name: "call_codex",
424
+ description: "Invoke Codex via codex-sm with a prompt and args; dry-run by default",
425
+ inputSchema: {
426
+ type: "object",
427
+ properties: {
428
+ prompt: { type: "string", description: "Prompt for Codex" },
429
+ args: {
430
+ type: "array",
431
+ items: { type: "string" },
432
+ description: "Additional CLI args for codex-sm"
433
+ },
434
+ execute: {
435
+ type: "boolean",
436
+ default: false,
437
+ description: "Actually run codex-sm (otherwise dry-run)"
438
+ }
439
+ },
440
+ required: ["prompt"]
441
+ }
442
+ },
443
+ {
444
+ name: "call_claude",
445
+ description: "Invoke Claude with a prompt (Anthropic SDK)",
446
+ inputSchema: {
447
+ type: "object",
448
+ properties: {
449
+ prompt: { type: "string", description: "Prompt for Claude" },
450
+ model: {
451
+ type: "string",
452
+ description: "Claude model (optional)"
453
+ },
454
+ system: {
455
+ type: "string",
456
+ description: "System prompt (optional)"
457
+ }
458
+ },
459
+ required: ["prompt"]
460
+ }
461
+ },
265
462
  {
266
463
  name: "add_decision",
267
464
  description: "Record a decision or important information",
@@ -940,6 +1137,30 @@ ${summary}...`, 0.8);
940
1137
  case "compress_old_traces":
941
1138
  result = await this.handleCompressOldTraces(args);
942
1139
  break;
1140
+ case "plan_only":
1141
+ result = await this.handlePlanOnly(args);
1142
+ break;
1143
+ case "call_codex":
1144
+ result = await this.handleCallCodex(args);
1145
+ break;
1146
+ case "call_claude":
1147
+ result = await this.handleCallClaude(args);
1148
+ break;
1149
+ case "plan_gate":
1150
+ result = await this.handlePlanGate(args);
1151
+ break;
1152
+ case "approve_plan":
1153
+ result = await this.handleApprovePlan(args);
1154
+ break;
1155
+ case "pending_list":
1156
+ result = await this.handlePendingList();
1157
+ break;
1158
+ case "pending_clear":
1159
+ result = await this.handlePendingClear(args);
1160
+ break;
1161
+ case "pending_show":
1162
+ result = await this.handlePendingShow(args);
1163
+ break;
943
1164
  case "smart_context":
944
1165
  result = await this.handleSmartContext(args);
945
1166
  break;
@@ -1008,6 +1229,349 @@ ${summary}...`, 0.8);
1008
1229
  }
1009
1230
  );
1010
1231
  }
1232
+ // Handle plan_and_code tool by invoking the mm harness
1233
+ async handlePlanAndCode(args) {
1234
+ const { runSpike } = await import("../../orchestrators/multimodal/harness.js");
1235
+ const plannerModel = process.env["STACKMEMORY_MM_PLANNER_MODEL"] || "claude-3-5-sonnet-latest";
1236
+ const reviewerModel = process.env["STACKMEMORY_MM_REVIEWER_MODEL"] || plannerModel;
1237
+ const implementer = args.implementer || process.env["STACKMEMORY_MM_IMPLEMENTER"] || "codex";
1238
+ const maxIters = Number(
1239
+ args.maxIters ?? process.env["STACKMEMORY_MM_MAX_ITERS"] ?? 2
1240
+ );
1241
+ const execute = Boolean(args.execute);
1242
+ const record = Boolean(args.record);
1243
+ const recordFrame = Boolean(args.recordFrame);
1244
+ const compact = Boolean(args.compact);
1245
+ const task = String(args.task || "Plan and implement change");
1246
+ const result = await runSpike(
1247
+ {
1248
+ task,
1249
+ repoPath: this.projectRoot
1250
+ },
1251
+ {
1252
+ plannerModel,
1253
+ reviewerModel,
1254
+ implementer: implementer === "claude" ? "claude" : "codex",
1255
+ maxIters: isFinite(maxIters) ? Math.max(1, maxIters) : 2,
1256
+ dryRun: !execute,
1257
+ auditDir: void 0,
1258
+ recordFrame
1259
+ }
1260
+ );
1261
+ if (record || recordFrame) {
1262
+ try {
1263
+ const planSummary = result.plan.summary || task;
1264
+ this.addContext("decision", `Plan: ${planSummary}`, 0.8);
1265
+ const approved = result.critique?.approved ? "approved" : "needs_changes";
1266
+ this.addContext("decision", `Critique: ${approved}`, 0.6);
1267
+ } catch {
1268
+ }
1269
+ }
1270
+ const payload = compact ? { ...result, plan: compactPlan(result.plan) } : result;
1271
+ return {
1272
+ content: [
1273
+ {
1274
+ type: "text",
1275
+ text: JSON.stringify({ ok: true, result: payload })
1276
+ }
1277
+ ],
1278
+ isError: false
1279
+ };
1280
+ }
1281
+ async handlePlanOnly(args) {
1282
+ const { runPlanOnly } = await import("../../orchestrators/multimodal/harness.js");
1283
+ const task = String(args.task || "Plan change");
1284
+ const plannerModel = args.plannerModel || process.env["STACKMEMORY_MM_PLANNER_MODEL"] || "claude-3-5-sonnet-latest";
1285
+ const plan = await runPlanOnly(
1286
+ { task, repoPath: this.projectRoot },
1287
+ { plannerModel }
1288
+ );
1289
+ return {
1290
+ content: [
1291
+ {
1292
+ type: "text",
1293
+ text: JSON.stringify({ ok: true, plan })
1294
+ }
1295
+ ],
1296
+ isError: false
1297
+ };
1298
+ }
1299
+ async handleCallCodex(args) {
1300
+ const { callCodexCLI } = await import("../../orchestrators/multimodal/providers.js");
1301
+ const prompt = String(args.prompt || "");
1302
+ const extraArgs = Array.isArray(args.args) ? args.args : [];
1303
+ const execute = Boolean(args.execute);
1304
+ const resp = callCodexCLI(prompt, extraArgs, !execute);
1305
+ return {
1306
+ content: [
1307
+ {
1308
+ type: "text",
1309
+ text: JSON.stringify({
1310
+ ok: resp.ok,
1311
+ command: resp.command,
1312
+ output: resp.output
1313
+ })
1314
+ }
1315
+ ],
1316
+ isError: false
1317
+ };
1318
+ }
1319
+ async handleCallClaude(args) {
1320
+ const { callClaude } = await import("../../orchestrators/multimodal/providers.js");
1321
+ const prompt = String(args.prompt || "");
1322
+ const model = args.model || process.env["STACKMEMORY_MM_PLANNER_MODEL"] || "claude-3-5-sonnet-latest";
1323
+ const system = args.system || "You are a precise assistant. Return plain text unless asked for JSON.";
1324
+ const text = await callClaude(prompt, { model, system });
1325
+ return {
1326
+ content: [
1327
+ {
1328
+ type: "text",
1329
+ text: JSON.stringify({ ok: true, text })
1330
+ }
1331
+ ],
1332
+ isError: false
1333
+ };
1334
+ }
1335
+ // Pending plan persistence (best-effort)
1336
+ getPendingStoreDir() {
1337
+ return join(this.projectRoot, ".stackmemory", "build");
1338
+ }
1339
+ getPendingStorePath() {
1340
+ return join(this.getPendingStoreDir(), "pending.json");
1341
+ }
1342
+ loadPendingPlans() {
1343
+ try {
1344
+ const file = this.getPendingStorePath();
1345
+ let sourceFile = file;
1346
+ if (!existsSync(file)) {
1347
+ const legacy = join(
1348
+ this.projectRoot,
1349
+ ".stackmemory",
1350
+ "mm-spike",
1351
+ "pending.json"
1352
+ );
1353
+ if (existsSync(legacy)) sourceFile = legacy;
1354
+ else return;
1355
+ }
1356
+ const data = JSON.parse(readFileSync(sourceFile, "utf-8"));
1357
+ if (data && typeof data === "object") {
1358
+ this.pendingPlans = new Map(Object.entries(data));
1359
+ if (sourceFile !== file) this.savePendingPlans();
1360
+ }
1361
+ } catch {
1362
+ }
1363
+ }
1364
+ savePendingPlans() {
1365
+ try {
1366
+ const dir = this.getPendingStoreDir();
1367
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1368
+ const file = this.getPendingStorePath();
1369
+ const obj = Object.fromEntries(this.pendingPlans);
1370
+ writeFileSync(file, JSON.stringify(obj, null, 2));
1371
+ } catch {
1372
+ }
1373
+ }
1374
+ async handlePlanGate(args) {
1375
+ const { runPlanOnly } = await import("../../orchestrators/multimodal/harness.js");
1376
+ const task = String(args.task || "Plan change");
1377
+ const plannerModel = args.plannerModel || process.env["STACKMEMORY_MM_PLANNER_MODEL"] || "claude-3-5-sonnet-latest";
1378
+ const plan = await runPlanOnly(
1379
+ { task, repoPath: this.projectRoot },
1380
+ { plannerModel }
1381
+ );
1382
+ const approvalId = `appr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1383
+ this.pendingPlans.set(approvalId, { task, plan, createdAt: Date.now() });
1384
+ this.savePendingPlans();
1385
+ const compact = Boolean(args.compact);
1386
+ const planOut = compact ? compactPlan(plan) : plan;
1387
+ return {
1388
+ content: [
1389
+ {
1390
+ type: "text",
1391
+ text: JSON.stringify({ ok: true, approvalId, plan: planOut })
1392
+ }
1393
+ ],
1394
+ isError: false
1395
+ };
1396
+ }
1397
+ async handleApprovePlan(args) {
1398
+ const { runSpike } = await import("../../orchestrators/multimodal/harness.js");
1399
+ const approvalId = String(args.approvalId || "");
1400
+ const pending = this.pendingPlans.get(approvalId);
1401
+ if (!pending) {
1402
+ return {
1403
+ content: [
1404
+ {
1405
+ type: "text",
1406
+ text: JSON.stringify({ ok: false, error: "Invalid approvalId" })
1407
+ }
1408
+ ],
1409
+ isError: false
1410
+ };
1411
+ }
1412
+ const implementer = args.implementer || process.env["STACKMEMORY_MM_IMPLEMENTER"] || "codex";
1413
+ const maxIters = Number(
1414
+ args.maxIters ?? process.env["STACKMEMORY_MM_MAX_ITERS"] ?? 2
1415
+ );
1416
+ const recordFrame = args.recordFrame !== false;
1417
+ const execute = args.execute !== false;
1418
+ const result = await runSpike(
1419
+ { task: pending.task, repoPath: this.projectRoot },
1420
+ {
1421
+ plannerModel: process.env["STACKMEMORY_MM_PLANNER_MODEL"] || "claude-3-5-sonnet-latest",
1422
+ reviewerModel: process.env["STACKMEMORY_MM_REVIEWER_MODEL"] || process.env["STACKMEMORY_MM_PLANNER_MODEL"] || "claude-3-5-sonnet-latest",
1423
+ implementer: implementer === "claude" ? "claude" : "codex",
1424
+ maxIters: isFinite(maxIters) ? Math.max(1, maxIters) : 2,
1425
+ dryRun: !execute,
1426
+ recordFrame
1427
+ }
1428
+ );
1429
+ this.pendingPlans.delete(approvalId);
1430
+ this.savePendingPlans();
1431
+ const compact = Boolean(args.compact);
1432
+ const payload = compact ? { ...result, plan: compactPlan(result.plan) } : result;
1433
+ return {
1434
+ content: [
1435
+ {
1436
+ type: "text",
1437
+ text: JSON.stringify({ ok: true, approvalId, result: payload })
1438
+ }
1439
+ ],
1440
+ isError: false
1441
+ };
1442
+ }
1443
+ async handlePendingList(args) {
1444
+ const schema = z.object({
1445
+ taskContains: z.string().optional(),
1446
+ olderThanMs: z.number().optional(),
1447
+ newerThanMs: z.number().optional(),
1448
+ sort: z.enum(["asc", "desc"]).optional(),
1449
+ limit: z.number().int().positive().optional()
1450
+ }).optional();
1451
+ const parsed = schema.safeParse(args);
1452
+ if (args && !parsed.success) {
1453
+ return {
1454
+ content: [
1455
+ {
1456
+ type: "text",
1457
+ text: JSON.stringify({
1458
+ ok: false,
1459
+ error: "Invalid arguments",
1460
+ details: parsed.error.issues
1461
+ })
1462
+ }
1463
+ ],
1464
+ isError: false
1465
+ };
1466
+ }
1467
+ const a = parsed.success && parsed.data ? parsed.data : {};
1468
+ const now = Date.now();
1469
+ let items = Array.from(this.pendingPlans.entries()).map(
1470
+ ([approvalId, data]) => ({
1471
+ approvalId,
1472
+ task: data?.task,
1473
+ createdAt: Number(data?.createdAt || 0) || null
1474
+ })
1475
+ );
1476
+ items = filterPending(items, a, now);
1477
+ return {
1478
+ content: [
1479
+ { type: "text", text: JSON.stringify({ ok: true, pending: items }) }
1480
+ ],
1481
+ isError: false
1482
+ };
1483
+ }
1484
+ async handlePendingClear(args) {
1485
+ const removed = [];
1486
+ const now = Date.now();
1487
+ const all = Boolean(args?.all);
1488
+ const approvalId = args?.approvalId ? String(args.approvalId) : void 0;
1489
+ const olderThanMs = Number.isFinite(Number(args?.olderThanMs)) ? Number(args.olderThanMs) : void 0;
1490
+ if (all) {
1491
+ for (const id of this.pendingPlans.keys()) removed.push(id);
1492
+ this.pendingPlans.clear();
1493
+ this.savePendingPlans();
1494
+ return {
1495
+ content: [
1496
+ { type: "text", text: JSON.stringify({ ok: true, removed }) }
1497
+ ],
1498
+ isError: false
1499
+ };
1500
+ }
1501
+ if (approvalId) {
1502
+ if (this.pendingPlans.has(approvalId)) {
1503
+ this.pendingPlans.delete(approvalId);
1504
+ removed.push(approvalId);
1505
+ this.savePendingPlans();
1506
+ }
1507
+ return {
1508
+ content: [
1509
+ { type: "text", text: JSON.stringify({ ok: true, removed }) }
1510
+ ],
1511
+ isError: false
1512
+ };
1513
+ }
1514
+ if (olderThanMs !== void 0 && olderThanMs >= 0) {
1515
+ for (const [id, data] of this.pendingPlans.entries()) {
1516
+ const ts = Number(data?.createdAt || 0);
1517
+ if (ts && now - ts > olderThanMs) {
1518
+ this.pendingPlans.delete(id);
1519
+ removed.push(id);
1520
+ }
1521
+ }
1522
+ this.savePendingPlans();
1523
+ return {
1524
+ content: [
1525
+ { type: "text", text: JSON.stringify({ ok: true, removed }) }
1526
+ ],
1527
+ isError: false
1528
+ };
1529
+ }
1530
+ return {
1531
+ content: [
1532
+ {
1533
+ type: "text",
1534
+ text: JSON.stringify({
1535
+ ok: false,
1536
+ error: "Specify approvalId, all=true, or olderThanMs"
1537
+ })
1538
+ }
1539
+ ],
1540
+ isError: false
1541
+ };
1542
+ }
1543
+ async handlePendingShow(args) {
1544
+ const approvalId = String(args?.approvalId || "");
1545
+ const data = this.pendingPlans.get(approvalId);
1546
+ if (!data) {
1547
+ return {
1548
+ content: [
1549
+ {
1550
+ type: "text",
1551
+ text: JSON.stringify({ ok: false, error: "Invalid approvalId" })
1552
+ }
1553
+ ],
1554
+ isError: false
1555
+ };
1556
+ }
1557
+ const compact = Boolean(args.compact);
1558
+ const planOut = compact ? compactPlan(data.plan) : data.plan;
1559
+ return {
1560
+ content: [
1561
+ {
1562
+ type: "text",
1563
+ text: JSON.stringify({
1564
+ ok: true,
1565
+ approvalId,
1566
+ task: data.task,
1567
+ plan: planOut,
1568
+ createdAt: data.createdAt || null
1569
+ })
1570
+ }
1571
+ ],
1572
+ isError: false
1573
+ };
1574
+ }
1011
1575
  async handleGetContext(args) {
1012
1576
  const { query: query2 = "", limit = 10 } = args;
1013
1577
  const contexts = Array.from(this.contexts.values()).sort((a, b) => b.importance - a.importance).slice(0, limit);