@agentuity/cli 1.0.1 → 1.0.3

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 (331) hide show
  1. package/AGENTS.md +40 -24
  2. package/bin/cli.ts +47 -22
  3. package/dist/agent-detection.d.ts +23 -38
  4. package/dist/agent-detection.d.ts.map +1 -1
  5. package/dist/agent-detection.js +412 -153
  6. package/dist/agent-detection.js.map +1 -1
  7. package/dist/ai-help.d.ts +23 -0
  8. package/dist/ai-help.d.ts.map +1 -0
  9. package/dist/ai-help.js +328 -0
  10. package/dist/ai-help.js.map +1 -0
  11. package/dist/api.js +1 -1
  12. package/dist/api.js.map +1 -1
  13. package/dist/auth.d.ts +10 -1
  14. package/dist/auth.d.ts.map +1 -1
  15. package/dist/auth.js +176 -16
  16. package/dist/auth.js.map +1 -1
  17. package/dist/banner.d.ts.map +1 -1
  18. package/dist/banner.js +5 -0
  19. package/dist/banner.js.map +1 -1
  20. package/dist/cache/agent-intro.d.ts +13 -0
  21. package/dist/cache/agent-intro.d.ts.map +1 -0
  22. package/dist/cache/agent-intro.js +54 -0
  23. package/dist/cache/agent-intro.js.map +1 -0
  24. package/dist/cache/index.d.ts +1 -0
  25. package/dist/cache/index.d.ts.map +1 -1
  26. package/dist/cache/index.js +1 -0
  27. package/dist/cache/index.js.map +1 -1
  28. package/dist/cache/resource-region.d.ts +3 -2
  29. package/dist/cache/resource-region.d.ts.map +1 -1
  30. package/dist/cache/resource-region.js +13 -4
  31. package/dist/cache/resource-region.js.map +1 -1
  32. package/dist/catalyst.d.ts +7 -0
  33. package/dist/catalyst.d.ts.map +1 -0
  34. package/dist/catalyst.js +15 -0
  35. package/dist/catalyst.js.map +1 -0
  36. package/dist/cli.d.ts +12 -1
  37. package/dist/cli.d.ts.map +1 -1
  38. package/dist/cli.js +290 -67
  39. package/dist/cli.js.map +1 -1
  40. package/dist/cmd/ai/detect.d.ts +3 -0
  41. package/dist/cmd/ai/detect.d.ts.map +1 -0
  42. package/dist/cmd/ai/detect.js +49 -0
  43. package/dist/cmd/ai/detect.js.map +1 -0
  44. package/dist/cmd/ai/index.d.ts.map +1 -1
  45. package/dist/cmd/ai/index.js +18 -1
  46. package/dist/cmd/ai/index.js.map +1 -1
  47. package/dist/cmd/ai/intro.d.ts +7 -0
  48. package/dist/cmd/ai/intro.d.ts.map +1 -0
  49. package/dist/cmd/ai/intro.js +141 -0
  50. package/dist/cmd/ai/intro.js.map +1 -0
  51. package/dist/cmd/ai/opencode/run.d.ts.map +1 -1
  52. package/dist/cmd/ai/opencode/run.js +5 -0
  53. package/dist/cmd/ai/opencode/run.js.map +1 -1
  54. package/dist/cmd/build/ast.d.ts.map +1 -1
  55. package/dist/cmd/build/ast.js +79 -0
  56. package/dist/cmd/build/ast.js.map +1 -1
  57. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  58. package/dist/cmd/build/vite/bun-dev-server.js +2 -0
  59. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  60. package/dist/cmd/build/vite/docs-generator.d.ts.map +1 -1
  61. package/dist/cmd/build/vite/docs-generator.js +15 -1
  62. package/dist/cmd/build/vite/docs-generator.js.map +1 -1
  63. package/dist/cmd/build/vite/env-types-generator.d.ts +26 -0
  64. package/dist/cmd/build/vite/env-types-generator.d.ts.map +1 -0
  65. package/dist/cmd/build/vite/env-types-generator.js +110 -0
  66. package/dist/cmd/build/vite/env-types-generator.js.map +1 -0
  67. package/dist/cmd/build/vite/index.d.ts +2 -0
  68. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  69. package/dist/cmd/build/vite/index.js +12 -1
  70. package/dist/cmd/build/vite/index.js.map +1 -1
  71. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts.map +1 -1
  72. package/dist/cmd/build/vite/public-asset-path-plugin.js.map +1 -1
  73. package/dist/cmd/build/vite/vite-builder.d.ts +2 -0
  74. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  75. package/dist/cmd/build/vite/vite-builder.js +10 -1
  76. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  77. package/dist/cmd/cloud/db/create.js.map +1 -1
  78. package/dist/cmd/cloud/db/delete.js.map +1 -1
  79. package/dist/cmd/cloud/db/get.d.ts.map +1 -1
  80. package/dist/cmd/cloud/db/get.js +27 -12
  81. package/dist/cmd/cloud/db/get.js.map +1 -1
  82. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
  83. package/dist/cmd/cloud/deploy-fork.js +2 -0
  84. package/dist/cmd/cloud/deploy-fork.js.map +1 -1
  85. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  86. package/dist/cmd/cloud/deploy.js +17 -0
  87. package/dist/cmd/cloud/deploy.js.map +1 -1
  88. package/dist/cmd/cloud/env/import.js.map +1 -1
  89. package/dist/cmd/cloud/env/list.js.map +1 -1
  90. package/dist/cmd/cloud/env/push.js.map +1 -1
  91. package/dist/cmd/cloud/keyvalue/util.d.ts.map +1 -1
  92. package/dist/cmd/cloud/keyvalue/util.js +3 -3
  93. package/dist/cmd/cloud/keyvalue/util.js.map +1 -1
  94. package/dist/cmd/cloud/machine/list.js +3 -3
  95. package/dist/cmd/cloud/machine/list.js.map +1 -1
  96. package/dist/cmd/cloud/region/index.js.map +1 -1
  97. package/dist/cmd/cloud/region-lookup.d.ts +7 -4
  98. package/dist/cmd/cloud/region-lookup.d.ts.map +1 -1
  99. package/dist/cmd/cloud/region-lookup.js +59 -14
  100. package/dist/cmd/cloud/region-lookup.js.map +1 -1
  101. package/dist/cmd/cloud/sandbox/cp.d.ts.map +1 -1
  102. package/dist/cmd/cloud/sandbox/cp.js +7 -5
  103. package/dist/cmd/cloud/sandbox/cp.js.map +1 -1
  104. package/dist/cmd/cloud/sandbox/create.js +2 -2
  105. package/dist/cmd/cloud/sandbox/create.js.map +1 -1
  106. package/dist/cmd/cloud/sandbox/delete.d.ts.map +1 -1
  107. package/dist/cmd/cloud/sandbox/delete.js +8 -7
  108. package/dist/cmd/cloud/sandbox/delete.js.map +1 -1
  109. package/dist/cmd/cloud/sandbox/download.d.ts.map +1 -1
  110. package/dist/cmd/cloud/sandbox/download.js +7 -5
  111. package/dist/cmd/cloud/sandbox/download.js.map +1 -1
  112. package/dist/cmd/cloud/sandbox/env.d.ts.map +1 -1
  113. package/dist/cmd/cloud/sandbox/env.js +7 -5
  114. package/dist/cmd/cloud/sandbox/env.js.map +1 -1
  115. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  116. package/dist/cmd/cloud/sandbox/exec.js +7 -5
  117. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  118. package/dist/cmd/cloud/sandbox/get.d.ts.map +1 -1
  119. package/dist/cmd/cloud/sandbox/get.js +12 -7
  120. package/dist/cmd/cloud/sandbox/get.js.map +1 -1
  121. package/dist/cmd/cloud/sandbox/list.d.ts.map +1 -1
  122. package/dist/cmd/cloud/sandbox/list.js +40 -63
  123. package/dist/cmd/cloud/sandbox/list.js.map +1 -1
  124. package/dist/cmd/cloud/sandbox/ls.d.ts.map +1 -1
  125. package/dist/cmd/cloud/sandbox/ls.js +7 -5
  126. package/dist/cmd/cloud/sandbox/ls.js.map +1 -1
  127. package/dist/cmd/cloud/sandbox/mkdir.d.ts.map +1 -1
  128. package/dist/cmd/cloud/sandbox/mkdir.js +7 -5
  129. package/dist/cmd/cloud/sandbox/mkdir.js.map +1 -1
  130. package/dist/cmd/cloud/sandbox/rm.d.ts.map +1 -1
  131. package/dist/cmd/cloud/sandbox/rm.js +7 -5
  132. package/dist/cmd/cloud/sandbox/rm.js.map +1 -1
  133. package/dist/cmd/cloud/sandbox/rmdir.d.ts.map +1 -1
  134. package/dist/cmd/cloud/sandbox/rmdir.js +7 -5
  135. package/dist/cmd/cloud/sandbox/rmdir.js.map +1 -1
  136. package/dist/cmd/cloud/sandbox/run.js +1 -1
  137. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  138. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  139. package/dist/cmd/cloud/sandbox/snapshot/get.js.map +1 -1
  140. package/dist/cmd/cloud/sandbox/upload.d.ts.map +1 -1
  141. package/dist/cmd/cloud/sandbox/upload.js +7 -5
  142. package/dist/cmd/cloud/sandbox/upload.js.map +1 -1
  143. package/dist/cmd/cloud/sandbox/util.d.ts +2 -2
  144. package/dist/cmd/cloud/sandbox/util.d.ts.map +1 -1
  145. package/dist/cmd/cloud/sandbox/util.js +14 -13
  146. package/dist/cmd/cloud/sandbox/util.js.map +1 -1
  147. package/dist/cmd/cloud/ssh.d.ts.map +1 -1
  148. package/dist/cmd/cloud/ssh.js +3 -3
  149. package/dist/cmd/cloud/ssh.js.map +1 -1
  150. package/dist/cmd/cloud/storage/create.js.map +1 -1
  151. package/dist/cmd/cloud/storage/delete.js.map +1 -1
  152. package/dist/cmd/cloud/storage/get.d.ts.map +1 -1
  153. package/dist/cmd/cloud/storage/get.js +5 -11
  154. package/dist/cmd/cloud/storage/get.js.map +1 -1
  155. package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
  156. package/dist/cmd/cloud/storage/list.js +6 -6
  157. package/dist/cmd/cloud/storage/list.js.map +1 -1
  158. package/dist/cmd/cloud/stream/create.d.ts.map +1 -1
  159. package/dist/cmd/cloud/stream/create.js +7 -4
  160. package/dist/cmd/cloud/stream/create.js.map +1 -1
  161. package/dist/cmd/cloud/stream/delete.d.ts.map +1 -1
  162. package/dist/cmd/cloud/stream/delete.js +25 -4
  163. package/dist/cmd/cloud/stream/delete.js.map +1 -1
  164. package/dist/cmd/cloud/stream/get.d.ts.map +1 -1
  165. package/dist/cmd/cloud/stream/get.js +91 -62
  166. package/dist/cmd/cloud/stream/get.js.map +1 -1
  167. package/dist/cmd/cloud/stream/list.d.ts.map +1 -1
  168. package/dist/cmd/cloud/stream/list.js +66 -38
  169. package/dist/cmd/cloud/stream/list.js.map +1 -1
  170. package/dist/cmd/cloud/stream/util.d.ts +20 -0
  171. package/dist/cmd/cloud/stream/util.d.ts.map +1 -1
  172. package/dist/cmd/cloud/stream/util.js +27 -3
  173. package/dist/cmd/cloud/stream/util.js.map +1 -1
  174. package/dist/cmd/cloud/vector/util.d.ts.map +1 -1
  175. package/dist/cmd/cloud/vector/util.js +3 -3
  176. package/dist/cmd/cloud/vector/util.js.map +1 -1
  177. package/dist/cmd/dev/index.js.map +1 -1
  178. package/dist/cmd/git/account/add.js.map +1 -1
  179. package/dist/cmd/git/list.js.map +1 -1
  180. package/dist/cmd/project/add/database.d.ts +2 -0
  181. package/dist/cmd/project/add/database.d.ts.map +1 -0
  182. package/dist/cmd/project/add/database.js +123 -0
  183. package/dist/cmd/project/add/database.js.map +1 -0
  184. package/dist/cmd/project/add/domain.d.ts +2 -0
  185. package/dist/cmd/project/add/domain.d.ts.map +1 -0
  186. package/dist/cmd/project/add/domain.js +152 -0
  187. package/dist/cmd/project/add/domain.js.map +1 -0
  188. package/dist/cmd/project/add/index.d.ts +2 -0
  189. package/dist/cmd/project/add/index.d.ts.map +1 -0
  190. package/dist/cmd/project/add/index.js +35 -0
  191. package/dist/cmd/project/add/index.js.map +1 -0
  192. package/dist/cmd/project/add/storage.d.ts +2 -0
  193. package/dist/cmd/project/add/storage.d.ts.map +1 -0
  194. package/dist/cmd/project/add/storage.js +123 -0
  195. package/dist/cmd/project/add/storage.js.map +1 -0
  196. package/dist/cmd/project/auth/init.js.map +1 -1
  197. package/dist/cmd/project/index.d.ts.map +1 -1
  198. package/dist/cmd/project/index.js +7 -0
  199. package/dist/cmd/project/index.js.map +1 -1
  200. package/dist/cmd/project/reconcile.d.ts.map +1 -1
  201. package/dist/cmd/project/reconcile.js +32 -0
  202. package/dist/cmd/project/reconcile.js.map +1 -1
  203. package/dist/cmd/support/report.js.map +1 -1
  204. package/dist/cmd/support/system.js +2 -2
  205. package/dist/cmd/support/system.js.map +1 -1
  206. package/dist/config.d.ts +6 -3
  207. package/dist/config.d.ts.map +1 -1
  208. package/dist/config.js +31 -7
  209. package/dist/config.js.map +1 -1
  210. package/dist/errors.d.ts +2 -1
  211. package/dist/errors.d.ts.map +1 -1
  212. package/dist/errors.js +5 -0
  213. package/dist/errors.js.map +1 -1
  214. package/dist/index.d.ts +4 -1
  215. package/dist/index.d.ts.map +1 -1
  216. package/dist/index.js +3 -0
  217. package/dist/index.js.map +1 -1
  218. package/dist/repl.js +2 -1
  219. package/dist/repl.js.map +1 -1
  220. package/dist/tui/box.d.ts +3 -1
  221. package/dist/tui/box.d.ts.map +1 -1
  222. package/dist/tui/box.js +22 -7
  223. package/dist/tui/box.js.map +1 -1
  224. package/dist/tui/colors.d.ts +0 -3
  225. package/dist/tui/colors.d.ts.map +1 -1
  226. package/dist/tui/colors.js +76 -23
  227. package/dist/tui/colors.js.map +1 -1
  228. package/dist/tui/prompt.d.ts +2 -0
  229. package/dist/tui/prompt.d.ts.map +1 -1
  230. package/dist/tui/prompt.js +44 -3
  231. package/dist/tui/prompt.js.map +1 -1
  232. package/dist/tui/symbols.d.ts +0 -4
  233. package/dist/tui/symbols.d.ts.map +1 -1
  234. package/dist/tui/symbols.js +5 -0
  235. package/dist/tui/symbols.js.map +1 -1
  236. package/dist/tui.d.ts +8 -0
  237. package/dist/tui.d.ts.map +1 -1
  238. package/dist/tui.js +54 -9
  239. package/dist/tui.js.map +1 -1
  240. package/dist/types.d.ts +37 -2
  241. package/dist/types.d.ts.map +1 -1
  242. package/dist/types.js +1 -0
  243. package/dist/types.js.map +1 -1
  244. package/dist/version-check.d.ts.map +1 -1
  245. package/dist/version-check.js +5 -0
  246. package/dist/version-check.js.map +1 -1
  247. package/package.json +6 -6
  248. package/src/agent-detection.ts +457 -160
  249. package/src/ai-help.ts +393 -0
  250. package/src/api.ts +1 -1
  251. package/src/auth.ts +226 -17
  252. package/src/banner.ts +5 -0
  253. package/src/cache/agent-intro.ts +62 -0
  254. package/src/cache/index.ts +2 -0
  255. package/src/cache/resource-region.ts +28 -7
  256. package/src/catalyst.ts +16 -0
  257. package/src/cli.ts +375 -93
  258. package/src/cmd/ai/detect.ts +54 -0
  259. package/src/cmd/ai/index.ts +18 -1
  260. package/src/cmd/ai/intro.ts +154 -0
  261. package/src/cmd/ai/opencode/run.ts +5 -0
  262. package/src/cmd/build/ast.ts +97 -0
  263. package/src/cmd/build/vite/bun-dev-server.ts +2 -0
  264. package/src/cmd/build/vite/docs-generator.ts +15 -1
  265. package/src/cmd/build/vite/env-types-generator.ts +145 -0
  266. package/src/cmd/build/vite/index.ts +15 -0
  267. package/src/cmd/build/vite/public-asset-path-plugin.ts +8 -2
  268. package/src/cmd/build/vite/vite-builder.ts +31 -11
  269. package/src/cmd/cloud/db/create.ts +16 -16
  270. package/src/cmd/cloud/db/delete.ts +19 -19
  271. package/src/cmd/cloud/db/get.ts +32 -17
  272. package/src/cmd/cloud/deploy-fork.ts +2 -0
  273. package/src/cmd/cloud/deploy.ts +17 -0
  274. package/src/cmd/cloud/env/import.ts +6 -6
  275. package/src/cmd/cloud/env/list.ts +11 -11
  276. package/src/cmd/cloud/env/push.ts +6 -6
  277. package/src/cmd/cloud/keyvalue/util.ts +3 -3
  278. package/src/cmd/cloud/machine/list.ts +3 -3
  279. package/src/cmd/cloud/region/index.ts +3 -3
  280. package/src/cmd/cloud/region-lookup.ts +82 -22
  281. package/src/cmd/cloud/sandbox/cp.ts +9 -4
  282. package/src/cmd/cloud/sandbox/create.ts +2 -2
  283. package/src/cmd/cloud/sandbox/delete.ts +10 -7
  284. package/src/cmd/cloud/sandbox/download.ts +8 -5
  285. package/src/cmd/cloud/sandbox/env.ts +8 -5
  286. package/src/cmd/cloud/sandbox/exec.ts +10 -5
  287. package/src/cmd/cloud/sandbox/get.ts +13 -7
  288. package/src/cmd/cloud/sandbox/list.ts +47 -73
  289. package/src/cmd/cloud/sandbox/ls.ts +9 -5
  290. package/src/cmd/cloud/sandbox/mkdir.ts +9 -5
  291. package/src/cmd/cloud/sandbox/rm.ts +9 -5
  292. package/src/cmd/cloud/sandbox/rmdir.ts +9 -5
  293. package/src/cmd/cloud/sandbox/run.ts +1 -1
  294. package/src/cmd/cloud/sandbox/snapshot/build.ts +31 -31
  295. package/src/cmd/cloud/sandbox/snapshot/get.ts +17 -17
  296. package/src/cmd/cloud/sandbox/upload.ts +8 -5
  297. package/src/cmd/cloud/sandbox/util.ts +15 -14
  298. package/src/cmd/cloud/ssh.ts +2 -4
  299. package/src/cmd/cloud/storage/create.ts +16 -16
  300. package/src/cmd/cloud/storage/delete.ts +19 -19
  301. package/src/cmd/cloud/storage/get.ts +5 -16
  302. package/src/cmd/cloud/storage/list.ts +12 -6
  303. package/src/cmd/cloud/stream/create.ts +8 -4
  304. package/src/cmd/cloud/stream/delete.ts +28 -4
  305. package/src/cmd/cloud/stream/get.ts +102 -64
  306. package/src/cmd/cloud/stream/list.ts +76 -44
  307. package/src/cmd/cloud/stream/util.ts +39 -3
  308. package/src/cmd/cloud/vector/util.ts +3 -3
  309. package/src/cmd/dev/index.ts +4 -4
  310. package/src/cmd/git/account/add.ts +5 -5
  311. package/src/cmd/git/list.ts +7 -7
  312. package/src/cmd/project/add/database.ts +145 -0
  313. package/src/cmd/project/add/domain.ts +181 -0
  314. package/src/cmd/project/add/index.ts +35 -0
  315. package/src/cmd/project/add/storage.ts +147 -0
  316. package/src/cmd/project/auth/init.ts +6 -6
  317. package/src/cmd/project/index.ts +7 -0
  318. package/src/cmd/project/reconcile.ts +40 -0
  319. package/src/cmd/support/report.ts +5 -5
  320. package/src/cmd/support/system.ts +2 -2
  321. package/src/config.ts +40 -12
  322. package/src/errors.ts +7 -0
  323. package/src/index.ts +11 -0
  324. package/src/repl.ts +4 -1
  325. package/src/tui/box.ts +24 -9
  326. package/src/tui/colors.ts +83 -26
  327. package/src/tui/prompt.ts +55 -3
  328. package/src/tui/symbols.ts +6 -0
  329. package/src/tui.ts +55 -9
  330. package/src/types.ts +46 -2
  331. package/src/version-check.ts +6 -0
