@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,113 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+
6
+ describe('save-context', () => {
7
+ let tempDir: string;
8
+ let handleSaveContext: typeof import('../mcp/tools').handleSaveContext;
9
+
10
+ beforeEach(async () => {
11
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ws-save-ctx-'));
12
+ process.env.WORKSPACE_MANAGER_DIR = tempDir;
13
+ process.env.WORKSPACE_MANAGER_CACHE_DIR = path.join(tempDir, '.cache');
14
+ vi.resetModules();
15
+ const mod = await import('../mcp/tools');
16
+ handleSaveContext = mod.handleSaveContext;
17
+ });
18
+
19
+ afterEach(async () => {
20
+ await fs.rm(tempDir, { recursive: true, force: true });
21
+ delete process.env.WORKSPACE_MANAGER_DIR;
22
+ delete process.env.WORKSPACE_MANAGER_CACHE_DIR;
23
+ });
24
+
25
+ async function createWorkspace(name: string, opts?: { prompt?: string }) {
26
+ const wsPath = path.join(tempDir, name);
27
+ await fs.mkdir(wsPath, { recursive: true });
28
+ await fs.writeFile(path.join(wsPath, '.workspace-meta.json'), JSON.stringify({
29
+ workspaceName: name,
30
+ createdAt: '2024-01-01T00:00:00Z',
31
+ repositories: [],
32
+ ...(opts?.prompt ? { prompt: opts.prompt } : {}),
33
+ }));
34
+ return wsPath;
35
+ }
36
+
37
+ describe('handleSaveContext (MCP tool)', () => {
38
+ it('creates CONTEXT.md with content', async () => {
39
+ const wsPath = await createWorkspace('my-ws');
40
+ const result = await handleSaveContext('my-ws', 'Completed auth refactor');
41
+ expect(result.saved).toBe(true);
42
+ expect(result.workspace).toBe('my-ws');
43
+
44
+ const content = await fs.readFile(path.join(wsPath, 'CONTEXT.md'), 'utf-8');
45
+ expect(content).toContain('Completed auth refactor');
46
+ expect(content).toContain('Workspace Context: my-ws');
47
+ });
48
+
49
+ it('replaces content by default', async () => {
50
+ const wsPath = await createWorkspace('my-ws');
51
+ await handleSaveContext('my-ws', 'First version');
52
+ await handleSaveContext('my-ws', 'Second version');
53
+
54
+ const content = await fs.readFile(path.join(wsPath, 'CONTEXT.md'), 'utf-8');
55
+ expect(content).toContain('Second version');
56
+ expect(content).not.toContain('First version');
57
+ });
58
+
59
+ it('appends when append=true', async () => {
60
+ const wsPath = await createWorkspace('my-ws');
61
+ await handleSaveContext('my-ws', 'First version');
62
+ await handleSaveContext('my-ws', 'Additional notes', true);
63
+
64
+ const content = await fs.readFile(path.join(wsPath, 'CONTEXT.md'), 'utf-8');
65
+ expect(content).toContain('First version');
66
+ expect(content).toContain('Additional notes');
67
+ expect(content).toContain('---'); // separator
68
+ });
69
+
70
+ it('creates fresh file when appending to non-existent', async () => {
71
+ const wsPath = await createWorkspace('my-ws');
72
+ await handleSaveContext('my-ws', 'Fresh content', true);
73
+
74
+ const content = await fs.readFile(path.join(wsPath, 'CONTEXT.md'), 'utf-8');
75
+ expect(content).toContain('Fresh content');
76
+ expect(content).toContain('Workspace Context: my-ws');
77
+ });
78
+
79
+ it('throws for non-existent workspace', async () => {
80
+ await expect(handleSaveContext('nope', 'content')).rejects.toThrow('Workspace not found');
81
+ });
82
+
83
+ it('throws for empty content', async () => {
84
+ await createWorkspace('my-ws');
85
+ await expect(handleSaveContext('my-ws', '')).rejects.toThrow('Content is required');
86
+ });
87
+
88
+ it('throws for empty workspace name', async () => {
89
+ await expect(handleSaveContext('', 'content')).rejects.toThrow('Workspace name is required');
90
+ });
91
+ });
92
+ });
93
+
94
+ describe('createMetadata with prompt', () => {
95
+ it('includes prompt in metadata when provided', async () => {
96
+ vi.resetModules();
97
+ process.env.WORKSPACE_MANAGER_DIR = '/tmp/test-ws';
98
+ const { createMetadata } = await import('../utils/workspace-meta');
99
+
100
+ const metadata = createMetadata('test-ws', [], { prompt: 'build a payment service' });
101
+ expect(metadata.prompt).toBe('build a payment service');
102
+ expect(metadata.workspaceName).toBe('test-ws');
103
+ });
104
+
105
+ it('omits prompt field when not provided', async () => {
106
+ vi.resetModules();
107
+ process.env.WORKSPACE_MANAGER_DIR = '/tmp/test-ws';
108
+ const { createMetadata } = await import('../utils/workspace-meta');
109
+
110
+ const metadata = createMetadata('test-ws', []);
111
+ expect(metadata.prompt).toBeUndefined();
112
+ });
113
+ });
@@ -0,0 +1,141 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs/promises';
4
+ import { WORKSPACES_DIR, META_FILENAME } from '../utils/config';
5
+ import { logInfo, logSuccess, logError } from '../utils/logger';
6
+ import { colorize } from '../utils/colors';
7
+ import { loadMetadata, saveMetadata, listWorkspaces } from '../utils/workspace-meta';
8
+ import { formatContextFile, appendToContextFile } from '../utils/context-file';
9
+ import inquirer from 'inquirer';
10
+
11
+ const CONTEXT_FILENAME = 'CONTEXT.md';
12
+
13
+ export function registerSaveContextCommand(parent: Command) {
14
+ parent
15
+ .command('save-context')
16
+ .alias('ctx')
17
+ .description('Save a progress summary to the workspace (persists across /clear)')
18
+ .option('-w, --workspace <name>', 'Workspace name (default: current directory)')
19
+ .option('-m, --message <text>', 'Summary text to save')
20
+ .option('-f, --file <path>', 'Read summary from a file')
21
+ .option('--append', 'Append to existing context instead of replacing')
22
+ .action(async (opts) => {
23
+ await handleSaveContext(opts);
24
+ });
25
+ }
26
+
27
+ async function resolveWorkspacePath(workspaceName?: string): Promise<{ name: string; path: string } | null> {
28
+ // Try current directory first
29
+ if (!workspaceName) {
30
+ const cwd = process.cwd();
31
+ const metaPath = path.join(cwd, META_FILENAME);
32
+ try {
33
+ await fs.access(metaPath);
34
+ const meta = JSON.parse(await fs.readFile(metaPath, 'utf-8'));
35
+ return { name: meta.workspaceName || path.basename(cwd), path: cwd };
36
+ } catch {
37
+ // Not in a workspace directory — check if we're inside a workspace subdirectory
38
+ const parentDir = path.dirname(cwd);
39
+ const parentMeta = path.join(parentDir, META_FILENAME);
40
+ try {
41
+ await fs.access(parentMeta);
42
+ const meta = JSON.parse(await fs.readFile(parentMeta, 'utf-8'));
43
+ return { name: meta.workspaceName || path.basename(parentDir), path: parentDir };
44
+ } catch {
45
+ // Not in any workspace
46
+ }
47
+ }
48
+ }
49
+
50
+ // Use provided workspace name
51
+ if (workspaceName) {
52
+ const wsPath = path.join(WORKSPACES_DIR, workspaceName);
53
+ try {
54
+ await fs.access(path.join(wsPath, META_FILENAME));
55
+ return { name: workspaceName, path: wsPath };
56
+ } catch {
57
+ logError(`Workspace not found: ${workspaceName}`);
58
+ return null;
59
+ }
60
+ }
61
+
62
+ // Interactive selection
63
+ if (process.stdout.isTTY) {
64
+ const workspaces = await listWorkspaces(false);
65
+ if (workspaces.length === 0) {
66
+ logError('No workspaces found');
67
+ return null;
68
+ }
69
+ const { selected } = await inquirer.prompt([{
70
+ type: 'list',
71
+ name: 'selected',
72
+ message: 'Select workspace:',
73
+ choices: workspaces.map(w => ({ name: w.name, value: w.name })),
74
+ }]);
75
+ return { name: selected, path: path.join(WORKSPACES_DIR, selected) };
76
+ }
77
+
78
+ logError('Could not determine workspace. Use --workspace <name> or run from within a workspace.');
79
+ return null;
80
+ }
81
+
82
+ async function handleSaveContext(opts: {
83
+ workspace?: string;
84
+ message?: string;
85
+ file?: string;
86
+ append?: boolean;
87
+ }) {
88
+ const ws = await resolveWorkspacePath(opts.workspace);
89
+ if (!ws) return;
90
+
91
+ // Get content from message, file, or stdin
92
+ let content: string;
93
+ if (opts.message) {
94
+ content = opts.message;
95
+ } else if (opts.file) {
96
+ try {
97
+ content = await fs.readFile(opts.file, 'utf-8');
98
+ } catch (err) {
99
+ logError(`Failed to read file: ${opts.file}`);
100
+ return;
101
+ }
102
+ } else if (!process.stdin.isTTY) {
103
+ // Read from stdin (piped input)
104
+ const chunks: Buffer[] = [];
105
+ for await (const chunk of process.stdin) {
106
+ chunks.push(chunk);
107
+ }
108
+ content = Buffer.concat(chunks).toString('utf-8').trim();
109
+ } else {
110
+ logError('No content provided. Use --message, --file, or pipe content via stdin.');
111
+ console.log(`\n Usage:`);
112
+ console.log(` ${colorize('w save-context -m "Completed auth refactor, moved to payments"', 'green')}`);
113
+ console.log(` ${colorize('w save-context -f summary.md', 'green')}`);
114
+ console.log(` ${colorize('echo "progress notes" | w save-context', 'green')}`);
115
+ return;
116
+ }
117
+
118
+ if (!content.trim()) {
119
+ logError('Empty content — nothing to save.');
120
+ return;
121
+ }
122
+
123
+ const contextPath = path.join(ws.path, CONTEXT_FILENAME);
124
+
125
+ if (opts.append) {
126
+ let existing: string | null = null;
127
+ try {
128
+ existing = await fs.readFile(contextPath, 'utf-8');
129
+ } catch {
130
+ // File doesn't exist yet
131
+ }
132
+ content = appendToContextFile(existing, ws.name, content);
133
+ } else {
134
+ content = formatContextFile(ws.name, content);
135
+ }
136
+
137
+ await fs.writeFile(contextPath, content, 'utf-8');
138
+ logSuccess(`Context saved to ${colorize(CONTEXT_FILENAME, 'cyan')} in ${ws.name}`);
139
+ logInfo('This file persists across /clear and session restarts.');
140
+ }
141
+
@@ -0,0 +1,99 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import * as fs from 'fs/promises';
5
+ import { getWorkspaceSessions, WorkspaceSession } from '../utils/claude-sessions';
6
+ import { listWorkspaces } from '../utils/workspace-meta';
7
+ import { logError, logInfo } from '../utils/logger';
8
+ import { colorize } from '../utils/colors';
9
+ import inquirer from 'inquirer';
10
+ import autocompletePrompt from 'inquirer-autocomplete-prompt';
11
+ import * as fuzzy from 'fuzzy';
12
+
13
+ inquirer.registerPrompt('autocomplete', autocompletePrompt);
14
+
15
+ const TEMP_FILE = path.join(os.homedir(), '.workspace-last-go');
16
+ const RESUME_FLAG_FILE = path.join(os.homedir(), '.workspace-resume-session');
17
+
18
+ export function registerSessionsCommand(parent: Command) {
19
+ parent
20
+ .command('sessions')
21
+ .alias('ses')
22
+ .description('Resume a Claude session in a workspace')
23
+ .action(async () => {
24
+ await handleSessions();
25
+ });
26
+ }
27
+
28
+ async function handleSessions() {
29
+ try {
30
+ const sessions = await getWorkspaceSessions();
31
+
32
+ if (sessions.length === 0) {
33
+ logInfo('No workspace sessions found.');
34
+ console.log('\nYou can create a workspace with: nemus create');
35
+ console.log('Or navigate to one with: w go');
36
+ return;
37
+ }
38
+
39
+ const workspaces = await listWorkspaces(false);
40
+ const workspaceMap = new Map(workspaces.map(ws => [ws.name, ws]));
41
+
42
+ const items = sessions.map(session => {
43
+ const ws = workspaceMap.get(session.workspaceName);
44
+ const repoCount = ws?.metadata?.repositories?.length ?? 0;
45
+ const repoLabel = repoCount > 0 ? `${repoCount} repos` : 'no repos';
46
+ return { session, repoLabel };
47
+ });
48
+
49
+ const maxNameLen = Math.max(...items.map(i => i.session.workspaceName.length));
50
+
51
+ console.log('');
52
+ console.log(colorize(' Workspace Sessions', 'bright') + colorize(' (sorted by last active)', 'dim'));
53
+ console.log(colorize(' ' + '─'.repeat(50), 'dim'));
54
+
55
+ for (const item of items.slice(0, 10)) {
56
+ const name = item.session.workspaceName.padEnd(maxNameLen);
57
+ const time = item.session.lastActiveLabel;
58
+ console.log(` ${colorize(name, 'cyan')} ${colorize(time, 'yellow')} ${colorize(item.repoLabel, 'dim')}`);
59
+ }
60
+ if (items.length > 10) {
61
+ console.log(colorize(` ... and ${items.length - 10} more`, 'dim'));
62
+ }
63
+ console.log('');
64
+
65
+ const { selected } = await inquirer.prompt([{
66
+ type: 'autocomplete', name: 'selected',
67
+ message: 'Select workspace to resume:',
68
+ source: async (_answersSoFar: any, input: string | undefined) => {
69
+ const searchInput = input || '';
70
+ const source = items.map(item => ({
71
+ name: `${item.session.workspaceName} ${colorize(item.session.lastActiveLabel, 'dim')} ${colorize(item.repoLabel, 'dim')}`,
72
+ value: item.session,
73
+ }));
74
+ if (!searchInput) return source;
75
+ const results = fuzzy.filter(searchInput, items, { extract: (item) => item.session.workspaceName });
76
+ return results.map(result => ({
77
+ name: `${result.original.session.workspaceName} ${colorize(result.original.session.lastActiveLabel, 'dim')} ${colorize(result.original.repoLabel, 'dim')}`,
78
+ value: result.original.session,
79
+ }));
80
+ },
81
+ pageSize: 15,
82
+ } as any]);
83
+
84
+ const session = selected as WorkspaceSession;
85
+ await fs.writeFile(TEMP_FILE, session.workspacePath, 'utf-8');
86
+ // Write JSON with session ID and agent type for correct resume
87
+ await fs.writeFile(RESUME_FLAG_FILE, JSON.stringify({
88
+ sessionId: session.sessionId,
89
+ agentType: session.agentType,
90
+ }), 'utf-8');
91
+
92
+ console.log(`\n${colorize('Resuming:', 'green')} ${session.workspaceName} (last active ${session.lastActiveLabel})`);
93
+ } catch (error) {
94
+ if ((error as any)?.name === 'ExitPromptError') return;
95
+ logError('Failed to list sessions');
96
+ if (error instanceof Error) { logError(error.message); }
97
+ process.exit(1);
98
+ }
99
+ }
@@ -0,0 +1,111 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import { WORKSPACES_DIR } from '../utils/config';
4
+ import { loadMetadata } from '../utils/workspace-meta';
5
+ import { getAllReposStatus } from '../utils/git-status';
6
+ import { logError, logInfo, logStep } from '../utils/logger';
7
+ import { colorize } from '../utils/colors';
8
+ import { resolveWorkspace } from '../utils/command-helpers';
9
+
10
+ const displayStatusTable = (statuses: Array<{ repo: string; branch: string; clean: boolean; ahead: number; behind: number; modifiedFiles: number; untrackedFiles: number; hasRemote: boolean; detachedHead: boolean }>) => {
11
+ console.log('\n' + colorize('═'.repeat(100), 'gray'));
12
+ console.log(
13
+ colorize('Repo', 'bright').padEnd(30) +
14
+ colorize('Branch', 'bright').padEnd(25) +
15
+ colorize('Status', 'bright').padEnd(15) +
16
+ colorize('Ahead/Behind', 'bright').padEnd(15) +
17
+ colorize('Modified', 'bright')
18
+ );
19
+ console.log(colorize('─'.repeat(100), 'gray'));
20
+
21
+ for (const status of statuses) {
22
+ const repoName = status.repo.padEnd(20);
23
+ let branchDisplay = status.branch.padEnd(15);
24
+ if (status.detachedHead) {
25
+ branchDisplay = colorize(branchDisplay, 'yellow');
26
+ }
27
+
28
+ let statusDisplay = '';
29
+ if (status.clean) {
30
+ statusDisplay = colorize('✓ Clean', 'green').padEnd(15);
31
+ } else {
32
+ const changes = [];
33
+ if (status.modifiedFiles > 0) changes.push(`${status.modifiedFiles} modified`);
34
+ if (status.untrackedFiles > 0) changes.push(`${status.untrackedFiles} untracked`);
35
+ statusDisplay = colorize(`⚠ ${changes.join(', ')}`, 'yellow').padEnd(15);
36
+ }
37
+
38
+ let trackingDisplay = '';
39
+ if (!status.hasRemote) {
40
+ trackingDisplay = colorize('No remote', 'gray').padEnd(15);
41
+ } else if (status.ahead === 0 && status.behind === 0) {
42
+ trackingDisplay = colorize('Up to date', 'green').padEnd(15);
43
+ } else {
44
+ const parts = [];
45
+ if (status.ahead > 0) parts.push(colorize(`↑${status.ahead}`, 'cyan'));
46
+ if (status.behind > 0) parts.push(colorize(`↓${status.behind}`, 'red'));
47
+ trackingDisplay = parts.join(' ').padEnd(15);
48
+ }
49
+
50
+ const modifiedDisplay = status.clean ? '-' : `${status.modifiedFiles + status.untrackedFiles} files`;
51
+ console.log(`${repoName} ${branchDisplay} ${statusDisplay} ${trackingDisplay} ${modifiedDisplay}`);
52
+ }
53
+
54
+ console.log(colorize('═'.repeat(100), 'gray'));
55
+
56
+ const cleanRepos = statuses.filter(s => s.clean).length;
57
+ const dirtyRepos = statuses.filter(s => !s.clean).length;
58
+ const needsPush = statuses.filter(s => s.ahead > 0).length;
59
+ const needsPull = statuses.filter(s => s.behind > 0).length;
60
+
61
+ console.log('\n' + colorize('Summary:', 'bright'));
62
+ console.log(` ${colorize('✓', 'green')} ${cleanRepos} clean repositories`);
63
+ if (dirtyRepos > 0) {
64
+ console.log(` ${colorize('⚠', 'yellow')} ${dirtyRepos} repositories with uncommitted changes`);
65
+ }
66
+ if (needsPush > 0) {
67
+ console.log(` ${colorize('↑', 'cyan')} ${needsPush} repositories ahead of remote`);
68
+ }
69
+ if (needsPull > 0) {
70
+ console.log(` ${colorize('↓', 'red')} ${needsPull} repositories behind remote`);
71
+ }
72
+ console.log('');
73
+ };
74
+
75
+ export function registerStatusCommand(parent: Command) {
76
+ parent
77
+ .command('status')
78
+ .alias('st')
79
+ .description('Show git status across all repos')
80
+ .argument('[workspace]', 'Workspace name')
81
+ .action(async (workspace) => {
82
+ await handleStatus(workspace);
83
+ });
84
+ }
85
+
86
+ async function handleStatus(workspaceArg?: string) {
87
+ try {
88
+ const selectedWorkspace = await resolveWorkspace(workspaceArg);
89
+ const workspacePath = path.join(WORKSPACES_DIR, selectedWorkspace);
90
+ const metadata = await loadMetadata(workspacePath);
91
+
92
+ if (!metadata) {
93
+ logError(`Workspace metadata not found for: ${selectedWorkspace}`);
94
+ process.exit(1);
95
+ }
96
+
97
+ logStep(`Checking status for workspace: ${colorize(selectedWorkspace, 'cyan')}`);
98
+ logInfo(`Found ${metadata.repositories.length} repositories`);
99
+
100
+ const repoDirectoryNames = metadata.repositories.map(r => r.directoryName);
101
+ const statuses = await getAllReposStatus(workspacePath, repoDirectoryNames, 3);
102
+
103
+ displayStatusTable(statuses);
104
+ } catch (error) {
105
+ logError('Failed to check workspace status');
106
+ if (error instanceof Error) {
107
+ logError(error.message);
108
+ }
109
+ process.exit(1);
110
+ }
111
+ }