chainlesschain 0.162.38 → 0.162.40
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/README.md +368 -1
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-DV0Q9zKL.js → AIOps-CPmKv82o.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-C6vH8rhL.js → ActionButton-BNDYY7Qd.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BvPDc2ui.js → Analytics-BgCMCOsk.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CWnyqTqY.js → AppLayout-Dv4oJcqS.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-BzenidV4.js → Audit-5iV3yrGa.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CSl7bNwK.js → Backup-CHDhnbzF.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DAY3iHIq.js → BaseInput-B6reFkra.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Jyhm9fgk.js → Chat-DwS5YyE2.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CwlAnVjy.js → ChatBubbleRenderer-CqXa87Hw.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-D4rwURAi.js → Checkbox-yiW0M4RE.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DYdjTEfC.js → Codegen-DoiVuD_g.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DsVyZ_fS.js → Col-BVASLexk.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CjCpl27Q.js → Community-D6KQ7JoU.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-kt18dsjm.js → Compact-Bl9Uhb6v.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BV5urquU.js → Compliance-MM31-dba.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-C4SovPWC.js → Cowork-PjU_1ieD.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-uuNs_xzA.js → Cron-DorNtPZL.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DR5a65tR.js → Crosschain-Bm5ts2Kw.js} +1 -1
- package/src/assets/web-panel/assets/{DID-B1KTf2-5.js → DID-7Y3jlFdY.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Dkj7XgED.js → Dashboard-1oE532bG.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BhXCuJ19.js → Dropdown-hJlOPs0s.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DG8365Iv.js → EmailListRenderer-BEqJxKaO.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BdHGPu39.js → FamilyGuardDashboard-BvCGwB6X.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Dwvxl0zR.js → Federation-CsXI72e5.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BVmhCVWU.js → FormItemContext-Dh9SMul-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DDPjvF2s.js → GenericCardRenderer-9edWzrtG.js} +1 -1
- package/src/assets/web-panel/assets/{Git-foK6WTSr.js → Git-ZYhNL8Xk.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CfqMdu6Y.js → Governance-BwAdp8QA.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BKrLO4GO.js → Inference-5C-M1XsH.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-6o6Q-mmF.js → KnowledgeGraph-zFAi-zCi.js} +1 -1
- package/src/assets/web-panel/assets/Logs-BZsEdbgE.js +2 -0
- package/src/assets/web-panel/assets/{Marketplace-BWkfEocP.js → Marketplace-BP6gErRK.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BPebQbWU.js → McpTools-CXVzoLrd.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-C0Dq-X3C.js → Memory-BIpChb4-.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DRBoutTY.js → MobileBridge-B4O7wDT8.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-7VPMoHus.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-Cj3QPM9p.js → Mtc-BTmEyTM5.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-rBQYbfQR.js → MtcAudit-CsbG9LlV.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Dbuy4OY4.js → Multisig-CL8yoGon.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CMnt1se-.js → NLProgramming-C2cIlIp_.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BX9tSCiF.js → Notes-7aBk_n_M.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-BFeirVRq.js → NotificationSettings-BuhQk4rJ.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-ybiMlKQW.js → OrderTableRenderer-mqMFZu0x.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-kTfRxKqk.js → Organization-CAdq-170.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CtuCAzwV.js → Overflow--Xn0E787.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-KfbciaP3.js → P2P-DYt3YAXI.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-bqEUFhgC.js → PdhVaultBrowser-Bgb_v8WN.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-BgMypz-z.js → Permissions-DoFlmoaW.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-C3zUE-1z.js → PersonalDataHub-C-FJB3a0.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-iX-pYHpC.js → Pipeline-3bL2RzzL.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B01uzeFM.js → Privacy-c4igYUCF.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-TsfbzJp7.js → ProjectInit-C0QS1UPR.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-iGvMp8sM.js → ProjectSettings-CkYC0xkE.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Be9k29iQ.js → Projects-Di17SYft.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-C9Pc8dqo.js → Providers-41NySsLt.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DN_yFiVO.js → QuickAsk-DHq9pD7z.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CvSNgl7H.js → Recommend-CLjgFPLv.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-S6BCz8xH.js → Reputation-EIrgErm3.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CTRYCaqP.js → Row-GAvKzKH7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Cu8_P5ll.js → RssFeed-CYCNsVmD.js} +3 -3
- package/src/assets/web-panel/assets/{Search-rZ1Xza_U.js → Search-DWOE32k8.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CF43IJHX.js → Security-Dgh8Jevn.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BobNHzne.js → Services-BxdgP67N.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DWJ2kfuI.js → Skeleton-D-xT4ZkA.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-AmEZgHYr.js → Skills-BKN4lfSa.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DTS-fBiY.js → Sla--N1TudpS.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DEr6MHRU.js → SpeechSettings-B0vfJpEh.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CVs9alv_.js → SyncSettings-BuBAbPAh.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-4XugjJ87.js +1 -0
- package/src/assets/web-panel/assets/{Templates-CTNjZRKA.js → Templates-DI2giLgc.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DPbXg0Pg.js → Tenant-BiTWvm0g.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DhKXcPw2.js → Terminal-vV6AWGDi.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-B0DMZOpk.js → TimelineRenderer-BmgzKdAp.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-RvWuBXgg.js → Tokens-Nvupdm6p.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-2O-BaTQG.js → Trigger-DRfR77WJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-6qY35L-C.js → Trust-De0Jal_6.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DhV1wYtQ.js → UkeySign-Dzo4-VAM.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DgqA5UZm.js → VideoEditing-hg2ytiJB.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DJRYdUAK.js → Wallet--bU5-gRh.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C2W-x0cg.js → WebAuthn-DZptt-PV.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-BP2tkDHe.js → WorkflowEditor-Dy9223bY.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CGVfeoTn.js → chat-DaxGeI9w.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BmjRolM1.js → colors-Cu2VEci3.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BvJJkjZE.js → compact-item-CGolhyJq.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DyhlvRYs.js → createContext-DY7EFhkD.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DV2BNd59.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BoBMR89s.js → hasIn-Bpc-NoFN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSgbOGaP.js → index-1D4sfByw.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1t-r7yV.js → index-8h9y5S6X.js} +1 -1
- package/src/assets/web-panel/assets/{index-FKFT-QTk.js → index-BP9P6chP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIaGw7vl.js → index-BQ2z6Ky5.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQJVedQ3.js → index-BRAgl2J_.js} +1 -1
- package/src/assets/web-panel/assets/{index-BycpeGfj.js → index-BTvwiqJE.js} +1 -1
- package/src/assets/web-panel/assets/index-BZqtTmyG.js +1 -0
- package/src/assets/web-panel/assets/{index-D0YToIi_.js → index-BjfxHEmX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BT1SQ9nj.js → index-BlHq81Ow.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyS4I4L-.js → index-Bn5gM9Oy.js} +1 -1
- package/src/assets/web-panel/assets/{index-81tWFqfN.js → index-Bz83ngs0.js} +1 -1
- package/src/assets/web-panel/assets/{index-xZdOioVg.js → index-C-Hkl_2G.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cbh-lCxq.js → index-C0_zeYnx.js} +1 -1
- package/src/assets/web-panel/assets/{index-xPSzUoWT.js → index-C2RpsAiO.js} +1 -1
- package/src/assets/web-panel/assets/{index-n-N19np-.js → index-CBSk_VrT.js} +1 -1
- package/src/assets/web-panel/assets/{index-VXVukhBA.js → index-CFAnEzRW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtKdCXHW.js → index-CGqeHu_F.js} +1 -1
- package/src/assets/web-panel/assets/{index-Beh7jDbS.js → index-CJFYF8F9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvvNnWXe.js → index-CLNqZF55.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeeLHcMY.js → index-CaKXhpEu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuQrONgf.js → index-Ciw5-X1B.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDPMHKQi.js → index-D0GN5tdM.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bm_MmdwP.js → index-D63ObMdQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIPZ6hbJ.js → index-DAov-rJR.js} +1 -1
- package/src/assets/web-panel/assets/{index-39VDXdn6.js → index-DElatOQ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwTgvhOL.js → index-DNX81oSR.js} +1 -1
- package/src/assets/web-panel/assets/index-DUpwdJt9.js +1 -0
- package/src/assets/web-panel/assets/{index-DgbWSwr5.js → index-DZ4Vm8dQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-93XwJd.js → index-DexYD87j.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0xn6hOr.js → index-DfKmAEtE.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZNIms1nA.js → index-DldaToUA.js} +1 -1
- package/src/assets/web-panel/assets/{index-wLAjVpmJ.js → index-DpRSzAFl.js} +1 -1
- package/src/assets/web-panel/assets/{index-Te0ruvY_.js → index-DxXkr-NS.js} +1 -1
- package/src/assets/web-panel/assets/{index-Czsbrn75.js → index-RumxOD0S.js} +1 -1
- package/src/assets/web-panel/assets/{index-vF1pR00A.js → index-VBRPxZeE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqGNmoKy.js → index-eF9RV_4c.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzDVBBcg.js → index-lfP8sdzB.js} +1 -1
- package/src/assets/web-panel/assets/{index-Y1b8i0NV.js → index-oJQgRCrR.js} +3 -3
- package/src/assets/web-panel/assets/{index-ByWpNjTj.js → index-rkm7dHwG.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BLKSE8he.js → initDefaultProps-CkJZfCo8.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Bb59qqLK.js → motion-BerbusV1.js} +1 -1
- package/src/assets/web-panel/assets/{move-CB3pYCk6.js → move-DyRzKPD4.js} +1 -1
- package/src/assets/web-panel/assets/{omit-iImQWuU7.js → omit-CCdrTUAs.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DRP2Chqo.js → pickAttrs-mVDeZx2m.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BrlfD4tF.js → placementArrow-Bb_-Fs_o.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Cqxkuh5H.js → responsiveObserve-C6TMj1R_.js} +1 -1
- package/src/assets/web-panel/assets/{slide-nxKEuLMj.js → slide-CdCNsy1J.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-30E47KSk.js → statusUtils-Ccxd1rFd.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Dn2_-5bn.js → styleChecker-3IL-yw1V.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DkZ00X6F.js → useFlexGapSupport-CH8DjUHl.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-ByrwSCOr.js → useFs-Cn9nE2sp.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BDY6jtUD.js → usePersonalDataHub-BPyT0HO7.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BL2q5BLv.js → vnode-Mfm7vy07.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BSkPKE42.js → zoom-CTpAiAE9.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/init.js +84 -2
- package/src/commands/session.js +36 -12
- package/src/index.js +10 -0
- package/src/lib/agent-session-export.js +124 -0
- package/src/lib/ide-context.js +333 -0
- package/src/lib/project-instructions.js +275 -0
- package/src/lib/project-inventory.js +355 -0
- package/src/lib/repl-bang-memorize.js +142 -0
- package/src/lib/repl-completer.js +154 -0
- package/src/lib/update-notice-refresh.mjs +10 -0
- package/src/lib/update-notice.js +154 -0
- package/src/repl/agent-repl.js +154 -0
- package/src/runtime/agent-core.js +195 -0
- package/src/runtime/headless-runner.js +19 -0
- package/src/runtime/headless-stream.js +19 -9
- package/src/runtime/system-prompt.js +21 -1
- package/src/assets/web-panel/assets/Logs-L5ZIW0Dz.js +0 -2
- package/src/assets/web-panel/assets/MobileProjects-BMP6eLp1.js +0 -1
- package/src/assets/web-panel/assets/Tasks-BcVDAxdi.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CetO0WH0.js +0 -1
- package/src/assets/web-panel/assets/index-BZVz-WfV.js +0 -1
- package/src/assets/web-panel/assets/index-D0-bvFy3.js +0 -1
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project inventory — offline resource census for `cc init --memory`.
|
|
3
|
+
*
|
|
4
|
+
* Scans the target folder (bounded walk: depth/entry caps, heavy dirs
|
|
5
|
+
* skipped) and produces a deterministic snapshot of what is already there:
|
|
6
|
+
* languages by extension, package manager, npm scripts, workspaces/monorepo
|
|
7
|
+
* layout, notable tool configs (TS/bundlers/docker/CI/lint), test runners and
|
|
8
|
+
* a README summary. `renderMemoryFile` turns that snapshot into a starter
|
|
9
|
+
* `cc.md` project-memory file (the file `cc agent` auto-loads — see
|
|
10
|
+
* project-instructions.js). No LLM, no network: the census is pure I/O and
|
|
11
|
+
* unit-testable via the `deps` seam; reads are explicit UTF-8 (encoding.md).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fsDefault from "fs";
|
|
15
|
+
import pathDefault from "path";
|
|
16
|
+
|
|
17
|
+
export const SKIP_DIRS = new Set([
|
|
18
|
+
"node_modules",
|
|
19
|
+
".git",
|
|
20
|
+
"dist",
|
|
21
|
+
"build",
|
|
22
|
+
"out",
|
|
23
|
+
"coverage",
|
|
24
|
+
".next",
|
|
25
|
+
".nuxt",
|
|
26
|
+
".venv",
|
|
27
|
+
"venv",
|
|
28
|
+
"__pycache__",
|
|
29
|
+
"target",
|
|
30
|
+
".gradle",
|
|
31
|
+
".idea",
|
|
32
|
+
".vscode",
|
|
33
|
+
"Pods",
|
|
34
|
+
"DerivedData",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_MAX_DEPTH = 4;
|
|
38
|
+
export const DEFAULT_MAX_ENTRIES = 20000;
|
|
39
|
+
|
|
40
|
+
const EXT_LANG = {
|
|
41
|
+
".js": "JavaScript",
|
|
42
|
+
".cjs": "JavaScript",
|
|
43
|
+
".mjs": "JavaScript",
|
|
44
|
+
".ts": "TypeScript",
|
|
45
|
+
".tsx": "TypeScript",
|
|
46
|
+
".jsx": "JavaScript",
|
|
47
|
+
".vue": "Vue",
|
|
48
|
+
".py": "Python",
|
|
49
|
+
".java": "Java",
|
|
50
|
+
".kt": "Kotlin",
|
|
51
|
+
".kts": "Kotlin",
|
|
52
|
+
".swift": "Swift",
|
|
53
|
+
".go": "Go",
|
|
54
|
+
".rs": "Rust",
|
|
55
|
+
".rb": "Ruby",
|
|
56
|
+
".php": "PHP",
|
|
57
|
+
".cs": "C#",
|
|
58
|
+
".c": "C",
|
|
59
|
+
".h": "C/C++ header",
|
|
60
|
+
".cpp": "C++",
|
|
61
|
+
".cc": "C++",
|
|
62
|
+
".m": "Objective-C",
|
|
63
|
+
".sql": "SQL",
|
|
64
|
+
".sh": "Shell",
|
|
65
|
+
".ps1": "PowerShell",
|
|
66
|
+
".md": "Markdown",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const CONFIG_MARKERS = [
|
|
70
|
+
["tsconfig.json", "TypeScript"],
|
|
71
|
+
["vite.config.js", "Vite"],
|
|
72
|
+
["vite.config.ts", "Vite"],
|
|
73
|
+
["webpack.config.js", "Webpack"],
|
|
74
|
+
["rollup.config.js", "Rollup"],
|
|
75
|
+
["next.config.js", "Next.js"],
|
|
76
|
+
["nuxt.config.ts", "Nuxt"],
|
|
77
|
+
["docker-compose.yml", "Docker Compose"],
|
|
78
|
+
["docker-compose.yaml", "Docker Compose"],
|
|
79
|
+
["Dockerfile", "Docker"],
|
|
80
|
+
["pom.xml", "Maven"],
|
|
81
|
+
["build.gradle", "Gradle"],
|
|
82
|
+
["build.gradle.kts", "Gradle (Kotlin DSL)"],
|
|
83
|
+
["settings.gradle.kts", "Gradle (Kotlin DSL)"],
|
|
84
|
+
["Cargo.toml", "Cargo / Rust"],
|
|
85
|
+
["go.mod", "Go modules"],
|
|
86
|
+
["pyproject.toml", "Python (pyproject)"],
|
|
87
|
+
["requirements.txt", "Python (requirements)"],
|
|
88
|
+
["pubspec.yaml", "Flutter/Dart"],
|
|
89
|
+
[".eslintrc.json", "ESLint"],
|
|
90
|
+
["eslint.config.js", "ESLint"],
|
|
91
|
+
[".prettierrc", "Prettier"],
|
|
92
|
+
["vitest.config.js", "Vitest"],
|
|
93
|
+
["vitest.config.ts", "Vitest"],
|
|
94
|
+
["jest.config.js", "Jest"],
|
|
95
|
+
["playwright.config.ts", "Playwright"],
|
|
96
|
+
["electron-builder.yml", "electron-builder"],
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const LOCKFILE_PM = [
|
|
100
|
+
["package-lock.json", "npm"],
|
|
101
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
102
|
+
["yarn.lock", "yarn"],
|
|
103
|
+
["bun.lockb", "bun"],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
function resolveDeps(opts) {
|
|
107
|
+
return {
|
|
108
|
+
fs: opts.deps?.fs || fsDefault,
|
|
109
|
+
path: opts.deps?.path || pathDefault,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readJson(fs, p) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readText(fs, p) {
|
|
122
|
+
try {
|
|
123
|
+
return fs.readFileSync(p, "utf-8");
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** First markdown paragraph after the title — a cheap README synopsis. */
|
|
130
|
+
export function readmeSynopsis(text, maxChars = 400) {
|
|
131
|
+
if (!text) return null;
|
|
132
|
+
const lines = String(text).split(/\r?\n/);
|
|
133
|
+
const out = [];
|
|
134
|
+
let started = false;
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
const t = line.trim();
|
|
137
|
+
if (!t) {
|
|
138
|
+
if (started) break;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (/^#/.test(t) || /^!\[/.test(t) || /^<.*>$/.test(t) || /^\[!\[/.test(t))
|
|
142
|
+
continue; // headings / badges / bare HTML
|
|
143
|
+
out.push(t);
|
|
144
|
+
started = true;
|
|
145
|
+
}
|
|
146
|
+
const para = out.join(" ").trim();
|
|
147
|
+
if (!para) return null;
|
|
148
|
+
return para.length > maxChars ? `${para.slice(0, maxChars)}…` : para;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Walk the tree (bounded) counting files per language and remembering
|
|
153
|
+
* top-level directories with file counts.
|
|
154
|
+
*/
|
|
155
|
+
function census(root, { fs, path, maxDepth, maxEntries }) {
|
|
156
|
+
const languages = new Map(); // lang -> count
|
|
157
|
+
const topDirs = new Map(); // top-level dir -> file count
|
|
158
|
+
let entries = 0;
|
|
159
|
+
let truncated = false;
|
|
160
|
+
|
|
161
|
+
const walk = (dir, depth, top) => {
|
|
162
|
+
if (entries >= maxEntries) {
|
|
163
|
+
truncated = true;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
let list;
|
|
167
|
+
try {
|
|
168
|
+
list = fs.readdirSync(dir, { withFileTypes: true });
|
|
169
|
+
} catch {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
for (const e of list) {
|
|
173
|
+
if (entries >= maxEntries) {
|
|
174
|
+
truncated = true;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
entries += 1;
|
|
178
|
+
if (e.isDirectory()) {
|
|
179
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith(".")) continue;
|
|
180
|
+
if (depth < maxDepth) {
|
|
181
|
+
walk(path.join(dir, e.name), depth + 1, top || e.name);
|
|
182
|
+
}
|
|
183
|
+
} else if (e.isFile()) {
|
|
184
|
+
if (top) topDirs.set(top, (topDirs.get(top) || 0) + 1);
|
|
185
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
186
|
+
const lang = EXT_LANG[ext];
|
|
187
|
+
if (lang) languages.set(lang, (languages.get(lang) || 0) + 1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
walk(root, 0, null);
|
|
192
|
+
return { languages, topDirs, truncated };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Inventory the folder. Pure data out — rendering is separate.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} cwd
|
|
199
|
+
* @param {object} [opts] { deps, maxDepth, maxEntries }
|
|
200
|
+
*/
|
|
201
|
+
export function inventoryProject(cwd, opts = {}) {
|
|
202
|
+
const { fs, path } = resolveDeps(opts);
|
|
203
|
+
const root = path.resolve(cwd);
|
|
204
|
+
const maxDepth = Number.isFinite(opts.maxDepth)
|
|
205
|
+
? opts.maxDepth
|
|
206
|
+
: DEFAULT_MAX_DEPTH;
|
|
207
|
+
const maxEntries = Number.isFinite(opts.maxEntries)
|
|
208
|
+
? opts.maxEntries
|
|
209
|
+
: DEFAULT_MAX_ENTRIES;
|
|
210
|
+
|
|
211
|
+
const pkg = readJson(fs, path.join(root, "package.json"));
|
|
212
|
+
|
|
213
|
+
let packageManager = null;
|
|
214
|
+
for (const [lock, pm] of LOCKFILE_PM) {
|
|
215
|
+
if (fs.existsSync(path.join(root, lock))) {
|
|
216
|
+
packageManager = pm;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const configs = [];
|
|
222
|
+
for (const [file, label] of CONFIG_MARKERS) {
|
|
223
|
+
if (fs.existsSync(path.join(root, file)) && !configs.includes(label)) {
|
|
224
|
+
configs.push(label);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let ciWorkflows = 0;
|
|
229
|
+
try {
|
|
230
|
+
ciWorkflows = fs
|
|
231
|
+
.readdirSync(path.join(root, ".github", "workflows"))
|
|
232
|
+
.filter((f) => /\.ya?ml$/.test(f)).length;
|
|
233
|
+
} catch {
|
|
234
|
+
/* no CI dir */
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const readme =
|
|
238
|
+
readText(fs, path.join(root, "README.md")) ||
|
|
239
|
+
readText(fs, path.join(root, "readme.md"));
|
|
240
|
+
|
|
241
|
+
const { languages, topDirs, truncated } = census(root, {
|
|
242
|
+
fs,
|
|
243
|
+
path,
|
|
244
|
+
maxDepth,
|
|
245
|
+
maxEntries,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const workspaces = Array.isArray(pkg?.workspaces)
|
|
249
|
+
? pkg.workspaces
|
|
250
|
+
: Array.isArray(pkg?.workspaces?.packages)
|
|
251
|
+
? pkg.workspaces.packages
|
|
252
|
+
: [];
|
|
253
|
+
|
|
254
|
+
const scripts = pkg?.scripts ? Object.entries(pkg.scripts) : [];
|
|
255
|
+
|
|
256
|
+
const existingMemory = ["cc.md", "CLAUDE.md", "AGENTS.md"].filter((f) =>
|
|
257
|
+
fs.existsSync(path.join(root, f)),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
root,
|
|
262
|
+
name: pkg?.name || path.basename(root),
|
|
263
|
+
description: pkg?.description || null,
|
|
264
|
+
synopsis: readmeSynopsis(readme),
|
|
265
|
+
packageManager,
|
|
266
|
+
configs,
|
|
267
|
+
ciWorkflows,
|
|
268
|
+
workspaces,
|
|
269
|
+
scripts,
|
|
270
|
+
languages: [...languages.entries()].sort((a, b) => b[1] - a[1]),
|
|
271
|
+
topDirs: [...topDirs.entries()].sort((a, b) => b[1] - a[1]),
|
|
272
|
+
truncated,
|
|
273
|
+
existingMemory,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Render the inventory as a starter cc.md project-memory file. */
|
|
278
|
+
export function renderMemoryFile(inv, opts = {}) {
|
|
279
|
+
const today = opts.date || new Date().toISOString().slice(0, 10);
|
|
280
|
+
const L = [];
|
|
281
|
+
L.push(`# ${inv.name} — Project Memory`);
|
|
282
|
+
L.push("");
|
|
283
|
+
L.push(
|
|
284
|
+
`> Generated by \`cc init --memory\` on ${today} from an inventory of this folder.`,
|
|
285
|
+
);
|
|
286
|
+
L.push(
|
|
287
|
+
"> `cc agent` auto-loads this file as authoritative project context — edit freely.",
|
|
288
|
+
);
|
|
289
|
+
L.push("");
|
|
290
|
+
|
|
291
|
+
// cc.md outranks CLAUDE.md/AGENTS.md in discovery — import any existing
|
|
292
|
+
// memory file so generating cc.md never shadows hand-maintained content.
|
|
293
|
+
const existing = (inv.existingMemory || []).filter((f) => f !== "cc.md");
|
|
294
|
+
if (existing.length) {
|
|
295
|
+
L.push("## Existing project memory (imported)");
|
|
296
|
+
L.push("");
|
|
297
|
+
for (const f of existing) L.push(`@${f}`);
|
|
298
|
+
L.push("");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (inv.synopsis || inv.description) {
|
|
302
|
+
L.push("## Overview");
|
|
303
|
+
L.push("");
|
|
304
|
+
L.push(inv.synopsis || inv.description);
|
|
305
|
+
L.push("");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
L.push("## Stack");
|
|
309
|
+
L.push("");
|
|
310
|
+
if (inv.languages.length) {
|
|
311
|
+
const langs = inv.languages
|
|
312
|
+
.slice(0, 8)
|
|
313
|
+
.map(([lang, n]) => `${lang} (${n})`)
|
|
314
|
+
.join(", ");
|
|
315
|
+
L.push(`- Languages by file count: ${langs}${inv.truncated ? " — scan truncated" : ""}`);
|
|
316
|
+
}
|
|
317
|
+
if (inv.packageManager) L.push(`- Package manager: ${inv.packageManager}`);
|
|
318
|
+
if (inv.configs.length) L.push(`- Tooling: ${inv.configs.join(", ")}`);
|
|
319
|
+
if (inv.ciWorkflows)
|
|
320
|
+
L.push(`- CI: GitHub Actions (${inv.ciWorkflows} workflows)`);
|
|
321
|
+
L.push("");
|
|
322
|
+
|
|
323
|
+
if (inv.scripts.length) {
|
|
324
|
+
L.push("## Commands");
|
|
325
|
+
L.push("");
|
|
326
|
+
const pm = inv.packageManager || "npm";
|
|
327
|
+
for (const [name, cmd] of inv.scripts.slice(0, 12)) {
|
|
328
|
+
L.push(`- \`${pm} run ${name}\` — \`${cmd}\``);
|
|
329
|
+
}
|
|
330
|
+
if (inv.scripts.length > 12) {
|
|
331
|
+
L.push(`- … ${inv.scripts.length - 12} more scripts in package.json`);
|
|
332
|
+
}
|
|
333
|
+
L.push("");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (inv.workspaces.length || inv.topDirs.length) {
|
|
337
|
+
L.push("## Layout");
|
|
338
|
+
L.push("");
|
|
339
|
+
if (inv.workspaces.length) {
|
|
340
|
+
L.push(`- Workspaces: ${inv.workspaces.join(", ")}`);
|
|
341
|
+
}
|
|
342
|
+
for (const [dir, n] of inv.topDirs.slice(0, 10)) {
|
|
343
|
+
L.push(`- \`${dir}/\` — ${n} files`);
|
|
344
|
+
}
|
|
345
|
+
L.push("");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
L.push("## Conventions");
|
|
349
|
+
L.push("");
|
|
350
|
+
L.push("- (add project rules here — code style, commit format, test policy)");
|
|
351
|
+
L.push("");
|
|
352
|
+
return L.join("\n");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export const _deps = { fs: fsDefault, path: pathDefault };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL `!` bash passthrough + `#` quick-memorize (Claude-Code parity).
|
|
3
|
+
*
|
|
4
|
+
* Pure logic for two agent-REPL input prefixes, extracted so it is unit
|
|
5
|
+
* testable without driving readline:
|
|
6
|
+
*
|
|
7
|
+
* - `! <cmd>` — run the shell command immediately (no LLM round-trip) and
|
|
8
|
+
* return a `<bash-input>/<bash-output>` context message so the model sees
|
|
9
|
+
* what happened on the next turn. Windows runs through `cmd.exe /d /s /c`
|
|
10
|
+
* with a `chcp 65001` prefix (encoding.md rule); POSIX through `/bin/sh -c`.
|
|
11
|
+
*
|
|
12
|
+
* - `# <note>` — append a one-line note to the project memory file (cc.md at
|
|
13
|
+
* the git root — the file project-instructions.js auto-loads). Creates the
|
|
14
|
+
* file/`## Notes` section when missing; next sessions pick it up
|
|
15
|
+
* automatically and the caller can also inject it into the live context.
|
|
16
|
+
*
|
|
17
|
+
* All process/fs access goes through `_deps` for tests.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawnSync as spawnSyncDefault } from "child_process";
|
|
21
|
+
import fsDefault from "fs";
|
|
22
|
+
import pathDefault from "path";
|
|
23
|
+
import { findProjectRoot } from "./project-instructions.js";
|
|
24
|
+
|
|
25
|
+
export const BANG_TIMEOUT_MS = 120_000;
|
|
26
|
+
export const BANG_MAX_BUFFER = 10 * 1024 * 1024;
|
|
27
|
+
export const BANG_OUTPUT_CAP = 30_000;
|
|
28
|
+
|
|
29
|
+
export const _deps = {
|
|
30
|
+
spawnSync: spawnSyncDefault,
|
|
31
|
+
fs: fsDefault,
|
|
32
|
+
path: pathDefault,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function cap(s) {
|
|
36
|
+
const str = s || "";
|
|
37
|
+
return str.length > BANG_OUTPUT_CAP
|
|
38
|
+
? `${str.slice(0, BANG_OUTPUT_CAP)}\n… [truncated]`
|
|
39
|
+
: str;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True when the REPL line is a `!` bash passthrough. */
|
|
43
|
+
export function isBangCommand(trimmed) {
|
|
44
|
+
return (
|
|
45
|
+
typeof trimmed === "string" &&
|
|
46
|
+
trimmed.startsWith("!") &&
|
|
47
|
+
trimmed.slice(1).trim().length > 0
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when the REPL line is a `#` quick-memorize. */
|
|
52
|
+
export function isMemorizeLine(trimmed) {
|
|
53
|
+
return (
|
|
54
|
+
typeof trimmed === "string" &&
|
|
55
|
+
trimmed.startsWith("#") &&
|
|
56
|
+
trimmed.slice(1).trim().length > 0
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run a `!` command synchronously.
|
|
62
|
+
*
|
|
63
|
+
* @returns {{ cmd, stdout, stderr, exitCode, error, contextMessage }}
|
|
64
|
+
* `contextMessage` is ready to push as a user-role message.
|
|
65
|
+
*/
|
|
66
|
+
export function runBangCommand(line, opts = {}) {
|
|
67
|
+
const spawnSync = opts.deps?.spawnSync || _deps.spawnSync;
|
|
68
|
+
const cwd = opts.cwd || process.cwd();
|
|
69
|
+
const isWin =
|
|
70
|
+
opts.platform != null ? opts.platform === "win32" : process.platform === "win32";
|
|
71
|
+
const cmd = String(line).replace(/^!/, "").trim();
|
|
72
|
+
|
|
73
|
+
const res = isWin
|
|
74
|
+
? spawnSync("cmd.exe", ["/d", "/s", "/c", `chcp 65001 >nul && ${cmd}`], {
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
timeout: BANG_TIMEOUT_MS,
|
|
77
|
+
maxBuffer: BANG_MAX_BUFFER,
|
|
78
|
+
cwd,
|
|
79
|
+
})
|
|
80
|
+
: spawnSync("/bin/sh", ["-c", cmd], {
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
timeout: BANG_TIMEOUT_MS,
|
|
83
|
+
maxBuffer: BANG_MAX_BUFFER,
|
|
84
|
+
cwd,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const exitCode = res.status == null ? (res.error ? -1 : 0) : res.status;
|
|
88
|
+
const stdout = cap(res.stdout);
|
|
89
|
+
const stderr = cap(res.stderr);
|
|
90
|
+
const body = [stdout, stderr].filter(Boolean).join("\n");
|
|
91
|
+
return {
|
|
92
|
+
cmd,
|
|
93
|
+
stdout,
|
|
94
|
+
stderr,
|
|
95
|
+
exitCode,
|
|
96
|
+
error: res.error || null,
|
|
97
|
+
contextMessage: {
|
|
98
|
+
role: "user",
|
|
99
|
+
content: `<bash-input>${cmd}</bash-input>\n<bash-output exit-code="${exitCode}">\n${body}\n</bash-output>`,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Append a `#` note to the project cc.md (created at the git root — falls
|
|
106
|
+
* back to cwd outside a repo). Inserts under a `## Notes` heading, creating
|
|
107
|
+
* file/section as needed.
|
|
108
|
+
*
|
|
109
|
+
* @returns {{ target, line, created }}
|
|
110
|
+
*/
|
|
111
|
+
export function appendMemoryNote(rawLine, opts = {}) {
|
|
112
|
+
const fs = opts.deps?.fs || _deps.fs;
|
|
113
|
+
const path = opts.deps?.path || _deps.path;
|
|
114
|
+
const cwd = opts.cwd || process.cwd();
|
|
115
|
+
const note = String(rawLine).replace(/^#/, "").trim();
|
|
116
|
+
const stamp = opts.date || new Date().toISOString().slice(0, 10);
|
|
117
|
+
|
|
118
|
+
const root =
|
|
119
|
+
findProjectRoot(cwd, { deps: { fs, path } }) || path.resolve(cwd);
|
|
120
|
+
const target = opts.target || path.join(root, "cc.md");
|
|
121
|
+
const line = `- ${note} _(noted ${stamp})_`;
|
|
122
|
+
|
|
123
|
+
let text = null;
|
|
124
|
+
try {
|
|
125
|
+
text = fs.readFileSync(target, "utf-8");
|
|
126
|
+
} catch {
|
|
127
|
+
/* file does not exist yet */
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let created = false;
|
|
131
|
+
if (text == null) {
|
|
132
|
+
text = `# Project Memory\n\n## Notes\n\n${line}\n`;
|
|
133
|
+
created = true;
|
|
134
|
+
} else if (/^## Notes\s*$/m.test(text)) {
|
|
135
|
+
// insert right after the heading (keeps newest notes on top)
|
|
136
|
+
text = text.replace(/^## Notes\s*$/m, (m) => `${m}\n\n${line}`);
|
|
137
|
+
} else {
|
|
138
|
+
text = `${text.trimEnd()}\n\n## Notes\n\n${line}\n`;
|
|
139
|
+
}
|
|
140
|
+
fs.writeFileSync(target, text, "utf-8");
|
|
141
|
+
return { target, line, note, created };
|
|
142
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL `@` file-reference tab-completion (Claude-Code @-mention parity).
|
|
3
|
+
*
|
|
4
|
+
* Typing `@src/fo<TAB>` in the agent REPL completes file/dir paths the same
|
|
5
|
+
* way the @-token will later be expanded by file-ref-expander. Two candidate
|
|
6
|
+
* sources, merged:
|
|
7
|
+
* 1. the filesystem under cwd (dirs get a trailing `/` so TAB can descend)
|
|
8
|
+
* 2. the connected IDE's OPEN EDITOR TABS (when the IDE bridge is up) —
|
|
9
|
+
* the files you are working on rank first, before any fs listing.
|
|
10
|
+
*
|
|
11
|
+
* IDE tabs come from an injected async `getIdeOpenFiles()`; the completer
|
|
12
|
+
* caches the last list (TTL) and refreshes in the background, so completion
|
|
13
|
+
* stays synchronous-fast and a slow/dead IDE costs nothing (first TAB simply
|
|
14
|
+
* has no IDE entries yet).
|
|
15
|
+
*/
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
|
|
19
|
+
/** Refresh the IDE open-editors list at most this often. */
|
|
20
|
+
const IDE_CACHE_TTL_MS = 5000;
|
|
21
|
+
/** Hard cap on candidates shown per TAB. */
|
|
22
|
+
const MAX_CANDIDATES = 30;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find a trailing `@token` being typed at the end of the line. Returns
|
|
26
|
+
* `{ prefix }` (token without `@`, may be "") or null when the cursor is not
|
|
27
|
+
* on an @-token. An @ must start the line or follow whitespace, mirroring
|
|
28
|
+
* file-ref-expander's token rules.
|
|
29
|
+
*/
|
|
30
|
+
export function extractAtPrefix(line) {
|
|
31
|
+
const m = /(^|\s)@([^\s@]*)$/.exec(line || "");
|
|
32
|
+
return m ? { prefix: m[2] } : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Normalize to forward slashes (what @refs use on every platform). */
|
|
36
|
+
function fwd(p) {
|
|
37
|
+
return String(p).replace(/\\/g, "/");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Filesystem candidates for an @-prefix, relative to cwd. Directories get a
|
|
42
|
+
* trailing `/`. Missing dirs / unreadable entries → empty (best-effort).
|
|
43
|
+
*/
|
|
44
|
+
export function fileCandidates(prefix, { cwd = process.cwd(), deps } = {}) {
|
|
45
|
+
const readdir = deps?.readdir || ((d) => fs.readdirSync(d));
|
|
46
|
+
const isDir =
|
|
47
|
+
deps?.isDir ||
|
|
48
|
+
((p) => {
|
|
49
|
+
try {
|
|
50
|
+
return fs.statSync(p).isDirectory();
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const norm = fwd(prefix);
|
|
56
|
+
const slash = norm.lastIndexOf("/");
|
|
57
|
+
const dirPart = slash >= 0 ? norm.slice(0, slash + 1) : "";
|
|
58
|
+
const basePart = slash >= 0 ? norm.slice(slash + 1) : norm;
|
|
59
|
+
const absDir = path.resolve(cwd, dirPart || ".");
|
|
60
|
+
let names;
|
|
61
|
+
try {
|
|
62
|
+
names = readdir(absDir);
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const name of names) {
|
|
68
|
+
if (basePart && !name.toLowerCase().startsWith(basePart.toLowerCase())) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (name === "node_modules" || name === ".git") continue;
|
|
72
|
+
const rel = dirPart + name;
|
|
73
|
+
out.push(isDir(path.join(absDir, name)) ? `${rel}/` : rel);
|
|
74
|
+
if (out.length >= MAX_CANDIDATES) break;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build a readline completer. Returns `completer(line)` → `[hits, replaced]`
|
|
81
|
+
* per the readline contract. Completes `@path` tokens anywhere in the line
|
|
82
|
+
* and `/command` names at line start (while the command token is still being
|
|
83
|
+
* typed); everything else completes to nothing.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} opts { cwd?, getIdeOpenFiles?: () => Promise<string[]>,
|
|
86
|
+
* slashCommands?: string[], deps? }
|
|
87
|
+
*/
|
|
88
|
+
export function makeAtCompleter(opts = {}) {
|
|
89
|
+
const cwd = opts.cwd || process.cwd();
|
|
90
|
+
const getIde = opts.getIdeOpenFiles || null;
|
|
91
|
+
const now = opts.deps?.now || Date.now;
|
|
92
|
+
let ideFiles = [];
|
|
93
|
+
let ideFetchedAt = -Infinity; // "never" — the first @ must always fetch
|
|
94
|
+
let ideInFlight = false;
|
|
95
|
+
|
|
96
|
+
const refreshIde = () => {
|
|
97
|
+
if (!getIde || ideInFlight || now() - ideFetchedAt < IDE_CACHE_TTL_MS) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
ideInFlight = true;
|
|
101
|
+
Promise.resolve()
|
|
102
|
+
.then(() => getIde())
|
|
103
|
+
.then((files) => {
|
|
104
|
+
ideFiles = Array.isArray(files)
|
|
105
|
+
? files
|
|
106
|
+
.filter((f) => typeof f === "string" && f.length > 0)
|
|
107
|
+
.map((f) => {
|
|
108
|
+
const rel = path.relative(cwd, f);
|
|
109
|
+
// Keep workspace files relative (the natural @ref form);
|
|
110
|
+
// out-of-workspace files keep their absolute path.
|
|
111
|
+
return rel && !rel.startsWith("..") ? fwd(rel) : fwd(f);
|
|
112
|
+
})
|
|
113
|
+
: [];
|
|
114
|
+
ideFetchedAt = now();
|
|
115
|
+
})
|
|
116
|
+
.catch(() => {
|
|
117
|
+
ideFetchedAt = now(); // don't hammer a dead IDE
|
|
118
|
+
})
|
|
119
|
+
.finally(() => {
|
|
120
|
+
ideInFlight = false;
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const slashCommands = Array.isArray(opts.slashCommands)
|
|
125
|
+
? [...opts.slashCommands].sort()
|
|
126
|
+
: [];
|
|
127
|
+
|
|
128
|
+
const completer = (line) => {
|
|
129
|
+
// `/command` completion (Claude-Code parity): only while typing the
|
|
130
|
+
// command token itself — once a space follows, args are the user's.
|
|
131
|
+
const slash = /^\/([A-Za-z_-]*)$/.exec(line);
|
|
132
|
+
if (slash && slashCommands.length) {
|
|
133
|
+
const pref = `/${slash[1].toLowerCase()}`;
|
|
134
|
+
const hits = slashCommands.filter((c) => c.toLowerCase().startsWith(pref));
|
|
135
|
+
return [hits, line];
|
|
136
|
+
}
|
|
137
|
+
const at = extractAtPrefix(line);
|
|
138
|
+
if (!at) return [[], line];
|
|
139
|
+
refreshIde(); // async top-up for the NEXT tab; this one uses the cache
|
|
140
|
+
const norm = fwd(at.prefix).toLowerCase();
|
|
141
|
+
const fromIde = ideFiles.filter((f) => f.toLowerCase().startsWith(norm));
|
|
142
|
+
const fromFs = fileCandidates(at.prefix, { cwd, deps: opts.deps });
|
|
143
|
+
const merged = [...new Set([...fromIde, ...fromFs])].slice(
|
|
144
|
+
0,
|
|
145
|
+
MAX_CANDIDATES,
|
|
146
|
+
);
|
|
147
|
+
// readline replaces `replaced` with the chosen hit — keep the `@`.
|
|
148
|
+
return [merged.map((m) => `@${m}`), `@${at.prefix}`];
|
|
149
|
+
};
|
|
150
|
+
// test seam: expose the cache refresher state
|
|
151
|
+
completer._refreshIde = refreshIde;
|
|
152
|
+
completer._ideState = () => ({ ideFiles, ideFetchedAt });
|
|
153
|
+
return completer;
|
|
154
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detached cache refresher for the startup update notice.
|
|
3
|
+
* Spawned unref'd by update-notice.js; argv[2] = cache file path.
|
|
4
|
+
* Exits quietly on any failure — the notice is strictly best-effort.
|
|
5
|
+
*/
|
|
6
|
+
import { refreshCacheOnce } from "./update-notice.js";
|
|
7
|
+
|
|
8
|
+
refreshCacheOnce({ cacheFile: process.argv[2] })
|
|
9
|
+
.catch(() => {})
|
|
10
|
+
.finally(() => process.exit(0));
|