chainlesschain 0.162.40 → 0.162.41
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 +1 -1
- package/src/assets/web-panel/assets/{AIOps-CPmKv82o.js → AIOps-Ut7EevnG.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BNDYY7Qd.js → ActionButton-Dv6BlfJg.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BgCMCOsk.js → Analytics-TQVQuJ7u.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-Dv4oJcqS.js → AppLayout-MSqLm2WK.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-5iV3yrGa.js → Audit-mw81HwVy.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CHDhnbzF.js → Backup-BQcPWDb1.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-B6reFkra.js → BaseInput-BYo_pwBH.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DwS5YyE2.js → Chat-zi3YUKx2.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CqXa87Hw.js → ChatBubbleRenderer-DWSm1XJJ.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-yiW0M4RE.js → Checkbox-BvC8Erjt.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DoiVuD_g.js → Codegen-C32vx0OP.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BVASLexk.js → Col-DMBwmqyZ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D6KQ7JoU.js → Community-nDWncmKV.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Bl9Uhb6v.js → Compact-lIc1HFn8.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-MM31-dba.js → Compliance-D14I_gd2.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-PjU_1ieD.js → Cowork-BiNI-_ZL.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DorNtPZL.js → Cron-N13sFzHb.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Bm5ts2Kw.js → Crosschain-Dlnl0-v6.js} +1 -1
- package/src/assets/web-panel/assets/{DID-7Y3jlFdY.js → DID-CxtYS31I.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-1oE532bG.js → Dashboard-G4UnHlTR.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-hJlOPs0s.js → Dropdown-BazlxFGY.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BEqJxKaO.js → EmailListRenderer-BrpNdihm.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BvCGwB6X.js → FamilyGuardDashboard-HD7jbOOR.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CsXI72e5.js → Federation-Bz8lzAGI.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Dh9SMul-.js → FormItemContext-CcyzGS00.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-9edWzrtG.js → GenericCardRenderer-DRo9cwmp.js} +1 -1
- package/src/assets/web-panel/assets/{Git-ZYhNL8Xk.js → Git-B7bn333J.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BwAdp8QA.js → Governance-DZX9CWAM.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-5C-M1XsH.js → Inference-B3XhsL6W.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-zFAi-zCi.js → KnowledgeGraph-CxFRTlQe.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BZsEdbgE.js → Logs-xuys6mKH.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-BP6gErRK.js → Marketplace-CXyxv4WU.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CXVzoLrd.js → McpTools-BzZLQVI3.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BIpChb4-.js → Memory-BANtaBa7.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-B4O7wDT8.js → MobileBridge-BJIwjmxr.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-B857uSAZ.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-BTmEyTM5.js → Mtc-Cn7ceFEz.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-CsbG9LlV.js → MtcAudit-B0zE978G.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CL8yoGon.js → Multisig-CQFT0wXW.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-C2cIlIp_.js → NLProgramming-DSxKdVY-.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-7aBk_n_M.js → Notes-DtlTfam8.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BuhQk4rJ.js → NotificationSettings-CHQwayAg.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-mqMFZu0x.js → OrderTableRenderer-Brpmzh9n.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CAdq-170.js → Organization-nF_tzZDT.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow--Xn0E787.js → Overflow-CgCSf_PH.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DYt3YAXI.js → P2P-Bvn46bLY.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Bgb_v8WN.js → PdhVaultBrowser-Bzl9k7Gj.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DoFlmoaW.js → Permissions-Dmezbuo8.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-C-FJB3a0.js → PersonalDataHub-lCKRxwZr.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-3bL2RzzL.js → Pipeline-DDCGm9PA.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-c4igYUCF.js → Privacy-Cgu18Kjl.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C0QS1UPR.js → ProjectInit-CkF1AeRY.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CkYC0xkE.js → ProjectSettings-D0Q-orz1.js} +2 -2
- package/src/assets/web-panel/assets/Projects-KfGELrSY.js +1 -0
- package/src/assets/web-panel/assets/{Providers-41NySsLt.js → Providers-BACLV0z8.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DHq9pD7z.js → QuickAsk-CPsZUqDl.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CLjgFPLv.js → Recommend-5jX0OI1-.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-EIrgErm3.js → Reputation-5JKv54z0.js} +1 -1
- package/src/assets/web-panel/assets/{Row-GAvKzKH7.js → Row-DLiTF5LY.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CYCNsVmD.js → RssFeed-CFdGmCKW.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DWOE32k8.js → Search-BjIOnmA7.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Dgh8Jevn.js → Security-BujPqQSo.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BxdgP67N.js → Services-ChciPnMu.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-D-xT4ZkA.js → Skeleton-Cwswp1Jv.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BKN4lfSa.js → Skills-CtwR4vJV.js} +1 -1
- package/src/assets/web-panel/assets/{Sla--N1TudpS.js → Sla-pRIevich.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B0vfJpEh.js → SpeechSettings-BRqB28Ai.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BuBAbPAh.js → SyncSettings-BYyj58_h.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-DTLpT48U.js +1 -0
- package/src/assets/web-panel/assets/{Templates-DI2giLgc.js → Templates-Bbz_h7oW.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BiTWvm0g.js → Tenant-D-H4E3cu.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-vV6AWGDi.js → Terminal-CLLi0-lV.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BmgzKdAp.js → TimelineRenderer-BKI6eG0k.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Nvupdm6p.js → Tokens-rsE_yDjM.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DRfR77WJ.js → Trigger-8TpwuTGk.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-De0Jal_6.js → Trust-sMtZkHPs.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Dzo4-VAM.js → UkeySign-BAy2bAdG.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-hg2ytiJB.js → VideoEditing-CBeR_DYK.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet--bU5-gRh.js → Wallet-BymDnBcq.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DZptt-PV.js → WebAuthn-DQIjmqNz.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-Dy9223bY.js → WorkflowEditor-Cj7PB73f.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DaxGeI9w.js → chat-DYnGj4vi.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Cu2VEci3.js → colors-qOLKZNvN.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CGolhyJq.js → compact-item-BpjCLPcW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DY7EFhkD.js → createContext-CfakUZVQ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DgtRXlrj.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Bpc-NoFN.js → hasIn-C9RW1s7t.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxXkr-NS.js → index-8Ia91vNV.js} +1 -1
- package/src/assets/web-panel/assets/{index-DldaToUA.js → index-B4kS312z.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBSk_VrT.js → index-BE67I0SW.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bz83ngs0.js → index-BFOSDeeo.js} +1 -1
- package/src/assets/web-panel/assets/{index-1D4sfByw.js → index-BIz-pX0k.js} +1 -1
- package/src/assets/web-panel/assets/{index-DNX81oSR.js → index-BJoWi1aR.js} +1 -1
- package/src/assets/web-panel/assets/{index-D63ObMdQ.js → index-B_K0YtG2.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpRSzAFl.js → index-BdR8XRyF.js} +1 -1
- package/src/assets/web-panel/assets/{index-eF9RV_4c.js → index-BfyRXPyV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJFYF8F9.js → index-Bl5LBZJM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFAnEzRW.js → index-BlxRICmz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTvwiqJE.js → index-BxiHBsfU.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ciw5-X1B.js → index-C2S1hUWG.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0_zeYnx.js → index-CEHyZ77C.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfKmAEtE.js → index-CJZ2noI2.js} +1 -1
- package/src/assets/web-panel/assets/{index-lfP8sdzB.js → index-COYEuArt.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-Hkl_2G.js → index-CVZTLSL1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAov-rJR.js → index-CbnJ6FsO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaKXhpEu.js → index-CvWFTG56.js} +1 -1
- package/src/assets/web-panel/assets/{index-rkm7dHwG.js → index-D-RzTqlR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGqeHu_F.js → index-DA80prWe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjfxHEmX.js → index-DAjszh8P.js} +1 -1
- package/src/assets/web-panel/assets/index-DIGTMmnW.js +1 -0
- package/src/assets/web-panel/assets/{index-BRAgl2J_.js → index-DQvVYNoJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0GN5tdM.js → index-DSWdpR3c.js} +1 -1
- package/src/assets/web-panel/assets/{index-DexYD87j.js → index-DadPmrxI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BP9P6chP.js → index-DgMJagCq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bn5gM9Oy.js → index-DkmLJFE_.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2RpsAiO.js → index-DzXYG5YJ.js} +1 -1
- package/src/assets/web-panel/assets/index-Ef5jERRW.js +1 -0
- package/src/assets/web-panel/assets/{index-DElatOQ0.js → index-JkOMWGMX.js} +1 -1
- package/src/assets/web-panel/assets/{index-oJQgRCrR.js → index-T3bIqK_p.js} +3 -3
- package/src/assets/web-panel/assets/{index-RumxOD0S.js → index-UiiqS5k2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQ2z6Ky5.js → index-VYIJmPvJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZ4Vm8dQ.js → index-ZCtDWP2C.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlHq81Ow.js → index-f9yoj84i.js} +1 -1
- package/src/assets/web-panel/assets/{index-8h9y5S6X.js → index-lPc7EzUi.js} +1 -1
- package/src/assets/web-panel/assets/{index-VBRPxZeE.js → index-m9JeDv6B.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLNqZF55.js → index-qf0fAus7.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CkJZfCo8.js → initDefaultProps-DgsgQr1H.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BerbusV1.js → motion-TeUH7wzx.js} +1 -1
- package/src/assets/web-panel/assets/{move-DyRzKPD4.js → move-DdkIeWQx.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CCdrTUAs.js → omit-BH_PH6HT.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-mVDeZx2m.js → pickAttrs-CllCh-Nl.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Bb_-Fs_o.js → placementArrow-BCjE2AzM.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-C6TMj1R_.js → responsiveObserve-BAVGAvRQ.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CdCNsy1J.js → slide-D4ZW-Inn.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Ccxd1rFd.js → statusUtils-j4pxhmKV.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-3IL-yw1V.js → styleChecker-DH2SLtPg.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-CH8DjUHl.js → useFlexGapSupport-CYMMs-_Q.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Cn9nE2sp.js → useFs-BOX2ddKh.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BPyT0HO7.js → usePersonalDataHub-BwcnN5z_.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Mfm7vy07.js → vnode-Cwalh7Hj.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CTpAiAE9.js → zoom-B2_q_nbu.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +38 -4
- package/src/commands/init.js +31 -0
- package/src/commands/mcp.js +57 -0
- package/src/commands/memory.js +62 -0
- package/src/commands/session.js +70 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/init-ai-refine.js +66 -0
- package/src/lib/json-schema-output.js +181 -0
- package/src/lib/mcp-serve.js +259 -0
- package/src/lib/project-instructions.js +89 -0
- package/src/lib/repl-completer.js +9 -3
- package/src/lib/repl-rewind.js +107 -0
- package/src/repl/agent-repl.js +145 -1
- package/src/assets/web-panel/assets/MobileProjects-7VPMoHus.js +0 -1
- package/src/assets/web-panel/assets/Projects-Di17SYft.js +0 -1
- package/src/assets/web-panel/assets/Tasks-4XugjJ87.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DV2BNd59.js +0 -1
- package/src/assets/web-panel/assets/index-BZqtTmyG.js +0 -1
- package/src/assets/web-panel/assets/index-DUpwdJt9.js +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cc agent -p --json-schema <file>` — structured output for headless runs.
|
|
3
|
+
*
|
|
4
|
+
* The final answer must be JSON that validates against a (subset) JSON
|
|
5
|
+
* Schema; invalid replies are retried with a corrective prompt (up to
|
|
6
|
+
* MAX_ATTEMPTS total). Implemented entirely AROUND runAgentHeadless using its
|
|
7
|
+
* `deps.writeOut` capture seam — the runner itself is untouched: each attempt
|
|
8
|
+
* runs with output captured, the validated JSON is the only thing printed.
|
|
9
|
+
*
|
|
10
|
+
* Validator subset (enough for tool/script contracts, not full draft-2020):
|
|
11
|
+
* type (object/array/string/number/integer/boolean/null), properties,
|
|
12
|
+
* required, items, enum, const, additionalProperties:false. Zero deps.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fsDefault from "fs";
|
|
16
|
+
|
|
17
|
+
export const MAX_ATTEMPTS = 3;
|
|
18
|
+
export const _deps = { fs: fsDefault };
|
|
19
|
+
|
|
20
|
+
/** Validate `value` against the schema subset. Returns error strings ([] = valid). */
|
|
21
|
+
export function validateAgainstSchema(value, schema, path = "$") {
|
|
22
|
+
const errors = [];
|
|
23
|
+
if (!schema || typeof schema !== "object") return errors;
|
|
24
|
+
|
|
25
|
+
const typeOf = (v) =>
|
|
26
|
+
v === null
|
|
27
|
+
? "null"
|
|
28
|
+
: Array.isArray(v)
|
|
29
|
+
? "array"
|
|
30
|
+
: typeof v === "number" && Number.isInteger(v)
|
|
31
|
+
? "integer"
|
|
32
|
+
: typeof v;
|
|
33
|
+
|
|
34
|
+
if (schema.type) {
|
|
35
|
+
const want = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
36
|
+
const got = typeOf(value);
|
|
37
|
+
const ok = want.some((t) => t === got || (t === "number" && got === "integer"));
|
|
38
|
+
if (!ok) {
|
|
39
|
+
errors.push(`${path}: expected type ${want.join("|")}, got ${got}`);
|
|
40
|
+
return errors; // type mismatch — deeper checks are noise
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (schema.enum && !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
|
|
44
|
+
errors.push(`${path}: value not in enum [${schema.enum.map((e) => JSON.stringify(e)).join(", ")}]`);
|
|
45
|
+
}
|
|
46
|
+
if (schema.const !== undefined && JSON.stringify(schema.const) !== JSON.stringify(value)) {
|
|
47
|
+
errors.push(`${path}: must equal const ${JSON.stringify(schema.const)}`);
|
|
48
|
+
}
|
|
49
|
+
if (typeOf(value) === "object" && !Array.isArray(value)) {
|
|
50
|
+
for (const req of schema.required || []) {
|
|
51
|
+
if (!(req in value)) errors.push(`${path}: missing required property "${req}"`);
|
|
52
|
+
}
|
|
53
|
+
const props = schema.properties || {};
|
|
54
|
+
for (const [k, v] of Object.entries(value)) {
|
|
55
|
+
if (props[k]) {
|
|
56
|
+
errors.push(...validateAgainstSchema(v, props[k], `${path}.${k}`));
|
|
57
|
+
} else if (schema.additionalProperties === false) {
|
|
58
|
+
errors.push(`${path}: unexpected property "${k}"`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(value) && schema.items) {
|
|
63
|
+
value.forEach((item, i) => {
|
|
64
|
+
errors.push(...validateAgainstSchema(item, schema.items, `${path}[${i}]`));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return errors;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Pull a JSON payload out of an LLM reply (bare, fenced, or embedded). */
|
|
71
|
+
export function extractJsonPayload(text) {
|
|
72
|
+
const raw = String(text || "").trim();
|
|
73
|
+
const tries = [];
|
|
74
|
+
tries.push(raw);
|
|
75
|
+
const fence = /```(?:json)?\s*([\s\S]*?)```/i.exec(raw);
|
|
76
|
+
if (fence) tries.push(fence[1].trim());
|
|
77
|
+
const firstObj = raw.indexOf("{");
|
|
78
|
+
const lastObj = raw.lastIndexOf("}");
|
|
79
|
+
if (firstObj !== -1 && lastObj > firstObj) tries.push(raw.slice(firstObj, lastObj + 1));
|
|
80
|
+
const firstArr = raw.indexOf("[");
|
|
81
|
+
const lastArr = raw.lastIndexOf("]");
|
|
82
|
+
if (firstArr !== -1 && lastArr > firstArr) tries.push(raw.slice(firstArr, lastArr + 1));
|
|
83
|
+
for (const candidate of tries) {
|
|
84
|
+
if (!candidate) continue;
|
|
85
|
+
try {
|
|
86
|
+
return { ok: true, value: JSON.parse(candidate) };
|
|
87
|
+
} catch {
|
|
88
|
+
/* next candidate */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { ok: false, error: "reply contains no parseable JSON" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildSchemaInstruction(schema) {
|
|
95
|
+
return [
|
|
96
|
+
"OUTPUT CONTRACT: your FINAL reply must be ONLY a JSON value (no prose, no markdown fences) that validates against this JSON Schema:",
|
|
97
|
+
JSON.stringify(schema),
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildRetryPrompt(originalPrompt, raw, errors) {
|
|
102
|
+
return [
|
|
103
|
+
originalPrompt,
|
|
104
|
+
"",
|
|
105
|
+
"Your previous reply failed JSON Schema validation:",
|
|
106
|
+
...errors.slice(0, 10).map((e) => `- ${e}`),
|
|
107
|
+
"",
|
|
108
|
+
`Previous reply (for reference): ${String(raw).slice(0, 2000)}`,
|
|
109
|
+
"",
|
|
110
|
+
"Reply again with ONLY the corrected JSON.",
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run a headless turn constrained to a schema, retrying on validation
|
|
116
|
+
* failure. Prints the validated JSON to writeOut; returns the exit code.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} cfg { schemaFile|schema, baseOptions, runHeadless,
|
|
119
|
+
* maxAttempts?, writeOut?, writeErr?, deps? }
|
|
120
|
+
*/
|
|
121
|
+
export async function runJsonSchemaConstrained(cfg = {}) {
|
|
122
|
+
const fs = cfg.deps?.fs || _deps.fs;
|
|
123
|
+
const writeOut = cfg.writeOut || ((s) => process.stdout.write(s));
|
|
124
|
+
const writeErr = cfg.writeErr || ((s) => process.stderr.write(s));
|
|
125
|
+
const maxAttempts = cfg.maxAttempts || MAX_ATTEMPTS;
|
|
126
|
+
|
|
127
|
+
const schema =
|
|
128
|
+
cfg.schema || JSON.parse(fs.readFileSync(cfg.schemaFile, "utf-8"));
|
|
129
|
+
const instruction = buildSchemaInstruction(schema);
|
|
130
|
+
const base = cfg.baseOptions || {};
|
|
131
|
+
|
|
132
|
+
let prompt = base.prompt;
|
|
133
|
+
let lastRaw = "";
|
|
134
|
+
let lastErrors = ["no attempts ran"];
|
|
135
|
+
|
|
136
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
137
|
+
let captured = "";
|
|
138
|
+
const outcome = await cfg.runHeadless(
|
|
139
|
+
{
|
|
140
|
+
...base,
|
|
141
|
+
prompt,
|
|
142
|
+
outputFormat: "text",
|
|
143
|
+
appendSystemPrompt: [base.appendSystemPrompt, instruction]
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join("\n\n"),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
writeOut: (s) => {
|
|
149
|
+
captured += s;
|
|
150
|
+
},
|
|
151
|
+
writeErr,
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
const raw = String(outcome?.result ?? captured ?? "").trim() || captured.trim();
|
|
155
|
+
lastRaw = raw;
|
|
156
|
+
const parsed = extractJsonPayload(raw);
|
|
157
|
+
if (parsed.ok) {
|
|
158
|
+
const errors = validateAgainstSchema(parsed.value, schema);
|
|
159
|
+
if (errors.length === 0) {
|
|
160
|
+
writeOut(`${JSON.stringify(parsed.value, null, 2)}\n`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
lastErrors = errors;
|
|
164
|
+
} else {
|
|
165
|
+
lastErrors = [parsed.error];
|
|
166
|
+
}
|
|
167
|
+
if (attempt < maxAttempts) {
|
|
168
|
+
writeErr(
|
|
169
|
+
`--json-schema: attempt ${attempt} failed validation (${lastErrors.length} error(s)) — retrying…\n`,
|
|
170
|
+
);
|
|
171
|
+
prompt = buildRetryPrompt(base.prompt, raw, lastErrors);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
writeErr(
|
|
176
|
+
`--json-schema: reply failed validation after ${maxAttempts} attempts:\n${lastErrors
|
|
177
|
+
.map((e) => ` - ${e}`)
|
|
178
|
+
.join("\n")}\nLast reply:\n${lastRaw.slice(0, 1000)}\n`,
|
|
179
|
+
);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cc mcp serve` — expose cc's local file tools as an MCP server so OTHER
|
|
3
|
+
* MCP clients (Claude Desktop, another cc, any Streamable-HTTP client) can
|
|
4
|
+
* use this machine's workspace. Claude-Code `claude mcp serve` parity.
|
|
5
|
+
*
|
|
6
|
+
* Protocol: Streamable-HTTP MCP, same shape the IDE-bridge work verified
|
|
7
|
+
* against the real CLI MCPClient — every request is POST JSON-RPC answered
|
|
8
|
+
* with application/json (no persistent SSE GET needed): `initialize`,
|
|
9
|
+
* `notifications/initialized`, `tools/list`, `tools/call`; tool failures are
|
|
10
|
+
* `isError` results, transport failures JSON-RPC errors.
|
|
11
|
+
*
|
|
12
|
+
* Security: tools are CONFINED to the serve root (path resolves inside root
|
|
13
|
+
* or the call fails), Bearer-token auth is on by default (random token,
|
|
14
|
+
* printed once), `--read-only` drops write_file. Zero npm deps (node http).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import http from "http";
|
|
18
|
+
import fsDefault from "fs";
|
|
19
|
+
import pathDefault from "path";
|
|
20
|
+
import { randomBytes } from "crypto";
|
|
21
|
+
|
|
22
|
+
export const MAX_READ_BYTES = 200 * 1024;
|
|
23
|
+
export const MAX_LIST_ENTRIES = 500;
|
|
24
|
+
export const MAX_SEARCH_RESULTS = 200;
|
|
25
|
+
export const MAX_SEARCH_ENTRIES = 50_000;
|
|
26
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build"]);
|
|
27
|
+
|
|
28
|
+
export const _deps = { fs: fsDefault, path: pathDefault };
|
|
29
|
+
|
|
30
|
+
/** Resolve `rel` inside `root`; throws on escape (.. traversal, abs paths out). */
|
|
31
|
+
export function confine(root, rel, deps = _deps) {
|
|
32
|
+
const abs = deps.path.resolve(root, rel || ".");
|
|
33
|
+
const normRoot = deps.path.resolve(root);
|
|
34
|
+
if (abs !== normRoot && !abs.startsWith(normRoot + deps.path.sep)) {
|
|
35
|
+
throw new Error(`path escapes serve root: ${rel}`);
|
|
36
|
+
}
|
|
37
|
+
return abs;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ok(text) {
|
|
41
|
+
return { content: [{ type: "text", text: String(text) }] };
|
|
42
|
+
}
|
|
43
|
+
function fail(message) {
|
|
44
|
+
return { content: [{ type: "text", text: String(message) }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Build the tool table (name → {description, inputSchema, handler}). */
|
|
48
|
+
export function buildTools({ root, readOnly = false, deps = _deps }) {
|
|
49
|
+
const fs = deps.fs;
|
|
50
|
+
const tools = {
|
|
51
|
+
read_file: {
|
|
52
|
+
description: `Read a UTF-8 file under the serve root (${MAX_READ_BYTES} byte cap)`,
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: { path: { type: "string" } },
|
|
56
|
+
required: ["path"],
|
|
57
|
+
},
|
|
58
|
+
handler: ({ path: rel }) => {
|
|
59
|
+
const abs = confine(root, rel, deps);
|
|
60
|
+
const buf = fs.readFileSync(abs);
|
|
61
|
+
const truncated = buf.length > MAX_READ_BYTES;
|
|
62
|
+
const text = (truncated ? buf.slice(0, MAX_READ_BYTES) : buf).toString(
|
|
63
|
+
"utf-8",
|
|
64
|
+
);
|
|
65
|
+
return ok(truncated ? `${text}\n… [truncated ${buf.length} bytes]` : text);
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
list_dir: {
|
|
69
|
+
description: "List a directory under the serve root (dirs get trailing /)",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: { path: { type: "string" } },
|
|
73
|
+
},
|
|
74
|
+
handler: ({ path: rel } = {}) => {
|
|
75
|
+
const abs = confine(root, rel || ".", deps);
|
|
76
|
+
const entries = fs
|
|
77
|
+
.readdirSync(abs, { withFileTypes: true })
|
|
78
|
+
.slice(0, MAX_LIST_ENTRIES)
|
|
79
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
|
|
80
|
+
return ok(entries.join("\n"));
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
search_files: {
|
|
84
|
+
description:
|
|
85
|
+
"Find files under the serve root whose RELATIVE PATH contains the query (case-insensitive, bounded walk)",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
query: { type: "string" },
|
|
90
|
+
dir: { type: "string" },
|
|
91
|
+
},
|
|
92
|
+
required: ["query"],
|
|
93
|
+
},
|
|
94
|
+
handler: ({ query, dir } = {}) => {
|
|
95
|
+
const base = confine(root, dir || ".", deps);
|
|
96
|
+
const q = String(query).toLowerCase();
|
|
97
|
+
const hits = [];
|
|
98
|
+
let seen = 0;
|
|
99
|
+
const walk = (d) => {
|
|
100
|
+
if (hits.length >= MAX_SEARCH_RESULTS || seen >= MAX_SEARCH_ENTRIES)
|
|
101
|
+
return;
|
|
102
|
+
let list;
|
|
103
|
+
try {
|
|
104
|
+
list = fs.readdirSync(d, { withFileTypes: true });
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (const e of list) {
|
|
109
|
+
if (hits.length >= MAX_SEARCH_RESULTS || ++seen >= MAX_SEARCH_ENTRIES)
|
|
110
|
+
return;
|
|
111
|
+
const abs = deps.path.join(d, e.name);
|
|
112
|
+
if (e.isDirectory()) {
|
|
113
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith("."))
|
|
114
|
+
walk(abs);
|
|
115
|
+
} else {
|
|
116
|
+
const rel = deps.path.relative(root, abs).replace(/\\/g, "/");
|
|
117
|
+
if (rel.toLowerCase().includes(q)) hits.push(rel);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
walk(base);
|
|
122
|
+
return ok(hits.join("\n") || "(no matches)");
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
if (!readOnly) {
|
|
127
|
+
tools.write_file = {
|
|
128
|
+
description: "Write a UTF-8 file under the serve root (creates parent dirs)",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
path: { type: "string" },
|
|
133
|
+
content: { type: "string" },
|
|
134
|
+
},
|
|
135
|
+
required: ["path", "content"],
|
|
136
|
+
},
|
|
137
|
+
handler: ({ path: rel, content }) => {
|
|
138
|
+
const abs = confine(root, rel, deps);
|
|
139
|
+
fs.mkdirSync(deps.path.dirname(abs), { recursive: true });
|
|
140
|
+
fs.writeFileSync(abs, String(content), "utf-8");
|
|
141
|
+
return ok(`wrote ${Buffer.byteLength(String(content))} bytes to ${rel}`);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return tools;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rpcResult(id, result) {
|
|
149
|
+
return JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
150
|
+
}
|
|
151
|
+
function rpcError(id, code, message) {
|
|
152
|
+
return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Start the server. Returns { server, port, token, url, close() }.
|
|
157
|
+
*
|
|
158
|
+
* @param {object} opts { root, port=0, token (null → random, false → no auth),
|
|
159
|
+
* readOnly, deps }
|
|
160
|
+
*/
|
|
161
|
+
export function startMcpServe(opts = {}) {
|
|
162
|
+
const deps = { ..._deps, ...(opts.deps || {}) };
|
|
163
|
+
const root = deps.path.resolve(opts.root || process.cwd());
|
|
164
|
+
const readOnly = Boolean(opts.readOnly);
|
|
165
|
+
const token =
|
|
166
|
+
opts.token === false
|
|
167
|
+
? null
|
|
168
|
+
: opts.token || randomBytes(16).toString("hex");
|
|
169
|
+
const tools = buildTools({ root, readOnly, deps });
|
|
170
|
+
|
|
171
|
+
const server = http.createServer((req, res) => {
|
|
172
|
+
const send = (status, body) => {
|
|
173
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
174
|
+
res.end(body);
|
|
175
|
+
};
|
|
176
|
+
if (req.method !== "POST") {
|
|
177
|
+
return send(405, rpcError(null, -32600, "POST only"));
|
|
178
|
+
}
|
|
179
|
+
if (token) {
|
|
180
|
+
const auth = req.headers.authorization || "";
|
|
181
|
+
if (auth !== `Bearer ${token}`) {
|
|
182
|
+
return send(401, rpcError(null, -32001, "unauthorized"));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let raw = "";
|
|
186
|
+
req.on("data", (c) => {
|
|
187
|
+
raw += c;
|
|
188
|
+
});
|
|
189
|
+
req.on("end", () => {
|
|
190
|
+
let msg;
|
|
191
|
+
try {
|
|
192
|
+
msg = JSON.parse(raw);
|
|
193
|
+
} catch {
|
|
194
|
+
return send(400, rpcError(null, -32700, "parse error"));
|
|
195
|
+
}
|
|
196
|
+
const { id, method, params } = msg || {};
|
|
197
|
+
try {
|
|
198
|
+
if (method === "initialize") {
|
|
199
|
+
return send(
|
|
200
|
+
200,
|
|
201
|
+
rpcResult(id, {
|
|
202
|
+
protocolVersion: params?.protocolVersion || "2025-03-26",
|
|
203
|
+
capabilities: { tools: {} },
|
|
204
|
+
serverInfo: { name: "cc-mcp-serve", version: "1.0.0" },
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (method === "notifications/initialized") {
|
|
209
|
+
res.writeHead(202);
|
|
210
|
+
return res.end();
|
|
211
|
+
}
|
|
212
|
+
if (method === "tools/list") {
|
|
213
|
+
return send(
|
|
214
|
+
200,
|
|
215
|
+
rpcResult(id, {
|
|
216
|
+
tools: Object.entries(tools).map(([name, t]) => ({
|
|
217
|
+
name,
|
|
218
|
+
description: t.description,
|
|
219
|
+
inputSchema: t.inputSchema,
|
|
220
|
+
})),
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (method === "tools/call") {
|
|
225
|
+
const tool = tools[params?.name];
|
|
226
|
+
if (!tool) {
|
|
227
|
+
return send(200, rpcResult(id, fail(`unknown tool: ${params?.name}`)));
|
|
228
|
+
}
|
|
229
|
+
let result;
|
|
230
|
+
try {
|
|
231
|
+
result = tool.handler(params?.arguments || {});
|
|
232
|
+
} catch (err) {
|
|
233
|
+
result = fail(err.message);
|
|
234
|
+
}
|
|
235
|
+
return send(200, rpcResult(id, result));
|
|
236
|
+
}
|
|
237
|
+
return send(200, rpcError(id, -32601, `method not found: ${method}`));
|
|
238
|
+
} catch (err) {
|
|
239
|
+
return send(500, rpcError(id, -32603, err.message));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
server.on("error", reject);
|
|
246
|
+
server.listen(opts.port || 0, "127.0.0.1", () => {
|
|
247
|
+
const port = server.address().port;
|
|
248
|
+
resolve({
|
|
249
|
+
server,
|
|
250
|
+
port,
|
|
251
|
+
token,
|
|
252
|
+
root,
|
|
253
|
+
readOnly,
|
|
254
|
+
url: `http://127.0.0.1:${port}/mcp`,
|
|
255
|
+
close: () => new Promise((r) => server.close(r)),
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -136,10 +136,88 @@ export function findInstructionFiles(opts = {}) {
|
|
|
136
136
|
// Template-scaffolded project rules (`cc init -t` writes these) join the
|
|
137
137
|
// chain too, so scaffold-flow and memory-flow projects both feed the agent.
|
|
138
138
|
push(path.join(d, ".chainlesschain", "rules.md"), "rules");
|
|
139
|
+
// Path-scoped rule files (`.claude/rules/*.md`, YAML frontmatter `paths:`
|
|
140
|
+
// globs). Glob filtering happens at LOAD time where content is available.
|
|
141
|
+
try {
|
|
142
|
+
const rulesDir = path.join(d, ".claude", "rules");
|
|
143
|
+
for (const f of fs
|
|
144
|
+
.readdirSync(rulesDir)
|
|
145
|
+
.filter((n) => n.endsWith(".md"))
|
|
146
|
+
.sort()) {
|
|
147
|
+
push(path.join(rulesDir, f), "rule");
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
/* no rules dir */
|
|
151
|
+
}
|
|
139
152
|
}
|
|
140
153
|
return out;
|
|
141
154
|
}
|
|
142
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Parse a rule file's YAML-ish frontmatter (zero-dep): `paths:`/`globs:` as a
|
|
158
|
+
* dash-list or inline value. Returns { globs, body } with frontmatter
|
|
159
|
+
* stripped from body; files without frontmatter pass through unchanged.
|
|
160
|
+
*/
|
|
161
|
+
export function parseRuleFrontmatter(text) {
|
|
162
|
+
const str = String(text);
|
|
163
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(str);
|
|
164
|
+
if (!m) return { globs: [], body: str };
|
|
165
|
+
const globs = [];
|
|
166
|
+
let inPaths = false;
|
|
167
|
+
const take = (raw) => {
|
|
168
|
+
const v = raw.trim().replace(/^["']|["']$/g, "");
|
|
169
|
+
if (v) globs.push(v);
|
|
170
|
+
};
|
|
171
|
+
for (const rawLine of m[1].split(/\r?\n/)) {
|
|
172
|
+
const line = rawLine.trim();
|
|
173
|
+
const key = /^(paths|globs)\s*:\s*(.*)$/.exec(line);
|
|
174
|
+
if (key) {
|
|
175
|
+
inPaths = true;
|
|
176
|
+
const inline = key[2].trim();
|
|
177
|
+
if (inline) {
|
|
178
|
+
for (const g of inline.startsWith("[")
|
|
179
|
+
? inline.replace(/^\[|\]$/g, "").split(",")
|
|
180
|
+
: [inline]) {
|
|
181
|
+
take(g);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (inPaths) {
|
|
187
|
+
const item = /^-\s*(.+)$/.exec(line);
|
|
188
|
+
if (item) take(item[1]);
|
|
189
|
+
else if (line) inPaths = false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { globs, body: str.slice(m[0].length) };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Does a path-scoped rule apply when the agent runs at `relCwd` (cwd relative
|
|
197
|
+
* to the dir holding `.claude/`)? v1 prefix-overlap semantics: the glob's
|
|
198
|
+
* literal prefix and the cwd must sit on the same path line — running at the
|
|
199
|
+
* project root loads every rule; running inside packages/cli loads rules
|
|
200
|
+
* whose glob prefix is packages/cli plus prefixless globs (star-star
|
|
201
|
+
* patterns). Finer tool-time injection is a later phase (module 99 §5.3).
|
|
202
|
+
*/
|
|
203
|
+
export function ruleApplies(globs, relCwd) {
|
|
204
|
+
if (!globs || globs.length === 0) return true;
|
|
205
|
+
const cwd = String(relCwd || "")
|
|
206
|
+
.replace(/\\/g, "/")
|
|
207
|
+
.replace(/^\.\/?/, "");
|
|
208
|
+
if (!cwd) return true; // at the project root every rule is in play
|
|
209
|
+
for (const glob of globs) {
|
|
210
|
+
const g = String(glob).replace(/\\/g, "/");
|
|
211
|
+
const star = g.search(/[*?[]/);
|
|
212
|
+
const prefix = (star === -1 ? g : g.slice(0, star)).replace(/\/+$/, "");
|
|
213
|
+
if (!prefix) return true; // "**/*.js" — applies everywhere
|
|
214
|
+
if (cwd === prefix || cwd.startsWith(`${prefix}/`) || prefix.startsWith(`${cwd}/`)) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
143
221
|
/**
|
|
144
222
|
* Collect `@path` import tokens from instruction text, skipping fenced code
|
|
145
223
|
* blocks (``` / ~~~). Line-level scanning is good enough for memory files,
|
|
@@ -212,6 +290,17 @@ export function loadProjectInstructions(opts = {}) {
|
|
|
212
290
|
warnings.push(`${abs} — cannot read: ${err.message}`);
|
|
213
291
|
continue;
|
|
214
292
|
}
|
|
293
|
+
if (scope === "rule") {
|
|
294
|
+
// <base>/.claude/rules/<file>.md → base is three dirs up.
|
|
295
|
+
const base = path.dirname(path.dirname(path.dirname(abs)));
|
|
296
|
+
const relCwd = path.relative(
|
|
297
|
+
base,
|
|
298
|
+
path.resolve(opts.cwd || process.cwd()),
|
|
299
|
+
);
|
|
300
|
+
const { globs, body } = parseRuleFrontmatter(entry.content);
|
|
301
|
+
if (!ruleApplies(globs, relCwd)) continue; // out of scope for this cwd
|
|
302
|
+
entry = { ...entry, content: body };
|
|
303
|
+
}
|
|
215
304
|
total += Math.min(entry.bytes, maxFileBytes);
|
|
216
305
|
out.push({ path: abs, scope, ...entry });
|
|
217
306
|
|
|
@@ -107,8 +107,12 @@ export function makeAtCompleter(opts = {}) {
|
|
|
107
107
|
.map((f) => {
|
|
108
108
|
const rel = path.relative(cwd, f);
|
|
109
109
|
// Keep workspace files relative (the natural @ref form);
|
|
110
|
-
// out-of-workspace files keep their absolute path.
|
|
111
|
-
|
|
110
|
+
// out-of-workspace files keep their absolute path. On
|
|
111
|
+
// Windows a cross-drive relative() returns an *absolute*
|
|
112
|
+
// path (no ".." prefix), so isAbsolute must also gate it.
|
|
113
|
+
return rel && !rel.startsWith("..") && !path.isAbsolute(rel)
|
|
114
|
+
? fwd(rel)
|
|
115
|
+
: fwd(f);
|
|
112
116
|
})
|
|
113
117
|
: [];
|
|
114
118
|
ideFetchedAt = now();
|
|
@@ -131,7 +135,9 @@ export function makeAtCompleter(opts = {}) {
|
|
|
131
135
|
const slash = /^\/([A-Za-z_-]*)$/.exec(line);
|
|
132
136
|
if (slash && slashCommands.length) {
|
|
133
137
|
const pref = `/${slash[1].toLowerCase()}`;
|
|
134
|
-
const hits = slashCommands.filter((c) =>
|
|
138
|
+
const hits = slashCommands.filter((c) =>
|
|
139
|
+
c.toLowerCase().startsWith(pref),
|
|
140
|
+
);
|
|
135
141
|
return [hits, line];
|
|
136
142
|
}
|
|
137
143
|
const at = extractAtPrefix(line);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL conversation rewind — Claude-Code double-Esc parity (v1: conversation
|
|
3
|
+
* state; file state stays on `cc checkpoint restore`, hinted alongside).
|
|
4
|
+
*
|
|
5
|
+
* Pure helpers over the REPL's live `messages` array so the picker logic is
|
|
6
|
+
* unit-testable without readline:
|
|
7
|
+
* - listUserTurns(): newest-first numbered list of user messages
|
|
8
|
+
* - rewindToTurn(): truncate the conversation BACK TO BEFORE turn #n and
|
|
9
|
+
* return the original text so the caller can prefill the input line
|
|
10
|
+
* (edit-and-resend, like Claude Code's rewind).
|
|
11
|
+
*
|
|
12
|
+
* Trigger surfaces (wired in agent-repl): `/rewind` lists, `/rewind <n>`
|
|
13
|
+
* rewinds, and a double-Esc while idle prints the same list as a shortcut.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_LIST_LIMIT = 10;
|
|
17
|
+
export const PREVIEW_CHARS = 60;
|
|
18
|
+
|
|
19
|
+
function previewOf(content) {
|
|
20
|
+
const text =
|
|
21
|
+
typeof content === "string" ? content : JSON.stringify(content || "");
|
|
22
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
23
|
+
return flat.length > PREVIEW_CHARS
|
|
24
|
+
? `${flat.slice(0, PREVIEW_CHARS)}…`
|
|
25
|
+
: flat;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Newest-first user turns.
|
|
30
|
+
* @returns {Array<{n:number, index:number, preview:string, content:any}>}
|
|
31
|
+
* n is the 1-based pick number (1 = most recent user message).
|
|
32
|
+
*/
|
|
33
|
+
export function listUserTurns(messages, { limit = DEFAULT_LIST_LIMIT } = {}) {
|
|
34
|
+
const turns = [];
|
|
35
|
+
for (let i = (messages || []).length - 1; i >= 0; i--) {
|
|
36
|
+
const m = messages[i];
|
|
37
|
+
if (!m || m.role !== "user") continue;
|
|
38
|
+
turns.push({
|
|
39
|
+
n: turns.length + 1,
|
|
40
|
+
index: i,
|
|
41
|
+
preview: previewOf(m.content),
|
|
42
|
+
content: m.content,
|
|
43
|
+
});
|
|
44
|
+
if (turns.length >= limit) break;
|
|
45
|
+
}
|
|
46
|
+
return turns;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Rewind the conversation to BEFORE the picked user turn (mutates `messages`
|
|
51
|
+
* in place — everything from that user message onward is dropped).
|
|
52
|
+
*
|
|
53
|
+
* @param {Array} messages live conversation array
|
|
54
|
+
* @param {number} n 1-based pick from listUserTurns
|
|
55
|
+
* @returns {{ removed:number, text:string|null }|null} null on bad pick;
|
|
56
|
+
* `text` is the original user text when it was a plain string
|
|
57
|
+
* (caller prefills the input line with it).
|
|
58
|
+
*/
|
|
59
|
+
export function rewindToTurn(messages, n) {
|
|
60
|
+
const turns = listUserTurns(messages, { limit: 1000 });
|
|
61
|
+
const turn = turns.find((t) => t.n === Number(n));
|
|
62
|
+
if (!turn) return null;
|
|
63
|
+
const removed = messages.length - turn.index;
|
|
64
|
+
messages.splice(turn.index);
|
|
65
|
+
return {
|
|
66
|
+
removed,
|
|
67
|
+
text: typeof turn.content === "string" ? turn.content : null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Offline extractive recap for a resumed conversation ("where were we") —
|
|
73
|
+
* no LLM call: turn counts + last ask + last reply previews.
|
|
74
|
+
* @returns {string[]|null} lines to print, or null when nothing to recap.
|
|
75
|
+
*/
|
|
76
|
+
export function buildResumeRecap(messages, { previewChars = 160 } = {}) {
|
|
77
|
+
const list = messages || [];
|
|
78
|
+
const flat = (c) =>
|
|
79
|
+
(typeof c === "string" ? c : JSON.stringify(c || ""))
|
|
80
|
+
.replace(/\s+/g, " ")
|
|
81
|
+
.trim();
|
|
82
|
+
const cap = (s) =>
|
|
83
|
+
s.length > previewChars ? `${s.slice(0, previewChars)}…` : s;
|
|
84
|
+
const lastOf = (role) => {
|
|
85
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
86
|
+
if (list[i]?.role === role) return list[i].content;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
const users = list.filter((m) => m?.role === "user").length;
|
|
91
|
+
const assistants = list.filter((m) => m?.role === "assistant").length;
|
|
92
|
+
if (!users && !assistants) return null;
|
|
93
|
+
const lines = [`${users} user / ${assistants} assistant turns`];
|
|
94
|
+
const lu = lastOf("user");
|
|
95
|
+
if (lu) lines.push(`last ask : ${cap(flat(lu))}`);
|
|
96
|
+
const la = lastOf("assistant");
|
|
97
|
+
if (la) lines.push(`last reply: ${cap(flat(la))}`);
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Render the picker list (shared by /rewind and double-Esc). */
|
|
102
|
+
export function renderTurnList(turns) {
|
|
103
|
+
if (!turns.length) return " (no user turns yet)";
|
|
104
|
+
return turns
|
|
105
|
+
.map((t) => ` ${String(t.n).padStart(2)}. ${t.preview}`)
|
|
106
|
+
.join("\n");
|
|
107
|
+
}
|