opencode-dux 1.0.0

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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/dist/agents/descriptions.d.ts +6 -0
  4. package/dist/agents/designer.d.ts +2 -0
  5. package/dist/agents/explorer.d.ts +2 -0
  6. package/dist/agents/fixer.d.ts +2 -0
  7. package/dist/agents/index.d.ts +22 -0
  8. package/dist/agents/interpreter.d.ts +2 -0
  9. package/dist/agents/librarian.d.ts +2 -0
  10. package/dist/agents/oracle.d.ts +2 -0
  11. package/dist/agents/orchestrator.d.ts +27 -0
  12. package/dist/agents/overrides.d.ts +18 -0
  13. package/dist/agents/prompt-blocks.d.ts +97 -0
  14. package/dist/agents/steward.d.ts +3 -0
  15. package/dist/cli/config-io.d.ts +24 -0
  16. package/dist/cli/config-manager.d.ts +4 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1006 -0
  19. package/dist/cli/install.d.ts +2 -0
  20. package/dist/cli/mcps.d.ts +13 -0
  21. package/dist/cli/model-key-normalization.d.ts +1 -0
  22. package/dist/cli/paths.d.ts +35 -0
  23. package/dist/cli/providers.d.ts +137 -0
  24. package/dist/cli/skills.d.ts +22 -0
  25. package/dist/cli/system.d.ts +5 -0
  26. package/dist/cli/types.d.ts +38 -0
  27. package/dist/config/constants.d.ts +12 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/loader.d.ts +40 -0
  30. package/dist/config/runtime-preset.d.ts +12 -0
  31. package/dist/config/schema.d.ts +281 -0
  32. package/dist/config/utils.d.ts +10 -0
  33. package/dist/discovery/local/types.d.ts +79 -0
  34. package/dist/discovery/local.d.ts +73 -0
  35. package/dist/discovery/mcp-servers.d.ts +88 -0
  36. package/dist/discovery/skills.d.ts +94 -0
  37. package/dist/hooks/apply-patch/codec.d.ts +7 -0
  38. package/dist/hooks/apply-patch/errors.d.ts +25 -0
  39. package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
  40. package/dist/hooks/apply-patch/index.d.ts +15 -0
  41. package/dist/hooks/apply-patch/matching.d.ts +26 -0
  42. package/dist/hooks/apply-patch/operations.d.ts +3 -0
  43. package/dist/hooks/apply-patch/patch.d.ts +2 -0
  44. package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
  45. package/dist/hooks/apply-patch/resolution.d.ts +19 -0
  46. package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
  47. package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
  48. package/dist/hooks/apply-patch/types.d.ts +80 -0
  49. package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
  52. package/dist/hooks/auto-update-checker/index.d.ts +18 -0
  53. package/dist/hooks/auto-update-checker/types.d.ts +22 -0
  54. package/dist/hooks/chat-headers.d.ts +16 -0
  55. package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
  56. package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
  57. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
  58. package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
  59. package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
  60. package/dist/hooks/filter-available-skills/index.d.ts +32 -0
  61. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  62. package/dist/hooks/image-hook.d.ts +5 -0
  63. package/dist/hooks/index.d.ts +14 -0
  64. package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
  65. package/dist/hooks/json-error-recovery/index.d.ts +1 -0
  66. package/dist/hooks/phase-reminder/index.d.ts +26 -0
  67. package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
  68. package/dist/hooks/task-session-manager/index.d.ts +52 -0
  69. package/dist/hooks/todo-continuation/index.d.ts +53 -0
  70. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
  71. package/dist/index.d.ts +5 -0
  72. package/dist/index.js +31782 -0
  73. package/dist/mcp/context7.d.ts +6 -0
  74. package/dist/mcp/grep-app.d.ts +6 -0
  75. package/dist/mcp/index.d.ts +13 -0
  76. package/dist/mcp/types.d.ts +12 -0
  77. package/dist/mcp/websearch.d.ts +9 -0
  78. package/dist/skills/registry.d.ts +29 -0
  79. package/dist/subscriptions/accounts-store.d.ts +57 -0
  80. package/dist/subscriptions/index.d.ts +13 -0
  81. package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
  82. package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
  83. package/dist/subscriptions/types.d.ts +115 -0
  84. package/dist/subscriptions/usage-service.d.ts +74 -0
  85. package/dist/tools/ast-grep/cli.d.ts +15 -0
  86. package/dist/tools/ast-grep/constants.d.ts +25 -0
  87. package/dist/tools/ast-grep/downloader.d.ts +5 -0
  88. package/dist/tools/ast-grep/index.d.ts +10 -0
  89. package/dist/tools/ast-grep/tools.d.ts +3 -0
  90. package/dist/tools/ast-grep/types.d.ts +30 -0
  91. package/dist/tools/ast-grep/utils.d.ts +4 -0
  92. package/dist/tools/delegate.d.ts +14 -0
  93. package/dist/tools/index.d.ts +5 -0
  94. package/dist/tools/preset-manager.d.ts +27 -0
  95. package/dist/tools/smartfetch/binary.d.ts +3 -0
  96. package/dist/tools/smartfetch/cache.d.ts +6 -0
  97. package/dist/tools/smartfetch/constants.d.ts +12 -0
  98. package/dist/tools/smartfetch/index.d.ts +3 -0
  99. package/dist/tools/smartfetch/network.d.ts +38 -0
  100. package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
  101. package/dist/tools/smartfetch/tool.d.ts +3 -0
  102. package/dist/tools/smartfetch/types.d.ts +122 -0
  103. package/dist/tools/smartfetch/utils.d.ts +18 -0
  104. package/dist/tui-state.d.ts +168 -0
  105. package/dist/tui.d.ts +37 -0
  106. package/dist/tui.js +1896 -0
  107. package/dist/utils/agent-variant.d.ts +63 -0
  108. package/dist/utils/compat.d.ts +30 -0
  109. package/dist/utils/env.d.ts +1 -0
  110. package/dist/utils/index.d.ts +9 -0
  111. package/dist/utils/internal-initiator.d.ts +6 -0
  112. package/dist/utils/logger.d.ts +8 -0
  113. package/dist/utils/polling.d.ts +21 -0
  114. package/dist/utils/session-manager.d.ts +55 -0
  115. package/dist/utils/session.d.ts +90 -0
  116. package/dist/utils/subagent-depth.d.ts +35 -0
  117. package/dist/utils/system-collapse.d.ts +6 -0
  118. package/dist/utils/task.d.ts +4 -0
  119. package/dist/utils/zip-extractor.d.ts +1 -0
  120. package/index.ts +1 -0
  121. package/opencode-dux.schema.json +634 -0
  122. package/package.json +103 -0
  123. package/src/agents/descriptions.ts +55 -0
  124. package/src/agents/designer.test.ts +86 -0
  125. package/src/agents/designer.ts +154 -0
  126. package/src/agents/display-name.test.ts +186 -0
  127. package/src/agents/explorer.test.ts +79 -0
  128. package/src/agents/explorer.ts +144 -0
  129. package/src/agents/fixer.test.ts +79 -0
  130. package/src/agents/fixer.ts +145 -0
  131. package/src/agents/index.test.ts +472 -0
  132. package/src/agents/index.ts +248 -0
  133. package/src/agents/interpreter.ts +136 -0
  134. package/src/agents/librarian.test.ts +80 -0
  135. package/src/agents/librarian.ts +145 -0
  136. package/src/agents/oracle.test.ts +89 -0
  137. package/src/agents/oracle.ts +184 -0
  138. package/src/agents/orchestrator.test.ts +116 -0
  139. package/src/agents/orchestrator.ts +574 -0
  140. package/src/agents/overrides.ts +95 -0
  141. package/src/agents/prompt-blocks.test.ts +114 -0
  142. package/src/agents/prompt-blocks.ts +640 -0
  143. package/src/agents/steward.ts +146 -0
  144. package/src/cli/config-io.test.ts +536 -0
  145. package/src/cli/config-io.ts +473 -0
  146. package/src/cli/config-manager.test.ts +141 -0
  147. package/src/cli/config-manager.ts +4 -0
  148. package/src/cli/index.ts +88 -0
  149. package/src/cli/install.ts +282 -0
  150. package/src/cli/mcps.test.ts +62 -0
  151. package/src/cli/mcps.ts +39 -0
  152. package/src/cli/model-key-normalization.test.ts +21 -0
  153. package/src/cli/model-key-normalization.ts +60 -0
  154. package/src/cli/paths.test.ts +167 -0
  155. package/src/cli/paths.ts +144 -0
  156. package/src/cli/providers.test.ts +118 -0
  157. package/src/cli/providers.ts +141 -0
  158. package/src/cli/skills.test.ts +111 -0
  159. package/src/cli/skills.ts +103 -0
  160. package/src/cli/system.test.ts +91 -0
  161. package/src/cli/system.ts +180 -0
  162. package/src/cli/types.ts +43 -0
  163. package/src/config/constants.ts +58 -0
  164. package/src/config/index.ts +4 -0
  165. package/src/config/loader.test.ts +1194 -0
  166. package/src/config/loader.ts +269 -0
  167. package/src/config/model-resolution.test.ts +176 -0
  168. package/src/config/runtime-preset.test.ts +61 -0
  169. package/src/config/runtime-preset.ts +37 -0
  170. package/src/config/schema.ts +248 -0
  171. package/src/config/utils.test.ts +41 -0
  172. package/src/config/utils.ts +23 -0
  173. package/src/discovery/local/types.ts +85 -0
  174. package/src/discovery/local.ts +322 -0
  175. package/src/discovery/mcp-servers.ts +804 -0
  176. package/src/discovery/skills.ts +959 -0
  177. package/src/hooks/apply-patch/codec.test.ts +184 -0
  178. package/src/hooks/apply-patch/codec.ts +352 -0
  179. package/src/hooks/apply-patch/errors.ts +117 -0
  180. package/src/hooks/apply-patch/execution-context.ts +432 -0
  181. package/src/hooks/apply-patch/hook.test.ts +768 -0
  182. package/src/hooks/apply-patch/index.ts +126 -0
  183. package/src/hooks/apply-patch/matching.test.ts +215 -0
  184. package/src/hooks/apply-patch/matching.ts +586 -0
  185. package/src/hooks/apply-patch/operations.test.ts +1535 -0
  186. package/src/hooks/apply-patch/operations.ts +3 -0
  187. package/src/hooks/apply-patch/patch.ts +9 -0
  188. package/src/hooks/apply-patch/prepared-changes.ts +400 -0
  189. package/src/hooks/apply-patch/resolution.test.ts +420 -0
  190. package/src/hooks/apply-patch/resolution.ts +437 -0
  191. package/src/hooks/apply-patch/rewrite.ts +496 -0
  192. package/src/hooks/apply-patch/test-helpers.ts +52 -0
  193. package/src/hooks/apply-patch/types.ts +111 -0
  194. package/src/hooks/auto-update-checker/cache.test.ts +179 -0
  195. package/src/hooks/auto-update-checker/cache.ts +188 -0
  196. package/src/hooks/auto-update-checker/checker.test.ts +159 -0
  197. package/src/hooks/auto-update-checker/checker.ts +308 -0
  198. package/src/hooks/auto-update-checker/constants.ts +33 -0
  199. package/src/hooks/auto-update-checker/index.test.ts +282 -0
  200. package/src/hooks/auto-update-checker/index.ts +225 -0
  201. package/src/hooks/auto-update-checker/types.ts +26 -0
  202. package/src/hooks/chat-headers.test.ts +236 -0
  203. package/src/hooks/chat-headers.ts +97 -0
  204. package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
  205. package/src/hooks/context-pressure-reminder/index.ts +137 -0
  206. package/src/hooks/delegate-task-retry/guidance.ts +41 -0
  207. package/src/hooks/delegate-task-retry/hook.ts +23 -0
  208. package/src/hooks/delegate-task-retry/index.test.ts +38 -0
  209. package/src/hooks/delegate-task-retry/index.ts +7 -0
  210. package/src/hooks/delegate-task-retry/patterns.ts +79 -0
  211. package/src/hooks/filter-available-skills/index.test.ts +297 -0
  212. package/src/hooks/filter-available-skills/index.ts +160 -0
  213. package/src/hooks/foreground-fallback/index.test.ts +624 -0
  214. package/src/hooks/foreground-fallback/index.ts +374 -0
  215. package/src/hooks/image-hook.ts +6 -0
  216. package/src/hooks/index.ts +17 -0
  217. package/src/hooks/json-error-recovery/hook.ts +73 -0
  218. package/src/hooks/json-error-recovery/index.test.ts +111 -0
  219. package/src/hooks/json-error-recovery/index.ts +6 -0
  220. package/src/hooks/phase-reminder/index.test.ts +74 -0
  221. package/src/hooks/phase-reminder/index.ts +85 -0
  222. package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
  223. package/src/hooks/post-file-tool-nudge/index.ts +63 -0
  224. package/src/hooks/task-session-manager/index.test.ts +833 -0
  225. package/src/hooks/task-session-manager/index.ts +434 -0
  226. package/src/hooks/todo-continuation/index.test.ts +3026 -0
  227. package/src/hooks/todo-continuation/index.ts +878 -0
  228. package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
  229. package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
  230. package/src/index.ts +1672 -0
  231. package/src/mcp/context7.ts +14 -0
  232. package/src/mcp/grep-app.ts +11 -0
  233. package/src/mcp/index.test.ts +96 -0
  234. package/src/mcp/index.ts +66 -0
  235. package/src/mcp/types.ts +16 -0
  236. package/src/mcp/websearch.ts +47 -0
  237. package/src/skills/codemap/README.md +60 -0
  238. package/src/skills/codemap/SKILL.md +174 -0
  239. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  240. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  241. package/src/skills/registry.ts +218 -0
  242. package/src/skills/simplify/README.md +19 -0
  243. package/src/skills/simplify/SKILL.md +138 -0
  244. package/src/subscriptions/accounts-store.test.ts +236 -0
  245. package/src/subscriptions/accounts-store.ts +184 -0
  246. package/src/subscriptions/index.ts +30 -0
  247. package/src/subscriptions/neuralwatt-scraper.ts +108 -0
  248. package/src/subscriptions/opencode-go-scraper.ts +301 -0
  249. package/src/subscriptions/types.ts +145 -0
  250. package/src/subscriptions/usage-service.test.ts +202 -0
  251. package/src/subscriptions/usage-service.ts +651 -0
  252. package/src/tools/ast-grep/cli.ts +257 -0
  253. package/src/tools/ast-grep/constants.ts +214 -0
  254. package/src/tools/ast-grep/downloader.ts +131 -0
  255. package/src/tools/ast-grep/index.ts +24 -0
  256. package/src/tools/ast-grep/tools.ts +117 -0
  257. package/src/tools/ast-grep/types.ts +51 -0
  258. package/src/tools/ast-grep/utils.ts +126 -0
  259. package/src/tools/delegate-handoff.test.ts +18 -0
  260. package/src/tools/delegate.ts +508 -0
  261. package/src/tools/index.ts +8 -0
  262. package/src/tools/preset-manager.test.ts +795 -0
  263. package/src/tools/preset-manager.ts +332 -0
  264. package/src/tools/smartfetch/binary.ts +58 -0
  265. package/src/tools/smartfetch/cache.test.ts +34 -0
  266. package/src/tools/smartfetch/cache.ts +112 -0
  267. package/src/tools/smartfetch/constants.ts +29 -0
  268. package/src/tools/smartfetch/index.ts +8 -0
  269. package/src/tools/smartfetch/network.test.ts +178 -0
  270. package/src/tools/smartfetch/network.ts +614 -0
  271. package/src/tools/smartfetch/secondary-model.test.ts +85 -0
  272. package/src/tools/smartfetch/secondary-model.ts +276 -0
  273. package/src/tools/smartfetch/tool.test.ts +60 -0
  274. package/src/tools/smartfetch/tool.ts +832 -0
  275. package/src/tools/smartfetch/types.ts +135 -0
  276. package/src/tools/smartfetch/utils.test.ts +24 -0
  277. package/src/tools/smartfetch/utils.ts +456 -0
  278. package/src/tui-state.test.ts +867 -0
  279. package/src/tui-state.ts +1255 -0
  280. package/src/tui.test.ts +336 -0
  281. package/src/tui.ts +1539 -0
  282. package/src/utils/agent-variant.test.ts +244 -0
  283. package/src/utils/agent-variant.ts +187 -0
  284. package/src/utils/compat.ts +91 -0
  285. package/src/utils/env.ts +12 -0
  286. package/src/utils/index.ts +9 -0
  287. package/src/utils/internal-initiator.ts +28 -0
  288. package/src/utils/logger.test.ts +220 -0
  289. package/src/utils/logger.ts +136 -0
  290. package/src/utils/polling.test.ts +191 -0
  291. package/src/utils/polling.ts +67 -0
  292. package/src/utils/session-manager.test.ts +173 -0
  293. package/src/utils/session-manager.ts +356 -0
  294. package/src/utils/session.test.ts +110 -0
  295. package/src/utils/session.ts +389 -0
  296. package/src/utils/subagent-depth.test.ts +170 -0
  297. package/src/utils/subagent-depth.ts +75 -0
  298. package/src/utils/system-collapse.test.ts +86 -0
  299. package/src/utils/system-collapse.ts +24 -0
  300. package/src/utils/task.test.ts +24 -0
  301. package/src/utils/task.ts +20 -0
  302. package/src/utils/zip-extractor.ts +102 -0
