@nemus-cli/nemus 0.2.1

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 (320) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/assets/README.md +53 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/banner.svg +66 -0
  7. package/assets/favicon-32.png +0 -0
  8. package/assets/favicon-64.png +0 -0
  9. package/assets/icon-256.png +0 -0
  10. package/assets/icon.svg +23 -0
  11. package/assets/logo-256.png +0 -0
  12. package/assets/logo-512.png +0 -0
  13. package/assets/logo-wordmark.svg +35 -0
  14. package/assets/logo.svg +32 -0
  15. package/bin/workspace.js +130 -0
  16. package/dist/cli/ai-prompt.js +421 -0
  17. package/dist/cli/dashboard/AgentList.js +61 -0
  18. package/dist/cli/dashboard/DashboardSidebar.js +280 -0
  19. package/dist/cli/dashboard/HelpFooter.js +73 -0
  20. package/dist/cli/dashboard/StatusBadge.js +50 -0
  21. package/dist/cli/dashboard/index.js +11 -0
  22. package/dist/cli/dashboard/types.js +2 -0
  23. package/dist/cli/tui.js +48 -0
  24. package/dist/commands/ai-prompt.js +22 -0
  25. package/dist/commands/analyze-deps.js +138 -0
  26. package/dist/commands/archive.js +113 -0
  27. package/dist/commands/branch/create.js +119 -0
  28. package/dist/commands/branch/index.js +104 -0
  29. package/dist/commands/branch/merge.js +90 -0
  30. package/dist/commands/branch/rebase.js +84 -0
  31. package/dist/commands/branch/switch.js +165 -0
  32. package/dist/commands/cache/index.js +80 -0
  33. package/dist/commands/cache/manager.js +198 -0
  34. package/dist/commands/cleanup.js +153 -0
  35. package/dist/commands/configure-claude.js +55 -0
  36. package/dist/commands/configure.js +231 -0
  37. package/dist/commands/create.js +204 -0
  38. package/dist/commands/dashboard/hook-handler.js +162 -0
  39. package/dist/commands/dashboard/index.js +258 -0
  40. package/dist/commands/dashboard/launcher.js +40 -0
  41. package/dist/commands/dashboard/session-picker.js +106 -0
  42. package/dist/commands/dashboard/sidebar.js +39 -0
  43. package/dist/commands/dashboard/workspace-picker.js +95 -0
  44. package/dist/commands/delete.js +194 -0
  45. package/dist/commands/deprecated-aliases.js +44 -0
  46. package/dist/commands/diff.js +129 -0
  47. package/dist/commands/doctor.js +141 -0
  48. package/dist/commands/generate-docs.js +77 -0
  49. package/dist/commands/ghq-status.js +72 -0
  50. package/dist/commands/go.js +154 -0
  51. package/dist/commands/history.js +124 -0
  52. package/dist/commands/list.js +191 -0
  53. package/dist/commands/mcp/index.js +69 -0
  54. package/dist/commands/migrate.js +296 -0
  55. package/dist/commands/remove-repo.js +217 -0
  56. package/dist/commands/report-bug.js +112 -0
  57. package/dist/commands/run.js +147 -0
  58. package/dist/commands/save-context.js +171 -0
  59. package/dist/commands/sessions.js +129 -0
  60. package/dist/commands/status.js +138 -0
  61. package/dist/commands/suite/create.js +212 -0
  62. package/dist/commands/suite/delete.js +64 -0
  63. package/dist/commands/suite/export.js +126 -0
  64. package/dist/commands/suite/import.js +128 -0
  65. package/dist/commands/suite/index.js +94 -0
  66. package/dist/commands/suite/list.js +49 -0
  67. package/dist/commands/suite/use.js +227 -0
  68. package/dist/commands/sync.js +117 -0
  69. package/dist/commands/update.js +203 -0
  70. package/dist/mcp/install.js +550 -0
  71. package/dist/mcp/server.js +398 -0
  72. package/dist/mcp/tools.js +922 -0
  73. package/dist/pi-extensions/permission-sync.ts.template +41 -0
  74. package/dist/pi-extensions/permissions-gate.ts.template +69 -0
  75. package/dist/pi-extensions/pr-fix-trigger.ts.template +124 -0
  76. package/dist/pi-extensions/review-reminder.ts.template +58 -0
  77. package/dist/pi-extensions/workspace-input-status.ts.template +919 -0
  78. package/dist/program.js +136 -0
  79. package/dist/scripts/workspace-table.py +394 -0
  80. package/dist/types/dashboard.js +11 -0
  81. package/dist/types/index.js +2 -0
  82. package/dist/utils/agent-config.js +263 -0
  83. package/dist/utils/agent-state.js +178 -0
  84. package/dist/utils/banner.js +24 -0
  85. package/dist/utils/branch-operations.js +232 -0
  86. package/dist/utils/bug-report.js +355 -0
  87. package/dist/utils/cache.js +168 -0
  88. package/dist/utils/claude-integration.js +617 -0
  89. package/dist/utils/claude-sessions.js +260 -0
  90. package/dist/utils/cleanup-operations.js +120 -0
  91. package/dist/utils/colors.js +26 -0
  92. package/dist/utils/command-helpers.js +44 -0
  93. package/dist/utils/config.js +150 -0
  94. package/dist/utils/context-file.js +33 -0
  95. package/dist/utils/dashboard-hooks.js +172 -0
  96. package/dist/utils/dependency-analyzer.js +234 -0
  97. package/dist/utils/diff-operations.js +80 -0
  98. package/dist/utils/doc-generator.js +184 -0
  99. package/dist/utils/ghq-integration.js +332 -0
  100. package/dist/utils/git-operations.js +237 -0
  101. package/dist/utils/git-status.js +187 -0
  102. package/dist/utils/github.js +68 -0
  103. package/dist/utils/health-checks.js +345 -0
  104. package/dist/utils/history.js +150 -0
  105. package/dist/utils/hooks.js +100 -0
  106. package/dist/utils/logger.js +40 -0
  107. package/dist/utils/permission-sync.js +681 -0
  108. package/dist/utils/pi-extensions.js +224 -0
  109. package/dist/utils/progress.js +92 -0
  110. package/dist/utils/prompts.js +279 -0
  111. package/dist/utils/repo-resolver.js +260 -0
  112. package/dist/utils/retry.js +65 -0
  113. package/dist/utils/run-operations.js +30 -0
  114. package/dist/utils/suite.js +159 -0
  115. package/dist/utils/sync-operations.js +107 -0
  116. package/dist/utils/tmux-dashboard.js +184 -0
  117. package/dist/utils/validation.js +153 -0
  118. package/dist/utils/version-check.js +121 -0
  119. package/dist/utils/workspace-meta.js +185 -0
  120. package/install-shell-integration.sh +532 -0
  121. package/install.sh +401 -0
  122. package/package.json +90 -0
  123. package/remove-shell-block.awk +44 -0
  124. package/scripts/postinstall.js +85 -0
  125. package/skills/analyze-deps.md +23 -0
  126. package/skills/archive-workspace.md +26 -0
  127. package/skills/branch-create.md +18 -0
  128. package/skills/create-workspace.md +39 -0
  129. package/skills/delete-workspace.md +37 -0
  130. package/skills/list-org-repos.md +26 -0
  131. package/skills/list-suites.md +24 -0
  132. package/skills/list-workspaces.md +27 -0
  133. package/skills/refresh-workspace-docs.md +89 -0
  134. package/skills/remove-repo.md +25 -0
  135. package/skills/run-command.md +23 -0
  136. package/skills/save-context.md +68 -0
  137. package/skills/search-repos.md +31 -0
  138. package/skills/snapshot-list.md +18 -0
  139. package/skills/snapshot-save.md +12 -0
  140. package/skills/switch-branch.md +18 -0
  141. package/skills/update-cache.md +21 -0
  142. package/skills/update-workspace.md +66 -0
  143. package/skills/workspace-cleanup.md +18 -0
  144. package/skills/workspace-diff.md +23 -0
  145. package/skills/workspace-doctor.md +23 -0
  146. package/skills/workspace-info.md +45 -0
  147. package/skills/workspace-manager/SKILL.md +201 -0
  148. package/skills/workspace-manager/references/ai-prompt.md +44 -0
  149. package/skills/workspace-manager/references/analyze-deps.md +45 -0
  150. package/skills/workspace-manager/references/archive-workspace.md +38 -0
  151. package/skills/workspace-manager/references/branch-create.md +31 -0
  152. package/skills/workspace-manager/references/branch-merge.md +22 -0
  153. package/skills/workspace-manager/references/branch-rebase.md +22 -0
  154. package/skills/workspace-manager/references/cache-clear.md +18 -0
  155. package/skills/workspace-manager/references/cache-info.md +22 -0
  156. package/skills/workspace-manager/references/configure-claude.md +31 -0
  157. package/skills/workspace-manager/references/configure.md +35 -0
  158. package/skills/workspace-manager/references/create-workspace.md +45 -0
  159. package/skills/workspace-manager/references/dashboard.md +61 -0
  160. package/skills/workspace-manager/references/delete-workspace.md +33 -0
  161. package/skills/workspace-manager/references/generate-docs.md +36 -0
  162. package/skills/workspace-manager/references/ghq-status.md +28 -0
  163. package/skills/workspace-manager/references/go.md +33 -0
  164. package/skills/workspace-manager/references/history.md +41 -0
  165. package/skills/workspace-manager/references/list-org-repos.md +26 -0
  166. package/skills/workspace-manager/references/list-suites.md +21 -0
  167. package/skills/workspace-manager/references/list-workspaces.md +27 -0
  168. package/skills/workspace-manager/references/mcp.md +49 -0
  169. package/skills/workspace-manager/references/remove-repo.md +26 -0
  170. package/skills/workspace-manager/references/run-command.md +44 -0
  171. package/skills/workspace-manager/references/search-repos.md +24 -0
  172. package/skills/workspace-manager/references/sessions.md +27 -0
  173. package/skills/workspace-manager/references/suite-create.md +22 -0
  174. package/skills/workspace-manager/references/suite-delete.md +21 -0
  175. package/skills/workspace-manager/references/suite-export.md +19 -0
  176. package/skills/workspace-manager/references/suite-import.md +19 -0
  177. package/skills/workspace-manager/references/suite-use.md +27 -0
  178. package/skills/workspace-manager/references/switch-branch.md +25 -0
  179. package/skills/workspace-manager/references/update-cache.md +22 -0
  180. package/skills/workspace-manager/references/update-workspace.md +50 -0
  181. package/skills/workspace-manager/references/workspace-cleanup.md +33 -0
  182. package/skills/workspace-manager/references/workspace-diff.md +27 -0
  183. package/skills/workspace-manager/references/workspace-doctor.md +27 -0
  184. package/skills/workspace-manager/references/workspace-info.md +33 -0
  185. package/skills/workspace-manager/references/workspace-status.md +43 -0
  186. package/skills/workspace-manager/references/workspace-sync.md +26 -0
  187. package/skills/workspace-status.md +26 -0
  188. package/skills/workspace-sync.md +23 -0
  189. package/src/cli/ai-prompt.test.ts +667 -0
  190. package/src/cli/ai-prompt.ts +421 -0
  191. package/src/cli/dashboard/AgentList.tsx +44 -0
  192. package/src/cli/dashboard/DashboardSidebar.tsx +291 -0
  193. package/src/cli/dashboard/HelpFooter.tsx +22 -0
  194. package/src/cli/dashboard/StatusBadge.tsx +23 -0
  195. package/src/cli/dashboard/index.ts +5 -0
  196. package/src/cli/dashboard/types.ts +16 -0
  197. package/src/cli/tui.tsx +51 -0
  198. package/src/cli/workspace-cli.test.ts +30 -0
  199. package/src/commands/ai-prompt.ts +21 -0
  200. package/src/commands/analyze-deps.ts +119 -0
  201. package/src/commands/archive.ts +114 -0
  202. package/src/commands/branch/create.ts +94 -0
  203. package/src/commands/branch/index.ts +74 -0
  204. package/src/commands/branch/merge.ts +61 -0
  205. package/src/commands/branch/rebase.ts +54 -0
  206. package/src/commands/branch/switch.ts +150 -0
  207. package/src/commands/cache/index.ts +51 -0
  208. package/src/commands/cache/manager.ts +168 -0
  209. package/src/commands/cleanup.ts +132 -0
  210. package/src/commands/configure-claude.ts +55 -0
  211. package/src/commands/configure.ts +204 -0
  212. package/src/commands/create.ts +196 -0
  213. package/src/commands/dashboard/hook-handler.ts +138 -0
  214. package/src/commands/dashboard/index.ts +248 -0
  215. package/src/commands/dashboard/launcher.ts +42 -0
  216. package/src/commands/dashboard/session-picker.ts +73 -0
  217. package/src/commands/dashboard/sidebar.js +39 -0
  218. package/src/commands/dashboard/workspace-picker.ts +62 -0
  219. package/src/commands/delete.test.ts +250 -0
  220. package/src/commands/delete.ts +171 -0
  221. package/src/commands/deprecated-aliases.ts +55 -0
  222. package/src/commands/diff.ts +106 -0
  223. package/src/commands/doctor.ts +110 -0
  224. package/src/commands/generate-docs.ts +45 -0
  225. package/src/commands/ghq-status.ts +70 -0
  226. package/src/commands/go.ts +125 -0
  227. package/src/commands/history.ts +136 -0
  228. package/src/commands/list.test.ts +219 -0
  229. package/src/commands/list.ts +176 -0
  230. package/src/commands/mcp/index.ts +39 -0
  231. package/src/commands/migrate.ts +277 -0
  232. package/src/commands/remove-repo.ts +194 -0
  233. package/src/commands/report-bug.ts +88 -0
  234. package/src/commands/run.ts +125 -0
  235. package/src/commands/save-context.test.ts +113 -0
  236. package/src/commands/save-context.ts +141 -0
  237. package/src/commands/sessions.ts +99 -0
  238. package/src/commands/status.ts +111 -0
  239. package/src/commands/suite/create.ts +240 -0
  240. package/src/commands/suite/delete.ts +65 -0
  241. package/src/commands/suite/export.ts +97 -0
  242. package/src/commands/suite/import.ts +100 -0
  243. package/src/commands/suite/index.ts +66 -0
  244. package/src/commands/suite/list.ts +54 -0
  245. package/src/commands/suite/use.ts +232 -0
  246. package/src/commands/sync.ts +97 -0
  247. package/src/commands/update.ts +197 -0
  248. package/src/mcp/install.ts +529 -0
  249. package/src/mcp/server.ts +579 -0
  250. package/src/mcp/tools.test.ts +545 -0
  251. package/src/mcp/tools.ts +1019 -0
  252. package/src/mcp/update-workspace.test.ts +220 -0
  253. package/src/pi-extensions/permission-sync.ts.template +41 -0
  254. package/src/pi-extensions/permissions-gate.ts.template +69 -0
  255. package/src/pi-extensions/pr-fix-trigger.ts.template +124 -0
  256. package/src/pi-extensions/review-reminder.ts.template +58 -0
  257. package/src/pi-extensions/workspace-input-status.ts.template +919 -0
  258. package/src/program.ts +112 -0
  259. package/src/scripts/workspace-table.py +394 -0
  260. package/src/types/dashboard.ts +29 -0
  261. package/src/types/index.ts +109 -0
  262. package/src/types/ink.d.ts +171 -0
  263. package/src/utils/agent-config.test.ts +303 -0
  264. package/src/utils/agent-config.ts +264 -0
  265. package/src/utils/agent-state.test.ts +166 -0
  266. package/src/utils/agent-state.ts +135 -0
  267. package/src/utils/banner.ts +24 -0
  268. package/src/utils/branch-operations.ts +237 -0
  269. package/src/utils/bug-report.test.ts +174 -0
  270. package/src/utils/bug-report.ts +355 -0
  271. package/src/utils/cache.ts +150 -0
  272. package/src/utils/claude-integration.test.ts +381 -0
  273. package/src/utils/claude-integration.ts +649 -0
  274. package/src/utils/claude-sessions.test.ts +262 -0
  275. package/src/utils/claude-sessions.ts +251 -0
  276. package/src/utils/cleanup-operations.ts +94 -0
  277. package/src/utils/colors.ts +25 -0
  278. package/src/utils/command-helpers.ts +46 -0
  279. package/src/utils/config.test.ts +203 -0
  280. package/src/utils/config.ts +133 -0
  281. package/src/utils/context-file.ts +36 -0
  282. package/src/utils/dashboard-hooks.test.ts +168 -0
  283. package/src/utils/dashboard-hooks.ts +163 -0
  284. package/src/utils/dependency-analyzer.ts +243 -0
  285. package/src/utils/diff-operations.ts +59 -0
  286. package/src/utils/doc-generator.ts +187 -0
  287. package/src/utils/ghq-integration.test.ts +195 -0
  288. package/src/utils/ghq-integration.ts +317 -0
  289. package/src/utils/git-operations.test.ts +265 -0
  290. package/src/utils/git-operations.ts +233 -0
  291. package/src/utils/git-status.ts +171 -0
  292. package/src/utils/github.ts +74 -0
  293. package/src/utils/health-checks.ts +359 -0
  294. package/src/utils/history.ts +140 -0
  295. package/src/utils/hooks.ts +92 -0
  296. package/src/utils/logger.ts +37 -0
  297. package/src/utils/permission-sync.test.ts +593 -0
  298. package/src/utils/permission-sync.ts +720 -0
  299. package/src/utils/pi-extensions.test.ts +182 -0
  300. package/src/utils/pi-extensions.ts +226 -0
  301. package/src/utils/progress.ts +73 -0
  302. package/src/utils/prompts.test.ts +169 -0
  303. package/src/utils/prompts.ts +296 -0
  304. package/src/utils/repo-resolver.test.ts +313 -0
  305. package/src/utils/repo-resolver.ts +294 -0
  306. package/src/utils/retry.test.ts +124 -0
  307. package/src/utils/retry.ts +85 -0
  308. package/src/utils/run-operations.ts +38 -0
  309. package/src/utils/suite.test.ts +256 -0
  310. package/src/utils/suite.ts +144 -0
  311. package/src/utils/sync-operations.ts +87 -0
  312. package/src/utils/tmux-dashboard.ts +193 -0
  313. package/src/utils/validation.test.ts +139 -0
  314. package/src/utils/validation.ts +128 -0
  315. package/src/utils/version-check.test.ts +108 -0
  316. package/src/utils/version-check.ts +96 -0
  317. package/src/utils/workspace-meta.ts +173 -0
  318. package/sync-permissions.sh +137 -0
  319. package/tsconfig.json +20 -0
  320. package/uninstall.sh +162 -0
