@mrpatronz/nexusflow 0.2.13 → 0.2.18

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 (228) hide show
  1. package/.agents/skills/ccc/SKILL.md +71 -0
  2. package/.agents/skills/ccc/references/management.md +110 -0
  3. package/.agents/skills/ccc/references/settings.md +126 -0
  4. package/.github/workflows/ci.yml +44 -0
  5. package/.github/workflows/dependabot-merge.yml +53 -0
  6. package/.github/workflows/release-desktop.yml +67 -0
  7. package/.github/workflows/release-npm.yml +65 -0
  8. package/.github/workflows/release-vscode.yml +61 -0
  9. package/GETTING_STARTED.md +5 -0
  10. package/README.md +53 -0
  11. package/desktop/build-installer.js +228 -0
  12. package/desktop/build.js +118 -0
  13. package/desktop/installer/nexusflow.iss +35 -0
  14. package/desktop/neutralino.config.json +39 -0
  15. package/desktop/package-lock.json +1060 -0
  16. package/desktop/package.json +14 -0
  17. package/dist/commands/adapter.d.ts +27 -0
  18. package/dist/commands/adapter.d.ts.map +1 -0
  19. package/dist/commands/adapter.js +318 -0
  20. package/dist/commands/adapter.js.map +1 -0
  21. package/dist/commands/add-repo.d.ts.map +1 -1
  22. package/dist/commands/add-repo.js +0 -1
  23. package/dist/commands/add-repo.js.map +1 -1
  24. package/dist/commands/commands.test.js +2 -2
  25. package/dist/commands/commands.test.js.map +1 -1
  26. package/dist/commands/config.d.ts +22 -0
  27. package/dist/commands/config.d.ts.map +1 -0
  28. package/dist/commands/config.js +50 -0
  29. package/dist/commands/config.js.map +1 -0
  30. package/dist/commands/create.d.ts.map +1 -1
  31. package/dist/commands/create.js +14 -17
  32. package/dist/commands/create.js.map +1 -1
  33. package/dist/commands/desktop.d.ts +12 -0
  34. package/dist/commands/desktop.d.ts.map +1 -0
  35. package/dist/commands/desktop.js +119 -0
  36. package/dist/commands/desktop.js.map +1 -0
  37. package/dist/commands/doctor.d.ts.map +1 -1
  38. package/dist/commands/doctor.js +11 -0
  39. package/dist/commands/doctor.js.map +1 -1
  40. package/dist/commands/refresh.d.ts +1 -0
  41. package/dist/commands/refresh.d.ts.map +1 -1
  42. package/dist/commands/refresh.js +2 -14
  43. package/dist/commands/refresh.js.map +1 -1
  44. package/dist/commands/sync.d.ts.map +1 -1
  45. package/dist/commands/sync.js +1 -8
  46. package/dist/commands/sync.js.map +1 -1
  47. package/dist/commands/tui.d.ts +8 -0
  48. package/dist/commands/tui.d.ts.map +1 -0
  49. package/dist/commands/tui.js +439 -0
  50. package/dist/commands/tui.js.map +1 -0
  51. package/dist/commands/ui.d.ts +3 -1
  52. package/dist/commands/ui.d.ts.map +1 -1
  53. package/dist/commands/ui.js +76 -19
  54. package/dist/commands/ui.js.map +1 -1
  55. package/dist/core/adapters/local-storage.d.ts +14 -0
  56. package/dist/core/adapters/local-storage.d.ts.map +1 -0
  57. package/dist/core/adapters/local-storage.js +58 -0
  58. package/dist/core/adapters/local-storage.js.map +1 -0
  59. package/dist/core/adapters/obsidian-storage.d.ts +28 -0
  60. package/dist/core/adapters/obsidian-storage.d.ts.map +1 -0
  61. package/dist/core/adapters/obsidian-storage.js +135 -0
  62. package/dist/core/adapters/obsidian-storage.js.map +1 -0
  63. package/dist/core/adapters/registry.d.ts +8 -0
  64. package/dist/core/adapters/registry.d.ts.map +1 -0
  65. package/dist/core/adapters/registry.js +37 -0
  66. package/dist/core/adapters/registry.js.map +1 -0
  67. package/dist/core/adapters/storage.test.d.ts +2 -0
  68. package/dist/core/adapters/storage.test.d.ts.map +1 -0
  69. package/dist/core/adapters/storage.test.js +106 -0
  70. package/dist/core/adapters/storage.test.js.map +1 -0
  71. package/dist/core/adapters/vault-storage.d.ts +16 -0
  72. package/dist/core/adapters/vault-storage.d.ts.map +1 -0
  73. package/dist/core/adapters/vault-storage.js +77 -0
  74. package/dist/core/adapters/vault-storage.js.map +1 -0
  75. package/dist/core/config.d.ts.map +1 -1
  76. package/dist/core/config.js +28 -4
  77. package/dist/core/config.js.map +1 -1
  78. package/dist/core/plugins/index.d.ts +14 -0
  79. package/dist/core/plugins/index.d.ts.map +1 -0
  80. package/dist/core/plugins/index.js +2 -0
  81. package/dist/core/plugins/index.js.map +1 -0
  82. package/dist/core/plugins/loader.d.ts +2 -0
  83. package/dist/core/plugins/loader.d.ts.map +1 -0
  84. package/dist/core/plugins/loader.js +34 -0
  85. package/dist/core/plugins/loader.js.map +1 -0
  86. package/dist/core/ports/storage.d.ts +59 -0
  87. package/dist/core/ports/storage.d.ts.map +1 -0
  88. package/dist/core/ports/storage.js +10 -0
  89. package/dist/core/ports/storage.js.map +1 -0
  90. package/dist/core/storage.d.ts +10 -0
  91. package/dist/core/storage.d.ts.map +1 -0
  92. package/dist/core/storage.js +29 -0
  93. package/dist/core/storage.js.map +1 -0
  94. package/dist/core/workspace.d.ts.map +1 -1
  95. package/dist/core/workspace.js +110 -16
  96. package/dist/core/workspace.js.map +1 -1
  97. package/dist/generators/base.d.ts.map +1 -1
  98. package/dist/generators/base.js +19 -6
  99. package/dist/generators/base.js.map +1 -1
  100. package/dist/generators/diff-context.d.ts +6 -0
  101. package/dist/generators/diff-context.d.ts.map +1 -0
  102. package/dist/generators/diff-context.js +68 -0
  103. package/dist/generators/diff-context.js.map +1 -0
  104. package/dist/generators/index.d.ts +1 -1
  105. package/dist/generators/index.d.ts.map +1 -1
  106. package/dist/generators/index.js +75 -18
  107. package/dist/generators/index.js.map +1 -1
  108. package/dist/generators/map-generator.d.ts.map +1 -1
  109. package/dist/generators/map-generator.js +7 -11
  110. package/dist/generators/map-generator.js.map +1 -1
  111. package/dist/generators/plan-generator.d.ts.map +1 -1
  112. package/dist/generators/plan-generator.js +3 -5
  113. package/dist/generators/plan-generator.js.map +1 -1
  114. package/dist/gui/app_logo.png +0 -0
  115. package/dist/gui/assets/index-CB-jWded.css +1 -0
  116. package/dist/gui/assets/index-DDXpZZu3.js +25 -0
  117. package/dist/gui/index.html +2 -2
  118. package/dist/index.js +174 -20
  119. package/dist/index.js.map +1 -1
  120. package/dist/server.d.ts.map +1 -1
  121. package/dist/server.js +212 -28
  122. package/dist/server.js.map +1 -1
  123. package/dist/server.test.js +173 -38
  124. package/dist/server.test.js.map +1 -1
  125. package/dist/types.d.ts +11 -2
  126. package/dist/types.d.ts.map +1 -1
  127. package/dist/utils/git.d.ts +16 -0
  128. package/dist/utils/git.d.ts.map +1 -1
  129. package/dist/utils/git.js +52 -0
  130. package/dist/utils/git.js.map +1 -1
  131. package/dist/utils/local-ai.d.ts.map +1 -1
  132. package/dist/utils/local-ai.js +7 -3
  133. package/dist/utils/local-ai.js.map +1 -1
  134. package/dist/utils/multi-git.d.ts +2 -0
  135. package/dist/utils/multi-git.d.ts.map +1 -1
  136. package/dist/utils/multi-git.js +5 -2
  137. package/dist/utils/multi-git.js.map +1 -1
  138. package/dist/utils/update-check.d.ts +3 -1
  139. package/dist/utils/update-check.d.ts.map +1 -1
  140. package/dist/utils/update-check.js +23 -34
  141. package/dist/utils/update-check.js.map +1 -1
  142. package/dist/utils/workflow-advisor.d.ts +16 -0
  143. package/dist/utils/workflow-advisor.d.ts.map +1 -0
  144. package/dist/utils/workflow-advisor.js +100 -0
  145. package/dist/utils/workflow-advisor.js.map +1 -0
  146. package/dist/utils/workflows.d.ts +11 -0
  147. package/dist/utils/workflows.d.ts.map +1 -0
  148. package/dist/utils/workflows.js +156 -0
  149. package/dist/utils/workflows.js.map +1 -0
  150. package/dist/utils/workflows.test.d.ts +2 -0
  151. package/dist/utils/workflows.test.d.ts.map +1 -0
  152. package/dist/utils/workflows.test.js +86 -0
  153. package/dist/utils/workflows.test.js.map +1 -0
  154. package/extension/media/icon.svg +1 -0
  155. package/extension/package-lock.json +4893 -1660
  156. package/extension/package.json +58 -11
  157. package/extension/src/extension.ts +476 -27
  158. package/extension/tsconfig.json +4 -2
  159. package/gui/e2e/wizard.spec.ts +19 -0
  160. package/gui/package-lock.json +342 -381
  161. package/gui/package.json +12 -12
  162. package/gui/public/app_logo.png +0 -0
  163. package/gui/src/App.tsx +991 -81
  164. package/gui/src/features/workspace/WorkspaceList.tsx +30 -30
  165. package/gui/src/index.css +64 -22
  166. package/gui/src/main.tsx +89 -5
  167. package/package.json +11 -12
  168. package/resources/workflows/plan-implement-review.md +8 -0
  169. package/resources/workflows/research-verify.md +6 -0
  170. package/resources/workflows/solo-developer.md +3 -0
  171. package/skills-lock.json +11 -0
  172. package/src/commands/adapter.ts +348 -0
  173. package/src/commands/add-repo.ts +0 -1
  174. package/src/commands/commands.test.ts +3 -2
  175. package/src/commands/config.ts +52 -0
  176. package/src/commands/create.ts +14 -17
  177. package/src/commands/desktop.ts +128 -0
  178. package/src/commands/doctor.ts +13 -0
  179. package/src/commands/mcp.ts +2 -0
  180. package/src/commands/refresh.ts +4 -14
  181. package/src/commands/sync.ts +2 -8
  182. package/src/commands/tui.ts +484 -0
  183. package/src/commands/ui.ts +83 -19
  184. package/src/core/adapters/local-storage.ts +66 -0
  185. package/src/core/adapters/obsidian-storage.ts +151 -0
  186. package/src/core/adapters/registry.ts +44 -0
  187. package/src/core/adapters/storage.test.ts +174 -0
  188. package/src/core/adapters/vault-storage.ts +86 -0
  189. package/src/core/config.ts +29 -4
  190. package/src/core/plugins/index.ts +17 -0
  191. package/src/core/plugins/loader.ts +36 -0
  192. package/src/core/ports/storage.ts +71 -0
  193. package/src/core/storage.ts +37 -0
  194. package/src/core/workspace.ts +122 -15
  195. package/src/generators/base.ts +21 -5
  196. package/src/generators/diff-context.ts +75 -0
  197. package/src/generators/index.ts +93 -21
  198. package/src/generators/map-generator.ts +7 -10
  199. package/src/generators/plan-generator.ts +6 -4
  200. package/src/index.ts +178 -21
  201. package/src/server.test.ts +193 -39
  202. package/src/server.ts +238 -28
  203. package/src/types.ts +14 -2
  204. package/src/utils/git.ts +56 -0
  205. package/src/utils/local-ai.ts +8 -3
  206. package/src/utils/multi-git.ts +16 -9
  207. package/src/utils/update-check.ts +42 -31
  208. package/src/utils/workflow-advisor.ts +118 -0
  209. package/src/utils/workflows.test.ts +114 -0
  210. package/src/utils/workflows.ts +178 -0
  211. package/.github/workflows/release.yml +0 -100
  212. package/dist/commands/pack.d.ts +0 -7
  213. package/dist/commands/pack.d.ts.map +0 -1
  214. package/dist/commands/pack.js +0 -64
  215. package/dist/commands/pack.js.map +0 -1
  216. package/dist/core/packer.d.ts +0 -14
  217. package/dist/core/packer.d.ts.map +0 -1
  218. package/dist/core/packer.js +0 -87
  219. package/dist/core/packer.js.map +0 -1
  220. package/dist/core/packer.test.d.ts +0 -2
  221. package/dist/core/packer.test.d.ts.map +0 -1
  222. package/dist/core/packer.test.js +0 -82
  223. package/dist/core/packer.test.js.map +0 -1
  224. package/dist/gui/assets/index-CFWwqFux.js +0 -23
  225. package/dist/gui/assets/index-Dvc9QMvl.css +0 -2
  226. package/src/commands/pack.ts +0 -73
  227. package/src/core/packer.test.ts +0 -99
  228. package/src/core/packer.ts +0 -107