@@ -1,8 +1,9 @@
1
- import { spawn } from 'node:child_process';
1
+ import { dlopen, FFIType, ptr } from 'bun:ffi';
2
+ import { readFileSync } from 'node:fs';
2
3
 
3
4
  /**
4
5
  * Map of process names to internal agent short names.
5
- * The key is the process name (or substring) that appears in the parent process command line.
6
+ * The key is the process name (or substring) that appears in the parent process path or command line.
6
7
  * The value is the internal short name used to identify the agent.
7
8
  *
8
9
  * Process names verified via `agentuity cloud sandbox run --runtime <agent>:latest`:
@@ -30,6 +31,7 @@ export const KNOWN_AGENTS: [string, string][] = [
30
31
  ['windsurf', 'windsurf'],
31
32
  ['zed', 'zed'],
32
33
  ['amp', 'amp'],
34
+ ['warp', 'warp'],
33
35
  // TODO: VSCode Agent Mode detection - need to find a reliable way to detect
34
36
  // when VSCode's built-in agent (Copilot Chat) is running commands vs just
35
37
  // running in VSCode's integrated terminal. May need env var detection.
@@ -38,234 +40,529 @@ export const KNOWN_AGENTS: [string, string][] = [
38
40
  export type KnownAgent = (typeof KNOWN_AGENTS)[number][1];
39
41
 
40
42
  /**
41
- * Promise for the detection result (set when detection starts)
43
+ * Display names for known agents (human-friendly names)
42
44
  */
