@duckmind/dm-windows-x64 0.60.6 → 0.60.9

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.
Files changed (197) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +70 -123
  3. package/extensions/dm-9router-ext/src/index.js +410 -5
  4. package/extensions/dm-caveman/extensions/caveman.js +283 -12
  5. package/extensions/dm-cliproxy/index.js +182 -2
  6. package/extensions/dm-cliproxy/scripts/check-config-migration.js +66 -8
  7. package/extensions/dm-cliproxy/src/apply.js +228 -1
  8. package/extensions/dm-cliproxy/src/cache.js +42 -1
  9. package/extensions/dm-cliproxy/src/commands.js +50 -2
  10. package/extensions/dm-cliproxy/src/compat.js +81 -1
  11. package/extensions/dm-cliproxy/src/config.js +192 -2
  12. package/extensions/dm-cliproxy/src/conflicts.js +46 -1
  13. package/extensions/dm-cliproxy/src/fetch-models.js +190 -1
  14. package/extensions/dm-cliproxy/src/fetch-usage.js +41 -1
  15. package/extensions/dm-cliproxy/src/log.js +23 -1
  16. package/extensions/dm-cliproxy/src/status-quota.js +77 -1
  17. package/extensions/dm-cliproxy/src/ui-frame.js +50 -1
  18. package/extensions/dm-cliproxy/src/ui-hub/hub.js +199 -2
  19. package/extensions/dm-cliproxy/src/ui-hub/index.js +26 -2
  20. package/extensions/dm-cliproxy/src/ui-hub/shell.js +46 -1
  21. package/extensions/dm-cliproxy/src/ui-hub/view-diagnostics.js +69 -1
  22. package/extensions/dm-cliproxy/src/ui-hub/view-models.js +379 -1
  23. package/extensions/dm-cliproxy/src/ui-hub/view-usage.js +106 -1
  24. package/extensions/dm-cliproxy/src/ui-picker/catalog.js +41 -1
  25. package/extensions/dm-cliproxy/src/ui-picker/mutate.js +115 -1
  26. package/extensions/dm-cliproxy/src/ui-picker/prompt-confirm.js +35 -1
  27. package/extensions/dm-cliproxy/src/ui-picker/prompt-name.js +62 -1
  28. package/extensions/dm-cliproxy/src/ui-picker/providers.js +45 -1
  29. package/extensions/dm-cliproxy/src/ui-picker/render-text.js +34 -1
  30. package/extensions/dm-cliproxy/src/ui-picker/rows.js +57 -1
  31. package/extensions/dm-cliproxy/src/ui-setup.js +260 -2
  32. package/extensions/dm-cliproxy/src/ui-usage.js +151 -1
  33. package/extensions/dm-cliproxy/src/usage-shared-cache.js +97 -1
  34. package/extensions/dm-context/src/context.js +144 -1
  35. package/extensions/dm-context/src/index.js +339 -7
  36. package/extensions/dm-context/src/utils.js +9 -1
  37. package/extensions/dm-cua/bin/browser-cua.mjs +73 -8
  38. package/extensions/dm-cua/index.js +75 -6
  39. package/extensions/dm-cua/src/browser-cua-lib.mjs +490 -6
  40. package/extensions/dm-cua/src/browser-install.mjs +331 -2
  41. package/extensions/dm-fff/src/index.js +688 -12
  42. package/extensions/dm-fff/src/query.js +60 -1
  43. package/extensions/dm-goal/src/goal.js +894 -23
  44. package/extensions/dm-image2/index.js +103 -9
  45. package/extensions/dm-image2/src/image-lib.mjs +1275 -8
  46. package/extensions/dm-subagents/install.mjs +62 -8
  47. package/extensions/dm-subagents/src/agents/agent-management.js +1190 -36
  48. package/extensions/dm-subagents/src/agents/agent-memory.js +216 -6
  49. package/extensions/dm-subagents/src/agents/agent-scope.js +5 -1
  50. package/extensions/dm-subagents/src/agents/agent-selection.js +20 -1
  51. package/extensions/dm-subagents/src/agents/agent-serializer.js +120 -5
  52. package/extensions/dm-subagents/src/agents/agents.js +1139 -11
  53. package/extensions/dm-subagents/src/agents/chain-serializer.js +299 -11
  54. package/extensions/dm-subagents/src/agents/frontmatter.js +65 -6
  55. package/extensions/dm-subagents/src/agents/identity.js +29 -1
  56. package/extensions/dm-subagents/src/agents/proactive-skills.js +141 -1
  57. package/extensions/dm-subagents/src/agents/skills.js +614 -6
  58. package/extensions/dm-subagents/src/extension/config.js +35 -2
  59. package/extensions/dm-subagents/src/extension/control-notices.js +69 -4
  60. package/extensions/dm-subagents/src/extension/doctor.js +172 -15
  61. package/extensions/dm-subagents/src/extension/fanout-child.js +158 -231
  62. package/extensions/dm-subagents/src/extension/index.js +521 -359
  63. package/extensions/dm-subagents/src/extension/rpc.js +266 -7
  64. package/extensions/dm-subagents/src/extension/schemas.js +275 -1
  65. package/extensions/dm-subagents/src/extension/tool-description.js +111 -6
  66. package/extensions/dm-subagents/src/intercom/intercom-bridge.js +126 -4
  67. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +452 -5
  68. package/extensions/dm-subagents/src/intercom/result-intercom.js +319 -3
  69. package/extensions/dm-subagents/src/profiles/profiles.js +458 -3
  70. package/extensions/dm-subagents/src/runs/background/async-execution.js +834 -40
  71. package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +435 -14
  72. package/extensions/dm-subagents/src/runs/background/async-resume.js +334 -8
  73. package/extensions/dm-subagents/src/runs/background/async-status.js +313 -12
  74. package/extensions/dm-subagents/src/runs/background/chain-append.js +245 -2
  75. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +136 -1
  76. package/extensions/dm-subagents/src/runs/background/completion-batcher.js +94 -1
  77. package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +54 -1
  78. package/extensions/dm-subagents/src/runs/background/control-channel.js +190 -1
  79. package/extensions/dm-subagents/src/runs/background/fleet-view.js +483 -17
  80. package/extensions/dm-subagents/src/runs/background/notify.js +129 -3
  81. package/extensions/dm-subagents/src/runs/background/parallel-groups.js +34 -1
  82. package/extensions/dm-subagents/src/runs/background/result-watcher.js +236 -6
  83. package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +76 -4
  84. package/extensions/dm-subagents/src/runs/background/run-status.js +427 -23
  85. package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +487 -4
  86. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +306 -9
  87. package/extensions/dm-subagents/src/runs/background/subagent-runner.js +2849 -73
  88. package/extensions/dm-subagents/src/runs/background/top-level-async.js +5 -1
  89. package/extensions/dm-subagents/src/runs/background/wait.js +206 -11
  90. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +1013 -12
  91. package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +980 -101
  92. package/extensions/dm-subagents/src/runs/foreground/execution.js +1165 -45
  93. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +3157 -222
  94. package/extensions/dm-subagents/src/runs/shared/acceptance.js +835 -3
  95. package/extensions/dm-subagents/src/runs/shared/chain-outputs.js +104 -1
  96. package/extensions/dm-subagents/src/runs/shared/completion-guard.js +116 -3
  97. package/extensions/dm-subagents/src/runs/shared/dm-args.js +208 -1
  98. package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +90 -1
  99. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +282 -1
  100. package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +148 -1
  101. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +305 -1
  102. package/extensions/dm-subagents/src/runs/shared/model-fallback.js +194 -1
  103. package/extensions/dm-subagents/src/runs/shared/model-scope.js +65 -1
  104. package/extensions/dm-subagents/src/runs/shared/nested-events.js +851 -8
  105. package/extensions/dm-subagents/src/runs/shared/nested-path.js +41 -1
  106. package/extensions/dm-subagents/src/runs/shared/nested-render.js +105 -1
  107. package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +81 -4
  108. package/extensions/dm-subagents/src/runs/shared/run-history.js +51 -4
  109. package/extensions/dm-subagents/src/runs/shared/single-output.js +149 -8
  110. package/extensions/dm-subagents/src/runs/shared/structured-output.js +58 -1
  111. package/extensions/dm-subagents/src/runs/shared/subagent-control.js +166 -5
  112. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +329 -13
  113. package/extensions/dm-subagents/src/runs/shared/tool-budget.js +73 -1
  114. package/extensions/dm-subagents/src/runs/shared/turn-budget.js +47 -4
  115. package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +196 -1
  116. package/extensions/dm-subagents/src/runs/shared/worktree.js +435 -3
  117. package/extensions/dm-subagents/src/shared/artifacts.js +92 -2
  118. package/extensions/dm-subagents/src/shared/atomic-json.js +55 -1
  119. package/extensions/dm-subagents/src/shared/child-transcript.js +167 -5
  120. package/extensions/dm-subagents/src/shared/file-coalescer.js +25 -1
  121. package/extensions/dm-subagents/src/shared/fork-context.js +147 -4
  122. package/extensions/dm-subagents/src/shared/formatters.js +98 -7
  123. package/extensions/dm-subagents/src/shared/jsonl-writer.js +56 -2
  124. package/extensions/dm-subagents/src/shared/model-info.js +62 -1
  125. package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.js +68 -1
  126. package/extensions/dm-subagents/src/shared/session-identity.js +6 -1
  127. package/extensions/dm-subagents/src/shared/session-tokens.js +39 -2
  128. package/extensions/dm-subagents/src/shared/settings.js +198 -11
  129. package/extensions/dm-subagents/src/shared/status-format.js +53 -1
  130. package/extensions/dm-subagents/src/shared/types.js +184 -6
  131. package/extensions/dm-subagents/src/shared/utils.js +462 -2
  132. package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +288 -1
  133. package/extensions/dm-subagents/src/slash/prompt-workflows.js +297 -7
  134. package/extensions/dm-subagents/src/slash/slash-bridge.js +118 -1
  135. package/extensions/dm-subagents/src/slash/slash-commands.js +1287 -31
  136. package/extensions/dm-subagents/src/slash/slash-live-state.js +240 -4
  137. package/extensions/dm-subagents/src/tui/render-helpers.js +64 -1
  138. package/extensions/dm-subagents/src/tui/render.js +1542 -4
  139. package/extensions/dm-usage/index.js +1294 -9
  140. package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +40 -9
  141. package/extensions/greedysearch-dm/bin/cdp-headless.mjs +5 -2
  142. package/extensions/greedysearch-dm/bin/cdp-visible.mjs +5 -2
  143. package/extensions/greedysearch-dm/bin/cdp.mjs +896 -30
  144. package/extensions/greedysearch-dm/bin/gschrome.mjs +30 -2
  145. package/extensions/greedysearch-dm/bin/kill-visible.mjs +7 -2
  146. package/extensions/greedysearch-dm/bin/launch-visible.mjs +13 -2
  147. package/extensions/greedysearch-dm/bin/launch.mjs +282 -10
  148. package/extensions/greedysearch-dm/bin/mcp.mjs +386 -361
  149. package/extensions/greedysearch-dm/bin/search.mjs +620 -540
  150. package/extensions/greedysearch-dm/bin/visible.mjs +22 -2
  151. package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +329 -579
  152. package/extensions/greedysearch-dm/extractors/chatgpt.mjs +301 -583
  153. package/extensions/greedysearch-dm/extractors/common.mjs +408 -32
  154. package/extensions/greedysearch-dm/extractors/consensus.mjs +376 -365
  155. package/extensions/greedysearch-dm/extractors/consent.mjs +303 -14
  156. package/extensions/greedysearch-dm/extractors/gemini.mjs +228 -592
  157. package/extensions/greedysearch-dm/extractors/google-ai.mjs +78 -499
  158. package/extensions/greedysearch-dm/extractors/logically.mjs +270 -347
  159. package/extensions/greedysearch-dm/extractors/perplexity.mjs +243 -581
  160. package/extensions/greedysearch-dm/extractors/selectors.mjs +32 -1
  161. package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +130 -317
  162. package/extensions/greedysearch-dm/index.js +123 -23
  163. package/extensions/greedysearch-dm/src/fetcher.mjs +576 -2
  164. package/extensions/greedysearch-dm/src/formatters/results.js +95 -10
  165. package/extensions/greedysearch-dm/src/formatters/sources.js +57 -1
  166. package/extensions/greedysearch-dm/src/formatters/synthesis.js +49 -1
  167. package/extensions/greedysearch-dm/src/github.mjs +222 -7
  168. package/extensions/greedysearch-dm/src/reddit.mjs +145 -14
  169. package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +340 -9
  170. package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +112 -4
  171. package/extensions/greedysearch-dm/src/search/chrome.mjs +486 -285
  172. package/extensions/greedysearch-dm/src/search/constants.mjs +109 -8
  173. package/extensions/greedysearch-dm/src/search/defaults.mjs +10 -1
  174. package/extensions/greedysearch-dm/src/search/engines.mjs +79 -9
  175. package/extensions/greedysearch-dm/src/search/fetch-source.mjs +441 -349
  176. package/extensions/greedysearch-dm/src/search/file-sources.mjs +29 -7
  177. package/extensions/greedysearch-dm/src/search/minimize.mjs +86 -1
  178. package/extensions/greedysearch-dm/src/search/output.mjs +51 -5
  179. package/extensions/greedysearch-dm/src/search/paths.mjs +48 -1
  180. package/extensions/greedysearch-dm/src/search/pdf.mjs +63 -2
  181. package/extensions/greedysearch-dm/src/search/port-pid.mjs +69 -1
  182. package/extensions/greedysearch-dm/src/search/progress.mjs +109 -2
  183. package/extensions/greedysearch-dm/src/search/query.mjs +21 -1
  184. package/extensions/greedysearch-dm/src/search/recovery.mjs +49 -1
  185. package/extensions/greedysearch-dm/src/search/research.mjs +2227 -458
  186. package/extensions/greedysearch-dm/src/search/scale-aware.mjs +61 -11
  187. package/extensions/greedysearch-dm/src/search/simple-research.mjs +396 -805
  188. package/extensions/greedysearch-dm/src/search/sources.mjs +412 -1
  189. package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +127 -12
  190. package/extensions/greedysearch-dm/src/search/synthesis.mjs +202 -12
  191. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +209 -23
  192. package/extensions/greedysearch-dm/src/tools/shared.js +226 -10
  193. package/extensions/greedysearch-dm/src/utils/content.mjs +35 -4
  194. package/extensions/greedysearch-dm/src/utils/helpers.js +22 -1
  195. package/extensions/greedysearch-dm/src/utils/node-runtime.mjs +10 -1
  196. package/extensions/greedysearch-dm/src/utils/system-cmds.mjs +61 -1
  197. package/package.json +1 -1