package/src/server.ts CHANGED
@@ -9,16 +9,20 @@ import { serveStatic } from '@hono/node-server/serve-static';
9
9
  import { cors } from 'hono/cors';
10
10
  import { streamSSE } from 'hono/streaming';
11
11
  import * as fs from 'node:fs/promises';
12
+ import { createWriteStream, existsSync } from 'node:fs';
12
13
  import * as path from 'node:path';
13
14
  import { fileURLToPath } from 'node:url';
14
15
  import { execa } from 'execa';
16
+ import * as os from 'node:os';
17
+ import { pipeline } from 'node:stream/promises';
18
+ import { spawn } from 'node:child_process';
15
19
 
16
20
  import { loadConfig, saveConfig, getConfigDir } from './core/config.js';
21
+ import { listStorageProviders } from './core/adapters/registry.js';
17
22
  import { scanForRepos } from './core/scanner.js';
18
23
  import { createWorkspace, listWorkspaces, loadFeatureConfig, deleteWorkspace, addRepoToWorkspace } from './core/workspace.js';
19
24
  import { analyzeAllRepos } from './analyzers/index.js';
20
25
  import { generateContextFiles } from './generators/index.js';
21
- import { packWorkspace } from './core/packer.js';
22
26
  import { isOllamaModelAvailable, getOpenAiCompatibleUrl, callLocalLlm } from './utils/local-ai.js';
