@mutmutco/cli 2.28.1 → 2.31.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/saga.cjs CHANGED
@@ -3411,18 +3411,28 @@ var import_node_path2 = require("node:path");
3411
3411
  var CLIENT_VERSION_HEADER = "x-client-version";
3412
3412
 
3413
3413
  // src/client-version.ts
3414
- function resolveClientVersion() {
3414
+ function resolveClientVersionManifestCandidates(distDir = __dirname) {
3415
+ return [
3416
+ (0, import_node_path2.join)(distDir, "..", "..", ".claude-plugin", "plugin.json"),
3417
+ (0, import_node_path2.join)(distDir, "..", "..", ".cursor-plugin", "plugin.json"),
3418
+ (0, import_node_path2.join)(distDir, "..", "..", ".codex-plugin", "plugin.json"),
3419
+ (0, import_node_path2.join)(distDir, "..", "package.json")
3420
+ ];
3421
+ }
3422
+ function readVersionFromManifest(path2) {
3415
3423
  try {
3416
- const manifest = (0, import_node_path2.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
3417
- return JSON.parse((0, import_node_fs2.readFileSync)(manifest, "utf8")).version || "0.0.0";
3424
+ const version = JSON.parse((0, import_node_fs2.readFileSync)(path2, "utf8")).version;
3425
+ return typeof version === "string" && version.trim() ? version.trim() : null;
3418
3426
  } catch {
3419
- try {
3420
- const pkg = (0, import_node_path2.join)(__dirname, "..", "package.json");
3421
- return JSON.parse((0, import_node_fs2.readFileSync)(pkg, "utf8")).version || "0.0.0";
3422
- } catch {
3423
- return "0.0.0";
3424
- }
3427
+ return null;
3428
+ }
3429
+ }
3430
+ function resolveClientVersion() {
3431
+ for (const manifest of resolveClientVersionManifestCandidates()) {
3432
+ const version = readVersionFromManifest(manifest);
3433
+ if (version) return version;
3425
3434
  }
3435
+ return "0.0.0";
3426
3436
  }
3427
3437
  function clientVersionHeaders() {
3428
3438
  return { [CLIENT_VERSION_HEADER]: resolveClientVersion() };
@@ -3430,6 +3440,26 @@ function clientVersionHeaders() {
3430
3440
 
3431
3441
  // src/saga-commands.ts
3432
3442
  var import_node_crypto3 = require("node:crypto");
3443
+ var import_promises2 = require("node:fs/promises");
3444
+
3445
+ // src/issue-body.ts
3446
+ async function resolveTextArg(input, deps, labels) {
3447
+ const hasValue = input.value !== void 0;
3448
+ const hasFile = input.file !== void 0;
3449
+ if (hasValue && hasFile) {
3450
+ throw new Error(`pass only one of ${labels.value} or ${labels.file}`);
3451
+ }
3452
+ if (!hasValue && !hasFile) {
3453
+ throw new Error(`pass ${labels.value} or ${labels.file}`);
3454
+ }
3455
+ if (hasValue) return input.value ?? "";
3456
+ const source = input.file ?? "";
3457
+ const text = source === "-" ? await deps.readStdin() : await deps.readFile(source, "utf8");
3458
+ if (text.trim().length === 0) {
3459
+ throw new Error(`${labels.file} produced an empty ${labels.noun}`);
3460
+ }
3461
+ return text;
3462
+ }
3433
3463
 
3434
3464
  // src/saga-capture.ts
3435
3465
  function parseHookInput(stdin) {
@@ -3566,6 +3596,7 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
3566
3596
 
3567
3597
  // src/saga-health.ts
3568
3598
  var MEMORY_STALE_DAYS = 14;
3599
+ var SESSION_START_LIVENESS = { attempts: 1, timeoutMs: 3e3 };
3569
3600
  function buildHealth(i) {
3570
3601
  const problems = [];
3571
3602
  if (!i.sagaApiUrl) problems.push("Hub API URL not configured");
@@ -3591,6 +3622,7 @@ function buildHealth(i) {
3591
3622
  authorized: i.authorized,
3592
3623
  sagaApiUrl: i.sagaApiUrl,
3593
3624
  pendingNotes: i.pendingNotes ?? 0,
3625
+ honchoPending: i.honchoPending ?? 0,
3594
3626
  key: i.key,
3595
3627
  source: i.source,
3596
3628
  problems,
@@ -3607,73 +3639,14 @@ function healthBanner(report) {
3607
3639
  if (report.warnings.length) return `saga health: NOTE - ${report.warnings.join("; ")}`;
3608
3640
  return null;
3609
3641
  }
3610
- function resumeCue() {
3611
- return '> STATUS/RESUME CUE \u2014 For any status, resume, or "where do I stand" report: read THIS saga HEAD first (`mmi-cli saga show`), then reconcile its NEXT / LAST 5 / DECISIONS against the live board + git/gh before reporting. Do not rebuild the picture from board/issues/memory while skipping the HEAD. PRECEDENCE: the HEAD is prior-session belief and MAY BE SUPERSEDED \u2014 the current live user/master instruction WINS over any conflicting HEAD anchor, NEXT, or checklist; follow the live instruction and treat the stale HEAD item as superseded.';
3612
- }
3613
-
3614
- // src/fetch-retry.ts
3615
- async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3616
- const attempts = opts.attempts ?? 3;
3617
- const baseDelayMs = opts.baseDelayMs ?? 250;
3618
- const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
3619
- const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3620
- let lastErr;
3621
- for (let i = 0; i < attempts; i++) {
3622
- const isLast = i === attempts - 1;
3623
- const attemptInit = opts.timeoutMs ? { ...init, signal: AbortSignal.timeout(opts.timeoutMs) } : init;
3624
- try {
3625
- const res = await fetchImpl(url, attemptInit);
3626
- if (!isLast && retryOn(res)) {
3627
- await sleep(baseDelayMs * 2 ** i);
3628
- continue;
3629
- }
3630
- return res;
3631
- } catch (e) {
3632
- lastErr = e;
3633
- if (isLast) throw e;
3634
- await sleep(baseDelayMs * 2 ** i);
3635
- }
3636
- }
3637
- throw lastErr;
3638
- }
3639
-
3640
- // src/saga-note.ts
3641
- var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
3642
- var ROUTE_LEVEL_403 = "saga API route-level 403 from HubSessionAuthorizer/session policy";
3643
- function agentSurface(env = process.env) {
3644
- const surface = env.MMI_AGENT_SURFACE?.trim() || (env.CODEX_THREAD_ID?.trim() && !env.CLAUDE_SESSION_ID?.trim() ? "codex" : "claude");
3645
- if (AGENT_SURFACE_TOKENS.includes(surface)) return surface;
3646
- throw new Error(`MMI_AGENT_SURFACE must be one of: ${AGENT_SURFACE_TOKENS.join(", ")}`);
3647
- }
3648
- function buildNoteCapture(summary, o, id, evidence) {
3649
- const queueOp = o.queueAdd ? { op: "add", text: o.queueAdd } : o.queueDone != null ? { op: "done", index: Number(o.queueDone) } : void 0;
3650
- const state = o.diagnostic ? "diagnostic" : o.verified ? "verified" : "asserted";
3651
- const source = o.diagnostic ? "probe" : "note";
3652
- const ev = {};
3653
- if (evidence.sha) ev.sha = evidence.sha;
3654
- if (evidence.branch) ev.branch = evidence.branch;
3655
- if (evidence.pr) ev.pr = evidence.pr;
3656
- if (evidence.file) ev.file = evidence.file;
3657
- const anchor = o.anchor ? { intent: o.anchor, setAt: (/* @__PURE__ */ new Date()).toISOString() } : void 0;
3658
- return {
3659
- event: "note",
3660
- id,
3661
- summary,
3662
- next: o.next,
3663
- decision: o.decision,
3664
- queueOp,
3665
- state,
3666
- source,
3667
- evidence: Object.keys(ev).length ? ev : void 0,
3668
- surface: agentSurface(),
3669
- supersedes: o.supersedes,
3670
- anchor,
3671
- anchorForce: o.anchorForce || void 0
3672
- };
3673
- }
3674
- function formatCaptureFailure(status, message) {
3675
- if (status === 403 && message === "Forbidden") return `saga: ${ROUTE_LEVEL_403} (HTTP 403)`;
3676
- return `saga: HTTP ${status}`;
3642
+ function memorySyncBanner(report) {
3643
+ const saga = report.pendingNotes;
3644
+ const honcho = report.honchoPending;
3645
+ if (saga <= 0 && honcho <= 0) return null;
3646
+ const parts = [];
3647
+ if (saga > 0) parts.push(`${saga} saga`);
3648
+ if (honcho > 0) parts.push(`${honcho} honcho`);
3649
+ return `MEMORY SYNC \u2014 ${parts.join(" + ")} write(s) queued locally \u2014 run \`mmi-cli saga flush\` / \`honcho flush\` (this device only).`;
3677
3650
  }
3678
3651
 
3679
3652
  // src/saga-pending.ts
@@ -3852,6 +3825,81 @@ async function flushPending(post, dir = ".mmi") {
3852
3825
  return { flushed, dropped, remaining: readPending(dir).length };
3853
3826
  }
3854
3827
 
3828
+ // src/honcho-pending.ts
3829
+ var HONCHO_QUEUE_DIR = ".mmi/honcho";
3830
+ function readHonchoPending(dir = HONCHO_QUEUE_DIR) {
3831
+ return readPending(dir);
3832
+ }
3833
+
3834
+ // src/fetch-retry.ts
3835
+ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3836
+ const attempts = opts.attempts ?? 3;
3837
+ const baseDelayMs = opts.baseDelayMs ?? 250;
3838
+ const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
3839
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3840
+ let lastErr;
3841
+ for (let i = 0; i < attempts; i++) {
3842
+ const isLast = i === attempts - 1;
3843
+ const attemptInit = opts.timeoutMs ? { ...init, signal: AbortSignal.timeout(opts.timeoutMs) } : init;
3844
+ try {
3845
+ const res = await fetchImpl(url, attemptInit);
3846
+ if (!isLast && retryOn(res)) {
3847
+ await sleep(baseDelayMs * 2 ** i);
3848
+ continue;
3849
+ }
3850
+ return res;
3851
+ } catch (e) {
3852
+ lastErr = e;
3853
+ if (isLast) throw e;
3854
+ await sleep(baseDelayMs * 2 ** i);
3855
+ }
3856
+ }
3857
+ throw lastErr;
3858
+ }
3859
+
3860
+ // src/saga-note.ts
3861
+ var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
3862
+ var ROUTE_LEVEL_403 = "saga API route-level 403 from HubSessionAuthorizer/session policy";
3863
+ function agentSurface(env = process.env) {
3864
+ const surface = env.MMI_AGENT_SURFACE?.trim() || (env.CODEX_THREAD_ID?.trim() && !env.CLAUDE_SESSION_ID?.trim() ? "codex" : "claude");
3865
+ if (AGENT_SURFACE_TOKENS.includes(surface)) return surface;
3866
+ throw new Error(`MMI_AGENT_SURFACE must be one of: ${AGENT_SURFACE_TOKENS.join(", ")}`);
3867
+ }
3868
+ function buildNoteCapture(summary, o, id, evidence) {
3869
+ const queueOp = o.queueAdd ? { op: "add", text: o.queueAdd } : o.queueDone != null ? { op: "done", index: Number(o.queueDone) } : void 0;
3870
+ const state = o.diagnostic ? "diagnostic" : o.verified ? "verified" : "asserted";
3871
+ const source = o.diagnostic ? "probe" : "note";
3872
+ const ev = {};
3873
+ if (evidence.sha) ev.sha = evidence.sha;
3874
+ if (evidence.branch) ev.branch = evidence.branch;
3875
+ if (evidence.pr) ev.pr = evidence.pr;
3876
+ if (evidence.file) ev.file = evidence.file;
3877
+ const anchor = o.anchor ? {
3878
+ intent: o.anchor,
3879
+ ...o.anchorSlug ? { slug: o.anchorSlug } : {},
3880
+ setAt: (/* @__PURE__ */ new Date()).toISOString()
3881
+ } : void 0;
3882
+ return {
3883
+ event: "note",
3884
+ id,
3885
+ summary,
3886
+ next: o.next,
3887
+ decision: o.decision,
3888
+ queueOp,
3889
+ state,
3890
+ source,
3891
+ evidence: Object.keys(ev).length ? ev : void 0,
3892
+ surface: agentSurface(),
3893
+ supersedes: o.supersedes,
3894
+ anchor,
3895
+ anchorForce: o.anchorForce || void 0
3896
+ };
3897
+ }
3898
+ function formatCaptureFailure(status, message) {
3899
+ if (status === 403 && message === "Forbidden") return `saga: ${ROUTE_LEVEL_403} (HTTP 403)`;
3900
+ return `saga: HTTP ${status}`;
3901
+ }
3902
+
3855
3903
  // src/hub-auth.ts
3856
3904
  var import_node_crypto = require("node:crypto");
3857
3905
  var import_node_fs5 = require("node:fs");
@@ -4147,6 +4195,13 @@ async function runNote(summary, o) {
4147
4195
  const capture = buildNoteCapture(summary, o, (0, import_node_crypto3.randomUUID)(), { sha: sha || void 0, branch: key.branch });
4148
4196
  await postCapture(capture);
4149
4197
  }
4198
+ function resolveSummary(summary, o) {
4199
+ return resolveTextArg({ value: summary, file: o.messageFile }, { readFile: import_promises2.readFile, readStdin }, {
4200
+ value: "a summary argument",
4201
+ file: "--message-file",
4202
+ noun: "message"
4203
+ });
4204
+ }
4150
4205
  async function runSagaFlush(o, io = consoleIo) {
4151
4206
  if (o.run) {
4152
4207
  try {
@@ -4181,7 +4236,6 @@ async function runSagaShow(opts, io = consoleIo) {
4181
4236
  const qs = opts.latestAnywhere ? "scope=anywhere" : new URLSearchParams({ project: key.project, branch: key.branch }).toString();
4182
4237
  const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await hubHeaders() }, { attempts: 2, timeoutMs: 3e3 });
4183
4238
  if (res.ok) {
4184
- io.log(resumeCue());
4185
4239
  return io.log(await res.text());
4186
4240
  }
4187
4241
  if (!opts.quiet) io.log(`saga show failed: HTTP ${res.status}`);
@@ -4192,9 +4246,9 @@ async function runSagaShow(opts, io = consoleIo) {
4192
4246
  }
4193
4247
  }
4194
4248
  }
4195
- async function probeBackend(url) {
4249
+ async function probeBackend(url, opts = { attempts: 3, timeoutMs: 4e3 }) {
4196
4250
  try {
4197
- const res = await fetchWithRetry(fetch, `${url}/saga/head`, { headers: await hubHeaders() }, { attempts: 3, timeoutMs: 4e3 });
4251
+ const res = await fetchWithRetry(fetch, `${url}/saga/head`, { headers: await hubHeaders() }, opts);
4198
4252
  let message = "";
4199
4253
  try {
4200
4254
  const body = await res.clone().json();
@@ -4233,12 +4287,13 @@ async function runSagaHealth(o, io = consoleIo) {
4233
4287
  const session = resolveSessionId();
4234
4288
  const key = await sagaKey(cfg, session);
4235
4289
  const source = session.source;
4290
+ const livenessOpts = o.banner ? SESSION_START_LIVENESS : { attempts: 3, timeoutMs: 4e3 };
4236
4291
  const [identity, liveness] = await Promise.all([
4237
4292
  hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }).then((s) => s?.login),
4238
- cfg.sagaApiUrl ? probeBackend(cfg.sagaApiUrl) : Promise.resolve({ reachable: false })
4293
+ cfg.sagaApiUrl ? probeBackend(cfg.sagaApiUrl, livenessOpts) : Promise.resolve({ reachable: false })
4239
4294
  ]);
4240
- const authorized = cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
4241
- const memoryAgeDays = cfg.sagaApiUrl && liveness.reachable ? await fetchMemoryAge(cfg.sagaApiUrl, key.project) : void 0;
4295
+ const authorized = o.banner ? void 0 : cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
4296
+ const memoryAgeDays = o.banner ? void 0 : cfg.sagaApiUrl && liveness.reachable ? await fetchMemoryAge(cfg.sagaApiUrl, key.project) : void 0;
4242
4297
  const report = buildHealth({
4243
4298
  key,
4244
4299
  source,
@@ -4249,12 +4304,15 @@ async function runSagaHealth(o, io = consoleIo) {
4249
4304
  authorized,
4250
4305
  sagaApiUrl: cfg.sagaApiUrl,
4251
4306
  pendingNotes: readPending().length,
4307
+ honchoPending: readHonchoPending().length,
4252
4308
  memoryAgeDays
4253
4309
  });
4254
4310
  if (o.json) return io.log(JSON.stringify(report));
4255
4311
  if (o.banner) {
4256
4312
  const banner = healthBanner(report);
4257
4313
  if (banner) io.log(banner);
4314
+ const sync = memorySyncBanner(report);
4315
+ if (sync) io.log(sync);
4258
4316
  return;
4259
4317
  }
4260
4318
  if (o.quiet) return;
@@ -4265,8 +4323,24 @@ async function runSagaHealth(o, io = consoleIo) {
4265
4323
  }
4266
4324
  function registerSagaCommands(program2) {
4267
4325
  const saga = program2.command("saga").description("per-session continuity");
4268
- saga.command("note <summary>").description("record a one-line structured note into your saga (the per-turn capture)").option("--next <text>", 'set "where I left off" (NEXT)').option("--decision <text>", "append a verbatim decision").option("--queue-add <text>", "add a worklist item").option("--queue-done <n>", "mark worklist item N done").option("--verified", "mark this claim as checked against source (state: verified, else asserted)").option("--diagnostic", "isolate a probe write (state: diagnostic, source: probe) \u2014 never resume/LAST 5").option("--supersedes <key>", "retire prior decisions matching an evidence key (pr:N | file:path)").option("--anchor <intent>", "set the sprint North-Star (write-protected; needs --anchor-force to change)").option("--anchor-force", "overwrite an existing anchor").action((summary, o) => runNote(summary, o));
4269
- saga.command("probe <summary>").description("record a diagnostic probe note (alias for `saga note --diagnostic`)").option("--next <text>", 'set "where I left off" (NEXT)').option("--decision <text>", "append a verbatim decision").option("--queue-add <text>", "add a worklist item").option("--queue-done <n>", "mark worklist item N done").action((summary, o) => runNote(summary, { ...o, diagnostic: true }));
4326
+ saga.command("note [summary]").description("record a one-line structured note into your saga (the per-turn capture)").option("--next <text>", 'set "where I left off" (NEXT)').option("--decision <text>", "append a verbatim decision").option("--queue-add <text>", "add a worklist item").option("--queue-done <n>", "mark worklist item N done").option("--verified", "mark this claim as checked against source (state: verified, else asserted)").option("--diagnostic", "isolate a probe write (state: diagnostic, source: probe) \u2014 never resume/LAST 5").option("--supersedes <key>", "retire prior decisions matching an evidence key (pr:N | file:path)").option("--anchor <intent>", "set the sprint North-Star (write-protected; needs --anchor-force to change)").option("--anchor-slug <slug>", "bind the anchor to a North Star plan slug (SSOT at plans/.../<slug>.md)").option("--anchor-force", "overwrite an existing anchor").option("--message-file <path|->", "read the summary from a UTF-8 file, or from stdin with - (avoids cmd.exe quoting)").action(async (summary, o) => {
4327
+ let text;
4328
+ try {
4329
+ text = await resolveSummary(summary, o);
4330
+ } catch (e) {
4331
+ return fail(`saga note: ${e.message}`);
4332
+ }
4333
+ await runNote(text, o);
4334
+ });
4335
+ saga.command("probe [summary]").description("record a diagnostic probe note (alias for `saga note --diagnostic`)").option("--next <text>", 'set "where I left off" (NEXT)').option("--decision <text>", "append a verbatim decision").option("--queue-add <text>", "add a worklist item").option("--queue-done <n>", "mark worklist item N done").option("--message-file <path|->", "read the summary from a UTF-8 file, or from stdin with - (avoids cmd.exe quoting)").action(async (summary, o) => {
4336
+ let text;
4337
+ try {
4338
+ text = await resolveSummary(summary, o);
4339
+ } catch (e) {
4340
+ return fail(`saga probe: ${e.message}`);
4341
+ }
4342
+ await runNote(text, { ...o, diagnostic: true });
4343
+ });
4270
4344
  saga.command("flush").option("--json", "machine-readable {flushed, dropped, remaining}").option("--run", "detached worker: drain the queue silently (spawned by note/capture)").description("roll the local pending-note queue forward (re-POST queued saga writes); reports what landed").action((o) => runSagaFlush(o));
4271
4345
  saga.command("show").option("--quiet", "no-op silently when unconfigured/unreachable (SessionStart hook)").option("--latest-anywhere", "resume the newest saga across all repos (default: current repo)").description("print your resume block \u2014 current repo HEAD + project memory (where you left off)").action((opts) => runSagaShow(opts));
4272
4346
  saga.command("capture").option("--quiet", "capture silently (for the Stop hook)").description("per-turn deterministic capture (Stop hook): turn boundary + current sha + gated HEAD-update").action(async (opts) => {
@@ -4380,8 +4454,16 @@ function socketPath(env = process.env, platform = process.platform, user) {
4380
4454
  return platform === "win32" ? `\\\\.\\pipe\\mmi-cli-${hash}` : (0, import_node_path6.join)(daemonDir(env), `mmi-cli-${hash}.sock`);
4381
4455
  }
4382
4456
  var HOT_VERBS = /* @__PURE__ */ new Set(["note", "probe", "capture", "session", "head-update"]);
4457
+ function argvReadsStdin(args) {
4458
+ for (let i = 0; i < args.length; i++) {
4459
+ const a = args[i];
4460
+ if (a.endsWith("-file=-")) return true;
4461
+ if (a.endsWith("-file") && args[i + 1] === "-") return true;
4462
+ }
4463
+ return false;
4464
+ }
4383
4465
  function daemonEligible(args) {
4384
- return args[0] === "saga" && HOT_VERBS.has(args[1] ?? "") && !args.includes("--run") && !args.includes("--help") && !args.includes("-h");
4466
+ return args[0] === "saga" && HOT_VERBS.has(args[1] ?? "") && !args.includes("--run") && !args.includes("--help") && !args.includes("-h") && !argvReadsStdin(args);
4385
4467
  }
4386
4468
  function buildStamp(version, bundleMtimeMs) {
4387
4469
  return `${version}#${Math.trunc(bundleMtimeMs)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.28.1",
3
+ "version": "2.31.0",
4
4
  "description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
42
- "esbuild": "^0.28.0",
42
+ "esbuild": "^0.28.1",
43
43
  "typescript": "^5.7.0",
44
44
  "vitest": "^4.1.0"
45
45
  }