43
- let detectionPromise: Promise<string | undefined> | null = null;
45
+ export const AGENT_DISPLAY_NAMES: Record<string, string> = {
46
+ opencode: 'Open Code',
47
+ codex: 'OpenAI Codex',
48
+ cursor: 'Cursor',
49
+ 'claude-code': 'Claude Code',
50
+ copilot: 'GitHub Copilot',
51
+ gemini: 'Gemini',
52
+ cline: 'Cline',
53
+ roo: 'Roo Code',
54
+ windsurf: 'Windsurf',
55
+ zed: 'Zed',
56
+ amp: 'Amp',
57
+ warp: 'Warp',
58
+ };
44
59
 
45
60
  /**
46
- * Cached result after detection completes (null = not yet resolved)
61
+ * Get the display name for an agent ID
47
62
  */
48
- let cachedResult: string | undefined | null = null;
63
+ export function getAgentDisplayName(agentId: string): string {
64
+ return AGENT_DISPLAY_NAMES[agentId] || agentId;
65
+ }
66
+
67
+ // ============================================================================
68
+ // FFI-based Fast Process Path Resolution
69
+ // ============================================================================
70
+
71
+ const PATH_MAX = 4096; // Linux PATH_MAX, also sufficient for macOS
72
+ const ARG_MAX = 65536; // Maximum command line length
49
73
 