23
27
  import { detectAIAssistants } from './utils/detect-ai.js';
24
28
  import { detectEditors } from './utils/detect-editors.js';
@@ -33,7 +37,9 @@ import {
33
37
  loadRunningState,
34
38
  } from './orchestration/index.js';
35
39
  import { checkForUpdates, getCurrentVersion, getToolsStatus } from './utils/update-check.js';
40
+ import { getWorkflowTemplates, saveWorkflowTemplate, deleteWorkflowTemplate } from './utils/workflows.js';
36
41
  import type { Feature, RepoInfo, WorkspaceContext } from './types.js';
42
+ import { suggestWorkflow } from './utils/workflow-advisor.js';
37
43
 
38
44
  // Resolve static files directory
39
45
  const __filename = fileURLToPath(import.meta.url);
@@ -71,10 +77,24 @@ app.get('/api/config', async (c) => {
71
77
  }
72
78
  });
73
79
 
80
+ // Get all registered storage adapters
81
+ app.get('/api/adapters', async (c) => {
82
+ try {
83
+ const adapters = listStorageProviders();
84
+ return c.json({ adapters });
85
+ } catch (error) {
86
+ const msg = error instanceof Error ? error.message : String(error);
87
+ return c.json({ error: msg }, 500);
88
+ }
89
+ });
90
+
74
91
  function isSafeLocalEndpoint(urlStr: string): boolean {
75
92
  try {
76
93
  const url = new URL(urlStr);
77
94
  const hostname = url.hostname.toLowerCase();
95
+ if (url.protocol === 'https:') {
96
+ return true;
97
+ }
78
98
  if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') {
79
99
  return true;
80
100
  }
@@ -94,7 +114,7 @@ app.post('/api/config', async (c) => {
94
114
  const newConfig = await c.req.json();
95
115
  if (newConfig?.localLlm?.enabled && newConfig.localLlm.endpoint) {
96
116
  if (!isSafeLocalEndpoint(newConfig.localLlm.endpoint)) {
97
- return c.json({ error: 'Local AI endpoint must be localhost, 127.0.0.1, or a private LAN IP.' }, 400);
117
+ return c.json({ error: 'Local AI endpoint must be HTTPS, localhost, 127.0.0.1, or a private LAN IP.' }, 400);
98
118
  }
99
119
  }
100
120
  await saveConfig(newConfig);
@@ -154,15 +174,15 @@ app.get('/api/editor-detect', async (c) => {
154
174
  // 6.5. Local LLM test & recommendation
155
175
  app.post('/api/local-llm/test', async (c) => {
156
176
  try {
157
- const { provider, endpoint, model, shoot } = await c.req.json();
177
+ const { provider, endpoint, model, apiKey, shoot } = await c.req.json();
158
178
  if (!endpoint || !isSafeLocalEndpoint(endpoint)) {
159
- return c.json({ success: false, error: 'Local AI endpoint must be localhost, 127.0.0.1, or a private LAN IP.' }, 400);
179
+ return c.json({ success: false, error: 'Local AI endpoint must be HTTPS, localhost, 127.0.0.1, or a private LAN IP.' }, 400);
160
180
  }
161
181
  const cleanEndpoint = endpoint.replace(/\/$/, '');
162
182
 
163
183
  if (shoot) {
164
184
  const responseText = await callLocalLlm(
165
- { enabled: true, provider, endpoint, model },
185
+ { enabled: true, provider, endpoint, model, apiKey },
166
186
  [{ role: 'user', content: 'Respond with the exact word "OK" and nothing else.' }]
167
187
  );
168
188
  const cleanResponse = responseText.trim();
@@ -298,6 +318,7 @@ async function runCreationJob(jobId: string, body: any, config: any) {
298
318
  createdAt: new Date().toISOString(),
299
319
  resumption: body.resumption,
300
320
  localLlmEnabled: body.localLlmEnabled,
321
+ teamworkInstructions: body.teamworkInstructions,
301
322
  };
302
323
  if (job) {
303
324
  job.feature = feature;
@@ -328,12 +349,7 @@ async function runCreationJob(jobId: string, body: any, config: any) {
328
349
  await generateContextFiles(ctx, body.assistants, workspacePath);
329
350
  updateJobStep(jobId, 'context', 'completed', 'AI context files generated.');
330
351
 
331
- // Step 4: Pack codebase context
332
- if (config.packContextXml) {
333
- updateJobStep(jobId, 'pack', 'running', 'Packing codebase context with Repomix...');
334
- const packResult = await packWorkspace(workspacePath);
335
- updateJobStep(jobId, 'pack', 'completed', `Packed codebase context (${packResult.totalFiles} files, ${(packResult.fileSize / 1024).toFixed(2)} KB).`);
336
- }
352
+ // XML context packing removed.
337
353
 
338
354
  } catch (error) {
339
355
  const msg = error instanceof Error ? error.message : String(error);
@@ -355,6 +371,7 @@ app.post('/api/workspace', async (c) => {
355
371
  repos: RepoInfo[];
356
372
  assistants: any[];
357
373
  localLlmEnabled?: boolean;
374
+ teamworkInstructions?: string;
358
375
  resumption?: {
359
376
  testCommand?: string;
360
377
  mockCommand?: string;
@@ -377,9 +394,7 @@ app.post('/api/workspace', async (c) => {
377
394
  { id: 'analysis', name: 'Analyze Repositories', status: 'pending', message: 'Waiting...' },
378
395
  { id: 'context', name: 'Generate AI Context Files', status: 'pending', message: 'Waiting...' },
379
396
  ];
380
- if (config.packContextXml) {
381
- steps.push({ id: 'pack', name: 'Pack Codebase Context', status: 'pending', message: 'Waiting...' });
382
- }
397
+ // XML context packing removed.
383
398
 
384
399
  const job: CreationJob = {
385
400
  id: jobId,
@@ -484,6 +499,27 @@ app.post('/api/workspace/:id/repo', async (c) => {
484
499
  }
485
500
  });
486
501
 
502
+ // 7.8. Suggest workflow strategy based on feature description & selected repos
503
+ app.post('/api/workspace/suggest-workflow', async (c) => {
504
+ try {
505
+ const { description, repos } = await c.req.json() as {
506
+ description: string;
507
+ repos: RepoInfo[];
508
+ };
509
+
510
+ const config = await loadConfig();
511
+ const suggestion = await suggestWorkflow(description, repos, config.localLlm);
512
+
513
+ return c.json({
514
+ success: true,
515
+ ...suggestion
516
+ });
517
+ } catch (error) {
518
+ const msg = error instanceof Error ? error.message : String(error);
519
+ return c.json({ error: msg }, 500);
520
+ }
521
+ });
522
+
487
523
  // 8. Open workspace in editor
488
524
  app.post('/api/open-editor', async (c) => {
489
525
  try {
@@ -829,7 +865,7 @@ app.post('/api/workspace/:id/sync', async (c) => {
829
865
  const results = [];
830
866
 
831
867
  for (const repo of repos) {
832
- const result = await rebaseRepo(repo.path, 'main');
868
+ const result = await rebaseRepo(repo.path, repo.defaultBranch || 'main');
833
869
  results.push({
834
870
  repoName: repo.name,
835
871
  success: result.success,
@@ -1025,7 +1061,6 @@ app.post('/api/updates/install', async (c) => {
1025
1061
  const { toolId } = await c.req.json() as { toolId: string };
1026
1062
  const tools = [
1027
1063
  { id: 'nexusflow', cmd: 'npm', args: ['install', '-g', '@mrpatronz/nexusflow'] },
1028
- { id: 'repomix', cmd: 'npm', args: ['install', '-g', 'repomix'] },
1029
1064
  { id: 'antigravity', cmd: 'agy', args: ['update'] },
1030
1065
  { id: 'claude', cmd: 'npm', args: ['install', '-g', '@anthropic-ai/claude-code'] },
1031
1066
  ];
@@ -1050,30 +1085,205 @@ app.post('/api/updates/install', async (c) => {
1050
1085
  }
1051
1086
  });
1052
1087
 
1053
- // 18. Pack workspace codebase and download
1054
- app.get('/api/workspace/:id/pack', async (c) => {
1088
+ let downloadedInstallerPath: string | null = null;
1089
+
1090
+ // 17.7. Download matching GitHub Release installer to temporary folder
1091
+ app.post('/api/updates/download', async (c) => {
1092
+ try {
1093
+ const { downloadUrl } = await c.req.json() as { downloadUrl: string };
1094
+ if (!downloadUrl) {
1095
+ return c.json({ error: 'Download URL is required' }, 400);
1096
+ }
1097
+
1098
+ const tempDir = os.tmpdir();
1099
+ const fileName = 'NexusFlowSetup_Update.exe';
1100
+ const targetPath = path.join(tempDir, fileName);
1101
+
1102
+ const response = await fetch(downloadUrl);
1103
+ if (!response.ok) {
1104
+ throw new Error(`HTTP error ${response.status}`);
1105
+ }
1106
+
1107
+ // Node fetch body stream download
1108
+ const fileStream = createWriteStream(targetPath);
1109
+ if (!response.body) {
1110
+ throw new Error('Response body is null');
1111
+ }
1112
+
1113
+ // Convert ReadableStream to Node stream
1114
+ await pipeline(response.body as any, fileStream);
1115
+
1116
+ downloadedInstallerPath = targetPath;
1117
+ return c.json({ success: true, path: targetPath });
1118
+ } catch (error) {
1119
+ const msg = error instanceof Error ? error.message : String(error);
1120
+ return c.json({ error: `Download failed: ${msg}` }, 500);
1121
+ }
1122
+ });
1123
+
1124
+ // 17.8. Launch the silent installer detached and exit server
1125
+ app.post('/api/updates/apply', async (c) => {
1126
+ if (!downloadedInstallerPath || !existsSync(downloadedInstallerPath)) {
1127
+ return c.json({ error: 'No downloaded installer found on disk' }, 400);
1128
+ }
1129
+
1130
+ try {
1131
+ const isWin = process.platform === 'win32';
1132
+ if (isWin) {
1133
+ // Inno Setup silent install flags
1134
+ console.log(`Applying update: Spawning detached installer at: ${downloadedInstallerPath}`);
1135
+ const child = spawn(downloadedInstallerPath, ['/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART'], {
1136
+ detached: true,
1137
+ stdio: 'ignore',
1138
+ });
1139
+ child.unref();
1140
+ }
1141
+
1142
+ // Gracefully exit server process in 1 second to release file locks
1143
+ setTimeout(() => {
1144
+ console.log('Update installer successfully spawned. Exiting Hono server process...');
1145
+ process.exit(0);
1146
+ }, 1000);
1147
+
1148
+ return c.json({ success: true, message: 'Installer spawned, app shutting down...' });
1149
+ } catch (error) {
1150
+ const msg = error instanceof Error ? error.message : String(error);
1151
+ return c.json({ error: `Failed to execute update: ${msg}` }, 500);
1152
+ }
1153
+ });
1154
+
1155
+ // 17.9. Get available workflow templates
1156
+ app.get('/api/workflows/templates', async (c) => {
1157
+ try {
1158
+ const templates = await getWorkflowTemplates();
1159
+ return c.json({ templates });
1160
+ } catch (error) {
1161
+ const msg = error instanceof Error ? error.message : String(error);
1162
+ return c.json({ error: msg }, 500);
1163
+ }
1164
+ });
1165
+
1166
+ // Save or update custom teamwork template
1167
+ app.post('/api/workflows/templates', async (c) => {
1168
+ try {
1169
+ const { id, name, content } = await c.req.json();
1170
+ if (!name || !content) {
1171
+ return c.json({ error: 'Name and content are required.' }, 400);
1172
+ }
1173
+ const template = await saveWorkflowTemplate(name, content, id);
1174
+ return c.json({ success: true, template });
1175
+ } catch (error) {
1176
+ const msg = error instanceof Error ? error.message : String(error);
1177
+ return c.json({ error: msg }, 500);
1178
+ }
1179
+ });
1180
+
1181
+ // Delete custom teamwork template
1182
+ app.delete('/api/workflows/templates/:id', async (c) => {
1055
1183
  try {
1056
1184
  const id = decodeURIComponent(c.req.param('id'));
1057
- const config = await loadConfig();
1058
- const workspacePath = path.join(config.workspacesDir, id);
1185
+ const templates = await getWorkflowTemplates();
1186
+ const target = templates.find(t => t.id === id);
1187
+ if (!target) {
1188
+ return c.json({ error: 'Template not found.' }, 404);
1189
+ }
1190
+ if (!target.custom) {
1191
+ return c.json({ error: 'Cannot delete built-in templates.' }, 403);
1192
+ }
1193
+ await deleteWorkflowTemplate(id);
1194
+ return c.json({ success: true });
1195
+ } catch (error) {
1196
+ const msg = error instanceof Error ? error.message : String(error);
1197
+ return c.json({ error: msg }, 500);
1198
+ }
1199
+ });
1059
1200
 
1060
- const feature = await loadFeatureConfig(workspacePath);
1061
- if (!feature) {
1062
- return c.json({ error: 'Workspace configuration not found.' }, 404);
1201
+ // Analyze teamwork template rules via selected AI coding assistant harness
1202
+ app.post('/api/workflows/templates/:id/analyze', async (c) => {
1203
+ try {
1204
+ const { content, assistant, comment } = await c.req.json();
1205
+ if (!content) {
1206
+ return c.json({ error: 'Content is required.' }, 400);
1207
+ }
1208
+
1209
+ const selectedAssistant = assistant || 'antigravity';
1210
+ let command = '';
1211
+ let args: string[] = [];
1212
+
1213
+ let prompt = `You are an expert AI system engineering reviewer. Analyze the following Agent Teamwork Strategy guidelines.
1214
+ Evaluate its instructions, identify any ambiguities or contradictions, rate its expected effectiveness for orchestrating subagents, and provide specific recommendations or improvements. Format your analysis in clean Markdown with clear headings (e.g. Overview, Strengths, Weaknesses, Recommendations).
1215
+
1216
+ After your analysis, provide a fully rewritten, optimized, and complete version of the strategy guidelines incorporating all your recommendations. This rewritten version must be suitable for production orchestration.
1217
+ You MUST prefix the rewritten version with the exact delimiter line:
1218
+ === SUGGESTED IMPROVEMENT START ===
1219
+ and suffix it with:
1220
+ === SUGGESTED IMPROVEMENT END ===`;
1221
+
1222
+ if (comment && comment.trim()) {
1223
+ prompt += `\n\nIMPORTANT: The user has provided the following specific instruction/comment that you MUST consider and prioritize during your evaluation and when rewriting the guidelines:\n"${comment.trim()}"`;
1224
+ }
1225
+
1226
+ prompt += `\n\n--- GUIDELINES START ---\n${content}\n--- GUIDELINES END ---`;
1227
+
1228
+ const assistants = await detectAIAssistants();
1229
+ const target = assistants.find(ai => ai.name === selectedAssistant);
1230
+
1231
+ if (!target || !target.detected || !target.command) {
1232
+ return c.json({
1233
+ error: `AI assistant harness '${selectedAssistant}' is not detected or does not support command-line execution.`
1234
+ }, 400);
1235
+ }
1236
+
1237
+ command = target.command;
1238
+ if (command === 'claude') {
1239
+ args = ['-p', prompt];
1240
+ } else if (command === 'agy') {
1241
+ args = [prompt];
1242
+ } else {
1243
+ args = [prompt];
1244
+ }
1245
+
1246
+ const result = await execa(command, args, {
1247
+ input: '',
1248
+ shell: false,
1249
+ reject: false
1250
+ });
1251
+
1252
+ if (result.exitCode !== 0) {
1253
+ return c.json({
1254
+ error: `AI Assistant harness execution failed (exit code ${result.exitCode}): ${result.stderr || result.stdout || 'Unknown error'}`
1255
+ }, 500);
1063
1256
  }
1064
1257
 
1065
- const result = await packWorkspace(workspacePath);
1066
- const content = await fs.readFile(result.outputPath, 'utf-8');
1258
+ // Clean potential warning lines or stdout prefixes if present
1259
+ let cleanText = result.stdout;
1260
+ if (cleanText.includes('Warning: no stdin data received')) {
1261
+ cleanText = cleanText.replace(/Warning: no stdin data received in \d+s, proceeding without it\. If piping from a slow command, redirect stdin explicitly: < \/dev\/null to skip, or wait longer\.\r?\n?/, '');
1262
+ }
1263
+
1264
+ let analysis = cleanText.trim();
1265
+ let suggestedImprovement = '';
1266
+
1267
+ const startDelimiter = '=== SUGGESTED IMPROVEMENT START ===';
1268
+ const endDelimiter = '=== SUGGESTED IMPROVEMENT END ===';
1269
+
1270
+ const startIdx = cleanText.indexOf(startDelimiter);
1271
+ const endIdx = cleanText.indexOf(endDelimiter);
1067
1272
 
1068
- c.header('Content-Disposition', `attachment; filename="nexusflow-context-${id.replace(/[\/\\ ]/g, '-')}.xml"`);
1069
- c.header('Content-Type', 'application/xml');
1070
- return c.text(content);
1273
+ if (startIdx !== -1 && endIdx !== -1) {
1274
+ analysis = cleanText.substring(0, startIdx).trim();
1275
+ suggestedImprovement = cleanText.substring(startIdx + startDelimiter.length, endIdx).trim();
1276
+ }
1277
+
1278
+ return c.json({ analysis, suggestedImprovement });
1071
1279
  } catch (error) {
1072
1280
  const msg = error instanceof Error ? error.message : String(error);
1073
1281
  return c.json({ error: msg }, 500);
1074
1282
  }
1075
1283
  });
1076
1284
 
1285
+ // Legacy pack endpoint removed.
1286
+
1077
1287
  // Serve index.html explicitly on root endpoint
1078
1288
  app.get('/', async (c) => {
1079
1289
  try {
package/src/types.ts CHANGED
@@ -11,6 +11,7 @@ export interface LocalLlmConfig {
11
11
  provider: 'ollama' | 'openai-compatible';
12
12
  endpoint: string;
13
13
  model: string;
14
+ apiKey?: string;
14
15
  }
15
16
 
16
17
  /** Top-level NexusFlow configuration stored in ~/.nexusflow/config.json. */
@@ -36,14 +37,22 @@ export interface NexusFlowConfig {
36
37
  /** Global patterns to exclude when packing/analyzing repositories. */
37
38
  excludePatterns?: string[];
38
39
 
39
- /** Whether to pack codebase context into XML format. Default: true */
40
- packContextXml?: boolean;
40
+ /** Active storage provider name. Default: 'local' */
41
+ storageProvider?: string;
42
+
43
+ /** Per-adapter settings keyed by adapter name. */
44
+ adapterConfig?: Record<string, Record<string, unknown>>;
45
+
46
+ /** List of plugin paths or npm packages to load. */
47
+ plugins?: string[];
41
48
 
42
49
  /** ISO timestamp of the last update check. */
43
50
  lastUpdateCheck?: string;
44
51
 
45
52
  /** The last checked latest version from NPM. */
46
53
  latestVersion?: string;
54
+ latestDownloadUrl?: string | null;
55
+ latestReleaseNotes?: string;
47
56
 
48
57
  /** Local LLM settings for delegating simple tasks. */
49
58
  localLlm?: LocalLlmConfig;
@@ -148,6 +157,9 @@ export interface Feature {
148
157
 
149
158
  /** Whether the Local LLM Co-processor is active for this workspace. */
150
159
  localLlmEnabled?: boolean;
160
+
161
+ /** Custom teamwork coordination instructions for the agent team. */
162
+ teamworkInstructions?: string;
151
163
  }
152
164
 
153
165
  /** Runtime context for an active workspace — now includes analysis data. */
package/src/utils/git.ts CHANGED
@@ -59,3 +59,59 @@ export async function detectDefaultBranch(repoPath: string): Promise<string> {
59
59
  return 'main';
60
60
  }
61
61
  }
62
+
63
+ /**
64
+ * Checks if the given branch name matches Git branch conventions (based on git-check-ref-format):
65
+ * - No spaces, backslashes, or control characters.
66
+ * - At least 1 character long.
67
+ * - Is not exactly '@'.
68
+ * - Does not start with a dash.
69
+ * - Does not start or end with a forward slash.
70
+ * - Does not contain consecutive dots, consecutive slashes, or '@{'.
71
+ * - Does not end with '.'.
72
+ * - Does not contain forbidden characters: ~, ^, :, ?, *, [
73
+ * - No component starts with a dot, ends with '.lock', or is exactly 'HEAD'.
74
+ *
75
+ * @param name - The branch name to validate.
76
+ * @returns `true` if the branch name is valid.
77
+ */
78
+ export function isValidBranchName(name: string): boolean {
79
+ if (!name || name.length === 0) {
80
+ return false;
81
+ }
82
+ if (name === '@') {
83
+ return false;
84
+ }
85
+ if (name.startsWith('-')) {
86
+ return false;
87
+ }
88
+ const forbiddenCharsOrSpaces = /[\x00-\x20\x7F~^:?*[\\]/;
89
+ if (forbiddenCharsOrSpaces.test(name)) {
90
+ return false;
91
+ }
92
+ if (name.includes('..') || name.includes('@{') || name.includes('//')) {
93
+ return false;
94
+ }
95
+ if (name.startsWith('/') || name.endsWith('/')) {
96
+ return false;
97
+ }
98
+ if (name.endsWith('.')) {
99
+ return false;
100
+ }
101
+
102
+ const components = name.split('/');
103
+ for (const component of components) {
104
+ if (component.startsWith('.')) {
105
+ return false;
106
+ }
107
+ if (component.endsWith('.lock')) {
108
+ return false;
109
+ }
110
+ if (component === 'HEAD') {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ return true;
116
+ }
117
+
@@ -53,11 +53,16 @@ export async function callLocalLlm(
53
53
  // OpenAI-compatible provider
54
54
  const url = getOpenAiCompatibleUrl(cleanEndpoint, '/v1/chat/completions');
55
55
 
56
+ const headers: Record<string, string> = {
57
+ 'Content-Type': 'application/json',
58
+ };
59
+ if (config.apiKey) {
60
+ headers['Authorization'] = `Bearer ${config.apiKey}`;
61
+ }
62
+
56
63
  const response = await fetch(url, {
57
64
  method: 'POST',
58
- headers: {
59
- 'Content-Type': 'application/json',
60
- },
65
+ headers,
61
66
  body: JSON.stringify({
62
67
  model,
63
68
  messages,
@@ -11,6 +11,7 @@ import * as path from 'node:path';
11
11
  import { execa } from 'execa';
12
12
 
13
13
  import type { Feature } from '../types.js';
14
+ import { detectDefaultBranch } from './git.js';
14
15
 
15
16
  // ─── Types ────────────────────────────────────────────────────────────────────
16
17
 
@@ -22,6 +23,8 @@ export interface WorkspaceRepo {
22
23
  path: string;
23
24
  /** Feature branch name shared across the workspace. */
24
25
  branchName: string;
26
+ /** Default branch of the repo (e.g. 'main' or 'master'). */
27
+ defaultBranch: string;
25
28
  }
26
29
 
27
30
  /** Result of inspecting the working-tree status of a repo. */
@@ -109,15 +112,19 @@ export async function getWorkspaceRepos(
109
112
  const raw = await fs.readFile(manifestPath, 'utf-8');
110
113
  const feature = JSON.parse(raw) as Feature;
111
114
 
112
- return feature.repos.map((repoPath) => {
113
- const name = path.basename(repoPath);
114
- const absolutePath = path.resolve(workspacePath, name);
115
- return {
116
- name,
117
- path: absolutePath,
118
- branchName: feature.branchName,
119
- };
120
- });
115
+ return Promise.all(
116
+ feature.repos.map(async (repoPath) => {
117
+ const name = path.basename(repoPath);
118
+ const absolutePath = path.resolve(workspacePath, name);
119
+ const defaultBranch = await detectDefaultBranch(absolutePath);
120
+ return {
121
+ name,
122
+ path: absolutePath,
123
+ branchName: feature.branchName,
124
+ defaultBranch,
125
+ };
126
+ })
127
+ );
121
128
  }
122
129
 
123
130
  /**
@@ -14,6 +14,21 @@ export interface UpdateStatus {
14
14
  currentVersion: string;
15
15
  latestVersion: string;
16
16
  updateAvailable: boolean;
17
+ downloadUrl?: string | null;
18
+ releaseNotes?: string;
19
+ }
20
+
21
+ interface GitHubAsset {
22
+ name: string;
23
+ browser_download_url: string;
24
+ size: number;
25
+ }
26
+
27
+ interface GitHubRelease {
28
+ tag_name: string;
29
+ html_url: string;
30
+ body: string;
31
+ assets: GitHubAsset[];
17
32
  }
18
33
 
19
34
  /**
@@ -43,7 +58,7 @@ export function getCurrentVersion(): string {
43
58
  }
44
59
 
45
60
  /**
46
- * Checks for updates from the NPM registry.
61
+ * Checks for updates from the GitHub Releases API (antan87/NexusFlow).
47
62
  * Employs a 24-hour cache unless forced.
48
63
  *
49
64
  * @param force - If true, ignores the 24-hour cache and fetches immediately.
@@ -63,31 +78,48 @@ export async function checkForUpdates(force = false): Promise<UpdateStatus | nul
63
78
  currentVersion,
64
79
  latestVersion: config.latestVersion,
65
80
  updateAvailable: isNewerVersion(currentVersion, config.latestVersion),
81
+ downloadUrl: config.latestDownloadUrl,
82
+ releaseNotes: config.latestReleaseNotes,
66
83
  };
67
84
  }
68
85
 
69
86
  try {
70
- // Query NPM registry latest endpoint
71
- const response = await fetch('https://registry.npmjs.org/@mrpatronz/nexusflow/latest', {
72
- signal: AbortSignal.timeout(3000), // 3-second timeout to prevent blocking CLI/server
87
+ const response = await fetch('https://api.github.com/repos/antan87/NexusFlow/releases/latest', {
88
+ headers: { 'User-Agent': 'NexusFlow-Updater' },
89
+ signal: AbortSignal.timeout(4000),
73
90
  });
74
91
 
75
92
  if (!response.ok) {
76
- throw new Error(`NPM registry returned status ${response.status}`);
93
+ throw new Error(`GitHub API returned status ${response.status}`);
94
+ }
95
+
96
+ const data = await response.json() as GitHubRelease;
97
+ const latestVersion = data.tag_name.replace(/^v/, '');
98
+
99
+ // Resolve platform-matching asset (e.g. Setup.exe for Windows)
100
+ let downloadUrl: string | null = null;
101
+ const targetAsset = data.assets.find(asset => {
102
+ return asset.name.endsWith('.exe') && asset.name.toLowerCase().includes('setup');
103
+ });
104
+ if (targetAsset) {
105
+ downloadUrl = targetAsset.browser_download_url;
77
106
  }
78
107
 
79
- const data = await response.json() as { version: string };
80
- const latestVersion = data.version;
108
+ const releaseNotes = data.body || '';
81
109
 
82
110
  // Cache results
83
111
  config.lastUpdateCheck = new Date().toISOString();
84
112
  config.latestVersion = latestVersion;
113
+ config.latestDownloadUrl = downloadUrl;
114
+ config.latestReleaseNotes = releaseNotes;
85
115
  await saveConfig(config);
86
116
 
87
117
  return {
88
118
  currentVersion,
89
119
  latestVersion,
90
120
  updateAvailable: isNewerVersion(currentVersion, latestVersion),
121
+ downloadUrl,
122
+ releaseNotes,
91
123
  };
92
124
  } catch {
93
125
  // Fallback: If offline or check fails, return cached version status if available
@@ -96,6 +128,8 @@ export async function checkForUpdates(force = false): Promise<UpdateStatus | nul
96
128
  currentVersion,
97
129
  latestVersion: config.latestVersion,
98
130
  updateAvailable: isNewerVersion(currentVersion, config.latestVersion),
131
+ downloadUrl: config.latestDownloadUrl,
132
+ releaseNotes: config.latestReleaseNotes,
99
133
  };
100
134
  }
101
135
  return null;
@@ -170,30 +204,7 @@ export async function getToolsStatus(force = false): Promise<ToolUpdateStatus[]>
170
204
  updateCmd: 'npm install -g @mrpatronz/nexusflow',
171
205
  getCurrent: async () => currentVersion,
172
206
  },
173
- {
174
- id: 'repomix',
175
- name: 'Repomix (Codebase Packer)',
176
- command: 'repomix',
177
- npmPackage: 'repomix',
178
- updateCmd: 'npm install -g repomix',
179
- getCurrent: async () => {
180
- try {
181
- const res = await execa('repomix', ['--version'], {
182
- reject: false,
183
- shell: process.platform === 'win32',
184
- });
185
- if (res.exitCode === 0) return res.stdout.trim();
186
- } catch {}
187
- try {
188
- const res = await execa('npx', ['repomix', '--version'], {
189
- reject: false,
190
- shell: process.platform === 'win32',
191
- });
192
- if (res.exitCode === 0) return res.stdout.trim();
193
- } catch {}
194
- return '';
195
- }
196
- },
207
+
197
208
  {
198
209
  id: 'antigravity',
199
210
  name: 'Antigravity CLI',