@@ -0,0 +1,257 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { crossSpawn } from '../../utils/compat';
3
+ import {
4
+ DEFAULT_MAX_MATCHES,
5
+ DEFAULT_MAX_OUTPUT_BYTES,
6
+ DEFAULT_TIMEOUT_MS,
7
+ findSgCliPathSync,
8
+ getSgCliPath,
9
+ setSgCliPath,
10
+ } from './constants';
11
+ import { ensureAstGrepBinary } from './downloader';
12
+ import type { CliLanguage, CliMatch, SgResult } from './types';
13
+
14
+ export interface RunOptions {
15
+ pattern: string;
16
+ lang: CliLanguage;
17
+ paths?: string[];
18
+ globs?: string[];
19
+ rewrite?: string;
20
+ context?: number;
21
+ updateAll?: boolean;
22
+ }
23
+
24
+ // Use a single init promise to avoid race conditions
25
+ let initPromise: Promise<string | null> | null = null;
26
+
27
+ export async function getAstGrepPath(): Promise<string | null> {
28
+ const currentPath = getSgCliPath();
29
+ if (currentPath !== 'sg' && existsSync(currentPath)) {
30
+ return currentPath;
31
+ }
32
+
33
+ if (initPromise) {
34
+ return initPromise;
35
+ }
36
+
37
+ initPromise = (async () => {
38
+ const syncPath = findSgCliPathSync();
39
+ if (syncPath && existsSync(syncPath)) {
40
+ setSgCliPath(syncPath);
41
+ return syncPath;
42
+ }
43
+
44
+ const downloadedPath = await ensureAstGrepBinary();
45
+ if (downloadedPath) {
46
+ setSgCliPath(downloadedPath);
47
+ return downloadedPath;
48
+ }
49
+
50
+ return null;
51
+ })();
52
+
53
+ return initPromise;
54
+ }
55
+
56
+ export function startBackgroundInit(): void {
57
+ if (!initPromise) {
58
+ initPromise = getAstGrepPath();
59
+ initPromise.catch((err) => {
60
+ console.warn(
61
+ '[ast-grep] Background initialization failed:',
62
+ err?.message ?? err,
63
+ );
64
+ });
65
+ }
66
+ }
67
+
68
+ export async function runSg(options: RunOptions): Promise<SgResult> {
69
+ const args = [
70
+ 'run',
71
+ '-p',
72
+ options.pattern,
73
+ '--lang',
74
+ options.lang,
75
+ '--json=compact',
76
+ ];
77
+
78
+ if (options.rewrite) {
79
+ args.push('-r', options.rewrite);
80
+ if (options.updateAll) {
81
+ args.push('--update-all');
82
+ }
83
+ }
84
+
85
+ if (options.context && options.context > 0) {
86
+ args.push('-C', String(options.context));
87
+ }
88
+
89
+ if (options.globs) {
90
+ for (const glob of options.globs) {
91
+ args.push('--globs', glob);
92
+ }
93
+ }
94
+
95
+ const paths =
96
+ options.paths && options.paths.length > 0 ? options.paths : ['.'];
97
+ args.push(...paths);
98
+
99
+ let cliPath = getSgCliPath();
100
+
101
+ if (!existsSync(cliPath) && cliPath !== 'sg') {
102
+ const downloadedPath = await getAstGrepPath();
103
+ if (downloadedPath) {
104
+ cliPath = downloadedPath;
105
+ }
106
+ }
107
+
108
+ const timeout = DEFAULT_TIMEOUT_MS;
109
+
110
+ const proc = crossSpawn([cliPath, ...args], {
111
+ stdout: 'pipe',
112
+ stderr: 'pipe',
113
+ });
114
+
115
+ const timeoutPromise = new Promise<never>((_, reject) => {
116
+ const id = setTimeout(() => {
117
+ proc.kill();
118
+ reject(new Error(`Search timeout after ${timeout}ms`));
119
+ }, timeout);
120
+ proc.exited.then(() => clearTimeout(id));
121
+ });
122
+
123
+ let stdout: string;
124
+ let stderr: string;
125
+ let exitCode: number;
126
+
127
+ try {
128
+ stdout = await Promise.race([proc.stdout(), timeoutPromise]);
129
+ stderr = await proc.stderr();
130
+ exitCode = await proc.exited;
131
+ } catch (e) {
132
+ const error = e as Error;
133
+ if (error.message?.includes('timeout')) {
134
+ return {
135
+ matches: [],
136
+ totalMatches: 0,
137
+ truncated: true,
138
+ truncatedReason: 'timeout',
139
+ error: error.message,
140
+ };
141
+ }
142
+
143
+ const nodeError = e as NodeJS.ErrnoException;
144
+ if (
145
+ nodeError.code === 'ENOENT' ||
146
+ nodeError.message?.includes('ENOENT') ||
147
+ nodeError.message?.includes('not found')
148
+ ) {
149
+ const downloadedPath = await ensureAstGrepBinary();
150
+ if (downloadedPath) {
151
+ setSgCliPath(downloadedPath);
152
+ return runSg(options);
153
+ } else {
154
+ return {
155
+ matches: [],
156
+ totalMatches: 0,
157
+ truncated: false,
158
+ error:
159
+ `ast-grep CLI binary not found.\n\n` +
160
+ `Auto-download failed. Manual install options:\n` +
161
+ ` bun add -D @ast-grep/cli\n` +
162
+ ` cargo install ast-grep --locked\n` +
163
+ ` brew install ast-grep`,
164
+ };
165
+ }
166
+ }
167
+
168
+ return {
169
+ matches: [],
170
+ totalMatches: 0,
171
+ truncated: false,
172
+ error: `Failed to spawn ast-grep: ${error.message}`,
173
+ };
174
+ }
175
+
176
+ if (exitCode !== 0 && stdout.trim() === '') {
177
+ if (stderr.includes('No files found')) {
178
+ return { matches: [], totalMatches: 0, truncated: false };
179
+ }
180
+ if (stderr.trim()) {
181
+ return {
182
+ matches: [],
183
+ totalMatches: 0,
184
+ truncated: false,
185
+ error: stderr.trim(),
186
+ };
187
+ }
188
+ return { matches: [], totalMatches: 0, truncated: false };
189
+ }
190
+
191
+ if (!stdout.trim()) {
192
+ return { matches: [], totalMatches: 0, truncated: false };
193
+ }
194
+
195
+ const outputTruncated = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES;
196
+ const outputToProcess = outputTruncated
197
+ ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES)
198
+ : stdout;
199
+
200
+ let matches: CliMatch[] = [];
201
+ try {
202
+ matches = JSON.parse(outputToProcess) as CliMatch[];
203
+ } catch {
204
+ if (outputTruncated) {
205
+ try {
206
+ const lastValidIndex = outputToProcess.lastIndexOf('}');
207
+ if (lastValidIndex > 0) {
208
+ const bracketIndex = outputToProcess.lastIndexOf(
209
+ '},',
210
+ lastValidIndex,
211
+ );
212
+ if (bracketIndex > 0) {
213
+ const truncatedJson = `${outputToProcess.substring(0, bracketIndex + 1)}]`;
214
+ matches = JSON.parse(truncatedJson) as CliMatch[];
215
+ }
216
+ }
217
+ } catch {
218
+ return {
219
+ matches: [],
220
+ totalMatches: 0,
221
+ truncated: true,
222
+ truncatedReason: 'max_output_bytes',
223
+ error: 'Output too large and could not be parsed',
224
+ };
225
+ }
226
+ } else {
227
+ return { matches: [], totalMatches: 0, truncated: false };
228
+ }
229
+ }
230
+
231
+ const totalMatches = matches.length;
232
+ const matchesTruncated = totalMatches > DEFAULT_MAX_MATCHES;
233
+ const finalMatches = matchesTruncated
234
+ ? matches.slice(0, DEFAULT_MAX_MATCHES)
235
+ : matches;
236
+
237
+ return {
238
+ matches: finalMatches,
239
+ totalMatches,
240
+ truncated: outputTruncated || matchesTruncated,
241
+ truncatedReason: outputTruncated
242
+ ? 'max_output_bytes'
243
+ : matchesTruncated
244
+ ? 'max_matches'
245
+ : undefined,
246
+ };
247
+ }
248
+
249
+ export function isCliAvailable(): boolean {
250
+ const path = findSgCliPathSync();
251
+ return path !== null && existsSync(path);
252
+ }
253
+
254
+ export async function ensureCliAvailable(): Promise<boolean> {
255
+ const path = await getAstGrepPath();
256
+ return path !== null && existsSync(path);
257
+ }
@@ -0,0 +1,214 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join } from 'node:path';
5
+ import { getCachedBinaryPath } from './downloader';
6
+ import { CLI_LANGUAGES } from './types';
7
+
8
+ type Platform = 'darwin' | 'linux' | 'win32' | 'unsupported';
9
+
10
+ // Minimum expected size for a valid sg binary (filters out stub files)
11
+ const MIN_BINARY_SIZE = 10_000;
12
+
13
+ function isValidBinary(filePath: string): boolean {
14
+ try {
15
+ return statSync(filePath).size > MIN_BINARY_SIZE;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function getPlatformPackageName(): string | null {
22
+ const platform = process.platform as Platform;
23
+ const arch = process.arch;
24
+
25
+ const platformMap: Record<string, string> = {
26
+ 'darwin-arm64': '@ast-grep/cli-darwin-arm64',
27
+ 'darwin-x64': '@ast-grep/cli-darwin-x64',
28
+ 'linux-arm64': '@ast-grep/cli-linux-arm64-gnu',
29
+ 'linux-x64': '@ast-grep/cli-linux-x64-gnu',
30
+ 'win32-x64': '@ast-grep/cli-win32-x64-msvc',
31
+ 'win32-arm64': '@ast-grep/cli-win32-arm64-msvc',
32
+ 'win32-ia32': '@ast-grep/cli-win32-ia32-msvc',
33
+ };
34
+
35
+ return platformMap[`${platform}-${arch}`] ?? null;
36
+ }
37
+
38
+ // Single source of truth for resolved CLI path
39
+ let resolvedCliPath: string | null = null;
40
+
41
+ export function findSgCliPathSync(): string | null {
42
+ const binaryName = process.platform === 'win32' ? 'sg.exe' : 'sg';
43
+
44
+ const cachedPath = getCachedBinaryPath();
45
+ if (cachedPath && isValidBinary(cachedPath)) {
46
+ return cachedPath;
47
+ }
48
+
49
+ try {
50
+ const require = createRequire(import.meta.url);
51
+ const cliPkgPath = require.resolve('@ast-grep/cli/package.json');
52
+ const cliDir = dirname(cliPkgPath);
53
+ const sgPath = join(cliDir, binaryName);
54
+
55
+ if (existsSync(sgPath) && isValidBinary(sgPath)) {
56
+ return sgPath;
57
+ }
58
+ } catch {
59
+ // @ast-grep/cli not installed
60
+ }
61
+
62
+ const platformPkg = getPlatformPackageName();
63
+ if (platformPkg) {
64
+ try {
65
+ const require = createRequire(import.meta.url);
66
+ const pkgPath = require.resolve(`${platformPkg}/package.json`);
67
+ const pkgDir = dirname(pkgPath);
68
+ const astGrepName =
69
+ process.platform === 'win32' ? 'ast-grep.exe' : 'ast-grep';
70
+ const binaryPath = join(pkgDir, astGrepName);
71
+
72
+ if (existsSync(binaryPath) && isValidBinary(binaryPath)) {
73
+ return binaryPath;
74
+ }
75
+ } catch {
76
+ // Platform-specific package not installed
77
+ }
78
+ }
79
+
80
+ if (process.platform === 'darwin') {
81
+ const homebrewPaths = ['/opt/homebrew/bin/sg', '/usr/local/bin/sg'];
82
+ for (const path of homebrewPaths) {
83
+ if (existsSync(path) && isValidBinary(path)) {
84
+ return path;
85
+ }
86
+ }
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ export function getSgCliPath(): string {
93
+ if (resolvedCliPath !== null) {
94
+ return resolvedCliPath;
95
+ }
96
+
97
+ const syncPath = findSgCliPathSync();
98
+ if (syncPath) {
99
+ resolvedCliPath = syncPath;
100
+ return syncPath;
101
+ }
102
+
103
+ return 'sg';
104
+ }
105
+
106
+ export function setSgCliPath(path: string): void {
107
+ resolvedCliPath = path;
108
+ }
109
+
110
+ // Re-export language constants
111
+ export { CLI_LANGUAGES };
112
+
113
+ // Defaults
114
+ export const DEFAULT_TIMEOUT_MS = 300_000;
115
+ export const DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
116
+ export const DEFAULT_MAX_MATCHES = 500;
117
+
118
+ export const LANG_EXTENSIONS: Record<string, string[]> = {
119
+ bash: ['.bash', '.sh', '.zsh', '.bats'],
120
+ c: ['.c', '.h'],
121
+ cpp: ['.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.h'],
122
+ csharp: ['.cs'],
123
+ css: ['.css'],
124
+ elixir: ['.ex', '.exs'],
125
+ go: ['.go'],
126
+ haskell: ['.hs', '.lhs'],
127
+ html: ['.html', '.htm'],
128
+ java: ['.java'],
129
+ javascript: ['.js', '.jsx', '.mjs', '.cjs'],
130
+ json: ['.json'],
131
+ kotlin: ['.kt', '.kts'],
132
+ lua: ['.lua'],
133
+ nix: ['.nix'],
134
+ php: ['.php'],
135
+ python: ['.py', '.pyi'],
136
+ ruby: ['.rb', '.rake'],
137
+ rust: ['.rs'],
138
+ scala: ['.scala', '.sc'],
139
+ solidity: ['.sol'],
140
+ swift: ['.swift'],
141
+ typescript: ['.ts', '.cts', '.mts'],
142
+ tsx: ['.tsx'],
143
+ yaml: ['.yml', '.yaml'],
144
+ };
145
+
146
+ export interface EnvironmentCheckResult {
147
+ cli: {
148
+ available: boolean;
149
+ path: string;
150
+ error?: string;
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Check if ast-grep CLI is available.
156
+ * Call this at startup to provide early feedback about missing dependencies.
157
+ */
158
+ export function checkEnvironment(): EnvironmentCheckResult {
159
+ const cliPath = getSgCliPath();
160
+ const result: EnvironmentCheckResult = {
161
+ cli: {
162
+ available: false,
163
+ path: cliPath,
164
+ },
165
+ };
166
+
167
+ if (existsSync(cliPath)) {
168
+ result.cli.available = true;
169
+ } else if (cliPath === 'sg') {
170
+ try {
171
+ const whichResult = spawnSync(
172
+ process.platform === 'win32' ? 'where' : 'which',
173
+ ['sg'],
174
+ {
175
+ encoding: 'utf-8',
176
+ timeout: 5000,
177
+ },
178
+ );
179
+ result.cli.available =
180
+ whichResult.status === 0 && !!whichResult.stdout?.trim();
181
+ if (!result.cli.available) {
182
+ result.cli.error = 'sg binary not found in PATH';
183
+ }
184
+ } catch {
185
+ result.cli.error = 'Failed to check sg availability';
186
+ }
187
+ } else {
188
+ result.cli.error = `Binary not found: ${cliPath}`;
189
+ }
190
+
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Format environment check result as user-friendly message.
196
+ */
197
+ export function formatEnvironmentCheck(result: EnvironmentCheckResult): string {
198
+ const lines: string[] = ['ast-grep Environment Status:', ''];
199
+
200
+ if (result.cli.available) {
201
+ lines.push(`✓ CLI: Available (${result.cli.path})`);
202
+ } else {
203
+ lines.push(`✗ CLI: Not available`);
204
+ if (result.cli.error) {
205
+ lines.push(` Error: ${result.cli.error}`);
206
+ }
207
+ lines.push(` Install: bun add -D @ast-grep/cli`);
208
+ }
209
+
210
+ lines.push('');
211
+ lines.push(`CLI supports ${CLI_LANGUAGES.length} languages`);
212
+
213
+ return lines.join('\n');
214
+ }
@@ -0,0 +1,131 @@
1
+ import { chmodSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { extractZip } from '../../utils';
6
+ import { crossWrite } from '../../utils/compat';
7
+
8
+ const REPO = 'ast-grep/ast-grep';
9
+
10
+ // IMPORTANT: Update this when bumping @ast-grep/cli in package.json
11
+ // This is only used as fallback when @ast-grep/cli package.json cannot be read
12
+ const DEFAULT_VERSION = '0.40.0';
13
+
14
+ function getAstGrepVersion(): string {
15
+ try {
16
+ const require = createRequire(import.meta.url);
17
+ const pkg = require('@ast-grep/cli/package.json');
18
+ return pkg.version;
19
+ } catch {
20
+ return DEFAULT_VERSION;
21
+ }
22
+ }
23
+
24
+ interface PlatformInfo {
25
+ arch: string;
26
+ os: string;
27
+ }
28
+
29
+ const PLATFORM_MAP: Record<string, PlatformInfo> = {
30
+ 'darwin-arm64': { arch: 'aarch64', os: 'apple-darwin' },
31
+ 'darwin-x64': { arch: 'x86_64', os: 'apple-darwin' },
32
+ 'linux-arm64': { arch: 'aarch64', os: 'unknown-linux-gnu' },
33
+ 'linux-x64': { arch: 'x86_64', os: 'unknown-linux-gnu' },
34
+ 'win32-x64': { arch: 'x86_64', os: 'pc-windows-msvc' },
35
+ 'win32-arm64': { arch: 'aarch64', os: 'pc-windows-msvc' },
36
+ 'win32-ia32': { arch: 'i686', os: 'pc-windows-msvc' },
37
+ };
38
+
39
+ export function getCacheDir(): string {
40
+ if (process.platform === 'win32') {
41
+ const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
42
+ const base = localAppData || join(homedir(), 'AppData', 'Local');
43
+ return join(base, 'opencode-dux', 'bin');
44
+ }
45
+
46
+ const xdgCache = process.env.XDG_CACHE_HOME;
47
+ const base = xdgCache || join(homedir(), '.cache');
48
+ return join(base, 'opencode-dux', 'bin');
49
+ }
50
+
51
+ export function getBinaryName(): string {
52
+ return process.platform === 'win32' ? 'sg.exe' : 'sg';
53
+ }
54
+
55
+ export function getCachedBinaryPath(): string | null {
56
+ const binaryPath = join(getCacheDir(), getBinaryName());
57
+ return existsSync(binaryPath) ? binaryPath : null;
58
+ }
59
+
60
+ export async function downloadAstGrep(
61
+ version: string = DEFAULT_VERSION,
62
+ ): Promise<string | null> {
63
+ const platformKey = `${process.platform}-${process.arch}`;
64
+ const platformInfo = PLATFORM_MAP[platformKey];
65
+
66
+ if (!platformInfo) {
67
+ console.error(
68
+ `[opencode-dux] Unsupported platform for ast-grep: ${platformKey}`,
69
+ );
70
+ return null;
71
+ }
72
+
73
+ const cacheDir = getCacheDir();
74
+ const binaryName = getBinaryName();
75
+ const binaryPath = join(cacheDir, binaryName);
76
+
77
+ if (existsSync(binaryPath)) {
78
+ return binaryPath;
79
+ }
80
+
81
+ const { arch, os } = platformInfo;
82
+ const assetName = `app-${arch}-${os}.zip`;
83
+ const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
84
+
85
+ console.log(`[opencode-dux] Downloading ast-grep binary...`);
86
+
87
+ try {
88
+ if (!existsSync(cacheDir)) {
89
+ mkdirSync(cacheDir, { recursive: true });
90
+ }
91
+
92
+ const response = await fetch(downloadUrl, { redirect: 'follow' });
93
+
94
+ if (!response.ok) {
95
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
96
+ }
97
+
98
+ const archivePath = join(cacheDir, assetName);
99
+ const arrayBuffer = await response.arrayBuffer();
100
+ await crossWrite(archivePath, arrayBuffer);
101
+
102
+ await extractZip(archivePath, cacheDir);
103
+
104
+ if (existsSync(archivePath)) {
105
+ unlinkSync(archivePath);
106
+ }
107
+
108
+ if (process.platform !== 'win32' && existsSync(binaryPath)) {
109
+ chmodSync(binaryPath, 0o755);
110
+ }
111
+
112
+ console.log(`[opencode-dux] ast-grep binary ready.`);
113
+
114
+ return binaryPath;
115
+ } catch (err) {
116
+ console.error(
117
+ `[opencode-dux] Failed to download ast-grep: ${err instanceof Error ? err.message : err}`,
118
+ );
119
+ return null;
120
+ }
121
+ }
122
+
123
+ export async function ensureAstGrepBinary(): Promise<string | null> {
124
+ const cachedPath = getCachedBinaryPath();
125
+ if (cachedPath) {
126
+ return cachedPath;
127
+ }
128
+
129
+ const version = getAstGrepVersion();
130
+ return downloadAstGrep(version);
131
+ }
@@ -0,0 +1,24 @@
1
+ import type { ToolDefinition } from '@opencode-ai/plugin';
2
+ import { ast_grep_replace, ast_grep_search } from './tools';
3
+
4
+ export const builtinTools: Record<string, ToolDefinition> = {
5
+ ast_grep_search,
6
+ ast_grep_replace,
7
+ };
8
+
9
+ export {
10
+ ensureCliAvailable,
11
+ getAstGrepPath,
12
+ isCliAvailable,
13
+ startBackgroundInit,
14
+ } from './cli';
15
+ export type { EnvironmentCheckResult } from './constants';
16
+ export { checkEnvironment, formatEnvironmentCheck } from './constants';
17
+ export {
18
+ ensureAstGrepBinary,
19
+ getCacheDir,
20
+ getCachedBinaryPath,
21
+ } from './downloader';
22
+ export type { CliLanguage, CliMatch, SgResult } from './types';
23
+ export { CLI_LANGUAGES } from './types';
24
+ export { ast_grep_replace, ast_grep_search };