50
74
  /**
51
- * Callbacks to invoke when agent detection completes
75
+ * Type for the FFI function that gets a process path
52
76
  */
53
- const detectionCallbacks: Array<(agent: string | undefined) => void> = [];
77
+ type GetProcessPath = (pid: number) => string | null;
54
78
 
55
79
  /**
56
- * Get the command line and parent PID for a given PID in a single ps call
80
+ * Type for the FFI function that gets a parent PID
57
81
  */
58
- function getProcessInfo(pid: number): Promise<{ command: string; ppid: number } | undefined> {
59
- return new Promise((resolve) => {
60
- const ps = spawn('ps', ['-p', String(pid), '-o', 'ppid=,command='], {
61
- stdio: ['ignore', 'pipe', 'ignore'],
62
- });
63
-
64
- let output = '';
65
-
66
- ps.stdout.on('data', (data: Buffer) => {
67
- output += data.toString();
68
- });
69
-
70
- ps.on('close', () => {
71
- const trimmed = output.trim();
72
- if (!trimmed) {
73
- resolve(undefined);
74
- return;
75
- }
76
-
77
- // Output format: " PPID COMMAND" (ppid is right-aligned, then space, then command)
78
- const match = trimmed.match(/^\s*(\d+)\s+(.+)$/);
79
- if (!match) {
80
- resolve(undefined);
81
- return;
82
- }
83
-
84
- const ppidStr = match[1];
85
- const commandStr = match[2];
86
- if (!ppidStr || !commandStr) {
87
- resolve(undefined);
88
- return;
89
- }
90
-
91
- const ppid = parseInt(ppidStr, 10);
92
- const command = commandStr.toLowerCase();
93
-
94
- if (isNaN(ppid) || ppid <= 1 || !command) {
95
- resolve(undefined);
96
- return;
97
- }
82
+ type GetParentPid = (pid: number) => number | null;
98
83
 
99
- resolve({ command, ppid });
100
- });
84
+ /**
85
+ * Type for the FFI function that gets command line arguments
86
+ */
87
+ type GetProcessCmdline = (pid: number) => string | null;
101
88
 
102
- ps.on('error', () => {
103
- resolve(undefined);
104
- });
105
- });
89
+ interface FFIFunctions {
90
+ getProcessPath: GetProcessPath;
91
+ getParentPid: GetParentPid;
92
+ getProcessCmdline: GetProcessCmdline;
106
93
  }
107
94
 
108
95
  /**
109
- * Check if a command matches any known agent
96
+ * Stub FFI functions for unsupported platforms or when FFI fails to load.
110
97
  */
111
- function matchAgent(command: string): string | undefined {
112
- for (const [processName, agentName] of KNOWN_AGENTS) {
113
- if (command.includes(processName)) {
114
- return agentName;
98
+ const unsupportedFFI: FFIFunctions = {
99
+ getProcessPath: () => null,
100
+ getParentPid: () => null,
101
+ getProcessCmdline: () => null,
102
+ };
103
+
104
+ /**
105
+ * Initialize FFI functions for the current platform.
106
+ * Returns null functions for unsupported platforms or if FFI initialization fails.
107
+ */
108
+ function initFFI(): FFIFunctions {
109
+ try {
110
+ if (process.platform === 'darwin') {
111
+ return initDarwinFFI();
112
+ } else if (process.platform === 'linux') {
113
+ return initLinuxFFI();
115
114
  }
115
+ } catch (err) {
116
+ // FFI initialization failed (e.g., missing libc on musl/Alpine, dlopen error)
117
+ // Fall back to unsupported stub - agent detection will be skipped
118
+ if (process.env.AGENTUITY_DEBUG_AGENT_DETECTION === '1') {
119
+ console.error(
120
+ '[agent-detection] FFI initialization failed:',
121
+ err instanceof Error ? err.message : err
122
+ );
123
+ }
124
+ return unsupportedFFI;
116
125
  }
117
- return undefined;
126
+ // Unsupported platform (Windows, etc.)
127
+ return unsupportedFFI;
118
128
  }
119
129
 
120
130
  /**
121
- * Detect the parent process command and check if it matches a known agent.
122
- * Walks up the process tree to find the first matching agent.
131
+ * Initialize macOS FFI functions using libSystem.dylib
132
+ *
133
+ * NOTE: Shared mutable buffers (pathBuf, argBuf, kinfoBuffer) are safe only in
134
+ * single-threaded use. Detection is synchronous and single-threaded, but any
135
+ * future concurrent usage would corrupt these buffers.
123
136
  */
124
- function detectParentAgent(): Promise<string | undefined> {
125
- return new Promise((resolve) => {
126
- // TODO: Implement Windows support using wmic or PowerShell
127
- if (process.platform === 'win32') {
128
- resolve(undefined);
129
- return;
130
- }
137
+ function initDarwinFFI(): FFIFunctions {
138
+ // Shared buffers - reused across calls (single-threaded assumption)
139
+ const pathBuf = new Uint8Array(PATH_MAX);
140
+ const argBuf = new Uint8Array(ARG_MAX);
141
+
142
+ // Size of kinfo_proc struct on macOS (arm64 and x86_64)
143
+ const KINFO_PROC_SIZE = 648;
144
+ const kinfoBuffer = new Uint8Array(KINFO_PROC_SIZE);
145
+
146
+ // Load libSystem for proc_pidpath and sysctl
147
+ const lib = dlopen('libSystem.dylib', {
148
+ proc_pidpath: {
149
+ args: [FFIType.i32, FFIType.ptr, FFIType.u32],
150
+ returns: FFIType.i32,
151
+ },
152
+ sysctl: {
153
+ args: [FFIType.ptr, FFIType.u32, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.u64],
154
+ returns: FFIType.i32,
155
+ },
156
+ });
131
157
 
132
- const maxDepth = 10; // Limit how far up we walk the tree
158
+ const getProcessPath: GetProcessPath = (pid: number) => {
159
+ if (pid <= 0) return null;
160
+ try {
161
+ const len = lib.symbols.proc_pidpath(pid, ptr(pathBuf), PATH_MAX);
162
+ if (len > 0) {
163
+ return new TextDecoder().decode(pathBuf.subarray(0, len)).replace(/\0.*/, '');
164
+ }
165
+ } catch {
166
+ // Ignore errors (process may have died, permission denied, etc.)
167
+ }
168
+ return null;
169
+ };
133
170
 
134
- async function walkTree(pid: number, depth: number): Promise<string | undefined> {
135
- if (depth >= maxDepth || pid <= 1) {
136
- return undefined;
171
+ // Get parent PID using sysctl KERN_PROC_PID
172
+ // CTL_KERN = 1, KERN_PROC = 14, KERN_PROC_PID = 1
173
+ const getParentPid: GetParentPid = (pid: number) => {
174
+ if (pid <= 1) return null;
175
+ try {
176
+ const mib = new Int32Array([1, 14, 1, pid]);
177
+ const sizePtr = new BigUint64Array([BigInt(KINFO_PROC_SIZE)]);
178
+
179
+ const result = lib.symbols.sysctl(ptr(mib), 4, ptr(kinfoBuffer), ptr(sizePtr), null, 0);
180
+ if (result === 0) {
181
+ // e_ppid offset in kinfo_proc on macOS arm64/x86_64:
182
+ // kp_eproc starts at 296, e_ppid is at offset 264 within kp_eproc
183
+ // Total offset: 560
184
+ const view = new DataView(kinfoBuffer.buffer);
185
+ const ppid = view.getInt32(560, true); // little-endian
186
+ return ppid > 1 ? ppid : null;
137
187
  }
188
+ } catch {
189
+ // Ignore errors
190
+ }
191
+ return null;
192
+ };
138
193
 
139
- // Get both command and parent PID in a single ps call
140
- const info = await getProcessInfo(pid);
141
- if (!info) {
142
- return undefined;
194
+ // Get command line using sysctl KERN_PROCARGS2
195
+ // CTL_KERN = 1, KERN_PROCARGS2 = 49
196
+ const getProcessCmdline: GetProcessCmdline = (pid: number) => {
197
+ if (pid <= 0) return null;
198
+ try {
199
+ // MIB for KERN_PROCARGS2: [CTL_KERN, KERN_PROCARGS2, pid]
200
+ const mib = new Int32Array([1, 49, pid]);
201
+ const sizePtr = new BigUint64Array([BigInt(ARG_MAX)]);
202
+
203
+ const result = lib.symbols.sysctl(ptr(mib), 3, ptr(argBuf), ptr(sizePtr), null, 0);
204
+
205
+ if (result === 0) {
206
+ const size = Number(sizePtr[0]);
207
+ if (size > 0) {
208
+ // KERN_PROCARGS2 format:
209
+ // - 4 bytes: argc (number of arguments)
210
+ // - exec_path\0
211
+ // - padding (zeros until aligned)
212
+ // - arg0\0arg1\0arg2\0...
213
+ // We want to extract exactly argc args (not env vars that follow)
214
+ const data = argBuf.subarray(0, size);
215
+
216
+ // Read argc (first 4 bytes, little-endian)
217
+ const argc = new DataView(data.buffer, data.byteOffset).getInt32(0, true);
218
+ let offset = 4;
219
+
220
+ // Skip exec_path (find first null)
221
+ while (offset < size && data[offset] !== 0) offset++;
222
+ offset++; // Skip the null
223
+
224
+ // Skip padding (multiple nulls)
225
+ while (offset < size && data[offset] === 0) offset++;
226
+
227
+ // Now we're at the arguments - collect exactly argc args
228
+ const args: string[] = [];
229
+ let start = offset;
230
+ for (let i = offset; i < size; i++) {
231
+ if (data[i] === 0) {
232
+ if (i > start) {
233
+ args.push(new TextDecoder().decode(data.subarray(start, i)));
234
+ }
235
+ start = i + 1;
236
+ // Stop after collecting argc args
237
+ if (args.length >= argc) break;
238
+ }
239
+ }
240
+
241
+ return args.join(' ');
242
+ }
143
243
  }
244
+ } catch {
245
+ // Ignore errors
246
+ }
247
+ return null;
248
+ };
144
249
 
145
- // Check if this process matches a known agent
146
- const agent = matchAgent(info.command);
147
- if (agent) {
148
- return agent;
250
+ return { getProcessPath, getParentPid, getProcessCmdline };
251
+ }
252
+
253
+ /**
254
+ * Initialize Linux FFI functions using libc.so.6
255
+ */
256
+ function initLinuxFFI(): FFIFunctions {
257
+ const pathBuf = new Uint8Array(PATH_MAX);
258
+
259
+ const lib = dlopen('libc.so.6', {
260
+ readlink: {
261
+ args: [FFIType.cstring, FFIType.ptr, FFIType.u64],
262
+ returns: FFIType.i64,
263
+ },
264
+ });
265
+
266
+ const getProcessPath: GetProcessPath = (pid: number) => {
267
+ if (pid <= 0) return null;
268
+ try {
269
+ const procPath = Buffer.from(`/proc/${pid}/exe\0`);
270
+ const len = Number(lib.symbols.readlink(ptr(procPath), ptr(pathBuf), PATH_MAX));
271
+ if (len > 0) {
272
+ return new TextDecoder().decode(pathBuf.subarray(0, len));
149
273
  }
274
+ } catch {
275
+ // Ignore errors (process may have died, permission denied, etc.)
276
+ }
277
+ return null;
278
+ };
150
279
 
151
- // Walk up to parent
152
- return walkTree(info.ppid, depth + 1);
280
+ const getParentPid: GetParentPid = (pid: number) => {
281
+ if (pid <= 1) return null;
282
+ try {
283
+ // Read /proc/{pid}/stat to get parent PID (4th field)
284
+ const statPath = `/proc/${pid}/stat`;
285
+ const content = readFileSync(statPath, 'utf-8');
286
+ // Format: pid (comm) state ppid ...
287
+ // Need to handle comm with spaces/parens: find last ')' then parse
288
+ const lastParen = content.lastIndexOf(')');
289
+ if (lastParen === -1) return null;
290
+ const rest = content.slice(lastParen + 2); // Skip ') '
291
+ const fields = rest.split(' ');
292
+ const ppidField = fields[1]; // ppid is 2nd field after state
293
+ if (!ppidField) return null;
294
+ const ppid = parseInt(ppidField, 10);
295
+ return isNaN(ppid) || ppid <= 1 ? null : ppid;
296
+ } catch {
297
+ // Ignore errors
153
298
  }
299
+ return null;
300
+ };
154
301
 
155
- const ppid = process.ppid;
156
- if (!ppid) {
157
- resolve(undefined);
158
- return;
302
+ // Read command line from /proc/{pid}/cmdline (null-separated args)
303
+ const getProcessCmdline: GetProcessCmdline = (pid: number) => {
304
+ if (pid <= 0) return null;
305
+ try {
306
+ const cmdlinePath = `/proc/${pid}/cmdline`;
307
+ const content = readFileSync(cmdlinePath);
308
+ if (content.length > 0) {
309
+ // Replace null bytes with spaces to get full command line
310
+ return new TextDecoder().decode(content).replace(/\0/g, ' ').trim();
311
+ }
312
+ } catch {
313
+ // Ignore errors
159
314
  }
315
+ return null;
316
+ };
160
317
 
161
- walkTree(ppid, 0)
162
- .then(resolve)
163
- .catch(() => resolve(undefined));
164
- });
318
+ return { getProcessPath, getParentPid, getProcessCmdline };
319
+ }
320
+
321
+ // Initialize FFI functions lazily
322
+ let ffi: FFIFunctions | null = null;
323
+
324
+ function getFFI(): FFIFunctions {
325
+ if (!ffi) {
326
+ ffi = initFFI();
327
+ }
328
+ return ffi;
165
329
  }
166
330
 
331
+ // ============================================================================
332
+ // Agent Detection Logic
333
+ // ============================================================================
334
+
167
335
  /**
168
- * Start agent detection immediately (non-blocking).
169
- * Call this early in CLI startup to begin detection in the background.
336
+ * Cached detection result (null = not yet run, undefined = no agent detected)
170
337
  */
171
- export function startAgentDetection(): void {
172
- if (detectionPromise !== null) {
173
- // Already started
174
- return;
175
- }
338
+ let cachedResult: string | undefined | null = null;
176
339
 
177
- detectionPromise = detectParentAgent().then((result) => {
178
- cachedResult = result;
179
- // Invoke all registered callbacks
180
- for (const callback of detectionCallbacks) {
181
- try {
182
- callback(result);
183
- } catch {
184
- // Ignore callback errors
185
- }
340
+ /**
341
+ * Check if a path's basename matches any known agent
342
+ */
343
+ function matchAgentPath(path: string): string | undefined {
344
+ // Extract basename from path
345
+ const basename = path.split('/').pop()?.toLowerCase() ?? '';
346
+ for (const [processName, agentName] of KNOWN_AGENTS) {
347
+ if (basename.includes(processName)) {
348
+ return agentName;
186
349
  }
187
- return result;
188
- });
350
+ }
351
+ return undefined;
189
352
  }
190
353
 
191
354
  /**
192
- * Register a callback to be invoked when agent detection completes.
193
- * If detection has already completed, the callback is invoked immediately.
194
- * This is non-blocking and does not return a promise.
195
- *
196
- * @example
197
- * ```typescript
198
- * onAgentDetected((agent) => {
199
- * if (agent) {
200
- * console.log(`Detected agent: ${agent}`);
201
- * }
202
- * });
203
- * ```
355
+ * Check if a cmdline matches any known agent.
356
+ * Only checks argv[0] and arguments that look like executable paths,
357
+ * NOT environment variables or arbitrary path strings.
204
358
  */
205
- export function onAgentDetected(callback: (agent: string | undefined) => void): void {
206
- // If detection already completed, invoke immediately
207
- if (cachedResult !== null) {
208
- try {
209
- callback(cachedResult);
210
- } catch {
211
- // Ignore callback errors
359
+ function matchAgentCmdline(cmdline: string): string | undefined {
360
+ // Split cmdline into arguments (null-separated on macOS/Linux, but we get space-separated here)
361
+ // The cmdline format from our FFI is: "cmd arg1 arg2 ENV1=val1 ENV2=val2..."
362
+ // We only want to check the command and args that look like executables
363
+ const parts = cmdline.split(/\s+/);
364
+
365
+ for (const part of parts) {
366
+ // Skip environment variables (contain =)
367
+ if (part.includes('=')) {
368
+ continue;
212
369
  }
213
- return;
214
- }
215
370
 
216
- // Otherwise, register for later invocation
217
- detectionCallbacks.push(callback);
371
+ // Check if this looks like an executable path or command
372
+ // - Starts with / (absolute path)
373
+ // - Is a simple command name (no path separators, no special chars)
374
+ const isPath = part.startsWith('/');
375
+ const isSimpleCommand = !part.includes('/') && !part.includes('=') && part.length < 50;
376
+
377
+ if (isPath || isSimpleCommand) {
378
+ const basename = part.split('/').pop()?.toLowerCase() ?? '';
379
+ for (const [processName, agentName] of KNOWN_AGENTS) {
380
+ if (basename.includes(processName)) {
381
+ return agentName;
382
+ }
383
+ }
384
+ }
385
+ }
386
+ return undefined;
218
387
  }
219
388
 
220
389
  /**
221
- * Get the cached detection result synchronously.
222
- * Returns undefined if detection hasn't completed yet or no agent was detected.
223
- * Returns null if detection hasn't started or completed yet.
224
- *
225
- * Use this for synchronous access when you don't want to wait for detection.
390
+ * Check if stdin is connected to a TTY (interactive terminal).
391
+ * When humans type commands, stdin is usually a TTY.
392
+ * When agents run commands programmatically, stdin is usually piped/closed.
226
393
  */
227
- export function getDetectedAgent(): string | undefined | null {
228
- return cachedResult;
394
+ function isInteractiveSession(): boolean {
395
+ return process.stdin.isTTY === true;
396
+ }
397
+
398
+ // Enable debug output with AGENTUITY_DEBUG_AGENT_DETECTION=1
399
+ const DEBUG = process.env.AGENTUITY_DEBUG_AGENT_DETECTION === '1';
400
+
401
+ function debugLog(...args: unknown[]): void {
402
+ if (DEBUG) {
403
+ console.error('[agent-detection]', ...args);
404
+ }
229
405
  }
230
406
 
231
407
  /**
232
- * Wait for agent detection to complete and ensure all callbacks have been invoked.
233
- * Call this before CLI exit to ensure the detected agent is written to session logs.
408
+ * Synchronously detect the parent agent by walking up the process tree.
409
+ * Uses FFI for fast process path and command line resolution.
234
410
  *
235
- * This is a no-op if detection hasn't started or has already completed.
411
+ * Detection strategy:
412
+ * - If stdin is a TTY (interactive session), don't report as agent
413
+ * (human is typing commands, even if inside an agent's terminal)
414
+ * - If stdin is NOT a TTY, check the process tree for known agents
236
415
  */
237
- export async function flushAgentDetection(): Promise<void> {
238
- if (detectionPromise !== null) {
239
- await detectionPromise;
416
+ function detectParentAgent(): string | undefined {
417
+ debugLog('Starting detection, PID:', process.pid, 'PPID:', process.ppid);
418
+ debugLog('stdin.isTTY:', process.stdin.isTTY);
419
+
420
+ // Dump relevant env vars for debugging agent detection
421
+ if (DEBUG) {
422
+ const relevantEnvVars = Object.entries(process.env)
423
+ .filter(
424
+ ([key]) =>
425
+ key.startsWith('WARP') ||
426
+ key.startsWith('TERM') ||
427
+ key.startsWith('AGENTUITY') ||
428
+ key === 'SHELL' ||
429
+ key === 'LC_TERMINAL' ||
430
+ key === 'ITERM_SESSION_ID' ||
431
+ key === 'VSCODE_INJECTION' ||
432
+ key === 'CURSOR_TRACE_ID'
433
+ )
434
+ .map(([key, value]) => `${key}=${value}`)
435
+ .join(', ');
436
+ debugLog('Relevant env vars:', relevantEnvVars || '(none)');
437
+ debugLog('WARP_AGENT_MODE:', process.env.WARP_AGENT_MODE ?? '(not set)');
438
+ }
439
+
440
+ // Short-circuit: if AGENTUITY_AGENT_MODE is set, use it directly
441
+ // Use "false", "0", or "none" to explicitly disable detection
442
+ const agentMode = process.env.AGENTUITY_AGENT_MODE;
443
+ if (agentMode) {
444
+ if (agentMode === 'false' || agentMode === '0' || agentMode === 'none') {
445
+ debugLog('AGENTUITY_AGENT_MODE explicitly disabled:', agentMode);
446
+ return undefined;
447
+ }
448
+ debugLog('Using AGENTUITY_AGENT_MODE:', agentMode);
449
+ return agentMode;
450
+ }
451
+
452
+ // Warp terminal: detect via WARP_AGENT_MODE env var (set by Warp AI)
453
+ // Note: Warp doesn't currently set this, but will in the future
454
+ if (process.env.WARP_AGENT_MODE === 'true') {
455
+ debugLog('Detected Warp AI via WARP_AGENT_MODE=true');
456
+ return 'warp';
457
+ }
458
+
459
+ // If this is an interactive session (TTY), assume human is running the command
460
+ // Note: Warp AI also runs with TTY=true, so Warp users should set
461
+ // AGENTUITY_AGENT_MODE=warp until Warp sets WARP_AGENT_MODE=true
462
+ if (isInteractiveSession()) {
463
+ debugLog('Interactive session (TTY), skipping detection');
464
+ return undefined;
465
+ }
466
+
467
+ // Unsupported on Windows (for now)
468
+ if (process.platform === 'win32') {
469
+ debugLog('Windows not supported');
470
+ return undefined;
240
471
  }
472
+
473
+ const { getProcessPath, getParentPid, getProcessCmdline } = getFFI();
474
+
475
+ // Guard for no parent process (e.g., PID 1 in containers)
476
+ const ppid = process.ppid;
477
+ if (!ppid || ppid <= 1) {
478
+ debugLog('No parent process (ppid:', ppid, ')');
479
+ return undefined;
480
+ }
481
+
482
+ // Walk up the process tree looking for known agents
483
+ const maxDepth = 10;
484
+ let currentPid = ppid;
485
+
486
+ for (let depth = 0; depth < maxDepth && currentPid > 1; depth++) {
487
+ // Check the executable path for agent match
488
+ const path = getProcessPath(currentPid);
489
+ debugLog(`[${depth}] PID ${currentPid} path:`, path);
490
+
491
+ if (path) {
492
+ const agent = matchAgentPath(path);
493
+ if (agent) {
494
+ debugLog(`[${depth}] Matched agent from path:`, agent);
495
+ return agent;
496
+ }
497
+ }
498
+
499
+ // Check the command line (for agents running as node/bun scripts)
500
+ const cmdline = getProcessCmdline(currentPid);
501
+ debugLog(
502
+ `[${depth}] PID ${currentPid} cmdline:`,
503
+ cmdline?.substring(0, 200) + (cmdline && cmdline.length > 200 ? '...' : '')
504
+ );
505
+
506
+ if (cmdline) {
507
+ const agent = matchAgentCmdline(cmdline);
508
+ if (agent) {
509
+ debugLog(`[${depth}] Matched agent from cmdline:`, agent);
510
+ return agent;
511
+ }
512
+ }
513
+
514
+ // Move up
515
+ const parentPid = getParentPid(currentPid);
516
+ debugLog(`[${depth}] Parent of ${currentPid}:`, parentPid);
517
+ if (!parentPid) break;
518
+ currentPid = parentPid;
519
+ }
520
+
521
+ debugLog('No agent found');
522
+ return undefined;
241
523
  }
242
524
 
243
525
  /**
244
- * Check if the CLI is being executed from a known coding agent.
245
- * Returns the agent name if detected, undefined otherwise.
526
+ * Get the executing agent if the CLI is being run from a known coding agent.
527
+ * Returns the agent ID if detected, undefined otherwise.
246
528
  *
247
- * This function returns immediately if detection has already completed,
248
- * otherwise it awaits the detection promise started by startAgentDetection().
529
+ * This function runs synchronously using FFI for fast process path resolution.
530
+ * Results are cached after the first call.
249
531
  *
250
532
  * @example
251
533
  * ```typescript
252
- * const agent = await isExecutingFromAgent();
534
+ * const agent = getExecutingAgent();
253
535
  * if (agent) {
254
536
  * logger.debug(`Running from agent: ${agent}`);
255
537
  * }
256
538
  * ```
257
539
  */
258
- export async function isExecutingFromAgent(): Promise<string | undefined> {
259
- // Return cached result if detection has completed
540
+ export function getExecutingAgent(): string | undefined {
541
+ // Return cached result if already detected
260
542
  if (cachedResult !== null) {
261
- return cachedResult;
543
+ return cachedResult ?? undefined;
262
544
  }
263
545
 
264
- // If detection hasn't started yet, start it now
265
- if (detectionPromise === null) {
266
- startAgentDetection();
267
- }
546
+ // Run detection and cache
547
+ cachedResult = detectParentAgent();
548
+ return cachedResult;
549
+ }
268
550
 
269
- // Wait for detection to complete
270
- return detectionPromise!;
551
+ /**
552
+ * Get environment variables to pass to subprocesses for agent detection.
553
+ * This allows child processes to skip re-detection by using the cached result.
554
+ *
555
+ * @example
556
+ * ```typescript
557
+ * const proc = Bun.spawn(['bun', 'run', 'dev'], {
558
+ * env: { ...process.env, ...getAgentEnv() },
559
+ * });
560
+ * ```
561
+ */
562
+ export function getAgentEnv(): Record<string, string> {
563
+ const agent = getExecutingAgent();
564
+ if (agent) {
565
+ return { AGENTUITY_AGENT_MODE: agent };
566
+ }
567
+ return {};
271
568
  }