chainlesschain 0.162.30 → 0.162.31
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 +2 -2
- package/src/assets/web-panel/assets/{AIOps-CsNttUU7.js → AIOps-BqWP6FKu.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-lgohjckQ.js → ActionButton-CXwMgOvX.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-ccV3LAca.js → Analytics-DAebZ4IY.js} +3 -3
- package/src/assets/web-panel/assets/AppLayout-CYsqYoME.js +9 -0
- package/src/assets/web-panel/assets/{Audit-B1gFM5U9.js → Audit-BbTtX1Nf.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BeWE3ERo.js → Backup-DgqY2Eb-.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CDkPsNG2.js → BaseInput-Cq2ZuSoO.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-ztb9ia6e.js → Chat-D2kqpUyO.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-C-svYkrC.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-BcfRBlIY.js → Checkbox-_9swHpyc.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DOs99xkr.js → Codegen-Cr9YbCPl.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D1X6tYlj.js → Col--wdpCMxx.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DTksIWtz.js → Community-DuFcVnLu.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DIJtAYBO.js → Compact-1yzYeT04.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BBf7LF_k.js → Compliance-Dq3aU9Df.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-UBPXQ40s.js → Cowork-CrWcnIg8.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CkRm1jPB.js → Cron-Bh6fKZ0h.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-qALlTl7e.js → Crosschain-8ofPaWVW.js} +1 -1
- package/src/assets/web-panel/assets/{DID-CqyqVS6E.js → DID-D3EiYm3w.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-BFjEdFne.js +3 -0
- package/src/assets/web-panel/assets/{Dropdown-Cb5UzbSZ.js → Dropdown-pYVPcP6O.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CarBq8Fk.js → EmailListRenderer-zBPodwJ1.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CSiGXaZz.js → FamilyGuardDashboard-CyQTW6PW.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DUxhVoBN.js → Federation-Ctaq3zYq.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BoMQpkhx.js → FormItemContext-CWYJCLq1.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DTVqC_CX.js → GenericCardRenderer-B1g6t9R9.js} +1 -1
- package/src/assets/web-panel/assets/{Git-C_XuPtK5.js → Git-DH-v8iwd.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BZyqlqz-.js → Governance-jZxXvOs5.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DdZVUimI.js → Inference-D07LRghn.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-IzZ-jnCn.js → KnowledgeGraph-DnGtRZhx.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-koTK6eNc.js → Logs-D2pM9C4W.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-6zpJ1L8n.js → Marketplace-UyIO7C7r.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Ywc4IVks.js → McpTools-Bf1gvZPf.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-C_zB9dUa.js → Memory-C1bWj4RN.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Nc05r24L.js → MobileBridge-C_Ot1H_a.js} +2 -2
- package/src/assets/web-panel/assets/{MobileProjects-BJGxL526.js → MobileProjects-zr-PpsT_.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Im7SIcz1.js → Mtc-CnzFUz5J.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-BFFzvzMD.js → MtcAudit-CAAh99wz.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CcNEbycq.js → Multisig-D6IAg6HE.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CDH6OTXN.js → NLProgramming-BFMarxb0.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Dqg3QXcU.js → Notes-BRp9ro3t.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-CDVmK1eU.js → NotificationSettings-C0Au3Cxb.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-ISp6btRY.js +1 -0
- package/src/assets/web-panel/assets/{Organization-DJb9bRQS.js → Organization-DYoxLBRX.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CK7Q5dje.js → Overflow-rO8JJWGJ.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CJIyYfwc.js → P2P-DJleeXIK.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-uqRULcuw.js → PdhVaultBrowser-DM5qghFp.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-Crvwt6bq.js → Permissions-D5v4Beya.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DcN5OWzg.js → PersonalDataHub-c2ZTX0Pv.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-DfWJvvJW.js → Pipeline-Crrkyhpz.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DepD0S3v.js → Privacy-DZVyrJKa.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B7OKhH27.js → ProjectInit-DKg7J0gz.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BJ4ueRFv.js → ProjectSettings-3ndmTvVH.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Dl_hPdhU.js → Projects-ll5wnj2L.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Dl0FT1S3.js → Providers-BeqBVMhB.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-V2hYLhfp.js → QuickAsk-DKAAxzuA.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-8Kaiodgv.js → Recommend-Byu7IGei.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CsxB3JGg.js → Reputation-BKhWAmCu.js} +1 -1
- package/src/assets/web-panel/assets/{Row-6-x7tEYq.js → Row-BFtn11O6.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Buv6f5tw.js → RssFeed-D5a0PT0k.js} +2 -2
- package/src/assets/web-panel/assets/{Search-ABrDz84n.js → Search-DAkuaZNe.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DqOJmz18.js → Security-C79Ml2Ms.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Cq4Tda3q.js → Services-BBk_jH6-.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-n74QlyYq.js → Skeleton-Cy0VvL0M.js} +1 -1
- package/src/assets/web-panel/assets/Skills-OQNky3uI.js +1 -0
- package/src/assets/web-panel/assets/{Sla-hwRgJ99Z.js → Sla-CbX1f8xN.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B6Bs6_-8.js → SpeechSettings-BIkoUjws.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CTp2dZ0z.js → SyncSettings-DG6Swk7G.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-D70Lis6S.js → Tasks-C9R8sgyi.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Cags0ssw.js → Templates-AaJPeCIz.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BxCMzzGt.js → Tenant-jVFRofww.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-v05SDqHd.js → Terminal-DHBMzfK6.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BLUDHbBL.js → TimelineRenderer-9RFfOHSI.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-D-xKLJYv.js → Tokens-ZTfwuABF.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-B47tVIbH.js → Trigger-Xo7uZNQs.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DmRU9kfs.js → Trust-C0cTPYvn.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DzgSGs-c.js → UkeySign-DmMKio71.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C6qu58up.js → VideoEditing-DP7B-oGT.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dh8ZWx8f.js → Wallet-B1kZDARo.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DFHOVuAY.js → WebAuthn-Bo5kBx27.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-B_fyQ3Y_.js → WorkflowEditor-DGI9SNHH.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BR-WxnCQ.js → chat-y97W1CIG.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C-6RysQe.js → colors-DtTNo0sH.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-B_9_SCKN.js → compact-item-D0q0exuS.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D6rklIbE.js → createContext-D7pLFs2I.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BDK34w0I.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BrotgSvd.js → hasIn-CXjG5B2j.js} +1 -1
- package/src/assets/web-panel/assets/{index-DkpDFJRn.js → index-1dwtkcJv.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfqUsPl2.js → index-4mWZhCzz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaLhL3Tj.js → index-6np5ESBM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DigjvHuo.js → index-8jxbZupG.js} +1 -1
- package/src/assets/web-panel/assets/{index-MCmNzIC7.js → index-B3y_4OdG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWmJukRW.js → index-B4dPdrvC.js} +1 -1
- package/src/assets/web-panel/assets/index-B6SaRuCI.js +1 -0
- package/src/assets/web-panel/assets/index-B9ekWb3I.js +1 -0
- package/src/assets/web-panel/assets/{index-GzuCTHVZ.js → index-BJUf19Wd.js} +3 -3
- package/src/assets/web-panel/assets/{index-PzM_GlKb.js → index-BO644Q4S.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTQkYbir.js → index-BPXhU-jp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsDNNDBN.js → index-BU944DeT.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bwkg_EJk.js → index-B_hjkMtX.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMnomft7.js → index-BdhEYW2a.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJ70GAW2.js → index-BgmvrPJH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTh0fWI4.js → index-BgyrM0UN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAfRNHna.js → index-BnLrbXDA.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCWzUY8K.js → index-BqVjUN8b.js} +1 -1
- package/src/assets/web-panel/assets/{index-kkjq_hwC.js → index-BzCPx1cq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CK8YwdNd.js → index-CFsPe2N7.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6KqyxG1.js → index-CKrbutAQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bv9BrnD2.js → index-CSdhC7Qo.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJgp_QFo.js → index-Cbqu804A.js} +1 -1
- package/src/assets/web-panel/assets/{index-rWiOF7Iu.js → index-CkGFqlYX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrGp-4E2.js → index-Ct6xtKkc.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cn5ghmbB.js → index-DY6KLlgG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbRl_gIW.js → index-D_4WcI1V.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTpElYJs.js → index-DjCawXk1.js} +1 -1
- package/src/assets/web-panel/assets/{index-MBOwmoOi.js → index-Dr45Nm9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2yXH6vy.js → index-EaIfumgW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTCUOKu9.js → index-POaFzYGS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIiCIC2j.js → index-TrBGgrwG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdDmzoPE.js → index-YWOEx3rP.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cn21XmDt.js → index-_3wPBMKt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsWVDOd2.js → index-aarO4HT9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTpxOc5s.js → index-bVJvqDAz.js} +1 -1
- package/src/assets/web-panel/assets/{index-B85rQNYG.js → index-gFLQe31v.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZehgEQYa.js → index-kvV0f4tV.js} +1 -1
- package/src/assets/web-panel/assets/{index-E_5VXq8H.js → index-qoB3whR9.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C2v_L5na.js → initDefaultProps-BnXISaAa.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DNDqGbfr.js → motion-ChY7C0zJ.js} +1 -1
- package/src/assets/web-panel/assets/{move-xvpQ_6hJ.js → move-ByFZMFM5.js} +1 -1
- package/src/assets/web-panel/assets/{omit-Cb0FsfrO.js → omit-BYeliY1H.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BxhYpnum.js → pickAttrs-B9dcAKnu.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B3soaW4h.js → placementArrow-D3F_txz7.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B-eRSLvd.js → responsiveObserve-ClkwY7wS.js} +1 -1
- package/src/assets/web-panel/assets/{slide--cM2ZOx-.js → slide-BNgy2Eea.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DjBhfi8Q.js → statusUtils-Bv3heMCD.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-C30mMh8o.js → styleChecker-DVdlHbQm.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-f7y2Qlzs.js → useFlexGapSupport-alrRY5BK.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-iTCXoLoZ.js → useFs-CcVh0-Vu.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BH0RXmVF.js → usePersonalDataHub-CkkHPhyq.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DQtmeDXM.js → vnode-DWi0X9WN.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-vw50zkLZ.js → zoom-DCbqxxLH.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +306 -1
- package/src/commands/ask.js +35 -1
- package/src/commands/checkpoint.js +239 -0
- package/src/commands/cost.js +114 -0
- package/src/commands/session.js +22 -2
- package/src/index.js +4 -0
- package/src/lib/file-checkpoint.js +300 -0
- package/src/lib/llm-pricing.js +227 -0
- package/src/lib/personal-data-hub-wiring.js +30 -0
- package/src/lib/recent-session.js +72 -0
- package/src/lib/session-picker.js +68 -0
- package/src/repl/agent-repl.js +58 -2
- package/src/repl/chat-repl.js +16 -1
- package/src/runtime/agent-core.js +68 -31
- package/src/runtime/fallback-model.js +109 -0
- package/src/runtime/file-ref-expander.js +258 -0
- package/src/runtime/headless-runner.js +576 -0
- package/src/runtime/headless-stream.js +302 -0
- package/src/runtime/policies/agent-policy.js +6 -0
- package/src/runtime/quiet-stdout.js +35 -0
- package/src/runtime/system-prompt.js +60 -0
- package/src/assets/web-panel/assets/AppLayout-B0hl5cPk.js +0 -9
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Dlw_6n3M.js +0 -1
- package/src/assets/web-panel/assets/Dashboard-XlMpT7K_.js +0 -3
- package/src/assets/web-panel/assets/OrderTableRenderer-Bg0bkfjR.js +0 -1
- package/src/assets/web-panel/assets/Skills-CC0iozL5.js +0 -1
- package/src/assets/web-panel/assets/devWarning-BiN5HELJ.js +0 -1
- package/src/assets/web-panel/assets/index-BhxiT2LJ.js +0 -1
- package/src/assets/web-panel/assets/index-DBNSZ2oz.js +0 -1
package/src/repl/agent-repl.js
CHANGED
|
@@ -65,6 +65,9 @@ import {
|
|
|
65
65
|
agentLoop as coreAgentLoop,
|
|
66
66
|
formatToolArgs,
|
|
67
67
|
} from "../runtime/agent-core.js";
|
|
68
|
+
import { expandFileRefs } from "../runtime/file-ref-expander.js";
|
|
69
|
+
import { composeSystemPrompt } from "../runtime/system-prompt.js";
|
|
70
|
+
import { makeFallbackChatFn } from "../runtime/fallback-model.js";
|
|
68
71
|
|
|
69
72
|
/**
|
|
70
73
|
* Reference to the runtime DB for hook execution (set during startAgentRepl)
|
|
@@ -150,6 +153,26 @@ export async function startAgentRepl(options = {}) {
|
|
|
150
153
|
let provider = options.provider || "ollama";
|
|
151
154
|
const baseUrl = options.baseUrl || "http://localhost:11434";
|
|
152
155
|
const apiKey = options.apiKey || null;
|
|
156
|
+
// Extra workspace roots (--add-dir): advertised in the system prompt and
|
|
157
|
+
// spanned by search_files.
|
|
158
|
+
const additionalDirectories = Array.isArray(options.additionalDirectories)
|
|
159
|
+
? options.additionalDirectories
|
|
160
|
+
: [];
|
|
161
|
+
|
|
162
|
+
// --fallback-model: retry a turn's LLM call once on a backup model when the
|
|
163
|
+
// primary errors out (overload / network). Built once; passed into every
|
|
164
|
+
// agentLoop call via chatFn. Undefined when no fallback configured.
|
|
165
|
+
const _fallbackChatFn = options.fallbackModel
|
|
166
|
+
? makeFallbackChatFn({
|
|
167
|
+
fallbackModel: options.fallbackModel,
|
|
168
|
+
onFallback: ({ from, to, error }) =>
|
|
169
|
+
logger.info(
|
|
170
|
+
chalk.yellow(
|
|
171
|
+
`[fallback] model "${from}" failed (${error}); retrying with "${to}"`,
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
})
|
|
175
|
+
: undefined;
|
|
153
176
|
|
|
154
177
|
// Bootstrap runtime (best-effort, DB not required)
|
|
155
178
|
let db = null;
|
|
@@ -299,7 +322,18 @@ export async function startAgentRepl(options = {}) {
|
|
|
299
322
|
}
|
|
300
323
|
|
|
301
324
|
const messages = [
|
|
302
|
-
{
|
|
325
|
+
{
|
|
326
|
+
role: "system",
|
|
327
|
+
// --system-prompt replaces the built-in prompt; --append-system-prompt
|
|
328
|
+
// extends it (parity with the headless runners).
|
|
329
|
+
content: composeSystemPrompt(
|
|
330
|
+
buildSystemPrompt(process.cwd(), { additionalDirectories }),
|
|
331
|
+
{
|
|
332
|
+
systemPrompt: options.systemPrompt,
|
|
333
|
+
appendSystemPrompt: options.appendSystemPrompt,
|
|
334
|
+
},
|
|
335
|
+
),
|
|
336
|
+
},
|
|
303
337
|
];
|
|
304
338
|
|
|
305
339
|
// Deep Agents Deploy Phase 1 — load agent bundle if --bundle provided.
|
|
@@ -1418,8 +1452,28 @@ export async function startAgentRepl(options = {}) {
|
|
|
1418
1452
|
logger.verbose(`[hook] prompt rewritten by UserPromptSubmit hook`);
|
|
1419
1453
|
}
|
|
1420
1454
|
|
|
1455
|
+
// Expand @path file references into context blocks (Claude-Code parity),
|
|
1456
|
+
// so `review @src/x.js` injects the file contents. Typo'd paths are warned
|
|
1457
|
+
// about and left as-is.
|
|
1458
|
+
let userContent = effectivePrompt;
|
|
1459
|
+
try {
|
|
1460
|
+
const fileRefs = expandFileRefs(effectivePrompt, { cwd: process.cwd() });
|
|
1461
|
+
userContent = fileRefs.prompt;
|
|
1462
|
+
for (const w of fileRefs.warnings) {
|
|
1463
|
+
logger.info(chalk.yellow(`[@ref] ${w}`));
|
|
1464
|
+
}
|
|
1465
|
+
if (fileRefs.refs.length > 0) {
|
|
1466
|
+
const summary = fileRefs.refs
|
|
1467
|
+
.map((r) => `${r.rel}${r.kind === "dir" ? "/" : ""}`)
|
|
1468
|
+
.join(", ");
|
|
1469
|
+
logger.verbose(`[@ref] injected: ${summary}`);
|
|
1470
|
+
}
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
logger.verbose(`[@ref] expansion skipped: ${err.message}`);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1421
1475
|
// Add user message
|
|
1422
|
-
messages.push({ role: "user", content:
|
|
1476
|
+
messages.push({ role: "user", content: userContent });
|
|
1423
1477
|
|
|
1424
1478
|
// Slot-filling: detect intent and fill missing parameters interactively
|
|
1425
1479
|
try {
|
|
@@ -1476,9 +1530,11 @@ export async function startAgentRepl(options = {}) {
|
|
|
1476
1530
|
iterationBudget,
|
|
1477
1531
|
sessionId,
|
|
1478
1532
|
cwd: process.cwd(),
|
|
1533
|
+
additionalDirectories,
|
|
1479
1534
|
prepareCall: defaultPrepareCall,
|
|
1480
1535
|
approvalGate: _approvalGate,
|
|
1481
1536
|
mcpClient: _bundleMcpClient || undefined,
|
|
1537
|
+
chatFn: _fallbackChatFn,
|
|
1482
1538
|
});
|
|
1483
1539
|
|
|
1484
1540
|
if (sessionId && usageEvents?.length) {
|
package/src/repl/chat-repl.js
CHANGED
|
@@ -12,6 +12,7 @@ import readline from "readline";
|
|
|
12
12
|
import chalk from "chalk";
|
|
13
13
|
import { logger } from "../lib/logger.js";
|
|
14
14
|
import { BUILT_IN_PROVIDERS } from "../lib/llm-providers.js";
|
|
15
|
+
import { expandFileRefs } from "../runtime/file-ref-expander.js";
|
|
15
16
|
import {
|
|
16
17
|
streamOllama,
|
|
17
18
|
streamOpenAI,
|
|
@@ -153,8 +154,22 @@ export async function startChatRepl(options = {}) {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
// Expand @path file references into context blocks (Claude-Code parity),
|
|
158
|
+
// so `summarize @notes.md` injects the file. The model sees the expanded
|
|
159
|
+
// content; the JSONL log keeps the original line for readability.
|
|
160
|
+
let userContent = trimmed;
|
|
161
|
+
try {
|
|
162
|
+
const fileRefs = expandFileRefs(trimmed, { cwd: process.cwd() });
|
|
163
|
+
userContent = fileRefs.prompt;
|
|
164
|
+
for (const w of fileRefs.warnings) {
|
|
165
|
+
logger.info(chalk.yellow(`[@ref] ${w}`));
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
logger.verbose?.(`[@ref] expansion skipped: ${err.message}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
156
171
|
// Add user message
|
|
157
|
-
messages.push({ role: "user", content:
|
|
172
|
+
messages.push({ role: "user", content: userContent });
|
|
158
173
|
|
|
159
174
|
// Stream the response
|
|
160
175
|
process.stdout.write(chalk.blue("ai> "));
|
|
@@ -338,9 +338,12 @@ function _buildPersonaPrompt(persona, envLines, cwd) {
|
|
|
338
338
|
* 4. Default hardcoded prompt → fallback when no persona
|
|
339
339
|
*
|
|
340
340
|
* @param {string} [cwd] - working directory
|
|
341
|
+
* @param {object} [opts]
|
|
342
|
+
* @param {string[]} [opts.additionalDirectories] - extra workspace roots
|
|
343
|
+
* (absolute paths) the agent may read/search/edit beyond `cwd`.
|
|
341
344
|
* @returns {string} complete system prompt
|
|
342
345
|
*/
|
|
343
|
-
export function buildSystemPrompt(cwd) {
|
|
346
|
+
export function buildSystemPrompt(cwd, opts = {}) {
|
|
344
347
|
const dir = cwd || process.cwd();
|
|
345
348
|
|
|
346
349
|
// Check for project persona
|
|
@@ -393,6 +396,19 @@ export function buildSystemPrompt(cwd) {
|
|
|
393
396
|
// Non-critical
|
|
394
397
|
}
|
|
395
398
|
|
|
399
|
+
// Advertise extra workspace roots (--add-dir) so the model knows it may
|
|
400
|
+
// reach beyond cwd and which absolute paths to use.
|
|
401
|
+
const extraDirs = Array.isArray(opts.additionalDirectories)
|
|
402
|
+
? opts.additionalDirectories.filter(Boolean)
|
|
403
|
+
: [];
|
|
404
|
+
if (extraDirs.length > 0) {
|
|
405
|
+
prompt +=
|
|
406
|
+
`\n\n## Additional working directories\n` +
|
|
407
|
+
`Beyond the current working directory, you may read, search, and edit ` +
|
|
408
|
+
`files under these absolute roots. Pass absolute paths to access them:\n` +
|
|
409
|
+
extraDirs.map((d) => `- ${d}`).join("\n");
|
|
410
|
+
}
|
|
411
|
+
|
|
396
412
|
return prompt;
|
|
397
413
|
}
|
|
398
414
|
|
|
@@ -529,6 +545,7 @@ export async function executeTool(name, args, context = {}) {
|
|
|
529
545
|
shellPolicyOverrides: context.shellPolicyOverrides || null,
|
|
530
546
|
approvalGate: context.approvalGate || null,
|
|
531
547
|
shellConfirm: context.shellConfirm || null,
|
|
548
|
+
additionalDirectories: context.additionalDirectories || null,
|
|
532
549
|
});
|
|
533
550
|
} catch (err) {
|
|
534
551
|
if (hookDb) {
|
|
@@ -598,6 +615,7 @@ async function executeToolInner(
|
|
|
598
615
|
shellPolicyOverrides,
|
|
599
616
|
approvalGate,
|
|
600
617
|
shellConfirm,
|
|
618
|
+
additionalDirectories,
|
|
601
619
|
},
|
|
602
620
|
) {
|
|
603
621
|
const localToolDescriptor =
|
|
@@ -962,41 +980,52 @@ async function executeToolInner(
|
|
|
962
980
|
}
|
|
963
981
|
|
|
964
982
|
case "search_files": {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
983
|
+
// An explicit directory scopes the search to one root; otherwise span
|
|
984
|
+
// cwd plus any --add-dir roots so cross-package searches find matches.
|
|
985
|
+
const extraRoots = Array.isArray(additionalDirectories)
|
|
986
|
+
? additionalDirectories.filter(Boolean)
|
|
987
|
+
: [];
|
|
988
|
+
const roots = args.directory
|
|
989
|
+
? [path.resolve(cwd, args.directory)]
|
|
990
|
+
: [cwd, ...extraRoots];
|
|
991
|
+
const isContent = Boolean(args.content_search);
|
|
992
|
+
const cmd = isContent
|
|
993
|
+
? process.platform === "win32"
|
|
994
|
+
? `findstr /s /i /n "${args.pattern}" *`
|
|
995
|
+
: `grep -r -l -i "${args.pattern}" . --include="*" 2>/dev/null | head -20`
|
|
996
|
+
: process.platform === "win32"
|
|
997
|
+
? `dir /s /b *${args.pattern}* 2>NUL`
|
|
998
|
+
: `find . -name "*${args.pattern}*" -type f 2>/dev/null | head -20`;
|
|
999
|
+
|
|
1000
|
+
const hits = [];
|
|
1001
|
+
const seen = new Set();
|
|
1002
|
+
for (const root of roots) {
|
|
1003
|
+
if (hits.length >= 20) break;
|
|
1004
|
+
try {
|
|
1005
|
+
if (!fs.existsSync(root)) continue;
|
|
985
1006
|
const output = execSync(cmd, {
|
|
986
|
-
cwd:
|
|
1007
|
+
cwd: root,
|
|
987
1008
|
encoding: "utf8",
|
|
988
1009
|
timeout: 10000,
|
|
989
1010
|
});
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1011
|
+
for (const line of output.trim().split("\n")) {
|
|
1012
|
+
const v = line.trim();
|
|
1013
|
+
if (!v || seen.has(v)) continue;
|
|
1014
|
+
// Qualify with the root so multi-root results stay unambiguous.
|
|
1015
|
+
const labeled = roots.length > 1 ? `${root}: ${v}` : v;
|
|
1016
|
+
seen.add(v);
|
|
1017
|
+
hits.push(labeled);
|
|
1018
|
+
if (hits.length >= 20) break;
|
|
1019
|
+
}
|
|
1020
|
+
} catch {
|
|
1021
|
+
// No matches in this root — continue to the next.
|
|
993
1022
|
}
|
|
994
|
-
} catch {
|
|
995
|
-
return attachDescriptor({
|
|
996
|
-
files: [],
|
|
997
|
-
message: "No matches found",
|
|
998
|
-
});
|
|
999
1023
|
}
|
|
1024
|
+
|
|
1025
|
+
if (hits.length === 0) {
|
|
1026
|
+
return attachDescriptor({ files: [], message: "No matches found" });
|
|
1027
|
+
}
|
|
1028
|
+
return attachDescriptor(isContent ? { matches: hits } : { files: hits });
|
|
1000
1029
|
}
|
|
1001
1030
|
|
|
1002
1031
|
case "list_dir": {
|
|
@@ -1608,9 +1637,16 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1608
1637
|
} = options;
|
|
1609
1638
|
|
|
1610
1639
|
const persona = _loadProjectPersona(options.cwd);
|
|
1640
|
+
// Merge the project-persona deny-list with any caller-supplied deny-list
|
|
1641
|
+
// (e.g. headless `--disallowed-tools`). Without this merge the caller's
|
|
1642
|
+
// deny-list is silently dropped and the tool stays callable.
|
|
1643
|
+
const mergedDisabledTools = [
|
|
1644
|
+
...(Array.isArray(persona?.toolsDisabled) ? persona.toolsDisabled : []),
|
|
1645
|
+
...(Array.isArray(options.disabledTools) ? options.disabledTools : []),
|
|
1646
|
+
];
|
|
1611
1647
|
const tools = getAgentToolDefinitions({
|
|
1612
1648
|
names: options.enabledToolNames,
|
|
1613
|
-
disabledTools:
|
|
1649
|
+
disabledTools: mergedDisabledTools,
|
|
1614
1650
|
extraTools: [
|
|
1615
1651
|
...(options.hostManagedToolPolicy?.toolDefinitions || []),
|
|
1616
1652
|
...(options.extraToolDefinitions || []),
|
|
@@ -1842,6 +1878,7 @@ export async function* agentLoop(messages, options) {
|
|
|
1842
1878
|
shellPolicyOverrides: options.shellPolicyOverrides || null,
|
|
1843
1879
|
approvalGate: options.approvalGate || null,
|
|
1844
1880
|
shellConfirm: options.shellConfirm || null,
|
|
1881
|
+
additionalDirectories: options.additionalDirectories || null,
|
|
1845
1882
|
};
|
|
1846
1883
|
|
|
1847
1884
|
throwIfAborted(signal);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--fallback-model` support — Claude-Code parity for unattended runs.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the agent loop's LLM call so a single request that fails with a
|
|
5
|
+
* *retryable* error (overload / rate-limit / transient network) is transparently
|
|
6
|
+
* re-issued once with a backup model. Because it sits at the chatFn seam
|
|
7
|
+
* (`agentLoop` uses `options.chatFn || chatWithTools`), fallback needs no changes
|
|
8
|
+
* to the runners — the wrapped fn is passed in via `options.chatFn`.
|
|
9
|
+
*
|
|
10
|
+
* Same-provider only: the fallback swaps `options.model`, keeping the configured
|
|
11
|
+
* provider / baseUrl / apiKey. Cross-provider fallback is a larger feature.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { chatWithTools } from "./agent-core.js";
|
|
15
|
+
|
|
16
|
+
// Heuristics for "try again on a different model" — overloaded backends,
|
|
17
|
+
// rate limits, and transient connectivity. Deliberately conservative: a 4xx
|
|
18
|
+
// that is not 429 (bad request / auth) is NOT retried.
|
|
19
|
+
const RETRYABLE_PATTERNS = [
|
|
20
|
+
/overload/i,
|
|
21
|
+
/rate.?limit/i,
|
|
22
|
+
/too many requests/i,
|
|
23
|
+
/temporarily unavailable/i,
|
|
24
|
+
/\b429\b/,
|
|
25
|
+
/\b50[0234]\b/,
|
|
26
|
+
/\b529\b/,
|
|
27
|
+
/timeout/i,
|
|
28
|
+
/timed out/i,
|
|
29
|
+
/ETIMEDOUT/i,
|
|
30
|
+
/ECONNREFUSED/i,
|
|
31
|
+
/ECONNRESET/i,
|
|
32
|
+
/ENOTFOUND/i,
|
|
33
|
+
/EAI_AGAIN/i,
|
|
34
|
+
/socket hang up/i,
|
|
35
|
+
/fetch failed/i,
|
|
36
|
+
/network error/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decide whether an error from an LLM call warrants a fallback retry.
|
|
41
|
+
* @param {any} err
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function isRetryableModelError(err) {
|
|
45
|
+
if (!err) return false;
|
|
46
|
+
const status =
|
|
47
|
+
typeof err.status === "number"
|
|
48
|
+
? err.status
|
|
49
|
+
: typeof err.statusCode === "number"
|
|
50
|
+
? err.statusCode
|
|
51
|
+
: null;
|
|
52
|
+
if (status === 429) return true;
|
|
53
|
+
if (status !== null && status >= 500 && status <= 599) return true;
|
|
54
|
+
|
|
55
|
+
const parts = [
|
|
56
|
+
err.message,
|
|
57
|
+
typeof err.code === "string" ? err.code : "",
|
|
58
|
+
err.cause?.message,
|
|
59
|
+
err.cause?.code,
|
|
60
|
+
]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(" ");
|
|
63
|
+
return RETRYABLE_PATTERNS.some((re) => re.test(parts));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a chatFn that retries once on the fallback model.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} opts
|
|
70
|
+
* @param {string} opts.fallbackModel backup model name (required)
|
|
71
|
+
* @param {Function} [opts.baseChatFn=chatWithTools] underlying LLM call
|
|
72
|
+
* @param {Function} [opts.isRetryable] error predicate (testing seam)
|
|
73
|
+
* @param {Function} [opts.onFallback] notified ({from,to,error}) on retry
|
|
74
|
+
* @returns {Function} a (messages, options) => Promise<result> chatFn
|
|
75
|
+
*/
|
|
76
|
+
export function makeFallbackChatFn(opts = {}) {
|
|
77
|
+
const fallbackModel = opts.fallbackModel;
|
|
78
|
+
const baseChatFn = opts.baseChatFn || chatWithTools;
|
|
79
|
+
const isRetryable = opts.isRetryable || isRetryableModelError;
|
|
80
|
+
const onFallback = opts.onFallback;
|
|
81
|
+
|
|
82
|
+
return async function chatWithFallback(messages, options = {}) {
|
|
83
|
+
try {
|
|
84
|
+
return await baseChatFn(messages, options);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const primaryModel = options.model;
|
|
87
|
+
// Skip a no-op retry when the fallback is the same model as the primary.
|
|
88
|
+
if (
|
|
89
|
+
!fallbackModel ||
|
|
90
|
+
fallbackModel === primaryModel ||
|
|
91
|
+
!isRetryable(err)
|
|
92
|
+
) {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
if (typeof onFallback === "function") {
|
|
96
|
+
try {
|
|
97
|
+
onFallback({
|
|
98
|
+
from: primaryModel,
|
|
99
|
+
to: fallbackModel,
|
|
100
|
+
error: err?.message || String(err),
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
103
|
+
// Notification is best-effort — never mask the retry.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return await baseChatFn(messages, { ...options, model: fallbackModel });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@file` reference expander — Claude-Code-style prompt file injection.
|
|
3
|
+
*
|
|
4
|
+
* Scans a prompt for `@<path>` tokens and, for each one that resolves to a real
|
|
5
|
+
* file/dir under the working directory, appends the file contents (or a dir
|
|
6
|
+
* listing) as a `<referenced-files>` context block. The original `@token` stays
|
|
7
|
+
* in the prose so the message remains readable and the model knows what the
|
|
8
|
+
* user pointed at.
|
|
9
|
+
*
|
|
10
|
+
* Design notes:
|
|
11
|
+
* - A token only matches when `@` is at start-of-input or preceded by
|
|
12
|
+
* whitespace / an opening bracket-quote. That skips email addresses
|
|
13
|
+
* (`foo@bar.com`) and decorative `@` — the `@` there is preceded by a word
|
|
14
|
+
* char and never matches.
|
|
15
|
+
* - Non-existent paths are NOT expanded and are surfaced as warnings (so a
|
|
16
|
+
* typo'd path is visible rather than silently dropped). Decorative `@words`
|
|
17
|
+
* that happen not to be files simply produce no ref and no warning unless
|
|
18
|
+
* they look path-like (contain a slash or a dot-extension).
|
|
19
|
+
* - Files are read as UTF-8 (encoding.md rule). Binary files are skipped with
|
|
20
|
+
* a note. Oversized files are truncated with a marker.
|
|
21
|
+
* - All filesystem access goes through an injectable `deps` seam so unit tests
|
|
22
|
+
* never touch the real disk (mirrors the project's `_deps` philosophy).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fsDefault from "fs";
|
|
26
|
+
import pathDefault from "path";
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_MAX_BYTES = 100 * 1024; // 100 KB per referenced file
|
|
29
|
+
export const DEFAULT_MAX_DIR_ENTRIES = 200;
|
|
30
|
+
|
|
31
|
+
// `@` preceded by start / whitespace / opening bracket-quote, then a path run.
|
|
32
|
+
// We capture greedily up to the next whitespace or a closing bracket-quote and
|
|
33
|
+
// trim trailing sentence punctuation afterwards.
|
|
34
|
+
const TOKEN_RE = /(^|[\s("'`[{])@([^\s"'`)\]}]+)/g;
|
|
35
|
+
|
|
36
|
+
/** A raw token looks path-like if it has a directory separator or a file ext. */
|
|
37
|
+
function looksPathLike(raw) {
|
|
38
|
+
return /[\\/]/.test(raw) || /\.[A-Za-z0-9]+$/.test(raw);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Strip trailing sentence punctuation that is unlikely to be part of a path. */
|
|
42
|
+
function trimTrailingPunct(raw) {
|
|
43
|
+
return raw.replace(/[),;:!?'".]+$/g, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find unique `@path` candidates in the text, in first-seen order.
|
|
48
|
+
* @returns {Array<{raw:string}>}
|
|
49
|
+
*/
|
|
50
|
+
export function findFileRefTokens(text) {
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const out = [];
|
|
53
|
+
let m;
|
|
54
|
+
TOKEN_RE.lastIndex = 0;
|
|
55
|
+
while ((m = TOKEN_RE.exec(text)) !== null) {
|
|
56
|
+
const raw = m[2];
|
|
57
|
+
if (!raw || seen.has(raw)) continue;
|
|
58
|
+
seen.add(raw);
|
|
59
|
+
out.push({ raw });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** True when a buffer/string looks binary (has a NUL in the sampled prefix). */
|
|
65
|
+
function looksBinary(buf) {
|
|
66
|
+
const sample = buf.slice(0, 8000);
|
|
67
|
+
for (let i = 0; i < sample.length; i++) {
|
|
68
|
+
if (sample[i] === 0) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a single raw token to an injectable ref descriptor, or null when it
|
|
75
|
+
* does not resolve to an existing path. Tries the literal path first, then a
|
|
76
|
+
* trailing-punctuation-stripped variant (handles "see @config.json.").
|
|
77
|
+
*/
|
|
78
|
+
function resolveRef(raw, { cwd, fs, path, maxBytes, maxDirEntries }) {
|
|
79
|
+
const candidates = [raw];
|
|
80
|
+
const trimmed = trimTrailingPunct(raw);
|
|
81
|
+
if (trimmed && trimmed !== raw) candidates.push(trimmed);
|
|
82
|
+
|
|
83
|
+
for (const cand of candidates) {
|
|
84
|
+
const abs = path.resolve(cwd, cand);
|
|
85
|
+
let stat;
|
|
86
|
+
try {
|
|
87
|
+
stat = fs.statSync(abs);
|
|
88
|
+
} catch {
|
|
89
|
+
continue; // not this candidate
|
|
90
|
+
}
|
|
91
|
+
if (stat.isDirectory()) {
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "error",
|
|
98
|
+
raw: cand,
|
|
99
|
+
rel: cand,
|
|
100
|
+
message: `cannot read directory: ${err.message}`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const names = entries
|
|
104
|
+
.slice(0, maxDirEntries)
|
|
105
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
|
|
106
|
+
.sort();
|
|
107
|
+
const truncated = entries.length > maxDirEntries;
|
|
108
|
+
return {
|
|
109
|
+
kind: "dir",
|
|
110
|
+
raw: cand,
|
|
111
|
+
rel: cand,
|
|
112
|
+
entries: names,
|
|
113
|
+
total: entries.length,
|
|
114
|
+
truncated,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (stat.isFile()) {
|
|
118
|
+
let buf;
|
|
119
|
+
try {
|
|
120
|
+
buf = fs.readFileSync(abs);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
kind: "error",
|
|
124
|
+
raw: cand,
|
|
125
|
+
rel: cand,
|
|
126
|
+
message: `cannot read file: ${err.message}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (looksBinary(buf)) {
|
|
130
|
+
return {
|
|
131
|
+
kind: "binary",
|
|
132
|
+
raw: cand,
|
|
133
|
+
rel: cand,
|
|
134
|
+
bytes: stat.size,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const truncated = buf.length > maxBytes;
|
|
138
|
+
const content = (truncated ? buf.slice(0, maxBytes) : buf).toString(
|
|
139
|
+
"utf-8",
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
kind: "file",
|
|
143
|
+
raw: cand,
|
|
144
|
+
rel: cand,
|
|
145
|
+
bytes: stat.size,
|
|
146
|
+
content,
|
|
147
|
+
truncated,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Something exotic (socket/fifo) — treat as non-expandable.
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function escapeAttr(s) {
|
|
157
|
+
return String(s).replace(/"/g, """);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Render the resolved refs into a single `<referenced-files>` block. */
|
|
161
|
+
function renderBlock(refs) {
|
|
162
|
+
const parts = [
|
|
163
|
+
'<referenced-files note="auto-injected from @path references">',
|
|
164
|
+
];
|
|
165
|
+
for (const ref of refs) {
|
|
166
|
+
if (ref.kind === "file") {
|
|
167
|
+
const attrs =
|
|
168
|
+
`path="${escapeAttr(ref.rel)}" bytes="${ref.bytes}"` +
|
|
169
|
+
(ref.truncated
|
|
170
|
+
? ` truncated="true" shown-bytes="${DEFAULT_MAX_BYTES}"`
|
|
171
|
+
: "");
|
|
172
|
+
parts.push(`<file ${attrs}>`);
|
|
173
|
+
parts.push(ref.content);
|
|
174
|
+
if (ref.truncated) {
|
|
175
|
+
parts.push(`\n… [truncated — file is ${ref.bytes} bytes]`);
|
|
176
|
+
}
|
|
177
|
+
parts.push("</file>");
|
|
178
|
+
} else if (ref.kind === "dir") {
|
|
179
|
+
const attrs =
|
|
180
|
+
`path="${escapeAttr(ref.rel)}" entries="${ref.total}"` +
|
|
181
|
+
(ref.truncated ? ` truncated="true"` : "");
|
|
182
|
+
parts.push(`<dir ${attrs}>`);
|
|
183
|
+
parts.push(ref.entries.join("\n"));
|
|
184
|
+
if (ref.truncated) {
|
|
185
|
+
parts.push(`… [truncated — ${ref.total} entries total]`);
|
|
186
|
+
}
|
|
187
|
+
parts.push("</dir>");
|
|
188
|
+
} else if (ref.kind === "binary") {
|
|
189
|
+
parts.push(
|
|
190
|
+
`<file path="${escapeAttr(ref.rel)}" bytes="${ref.bytes}" binary="true" note="binary file — contents omitted" />`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
parts.push("</referenced-files>");
|
|
195
|
+
return parts.join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Expand `@file` references in a prompt.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} prompt
|
|
202
|
+
* @param {object} [opts]
|
|
203
|
+
* @param {string} [opts.cwd=process.cwd()]
|
|
204
|
+
* @param {number} [opts.maxBytes]
|
|
205
|
+
* @param {number} [opts.maxDirEntries]
|
|
206
|
+
* @param {object} [opts.deps] Injection seam: { fs, path }.
|
|
207
|
+
* @returns {{ prompt:string, refs:Array, warnings:string[] }}
|
|
208
|
+
* `prompt` is unchanged when nothing resolved; otherwise it is the
|
|
209
|
+
* original text + a trailing `<referenced-files>` block.
|
|
210
|
+
*/
|
|
211
|
+
export function expandFileRefs(prompt, opts = {}) {
|
|
212
|
+
const text = typeof prompt === "string" ? prompt : "";
|
|
213
|
+
if (!text || !text.includes("@")) {
|
|
214
|
+
return { prompt: text, refs: [], warnings: [] };
|
|
215
|
+
}
|
|
216
|
+
const fs = opts.deps?.fs || fsDefault;
|
|
217
|
+
const path = opts.deps?.path || pathDefault;
|
|
218
|
+
const cwd = opts.cwd || process.cwd();
|
|
219
|
+
const maxBytes = Number.isFinite(opts.maxBytes)
|
|
220
|
+
? opts.maxBytes
|
|
221
|
+
: DEFAULT_MAX_BYTES;
|
|
222
|
+
const maxDirEntries = Number.isFinite(opts.maxDirEntries)
|
|
223
|
+
? opts.maxDirEntries
|
|
224
|
+
: DEFAULT_MAX_DIR_ENTRIES;
|
|
225
|
+
|
|
226
|
+
const tokens = findFileRefTokens(text);
|
|
227
|
+
const refs = [];
|
|
228
|
+
const warnings = [];
|
|
229
|
+
|
|
230
|
+
for (const { raw } of tokens) {
|
|
231
|
+
const ref = resolveRef(raw, { cwd, fs, path, maxBytes, maxDirEntries });
|
|
232
|
+
if (!ref) {
|
|
233
|
+
// Only warn for path-like tokens; bare @mentions are left alone.
|
|
234
|
+
if (looksPathLike(raw)) {
|
|
235
|
+
warnings.push(`@${raw} — no such file or directory (left as-is)`);
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (ref.kind === "error") {
|
|
240
|
+
warnings.push(`@${ref.raw} — ${ref.message} (left as-is)`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
refs.push(ref);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (refs.length === 0) {
|
|
247
|
+
return { prompt: text, refs: [], warnings };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const block = renderBlock(refs);
|
|
251
|
+
return {
|
|
252
|
+
prompt: `${text}\n\n${block}`,
|
|
253
|
+
refs,
|
|
254
|
+
warnings,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const _deps = { fs: fsDefault, path: pathDefault };
|