@sechroom/cli 2026.7.3 → 2026.7.4-rc.ce2b31c6

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 +411 -35
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync11 } from "fs";
4
+ import { readFileSync as readFileSync12 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/auth.ts
@@ -1544,11 +1544,16 @@ function registerChannel(program2) {
1544
1544
  const cfg = resolveConfig(cmd.optsWithGlobals());
1545
1545
  const filter = readFilter(opts);
1546
1546
  const sub = await ensureSubscription(cfg, opts.name, filter);
1547
- const conn = await openConnection(cfg, (payload) => {
1548
- process.stdout.write(
1547
+ const seen = /* @__PURE__ */ new Set();
1548
+ const deliver = makeDeliver(
1549
+ filter,
1550
+ seen,
1551
+ (payload) => process.stdout.write(
1549
1552
  (typeof payload === "string" ? payload : JSON.stringify(payload)) + "\n"
1550
- );
1551
- });
1553
+ )
1554
+ );
1555
+ const conn = await openConnection(cfg, deliver);
1556
+ await reconcile(cfg, filter, deliver);
1552
1557
  if (json) {
1553
1558
  emit(
1554
1559
  {
@@ -1585,7 +1590,8 @@ function registerChannel(program2) {
1585
1590
  );
1586
1591
  await mcp.connect(new StdioServerTransport());
1587
1592
  await ensureSubscription(cfg, opts.name, filter);
1588
- const conn = await openConnection(cfg, (payload) => {
1593
+ const seen = /* @__PURE__ */ new Set();
1594
+ const deliver = makeDeliver(filter, seen, (payload) => {
1589
1595
  const { content, meta } = summarizeEvent(payload);
1590
1596
  void mcp.notification({
1591
1597
  method: "notifications/claude/channel",
@@ -1595,6 +1601,8 @@ function registerChannel(program2) {
1595
1601
  `))
1596
1602
  );
1597
1603
  });
1604
+ const conn = await openConnection(cfg, deliver);
1605
+ await reconcile(cfg, filter, deliver);
1598
1606
  process.stderr.write(
1599
1607
  style.dim(
1600
1608
  `sechroom channel (mcp) \u2014 tenant ${cfg.tenant}, tags [${filter.tags.join(", ")}]
@@ -1605,7 +1613,10 @@ function registerChannel(program2) {
1605
1613
  });
1606
1614
  channel.command("install").description(
1607
1615
  "Wire `sechroom channel mcp` into the project .mcp.json as a Claude Code channel MCP server (idempotent)"
1608
- ).option("--workspace <wsp...>", "Restrict dispatches to these workspace id(s)").option(
1616
+ ).option(
1617
+ "--workspace <wsp...>",
1618
+ "Restrict dispatches to these workspace id(s)"
1619
+ ).option(
1609
1620
  "--tag <tag...>",
1610
1621
  "Tag(s) a dispatch must carry to match (repeatable). Default targets WLP task dispatches.",
1611
1622
  ["kind:task"]
@@ -1726,20 +1737,134 @@ function holdOpen(conn) {
1726
1737
  process.on("SIGTERM", stop);
1727
1738
  });
1728
1739
  }
1729
- function summarizeEvent(payload) {
1740
+ function parseEvent(payload) {
1730
1741
  let data = payload;
1731
1742
  if (typeof payload === "string") {
1732
1743
  try {
1733
1744
  data = JSON.parse(payload);
1734
1745
  } catch {
1735
- return { content: payload, meta: {} };
1746
+ return { eventType: "", memoryId: "", workspaceId: "", tags: void 0 };
1736
1747
  }
1737
1748
  }
1738
1749
  const obj = data ?? {};
1739
1750
  const inner = obj.data ?? obj;
1740
- const eventType = str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event";
1741
- const memoryId = str(inner.memoryId ?? inner.MemoryId);
1742
- const workspaceId = str(inner.workspaceId ?? inner.WorkspaceId);
1751
+ const rawTags = inner.tags ?? inner.Tags;
1752
+ return {
1753
+ eventType: str(inner.eventType ?? inner.EventType ?? obj.type) || "substrate.event",
1754
+ memoryId: str(inner.memoryId ?? inner.MemoryId),
1755
+ workspaceId: str(inner.workspaceId ?? inner.WorkspaceId),
1756
+ tags: Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string") : void 0
1757
+ };
1758
+ }
1759
+ function shouldDeliver(payload, filter) {
1760
+ const { workspaceId, tags } = parseEvent(payload);
1761
+ if (filter.workspaceScope.length > 0 && (!workspaceId || !filter.workspaceScope.includes(workspaceId)))
1762
+ return false;
1763
+ if (filter.tags.length > 0) {
1764
+ if (!tags) return false;
1765
+ return facetedTagMatch(tags, filter.tags);
1766
+ }
1767
+ return true;
1768
+ }
1769
+ function makeDeliver(filter, seen, forward) {
1770
+ return (payload) => {
1771
+ if (!shouldDeliver(payload, filter)) return;
1772
+ const { memoryId } = parseEvent(payload);
1773
+ if (memoryId) {
1774
+ if (seen.has(memoryId)) return;
1775
+ seen.add(memoryId);
1776
+ }
1777
+ forward(payload);
1778
+ };
1779
+ }
1780
+ async function reconcile(cfg, filter, deliver) {
1781
+ if (filter.workspaceScope.length === 0) {
1782
+ process.stderr.write(
1783
+ style.dim(
1784
+ "channel: no --workspace to reconcile against; live feed only (a dropped dispatch won't be recovered).\n"
1785
+ )
1786
+ );
1787
+ return;
1788
+ }
1789
+ if (filter.tags.length === 0) return;
1790
+ let token;
1791
+ try {
1792
+ token = await requireToken(cfg);
1793
+ } catch {
1794
+ return;
1795
+ }
1796
+ const qs = `filterTags=${encodeURIComponent(filter.tags.join(","))}&limit=100`;
1797
+ let recovered = 0;
1798
+ for (const ws of filter.workspaceScope) {
1799
+ try {
1800
+ const resp = await fetch(
1801
+ `${cfg.baseUrl}/workspaces/${encodeURIComponent(ws)}/memories/feed?${qs}`,
1802
+ {
1803
+ headers: {
1804
+ authorization: `Bearer ${token}`,
1805
+ tenant: cfg.tenant,
1806
+ "x-sechroom-surface": "cli"
1807
+ }
1808
+ }
1809
+ );
1810
+ if (!resp.ok) {
1811
+ process.stderr.write(
1812
+ err(
1813
+ `channel: reconcile query for ${ws} failed (HTTP ${resp.status})
1814
+ `
1815
+ )
1816
+ );
1817
+ continue;
1818
+ }
1819
+ const data = await resp.json();
1820
+ for (const m of data.results ?? []) {
1821
+ if (!m.id) continue;
1822
+ deliver({
1823
+ eventType: "reconcile",
1824
+ memoryId: m.id,
1825
+ workspaceId: ws,
1826
+ tags: m.tags ?? []
1827
+ });
1828
+ recovered++;
1829
+ }
1830
+ } catch (e) {
1831
+ process.stderr.write(
1832
+ err(`channel: reconcile error for ${ws}: ${String(e)}
1833
+ `)
1834
+ );
1835
+ }
1836
+ }
1837
+ if (recovered > 0)
1838
+ process.stderr.write(
1839
+ style.dim(
1840
+ `channel: reconciled ${recovered} already-queued event(s) on connect.
1841
+ `
1842
+ )
1843
+ );
1844
+ }
1845
+ function facetedTagMatch(eventTags, filterTags) {
1846
+ const have = new Set(eventTags);
1847
+ const groups = /* @__PURE__ */ new Map();
1848
+ for (const f of filterTags) {
1849
+ const ns = f.endsWith(":*") ? f.slice(0, -2) : namespaceOf(f);
1850
+ const group = groups.get(ns) ?? [];
1851
+ group.push(f);
1852
+ groups.set(ns, group);
1853
+ }
1854
+ for (const [ns, group] of groups) {
1855
+ const ok2 = group.some(
1856
+ (f) => f.endsWith(":*") ? eventTags.some((t) => namespaceOf(t) === ns) : have.has(f)
1857
+ );
1858
+ if (!ok2) return false;
1859
+ }
1860
+ return true;
1861
+ }
1862
+ function namespaceOf(tag) {
1863
+ const i = tag.indexOf(":");
1864
+ return i >= 0 ? tag.slice(0, i) : tag;
1865
+ }
1866
+ function summarizeEvent(payload) {
1867
+ const { eventType, memoryId, workspaceId } = parseEvent(payload);
1743
1868
  const content = memoryId ? `${eventType}: ${memoryId}${workspaceId ? ` (workspace ${workspaceId})` : ""}` : typeof payload === "string" ? payload : JSON.stringify(payload);
1744
1869
  const meta = {};
1745
1870
  if (eventType) meta.event_type = eventType;
@@ -2408,6 +2533,149 @@ Examples:
2408
2533
  });
2409
2534
  }
2410
2535
 
2536
+ // src/commands/close.ts
2537
+ import { readFileSync as readFileSync7 } from "fs";
2538
+ var VERDICTS = ["pass", "soft-fail", "plan-invalid", "blocked"];
2539
+ function registerClose(program2) {
2540
+ program2.command("close").description(
2541
+ "Close a dispatched WorkTask in one call: closeout memo (verdict + correlation) + Reference edge + source status flip. Managed runs stamp the run's matchTags verbatim (SBC-1180)."
2542
+ ).requiredOption("--task <id>", "The WorkTask memory id being closed").requiredOption("--verdict <verdict>", `Verdict: ${VERDICTS.join(" | ")}`).requiredOption(
2543
+ "--workspace <wsp>",
2544
+ "Workspace that owns the closeout memo"
2545
+ ).requiredOption("--title <title>", "Closeout title").option(
2546
+ "--file <path>",
2547
+ "Read the closeout body from a file (default: stdin)"
2548
+ ).option(
2549
+ "--decomposition <id>",
2550
+ "Managed-run selector \u2014 the sug_ decomposition the task belongs to. Locates the parked run so its matchTags/verdict contract are read from state, not assembled from args."
2551
+ ).option(
2552
+ "--no-status-flip",
2553
+ "Skip the status flip on a BARE task. (Managed runs never flip here \u2014 the run engine reflects the verdict onto the task on resume.)"
2554
+ ).option("--source <source>", "Source / lane stamp", "cli").addHelpText(
2555
+ "after",
2556
+ `
2557
+ Examples:
2558
+ # Managed run \u2014 reads matchTags from run state, stamps them verbatim, un-parks the run:
2559
+ $ echo "Shipped X. Tests green." | sechroom close --task mem_XXXX --decomposition sug_YYYY \\
2560
+ --verdict pass --workspace wsp_ZZZZ --title "w4_build closeout"
2561
+
2562
+ # Bare task:
2563
+ $ sechroom close --task mem_XXXX --verdict pass --workspace wsp_ZZZZ \\
2564
+ --title "done" --file ./closeout.md`
2565
+ ).action(async (opts, cmd) => {
2566
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2567
+ const json = cmd.optsWithGlobals().json;
2568
+ const verdict = String(opts.verdict);
2569
+ if (!VERDICTS.includes(verdict))
2570
+ fail(
2571
+ `--verdict must be one of ${VERDICTS.join(" | ")} \u2014 got '${opts.verdict}'`
2572
+ );
2573
+ let bodyText;
2574
+ try {
2575
+ bodyText = opts.file ? readFileSync7(opts.file, "utf8") : readFileSync7(0, "utf8");
2576
+ } catch {
2577
+ fail(
2578
+ opts.file ? `could not read --file ${opts.file}` : "no closeout body \u2014 pass --file <path> or pipe it on stdin"
2579
+ );
2580
+ }
2581
+ if (!bodyText.trim())
2582
+ fail(
2583
+ "closeout body is empty \u2014 pass --file <path> or pipe the body on stdin"
2584
+ );
2585
+ const client = await makeClient(cfg);
2586
+ let matchTags;
2587
+ if (opts.decomposition) {
2588
+ const run = await runApi(
2589
+ "Reading run contract",
2590
+ async () => client.GET("/decompositions/{id}/run", {
2591
+ params: { path: { id: opts.decomposition } }
2592
+ })
2593
+ );
2594
+ const await_ = run.awaitingCompletion;
2595
+ if (!await_ || !Array.isArray(await_.matchTags) || await_.matchTags.length === 0)
2596
+ fail(
2597
+ `decomposition ${opts.decomposition} has no parked run awaiting a completion \u2014 nothing to close (run outcome: ${run.outcome ?? "unknown"}). Idempotent no-op on an already-resumed/terminal run.`
2598
+ );
2599
+ if (run.awaitingTaskId && run.awaitingTaskId !== opts.task)
2600
+ fail(
2601
+ `run ${opts.decomposition} is awaiting task ${run.awaitingTaskId}, not ${opts.task} \u2014 refusing to mis-key the resume.`
2602
+ );
2603
+ matchTags = await_.matchTags;
2604
+ } else {
2605
+ matchTags = [`wlp-task:${opts.task}`];
2606
+ }
2607
+ const tags = [
2608
+ .../* @__PURE__ */ new Set([
2609
+ ...matchTags,
2610
+ `wlp-task:${opts.task}`,
2611
+ `verdict:${verdict}`
2612
+ ])
2613
+ ];
2614
+ const closeout = await runApi(
2615
+ "Creating closeout",
2616
+ async () => client.POST("/memories", {
2617
+ body: {
2618
+ text: bodyText,
2619
+ type: "reference",
2620
+ content: "{}",
2621
+ confidence: 1,
2622
+ source: opts.source,
2623
+ archetype: "Document",
2624
+ title: opts.title,
2625
+ tags,
2626
+ owner: { type: "Workspace", id: opts.workspace }
2627
+ }
2628
+ })
2629
+ );
2630
+ const edge = await runApi(
2631
+ "Wiring Reference edge",
2632
+ async () => client.POST("/memories/{memoryId}/relationships", {
2633
+ params: { path: { memoryId: closeout.id } },
2634
+ body: { toMemoryId: opts.task, type: "Reference" }
2635
+ })
2636
+ );
2637
+ let taskStatus;
2638
+ if (opts.statusFlip !== false && !opts.decomposition) {
2639
+ const current = await runApi(
2640
+ "Reading task tags",
2641
+ async () => client.GET("/memories/{memoryId}", {
2642
+ params: { path: { memoryId: opts.task } }
2643
+ })
2644
+ );
2645
+ const currentTags = current?.item?.tags ?? current?.tags ?? [];
2646
+ const target = verdict === "blocked" ? "status:blocked" : "status:done";
2647
+ const nextTags = [
2648
+ ...new Set(currentTags.filter((t) => !t.startsWith("status:")))
2649
+ ].concat(target);
2650
+ await runApi(
2651
+ "Flipping task status",
2652
+ async () => client.PATCH("/memories/{memoryId}/metadata", {
2653
+ params: { path: { memoryId: opts.task } },
2654
+ body: {
2655
+ memoryId: opts.task,
2656
+ source: opts.source,
2657
+ tags: nextTags
2658
+ }
2659
+ })
2660
+ );
2661
+ taskStatus = target.slice("status:".length);
2662
+ }
2663
+ const view = resolveViewUrl(cfg.baseUrl, closeout.url);
2664
+ const result = {
2665
+ closeoutId: closeout.id,
2666
+ edgeId: edge.id,
2667
+ taskStatus: taskStatus ?? null,
2668
+ verdict,
2669
+ tags
2670
+ };
2671
+ emitAction(
2672
+ `closed ${style.bold(opts.task)} verdict:${style.bold(verdict)} \u2192 closeout ${style.bold(closeout.id)}${taskStatus ? ` ${style.dim(`(task ${taskStatus})`)}` : ""}${view ? ` ${style.dim("\u2192")} ${view}` : ""}`,
2673
+ result,
2674
+ json
2675
+ );
2676
+ });
2677
+ }
2678
+
2411
2679
  // src/commands/continuity.ts
2412
2680
  function registerContinuity(program2) {
2413
2681
  const continuity = program2.command("continuity").description("Continuity snapshots: checkpoint and resume work");
@@ -2568,6 +2836,90 @@ Examples:
2568
2836
  });
2569
2837
  }
2570
2838
 
2839
+ // src/commands/decomposition.ts
2840
+ function registerDecomposition(program2) {
2841
+ const decomposition = program2.command("decomposition").description(
2842
+ "Drive a WLP decomposition: decompose a brief, then execute / accept / reject"
2843
+ );
2844
+ decomposition.addHelpText(
2845
+ "after",
2846
+ `
2847
+ Examples:
2848
+ $ sechroom decomposition decompose mem_XXXX
2849
+ $ sechroom decomposition execute sug_XXXX
2850
+ $ sechroom decomposition accept sug_XXXX
2851
+ $ sechroom decomposition reject sug_XXXX --reason "wrong shape"`
2852
+ );
2853
+ decomposition.command("decompose <briefId>").description(
2854
+ "Decompose a work brief into a candidate Task graph (POST /work-briefs/{id}/decompose)"
2855
+ ).action(async (briefId, _opts, cmd) => {
2856
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2857
+ const data = await runApi("Queueing decomposition", async () => {
2858
+ const client = await makeClient(cfg);
2859
+ return client.POST("/work-briefs/{id}/decompose", {
2860
+ params: { path: { id: briefId } },
2861
+ body: { id: briefId }
2862
+ });
2863
+ });
2864
+ emitAction(
2865
+ `queued decomposition of ${style.bold(briefId)} \u2192 ${style.bold(data.suggestionId)}`,
2866
+ data,
2867
+ cmd.optsWithGlobals().json
2868
+ );
2869
+ });
2870
+ decomposition.command("execute <decompositionId>").description(
2871
+ "Execute a decomposition's Task graph (POST /decompositions/{id}/execute)"
2872
+ ).action(async (decompositionId, _opts, cmd) => {
2873
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2874
+ const data = await runApi("Executing decomposition", async () => {
2875
+ const client = await makeClient(cfg);
2876
+ return client.POST("/decompositions/{id}/execute", {
2877
+ params: { path: { id: decompositionId } },
2878
+ body: {}
2879
+ });
2880
+ });
2881
+ emitAction(
2882
+ `executed ${style.bold(decompositionId)} \u2192 run ${style.bold(data.runRecordId)} (${data.outcome})`,
2883
+ data,
2884
+ cmd.optsWithGlobals().json
2885
+ );
2886
+ });
2887
+ decomposition.command("accept <decompositionId>").description(
2888
+ "Accept a Pending decomposition \u2014 promote + ratify its Tasks (POST /decompositions/{id}/accept)"
2889
+ ).action(async (decompositionId, _opts, cmd) => {
2890
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2891
+ const data = await runApi("Accepting decomposition", async () => {
2892
+ const client = await makeClient(cfg);
2893
+ return client.POST("/decompositions/{id}/accept", {
2894
+ params: { path: { id: decompositionId } },
2895
+ body: {}
2896
+ });
2897
+ });
2898
+ emitAction(
2899
+ `accepted decomposition ${style.bold(decompositionId)}`,
2900
+ data,
2901
+ cmd.optsWithGlobals().json
2902
+ );
2903
+ });
2904
+ decomposition.command("reject <decompositionId>").description(
2905
+ "Reject a Pending decomposition \u2014 archive its Tasks, bounce the brief (POST /decompositions/{id}/reject)"
2906
+ ).option("--reason <reason>", "Optional free-text rejection reason").action(async (decompositionId, opts, cmd) => {
2907
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2908
+ const data = await runApi("Rejecting decomposition", async () => {
2909
+ const client = await makeClient(cfg);
2910
+ return client.POST("/decompositions/{id}/reject", {
2911
+ params: { path: { id: decompositionId } },
2912
+ body: { reasonText: opts.reason ?? null }
2913
+ });
2914
+ });
2915
+ emitAction(
2916
+ `rejected decomposition ${style.bold(decompositionId)}`,
2917
+ data,
2918
+ cmd.optsWithGlobals().json
2919
+ );
2920
+ });
2921
+ }
2922
+
2571
2923
  // src/commands/filing.ts
2572
2924
  function registerFiling(program2) {
2573
2925
  const filing = program2.command("filing").description("Review and act on filing suggestions");
@@ -3079,7 +3431,7 @@ Examples:
3079
3431
 
3080
3432
  // src/setup/apply.ts
3081
3433
  import { createHash as createHash3 } from "crypto";
3082
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
3434
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync9 } from "fs";
3083
3435
  import { dirname as dirname8 } from "path";
3084
3436
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
3085
3437
  var MARKER_END = "<!-- @sechroom/cli:end";
@@ -3137,7 +3489,7 @@ function ensureDir2(path) {
3137
3489
  }
3138
3490
  function readOr(path, fallback) {
3139
3491
  try {
3140
- return readFileSync7(path, "utf8");
3492
+ return readFileSync8(path, "utf8");
3141
3493
  } catch {
3142
3494
  return fallback;
3143
3495
  }
@@ -3148,7 +3500,7 @@ function mergeMcpJson(path, snippet, dryRun) {
3148
3500
  let current = {};
3149
3501
  if (existed) {
3150
3502
  try {
3151
- current = JSON.parse(readFileSync7(path, "utf8"));
3503
+ current = JSON.parse(readFileSync8(path, "utf8"));
3152
3504
  } catch {
3153
3505
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
3154
3506
  }
@@ -3864,7 +4216,7 @@ import { basename as basename2, join as join13 } from "path";
3864
4216
 
3865
4217
  // src/commands/fanout.ts
3866
4218
  import { spawnSync } from "child_process";
3867
- import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
4219
+ import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3868
4220
  import { isAbsolute, join as join12, resolve } from "path";
3869
4221
  var ICON = {
3870
4222
  refresh: "\u21BB",
@@ -3899,7 +4251,7 @@ function readManifest(path) {
3899
4251
  if (!existsSync10(path)) return null;
3900
4252
  let parsed;
3901
4253
  try {
3902
- parsed = JSON.parse(readFileSync8(path, "utf8"));
4254
+ parsed = JSON.parse(readFileSync9(path, "utf8"));
3903
4255
  } catch (err2) {
3904
4256
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
3905
4257
  }
@@ -4713,24 +5065,45 @@ Examples:
4713
5065
  $ sechroom relationship suggestions --status Pending --memory mem_XXXX
4714
5066
  $ sechroom relationship suggestion accept rsg_XXXX`
4715
5067
  );
4716
- relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").action(async (fromMemoryId, toMemoryId, opts, cmd) => {
5068
+ relationship.command("create <fromMemoryId> <toMemoryId>").description("Create a relationship (POST /memories/{memoryId}/relationships)").option("--type <type>", "Relationship type (Reference, Related, Parent, Child, Follows, \u2026)", "Reference").option(
5069
+ "--to-version <number>",
5070
+ "Version of the target memory to pin the edge to (defaults to the target's current version)"
5071
+ ).action(async (fromMemoryId, toMemoryId, opts, cmd) => {
4717
5072
  const cfg = resolveConfig(cmd.optsWithGlobals());
4718
- const data = await runApi("Creating relationship", async () => {
4719
- const client = await makeClient(cfg);
4720
- return client.POST("/memories/{memoryId}/relationships", {
5073
+ const client = await makeClient(cfg);
5074
+ let toVersion;
5075
+ if (opts.toVersion !== void 0) {
5076
+ toVersion = Number(opts.toVersion);
5077
+ if (!Number.isInteger(toVersion) || toVersion < 1) {
5078
+ fail(`--to-version must be a positive integer (got '${opts.toVersion}').`);
5079
+ }
5080
+ } else {
5081
+ const target = await runApi(
5082
+ "Resolving target version",
5083
+ async () => client.GET("/memories/{memoryId}", { params: { path: { memoryId: toMemoryId } } })
5084
+ );
5085
+ if (typeof target.item?.currentVersion !== "number") {
5086
+ fail(`Could not resolve the current version of ${toMemoryId}; pass --to-version explicitly.`);
5087
+ }
5088
+ toVersion = target.item.currentVersion;
5089
+ }
5090
+ const data = await runApi(
5091
+ "Creating relationship",
5092
+ async () => client.POST("/memories/{memoryId}/relationships", {
4721
5093
  params: { path: { memoryId: fromMemoryId } },
4722
5094
  body: {
4723
5095
  toMemoryId,
4724
- type: opts.type
5096
+ type: opts.type,
5097
+ toVersion
4725
5098
  }
4726
- });
4727
- });
5099
+ })
5100
+ );
4728
5101
  const inversePart = data.inverseId ? ` ${style.dim(`(inverse ${data.inverseId})`)}` : "";
4729
5102
  const view = resolveViewUrl(cfg.baseUrl, data.url);
4730
5103
  const urlPart = view ? ` ${style.dim("\u2192")} ${view}` : "";
4731
5104
  emitAction(
4732
- `created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId}`)}${inversePart}${urlPart}`,
4733
- data,
5105
+ `created relationship ${style.bold(data.id)} ${style.dim(`${fromMemoryId} \u2192 ${toMemoryId} @v${toVersion}`)}${inversePart}${urlPart}`,
5106
+ { ...data, toVersion },
4734
5107
  cmd.optsWithGlobals().json
4735
5108
  );
4736
5109
  });
@@ -4854,7 +5227,7 @@ Examples:
4854
5227
  // src/commands/reset.ts
4855
5228
  import { homedir as homedir4 } from "os";
4856
5229
  import { join as join14 } from "path";
4857
- import { existsSync as existsSync12, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
5230
+ import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync3 } from "fs";
4858
5231
  var SKILLS_LOCK2 = ".sechroom-skills.json";
4859
5232
  var localSkillsDir = () => join14(process.cwd(), ".claude", "skills");
4860
5233
  var globalSkillsDir = () => join14(homedir4(), ".claude", "skills");
@@ -4865,7 +5238,7 @@ function removeMaterialisedSkills(dir) {
4865
5238
  const lockPath = join14(dir, SKILLS_LOCK2);
4866
5239
  if (!existsSync12(lockPath)) return removed;
4867
5240
  try {
4868
- const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
5241
+ const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
4869
5242
  for (const entry of Object.values(lock)) {
4870
5243
  for (const name of entry.skills ?? []) {
4871
5244
  const p = join14(dir, name);
@@ -5265,7 +5638,7 @@ Examples:
5265
5638
  import {
5266
5639
  existsSync as existsSync15,
5267
5640
  mkdirSync as mkdirSync12,
5268
- readFileSync as readFileSync10,
5641
+ readFileSync as readFileSync11,
5269
5642
  rmSync as rmSync4,
5270
5643
  writeFileSync as writeFileSync12
5271
5644
  } from "fs";
@@ -5480,7 +5853,7 @@ function findBinding(start) {
5480
5853
  if (existsSync15(path)) {
5481
5854
  try {
5482
5855
  const b = JSON.parse(
5483
- readFileSync10(path, "utf8")
5856
+ readFileSync11(path, "utf8")
5484
5857
  );
5485
5858
  if (b.decompositionId && b.taskId)
5486
5859
  return { decompositionId: b.decompositionId, taskId: b.taskId };
@@ -5499,7 +5872,7 @@ function parseTranscript(path) {
5499
5872
  let tokensOut = 0;
5500
5873
  let contextUsed = 0;
5501
5874
  let model = "";
5502
- for (const line of readFileSync10(path, "utf8").split("\n")) {
5875
+ for (const line of readFileSync11(path, "utf8").split("\n")) {
5503
5876
  if (!line.trim()) continue;
5504
5877
  let obj;
5505
5878
  try {
@@ -5516,11 +5889,12 @@ function parseTranscript(path) {
5516
5889
  if (obj.message?.model) model = obj.message.model;
5517
5890
  }
5518
5891
  if (tokensIn === 0 && tokensOut === 0) return null;
5519
- return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model) };
5892
+ return { tokensIn, tokensOut, contextUsed, contextWindow: windowFor(model, contextUsed) };
5520
5893
  }
5521
- function windowFor(model) {
5894
+ function windowFor(model, contextUsed = 0) {
5522
5895
  const m = model.toLowerCase();
5523
- return m.includes("[1m]") || m.includes("-1m") ? 1e6 : 2e5;
5896
+ if (m.includes("[1m]") || m.includes("-1m")) return 1e6;
5897
+ return contextUsed > 2e5 ? 1e6 : 2e5;
5524
5898
  }
5525
5899
  async function readStdin2() {
5526
5900
  if (process.stdin.isTTY) return "";
@@ -5756,7 +6130,7 @@ Examples:
5756
6130
  function resolveVersion() {
5757
6131
  try {
5758
6132
  const pkg = JSON.parse(
5759
- readFileSync11(new URL("../package.json", import.meta.url), "utf8")
6133
+ readFileSync12(new URL("../package.json", import.meta.url), "utf8")
5760
6134
  );
5761
6135
  return pkg.version ?? "0.0.0";
5762
6136
  } catch {
@@ -5914,6 +6288,8 @@ registerLookup(program);
5914
6288
  registerRelationships(program);
5915
6289
  registerWorkspace(program);
5916
6290
  registerProject(program);
6291
+ registerDecomposition(program);
6292
+ registerClose(program);
5917
6293
  registerFiling(program);
5918
6294
  registerContinuity(program);
5919
6295
  registerCheckpoint(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.7.3",
3
+ "version": "2026.7.4-rc.ce2b31c6",
4
4
  "description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",