@@ -0,0 +1,421 @@
1
+ import { spawn, exec, execFileSync } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ import { WORKSPACES_DIR, getUserConfig } from '../utils/config';
7
+ import { logInfo, logError, logStep } from '../utils/logger';
8
+ import { colorize } from '../utils/colors';
9
+ import { getPrimaryAgent } from '../utils/agent-config';
10
+ import { sanitizeWorkspaceName, checkWorkspaceExists, resolveWorkspaceNameConflict } from '../utils/validation';
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ export const AI_PROMPT_FILE = path.join(os.homedir(), '.workspace-ai-prompt');
15
+
16
+ const EXTRACT_SCHEMA = JSON.stringify({
17
+ type: 'object',
18
+ properties: {
19
+ workspaceName: { type: 'string', description: 'Workspace name (kebab-case, e.g. "payments-team")' },
20
+ repos: {
21
+ type: 'array',
22
+ items: { type: 'string' },
23
+ description: 'Repository names to clone (e.g. ["api", "web"]). Empty array when the repos cannot be known without investigation first.',
24
+ },
25
+ investigateFirst: {
26
+ type: 'boolean',
27
+ description: 'True when the user does NOT name concrete repos and instead wants the agent to investigate first (e.g. "search the logs and figure out which repos") and add the relevant repos itself. When true, repos must be an empty array.',
28
+ },
29
+ remainingIntent: {
30
+ type: 'string',
31
+ description: 'What the user wants done AFTER workspace creation (empty string if nothing beyond creating the workspace)',
32
+ },
33
+ },
34
+ required: ['workspaceName', 'repos', 'investigateFirst', 'remainingIntent'],
35
+ });
36
+
37
+ /**
38
+ * Build the extraction prompt for parsing workspace details from natural language.
39
+ */
40
+ export function buildExtractionPrompt(userPrompt: string): string {
41
+ return `Extract the workspace name and repository names from this request. If the user didn't specify a workspace name, generate a short descriptive kebab-case name. Repository names should be exact GitHub repo names (without org prefix). If the user's request includes things to do AFTER creating the workspace (like fixing code, creating branches, etc.), put that in remainingIntent.
42
+
43
+ IMPORTANT — investigate-first requests: if the user does NOT name concrete repositories and instead wants the agent to figure out which repos are relevant by investigating (e.g. "search the logs for X and open the repos involved", "look at this trace and pull the services' code", "find which repo owns this error"), then set "investigateFirst": true and "repos": [] (empty), and put the full investigation task in remainingIntent. Only list repos explicitly when the user actually names them or they're unambiguous.
44
+
45
+ Respond with ONLY a raw JSON object (no markdown, no backticks, no explanation). The JSON must have exactly these fields:
46
+ {"workspaceName": "short-kebab-name", "repos": ["repo-name-1", "repo-name-2"], "investigateFirst": false, "remainingIntent": "what to do after creation or empty string"}
47
+
48
+ User request: ${userPrompt}`;
49
+ }
50
+
51
+ /**
52
+ * Build the in-session task for an investigate-first workspace: the workspace
53
+ * starts empty, and the agent must discover which repos are relevant and add
54
+ * them itself before reading any code.
55
+ *
56
+ * Deliberately GENERIC (works for log/trace searches, GitHub search, stack
57
+ * traces, etc.). The discovery step adapts to the active agent: MCP tools when
58
+ * available, otherwise the always-present `gh` CLI (honouring a configured org).
59
+ */
60
+ export function buildInvestigationPreamble(
61
+ workspaceName: string,
62
+ task: string,
63
+ opts: { useMcpTools?: boolean; githubOrg?: string } = {}
64
+ ): string {
65
+ const org = (opts.githubOrg || '').trim();
66
+ const ownerFlag = org ? ` --owner ${org}` : '';
67
+ const repoListCmd = org ? `gh repo list ${org} --limit 200` : `gh repo list --limit 200`;
68
+ // Discovery differs by agent capability: "search-repos"/"list-org-repos" exist
69
+ // ONLY as Nemus MCP tools (there are no `nemus search-repos` CLI commands).
70
+ // When the active agent can't use MCP (e.g. Pi) or MCP is disabled, point it
71
+ // at the always-available `gh` CLI so we never name tools it doesn't have.
72
+ const discoveryStep = opts.useMcpTools
73
+ ? `3. Map each service to its GitHub repository using the Nemus MCP tools: try the "search-repos" tool (fuzzy) and "list-org-repos" to confirm the real repo name. Do not guess — verify the repo exists.`
74
+ : `3. Map each service to its GitHub repository using the \`gh\` CLI: \`gh search repos <name>${ownerFlag} --limit 20\` (or \`${repoListCmd}\`) to find and confirm the real repo name. Do not guess — verify the repo exists.`;
75
+ return [
76
+ `You are in a NEW, EMPTY workspace named "${workspaceName}" — it has no repositories cloned yet.`,
77
+ `Your job is to investigate first, then add the repositories you discover, and only then dig into the code.`,
78
+ ``,
79
+ `Follow this workflow:`,
80
+ `1. Investigate the request below using whatever sources it points to (e.g. logs/traces, a stack trace, GitHub search). Use your available tools/skills.`,
81
+ `2. From the investigation, identify the SERVICE / component names involved (e.g. from a trace's spans). Strip environment prefixes/suffixes like "production-", "-prod", "staging-" to get the base service name.`,
82
+ discoveryStep,
83
+ `4. Add the repositories you identified to THIS workspace by running: \`nemus update --workspace ${workspaceName} --repos <repo1,repo2,...> --yes\`. This clones them into the workspace.`,
84
+ `5. Briefly tell me which repos you added and why (which service/evidence pointed to each).`,
85
+ `6. THEN read the relevant code in those repos to complete the task.`,
86
+ ``,
87
+ `If the investigation points to no repos, say so instead of adding unrelated ones.`,
88
+ ``,
89
+ `--- Task ---`,
90
+ task,
91
+ ].join('\n');
92
+ }
93
+
94
+ /**
95
+ * Check if the primary agent CLI is available on the system.
96
+ */
97
+ export async function isPrimaryAgentAvailable(): Promise<boolean> {
98
+ const agent = getPrimaryAgent();
99
+ try {
100
+ await execAsync(`which ${agent.launchCommand}`);
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ // Backward compat alias
108
+ export const isClaudeAvailable = isPrimaryAgentAvailable;
109
+
110
+ interface ExtractedIntent {
111
+ workspaceName: string;
112
+ repos: string[];
113
+ remainingIntent: string;
114
+ /** True when repos aren't known yet and the agent should investigate then add them itself. */
115
+ investigateFirst?: boolean;
116
+ }
117
+
118
+ /**
119
+ * Use the configured AI agent to extract structured workspace details from a natural language prompt.
120
+ */
121
+ // Extraction is a tiny JSON task, but the underlying model call can stall
122
+ // intermittently — e.g. a slow/large default model (Opus) plus provider
123
+ // retries/backoff (Bedrock rate-limits), or a cold agent startup. 60s was
124
+ // too tight and produced flaky `spawnSync ETIMEDOUT`. 120s gives those
125
+ // transient cases room while still bounding a genuine hang.
126
+ const EXTRACTION_TIMEOUT_MS = 120_000;
127
+
128
+ /**
129
+ * Run an agent CLI for intent extraction, turning a timeout or non-zero exit
130
+ * into a clear, actionable error (instead of a cryptic `spawnSync ETIMEDOUT`).
131
+ *
132
+ * `fullArgs` is the preferred (lean) invocation. `fallbackArgs`, when provided,
133
+ * is a plainer invocation retried ONCE if the lean run fails fast (i.e. not a
134
+ * timeout or buffer overflow) — this covers older agent versions that reject
135
+ * newer speed flags like `--bare` / `--no-extensions` with an instant non-zero
136
+ * exit. Older versions then still work (just without the speedup).
137
+ */
138
+ function runExtraction(cmd: string, fullArgs: string[], fallbackArgs?: string[]): string {
139
+ const exec = (args: string[]) =>
140
+ execFileSync(cmd, args, {
141
+ encoding: 'utf-8',
142
+ timeout: EXTRACTION_TIMEOUT_MS,
143
+ maxBuffer: 10 * 1024 * 1024,
144
+ });
145
+
146
+ const classify = (err: any) => {
147
+ const message = String(err?.message ?? '');
148
+ return {
149
+ timedOut: err?.code === 'ETIMEDOUT' || /ETIMEDOUT/.test(message),
150
+ bufferOverflow: err?.code === 'ENOBUFS' || /maxBuffer/i.test(message),
151
+ stderr: typeof err?.stderr === 'string' ? err.stderr.trim() : '',
152
+ stdout: typeof err?.stdout === 'string' ? err.stdout.trim() : '',
153
+ message,
154
+ };
155
+ };
156
+
157
+ try {
158
+ return exec(fullArgs);
159
+ } catch (err: any) {
160
+ const c = classify(err);
161
+
162
+ // Fast failure (unsupported flag, etc.) on the lean invocation — retry
163
+ // once with the plainer args so older agent versions still work.
164
+ if (fallbackArgs && !c.timedOut && !c.bufferOverflow) {
165
+ try {
166
+ return exec(fallbackArgs);
167
+ } catch (err2: any) {
168
+ return throwExtractionError(cmd, classify(err2));
169
+ }
170
+ }
171
+ return throwExtractionError(cmd, c);
172
+ }
173
+ }
174
+
175
+ function throwExtractionError(
176
+ cmd: string,
177
+ c: { timedOut: boolean; bufferOverflow: boolean; stderr: string; stdout: string; message: string },
178
+ ): never {
179
+ const testCmd = cmd === 'opencode'
180
+ ? `opencode run "reply with OK"`
181
+ : `${cmd} -p "reply with OK"`;
182
+ // claude -p --output-format json emits error details on stdout, not stderr.
183
+ const detail = c.stderr || c.stdout;
184
+
185
+ if (c.timedOut) {
186
+ const secs = Math.round(EXTRACTION_TIMEOUT_MS / 1000);
187
+ let msg =
188
+ `${cmd} did not respond within ${secs}s while parsing your request.\n` +
189
+ ` This is usually a transient model/provider stall, not a workspace-manager bug.\n` +
190
+ ` • Verify the agent itself responds quickly: time ${testCmd}\n` +
191
+ ` • If it's slow, your default model may be heavy (e.g. Opus) or the provider\n` +
192
+ ` may be rate-limiting/refreshing credentials. Retry, or switch to a faster model.\n` +
193
+ ` • You can still create the workspace manually: nemus create --workspace <name> --repos <repos>`;
194
+ if (detail) msg += `\n agent output: ${detail.slice(0, 500)}`;
195
+ throw new Error(msg);
196
+ }
197
+
198
+ if (c.bufferOverflow) {
199
+ throw new Error(
200
+ `${cmd} produced more output than expected while parsing your request ` +
201
+ `(maxBuffer exceeded). Try a shorter request, or create the workspace ` +
202
+ `manually: nemus create --workspace <name> --repos <repos>` +
203
+ (detail ? `\n agent output: ${detail.slice(0, 500)}` : ''),
204
+ );
205
+ }
206
+
207
+ throw new Error(
208
+ `${cmd} failed while parsing your request.\n` +
209
+ ` • Verify the agent works: ${testCmd}\n` +
210
+ ` • Or create the workspace manually: nemus create --workspace <name> --repos <repos>` +
211
+ (detail ? `\n agent output: ${detail.slice(0, 800)}` : ` (${c.message || 'unknown error'})`),
212
+ );
213
+ }
214
+
215
+ export async function extractIntent(prompt: string): Promise<ExtractedIntent> {
216
+ const extractionPrompt = buildExtractionPrompt(prompt);
217
+ const agent = getPrimaryAgent();
218
+
219
+ let result: string;
220
+ if (agent.type === 'claude') {
221
+ // Preferred: structured + lean. --output-format/--json-schema give clean
222
+ // structured output; --bare/--strict-mcp-config/--disable-slash-commands
223
+ // skip the heavy ~/.claude (hooks, MCP servers, skills, plugins, context)
224
+ // so a bloated setup can't time out.
225
+ const preferred = [
226
+ '-p', extractionPrompt,
227
+ '--output-format', 'json',
228
+ '--json-schema', EXTRACT_SCHEMA,
229
+ '--bare',
230
+ '--strict-mcp-config',
231
+ '--disable-slash-commands',
232
+ ];
233
+ // Fallback: the most basic invocation that works on ANY claude version.
234
+ // If the user's claude rejects ANY of the flags above (it exits instantly
235
+ // non-zero), retry with plain `-p <prompt>` and parse the raw JSON text
236
+ // (same approach as pi). The extraction prompt already asks for raw JSON.
237
+ const fallback = ['-p', extractionPrompt];
238
+ result = runExtraction('claude', preferred, fallback);
239
+ } else if (agent.type === 'opencode') {
240
+ // OpenCode: use 'run' subcommand with positional message argument
241
+ result = runExtraction('opencode', ['run', extractionPrompt]);
242
+ } else {
243
+ // Pi: run as LEAN as possible. Intent extraction is a pure text->JSON
244
+ // transform that needs no extensions, skills, prompt-templates, context
245
+ // files, tools, or saved session. Loading the user's full environment
246
+ // (especially many MCP tools/skills or a large AGENTS.md context) is what
247
+ // made `pi -p` slow enough to blow past the timeout on bloated setups.
248
+ // These flags also avoid spawning MCP servers / credential-refresh
249
+ // extensions that can hang a non-interactive subprocess.
250
+ const piCore = ['-p', extractionPrompt];
251
+ const piLean = [
252
+ '--no-extensions',
253
+ '--no-skills',
254
+ '--no-prompt-templates',
255
+ '--no-context-files',
256
+ '--no-tools',
257
+ '--no-session',
258
+ ];
259
+ result = runExtraction('pi', [...piLean, ...piCore], piCore);
260
+ }
261
+
262
+ // Strip markdown code fences if present (Pi may wrap JSON in ```json...```)
263
+ let jsonStr = result.trim();
264
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
265
+ if (fenceMatch) {
266
+ jsonStr = fenceMatch[1].trim();
267
+ }
268
+
269
+ const parsed = JSON.parse(jsonStr);
270
+
271
+ // Handle different output formats:
272
+ // - Claude: { structured_output: {...} } or { result: "..." }
273
+ // - Pi: may return the object directly or wrap it
274
+ let intent: ExtractedIntent | undefined;
275
+ if (parsed.structured_output) {
276
+ intent = parsed.structured_output;
277
+ } else if (typeof parsed.result === 'string' && parsed.result) {
278
+ try { intent = JSON.parse(parsed.result); } catch { /* ignore */ }
279
+ } else if (typeof parsed.result === 'object' && parsed.result !== null) {
280
+ intent = parsed.result;
281
+ } else if (parsed.workspaceName || parsed.repos || parsed.remainingIntent !== undefined) {
282
+ // Pi may return the extracted object directly
283
+ intent = parsed;
284
+ }
285
+
286
+ if (!intent) {
287
+ throw new Error(`Could not extract intent from agent response. Parsed: ${JSON.stringify(parsed).slice(0, 200)}`);
288
+ }
289
+
290
+ // Coerce/validate field types so malformed model output can't crash the
291
+ // downstream .trim()/sanitize path (a stringly-typed workspaceName or a
292
+ // non-array repos would otherwise throw). Non-conforming values are dropped
293
+ // to their safe empty form, so run()'s existing "could not determine" guard
294
+ // handles them cleanly instead of an unhandled exception.
295
+ const normalized: ExtractedIntent = {
296
+ workspaceName: typeof intent.workspaceName === 'string' ? intent.workspaceName : '',
297
+ repos: Array.isArray(intent.repos)
298
+ ? intent.repos.filter((r): r is string => typeof r === 'string')
299
+ : [],
300
+ remainingIntent: typeof intent.remainingIntent === 'string' ? intent.remainingIntent : '',
301
+ investigateFirst: intent.investigateFirst === true,
302
+ };
303
+
304
+ return normalized;
305
+ }
306
+
307
+ /**
308
+ * Run the AI prompt command.
309
+ *
310
+ * 1. Extract workspace name + repos from the prompt (fast, no tool use).
311
+ * 2. Create the workspace using the CLI directly (shows progress).
312
+ * 3. Shell integration CDs to workspace and launches interactive Claude.
313
+ */
314
+ export async function run(prompt: string): Promise<number> {
315
+ const agentAvailable = await isClaudeAvailable();
316
+ if (!agentAvailable) {
317
+ const agent = getPrimaryAgent();
318
+ logError(`${agent.type} CLI not found. Install it first.`);
319
+ return 1;
320
+ }
321
+
322
+ const displayPrompt = prompt.length > 80 ? prompt.slice(0, 77) + '...' : prompt;
323
+ logInfo(`AI prompt: ${colorize(displayPrompt, 'cyan')}`);
324
+
325
+ fs.mkdirSync(WORKSPACES_DIR, { recursive: true });
326
+
327
+ // Step 1: Extract workspace details from natural language
328
+ logStep(1, 2, 'Understanding request...');
329
+ let intent: ExtractedIntent;
330
+ try {
331
+ intent = await extractIntent(prompt);
332
+ } catch (err) {
333
+ logError('Failed to parse workspace request');
334
+ if (err instanceof Error) logError(err.message);
335
+ return 1;
336
+ }
337
+
338
+ const repos = Array.isArray(intent.repos) ? intent.repos : [];
339
+ // Investigate-first: no concrete repos yet — create an empty workspace and let
340
+ // the in-session agent discover + add the repos. This only applies when the
341
+ // model produced NO repos: if it already identified some, we must clone them
342
+ // (never silently discard them for an empty workspace), even if it also set
343
+ // the investigateFirst flag. An empty repo list with a real task is treated
344
+ // as investigate-mode too, even if the model forgot the flag.
345
+ const investigateFirst = repos.length === 0
346
+ && (intent.investigateFirst === true || !!(intent.remainingIntent || '').trim());
347
+
348
+ if (!intent.workspaceName || (repos.length === 0 && !investigateFirst)) {
349
+ logError('Could not determine workspace name or repos from prompt');
350
+ logInfo(`Parsed: name=${intent.workspaceName || '(none)'}, repos=${repos.join(', ') || '(none)'}`);
351
+ return 1;
352
+ }
353
+
354
+ // In investigate-first mode the preamble tells the agent to run
355
+ // `nemus update --workspace <name> ...`, so that name MUST match the workspace
356
+ // `nemus create` actually produces. Pre-resolve it here (same sanitize +
357
+ // conflict resolution create uses) and pass the SAME resolved name to both
358
+ // the preamble and create, so a sanitized/de-duplicated name can't drift.
359
+ let workspaceName = intent.workspaceName;
360
+ if (investigateFirst) {
361
+ workspaceName = sanitizeWorkspaceName(intent.workspaceName);
362
+ if (await checkWorkspaceExists(workspaceName)) {
363
+ workspaceName = await resolveWorkspaceNameConflict(workspaceName, []);
364
+ }
365
+ }
366
+
367
+ if (investigateFirst) {
368
+ logInfo(`Workspace: ${colorize(workspaceName, 'cyan')} ${colorize('(investigate-first — no repos yet)', 'gray')}`);
369
+ } else {
370
+ logInfo(`Workspace: ${colorize(workspaceName, 'cyan')}, Repos: ${colorize(repos.join(', '), 'cyan')}`);
371
+ }
372
+
373
+ // Save the task for the follow-up interactive session. In investigate-first
374
+ // mode, wrap it with a workflow preamble so the agent discovers repos and
375
+ // adds them itself before reading code.
376
+ const baseTask = intent.remainingIntent || prompt;
377
+ // The "search-repos"/"list-org-repos" discovery helpers are MCP-only, so the
378
+ // preamble may reference them only when the active agent supports MCP AND MCP
379
+ // is enabled; otherwise it falls back to the `gh` CLI.
380
+ const cfg = getUserConfig();
381
+ const useMcpTools = getPrimaryAgent().supportsMcp && cfg.installMcp;
382
+ const remaining = investigateFirst
383
+ ? buildInvestigationPreamble(workspaceName, baseTask, { useMcpTools, githubOrg: cfg.githubOrg })
384
+ : baseTask;
385
+ try { fs.writeFileSync(AI_PROMPT_FILE, remaining, 'utf-8'); } catch {}
386
+
387
+ // Step 2: Create workspace using the CLI directly
388
+ logStep(2, 2, 'Creating workspace...');
389
+ const createArgs = investigateFirst
390
+ ? ['create', '--workspace', workspaceName, '--allow-empty', '--prompt', prompt, '--yes']
391
+ : ['create', '--workspace', workspaceName, '--repos', repos.join(','), '--prompt', prompt, '--yes'];
392
+ return new Promise<number>((resolve) => {
393
+ const child = spawn('nemus', createArgs, {
394
+ stdio: 'inherit',
395
+ });
396
+
397
+ child.on('error', (err) => {
398
+ logError(`Failed to create workspace: ${err.message}`);
399
+ resolve(1);
400
+ });
401
+
402
+ child.on('exit', (code) => {
403
+ resolve(code || 0);
404
+ });
405
+ });
406
+ }
407
+
408
+ export async function main(): Promise<void> {
409
+ const prompt = process.argv.slice(2).join(' ').trim();
410
+
411
+ if (!prompt) {
412
+ logError('No prompt provided');
413
+ console.log(`\n Usage: ${colorize('nemus -- <prompt>', 'green')}`);
414
+ console.log(` Example: ${colorize('nemus -- create a workspace for the payments team', 'gray')}\n`);
415
+ process.exit(1);
416
+ return;
417
+ }
418
+
419
+ const exitCode = await run(prompt);
420
+ process.exit(exitCode);
421
+ }
@@ -0,0 +1,44 @@
1
+ import * as React from 'react';
2
+ import { AgentState, InkComponents } from './types';
3
+ import { StatusBadge } from './StatusBadge';
4
+
5
+ interface AgentListProps extends InkComponents {
6
+ agents: AgentState[];
7
+ selectedIndex: number;
8
+ onSelect: (index: number) => void;
9
+ }
10
+
11
+ export const AgentList: React.FC<AgentListProps> = ({ agents, selectedIndex, onSelect, Box, Text }) => {
12
+ if (agents.length === 0) {
13
+ return (
14
+ <Box flexDirection="column">
15
+ <Text dimColor>No agents running</Text>
16
+ <Text dimColor>Press n to launch</Text>
17
+ </Box>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <Box flexDirection="column">
23
+ {agents.map((agent, i) => {
24
+ const isSelected = i === selectedIndex;
25
+ const ws = agent.workspace.length > 13
26
+ ? agent.workspace.slice(0, 12) + '…'
27
+ : agent.workspace;
28
+ const num = `${i + 1}`;
29
+
30
+ return (
31
+ <Box key={agent.sessionId} gap={1}>
32
+ {isSelected ? (
33
+ <Text backgroundColor="blue" color="white" bold>▸{num}</Text>
34
+ ) : (
35
+ <Text dimColor> {num}</Text>
36
+ )}
37
+ <Text color="cyan" bold={isSelected}>{ws.padEnd(13)}</Text>
38
+ <StatusBadge status={agent.status} Box={Box} Text={Text} />
39
+ </Box>
40
+ );
41
+ })}
42
+ </Box>
43
+ );
44
+ };