@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.
- package/dist/index.js +411 -35
- 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
|
|
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
|
|
1548
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
|
1741
|
-
|
|
1742
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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").
|
|
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
|
|
4719
|
-
|
|
4720
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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