@@ -1,15 +1,691 @@
1
- import{Text as L}from"@duckmind/dm-tui";import{FileFinder as Kj}from"@ff-labs/fff-node";import{Type as W}from"@sinclair/typebox";import w from"node:path";function c(J,$=process.cwd()){let Z=J.trim();if(!Z)return Z;if(w.isAbsolute(Z)){let k=w.relative($,Z).replaceAll(w.sep,"/");if(k==="")return null;if(k.startsWith("../")||k===".."||w.isAbsolute(k))throw Error(`Path constraint must be relative to the workspace: ${J}`);Z=k}if(Z==="."||Z==="./")return null;if(Z.startsWith("./"))Z=Z.slice(2);let G=Z.match(/^(.*)\/\*\*(?:\/\*)?$/);if(G){let k=G[1];if(k&&!/[*?[{]/.test(k))return`${k}/`}if(Z.startsWith("/")||Z.endsWith("/"))return Z;if(/[*?[{]/.test(Z))return Z;let B=Z.split("/").pop()??"";if(/\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(B))return Z;return`${Z}/`}function $j(J,$=process.cwd()){if(!J)return[];let Z=Array.isArray(J)?J:[J],G=[];for(let B of Z){let k=B.split(/[,\s]+/).map((V)=>V.trim()).filter(Boolean);for(let V of k){let z=V.startsWith("!")?V.slice(1):V,P=c(z,$);if(P)G.push(`!${P}`)}}return G}function x(J,$,Z,G=process.cwd()){let B=[];if(J){let k=c(J,G);if(k)B.push(k)}return B.push(...$j(Z,G)),B.push($),B.join(" ")}var E=20,f=30,Zj=500,p=20,h=["tools-and-ui","tools-only","override"],Hj={grep:"ffgrep",find:"fffind",multiGrep:"fff-multi-grep"},Qj={grep:"grep",find:"find",multiGrep:"multi_grep"};function Wj(J){return J==="override"?Qj:Hj}var _=new Map,Yj=0;function u(J){let $=`fff_c${++Yj}`;if(_.set($,J),_.size>200){let Z=_.keys().next().value;if(Z)_.delete(Z)}return $}function d(J){return _.get(J)}var M=new Map,Bj=0;function Xj(J){let $=`${++Bj}`;if(M.set($,J),M.size>200){let Z=M.keys().next().value;if(Z)M.delete(Z)}return $}function kj(J){return M.get(J)}function l(J,$=Zj){let Z=J.trim();return Z.length<=$?Z:`${Z.slice(0,$)}...`}var qj=25,Vj=20;function n(J){let $=J.gitStatus;if($&&$!=="clean"&&$!=="unknown"&&$!=="")return` [${$} in git]`;let Z=J.totalFrecencyScore??J.accessFrecencyScore??0;if(Z>=qj)return" [VERY often touched file]";if(Z>=Vj)return" [often touched file]";return""}function o(J){if(J.items.length===0)return"No matches found";let $=[],Z="",G=0;for(let B of J.items){if(B.relativePath!==Z){if($.length>0)$.push("");Z=B.relativePath,$.push(`${Z}${n(B)}`)}B.contextBefore?.forEach((k,V)=>{let z=B.lineNumber-B.contextBefore.length+V;$.push(` ${z}- ${l(k)}`)}),$.push(` ${B.lineNumber}: ${l(B.lineContent)}`),G++,B.contextAfter?.forEach((k,V)=>{let z=B.lineNumber+1+V;$.push(` ${z}- ${l(k)}`)})}return $.join(`
2
- `)}var Uj=5;function Gj(J){let $=J.length*12;return Math.floor($*50/100)}function bj(J,$,Z){if(J.items.length===0)return{output:"No files found matching pattern",weak:!1,shownCount:0};let G=J.items.map((P)=>({item:P})),k=(J.scores[0]?.total??0)<Gj(Z),V=k?Math.min(Uj,$):$,z=G.slice(0,V);return{output:z.map((P)=>`${P.item.relativePath}${n(P.item)}`).join(`
3
- `),weak:k,shownCount:z.length}}function Oj(J){return J.match(/(?:^|[ \t])(@(?:"[^"]*|[^\s]*))$/)?.[1]??null}function m(J){return J.includes(" ")?`@"${J}"`:`@${J}`}function zj(J){return{async getSuggestions($,Z,G,B){let k=$[Z]||"",V=Oj(k.slice(0,G));if(!V||B.signal.aborted)return null;let z=V.startsWith('@"')?V.slice(2):V.slice(1),P=await J(z,B.signal);return B.signal.aborted||P.length===0?null:{items:P,prefix:V}},applyCompletion($,Z,G,B,k){let V=$[Z]||"",z=V.slice(0,G-k.length),P=V.slice(G),C=z+B.value+P,y=G-k.length+B.value.length;return{lines:[...$.slice(0,Z),C,...$.slice(Z+1)],cursorLine:Z,cursorCol:y}}}}function Pj(J){let $=null,Z=null,G=null,B=process.cwd(),k=J.getFlag("fff-mode")??process.env.DM_FFF_MODE??"tools-and-ui",V=Wj(k),z=J.getFlag("fff-frecency-db")??process.env.FFF_FRECENCY_DB??void 0,P=J.getFlag("fff-history-db")??process.env.FFF_HISTORY_DB??void 0;function C(Y,j){let K=J.getFlag(Y);if(typeof K==="boolean")return K;if(typeof K==="string")return K==="true"||K==="1";let H=process.env[j];return H==="1"||H==="true"}let y=C("fff-enable-root-scan","FFF_ENABLE_ROOT_SCAN");function T(){return k}function a(Y){k=Y}function i(){return k!=="tools-only"}function F(Y){if($&&!$.isDestroyed&&Z===Y)return Promise.resolve($);if(G)return G;return G=(async()=>{if($&&!$.isDestroyed)$.destroy(),$=null,Z=null;let j=Kj.create({basePath:Y,frecencyDbPath:z,historyDbPath:P,aiMode:!0,enableHomeDirScanning:!0,enableFsRootScanning:y});if(!j.ok)throw Error(`Failed to create FFF file finder: ${j.error}`);return $=j.value,Z=Y,await $.waitForScan(15000),$})().finally(()=>{G=null}),G}function s(){if($&&!$.isDestroyed)$.destroy(),$=null,Z=null}async function r(Y,j){if(j.aborted)return[];let K=await F(B);if(j.aborted)return[];let H=K.mixedSearch(Y,{pageSize:p});if(!H.ok)return[];return H.value.items.slice(0,p).map((Q)=>{if(Q.type==="directory")return{value:m(Q.item.relativePath),label:Q.item.dirName,description:Q.item.relativePath};return{value:m(Q.item.relativePath),label:Q.item.fileName,description:Q.item.relativePath}})}function t(Y){Y.ui.addAutocompleteProvider((j)=>{let K=zj(r);return{async getSuggestions(H,Q,X,U){if(i())try{let q=await K.getSuggestions(H,Q,X,U);if(q)return q}catch{}return j.getSuggestions(H,Q,X,U)},applyCompletion(H,Q,X,U,q){return j.applyCompletion(H,Q,X,U,q)},shouldTriggerFileCompletion(H,Q,X){return j.shouldTriggerFileCompletion?.(H,Q,X)??!0}}})}J.registerFlag("fff-mode",{description:"FFF mode: tools-and-ui | tools-only | override",type:"string"}),J.registerFlag("fff-frecency-db",{description:"Path to the frecency database (overrides FFF_FRECENCY_DB env)",type:"string"}),J.registerFlag("fff-history-db",{description:"Path to the query history database (overrides FFF_HISTORY_DB env)",type:"string"}),J.registerFlag("fff-enable-root-scan",{description:"Allow indexing when launched from the filesystem root (also: FFF_ENABLE_ROOT_SCAN env)",type:"boolean"}),J.on("session_start",async(Y,j)=>{try{B=j.cwd;let K=j.sessionManager?.getEntries();if(K){let H=[...K].reverse().find((Q)=>Q.type==="custom"&&Q.customType==="fff-mode");if(H&&typeof H.data?.mode==="string"&&h.includes(H.data.mode)){let Q=H.data.mode;if(Q!==k)k=Q}}t(j),await F(B)}catch(K){j.ui.notify(`FFF init failed: ${K instanceof Error?K.message:String(K)}`,"error")}}),J.on("session_shutdown",async()=>{s()});let g=(Y,j,K,H,Q=15)=>{let X=H.lastComponent??new L("",0,0),U=Y.content?.find((O)=>O.type==="text")?.text?.trim()??"";if(!U)return X.setText(K.fg("muted","No output")),X;let q=U.split(`
4
- `),b=q.slice(0,j.expanded?q.length:Q),S=`
5
- ${b.map((O)=>K.fg("toolOutput",O)).join(`
6
- `)}`;if(q.length>b.length)S+=K.fg("muted",`
7
- ... (${q.length-b.length} more lines)`);return X.setText(S),X},e=W.Object({pattern:W.String({description:"Search pattern (literal text or regex)"}),path:W.Optional(W.String({description:"Repo-relative path constraint. Directory prefix (src/ or src/foo/), bare filename with extension (main.rs), or glob (*.ts, src/**/*.cc, {src,lib}/**). Applied to the full repo-relative path."})),exclude:W.Optional(W.Union([W.String(),W.Array(W.String())],{description:"Exclude paths (comma/space-separated or array). Same syntax as path: directory prefix ('test/'), filename with extension ('config.json'), or glob ('*.min.js', '**/*.{rs,go}'). A leading '!' is optional and ignored — both 'test/' and '!test/' work. Example: 'test/,*.min.js,!vendor/'."})),caseSensitive:W.Optional(W.Boolean({description:"Force case-sensitive matching. Default uses smart-case (case-insensitive when pattern is all lowercase)."})),context:W.Optional(W.Number({description:"Context lines before+after each match"})),limit:W.Optional(W.Number({description:`Max matches (default ${E})`})),cursor:W.Optional(W.String({description:"Pagination cursor from previous result"}))});J.registerTool({name:V.grep,label:V.grep,description:`Grep file contents. Smart-case, auto-detects regex vs literal, git-aware. Results are ranked by frecency (most-accessed files first); matches within a file stay in source order. Default limit ${E}.`,promptSnippet:"Grep contents",promptGuidelines:["Prefer bare identifiers as patterns. Literal queries are most efficient.","Use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').","caseSensitive: true when you need exact case (smart-case otherwise).","After 1-2 greps, read the top match instead of more greps."],parameters:e,async execute(Y,j,K){if(K?.aborted)throw Error("Operation aborted");let H=await F(B),Q=Math.max(1,j.limit??E),X=x(j.path,j.pattern,j.exclude,B),U=j.pattern!==j.pattern.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),q=U?"regex":"plain";if(q==="regex")try{new RegExp(j.pattern)}catch{q="plain"}let b=j.pattern.trim();if(U&&/^(?:[.^$]*(?:[.][*+?]|\*|\+)[.^$]*|[.^$\s]*|\.\*\??|\.\*[+?]?|\.\+\??|\.|\*|\?)$/.test(b))return{content:[{type:"text",text:`Pattern '${j.pattern}' matches everything — grep needs a concrete substring or identifier. Example: \`pattern: 'MyClass'\` or \`pattern: 'export function'\`.`}],details:{totalMatched:0,totalFiles:0}};let O=j.caseSensitive!==!0,A=H.grep(X,{mode:q,smartCase:O,maxMatchesPerFile:Math.min(Q,50),cursor:(j.cursor?d(j.cursor):null)??null,beforeContext:j.context??0,afterContext:j.context??0,classifyDefinitions:!0});if(!A.ok)throw Error(A.error);let D=A.value,v=null;if(D.items.length===0&&!j.cursor&&q!=="regex"){let I=H.grep(j.pattern,{mode:"fuzzy",smartCase:O,maxMatchesPerFile:Math.min(Q,50),cursor:null,beforeContext:0,afterContext:0,classifyDefinitions:!0});if(I.ok&&I.value.items.length>0)v="0 exact matches. Maybe you meant this?",D=I.value}let N=o(D),R=[];if(D.regexFallbackError)R.push(`Invalid regex: ${D.regexFallbackError}, used literal match`);if(D.nextCursor)R.push(`Continue with cursor="${u(D.nextCursor)}"`);if(R.length>0)N+=`
1
+ import {
2
+ Text
3
+ } from "@duckmind/dm-tui";
4
+ import { FileFinder } from "@ff-labs/fff-node";
5
+ import { Type } from "@sinclair/typebox";
6
+ import { buildQuery } from "./query";
7
+ const DEFAULT_GREP_LIMIT = 20;
8
+ const DEFAULT_FIND_LIMIT = 30;
9
+ const GREP_MAX_LINE_LENGTH = 500;
10
+ const MENTION_MAX_RESULTS = 20;
11
+ const VALID_MODES = ["tools-and-ui", "tools-only", "override"];
12
+ const FFF_TOOL_NAMES = {
13
+ grep: "ffgrep",
14
+ find: "fffind",
15
+ multiGrep: "fff-multi-grep"
16
+ };
17
+ const OVERRIDE_TOOL_NAMES = {
18
+ grep: "grep",
19
+ find: "find",
20
+ multiGrep: "multi_grep"
21
+ };
22
+ function resolveToolNames(mode) {
23
+ return mode === "override" ? OVERRIDE_TOOL_NAMES : FFF_TOOL_NAMES;
24
+ }
25
+ const cursorCache = new Map;
26
+ let cursorCounter = 0;
27
+ function storeCursor(cursor) {
28
+ const id = `fff_c${++cursorCounter}`;
29
+ cursorCache.set(id, cursor);
30
+ if (cursorCache.size > 200) {
31
+ const first = cursorCache.keys().next().value;
32
+ if (first)
33
+ cursorCache.delete(first);
34
+ }
35
+ return id;
36
+ }
37
+ function getCursor(id) {
38
+ return cursorCache.get(id);
39
+ }
40
+ const findCursorCache = new Map;
41
+ let findCursorCounter = 0;
42
+ function storeFindCursor(cursor) {
43
+ const id = `${++findCursorCounter}`;
44
+ findCursorCache.set(id, cursor);
45
+ if (findCursorCache.size > 200) {
46
+ const first = findCursorCache.keys().next().value;
47
+ if (first)
48
+ findCursorCache.delete(first);
49
+ }
50
+ return id;
51
+ }
52
+ function getFindCursor(id) {
53
+ return findCursorCache.get(id);
54
+ }
55
+ function truncateLine(line, max = GREP_MAX_LINE_LENGTH) {
56
+ const trimmed = line.trim();
57
+ return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max)}...`;
58
+ }
59
+ const HOT_FRECENCY = 25;
60
+ const WARM_FRECENCY = 20;
61
+ export function fffFileAnnotation(item) {
62
+ const git = item.gitStatus;
63
+ if (git && git !== "clean" && git !== "unknown" && git !== "") {
64
+ return ` [${git} in git]`;
65
+ }
66
+ const frecency = item.totalFrecencyScore ?? item.accessFrecencyScore ?? 0;
67
+ if (frecency >= HOT_FRECENCY)
68
+ return " [VERY often touched file]";
69
+ if (frecency >= WARM_FRECENCY)
70
+ return " [often touched file]";
71
+ return "";
72
+ }
73
+ function formatGrepOutput(result) {
74
+ if (result.items.length === 0)
75
+ return "No matches found";
76
+ const lines = [];
77
+ let currentFile = "";
78
+ let shown = 0;
79
+ for (const match of result.items) {
80
+ if (match.relativePath !== currentFile) {
81
+ if (lines.length > 0)
82
+ lines.push("");
83
+ currentFile = match.relativePath;
84
+ lines.push(`${currentFile}${fffFileAnnotation(match)}`);
85
+ }
86
+ match.contextBefore?.forEach((line, i) => {
87
+ const lineNum = match.lineNumber - match.contextBefore.length + i;
88
+ lines.push(` ${lineNum}- ${truncateLine(line)}`);
89
+ });
90
+ lines.push(` ${match.lineNumber}: ${truncateLine(match.lineContent)}`);
91
+ shown++;
92
+ match.contextAfter?.forEach((line, i) => {
93
+ const lineNum = match.lineNumber + 1 + i;
94
+ lines.push(` ${lineNum}- ${truncateLine(line)}`);
95
+ });
96
+ }
97
+ return lines.join(`
98
+ `);
99
+ }
100
+ const FIND_WEAK_SAMPLE_SIZE = 5;
101
+ function weakScoreThreshold(pattern) {
102
+ const perfect = pattern.length * 12;
103
+ return Math.floor(perfect * 50 / 100);
104
+ }
105
+ function formatFindOutput(result, limit, pattern) {
106
+ if (result.items.length === 0) {
107
+ return {
108
+ output: "No files found matching pattern",
109
+ weak: false,
110
+ shownCount: 0
111
+ };
112
+ }
113
+ const reordered = result.items.map((item) => ({ item }));
114
+ const topScore = result.scores[0]?.total ?? 0;
115
+ const weak = topScore < weakScoreThreshold(pattern);
116
+ const effective = weak ? Math.min(FIND_WEAK_SAMPLE_SIZE, limit) : limit;
117
+ const shown = reordered.slice(0, effective);
118
+ return {
119
+ output: shown.map((p) => `${p.item.relativePath}${fffFileAnnotation(p.item)}`).join(`
120
+ `),
121
+ weak,
122
+ shownCount: shown.length
123
+ };
124
+ }
125
+ function extractAtPrefix(textBeforeCursor) {
126
+ const match = textBeforeCursor.match(/(?:^|[ \t])(@(?:"[^"]*|[^\s]*))$/);
127
+ return match?.[1] ?? null;
128
+ }
129
+ function buildAtCompletionValue(path) {
130
+ return path.includes(" ") ? `@"${path}"` : `@${path}`;
131
+ }
132
+ function createFffMentionProvider(getItems) {
133
+ return {
134
+ async getSuggestions(lines, cursorLine, cursorCol, options) {
135
+ const currentLine = lines[cursorLine] || "";
136
+ const prefix = extractAtPrefix(currentLine.slice(0, cursorCol));
137
+ if (!prefix || options.signal.aborted)
138
+ return null;
139
+ const query = prefix.startsWith('@"') ? prefix.slice(2) : prefix.slice(1);
140
+ const items = await getItems(query, options.signal);
141
+ return options.signal.aborted || items.length === 0 ? null : { items, prefix };
142
+ },
143
+ applyCompletion(_lines, cursorLine, cursorCol, item, prefix) {
144
+ const currentLine = _lines[cursorLine] || "";
145
+ const before = currentLine.slice(0, cursorCol - prefix.length);
146
+ const after = currentLine.slice(cursorCol);
147
+ const newLine = before + item.value + after;
148
+ const newCursorCol = cursorCol - prefix.length + item.value.length;
149
+ return {
150
+ lines: [..._lines.slice(0, cursorLine), newLine, ..._lines.slice(cursorLine + 1)],
151
+ cursorLine,
152
+ cursorCol: newCursorCol
153
+ };
154
+ }
155
+ };
156
+ }
157
+ export default function fffExtension(dm) {
158
+ let finder = null;
159
+ let finderCwd = null;
160
+ let finderPromise = null;
161
+ let activeCwd = process.cwd();
162
+ let currentMode = dm.getFlag("fff-mode") ?? process.env.DM_FFF_MODE ?? "tools-and-ui";
163
+ const toolNames = resolveToolNames(currentMode);
164
+ const frecencyDbPath = dm.getFlag("fff-frecency-db") ?? process.env.FFF_FRECENCY_DB ?? undefined;
165
+ const historyDbPath = dm.getFlag("fff-history-db") ?? process.env.FFF_HISTORY_DB ?? undefined;
166
+ function resolveBoolOpt(flagName, envName) {
167
+ const flag = dm.getFlag(flagName);
168
+ if (typeof flag === "boolean")
169
+ return flag;
170
+ if (typeof flag === "string")
171
+ return flag === "true" || flag === "1";
172
+ const env = process.env[envName];
173
+ return env === "1" || env === "true";
174
+ }
175
+ const enableFsRootScanning = resolveBoolOpt("fff-enable-root-scan", "FFF_ENABLE_ROOT_SCAN");
176
+ function getMode() {
177
+ return currentMode;
178
+ }
179
+ function setMode(mode) {
180
+ currentMode = mode;
181
+ }
182
+ function shouldEnableMentions() {
183
+ return currentMode !== "tools-only";
184
+ }
185
+ function ensureFinder(cwd) {
186
+ if (finder && !finder.isDestroyed && finderCwd === cwd)
187
+ return Promise.resolve(finder);
188
+ if (finderPromise)
189
+ return finderPromise;
190
+ finderPromise = (async () => {
191
+ if (finder && !finder.isDestroyed) {
192
+ finder.destroy();
193
+ finder = null;
194
+ finderCwd = null;
195
+ }
196
+ const result = FileFinder.create({
197
+ basePath: cwd,
198
+ frecencyDbPath,
199
+ historyDbPath,
200
+ aiMode: true,
201
+ enableHomeDirScanning: true,
202
+ enableFsRootScanning
203
+ });
204
+ if (!result.ok)
205
+ throw new Error(`Failed to create FFF file finder: ${result.error}`);
206
+ finder = result.value;
207
+ finderCwd = cwd;
208
+ await finder.waitForScan(15000);
209
+ return finder;
210
+ })().finally(() => {
211
+ finderPromise = null;
212
+ });
213
+ return finderPromise;
214
+ }
215
+ function destroyFinder() {
216
+ if (finder && !finder.isDestroyed) {
217
+ finder.destroy();
218
+ finder = null;
219
+ finderCwd = null;
220
+ }
221
+ }
222
+ async function getMentionItems(query, signal) {
223
+ if (signal.aborted)
224
+ return [];
225
+ const f = await ensureFinder(activeCwd);
226
+ if (signal.aborted)
227
+ return [];
228
+ const result = f.mixedSearch(query, { pageSize: MENTION_MAX_RESULTS });
229
+ if (!result.ok)
230
+ return [];
231
+ return result.value.items.slice(0, MENTION_MAX_RESULTS).map((mixed) => {
232
+ if (mixed.type === "directory") {
233
+ return {
234
+ value: buildAtCompletionValue(mixed.item.relativePath),
235
+ label: mixed.item.dirName,
236
+ description: mixed.item.relativePath
237
+ };
238
+ }
239
+ return {
240
+ value: buildAtCompletionValue(mixed.item.relativePath),
241
+ label: mixed.item.fileName,
242
+ description: mixed.item.relativePath
243
+ };
244
+ });
245
+ }
246
+ function registerAutocompleteProvider(ctx) {
247
+ ctx.ui.addAutocompleteProvider((current) => {
248
+ const mentionProvider = createFffMentionProvider(getMentionItems);
249
+ return {
250
+ async getSuggestions(lines, cursorLine, cursorCol, options) {
251
+ if (shouldEnableMentions()) {
252
+ try {
253
+ const mentionResult = await mentionProvider.getSuggestions(lines, cursorLine, cursorCol, options);
254
+ if (mentionResult)
255
+ return mentionResult;
256
+ } catch {}
257
+ }
258
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
259
+ },
260
+ applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
261
+ return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
262
+ },
263
+ shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
264
+ return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
265
+ }
266
+ };
267
+ });
268
+ }
269
+ dm.registerFlag("fff-mode", {
270
+ description: "FFF mode: tools-and-ui | tools-only | override",
271
+ type: "string"
272
+ });
273
+ dm.registerFlag("fff-frecency-db", {
274
+ description: "Path to the frecency database (overrides FFF_FRECENCY_DB env)",
275
+ type: "string"
276
+ });
277
+ dm.registerFlag("fff-history-db", {
278
+ description: "Path to the query history database (overrides FFF_HISTORY_DB env)",
279
+ type: "string"
280
+ });
281
+ dm.registerFlag("fff-enable-root-scan", {
282
+ description: "Allow indexing when launched from the filesystem root (also: FFF_ENABLE_ROOT_SCAN env)",
283
+ type: "boolean"
284
+ });
285
+ dm.on("session_start", async (_event, ctx) => {
286
+ try {
287
+ activeCwd = ctx.cwd;
288
+ const entries = ctx.sessionManager?.getEntries();
289
+ if (entries) {
290
+ const modeEntry = [...entries].reverse().find((e) => e.type === "custom" && e.customType === "fff-mode");
291
+ if (modeEntry && typeof modeEntry.data?.mode === "string" && VALID_MODES.includes(modeEntry.data.mode)) {
292
+ const restored = modeEntry.data.mode;
293
+ if (restored !== currentMode) {
294
+ currentMode = restored;
295
+ }
296
+ }
297
+ }
298
+ registerAutocompleteProvider(ctx);
299
+ await ensureFinder(activeCwd);
300
+ } catch (e) {
301
+ ctx.ui.notify(`FFF init failed: ${e instanceof Error ? e.message : String(e)}`, "error");
302
+ }
303
+ });
304
+ dm.on("session_shutdown", async () => {
305
+ destroyFinder();
306
+ });
307
+ const renderTextResult = (result, options, theme, context, maxLines = 15) => {
308
+ const text = context.lastComponent ?? new Text("", 0, 0);
309
+ const output = result.content?.find((c) => c.type === "text")?.text?.trim() ?? "";
310
+ if (!output) {
311
+ text.setText(theme.fg("muted", "No output"));
312
+ return text;
313
+ }
314
+ const lines = output.split(`
315
+ `);
316
+ const displayLines = lines.slice(0, options.expanded ? lines.length : maxLines);
317
+ let content = `
318
+ ${displayLines.map((line) => theme.fg("toolOutput", line)).join(`
319
+ `)}`;
320
+ if (lines.length > displayLines.length) {
321
+ content += theme.fg("muted", `
322
+ ... (${lines.length - displayLines.length} more lines)`);
323
+ }
324
+ text.setText(content);
325
+ return text;
326
+ };
327
+ const grepSchema = Type.Object({
328
+ pattern: Type.String({
329
+ description: "Search pattern (literal text or regex)"
330
+ }),
331
+ path: Type.Optional(Type.String({
332
+ description: "Repo-relative path constraint. Directory prefix (src/ or src/foo/), bare filename with extension (main.rs), or glob (*.ts, src/**/*.cc, {src,lib}/**). Applied to the full repo-relative path."
333
+ })),
334
+ exclude: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], {
335
+ description: "Exclude paths (comma/space-separated or array). Same syntax as path: directory prefix ('test/'), filename with extension ('config.json'), or glob ('*.min.js', '**/*.{rs,go}'). A leading '!' is optional and ignored — both 'test/' and '!test/' work. Example: 'test/,*.min.js,!vendor/'."
336
+ })),
337
+ caseSensitive: Type.Optional(Type.Boolean({
338
+ description: "Force case-sensitive matching. Default uses smart-case (case-insensitive when pattern is all lowercase)."
339
+ })),
340
+ context: Type.Optional(Type.Number({ description: "Context lines before+after each match" })),
341
+ limit: Type.Optional(Type.Number({
342
+ description: `Max matches (default ${DEFAULT_GREP_LIMIT})`
343
+ })),
344
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor from previous result" }))
345
+ });
346
+ dm.registerTool({
347
+ name: toolNames.grep,
348
+ label: toolNames.grep,
349
+ description: `Grep file contents. Smart-case, auto-detects regex vs literal, git-aware. Results are ranked by frecency (most-accessed files first); matches within a file stay in source order. Default limit ${DEFAULT_GREP_LIMIT}.`,
350
+ promptSnippet: "Grep contents",
351
+ promptGuidelines: [
352
+ "Prefer bare identifiers as patterns. Literal queries are most efficient.",
353
+ "Use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').",
354
+ "caseSensitive: true when you need exact case (smart-case otherwise).",
355
+ "After 1-2 greps, read the top match instead of more greps."
356
+ ],
357
+ parameters: grepSchema,
358
+ async execute(_toolCallId, params, signal) {
359
+ if (signal?.aborted)
360
+ throw new Error("Operation aborted");
361
+ const f = await ensureFinder(activeCwd);
362
+ const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
363
+ const query = buildQuery(params.path, params.pattern, params.exclude, activeCwd);
364
+ const hasRegexSyntax = params.pattern !== params.pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
365
+ let mode = hasRegexSyntax ? "regex" : "plain";
366
+ if (mode === "regex") {
367
+ try {
368
+ new RegExp(params.pattern);
369
+ } catch {
370
+ mode = "plain";
371
+ }
372
+ }
373
+ const p = params.pattern.trim();
374
+ const isWildcardOnly = hasRegexSyntax && /^(?:[.^$]*(?:[.][*+?]|\*|\+)[.^$]*|[.^$\s]*|\.\*\??|\.\*[+?]?|\.\+\??|\.|\*|\?)$/.test(p);
375
+ if (isWildcardOnly) {
376
+ return {
377
+ content: [
378
+ {
379
+ type: "text",
380
+ text: `Pattern '${params.pattern}' matches everything — grep needs a concrete substring or identifier. Example: \`pattern: 'MyClass'\` or \`pattern: 'export function'\`.`
381
+ }
382
+ ],
383
+ details: { totalMatched: 0, totalFiles: 0 }
384
+ };
385
+ }
386
+ const smartCase = params.caseSensitive !== true;
387
+ const grepResult = f.grep(query, {
388
+ mode,
389
+ smartCase,
390
+ maxMatchesPerFile: Math.min(effectiveLimit, 50),
391
+ cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null,
392
+ beforeContext: params.context ?? 0,
393
+ afterContext: params.context ?? 0,
394
+ classifyDefinitions: true
395
+ });
396
+ if (!grepResult.ok)
397
+ throw new Error(grepResult.error);
398
+ let result = grepResult.value;
399
+ let fuzzyNotice = null;
400
+ if (result.items.length === 0 && !params.cursor && mode !== "regex") {
401
+ const fuzzy = f.grep(params.pattern, {
402
+ mode: "fuzzy",
403
+ smartCase,
404
+ maxMatchesPerFile: Math.min(effectiveLimit, 50),
405
+ cursor: null,
406
+ beforeContext: 0,
407
+ afterContext: 0,
408
+ classifyDefinitions: true
409
+ });
410
+ if (fuzzy.ok && fuzzy.value.items.length > 0) {
411
+ fuzzyNotice = `0 exact matches. Maybe you meant this?`;
412
+ result = fuzzy.value;
413
+ }
414
+ }
415
+ let output = formatGrepOutput(result);
416
+ const notices = [];
417
+ if (result.regexFallbackError) {
418
+ notices.push(`Invalid regex: ${result.regexFallbackError}, used literal match`);
419
+ }
420
+ if (result.nextCursor) {
421
+ notices.push(`Continue with cursor="${storeCursor(result.nextCursor)}"`);
422
+ }
423
+ if (notices.length > 0)
424
+ output += `
8
425
 
9
- [${R.join(". ")}]`;if(v)N=`[${v}]
10
- ${N}`;return{content:[{type:"text",text:N}],details:{totalMatched:D.totalMatched,totalFiles:D.totalFiles}}},renderCall(Y,j,K){let H=K.lastComponent??new L("",0,0),Q=Y?.pattern??"",X=Y?.path??".",U=j.fg("toolTitle",j.bold(V.grep))+" "+j.fg("accent",`/${Q}/`)+j.fg("toolOutput",` in ${X}`);if(Y?.limit!==void 0)U+=j.fg("toolOutput",` limit ${Y.limit}`);if(Y?.cursor)U+=j.fg("muted"," (page)");return H.setText(U),H},renderResult(Y,j,K,H){return g(Y,j,K,H,15)}});let jj=W.Object({pattern:W.String({description:"Fuzzy filename search and glob search. Frecency-ranked, git-aware. Multi-word = narrower (AND) not bound to order, use for multi word related concept search. Prefer this over ls/find/bash as the first exploration step whenever the user names a concept, feature, or symbol — it surfaces the relevant files in one call. Only use ls/read on a directory when you specifically need the alphabetical layout of an unknown repo, or when a concept search returned nothing."}),path:W.Optional(W.String({description:"Repo-relative path constraint. Directory prefix (src/ or src/foo/), bare filename with extension (main.rs), or glob (*.ts, src/**/*.cc, {src,lib}/**). Applied to the full repo-relative path."})),exclude:W.Optional(W.Union([W.String(),W.Array(W.String())],{description:"Exclude paths (comma/space-separated or array). Same syntax as path: directory prefix ('test/'), filename with extension ('config.json'), or glob ('*.min.js', '**/*.{rs,go}'). A leading '!' is optional and ignored — both 'test/' and '!test/' work. Example: 'test/,*.min.js,!vendor/'."})),limit:W.Optional(W.Number({description:`Max results per page (default ${f})`})),cursor:W.Optional(W.String({description:"Pagination cursor from previous result"}))});if(J.registerTool({name:V.find,label:V.find,description:`Fuzzy path search and glob search. Matches against the whole repo-relative path, not just the filename. Frecency-ranked, git-aware. Multi-word = narrower (AND). Default limit ${f}.`,promptSnippet:"Find files by path or glob",promptGuidelines:["Matches the WHOLE path, not just the filename — `profile` hits `chrome/browser/profiles/x.cc` too.","Keep queries to 1-2 terms; extra words narrow.","Use for paths, not content. Use grep for content.","For exact path matches use a glob in `path` — e.g. path: '**/profile.h' for exact filename, or path: 'src/**/profile.h' scoped to a subtree. Bare patterns are fuzzy.","To list everything inside a directory, pass path: 'dir/**' with an empty or wildcard pattern instead of using pattern alone.","Use exclude: 'test/,*.min.js' to cut noise in large repos."],parameters:jj,async execute(Y,j,K){if(K?.aborted)throw Error("Operation aborted");let H=await F(B),Q=j.cursor?kj(j.cursor):void 0,X=Q?Q.pageSize:Math.max(1,j.limit??f),U=Q?Q.query:x(j.path,j.pattern,j.exclude,B),q=Q?Q.pattern:j.pattern,b=Q?.nextPageIndex??0,S=H.fileSearch(U,{pageIndex:b,pageSize:X});if(!S.ok)throw Error(S.error);let O=S.value,A=bj(O,X,q),D=A.output,v=b*X+O.items.length,N=O.items.length>=X&&O.totalMatched>v,R=[];if(A.weak&&A.shownCount>0)R.push(`Query "${q}" produced only weak scattered fuzzy matches. Output capped at ${A.shownCount}/${O.totalMatched}.`);if(!A.weak&&N){let I=O.totalMatched-v,Jj=Xj({query:U,pattern:q,pageSize:X,nextPageIndex:b+1});R.push(`${I} more match${I===1?"":"es"} available. cursor="${Jj}" to continue`)}if(R.length>0)D+=`
426
+ [${notices.join(". ")}]`;
427
+ if (fuzzyNotice)
428
+ output = `[${fuzzyNotice}]
429
+ ${output}`;
430
+ return {
431
+ content: [{ type: "text", text: output }],
432
+ details: {
433
+ totalMatched: result.totalMatched,
434
+ totalFiles: result.totalFiles
435
+ }
436
+ };
437
+ },
438
+ renderCall(args, theme, context) {
439
+ const text = context.lastComponent ?? new Text("", 0, 0);
440
+ const pattern = args?.pattern ?? "";
441
+ const path = args?.path ?? ".";
442
+ let content = theme.fg("toolTitle", theme.bold(toolNames.grep)) + " " + theme.fg("accent", `/${pattern}/`) + theme.fg("toolOutput", ` in ${path}`);
443
+ if (args?.limit !== undefined)
444
+ content += theme.fg("toolOutput", ` limit ${args.limit}`);
445
+ if (args?.cursor)
446
+ content += theme.fg("muted", ` (page)`);
447
+ text.setText(content);
448
+ return text;
449
+ },
450
+ renderResult(result, options, theme, context) {
451
+ return renderTextResult(result, options, theme, context, 15);
452
+ }
453
+ });
454
+ const findSchema = Type.Object({
455
+ pattern: Type.String({
456
+ description: "Fuzzy filename search and glob search. Frecency-ranked, git-aware. Multi-word = narrower (AND) not bound to order, use for multi word related concept search. Prefer this over ls/find/bash as the first exploration step whenever the user names a concept, feature, or symbol — it surfaces the relevant files in one call. Only use ls/read on a directory when you specifically need the alphabetical layout of an unknown repo, or when a concept search returned nothing."
457
+ }),
458
+ path: Type.Optional(Type.String({
459
+ description: "Repo-relative path constraint. Directory prefix (src/ or src/foo/), bare filename with extension (main.rs), or glob (*.ts, src/**/*.cc, {src,lib}/**). Applied to the full repo-relative path."
460
+ })),
461
+ exclude: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], {
462
+ description: "Exclude paths (comma/space-separated or array). Same syntax as path: directory prefix ('test/'), filename with extension ('config.json'), or glob ('*.min.js', '**/*.{rs,go}'). A leading '!' is optional and ignored — both 'test/' and '!test/' work. Example: 'test/,*.min.js,!vendor/'."
463
+ })),
464
+ limit: Type.Optional(Type.Number({
465
+ description: `Max results per page (default ${DEFAULT_FIND_LIMIT})`
466
+ })),
467
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor from previous result" }))
468
+ });
469
+ dm.registerTool({
470
+ name: toolNames.find,
471
+ label: toolNames.find,
472
+ description: `Fuzzy path search and glob search. Matches against the whole repo-relative path, not just the filename. Frecency-ranked, git-aware. Multi-word = narrower (AND). Default limit ${DEFAULT_FIND_LIMIT}.`,
473
+ promptSnippet: "Find files by path or glob",
474
+ promptGuidelines: [
475
+ "Matches the WHOLE path, not just the filename — `profile` hits `chrome/browser/profiles/x.cc` too.",
476
+ "Keep queries to 1-2 terms; extra words narrow.",
477
+ "Use for paths, not content. Use grep for content.",
478
+ "For exact path matches use a glob in `path` — e.g. path: '**/profile.h' for exact filename, or path: 'src/**/profile.h' scoped to a subtree. Bare patterns are fuzzy.",
479
+ "To list everything inside a directory, pass path: 'dir/**' with an empty or wildcard pattern instead of using pattern alone.",
480
+ "Use exclude: 'test/,*.min.js' to cut noise in large repos."
481
+ ],
482
+ parameters: findSchema,
483
+ async execute(_toolCallId, params, signal) {
484
+ if (signal?.aborted)
485
+ throw new Error("Operation aborted");
486
+ const f = await ensureFinder(activeCwd);
487
+ const resumed = params.cursor ? getFindCursor(params.cursor) : undefined;
488
+ const effectiveLimit = resumed ? resumed.pageSize : Math.max(1, params.limit ?? DEFAULT_FIND_LIMIT);
489
+ const query = resumed ? resumed.query : buildQuery(params.path, params.pattern, params.exclude, activeCwd);
490
+ const pattern = resumed ? resumed.pattern : params.pattern;
491
+ const pageIndex = resumed?.nextPageIndex ?? 0;
492
+ const searchResult = f.fileSearch(query, {
493
+ pageIndex,
494
+ pageSize: effectiveLimit
495
+ });
496
+ if (!searchResult.ok)
497
+ throw new Error(searchResult.error);
498
+ const result = searchResult.value;
499
+ const formatted = formatFindOutput(result, effectiveLimit, pattern);
500
+ let output = formatted.output;
501
+ const shownSoFar = pageIndex * effectiveLimit + result.items.length;
502
+ const hasMore = result.items.length >= effectiveLimit && result.totalMatched > shownSoFar;
503
+ const notices = [];
504
+ if (formatted.weak && formatted.shownCount > 0)
505
+ notices.push(`Query "${pattern}" produced only weak scattered fuzzy matches. Output capped at ${formatted.shownCount}/${result.totalMatched}.`);
506
+ if (!formatted.weak && hasMore) {
507
+ const remaining = result.totalMatched - shownSoFar;
508
+ const cursorId = storeFindCursor({
509
+ query,
510
+ pattern,
511
+ pageSize: effectiveLimit,
512
+ nextPageIndex: pageIndex + 1
513
+ });
514
+ notices.push(`${remaining} more match${remaining === 1 ? "" : "es"} available. cursor="${cursorId}" to continue`);
515
+ }
516
+ if (notices.length > 0)
517
+ output += `
11
518
 
12
- [${R.join(". ")}]`;return{content:[{type:"text",text:D}],details:{totalMatched:O.totalMatched,totalFiles:O.totalFiles,pageIndex:b,hasMore:N}}},renderCall(Y,j,K){let H=K.lastComponent??new L("",0,0),Q=Y?.pattern??"",X=Y?.path??".",U=j.fg("toolTitle",j.bold(V.find))+" "+j.fg("accent",Q)+j.fg("toolOutput",` in ${X}`);if(Y?.limit!==void 0)U+=j.fg("toolOutput",` (limit ${Y.limit})`);if(Y?.cursor)U+=j.fg("muted"," (page)");return H.setText(U),H},renderResult(Y,j,K,H){return g(Y,j,K,H,20)}}),process.env.DM_FFF_MULTIGREP==="1"){let Y=W.Object({patterns:W.Array(W.String(),{description:"Literal patterns (OR). Include snake_case/camelCase/PascalCase variants."}),constraints:W.Optional(W.String({description:"File filter, e.g. '*.{ts,tsx} !test/'"})),context:W.Optional(W.Number({description:"Context lines before+after"})),limit:W.Optional(W.Number({description:`Max matches (default ${E})`})),cursor:W.Optional(W.String({description:"Pagination cursor"}))});J.registerTool({name:V.multiGrep,label:V.multiGrep,description:"Search file contents for ANY of multiple literal patterns (OR, SIMD Aho-Corasick). Faster than regex alternation.",promptSnippet:"Multi-pattern OR content search",promptGuidelines:["Use when searching for several identifiers at once.","Include all naming-convention variants (snake/camel/Pascal).","Patterns are literal. Use constraints for file filters."],parameters:Y,async execute(j,K,H){if(H?.aborted)throw Error("Operation aborted");if(!K.patterns?.length)throw Error("patterns array must have at least 1 element");let Q=await F(B),X=Math.max(1,K.limit??E),U=Q.multiGrep({patterns:K.patterns,constraints:K.constraints,maxMatchesPerFile:Math.min(X,50),smartCase:!0,cursor:(K.cursor?d(K.cursor):null)??null,beforeContext:K.context??0,afterContext:K.context??0});if(!U.ok)throw Error(U.error);let q=U.value,b=o(q),S=[];if(q.items.length>=X)S.push(`${X}+ matches (refine patterns)`);if(q.nextCursor)S.push(`More available. cursor="${u(q.nextCursor)}" to continue`);if(S.length>0)b+=`
519
+ [${notices.join(". ")}]`;
520
+ return {
521
+ content: [{ type: "text", text: output }],
522
+ details: {
523
+ totalMatched: result.totalMatched,
524
+ totalFiles: result.totalFiles,
525
+ pageIndex,
526
+ hasMore
527
+ }
528
+ };
529
+ },
530
+ renderCall(args, theme, context) {
531
+ const text = context.lastComponent ?? new Text("", 0, 0);
532
+ const pattern = args?.pattern ?? "";
533
+ const path = args?.path ?? ".";
534
+ let content = theme.fg("toolTitle", theme.bold(toolNames.find)) + " " + theme.fg("accent", pattern) + theme.fg("toolOutput", ` in ${path}`);
535
+ if (args?.limit !== undefined)
536
+ content += theme.fg("toolOutput", ` (limit ${args.limit})`);
537
+ if (args?.cursor)
538
+ content += theme.fg("muted", ` (page)`);
539
+ text.setText(content);
540
+ return text;
541
+ },
542
+ renderResult(result, options, theme, context) {
543
+ return renderTextResult(result, options, theme, context, 20);
544
+ }
545
+ });
546
+ const enableMultiGrep = process.env.DM_FFF_MULTIGREP === "1";
547
+ if (enableMultiGrep) {
548
+ const multiGrepSchema = Type.Object({
549
+ patterns: Type.Array(Type.String(), {
550
+ description: "Literal patterns (OR). Include snake_case/camelCase/PascalCase variants."
551
+ }),
552
+ constraints: Type.Optional(Type.String({ description: "File filter, e.g. '*.{ts,tsx} !test/'" })),
553
+ context: Type.Optional(Type.Number({ description: "Context lines before+after" })),
554
+ limit: Type.Optional(Type.Number({
555
+ description: `Max matches (default ${DEFAULT_GREP_LIMIT})`
556
+ })),
557
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor" }))
558
+ });
559
+ dm.registerTool({
560
+ name: toolNames.multiGrep,
561
+ label: toolNames.multiGrep,
562
+ description: "Search file contents for ANY of multiple literal patterns (OR, SIMD Aho-Corasick). Faster than regex alternation.",
563
+ promptSnippet: "Multi-pattern OR content search",
564
+ promptGuidelines: [
565
+ "Use when searching for several identifiers at once.",
566
+ "Include all naming-convention variants (snake/camel/Pascal).",
567
+ "Patterns are literal. Use constraints for file filters."
568
+ ],
569
+ parameters: multiGrepSchema,
570
+ async execute(_toolCallId, params, signal) {
571
+ if (signal?.aborted)
572
+ throw new Error("Operation aborted");
573
+ if (!params.patterns?.length)
574
+ throw new Error("patterns array must have at least 1 element");
575
+ const f = await ensureFinder(activeCwd);
576
+ const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
577
+ const grepResult = f.multiGrep({
578
+ patterns: params.patterns,
579
+ constraints: params.constraints,
580
+ maxMatchesPerFile: Math.min(effectiveLimit, 50),
581
+ smartCase: true,
582
+ cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null,
583
+ beforeContext: params.context ?? 0,
584
+ afterContext: params.context ?? 0
585
+ });
586
+ if (!grepResult.ok)
587
+ throw new Error(grepResult.error);
588
+ const result = grepResult.value;
589
+ let output = formatGrepOutput(result);
590
+ const notices = [];
591
+ if (result.items.length >= effectiveLimit)
592
+ notices.push(`${effectiveLimit}+ matches (refine patterns)`);
593
+ if (result.nextCursor)
594
+ notices.push(`More available. cursor="${storeCursor(result.nextCursor)}" to continue`);
595
+ if (notices.length > 0)
596
+ output += `
13
597
 
14
- [${S.join(". ")}]`;return{content:[{type:"text",text:b}],details:{totalMatched:q.totalMatched,totalFiles:q.totalFiles,patterns:K.patterns}}},renderCall(j,K,H){let Q=H.lastComponent??new L("",0,0),X=j?.patterns??[],U=j?.constraints,q=K.fg("toolTitle",K.bold(V.multiGrep))+" "+K.fg("accent",X.map((b)=>`"${b}"`).join(", "));if(U)q+=K.fg("toolOutput",` (${U})`);if(j?.cursor)q+=K.fg("muted"," (page)");return Q.setText(q),Q},renderResult(j,K,H,Q){return g(j,K,H,Q,15)}})}J.registerCommand("fff-mode",{description:"Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]",handler:async(Y,j)=>{let K=(Y||"").trim();if(!K){let U=T(),q=J.getFlag("fff-mode")??"unset";j.ui.notify(`Current mode: '${U}' (flag: ${q})`,"info");return}if(!h.includes(K)){j.ui.notify(`Usage: /fff-mode [${h.join(" | ")}]`,"warning");return}let H=K,Q=T();a(H),J.appendEntry("fff-mode",{mode:H});let X=Q==="override"!==(H==="override")?" (tool name change requires /reload)":"";j.ui.notify(`Mode changed: '${Q}' → '${H}'${X}`,"info")}}),J.registerCommand("fff-health",{description:"Show FFF file finder health and status",handler:async(Y,j)=>{if(!$||$.isDestroyed){j.ui.notify("FFF not initialized","warning");return}let K=$.healthCheck();if(!K.ok){j.ui.notify(`Health check failed: ${K.error}`,"error");return}let H=K.value,Q=[`FFF v${H.version}`,`Mode: ${T()}`,`Git: ${H.git.repositoryFound?`yes (${H.git.workdir??"unknown"})`:"no"}`,`Picker: ${H.filePicker.initialized?`${H.filePicker.indexedFiles??0} files`:"not initialized"}`,`Frecency: ${H.frecency.initialized?"active":"disabled"}`,`Query tracker: ${H.queryTracker.initialized?"active":"disabled"}`],X=$.getScanProgress();if(X.ok)Q.push(`Scanning: ${X.value.isScanning?"yes":"no"} (${X.value.scannedFilesCount} files)`);j.ui.notify(Q.join(`
15
- `),"info")}}),J.registerCommand("fff-rescan",{description:"Trigger FFF to rescan files",handler:async(Y,j)=>{if(!$||$.isDestroyed){j.ui.notify("FFF not initialized","warning");return}let K=$.scanFiles();if(!K.ok){j.ui.notify(`Rescan failed: ${K.error}`,"error");return}j.ui.notify("FFF rescan triggered","info")}})}export{n as fffFileAnnotation,Pj as default};
598
+ [${notices.join(". ")}]`;
599
+ return {
600
+ content: [{ type: "text", text: output }],
601
+ details: {
602
+ totalMatched: result.totalMatched,
603
+ totalFiles: result.totalFiles,
604
+ patterns: params.patterns
605
+ }
606
+ };
607
+ },
608
+ renderCall(args, theme, context) {
609
+ const text = context.lastComponent ?? new Text("", 0, 0);
610
+ const patterns = args?.patterns ?? [];
611
+ const constraints = args?.constraints;
612
+ let content = theme.fg("toolTitle", theme.bold(toolNames.multiGrep)) + " " + theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "));
613
+ if (constraints)
614
+ content += theme.fg("toolOutput", ` (${constraints})`);
615
+ if (args?.cursor)
616
+ content += theme.fg("muted", ` (page)`);
617
+ text.setText(content);
618
+ return text;
619
+ },
620
+ renderResult(result, options, theme, context) {
621
+ return renderTextResult(result, options, theme, context, 15);
622
+ }
623
+ });
624
+ }
625
+ dm.registerCommand("fff-mode", {
626
+ description: "Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]",
627
+ handler: async (args, ctx) => {
628
+ const arg = (args || "").trim();
629
+ if (!arg) {
630
+ const mode = getMode();
631
+ const flag = dm.getFlag("fff-mode") ?? "unset";
632
+ ctx.ui.notify(`Current mode: '${mode}' (flag: ${flag})`, "info");
633
+ return;
634
+ }
635
+ if (!VALID_MODES.includes(arg)) {
636
+ ctx.ui.notify(`Usage: /fff-mode [${VALID_MODES.join(" | ")}]`, "warning");
637
+ return;
638
+ }
639
+ const newMode = arg;
640
+ const oldMode = getMode();
641
+ setMode(newMode);
642
+ dm.appendEntry("fff-mode", { mode: newMode });
643
+ const note = oldMode === "override" !== (newMode === "override") ? " (tool name change requires /reload)" : "";
644
+ ctx.ui.notify(`Mode changed: '${oldMode}' → '${newMode}'${note}`, "info");
645
+ }
646
+ });
647
+ dm.registerCommand("fff-health", {
648
+ description: "Show FFF file finder health and status",
649
+ handler: async (_args, ctx) => {
650
+ if (!finder || finder.isDestroyed) {
651
+ ctx.ui.notify("FFF not initialized", "warning");
652
+ return;
653
+ }
654
+ const health = finder.healthCheck();
655
+ if (!health.ok) {
656
+ ctx.ui.notify(`Health check failed: ${health.error}`, "error");
657
+ return;
658
+ }
659
+ const h = health.value;
660
+ const lines = [
661
+ `FFF v${h.version}`,
662
+ `Mode: ${getMode()}`,
663
+ `Git: ${h.git.repositoryFound ? `yes (${h.git.workdir ?? "unknown"})` : "no"}`,
664
+ `Picker: ${h.filePicker.initialized ? `${h.filePicker.indexedFiles ?? 0} files` : "not initialized"}`,
665
+ `Frecency: ${h.frecency.initialized ? "active" : "disabled"}`,
666
+ `Query tracker: ${h.queryTracker.initialized ? "active" : "disabled"}`
667
+ ];
668
+ const progress = finder.getScanProgress();
669
+ if (progress.ok) {
670
+ lines.push(`Scanning: ${progress.value.isScanning ? "yes" : "no"} (${progress.value.scannedFilesCount} files)`);
671
+ }
672
+ ctx.ui.notify(lines.join(`
673
+ `), "info");
674
+ }
675
+ });
676
+ dm.registerCommand("fff-rescan", {
677
+ description: "Trigger FFF to rescan files",
678
+ handler: async (_args, ctx) => {
679
+ if (!finder || finder.isDestroyed) {
680
+ ctx.ui.notify("FFF not initialized", "warning");
681
+ return;
682
+ }
683
+ const result = finder.scanFiles();
684
+ if (!result.ok) {
685
+ ctx.ui.notify(`Rescan failed: ${result.error}`, "error");
686
+ return;
687
+ }
688
+ ctx.ui.notify("FFF rescan triggered", "info");
689
+ }
690
+ });
691
+ }