@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,649 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as fsSync from 'fs';
3
+ import * as path from 'path';
4
+ import { GitHubRepo, WorkspaceMetadata } from '../types';
5
+ import { logSuccess, logWarning } from './logger';
6
+ import { colorize } from './colors';
7
+ import { getUserConfig } from './config';
8
+ import { getContextFileNames, getAllKnownContextFileNames } from './agent-config';
9
+
10
+ interface ClaudeConfig {
11
+ autoLaunch: boolean;
12
+ generateContext: boolean;
13
+ }
14
+
15
+ const DEFAULT_CONFIG: ClaudeConfig = {
16
+ autoLaunch: false,
17
+ generateContext: true,
18
+ };
19
+
20
+ /**
21
+ * Version of the AI Agent Rules block. Bump whenever the rules text in
22
+ * {@link buildAgentRulesSection} changes, so {@link backfillAgentRules}
23
+ * replaces a previously-embedded (now stale) block in existing workspaces.
24
+ */
25
+ export const AGENT_RULES_VERSION = 2;
26
+
27
+ /** Locate + version the rules block in an existing context file. */
28
+ const AGENT_RULES_MARKER_RE = /<!-- ws-rules:v(\d+) -->/;
29
+
30
+ /**
31
+ * Build the "AI Agent Rules" markdown section for a given workspace.
32
+ * Used both in new-workspace generation and in the upgrade backfill.
33
+ */
34
+ export function buildAgentRulesSection(workspaceName: string): string {
35
+ let s = `## AI Agent Rules\n\n`;
36
+ s += `<!-- ws-rules:v${AGENT_RULES_VERSION} -->\n`;
37
+ s += `**NEVER** use \`git clone\` directly to add repositories to this workspace.\n`;
38
+ s += `**ALWAYS** use workspace manager commands for all repo management:\n\n`;
39
+ s += `| Intent | Command |\n`;
40
+ s += `|---|---|\n`;
41
+ s += `| Add a repo to this workspace | \`nemus update --workspace ${workspaceName} --repos <repo-name> --yes\` |\n`;
42
+ s += `| Sync / pull latest on all repos | \`w sync ${workspaceName}\` |\n`;
43
+ s += `| Check git status across repos | \`w status ${workspaceName}\` |\n`;
44
+ s += `| Create a new workspace | \`nemus create --workspace <name> --repos <repos> --yes\` |\n`;
45
+ s += `\nUsing \`git clone\` directly bypasses workspace metadata tracking and breaks \`w status\`, \`w sync\`, and context file updates.\n\n`;
46
+ s += `**ONLY read code from repos in THIS workspace.** If you need to read or reference code from a repo that isn't here, add it with \`nemus update --workspace ${workspaceName} --repos <repo-name> --yes\` and read it from this workspace. **NEVER** read that repo's code from another workspace, a global ghq/clone path, or anywhere outside this workspace — other workspaces may be on different branches or stale, and reading them silently breaks context isolation.\n\n`;
47
+ return s;
48
+ }
49
+
50
+ /**
51
+ * Backfill (or update) the "AI Agent Rules" section in existing context files
52
+ * (AGENTS.md / .claude.md).
53
+ *
54
+ * - If the section is missing entirely, it is inserted.
55
+ * - If the section is present but carries an older `ws-rules:vN` marker (or no
56
+ * marker at all, i.e. a pre-versioning block), it is REPLACED in place with
57
+ * the current version. This is how rule changes reach existing workspaces
58
+ * on `w mcp upgrade` / migrate.
59
+ * - If the section is already at the current version, the file is left untouched.
60
+ *
61
+ * Idempotent. Returns the number of files updated.
62
+ */
63
+ export async function backfillAgentRules(workspacePath: string, workspaceName: string): Promise<number> {
64
+ const contextFileNames = getContextFileNames();
65
+ let updated = 0;
66
+
67
+ for (const fileName of contextFileNames) {
68
+ const filePath = path.join(workspacePath, fileName);
69
+ let existing: string;
70
+ try {
71
+ existing = await fs.readFile(filePath, 'utf-8');
72
+ } catch {
73
+ continue; // file doesn't exist — skip
74
+ }
75
+
76
+ const rulesBlock = buildAgentRulesSection(workspaceName);
77
+ let patched: string;
78
+
79
+ if (existing.includes('## AI Agent Rules')) {
80
+ // Section already present — check its version marker.
81
+ const marker = existing.match(AGENT_RULES_MARKER_RE);
82
+ const embeddedVersion = marker ? parseInt(marker[1], 10) : 0;
83
+ if (embeddedVersion >= AGENT_RULES_VERSION) continue; // already current
84
+
85
+ // Replace the stale block (from its "## AI Agent Rules" heading up to,
86
+ // but not including, the next "## " heading or EOF).
87
+ // Match the heading and body up to the next "## " heading or EOF.
88
+ // Tolerate CRLF (\r\n) line endings so Windows-authored context files
89
+ // also get their stale block replaced.
90
+ const sectionRe = /## AI Agent Rules\r?\n[\s\S]*?(?=\r?\n## |$)/;
91
+ // buildAgentRulesSection ends with a trailing blank line; trim one so the
92
+ // spacing before the following heading stays consistent on replace.
93
+ patched = existing.replace(sectionRe, rulesBlock.replace(/\n+$/, '\n'));
94
+ if (patched === existing) continue; // nothing changed (defensive)
95
+ } else {
96
+ // Section missing — insert it before Tips, else before Notes, else append.
97
+ if (existing.includes('## Tips for Working with AI Agents')) {
98
+ patched = existing.replace('## Tips for Working with AI Agents', `${rulesBlock}## Tips for Working with AI Agents`);
99
+ } else if (existing.includes('## Notes')) {
100
+ patched = existing.replace('## Notes', `${rulesBlock}## Notes`);
101
+ } else {
102
+ patched = existing + `\n${rulesBlock}`;
103
+ }
104
+ }
105
+
106
+ await fs.writeFile(filePath, patched, 'utf-8');
107
+ updated++;
108
+ }
109
+
110
+ return updated;
111
+ }
112
+
113
+
114
+ export async function loadClaudeConfig(): Promise<ClaudeConfig> {
115
+ const configFile = path.join(process.env.HOME || '~', '.workspace-manager-claude-config.json');
116
+
117
+ try {
118
+ const content = await fs.readFile(configFile, 'utf-8');
119
+ return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
120
+ } catch {
121
+ return DEFAULT_CONFIG;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Save Claude integration config
127
+ */
128
+ export async function saveClaudeConfig(config: ClaudeConfig): Promise<void> {
129
+ const configFile = path.join(process.env.HOME || '~', '.workspace-manager-claude-config.json');
130
+
131
+ try {
132
+ await fs.writeFile(configFile, JSON.stringify(config, null, 2), 'utf-8');
133
+ } catch (error) {
134
+ logWarning('Failed to save Claude config');
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Generate context files (e.g. .claude.md, AGENTS.md) with workspace context.
140
+ * Writes to all context file names configured for active agents.
141
+ */
142
+ export async function generateClaudeContext(
143
+ workspacePath: string,
144
+ workspaceName: string,
145
+ repos: GitHubRepo[],
146
+ metadata?: WorkspaceMetadata
147
+ ): Promise<void> {
148
+ try {
149
+ const contextFileNames = getContextFileNames();
150
+
151
+ // Build a lookup from repo name to GitHubRepo for description/url
152
+ const repoInfoMap = new Map<string, GitHubRepo>();
153
+ for (const r of repos) {
154
+ repoInfoMap.set(r.name, r);
155
+ }
156
+
157
+ // Build the list of repo entries to iterate over. Use metadata.repositories
158
+ // as the primary source (each entry has a unique directoryName, handling
159
+ // duplicate repos with different suffixes). Fall back to repos array.
160
+ interface RepoEntry { name: string; directoryName: string; description: string; url: string }
161
+ const entries: RepoEntry[] = [];
162
+ if (metadata && metadata.repositories.length > 0) {
163
+ for (const r of metadata.repositories.filter(r => r.status === 'success')) {
164
+ const info = repoInfoMap.get(r.name);
165
+ entries.push({
166
+ name: r.name,
167
+ directoryName: r.directoryName,
168
+ description: info?.description || '',
169
+ url: info?.url || r.cloneUrl,
170
+ });
171
+ }
172
+ } else {
173
+ for (const r of repos) {
174
+ entries.push({
175
+ name: r.name,
176
+ directoryName: r.name,
177
+ description: r.description || '',
178
+ url: r.url,
179
+ });
180
+ }
181
+ }
182
+
183
+ const { githubOrg } = getUserConfig();
184
+
185
+ let content = `# Workspace: ${workspaceName}
186
+
187
+ This workspace was created with [Workspace Manager](https://github.com/${githubOrg}/workspace-manager).
188
+
189
+ ## Organization
190
+
191
+ All repositories in this workspace belong to the **${githubOrg}** GitHub organization (\`github.com/${githubOrg}\`). When searching for code, packages, or dependencies, assume the \`@${githubOrg}\` npm scope and the \`${githubOrg}/\` GitHub org prefix.
192
+
193
+ ## Overview
194
+
195
+ This workspace contains ${entries.length} repositories for focused development work.
196
+
197
+ `;
198
+
199
+ // Add creation date if available
200
+ if (metadata?.createdAt) {
201
+ const createdDate = new Date(metadata.createdAt).toLocaleDateString('en-US', {
202
+ year: 'numeric',
203
+ month: 'long',
204
+ day: 'numeric',
205
+ });
206
+ content += `**Created:** ${createdDate}\n\n`;
207
+ }
208
+
209
+ // Add original prompt if available
210
+ if (metadata?.prompt) {
211
+ content += `**Original prompt:** ${metadata.prompt}\n\n`;
212
+ }
213
+
214
+ // Add repositories section
215
+ content += `## Repositories\n\n`;
216
+
217
+ // Group entries by prefix for display
218
+ const grouped = groupEntriesByPrefix(entries);
219
+
220
+ if (Object.keys(grouped).length > 1) {
221
+ for (const [prefix, groupEntries] of Object.entries(grouped)) {
222
+ if (prefix !== 'other') {
223
+ content += `### ${prefix.charAt(0).toUpperCase() + prefix.slice(1)} Services\n\n`;
224
+ } else {
225
+ content += `### Other Repositories\n\n`;
226
+ }
227
+
228
+ for (const entry of groupEntries) {
229
+ const description = entry.description ? ` - ${entry.description}` : '';
230
+ const aliasNote = entry.directoryName !== entry.name ? ` (instance: ${entry.directoryName})` : '';
231
+ content += `- **${entry.name}**${aliasNote}${description}\n`;
232
+ content += ` - Path: \`${entry.directoryName}/\`\n`;
233
+ content += ` - GitHub: ${entry.url}\n\n`;
234
+ }
235
+ }
236
+ } else {
237
+ for (const entry of entries) {
238
+ const description = entry.description ? ` - ${entry.description}` : '';
239
+ const aliasNote = entry.directoryName !== entry.name ? ` (instance: ${entry.directoryName})` : '';
240
+ content += `### ${entry.directoryName}${aliasNote}\n\n`;
241
+ content += `${description}\n\n`;
242
+ content += `- Path: \`${entry.directoryName}/\`\n`;
243
+ content += `- GitHub: ${entry.url}\n\n`;
244
+ }
245
+ }
246
+
247
+ // Workspace structure will be added per-file (different context file names)
248
+ const allDirNames = entries.map(e => e.directoryName);
249
+ content += '{{WORKSPACE_STRUCTURE}}\n';
250
+
251
+ // Embed per-repo context files HERE — right after the workspace structure,
252
+ // before generic workflow/management boilerplate. LLMs weight earlier
253
+ // content more heavily, so per-repo rules need to be at the top of the
254
+ // file, not buried 80 lines down after generic tips.
255
+ const perRepoSection = await buildPerRepoContextSection(workspacePath, allDirNames);
256
+ if (perRepoSection) {
257
+ content += perRepoSection + '\n';
258
+ }
259
+
260
+ // Add common workflows
261
+ content += `## Common Workflows\n\n`;
262
+ content += `### Opening Specific Repository\n\n`;
263
+ content += '```bash\n';
264
+ content += `cd ${allDirNames[0] || '<repo-name>'}\n`;
265
+ content += '# Work on specific repository\n';
266
+ content += '```\n\n';
267
+
268
+ content += `### Running Commands Across All Repos\n\n`;
269
+ content += '```bash\n';
270
+ content += '# Example: Check git status for all repos\n';
271
+ content += 'for dir in */; do\n';
272
+ content += ' echo "=== $dir ===" && cd "$dir" && git status -s && cd ..\n';
273
+ content += 'done\n';
274
+ content += '```\n\n';
275
+
276
+ // Add workspace management commands
277
+ content += `## Workspace Management\n\n`;
278
+ content += `These commands help you manage this workspace:\n\n`;
279
+ content += '```bash\n';
280
+ content += 'workspace sync # Pull latest changes for all repos\n';
281
+ content += 'workspace switch-branch # Switch all repos to same branch\n';
282
+ content += 'workspace update # Add more repositories\n';
283
+ content += 'workspace suite create # Save repos as a reusable suite\n';
284
+ content += '```\n\n';
285
+
286
+ // Add explicit AI agent rules — prevent direct git clone
287
+ content += buildAgentRulesSection(workspaceName);
288
+
289
+ // Add tips section
290
+ content += `## Tips for Working with AI Agents\n\n`;
291
+ content += `- **Multi-repo context**: Your AI agent can see all repositories in this workspace\n`;
292
+ content += `- **Cross-repo changes**: Ask the agent to make changes across multiple repos\n`;
293
+ content += `- **Architecture questions**: The agent has context of your entire stack\n`;
294
+ content += `- **Workspace commands**: Use the workspace manager commands above\n\n`;
295
+
296
+ // Add notes section
297
+ content += `## Notes\n\n`;
298
+ content += `Add your own notes here:\n\n`;
299
+ content += `- \n\n`;
300
+
301
+ // Reference CONTEXT.md if it exists (saved progress from w save-context)
302
+ content += `## Saved Context\n\n`;
303
+ content += `If a \`CONTEXT.md\` file exists in this workspace, it contains saved progress and summaries from previous sessions. Read it for continuity after \`/clear\` or when resuming work.\n\n`;
304
+
305
+ // Write context file for each active agent (customize structure per-file)
306
+ for (const fileName of contextFileNames) {
307
+ // Build workspace structure block with correct filename
308
+ const padding = ' '.repeat(Math.max(1, 28 - fileName.length));
309
+ const structureBlock =
310
+ '## Workspace Structure\n\n' +
311
+ '```\n' +
312
+ `${workspaceName}/\n` +
313
+ `├── ${fileName}${padding}# Workspace context\n` +
314
+ `├── .workspace-meta.json # Workspace metadata\n` +
315
+ `├── CONTEXT.md # Saved progress (if exists)\n` +
316
+ allDirNames.slice(0, 5).map(d => `├── ${d}/\n`).join('') +
317
+ (allDirNames.length > 5 ? `└── ... (${allDirNames.length - 5} more repositories)\n` : '') +
318
+ '```\n\n';
319
+
320
+ // Insert structure block at placeholder
321
+ let fileContent = content.replace('{{WORKSPACE_STRUCTURE}}\n', structureBlock);
322
+ const contextFile = path.join(workspacePath, fileName);
323
+ // Regeneration (e.g. after `nemus update`/`remove-repo`) overwrites the
324
+ // file, so carry over any operator-authored "## Notes" the user added
325
+ // since the last generation instead of silently discarding it.
326
+ fileContent = await preserveOperatorNotes(contextFile, fileContent);
327
+ await fs.writeFile(contextFile, fileContent, 'utf-8');
328
+ logSuccess(`Generated ${colorize(fileName, 'cyan')} with workspace context`);
329
+ }
330
+
331
+ // Generate .mcp.json with workspace-manager MCP server (if enabled)
332
+ const { installMcp } = getUserConfig();
333
+ if (installMcp) {
334
+ await generateMcpConfig(workspacePath);
335
+ }
336
+ } catch (error) {
337
+ logWarning('Failed to generate context file(s)');
338
+ if (error instanceof Error) {
339
+ logWarning(error.message);
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Extract a top-level markdown section (heading + body up to the next `## `
346
+ * heading at the start of a line, or EOF). Returns null when the heading is
347
+ * absent. The heading must be a WHOLE top-level heading line — anchored to the
348
+ * start of a line and terminated by end-of-line — so a deeper heading like
349
+ * `### Notes`, inline prose, or fenced-code text that merely contains the
350
+ * string can't be mistaken for the real section.
351
+ */
352
+ function extractSection(content: string, heading: string): string | null {
353
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
354
+ const headingRe = new RegExp(`(^|\\n)${escaped}[ \\t]*(?:\\n|$)`);
355
+ const m = headingRe.exec(content);
356
+ if (!m) return null;
357
+ const start = m.index + (m[1] ? m[1].length : 0); // position of the heading itself
358
+ const afterHeading = start + heading.length;
359
+ const nextIdx = content.indexOf('\n## ', afterHeading);
360
+ return nextIdx === -1 ? content.slice(start) : content.slice(start, nextIdx + 1);
361
+ }
362
+
363
+ /**
364
+ * When regenerating a context file, carry over the operator-authored "## Notes"
365
+ * section from the existing file if the user filled it in. The freshly built
366
+ * content always contains an empty "## Notes" placeholder; if the existing file
367
+ * has a non-default Notes section, splice it into the new content so an add/
368
+ * remove-repo regeneration doesn't silently delete the operator's notes.
369
+ * Best-effort: on any read/parse issue we fall back to the new content.
370
+ */
371
+ export async function preserveOperatorNotes(contextFile: string, newContent: string): Promise<string> {
372
+ try {
373
+ const existing = await fs.readFile(contextFile, 'utf-8');
374
+ const existingNotes = extractSection(existing, '## Notes');
375
+ if (!existingNotes) return newContent;
376
+ // Default placeholder the generator emits — treat as "no operator notes".
377
+ const isDefault = existingNotes.replace(/\s+/g, ' ').trim()
378
+ === '## Notes Add your own notes here: -';
379
+ if (isDefault) return newContent;
380
+ const newNotes = extractSection(newContent, '## Notes');
381
+ if (!newNotes) return newContent;
382
+ // Preserve the trailing separator style of the section being replaced.
383
+ const tail = newNotes.endsWith('\n\n') ? '\n\n' : (newNotes.endsWith('\n') ? '\n' : '');
384
+ const replacement = existingNotes.replace(/\s*$/, '') + tail;
385
+ // Use a function replacer so `$&`, `$$`, `$1`, etc. in the operator's notes
386
+ // are inserted literally rather than interpreted by String.replace.
387
+ return newContent.replace(newNotes, () => replacement);
388
+ } catch {
389
+ return newContent;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Resolve the MCP server.js path from the installed package.
395
+ * When compiled, __dirname is dist/utils/ — server.js is at dist/mcp/server.js.
396
+ */
397
+ function resolveMcpServerPath(): string | null {
398
+ const candidates = [
399
+ path.join(__dirname, '..', 'mcp', 'server.js'), // dist/utils/ -> dist/mcp/server.js
400
+ path.join(__dirname, '..', '..', 'dist', 'mcp', 'server.js'), // src/utils/ -> dist/mcp/server.js
401
+ ];
402
+ for (const p of candidates) {
403
+ if (fsSync.existsSync(p)) {
404
+ return path.resolve(p);
405
+ }
406
+ }
407
+ return null;
408
+ }
409
+
410
+ /**
411
+ * Generate .mcp.json with workspace-manager MCP server config.
412
+ * This ensures Claude Code sessions in the workspace have access to
413
+ * workspace-manager tools even without global MCP registration.
414
+ */
415
+ export async function generateMcpConfig(workspacePath: string): Promise<boolean> {
416
+ try {
417
+ const mcpFile = path.join(workspacePath, '.mcp.json');
418
+ const serverPath = resolveMcpServerPath();
419
+
420
+ if (!serverPath) {
421
+ logWarning('Could not resolve MCP server path — skipping .mcp.json generation');
422
+ return false;
423
+ }
424
+
425
+ let existingConfig: any = {};
426
+ try {
427
+ const existingContent = await fs.readFile(mcpFile, 'utf-8');
428
+ existingConfig = JSON.parse(existingContent);
429
+ } catch {
430
+ // No existing config or invalid JSON — start fresh
431
+ }
432
+
433
+ const mcpConfig = {
434
+ ...existingConfig,
435
+ mcpServers: {
436
+ ...(existingConfig.mcpServers || {}),
437
+ 'workspace-manager': {
438
+ command: 'node',
439
+ args: [serverPath],
440
+ },
441
+ },
442
+ };
443
+
444
+ await fs.writeFile(mcpFile, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf-8');
445
+ logSuccess(`Generated ${colorize('.mcp.json', 'cyan')} with workspace-manager MCP server`);
446
+ return true;
447
+ } catch (error) {
448
+ logWarning('Failed to generate .mcp.json file');
449
+ return false;
450
+ }
451
+ }
452
+
453
+ const KNOWN_PREFIXES = ['api', 'service', 'lib', 'tool', 'app', 'web', 'mobile', 'backend', 'frontend'];
454
+
455
+ /**
456
+ * How many directory levels below the repo root to scan for context files.
457
+ * 0 = repo root only (e.g. repo/AGENTS.md)
458
+ * 1 = one level deep (e.g. repo/packages/AGENTS.md)
459
+ * 2 = two levels deep (e.g. repo/packages/ui/AGENTS.md)
460
+ * Keep this low — deeper scans grow quickly and context files are rarely nested.
461
+ */
462
+ const PER_REPO_MAX_DEPTH = 2;
463
+
464
+ /**
465
+ * Directories that are never useful to scan for context files.
466
+ */
467
+ const SCAN_SKIP_DIRS = new Set([
468
+ 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',
469
+ 'coverage', '__pycache__', '.turbo', '.cache', 'out', 'tmp', '.tmp',
470
+ ]);
471
+
472
+ /**
473
+ * Recursively find context files within a directory, up to maxDepth levels
474
+ * below the starting point. Skips SCAN_SKIP_DIRS to avoid scanning build
475
+ * artifacts, dependencies, or git internals.
476
+ *
477
+ * @param dirPath Absolute path of the directory to scan.
478
+ * @param candidates File names to look for (from getAllKnownContextFileNames).
479
+ * @param repoRoot Absolute path of the repo root (used to build relative labels).
480
+ * @param maxDepth Maximum levels to descend from dirPath.
481
+ * @param depth Current recursion depth (0 = dirPath itself).
482
+ */
483
+ async function findContextFilesInDir(
484
+ dirPath: string,
485
+ candidates: string[],
486
+ repoRoot: string,
487
+ maxDepth: number,
488
+ depth: number = 0,
489
+ ): Promise<Array<{ relPath: string; fileName: string; content: string }>> {
490
+ const results: Array<{ relPath: string; fileName: string; content: string }> = [];
491
+
492
+ // Check for a context file at this level (first candidate that exists wins).
493
+ for (const fileName of candidates) {
494
+ let content: string;
495
+ try {
496
+ content = await fs.readFile(path.join(dirPath, fileName), 'utf-8');
497
+ } catch {
498
+ continue;
499
+ }
500
+ if (!content?.trim()) continue;
501
+
502
+ const relPath = path.relative(repoRoot, dirPath) || '.';
503
+ results.push({ relPath, fileName, content: content.trim() });
504
+ break; // one file per directory level
505
+ }
506
+
507
+ // Recurse into subdirectories if we haven't hit the depth limit.
508
+ if (depth < maxDepth) {
509
+ let entries: fsSync.Dirent[];
510
+ try {
511
+ entries = await fs.readdir(dirPath, { withFileTypes: true });
512
+ } catch {
513
+ return results;
514
+ }
515
+ for (const entry of entries) {
516
+ if (!entry.isDirectory() || SCAN_SKIP_DIRS.has(entry.name)) continue;
517
+ const sub = await findContextFilesInDir(
518
+ path.join(dirPath, entry.name),
519
+ candidates,
520
+ repoRoot,
521
+ maxDepth,
522
+ depth + 1,
523
+ );
524
+ results.push(...sub);
525
+ }
526
+ }
527
+
528
+ return results;
529
+ }
530
+
531
+ /**
532
+ * Scan each repo subdirectory for context files (AGENTS.md / .claude.md / …)
533
+ * and embed the contents inline in the workspace-level context file.
534
+ * Covers ALL known agents (not just the configured one) because individual
535
+ * repos may have been set up with a different agent.
536
+ *
537
+ * Scans up to PER_REPO_MAX_DEPTH levels inside each repo so monorepo
538
+ * packages are included, while still having a stop condition that prevents
539
+ * runaway recursion into large dependency trees.
540
+ *
541
+ * Returns a markdown section string, or null when no per-repo files exist.
542
+ */
543
+ export async function buildPerRepoContextSection(
544
+ workspacePath: string,
545
+ dirNames: string[],
546
+ ): Promise<string | null> {
547
+ const candidates = getAllKnownContextFileNames();
548
+ const sections: string[] = [];
549
+
550
+ for (const dir of dirNames) {
551
+ const repoRoot = path.join(workspacePath, dir);
552
+ const found = await findContextFilesInDir(
553
+ repoRoot, candidates, repoRoot, PER_REPO_MAX_DEPTH,
554
+ );
555
+
556
+ for (const { relPath, fileName, content } of found) {
557
+ const label = relPath === '.' ? dir : `${dir}/${relPath}`;
558
+ // Demote heading levels by 3 so the repo's content nests properly
559
+ // under '### `repo/` ...' — prevents repo's '## Architecture' from
560
+ // colliding with workspace headings like '## Notes'.
561
+ // Caps at h6 (markdown’s deepest level).
562
+ // Critically, only touch lines OUTSIDE fenced code blocks so that
563
+ // shell comments like '# Build the project' inside ```bash blocks
564
+ // are not corrupted.
565
+ const demoted = (() => {
566
+ const out: string[] = [];
567
+ let inFence = false;
568
+ let fenceMarker = '';
569
+ for (const line of content.split('\n')) {
570
+ const fenceMatch = line.match(/^(```+|~~~+)/);
571
+ if (fenceMatch) {
572
+ if (!inFence) {
573
+ inFence = true;
574
+ fenceMarker = fenceMatch[1];
575
+ } else if (line.startsWith(fenceMarker)) {
576
+ inFence = false;
577
+ fenceMarker = '';
578
+ }
579
+ out.push(line);
580
+ continue;
581
+ }
582
+ if (!inFence) {
583
+ const headingMatch = line.match(/^(#{1,6})(\s|$)/);
584
+ if (headingMatch) {
585
+ const newLevel = Math.min(6, headingMatch[1].length + 3);
586
+ out.push('#'.repeat(newLevel) + line.slice(headingMatch[1].length));
587
+ continue;
588
+ }
589
+ }
590
+ out.push(line);
591
+ }
592
+ return out.join('\n');
593
+ })();
594
+
595
+ sections.push(
596
+ `### \`${label}/\` — repo-specific rules (${fileName})\n\n` +
597
+ `**You MUST follow these rules whenever you work with files inside ` +
598
+ `\`${label}/\`. They override generic workspace guidance for that repo.**\n\n` +
599
+ demoted.trim() + '\n\n---\n',
600
+ );
601
+ }
602
+ }
603
+
604
+ if (sections.length === 0) return null;
605
+
606
+ return (
607
+ `## Per-Repository Context\n\n` +
608
+ `Each repository in this workspace below has its own \`AGENTS.md\` / ` +
609
+ `\`CLAUDE.md\` file. Their contents are embedded verbatim here.\n\n` +
610
+ `**When you work on files inside a specific repo subdirectory, the rules ` +
611
+ `in that repo’s section are AUTHORITATIVE for that repo — follow them ` +
612
+ `literally. They take precedence over generic workspace-level guidance.**\n\n` +
613
+ `---\n\n` +
614
+ sections.join('\n')
615
+ );
616
+ }
617
+
618
+
619
+ function getPrefixForName(name: string): string {
620
+ const parts = name.split('-');
621
+ if (parts.length > 1 && KNOWN_PREFIXES.includes(parts[0].toLowerCase())) {
622
+ return parts[0].toLowerCase();
623
+ }
624
+ return 'other';
625
+ }
626
+
627
+ /**
628
+ * Group repositories by common prefix
629
+ */
630
+ function groupReposByPrefix(repos: GitHubRepo[]): Record<string, GitHubRepo[]> {
631
+ const groups: Record<string, GitHubRepo[]> = {};
632
+ for (const repo of repos) {
633
+ const prefix = getPrefixForName(repo.name);
634
+ if (!groups[prefix]) groups[prefix] = [];
635
+ groups[prefix].push(repo);
636
+ }
637
+ return groups;
638
+ }
639
+
640
+ function groupEntriesByPrefix<T extends { name: string }>(entries: T[]): Record<string, T[]> {
641
+ const groups: Record<string, T[]> = {};
642
+ for (const entry of entries) {
643
+ const prefix = getPrefixForName(entry.name);
644
+ if (!groups[prefix]) groups[prefix] = [];
645
+ groups[prefix].push(entry);
646
+ }
647
+ return groups;
648
+ }
649
+