chainlesschain 0.162.36 → 0.162.38
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/package.json +3 -2
- package/src/assets/web-panel/assets/{AIOps-vAVAFNJ4.js → AIOps-DV0Q9zKL.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BnRHFCKM.js → ActionButton-C6vH8rhL.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BOjwqWqG.js → Analytics-BvPDc2ui.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-Dc0D1Txn.js → AppLayout-CWnyqTqY.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-dd_2efaZ.js → Audit-BzenidV4.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-HF1jgm8G.js → Backup-CSl7bNwK.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CCtzmoKe.js → BaseInput-DAY3iHIq.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BNfH1c3p.js → Chat-Jyhm9fgk.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DCWFqmI4.js → ChatBubbleRenderer-CwlAnVjy.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BOr-NscK.js → Checkbox-D4rwURAi.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DE058N7-.js → Codegen-DYdjTEfC.js} +1 -1
- package/src/assets/web-panel/assets/{Col-SOREo1XE.js → Col-DsVyZ_fS.js} +1 -1
- package/src/assets/web-panel/assets/{Community-sOvNZo9f.js → Community-CjCpl27Q.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DnBe558D.js → Compact-kt18dsjm.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-o-r6CUbg.js → Compliance-BV5urquU.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-D6_k9mHP.js → Cowork-C4SovPWC.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-CEV3Xkrm.js → Cron-uuNs_xzA.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-eJ1lQWKU.js → Crosschain-DR5a65tR.js} +1 -1
- package/src/assets/web-panel/assets/{DID-B-WqM9Hp.js → DID-B1KTf2-5.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-ZnKPcsHN.js → Dashboard-Dkj7XgED.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-B8uLWDIP.js → Dropdown-BhXCuJ19.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Jmj2Y7aH.js → EmailListRenderer-DG8365Iv.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Cb2xetG-.js → FamilyGuardDashboard-BdHGPu39.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C_07GXoq.js → Federation-Dwvxl0zR.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-D3kbYrMU.js → FormItemContext-BVmhCVWU.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-9xgqvGPg.js → GenericCardRenderer-DDPjvF2s.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BlwWlMMB.js → Git-foK6WTSr.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DxN3wQZ_.js → Governance-CfqMdu6Y.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-ls7pSw_D.js → Inference-BKrLO4GO.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-_n9hYuPI.js → KnowledgeGraph-6o6Q-mmF.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CvEVY5TK.js → Logs-L5ZIW0Dz.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-C3qvQJT7.js → Marketplace-BWkfEocP.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DiwKpnKx.js → McpTools-BPebQbWU.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-CIBPi_da.js → Memory-C0Dq-X3C.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-D-v0Se8y.js → MobileBridge-DRBoutTY.js} +2 -2
- package/src/assets/web-panel/assets/{MobileProjects-cP1apTQD.js → MobileProjects-BMP6eLp1.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BMFWrI65.js → Mtc-Cj3QPM9p.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-2s8LaHtR.js → MtcAudit-rBQYbfQR.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-dL_nvj7d.js → Multisig-Dbuy4OY4.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BbrJp06R.js → NLProgramming-CMnt1se-.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-jR9irwy3.js → Notes-BX9tSCiF.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Dk-STCIX.js → NotificationSettings-BFeirVRq.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CqqfY6zq.js → OrderTableRenderer-ybiMlKQW.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BCK5jylo.js → Organization-kTfRxKqk.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BRAY7Smt.js → Overflow-CtuCAzwV.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BltVRGjb.js → P2P-KfbciaP3.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CV8UbXHe.js → PdhVaultBrowser-bqEUFhgC.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-_tNl47Qh.js → Permissions-BgMypz-z.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Cgc4HjpX.js → PersonalDataHub-C3zUE-1z.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-Bn_QU4mu.js → Pipeline-iX-pYHpC.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-jzJowp5P.js → Privacy-B01uzeFM.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B_1pJ8qd.js → ProjectInit-TsfbzJp7.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CPVZpXzs.js → ProjectSettings-iGvMp8sM.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CQsHOWnT.js → Projects-Be9k29iQ.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CzzMiLC0.js → Providers-C9Pc8dqo.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-MxBKIn9o.js → QuickAsk-DN_yFiVO.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-D8lN6Lis.js → Recommend-CvSNgl7H.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CfYK-IrV.js → Reputation-S6BCz8xH.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Bg7NZDP9.js → Row-CTRYCaqP.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BOVNJhj0.js → RssFeed-Cu8_P5ll.js} +3 -3
- package/src/assets/web-panel/assets/{Search-B38qzmhY.js → Search-rZ1Xza_U.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CjqleZpe.js → Security-CF43IJHX.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Bu9JSJap.js → Services-BobNHzne.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B2RvRkaX.js → Skeleton-DWJ2kfuI.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-_h42mxMN.js → Skills-AmEZgHYr.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BssLs56D.js → Sla-DTS-fBiY.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DCxFYHsd.js → SpeechSettings-DEr6MHRU.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D2xQuNLE.js → SyncSettings-CVs9alv_.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DhpOGOlo.js → Tasks-BcVDAxdi.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CYG-R-aS.js → Templates-CTNjZRKA.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BQRYLsvP.js → Tenant-DPbXg0Pg.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-imKU7N5j.js → Terminal-DhKXcPw2.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BIZzBftk.js → TimelineRenderer-B0DMZOpk.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-uMLH5p_a.js → Tokens-RvWuBXgg.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BzS6XPqx.js → Trigger-2O-BaTQG.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-R4zhHufZ.js → Trust-6qY35L-C.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DATQCoGe.js → UkeySign-DhV1wYtQ.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-ClUmKOtS.js → VideoEditing-DgqA5UZm.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DzJTbQzD.js → Wallet-DJRYdUAK.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CrXrLmzQ.js → WebAuthn-C2W-x0cg.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-CpvZ0Tma.js → WorkflowEditor-BP2tkDHe.js} +1 -1
- package/src/assets/web-panel/assets/{chat-a6wpYmVL.js → chat-CGVfeoTn.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CXJADb1t.js → colors-BmjRolM1.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CL2pohS_.js → compact-item-BvJJkjZE.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-xFi_1G5_.js → createContext-DyhlvRYs.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CetO0WH0.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Bchh1rAi.js → hasIn-BoBMR89s.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2eMYASq.js → index-39VDXdn6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CR3kFPuC.js → index-81tWFqfN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTRd7vkq.js → index-BT1SQ9nj.js} +1 -1
- package/src/assets/web-panel/assets/index-BZVz-WfV.js +1 -0
- package/src/assets/web-panel/assets/{index-D-TT9Swq.js → index-Beh7jDbS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrbJBnT-.js → index-Bm_MmdwP.js} +1 -1
- package/src/assets/web-panel/assets/{index-dsLc7t6W.js → index-BqGNmoKy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DEYcLAl7.js → index-BuQrONgf.js} +1 -1
- package/src/assets/web-panel/assets/{index-KCib1PTw.js → index-BvvNnWXe.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxahxRP7.js → index-ByWpNjTj.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6NehWty.js → index-BycpeGfj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTEu7TSF.js → index-C0xn6hOr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BH9t10pe.js → index-C1t-r7yV.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7wT5VRi.js → index-CDPMHKQi.js} +1 -1
- package/src/assets/web-panel/assets/{index-majCS3s2.js → index-CIaGw7vl.js} +1 -1
- package/src/assets/web-panel/assets/{index-EPERz4Pu.js → index-CQJVedQ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-IkvkNxbc.js → index-CSgbOGaP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQ_hw_5P.js → index-Cbh-lCxq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CMybtJY6.js → index-CzDVBBcg.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4zNisy9.js → index-Czsbrn75.js} +1 -1
- package/src/assets/web-panel/assets/{index-M8SZI11a.js → index-D-93XwJd.js} +1 -1
- package/src/assets/web-panel/assets/index-D0-bvFy3.js +1 -0
- package/src/assets/web-panel/assets/{index-TxbHusq2.js → index-D0YToIi_.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7knYOpm.js → index-DIPZ6hbJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGq4HQno.js → index-DeeLHcMY.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdU8BwRW.js → index-DgbWSwr5.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4yBRKT4.js → index-DtKdCXHW.js} +1 -1
- package/src/assets/web-panel/assets/{index-jMcv1u5o.js → index-DwTgvhOL.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3Tpv7-d.js → index-DyS4I4L-.js} +1 -1
- package/src/assets/web-panel/assets/{index-u8K1y_lh.js → index-FKFT-QTk.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cua_P8St.js → index-Te0ruvY_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVo1GJoj.js → index-VXVukhBA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPH5ESqs.js → index-Y1b8i0NV.js} +3 -3
- package/src/assets/web-panel/assets/{index-DsbMVBj1.js → index-ZNIms1nA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoaRB-4a.js → index-n-N19np-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmsIKzyu.js → index-vF1pR00A.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuehgDOp.js → index-wLAjVpmJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjdOL159.js → index-xPSzUoWT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BF4xx1_b.js → index-xZdOioVg.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DYn3Gc09.js → initDefaultProps-BLKSE8he.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ZS3eolb9.js → motion-Bb59qqLK.js} +1 -1
- package/src/assets/web-panel/assets/{move-CEw4uqr3.js → move-CB3pYCk6.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DlHFZnPp.js → omit-iImQWuU7.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-eZQvV5fA.js → pickAttrs-DRP2Chqo.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B31jQwa-.js → placementArrow-BrlfD4tF.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DAsNmVto.js → responsiveObserve-Cqxkuh5H.js} +1 -1
- package/src/assets/web-panel/assets/{slide-gPQPrYZC.js → slide-nxKEuLMj.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DwWKX5co.js → statusUtils-30E47KSk.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B3VOtXuH.js → styleChecker-Dn2_-5bn.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-6ADctM2r.js → useFlexGapSupport-DkZ00X6F.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-6Zx1SSKs.js → useFs-ByrwSCOr.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BzReowln.js → usePersonalDataHub-BDY6jtUD.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C8IpEQbD.js → vnode-BL2q5BLv.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-ruc9vHr0.js → zoom-BSkPKE42.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +31 -0
- package/src/commands/cli-anything.js +14 -6
- package/src/commands/loop.js +450 -0
- package/src/commands/mcp.js +236 -6
- package/src/harness/mcp-client.js +70 -1
- package/src/index.js +2 -0
- package/src/lib/loop.js +198 -0
- package/src/lib/settings-hooks.cjs +1 -0
- package/src/repl/agent-repl.js +57 -20
- package/src/repl/mcp-prompt.js +122 -0
- package/src/runtime/agent-core.js +123 -17
- package/src/runtime/headless-runner.js +34 -9
- package/src/runtime/mcp-config.js +118 -9
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/assets/web-panel/assets/devWarning-BtmELbtB.js +0 -1
- package/src/assets/web-panel/assets/index-B4l4vLTB.js +0 -1
- package/src/assets/web-panel/assets/index-B7Ek5iiY.js +0 -1
package/src/commands/mcp.js
CHANGED
|
@@ -38,6 +38,49 @@ function getClient() {
|
|
|
38
38
|
return mcpClient;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Connect MCP server(s) for a one-shot query (resources / prompts). Connects
|
|
43
|
+
* the named server, or every registered server when `serverName` is omitted.
|
|
44
|
+
* Returns `{ client, connected }`; the caller must `await shutdown()`.
|
|
45
|
+
*/
|
|
46
|
+
async function connectForQuery(program, serverName) {
|
|
47
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
48
|
+
if (!ctx.db) {
|
|
49
|
+
logger.error("Database not available");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const db = ctx.db.getDatabase();
|
|
53
|
+
const config = new MCPServerConfig(db);
|
|
54
|
+
let rows;
|
|
55
|
+
if (serverName) {
|
|
56
|
+
const row = config.get(serverName);
|
|
57
|
+
if (!row) {
|
|
58
|
+
logger.error(
|
|
59
|
+
`Server "${serverName}" not configured. Use 'mcp add' first.`,
|
|
60
|
+
);
|
|
61
|
+
await shutdown();
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
rows = [row];
|
|
65
|
+
} else {
|
|
66
|
+
rows = config.list();
|
|
67
|
+
}
|
|
68
|
+
const client = new MCPClient();
|
|
69
|
+
const connected = [];
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
if (!row) continue;
|
|
72
|
+
try {
|
|
73
|
+
await client.connect(row.name, row);
|
|
74
|
+
connected.push(row.name);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.log(
|
|
77
|
+
chalk.yellow(` Failed to connect "${row.name}": ${err.message}`),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { client, connected };
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
// Phase 3 (Hosted MCP Policy): resolve runtime mode from --mode > env > local.
|
|
42
85
|
function resolveMode(options) {
|
|
43
86
|
return (
|
|
@@ -68,10 +111,20 @@ export function registerMcpCommand(program) {
|
|
|
68
111
|
// mcp login — OAuth 2.0 (Auth Code + PKCE) for a remote MCP server.
|
|
69
112
|
mcp
|
|
70
113
|
.command("login <url>")
|
|
71
|
-
.description(
|
|
114
|
+
.description(
|
|
115
|
+
"Authorize a remote MCP server via OAuth (opens a browser); stores the token",
|
|
116
|
+
)
|
|
72
117
|
.option("--scope <scope>", "OAuth scope(s) to request")
|
|
73
|
-
.option(
|
|
74
|
-
|
|
118
|
+
.option(
|
|
119
|
+
"--client-id <id>",
|
|
120
|
+
"Use a pre-registered client_id instead of dynamic registration",
|
|
121
|
+
)
|
|
122
|
+
.option(
|
|
123
|
+
"--port <n>",
|
|
124
|
+
"Localhost callback port",
|
|
125
|
+
(v) => parseInt(v, 10),
|
|
126
|
+
53682,
|
|
127
|
+
)
|
|
75
128
|
.option("--no-open", "Print the authorize URL instead of opening a browser")
|
|
76
129
|
.action(async (url, options) => {
|
|
77
130
|
try {
|
|
@@ -109,7 +162,8 @@ export function registerMcpCommand(program) {
|
|
|
109
162
|
.description("Delete the stored OAuth token for a remote MCP server")
|
|
110
163
|
.action(async (url) => {
|
|
111
164
|
try {
|
|
112
|
-
const { deleteStoredToken, serverKey } =
|
|
165
|
+
const { deleteStoredToken, serverKey } =
|
|
166
|
+
await import("../lib/mcp-oauth.js");
|
|
113
167
|
const ok = deleteStoredToken(url);
|
|
114
168
|
logger.log(
|
|
115
169
|
ok
|
|
@@ -129,7 +183,8 @@ export function registerMcpCommand(program) {
|
|
|
129
183
|
.option("--json", "Output as JSON")
|
|
130
184
|
.action(async (options) => {
|
|
131
185
|
try {
|
|
132
|
-
const { loadTokenStore, isTokenExpired } =
|
|
186
|
+
const { loadTokenStore, isTokenExpired } =
|
|
187
|
+
await import("../lib/mcp-oauth.js");
|
|
133
188
|
const store = loadTokenStore();
|
|
134
189
|
const rows = Object.values(store).map((r) => ({
|
|
135
190
|
server: r.server,
|
|
@@ -142,7 +197,9 @@ export function registerMcpCommand(program) {
|
|
|
142
197
|
return;
|
|
143
198
|
}
|
|
144
199
|
if (rows.length === 0) {
|
|
145
|
-
logger.log(
|
|
200
|
+
logger.log(
|
|
201
|
+
chalk.gray("No MCP OAuth tokens. Run: cc mcp login <url>"),
|
|
202
|
+
);
|
|
146
203
|
return;
|
|
147
204
|
}
|
|
148
205
|
for (const r of rows) {
|
|
@@ -566,6 +623,179 @@ export function registerMcpCommand(program) {
|
|
|
566
623
|
}
|
|
567
624
|
});
|
|
568
625
|
|
|
626
|
+
// mcp resources — list resources exposed by configured servers
|
|
627
|
+
mcp
|
|
628
|
+
.command("resources")
|
|
629
|
+
.description("List resources exposed by MCP servers")
|
|
630
|
+
.option("-s, --server <name>", "Filter by / connect only this server")
|
|
631
|
+
.option("--json", "Output as JSON")
|
|
632
|
+
.action(async (options) => {
|
|
633
|
+
try {
|
|
634
|
+
const { client } = await connectForQuery(program, options.server);
|
|
635
|
+
const resources = client.listResources(options.server);
|
|
636
|
+
if (options.json) {
|
|
637
|
+
console.log(JSON.stringify(resources, null, 2));
|
|
638
|
+
} else if (resources.length === 0) {
|
|
639
|
+
logger.info("No resources available.");
|
|
640
|
+
} else {
|
|
641
|
+
logger.log(chalk.bold(`MCP Resources (${resources.length}):\n`));
|
|
642
|
+
for (const r of resources) {
|
|
643
|
+
logger.log(` ${chalk.cyan(r.uri)} ${chalk.gray(`[${r.server}]`)}`);
|
|
644
|
+
if (r.name) logger.log(` ${chalk.gray(r.name)}`);
|
|
645
|
+
if (r.description) logger.log(` ${chalk.gray(r.description)}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
await client.disconnectAll();
|
|
649
|
+
await shutdown();
|
|
650
|
+
} catch (err) {
|
|
651
|
+
logger.error(`Failed: ${err.message}`);
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// mcp read-resource — read a resource's contents by URI
|
|
657
|
+
mcp
|
|
658
|
+
.command("read-resource")
|
|
659
|
+
.description("Read an MCP resource by URI")
|
|
660
|
+
.argument("<uri>", "Resource URI")
|
|
661
|
+
.option("-s, --server <name>", "Server that owns the resource")
|
|
662
|
+
.option("--json", "Output as JSON")
|
|
663
|
+
.action(async (uri, options) => {
|
|
664
|
+
try {
|
|
665
|
+
const { client } = await connectForQuery(program, options.server);
|
|
666
|
+
let server = options.server;
|
|
667
|
+
if (!server) {
|
|
668
|
+
const match = client.listResources().find((r) => r.uri === uri);
|
|
669
|
+
if (!match) {
|
|
670
|
+
logger.error(
|
|
671
|
+
`Resource "${uri}" not found. Run 'mcp resources' to list URIs.`,
|
|
672
|
+
);
|
|
673
|
+
await client.disconnectAll();
|
|
674
|
+
await shutdown();
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
server = match.server;
|
|
678
|
+
}
|
|
679
|
+
const result = await client.readResource(server, uri);
|
|
680
|
+
if (options.json) {
|
|
681
|
+
console.log(JSON.stringify(result, null, 2));
|
|
682
|
+
} else if (Array.isArray(result?.contents)) {
|
|
683
|
+
for (const c of result.contents) {
|
|
684
|
+
if (typeof c.text === "string") {
|
|
685
|
+
logger.log(c.text);
|
|
686
|
+
} else if (c.blob) {
|
|
687
|
+
logger.log(
|
|
688
|
+
chalk.gray(
|
|
689
|
+
`[Binary: ${c.mimeType || "application/octet-stream"}]`,
|
|
690
|
+
),
|
|
691
|
+
);
|
|
692
|
+
} else {
|
|
693
|
+
logger.log(JSON.stringify(c, null, 2));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
logger.log(JSON.stringify(result, null, 2));
|
|
698
|
+
}
|
|
699
|
+
await client.disconnectAll();
|
|
700
|
+
await shutdown();
|
|
701
|
+
} catch (err) {
|
|
702
|
+
logger.error(`Read failed: ${err.message}`);
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// mcp prompts — list prompts (server-provided slash commands)
|
|
708
|
+
mcp
|
|
709
|
+
.command("prompts")
|
|
710
|
+
.description("List prompts exposed by MCP servers")
|
|
711
|
+
.option("-s, --server <name>", "Filter by / connect only this server")
|
|
712
|
+
.option("--json", "Output as JSON")
|
|
713
|
+
.action(async (options) => {
|
|
714
|
+
try {
|
|
715
|
+
const { client } = await connectForQuery(program, options.server);
|
|
716
|
+
const prompts = client.listPrompts(options.server);
|
|
717
|
+
if (options.json) {
|
|
718
|
+
console.log(JSON.stringify(prompts, null, 2));
|
|
719
|
+
} else if (prompts.length === 0) {
|
|
720
|
+
logger.info("No prompts available.");
|
|
721
|
+
} else {
|
|
722
|
+
logger.log(chalk.bold(`MCP Prompts (${prompts.length}):\n`));
|
|
723
|
+
for (const p of prompts) {
|
|
724
|
+
logger.log(
|
|
725
|
+
` ${chalk.cyan(`/mcp__${p.server}__${p.name}`)} ${chalk.gray(`[${p.server}]`)}`,
|
|
726
|
+
);
|
|
727
|
+
if (p.description) logger.log(` ${chalk.gray(p.description)}`);
|
|
728
|
+
if (Array.isArray(p.arguments) && p.arguments.length > 0) {
|
|
729
|
+
const argNames = p.arguments
|
|
730
|
+
.map((a) => (a.required ? `${a.name}*` : a.name))
|
|
731
|
+
.join(", ");
|
|
732
|
+
logger.log(` ${chalk.gray(`args: ${argNames}`)}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
await client.disconnectAll();
|
|
737
|
+
await shutdown();
|
|
738
|
+
} catch (err) {
|
|
739
|
+
logger.error(`Failed: ${err.message}`);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// mcp get-prompt — fetch a rendered prompt by name
|
|
745
|
+
mcp
|
|
746
|
+
.command("get-prompt")
|
|
747
|
+
.description("Fetch a rendered MCP prompt by name")
|
|
748
|
+
.argument("<name>", "Prompt name")
|
|
749
|
+
.option("-s, --server <name>", "Server that owns the prompt")
|
|
750
|
+
.option("-a, --args <json>", "Prompt arguments as JSON")
|
|
751
|
+
.option("--json", "Output as JSON")
|
|
752
|
+
.action(async (name, options) => {
|
|
753
|
+
try {
|
|
754
|
+
const { client } = await connectForQuery(program, options.server);
|
|
755
|
+
let server = options.server;
|
|
756
|
+
if (!server) {
|
|
757
|
+
const match = client.listPrompts().find((p) => p.name === name);
|
|
758
|
+
if (!match) {
|
|
759
|
+
logger.error(
|
|
760
|
+
`Prompt "${name}" not found. Run 'mcp prompts' to list prompts.`,
|
|
761
|
+
);
|
|
762
|
+
await client.disconnectAll();
|
|
763
|
+
await shutdown();
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
server = match.server;
|
|
767
|
+
}
|
|
768
|
+
const args = options.args ? JSON.parse(options.args) : {};
|
|
769
|
+
const result = await client.getPrompt(server, name, args);
|
|
770
|
+
if (options.json) {
|
|
771
|
+
console.log(JSON.stringify(result, null, 2));
|
|
772
|
+
} else {
|
|
773
|
+
if (result?.description) {
|
|
774
|
+
logger.log(chalk.gray(result.description) + "\n");
|
|
775
|
+
}
|
|
776
|
+
for (const msg of result?.messages || []) {
|
|
777
|
+
const blocks = Array.isArray(msg.content)
|
|
778
|
+
? msg.content
|
|
779
|
+
: [msg.content];
|
|
780
|
+
for (const b of blocks) {
|
|
781
|
+
if (b && b.type === "text") {
|
|
782
|
+
logger.log(`${chalk.gray(`[${msg.role}]`)} ${b.text}`);
|
|
783
|
+
} else {
|
|
784
|
+
logger.log(
|
|
785
|
+
`${chalk.gray(`[${msg.role}]`)} ${JSON.stringify(b)}`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
await client.disconnectAll();
|
|
792
|
+
await shutdown();
|
|
793
|
+
} catch (err) {
|
|
794
|
+
logger.error(`Get prompt failed: ${err.message}`);
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
569
799
|
// mcp scaffold — generate a boilerplate MCP server project
|
|
570
800
|
mcp
|
|
571
801
|
.command("scaffold <name>")
|
|
@@ -156,7 +156,7 @@ export class MCPClient extends EventEmitter {
|
|
|
156
156
|
// Initialize MCP protocol
|
|
157
157
|
const initResult = await this._sendRequest(name, "initialize", {
|
|
158
158
|
protocolVersion: "2024-11-05",
|
|
159
|
-
capabilities: { tools: {}, resources: {} },
|
|
159
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
160
160
|
clientInfo: { name: "chainlesschain-cli", version: "0.37.9" },
|
|
161
161
|
});
|
|
162
162
|
|
|
@@ -187,12 +187,21 @@ export class MCPClient extends EventEmitter {
|
|
|
187
187
|
// Server may not support resources
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
// Fetch available prompts (server-provided slash commands)
|
|
191
|
+
try {
|
|
192
|
+
const promptsResult = await this._sendRequest(name, "prompts/list", {});
|
|
193
|
+
entry.prompts = promptsResult?.prompts || [];
|
|
194
|
+
} catch {
|
|
195
|
+
// Server may not support prompts
|
|
196
|
+
}
|
|
197
|
+
|
|
190
198
|
this.emit("server-connected", { name, tools: entry.tools.length });
|
|
191
199
|
return {
|
|
192
200
|
name,
|
|
193
201
|
state: entry.state,
|
|
194
202
|
tools: entry.tools,
|
|
195
203
|
resources: entry.resources,
|
|
204
|
+
prompts: entry.prompts,
|
|
196
205
|
serverInfo: entry.serverInfo,
|
|
197
206
|
};
|
|
198
207
|
} catch (err) {
|
|
@@ -250,6 +259,7 @@ export class MCPClient extends EventEmitter {
|
|
|
250
259
|
state: entry.state,
|
|
251
260
|
tools: entry.tools.length,
|
|
252
261
|
resources: entry.resources.length,
|
|
262
|
+
prompts: (entry.prompts || []).length,
|
|
253
263
|
serverInfo: entry.serverInfo || {},
|
|
254
264
|
});
|
|
255
265
|
}
|
|
@@ -296,6 +306,26 @@ export class MCPClient extends EventEmitter {
|
|
|
296
306
|
return result;
|
|
297
307
|
}
|
|
298
308
|
|
|
309
|
+
/**
|
|
310
|
+
* List resources from a specific server or all servers. Each resource is
|
|
311
|
+
* annotated with its owning `server` (mirrors `listTools`).
|
|
312
|
+
*/
|
|
313
|
+
listResources(serverName) {
|
|
314
|
+
if (serverName) {
|
|
315
|
+
const entry = this.servers.get(serverName);
|
|
316
|
+
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
317
|
+
return (entry.resources || []).map((r) => ({ ...r, server: serverName }));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const all = [];
|
|
321
|
+
for (const [name, entry] of this.servers) {
|
|
322
|
+
for (const r of entry.resources || []) {
|
|
323
|
+
all.push({ ...r, server: name });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return all;
|
|
327
|
+
}
|
|
328
|
+
|
|
299
329
|
/**
|
|
300
330
|
* Read a resource from a server.
|
|
301
331
|
*/
|
|
@@ -309,6 +339,45 @@ export class MCPClient extends EventEmitter {
|
|
|
309
339
|
return result;
|
|
310
340
|
}
|
|
311
341
|
|
|
342
|
+
/**
|
|
343
|
+
* List prompts from a specific server or all servers. Each prompt is
|
|
344
|
+
* annotated with its owning `server` (mirrors `listTools`).
|
|
345
|
+
*/
|
|
346
|
+
listPrompts(serverName) {
|
|
347
|
+
if (serverName) {
|
|
348
|
+
const entry = this.servers.get(serverName);
|
|
349
|
+
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
350
|
+
return (entry.prompts || []).map((p) => ({ ...p, server: serverName }));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const all = [];
|
|
354
|
+
for (const [name, entry] of this.servers) {
|
|
355
|
+
for (const p of entry.prompts || []) {
|
|
356
|
+
all.push({ ...p, server: name });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return all;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Fetch a rendered prompt (`prompts/get`) from a server. `args` is a map of
|
|
364
|
+
* the prompt's named arguments to string values. Returns the server's result
|
|
365
|
+
* `{ description?, messages: [...] }`.
|
|
366
|
+
*/
|
|
367
|
+
async getPrompt(serverName, promptName, args = {}) {
|
|
368
|
+
const entry = this.servers.get(serverName);
|
|
369
|
+
if (!entry) throw new Error(`Server "${serverName}" not found`);
|
|
370
|
+
if (entry.state !== ServerState.CONNECTED) {
|
|
371
|
+
throw new Error(`Server "${serverName}" is not connected`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const result = await this._sendRequest(serverName, "prompts/get", {
|
|
375
|
+
name: promptName,
|
|
376
|
+
arguments: args || {},
|
|
377
|
+
});
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
312
381
|
// ─── Internal JSON-RPC transport ──────────────────────────────
|
|
313
382
|
|
|
314
383
|
_sendRequest(serverName, method, params) {
|
package/src/index.js
CHANGED
|
@@ -62,6 +62,7 @@ import { registerCheckpointCommand } from "./commands/checkpoint.js";
|
|
|
62
62
|
import { registerGoalCommand } from "./commands/goal.js";
|
|
63
63
|
import { registerCommandCommand } from "./commands/command.js";
|
|
64
64
|
import { registerCompactCommand } from "./commands/compact.js";
|
|
65
|
+
import { registerLoopCommand } from "./commands/loop.js";
|
|
65
66
|
import { registerPermissionsCommand } from "./commands/permissions.js";
|
|
66
67
|
import { registerOutputStyleCommand } from "./commands/output-style.js";
|
|
67
68
|
import { registerStatuslineCommand } from "./commands/statusline.js";
|
|
@@ -464,6 +465,7 @@ export function createProgram(opts = {}) {
|
|
|
464
465
|
registerGoalCommand(program);
|
|
465
466
|
registerCommandCommand(program);
|
|
466
467
|
registerCompactCommand(program);
|
|
468
|
+
registerLoopCommand(program);
|
|
467
469
|
registerPermissionsCommand(program);
|
|
468
470
|
registerOutputStyleCommand(program);
|
|
469
471
|
registerStatuslineCommand(program);
|
package/src/lib/loop.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc loop — core driver (pure, dependency-injected) for the fixed-interval
|
|
3
|
+
* loop runner. The command layer (src/commands/loop.js) supplies a concrete
|
|
4
|
+
* `runIteration` (spawns a child process) plus a real clock; everything here
|
|
5
|
+
* is side-effect-free and clock-injected so the loop semantics — iteration
|
|
6
|
+
* counting, stop conditions, between-run delay — are deterministically
|
|
7
|
+
* testable without timers or subprocesses.
|
|
8
|
+
*
|
|
9
|
+
* Claude-Code `/loop` parity (fixed-interval MVP): run a command or agent
|
|
10
|
+
* prompt repeatedly until a stop condition fires (max iterations / exit 0 /
|
|
11
|
+
* output match) or the caller aborts (Ctrl-C).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Multipliers for the duration suffixes we accept. */
|
|
15
|
+
const DURATION_UNITS = { ms: 1, s: 1000, m: 60000, h: 3600000 };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a human interval ("30s", "5m", "1.5h", "500ms") into milliseconds.
|
|
19
|
+
* A bare number is interpreted as SECONDS (the natural unit for an interval),
|
|
20
|
+
* so `--every 30` === `--every 30s`. Throws on anything unparseable.
|
|
21
|
+
*/
|
|
22
|
+
export function parseDuration(input) {
|
|
23
|
+
if (typeof input === "number" && Number.isFinite(input)) {
|
|
24
|
+
return Math.max(0, Math.round(input));
|
|
25
|
+
}
|
|
26
|
+
const s = String(input ?? "")
|
|
27
|
+
.trim()
|
|
28
|
+
.toLowerCase();
|
|
29
|
+
const m = s.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/);
|
|
30
|
+
if (!m) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`invalid duration: "${input}" (use 30s, 5m, 1.5h, or 500ms)`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const value = parseFloat(m[1]);
|
|
36
|
+
const unit = m[2] || "s"; // bare number → seconds
|
|
37
|
+
return Math.round(value * DURATION_UNITS[unit]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Render a millisecond duration back to a compact human string. */
|
|
41
|
+
export function formatDuration(ms) {
|
|
42
|
+
if (ms < 1000) return `${ms}ms`;
|
|
43
|
+
if (ms < 60000) return `${trim(ms / 1000)}s`;
|
|
44
|
+
if (ms < 3600000) return `${trim(ms / 60000)}m`;
|
|
45
|
+
return `${trim(ms / 3600000)}h`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function trim(n) {
|
|
49
|
+
// Strip trailing ".0" so 5.0 → "5" but 1.5 stays "1.5".
|
|
50
|
+
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/\.?0+$/, "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse the `--dynamic` control directives an iteration may print so it can
|
|
55
|
+
* self-pace. An iteration ends its output with at most one of:
|
|
56
|
+
* [[loop:next <interval>]] schedule the next run after <interval>
|
|
57
|
+
* [[loop:stop]] the task is done; stop looping
|
|
58
|
+
* Returns { done, nextDelayMs }. `stop` wins over `next` (done short-circuits
|
|
59
|
+
* before the next sleep). A malformed interval is ignored (falls back to the
|
|
60
|
+
* fixed `--every`). Lives here so the protocol is unit-testable in isolation.
|
|
61
|
+
*/
|
|
62
|
+
export function parseLoopDirectives(output) {
|
|
63
|
+
const text = String(output || "");
|
|
64
|
+
const result = { done: false, nextDelayMs: null };
|
|
65
|
+
if (/\[\[\s*loop:stop\s*\]\]/i.test(text)) result.done = true;
|
|
66
|
+
const m = text.match(/\[\[\s*loop:next\s+([0-9.]+\s*(?:ms|s|m|h)?)\s*\]\]/i);
|
|
67
|
+
if (m) {
|
|
68
|
+
try {
|
|
69
|
+
result.nextDelayMs = parseDuration(m[1]);
|
|
70
|
+
} catch {
|
|
71
|
+
/* malformed interval → leave null, caller falls back to --every */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reduce a persisted loop session's events into the state needed to resume it:
|
|
79
|
+
* the original `loop_config`, how many iterations already completed, and the
|
|
80
|
+
* last recorded exit code. Pure (operates on the event array, no fs) so the
|
|
81
|
+
* resume reconstruction is unit-testable without the session store.
|
|
82
|
+
*/
|
|
83
|
+
export function summarizeLoopEvents(events) {
|
|
84
|
+
let config = null;
|
|
85
|
+
let completedIterations = 0;
|
|
86
|
+
let lastExitCode = null;
|
|
87
|
+
for (const e of events || []) {
|
|
88
|
+
if (e?.type === "loop_config") {
|
|
89
|
+
config = e.data || null;
|
|
90
|
+
} else if (e?.type === "loop_iteration") {
|
|
91
|
+
completedIterations += 1;
|
|
92
|
+
if (e.data && typeof e.data.exitCode !== "undefined") {
|
|
93
|
+
lastExitCode = e.data.exitCode;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { config, completedIterations, lastExitCode };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Default abortable sleep — resolves early if the signal aborts. */
|
|
101
|
+
export function makeSleep(signal) {
|
|
102
|
+
return (ms) =>
|
|
103
|
+
new Promise((resolve) => {
|
|
104
|
+
if (signal?.aborted || ms <= 0) return resolve();
|
|
105
|
+
// NB: do NOT unref() — the pending interval timer is what keeps the
|
|
106
|
+
// process alive between rounds. Under a TTY the active stdin would mask
|
|
107
|
+
// an unref'd timer, but headless (piped stdin / CI / cron) the loop would
|
|
108
|
+
// exit after the first round. SIGINT aborts the wait via the signal.
|
|
109
|
+
const t = setTimeout(resolve, ms);
|
|
110
|
+
signal?.addEventListener(
|
|
111
|
+
"abort",
|
|
112
|
+
() => {
|
|
113
|
+
clearTimeout(t);
|
|
114
|
+
resolve();
|
|
115
|
+
},
|
|
116
|
+
{ once: true },
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Drive the loop. Calls `runIteration(n)` once per round (1-based), evaluates
|
|
123
|
+
* the stop conditions AFTER each round, and sleeps `intervalMs` between rounds
|
|
124
|
+
* (never after the final round). Returns a summary describing why it stopped.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} opts
|
|
127
|
+
* @param {(n:number)=>Promise<{exitCode?:number, output?:string}>} opts.runIteration
|
|
128
|
+
* @param {number} opts.intervalMs default delay between iterations (>= 0)
|
|
129
|
+
* @param {number} [opts.maxIterations] stop after N rounds (>= 1)
|
|
130
|
+
* @param {boolean} [opts.untilExitZero] stop once a round exits with code 0
|
|
131
|
+
* @param {RegExp} [opts.untilRegex] stop once a round's output matches
|
|
132
|
+
* @param {(ms:number)=>Promise<void>} [opts.sleep] injectable delay
|
|
133
|
+
* @param {()=>boolean} [opts.shouldStop] external stop probe (e.g. SIGINT)
|
|
134
|
+
* @param {(n:number, res:object)=>void} [opts.onIteration] per-round hook
|
|
135
|
+
* @param {number} [opts.startIndex] iterations already done (resume)
|
|
136
|
+
* @returns {Promise<{iterations:number, stoppedBy:string, results:object[]}>}
|
|
137
|
+
* `iterations` is cumulative (startIndex + rounds run this call);
|
|
138
|
+
* `results` holds only this call's rounds.
|
|
139
|
+
*/
|
|
140
|
+
export async function runLoop({
|
|
141
|
+
runIteration,
|
|
142
|
+
intervalMs,
|
|
143
|
+
maxIterations,
|
|
144
|
+
untilExitZero = false,
|
|
145
|
+
untilRegex = null,
|
|
146
|
+
sleep,
|
|
147
|
+
shouldStop,
|
|
148
|
+
onIteration,
|
|
149
|
+
startIndex = 0,
|
|
150
|
+
}) {
|
|
151
|
+
if (typeof runIteration !== "function") {
|
|
152
|
+
throw new Error("runLoop requires a runIteration function");
|
|
153
|
+
}
|
|
154
|
+
const delay = sleep || makeSleep();
|
|
155
|
+
const results = [];
|
|
156
|
+
// Iterations already completed in a prior (resumed) run. `i` continues from
|
|
157
|
+
// here so the displayed/persisted round numbers are cumulative and
|
|
158
|
+
// `maxIterations` counts across resume.
|
|
159
|
+
let i = startIndex;
|
|
160
|
+
|
|
161
|
+
while (true) {
|
|
162
|
+
if (shouldStop && shouldStop()) {
|
|
163
|
+
return { iterations: i, stoppedBy: "signal", results };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
i += 1;
|
|
167
|
+
const res = (await runIteration(i)) || {};
|
|
168
|
+
results.push(res);
|
|
169
|
+
if (onIteration) onIteration(i, res);
|
|
170
|
+
|
|
171
|
+
// Stop conditions, most-specific first. Evaluated after the round so the
|
|
172
|
+
// work always runs at least once before any condition can end the loop.
|
|
173
|
+
// `res.done` is the iteration's own explicit stop (e.g. a --dynamic
|
|
174
|
+
// [[loop:stop]] directive) and wins over everything else.
|
|
175
|
+
if (res.done) {
|
|
176
|
+
return { iterations: i, stoppedBy: "done", results };
|
|
177
|
+
}
|
|
178
|
+
if (untilExitZero && res.exitCode === 0) {
|
|
179
|
+
return { iterations: i, stoppedBy: "exit-zero", results };
|
|
180
|
+
}
|
|
181
|
+
if (untilRegex && untilRegex.test(res.output || "")) {
|
|
182
|
+
return { iterations: i, stoppedBy: "match", results };
|
|
183
|
+
}
|
|
184
|
+
if (maxIterations && i >= maxIterations) {
|
|
185
|
+
return { iterations: i, stoppedBy: "max-iterations", results };
|
|
186
|
+
}
|
|
187
|
+
if (shouldStop && shouldStop()) {
|
|
188
|
+
return { iterations: i, stoppedBy: "signal", results };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// An iteration may set its own next interval (--dynamic [[loop:next]]);
|
|
192
|
+
// otherwise fall back to the fixed --every delay.
|
|
193
|
+
const nextMs = Number.isFinite(res.nextDelayMs)
|
|
194
|
+
? res.nextDelayMs
|
|
195
|
+
: intervalMs;
|
|
196
|
+
await delay(nextMs);
|
|
197
|
+
}
|
|
198
|
+
}
|