@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,264 @@
1
+ import * as os from 'os';
2
+ import * as path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { getUserConfig, AgentType, PrimaryAgentType, ConcreteAgentType } from './config';
5
+
6
+ // Re-export for convenience
7
+ export { AgentType, PrimaryAgentType, ConcreteAgentType } from './config';
8
+
9
+ export interface AgentPaths {
10
+ /** Agent identifier */
11
+ type: ConcreteAgentType;
12
+ /** Directory for skills (e.g. ~/.claude/skills or ~/.pi/agent/skills) */
13
+ skillsDir: string;
14
+ /** Settings file (e.g. ~/.claude/settings.json or ~/.pi/agent/settings.json) */
15
+ settingsFile: string;
16
+ /** Context file name generated in workspaces (e.g. CLAUDE.md or AGENTS.md) */
17
+ contextFileName: string;
18
+ /** CLI command to launch the agent */
19
+ launchCommand: string;
20
+ /** CLI command to resume a session */
21
+ resumeCommand: (sessionId: string) => string;
22
+ /** Whether this agent supports MCP (Model Context Protocol) */
23
+ supportsMcp: boolean;
24
+ /** Whether this agent supports hooks in settings.json (Claude Code format) */
25
+ supportsHooks: boolean;
26
+ /** Session projects directory (for session discovery) */
27
+ sessionProjectsDir: string;
28
+ }
29
+
30
+ const HOME = os.homedir();
31
+
32
+ /**
33
+ * Detection order for `auto` / `both` / `getPrimaryAgent`. Also the order agents
34
+ * appear in `getActiveAgents()`. Add a new agent's config below and to this list.
35
+ */
36
+ export const AGENT_ORDER: ConcreteAgentType[] = ['claude', 'pi', 'opencode', 'codex', 'gemini'];
37
+
38
+ const CLAUDE_PATHS: AgentPaths = {
39
+ type: 'claude',
40
+ skillsDir: path.join(HOME, '.claude', 'skills'),
41
+ settingsFile: path.join(HOME, '.claude', 'settings.json'),
42
+ // Must be exactly 'CLAUDE.md': Claude Code only auto-discovers CLAUDE.md /
43
+ // AGENTS.md up the cwd/ancestor chain. A hidden lowercase '.claude.md' is
44
+ // invisible to its project-memory loader, so the workspace context (and its
45
+ // 'Saved Context' pointer) would never surface automatically (issue #186).
46
+ contextFileName: 'CLAUDE.md',
47
+ launchCommand: 'claude',
48
+ resumeCommand: (id: string) => `claude --resume ${id} --fork-session`,
49
+ supportsMcp: true,
50
+ supportsHooks: true,
51
+ sessionProjectsDir: path.join(HOME, '.claude', 'projects'),
52
+ };
53
+
54
+ const PI_PATHS: AgentPaths = {
55
+ type: 'pi',
56
+ skillsDir: path.join(HOME, '.pi', 'agent', 'skills'),
57
+ settingsFile: path.join(HOME, '.pi', 'agent', 'settings.json'),
58
+ contextFileName: 'AGENTS.md',
59
+ launchCommand: 'pi',
60
+ resumeCommand: (id: string) => `pi --session ${id} --fork`,
61
+ supportsMcp: false,
62
+ supportsHooks: false,
63
+ sessionProjectsDir: path.join(HOME, '.pi', 'agent', 'sessions'),
64
+ };
65
+
66
+ // OpenCode uses XDG paths: ~/.local/share/opencode for data, ~/.config/opencode for config
67
+ // It reads skills from ~/.claude/skills/ automatically (Claude-compatible)
68
+ // Project config lives in .opencode/ within the project directory
69
+ const OPENCODE_DATA_DIR = path.join(process.env.XDG_DATA_HOME || path.join(HOME, '.local', 'share'), 'opencode');
70
+ const OPENCODE_CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(HOME, '.config'), 'opencode');
71
+
72
+ const OPENCODE_PATHS: AgentPaths = {
73
+ type: 'opencode',
74
+ // OpenCode reads skills from ~/.claude/skills/ natively — no separate install needed
75
+ skillsDir: path.join(HOME, '.claude', 'skills'),
76
+ settingsFile: path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc'),
77
+ contextFileName: 'AGENTS.md',
78
+ launchCommand: 'opencode',
79
+ resumeCommand: (_id: string) => `opencode --continue`,
80
+ supportsMcp: true,
81
+ supportsHooks: false,
82
+ sessionProjectsDir: OPENCODE_DATA_DIR,
83
+ };
84
+
85
+ // Codex (OpenAI Codex CLI) — config under ~/.codex, reads AGENTS.md for context,
86
+ // supports MCP servers via ~/.codex/config.toml. No native skills or hooks.
87
+ const CODEX_HOME = process.env.CODEX_HOME || path.join(HOME, '.codex');
88
+ const CODEX_PATHS: AgentPaths = {
89
+ type: 'codex',
90
+ skillsDir: path.join(CODEX_HOME, 'skills'),
91
+ settingsFile: path.join(CODEX_HOME, 'config.toml'),
92
+ contextFileName: 'AGENTS.md',
93
+ launchCommand: 'codex',
94
+ resumeCommand: (id: string) => `codex resume ${id}`,
95
+ supportsMcp: true,
96
+ supportsHooks: false,
97
+ sessionProjectsDir: path.join(CODEX_HOME, 'sessions'),
98
+ };
99
+
100
+ // Gemini (Google Gemini CLI) — config under ~/.gemini, reads GEMINI.md for
101
+ // context, supports MCP servers via ~/.gemini/settings.json. No hooks.
102
+ const GEMINI_HOME = path.join(HOME, '.gemini');
103
+ const GEMINI_PATHS: AgentPaths = {
104
+ type: 'gemini',
105
+ skillsDir: path.join(GEMINI_HOME, 'skills'),
106
+ settingsFile: path.join(GEMINI_HOME, 'settings.json'),
107
+ contextFileName: 'GEMINI.md',
108
+ launchCommand: 'gemini',
109
+ resumeCommand: (_id: string) => `gemini`,
110
+ supportsMcp: true,
111
+ supportsHooks: false,
112
+ sessionProjectsDir: path.join(GEMINI_HOME, 'tmp'),
113
+ };
114
+
115
+ const AGENT_REGISTRY: Record<ConcreteAgentType, AgentPaths> = {
116
+ claude: CLAUDE_PATHS,
117
+ pi: PI_PATHS,
118
+ opencode: OPENCODE_PATHS,
119
+ codex: CODEX_PATHS,
120
+ gemini: GEMINI_PATHS,
121
+ };
122
+
123
+ /**
124
+ * Get AgentPaths for a specific agent type.
125
+ */
126
+ export function getAgentPaths(agent: ConcreteAgentType): AgentPaths {
127
+ return { ...AGENT_REGISTRY[agent] };
128
+ }
129
+
130
+ /**
131
+ * Resolve 'auto' to a concrete agent type by detecting installed CLIs.
132
+ * Cached after first call to avoid repeated shell exec.
133
+ */
134
+ let resolvedAutoAgent: ConcreteAgentType | 'both' | null = null;
135
+
136
+ function resolveAutoAgent(): ConcreteAgentType | 'both' {
137
+ if (resolvedAutoAgent) return resolvedAutoAgent;
138
+
139
+ const available = AGENT_ORDER.filter(a => isAgentCliAvailable(a));
140
+
141
+ // If multiple agents detected, use 'both' (meaning: all available agents).
142
+ if (available.length > 1) resolvedAutoAgent = 'both';
143
+ else if (available.length === 1) resolvedAutoAgent = available[0];
144
+ else resolvedAutoAgent = 'claude'; // default to claude even if not installed
145
+
146
+ return resolvedAutoAgent;
147
+ }
148
+
149
+ /**
150
+ * Get all active agent configurations based on user config.
151
+ * Resolves 'auto' by detecting installed CLIs. 'both' means every available agent.
152
+ */
153
+ export function getActiveAgents(): AgentPaths[] {
154
+ let { aiAgent } = getUserConfig();
155
+ if (aiAgent === 'auto') aiAgent = resolveAutoAgent();
156
+
157
+ if (aiAgent === 'both') {
158
+ const agents = AGENT_ORDER.filter(a => isAgentCliAvailable(a)).map(a => ({ ...AGENT_REGISTRY[a] }));
159
+ // Fallback to Claude if nothing detected (shouldn't happen in 'both' mode)
160
+ return agents.length > 0 ? agents : [{ ...CLAUDE_PATHS }];
161
+ }
162
+
163
+ return [{ ...AGENT_REGISTRY[aiAgent] }];
164
+ }
165
+
166
+ /**
167
+ * Get the preferred (primary) agent — the one used for launching, dashboards, etc.
168
+ * Uses the `primaryAgent` config. When 'auto', picks the first available CLI.
169
+ */
170
+ export function getPrimaryAgent(): AgentPaths {
171
+ const { primaryAgent } = getUserConfig();
172
+
173
+ if (primaryAgent !== 'auto') return { ...AGENT_REGISTRY[primaryAgent] };
174
+
175
+ // 'auto' — pick first available in detection order
176
+ const first = AGENT_ORDER.find(a => isAgentCliAvailable(a));
177
+ return { ...AGENT_REGISTRY[first ?? 'claude'] };
178
+ }
179
+
180
+ /**
181
+ * Check if a specific agent CLI is available on the system.
182
+ * Result is cached per agent to avoid repeated execSync overhead.
183
+ */
184
+ const cliAvailabilityCache = new Map<string, boolean>();
185
+
186
+ export function isAgentCliAvailable(agent: ConcreteAgentType): boolean {
187
+ if (cliAvailabilityCache.has(agent)) return cliAvailabilityCache.get(agent)!;
188
+
189
+ const command = `${AGENT_REGISTRY[agent].launchCommand} --version`;
190
+ let available: boolean;
191
+ try {
192
+ execSync(command, { stdio: 'pipe' });
193
+ available = true;
194
+ } catch {
195
+ available = false;
196
+ }
197
+
198
+ cliAvailabilityCache.set(agent, available);
199
+ return available;
200
+ }
201
+
202
+ /**
203
+ * Reset all cached values. For use in tests to ensure clean state.
204
+ * @internal
205
+ */
206
+ export function resetAgentConfigCache(): void {
207
+ resolvedAutoAgent = null;
208
+ cliAvailabilityCache.clear();
209
+ }
210
+
211
+ /**
212
+ * Get all skills directories that should be written to.
213
+ */
214
+ export function getSkillsTargetDirs(): string[] {
215
+ const dirs = getActiveAgents().map(a => a.skillsDir);
216
+ return [...new Set(dirs)];
217
+ }
218
+
219
+ /**
220
+ * Get all context file names that should be generated in workspaces.
221
+ * Deduplicates if multiple agents use the same file.
222
+ */
223
+ export function getContextFileNames(): string[] {
224
+ const names = getActiveAgents().map(a => a.contextFileName);
225
+ return [...new Set(names)];
226
+ }
227
+
228
+ /**
229
+ * Get all settings files where hooks should be installed.
230
+ * Only returns agents that support hooks.
231
+ */
232
+ export function getHookTargetFiles(): string[] {
233
+ return getActiveAgents()
234
+ .filter(a => a.supportsHooks)
235
+ .map(a => a.settingsFile);
236
+ }
237
+
238
+ /**
239
+ * Get all agents that support MCP.
240
+ */
241
+ export function getMcpAgents(): AgentPaths[] {
242
+ return getActiveAgents().filter(a => a.supportsMcp);
243
+ }
244
+
245
+ /**
246
+ * Get every context file name that any known agent can produce, regardless
247
+ * of which agents are currently configured. Used when scanning repos for
248
+ * existing context files (e.g. a Pi workspace may contain repos that were
249
+ * previously set up for Claude Code, and vice-versa).
250
+ *
251
+ * Also includes legacy '.claude.md' — the filename generated for Claude before
252
+ * issue #186 — so repos/workspaces that still contain one are still recognized.
253
+ */
254
+ export function getAllKnownContextFileNames(): string[] {
255
+ // Order matters: this is the per-repo scan PRIORITY (findContextFilesInDir
256
+ // embeds the first existing candidate per directory). AGENTS.md must come
257
+ // before CLAUDE.md — in dual-agent repos CLAUDE.md is commonly a thin
258
+ // "@AGENTS.md" shim, so AGENTS.md holds the real content. '.claude.md' is the
259
+ // legacy output (pre-#186), kept last so any lingering files are still
260
+ // recognized.
261
+ const priority = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.claude.md'];
262
+ const fromAgents = AGENT_ORDER.map(a => AGENT_REGISTRY[a].contextFileName);
263
+ return [...new Set([...priority, ...fromAgents])];
264
+ }
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+
6
+ // We need to mock the AGENTS_STATE_DIR to use a temp dir for tests
7
+ let tmpDir: string;
8
+
9
+ vi.mock('../types/dashboard', async () => {
10
+ const actual = await vi.importActual('../types/dashboard');
11
+ return {
12
+ ...actual as any,
13
+ };
14
+ });
15
+
16
+ beforeEach(() => {
17
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-state-test-'));
18
+ });
19
+
20
+ afterEach(() => {
21
+ fs.rmSync(tmpDir, { recursive: true, force: true });
22
+ vi.restoreAllMocks();
23
+ });
24
+
25
+ // Helper to write a state file directly
26
+ function writeStateFile(sessionId: string, state: any) {
27
+ fs.writeFileSync(
28
+ path.join(tmpDir, `${sessionId}.json`),
29
+ JSON.stringify(state, null, 2),
30
+ 'utf-8'
31
+ );
32
+ }
33
+
34
+ describe('agent-state', () => {
35
+ it('writeAgentState + readAgentState round-trip', async () => {
36
+ // Override the module's AGENTS_STATE_DIR
37
+ vi.doMock('./agent-state', async () => {
38
+ const mod = await vi.importActual('./agent-state') as any;
39
+ return {
40
+ ...mod,
41
+ AGENTS_STATE_DIR: tmpDir,
42
+ ensureStateDir: () => fs.mkdirSync(tmpDir, { recursive: true }),
43
+ };
44
+ });
45
+
46
+ // Direct file-based test instead of mocking
47
+ const state = {
48
+ sessionId: 'test-123',
49
+ workspace: 'my-workspace',
50
+ workspacePath: '/home/user/workspaces/my-workspace',
51
+ pid: process.pid, // Use current PID so it's alive
52
+ status: 'thinking' as const,
53
+ startedAt: new Date().toISOString(),
54
+ lastUpdatedAt: new Date().toISOString(),
55
+ };
56
+
57
+ const filePath = path.join(tmpDir, `${state.sessionId}.json`);
58
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf-8');
59
+
60
+ const content = fs.readFileSync(filePath, 'utf-8');
61
+ const read = JSON.parse(content);
62
+ expect(read.sessionId).toBe('test-123');
63
+ expect(read.workspace).toBe('my-workspace');
64
+ expect(read.status).toBe('thinking');
65
+ });
66
+
67
+ it('handles malformed JSON gracefully', () => {
68
+ fs.writeFileSync(path.join(tmpDir, 'bad.json'), 'not json{{{', 'utf-8');
69
+
70
+ // Reading should return null equivalent
71
+ try {
72
+ const content = fs.readFileSync(path.join(tmpDir, 'bad.json'), 'utf-8');
73
+ const parsed = JSON.parse(content);
74
+ // Should not get here
75
+ expect(parsed).toBeUndefined();
76
+ } catch {
77
+ // Expected — malformed JSON
78
+ expect(true).toBe(true);
79
+ }
80
+ });
81
+
82
+ it('state file structure matches AgentState interface', () => {
83
+ const state = {
84
+ sessionId: 'sess-abc',
85
+ workspace: 'test-ws',
86
+ workspacePath: '/tmp/ws',
87
+ pid: 12345,
88
+ status: 'waiting',
89
+ startedAt: '2026-03-24T00:00:00.000Z',
90
+ lastUpdatedAt: '2026-03-24T00:01:00.000Z',
91
+ tmuxPane: 'ws-dashboard:0.1',
92
+ };
93
+
94
+ writeStateFile('sess-abc', state);
95
+
96
+ const content = fs.readFileSync(path.join(tmpDir, 'sess-abc.json'), 'utf-8');
97
+ const read = JSON.parse(content);
98
+
99
+ expect(read).toEqual(state);
100
+ expect(read.sessionId).toBe('sess-abc');
101
+ expect(read.status).toBe('waiting');
102
+ expect(read.tmuxPane).toBe('ws-dashboard:0.1');
103
+ });
104
+
105
+ it('filters .json files only', () => {
106
+ writeStateFile('valid', { sessionId: 'valid', status: 'idle' });
107
+ fs.writeFileSync(path.join(tmpDir, 'not-json.txt'), 'hello', 'utf-8');
108
+ fs.writeFileSync(path.join(tmpDir, '.hidden.json'), '{}', 'utf-8');
109
+
110
+ const files = fs.readdirSync(tmpDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
111
+ expect(files).toEqual(['valid.json']);
112
+ });
113
+
114
+ it('sorts states by workspace then startedAt', () => {
115
+ const states = [
116
+ { workspace: 'beta', startedAt: '2026-03-24T02:00:00Z' },
117
+ { workspace: 'alpha', startedAt: '2026-03-24T01:00:00Z' },
118
+ { workspace: 'alpha', startedAt: '2026-03-24T00:00:00Z' },
119
+ { workspace: 'beta', startedAt: '2026-03-24T01:00:00Z' },
120
+ ];
121
+
122
+ states.sort((a, b) => {
123
+ const wsCmp = a.workspace.localeCompare(b.workspace);
124
+ if (wsCmp !== 0) return wsCmp;
125
+ return new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime();
126
+ });
127
+
128
+ expect(states.map(s => `${s.workspace}:${s.startedAt}`)).toEqual([
129
+ 'alpha:2026-03-24T00:00:00Z',
130
+ 'alpha:2026-03-24T01:00:00Z',
131
+ 'beta:2026-03-24T01:00:00Z',
132
+ 'beta:2026-03-24T02:00:00Z',
133
+ ]);
134
+ });
135
+
136
+ it('rejects invalid session IDs (path traversal protection)', () => {
137
+ const invalidIds = [
138
+ '../../etc/passwd',
139
+ '../secret',
140
+ 'valid/../evil',
141
+ 'a'.repeat(101), // too long
142
+ 'has spaces',
143
+ 'has/slash',
144
+ '',
145
+ ];
146
+ for (const id of invalidIds) {
147
+ // isValidSessionId is internal, but we can verify that reading returns null
148
+ // and that writing to these IDs doesn't create files
149
+ const filePath = path.join(tmpDir, `${id}.json`);
150
+ expect(fs.existsSync(filePath)).toBe(false);
151
+ }
152
+ });
153
+
154
+ it('accepts valid session IDs', () => {
155
+ const validIds = [
156
+ 'abc123',
157
+ 'a1bf93e4-c587-441b-bac8-0c20b6d0cbe4',
158
+ 'dash-1774438882722-6sl23p',
159
+ 'a',
160
+ ];
161
+ for (const id of validIds) {
162
+ // All valid — just check the regex pattern matches
163
+ expect(/^[a-zA-Z0-9\-]{1,100}$/.test(id)).toBe(true);
164
+ }
165
+ });
166
+ });
@@ -0,0 +1,135 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { AgentState, DASHBOARD_DEFAULTS } from '../types/dashboard';
5
+
6
+ export const AGENTS_STATE_DIR = path.join(os.homedir(), '.workspace-manager', 'agents');
7
+
8
+ export function ensureStateDir(): void {
9
+ fs.mkdirSync(AGENTS_STATE_DIR, { recursive: true });
10
+ }
11
+
12
+ /**
13
+ * Validate that a session ID is safe to use as a filename.
14
+ * Allows only alphanumeric characters and hyphens, max 100 chars.
15
+ */
16
+ function isValidSessionId(id: string): boolean {
17
+ return typeof id === 'string' && /^[a-zA-Z0-9\-]{1,100}$/.test(id);
18
+ }
19
+
20
+ /**
21
+ * Write agent state atomically (write tmp + rename).
22
+ */
23
+ export function writeAgentState(state: AgentState): void {
24
+ if (!isValidSessionId(state.sessionId)) return; // reject invalid IDs silently
25
+ ensureStateDir();
26
+ const filePath = path.join(AGENTS_STATE_DIR, `${state.sessionId}.json`);
27
+ const tmpPath = filePath + '.tmp';
28
+ try {
29
+ fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf-8');
30
+ fs.renameSync(tmpPath, filePath);
31
+ } catch {
32
+ // Clean up tmp on failure
33
+ try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Read a single agent state by session ID.
39
+ */
40
+ export function readAgentState(sessionId: string): AgentState | null {
41
+ if (!isValidSessionId(sessionId)) return null;
42
+ try {
43
+ const filePath = path.join(AGENTS_STATE_DIR, `${sessionId}.json`);
44
+ const content = fs.readFileSync(filePath, 'utf-8');
45
+ return JSON.parse(content) as AgentState;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Check if a process is alive by sending signal 0.
53
+ */
54
+ function isProcessAlive(pid: number): boolean {
55
+ try {
56
+ process.kill(pid, 0);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Read all agent states, check PID liveness, and clean stale entries.
65
+ * Returns active states sorted by workspace, then startedAt.
66
+ */
67
+ export function readAllAgentStates(): AgentState[] {
68
+ ensureStateDir();
69
+
70
+ let entries: string[];
71
+ try {
72
+ entries = fs.readdirSync(AGENTS_STATE_DIR);
73
+ } catch {
74
+ return [];
75
+ }
76
+
77
+ const now = Date.now();
78
+ const states: AgentState[] = [];
79
+
80
+ for (const entry of entries) {
81
+ if (!entry.endsWith('.json') || entry.startsWith('.')) continue;
82
+
83
+ const sessionId = entry.replace('.json', '');
84
+ const state = readAgentState(sessionId);
85
+ if (!state) {
86
+ // Malformed file — remove it
87
+ try { fs.unlinkSync(path.join(AGENTS_STATE_DIR, entry)); } catch { /* ignore */ }
88
+ continue;
89
+ }
90
+
91
+ // PID liveness check for non-stopped states
92
+ // Skip the check if the state was updated very recently (within 10s)
93
+ // to allow hooks time to update the PID on session resume
94
+ if (state.status !== 'stopped' && state.pid > 0) {
95
+ const age = now - new Date(state.lastUpdatedAt).getTime();
96
+ if (age > 10_000 && !isProcessAlive(state.pid)) {
97
+ state.status = 'stopped';
98
+ state.lastUpdatedAt = new Date().toISOString();
99
+ writeAgentState(state);
100
+ }
101
+ }
102
+
103
+ // Filter out stale stopped states
104
+ if (state.status === 'stopped') {
105
+ const age = now - new Date(state.lastUpdatedAt).getTime();
106
+ if (age > DASHBOARD_DEFAULTS.staleTtlMs) {
107
+ try { fs.unlinkSync(path.join(AGENTS_STATE_DIR, entry)); } catch { /* ignore */ }
108
+ continue;
109
+ }
110
+ }
111
+
112
+ states.push(state);
113
+ }
114
+
115
+ // Sort by workspace ascending, then startedAt ascending
116
+ states.sort((a, b) => {
117
+ const wsCmp = a.workspace.localeCompare(b.workspace);
118
+ if (wsCmp !== 0) return wsCmp;
119
+ return new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime();
120
+ });
121
+
122
+ return states;
123
+ }
124
+
125
+ /**
126
+ * Remove a single agent state file.
127
+ */
128
+ export function removeAgentState(sessionId: string): void {
129
+ if (!isValidSessionId(sessionId)) return;
130
+ try {
131
+ fs.unlinkSync(path.join(AGENTS_STATE_DIR, `${sessionId}.json`));
132
+ } catch {
133
+ // File doesn't exist — fine
134
+ }
135
+ }
@@ -0,0 +1,24 @@
1
+ import { colors } from './colors';
2
+
3
+ const g = colors.green;
4
+ const d = colors.dim;
5
+ const b = colors.bright;
6
+ const r = colors.reset;
7
+
8
+ export const BANNER = `
9
+ ${d} ╭──────────────────────────────────────╮${r}
10
+ ${d} │${r} ${g}>_${r} ${b}Nemus${r} ${d}│${r}
11
+ ${d} │${r} ${d}multi-repo workspaces${r} ${d}│${r}
12
+ ${d} ╰──────────────────────────────────────╯${r}
13
+ `;
14
+
15
+ export const BANNER_PLAIN = `
16
+ ╭──────────────────────────────────────╮
17
+ │ >_ Nemus │
18
+ │ multi-repo workspaces │
19
+ ╰──────────────────────────────────────╯
20
+ `;
21
+
22
+ export const printBanner = (): void => {
23
+ console.log(BANNER);
24
+ };