@akon-labs/nexus-cli 0.1.10 → 0.1.11

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 (127) hide show
  1. package/dist/cli/require-login.d.ts.map +1 -1
  2. package/dist/cli/require-login.js +2 -2
  3. package/dist/cli/require-login.js.map +1 -1
  4. package/dist/cli.js +2492 -1088
  5. package/dist/core/agent-session.d.ts +3 -1
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +4 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/extensions/index.d.ts +1 -1
  10. package/dist/core/extensions/index.d.ts.map +1 -1
  11. package/dist/core/extensions/index.js.map +1 -1
  12. package/dist/core/extensions/loader.d.ts.map +1 -1
  13. package/dist/core/extensions/loader.js +6 -0
  14. package/dist/core/extensions/loader.js.map +1 -1
  15. package/dist/core/extensions/runner.d.ts +7 -1
  16. package/dist/core/extensions/runner.d.ts.map +1 -1
  17. package/dist/core/extensions/runner.js +14 -0
  18. package/dist/core/extensions/runner.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +18 -0
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js.map +1 -1
  22. package/dist/core/graph-rail-store.d.ts +10 -0
  23. package/dist/core/graph-rail-store.d.ts.map +1 -1
  24. package/dist/core/graph-rail-store.js +9 -0
  25. package/dist/core/graph-rail-store.js.map +1 -1
  26. package/dist/core/keybindings.d.ts +2 -1
  27. package/dist/core/keybindings.d.ts.map +1 -1
  28. package/dist/core/keybindings.js +4 -1
  29. package/dist/core/keybindings.js.map +1 -1
  30. package/dist/core/tools/find.d.ts.map +1 -1
  31. package/dist/core/tools/find.js +1 -1
  32. package/dist/core/tools/find.js.map +1 -1
  33. package/dist/core/tools/grep.d.ts.map +1 -1
  34. package/dist/core/tools/grep.js +1 -1
  35. package/dist/core/tools/grep.js.map +1 -1
  36. package/dist/extensions/gitnexus/client.d.ts.map +1 -1
  37. package/dist/extensions/gitnexus/client.js +45 -14
  38. package/dist/extensions/gitnexus/client.js.map +1 -1
  39. package/dist/extensions/gitnexus/config.d.ts +15 -1
  40. package/dist/extensions/gitnexus/config.d.ts.map +1 -1
  41. package/dist/extensions/gitnexus/config.js +86 -2
  42. package/dist/extensions/gitnexus/config.js.map +1 -1
  43. package/dist/extensions/gitnexus/hub-api.d.ts +56 -0
  44. package/dist/extensions/gitnexus/hub-api.d.ts.map +1 -0
  45. package/dist/extensions/gitnexus/hub-api.js +105 -0
  46. package/dist/extensions/gitnexus/hub-api.js.map +1 -0
  47. package/dist/extensions/gitnexus/index-repo.d.ts +50 -0
  48. package/dist/extensions/gitnexus/index-repo.d.ts.map +1 -0
  49. package/dist/extensions/gitnexus/index-repo.js +162 -0
  50. package/dist/extensions/gitnexus/index-repo.js.map +1 -0
  51. package/dist/extensions/gitnexus/index.d.ts.map +1 -1
  52. package/dist/extensions/gitnexus/index.js +137 -17
  53. package/dist/extensions/gitnexus/index.js.map +1 -1
  54. package/dist/extensions/gitnexus/observatory.d.ts.map +1 -1
  55. package/dist/extensions/gitnexus/observatory.js +2 -2
  56. package/dist/extensions/gitnexus/observatory.js.map +1 -1
  57. package/dist/extensions/gitnexus/resolve.d.ts +9 -0
  58. package/dist/extensions/gitnexus/resolve.d.ts.map +1 -1
  59. package/dist/extensions/gitnexus/resolve.js +52 -2
  60. package/dist/extensions/gitnexus/resolve.js.map +1 -1
  61. package/dist/extensions/index.d.ts.map +1 -1
  62. package/dist/extensions/index.js +3 -0
  63. package/dist/extensions/index.js.map +1 -1
  64. package/dist/extensions/mcp/commands.d.ts +22 -2
  65. package/dist/extensions/mcp/commands.d.ts.map +1 -1
  66. package/dist/extensions/mcp/commands.js +201 -9
  67. package/dist/extensions/mcp/commands.js.map +1 -1
  68. package/dist/extensions/mcp/config.d.ts +2 -0
  69. package/dist/extensions/mcp/config.d.ts.map +1 -1
  70. package/dist/extensions/mcp/config.js +6 -1
  71. package/dist/extensions/mcp/config.js.map +1 -1
  72. package/dist/extensions/mcp/index.d.ts.map +1 -1
  73. package/dist/extensions/mcp/index.js +54 -5
  74. package/dist/extensions/mcp/index.js.map +1 -1
  75. package/dist/extensions/mcp/mcp-setup-panel.d.ts +63 -2
  76. package/dist/extensions/mcp/mcp-setup-panel.d.ts.map +1 -1
  77. package/dist/extensions/mcp/mcp-setup-panel.js +563 -77
  78. package/dist/extensions/mcp/mcp-setup-panel.js.map +1 -1
  79. package/dist/extensions/subagent/index.d.ts +2 -1
  80. package/dist/extensions/subagent/index.d.ts.map +1 -1
  81. package/dist/extensions/subagent/index.js +19 -6
  82. package/dist/extensions/subagent/index.js.map +1 -1
  83. package/dist/extensions/subagent-render.d.ts +3 -0
  84. package/dist/extensions/subagent-render.d.ts.map +1 -0
  85. package/dist/extensions/subagent-render.js +39 -0
  86. package/dist/extensions/subagent-render.js.map +1 -0
  87. package/dist/index.js +2524 -1120
  88. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  89. package/dist/modes/interactive/components/assistant-message.js +2 -0
  90. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  91. package/dist/modes/interactive/components/command-palette.d.ts +2 -1
  92. package/dist/modes/interactive/components/command-palette.d.ts.map +1 -1
  93. package/dist/modes/interactive/components/command-palette.js +11 -4
  94. package/dist/modes/interactive/components/command-palette.js.map +1 -1
  95. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  96. package/dist/modes/interactive/components/footer.js +9 -3
  97. package/dist/modes/interactive/components/footer.js.map +1 -1
  98. package/dist/modes/interactive/components/graph-rail.d.ts.map +1 -1
  99. package/dist/modes/interactive/components/graph-rail.js +18 -3
  100. package/dist/modes/interactive/components/graph-rail.js.map +1 -1
  101. package/dist/modes/interactive/components/keybinding-hints.d.ts +1 -0
  102. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  103. package/dist/modes/interactive/components/keybinding-hints.js +7 -1
  104. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  105. package/dist/modes/interactive/components/landing-panel.d.ts.map +1 -1
  106. package/dist/modes/interactive/components/landing-panel.js +15 -2
  107. package/dist/modes/interactive/components/landing-panel.js.map +1 -1
  108. package/dist/modes/interactive/components/tool-execution.d.ts +3 -2
  109. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/tool-execution.js +12 -2
  111. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  112. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  113. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  114. package/dist/modes/interactive/interactive-mode.js +42 -8
  115. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  116. package/dist/rpc-entry.js +2492 -1088
  117. package/dist/utils/hidden-spawn.d.ts +29 -0
  118. package/dist/utils/hidden-spawn.d.ts.map +1 -0
  119. package/dist/utils/hidden-spawn.js +101 -0
  120. package/dist/utils/hidden-spawn.js.map +1 -0
  121. package/dist/vendor/ai.js +233 -233
  122. package/dist/vendor/tui.js +1 -1
  123. package/native/win32/build.mjs +95 -0
  124. package/native/win32/prebuilds/win32-x64/hidden-spawn.exe +0 -0
  125. package/native/win32/src/hidden-spawn.c +157 -0
  126. package/npm-shrinkwrap.json +2 -37
  127. package/package.json +7 -3
@@ -8,13 +8,15 @@ import { applyNexusSubagentEnvAliases, isDelegatedWorkerProcess, isExternalDeleg
8
8
  import { registerAugmentedGrep } from "./augment.js";
9
9
  import { registerChangeTracking } from "./changes.js";
10
10
  import { GitNexusClient, GitNexusClientError } from "./client.js";
11
- import { DEFAULT_HUB_URL, resolveGitNexusConfig, saveGitNexusConfig } from "./config.js";
11
+ import { resolveGitNexusConfig, resolveHubUrl, saveGitNexusConfig } from "./config.js";
12
12
  import { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from "./device-auth.js";
13
13
  import { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from "./gateway.js";
14
+ import { GitNexusHubApi } from "./hub-api.js";
15
+ import { monitorRepoIndex, runIndexRepoCommand } from "./index-repo.js";
14
16
  import { spawnObservatoryFetch } from "./observatory.js";
15
17
  import { buildGitNexusPrompt } from "./prompt.js";
16
18
  import { withGitNexusRenderer } from "./render.js";
17
- import { gitRemoteSlug, matchCurrentRepo, parseRepoInventory, } from "./resolve.js";
19
+ import { currentGitHubRemote, gitRemoteSlug, matchCurrentRepo, parseRepoInventory, } from "./resolve.js";
18
20
  import { createGitNexusTools } from "./tools.js";
19
21
  /** Resolve workflow skills in source mode and bundled dist/cli.js mode. */
20
22
  export function resolveGitNexusSkillsDir(moduleUrl = import.meta.url, pathExists = existsSync) {
@@ -41,6 +43,7 @@ export default function gitnexusExtension(pi) {
41
43
  let delegationRegistered = false;
42
44
  let startupNotice;
43
45
  let loginRequired = false;
46
+ let indexMonitorController;
44
47
  const cwd = process.cwd();
45
48
  // Nexus Agent must also register at factory time: the initial model is
46
49
  // resolved before session_start fires, and a provider that appears after
@@ -69,31 +72,47 @@ export default function gitnexusExtension(pi) {
69
72
  // behaves as plain grep until connect() provides a client + current repo.
70
73
  registerAugmentedGrep(pi, cwd, () => activeClient, () => current?.name);
71
74
  registerChangeTracking(pi, cwd);
72
- async function connect() {
75
+ function clearConnectionState(notice, requiresLogin = false) {
73
76
  inventory = undefined;
74
77
  current = undefined;
75
78
  activeClient = undefined;
76
- startupNotice = undefined;
77
- loginRequired = false;
79
+ startupNotice = notice;
80
+ loginRequired = requiresLogin;
81
+ }
82
+ async function connect(signal) {
83
+ signal?.throwIfAborted();
78
84
  const config = resolveGitNexusConfig();
79
- if (!config)
85
+ if (!config) {
86
+ clearConnectionState();
80
87
  return;
88
+ }
81
89
  const client = new GitNexusClient(config);
90
+ let nextInventory;
82
91
  try {
83
- const listResult = await client.callTool("list_repos", {});
84
- inventory = parseRepoInventory(GitNexusClient.text(listResult));
92
+ const listResult = await client.callTool("list_repos", {}, signal);
93
+ signal?.throwIfAborted();
94
+ nextInventory = parseRepoInventory(GitNexusClient.text(listResult));
85
95
  }
86
96
  catch (error) {
97
+ signal?.throwIfAborted();
87
98
  const reason = error instanceof GitNexusClientError ? error.message : String(error);
88
99
  if (error instanceof GitNexusClientError && error.kind === "auth") {
89
- loginRequired = true;
100
+ clearConnectionState(undefined, true);
90
101
  return;
91
102
  }
92
- startupNotice = `GitNexus unavailable: ${reason}`;
103
+ clearConnectionState(`GitNexus unavailable: ${reason}`);
93
104
  return;
94
105
  }
95
- current = matchCurrentRepo(inventory, gitRemoteSlug(cwd));
106
+ const nextCurrent = matchCurrentRepo(nextInventory, gitRemoteSlug(cwd));
107
+ const nextStartupNotice = nextCurrent
108
+ ? `GitNexus: connected — ${nextCurrent.name} (${nextCurrent.symbols} symbols, indexed ${nextCurrent.indexedAt}), ${nextInventory.repos.length} repos available.`
109
+ : `GitNexus: connected — current directory is not indexed; ${nextInventory.repos.length} indexed repos remain queryable. Run /gitnexus-index to index this repo and unlock local graph context.`;
110
+ signal?.throwIfAborted();
111
+ inventory = nextInventory;
112
+ current = nextCurrent;
96
113
  activeClient = client;
114
+ startupNotice = nextStartupNotice;
115
+ loginRequired = false;
97
116
  graphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));
98
117
  // Observatory (community map) rides a detached subprocess + temp-file
99
118
  // handoff — in-process hub calls made after startup can starve in some
@@ -122,15 +141,108 @@ export default function gitnexusExtension(pi) {
122
141
  pi.registerTool(withGitNexusRenderer(tool));
123
142
  toolsRegistered = true;
124
143
  }
125
- startupNotice = current
126
- ? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`
127
- : `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;
144
+ }
145
+ async function requestRepositoryIndex(targetCwd, branch, ui) {
146
+ if (/\s/.test(branch)) {
147
+ const message = "Branch names containing whitespace are not supported.";
148
+ ui.notify(message, "error");
149
+ return;
150
+ }
151
+ const config = resolveGitNexusConfig();
152
+ if (!config) {
153
+ const message = "GitNexus is not signed in. Run /login.";
154
+ ui.notify(message, "warning");
155
+ return;
156
+ }
157
+ const remote = currentGitHubRemote(targetCwd);
158
+ if (!remote) {
159
+ const message = `No supported github.com origin was found in ${targetCwd}.`;
160
+ ui.notify(message, "error");
161
+ return;
162
+ }
163
+ indexMonitorController?.abort();
164
+ graphRailStore.setIndexProgress(undefined);
165
+ const controller = new AbortController();
166
+ indexMonitorController = controller;
167
+ const releaseController = () => {
168
+ if (indexMonitorController === controller)
169
+ indexMonitorController = undefined;
170
+ };
171
+ try {
172
+ const api = new GitNexusHubApi(config);
173
+ const result = await runIndexRepoCommand({ api, remote, branch, ui, signal: controller.signal });
174
+ if (controller.signal.aborted || indexMonitorController !== controller) {
175
+ releaseController();
176
+ return;
177
+ }
178
+ if (result.kind === "blocked") {
179
+ releaseController();
180
+ return;
181
+ }
182
+ if (result.kind === "cancelled") {
183
+ releaseController();
184
+ ui.notify("Repository indexing cancelled.", "info");
185
+ return;
186
+ }
187
+ if (result.kind === "ready") {
188
+ const message = `${remote.fullName} is already indexed; no reindex was started.`;
189
+ ui.notify(message, "info");
190
+ await connect(controller.signal);
191
+ releaseController();
192
+ return;
193
+ }
194
+ void monitorRepoIndex({
195
+ api,
196
+ repoId: result.repoId,
197
+ ui: {
198
+ notify: (message, level) => ui.notify(message, level),
199
+ setStatus: (key, text) => {
200
+ if (indexMonitorController !== controller)
201
+ return;
202
+ ui.setStatus(key, text);
203
+ },
204
+ },
205
+ signal: controller.signal,
206
+ onProgress: (progress) => {
207
+ if (indexMonitorController !== controller)
208
+ return;
209
+ graphRailStore.setIndexProgress(progress);
210
+ },
211
+ onReady: async () => {
212
+ if (controller.signal.aborted || indexMonitorController !== controller)
213
+ return;
214
+ await connect(controller.signal);
215
+ if (controller.signal.aborted || indexMonitorController !== controller)
216
+ return;
217
+ ui.notify(`${remote.fullName} (${branch}) is indexed and ready.`, "info");
218
+ },
219
+ })
220
+ .catch((error) => {
221
+ if (controller.signal.aborted)
222
+ return;
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ ui.notify(`GitNexus indexing monitor failed: ${message}`, "error");
225
+ })
226
+ .finally(releaseController);
227
+ return;
228
+ }
229
+ catch (error) {
230
+ releaseController();
231
+ if (controller.signal.aborted)
232
+ return;
233
+ const reason = error instanceof Error ? error.message : String(error);
234
+ const message = `GitNexus repository indexing failed: ${reason}`;
235
+ ui.notify(message, "error");
236
+ }
128
237
  }
129
238
  pi.on("session_start", async (_event, ctx) => {
130
239
  await connect();
131
240
  updateCreditStatus(ctx);
132
- if (startupNotice)
133
- ctx.ui.notify(startupNotice, inventory ? "info" : "warning");
241
+ if (startupNotice && !inventory)
242
+ ctx.ui.notify(startupNotice, "warning");
243
+ });
244
+ pi.on("session_shutdown", () => {
245
+ indexMonitorController?.abort();
134
246
  });
135
247
  pi.on("agent_settled", async (_event, ctx) => {
136
248
  if (!gatewayPolicy || ctx.model?.provider !== "gitnexus")
@@ -164,7 +276,7 @@ export default function gitnexusExtension(pi) {
164
276
  pi.registerCommand("gitnexus-login", {
165
277
  description: "Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)",
166
278
  handler: async (args, ctx) => {
167
- const hubUrl = process.env.GITNEXUS_HUB_URL ?? DEFAULT_HUB_URL;
279
+ const hubUrl = resolveHubUrl();
168
280
  const pasted = args.trim();
169
281
  // Pasting a token stays supported for CI and headless installs.
170
282
  if (pasted) {
@@ -211,6 +323,7 @@ export default function gitnexusExtension(pi) {
211
323
  description: "Show GitNexus connection status",
212
324
  handler: async (_args, ctx) => {
213
325
  const config = resolveGitNexusConfig();
326
+ await connect();
214
327
  if (config)
215
328
  gatewayPolicy = await refreshGatewayPolicy(config);
216
329
  updateCreditStatus(ctx);
@@ -226,5 +339,12 @@ export default function gitnexusExtension(pi) {
226
339
  ctx.ui.notify(`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : ""}`, "info");
227
340
  },
228
341
  });
342
+ pi.registerCommand("gitnexus-index", {
343
+ description: "Index the current repository in GitNexus (optional branch)",
344
+ handler: async (args, ctx) => {
345
+ const branch = args.trim() || "default";
346
+ await requestRepositoryIndex(cwd, branch, ctx.ui);
347
+ },
348
+ });
229
349
  }
230
350
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport {\n\tapplyNexusSubagentEnvAliases,\n\tisDelegatedWorkerProcess,\n\tisExternalDelegationMode,\n\tregisterDelegateTaskTool,\n} from \"../subagent/index.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { DEFAULT_HUB_URL, resolveGitNexusConfig, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet delegationRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Nexus Agent must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tapplyNexusSubagentEnvAliases();\n\tlet gatewayPolicy = readGatewayCache();\n\tconst initialConfig = resolveGitNexusConfig();\n\tregisterAutoProvider(pi, initialConfig, gatewayPolicy);\n\n\tfunction registerDelegationIfAvailable(): void {\n\t\tif (\n\t\t\tdelegationRegistered ||\n\t\t\tisDelegatedWorkerProcess() ||\n\t\t\tisExternalDelegationMode() ||\n\t\t\t!resolveGitNexusConfig() ||\n\t\t\t!gatewayPolicy?.delegation\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tregisterDelegateTaskTool(pi, gatewayPolicy.delegation);\n\t\tdelegationRegistered = true;\n\t}\n\n\tregisterDelegationIfAvailable();\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tasync function connect(): Promise<void> {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = undefined;\n\t\tloginRequired = false;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) return;\n\t\tconst client = new GitNexusClient(config);\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {});\n\t\t\tinventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tloginRequired = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartupNotice = `GitNexus unavailable: ${reason}`;\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = matchCurrentRepo(inventory, gitRemoteSlug(cwd));\n\t\tactiveClient = client;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\n\t\tstartupNotice = current\n\t\t\t? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice) ctx.ui.notify(startupNotice, inventory ? \"info\" : \"warning\");\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tregisterDelegationIfAvailable();\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = process.env.GITNEXUS_HUB_URL ?? DEFAULT_HUB_URL;\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\ttry {\n\t\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\t\tawait connect();\n\t\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport {\n\tapplyNexusSubagentEnvAliases,\n\tisDelegatedWorkerProcess,\n\tisExternalDelegationMode,\n\tregisterDelegateTaskTool,\n} from \"../subagent/index.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { resolveGitNexusConfig, resolveHubUrl, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { GitNexusHubApi } from \"./hub-api.ts\";\nimport { type IndexRepoUi, monitorRepoIndex, runIndexRepoCommand } from \"./index-repo.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tcurrentGitHubRemote,\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet delegationRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\tlet indexMonitorController: AbortController | undefined;\n\n\tconst cwd = process.cwd();\n\n\t// Nexus Agent must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tapplyNexusSubagentEnvAliases();\n\tlet gatewayPolicy = readGatewayCache();\n\tconst initialConfig = resolveGitNexusConfig();\n\tregisterAutoProvider(pi, initialConfig, gatewayPolicy);\n\n\tfunction registerDelegationIfAvailable(): void {\n\t\tif (\n\t\t\tdelegationRegistered ||\n\t\t\tisDelegatedWorkerProcess() ||\n\t\t\tisExternalDelegationMode() ||\n\t\t\t!resolveGitNexusConfig() ||\n\t\t\t!gatewayPolicy?.delegation\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tregisterDelegateTaskTool(pi, gatewayPolicy.delegation);\n\t\tdelegationRegistered = true;\n\t}\n\n\tregisterDelegationIfAvailable();\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tfunction clearConnectionState(notice?: string, requiresLogin = false): void {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = notice;\n\t\tloginRequired = requiresLogin;\n\t}\n\n\tasync function connect(signal?: AbortSignal): Promise<void> {\n\t\tsignal?.throwIfAborted();\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) {\n\t\t\tclearConnectionState();\n\t\t\treturn;\n\t\t}\n\t\tconst client = new GitNexusClient(config);\n\t\tlet nextInventory: RepoInventory;\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {}, signal);\n\t\t\tsignal?.throwIfAborted();\n\t\t\tnextInventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tsignal?.throwIfAborted();\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tclearConnectionState(undefined, true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tclearConnectionState(`GitNexus unavailable: ${reason}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst nextCurrent = matchCurrentRepo(nextInventory, gitRemoteSlug(cwd));\n\t\tconst nextStartupNotice = nextCurrent\n\t\t\t? `GitNexus: connected — ${nextCurrent.name} (${nextCurrent.symbols} symbols, indexed ${nextCurrent.indexedAt}), ${nextInventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${nextInventory.repos.length} indexed repos remain queryable. Run /gitnexus-index to index this repo and unlock local graph context.`;\n\t\tsignal?.throwIfAborted();\n\t\tinventory = nextInventory;\n\t\tcurrent = nextCurrent;\n\t\tactiveClient = client;\n\t\tstartupNotice = nextStartupNotice;\n\t\tloginRequired = false;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\t}\n\n\tasync function requestRepositoryIndex(targetCwd: string, branch: string, ui: IndexRepoUi): Promise<void> {\n\t\tif (/\\s/.test(branch)) {\n\t\t\tconst message = \"Branch names containing whitespace are not supported.\";\n\t\t\tui.notify(message, \"error\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) {\n\t\t\tconst message = \"GitNexus is not signed in. Run /login.\";\n\t\t\tui.notify(message, \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tconst remote = currentGitHubRemote(targetCwd);\n\t\tif (!remote) {\n\t\t\tconst message = `No supported github.com origin was found in ${targetCwd}.`;\n\t\t\tui.notify(message, \"error\");\n\t\t\treturn;\n\t\t}\n\n\t\tindexMonitorController?.abort();\n\t\tgraphRailStore.setIndexProgress(undefined);\n\t\tconst controller = new AbortController();\n\t\tindexMonitorController = controller;\n\t\tconst releaseController = () => {\n\t\t\tif (indexMonitorController === controller) indexMonitorController = undefined;\n\t\t};\n\n\t\ttry {\n\t\t\tconst api = new GitNexusHubApi(config);\n\t\t\tconst result = await runIndexRepoCommand({ api, remote, branch, ui, signal: controller.signal });\n\t\t\tif (controller.signal.aborted || indexMonitorController !== controller) {\n\t\t\t\treleaseController();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (result.kind === \"blocked\") {\n\t\t\t\treleaseController();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (result.kind === \"cancelled\") {\n\t\t\t\treleaseController();\n\t\t\t\tui.notify(\"Repository indexing cancelled.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (result.kind === \"ready\") {\n\t\t\t\tconst message = `${remote.fullName} is already indexed; no reindex was started.`;\n\t\t\t\tui.notify(message, \"info\");\n\t\t\t\tawait connect(controller.signal);\n\t\t\t\treleaseController();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvoid monitorRepoIndex({\n\t\t\t\tapi,\n\t\t\t\trepoId: result.repoId,\n\t\t\t\tui: {\n\t\t\t\t\tnotify: (message, level) => ui.notify(message, level),\n\t\t\t\t\tsetStatus: (key, text) => {\n\t\t\t\t\t\tif (indexMonitorController !== controller) return;\n\t\t\t\t\t\tui.setStatus(key, text);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsignal: controller.signal,\n\t\t\t\tonProgress: (progress) => {\n\t\t\t\t\tif (indexMonitorController !== controller) return;\n\t\t\t\t\tgraphRailStore.setIndexProgress(progress);\n\t\t\t\t},\n\t\t\t\tonReady: async () => {\n\t\t\t\t\tif (controller.signal.aborted || indexMonitorController !== controller) return;\n\t\t\t\t\tawait connect(controller.signal);\n\t\t\t\t\tif (controller.signal.aborted || indexMonitorController !== controller) return;\n\t\t\t\t\tui.notify(`${remote.fullName} (${branch}) is indexed and ready.`, \"info\");\n\t\t\t\t},\n\t\t\t})\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (controller.signal.aborted) return;\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tui.notify(`GitNexus indexing monitor failed: ${message}`, \"error\");\n\t\t\t\t})\n\t\t\t\t.finally(releaseController);\n\t\t\treturn;\n\t\t} catch (error) {\n\t\t\treleaseController();\n\t\t\tif (controller.signal.aborted) return;\n\t\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\t\tconst message = `GitNexus repository indexing failed: ${reason}`;\n\t\t\tui.notify(message, \"error\");\n\t\t}\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice && !inventory) ctx.ui.notify(startupNotice, \"warning\");\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tindexMonitorController?.abort();\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tregisterDelegationIfAvailable();\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = resolveHubUrl();\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\ttry {\n\t\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\t\tawait connect();\n\t\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tawait connect();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-index\", {\n\t\tdescription: \"Index the current repository in GitNexus (optional branch)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst branch = args.trim() || \"default\";\n\t\t\tawait requestRepositoryIndex(cwd, branch, ctx.ui);\n\t\t},\n\t});\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"observatory.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/observatory.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkClD,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAiBxD;AAeD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B1F;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAmBhF","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { appendFileSync } from \"node:fs\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { observatoryCachePath } from \"../../core/observatory-cache.ts\";\nimport { GitNexusClient } from \"./client.ts\";\nimport type { GitNexusConfig } from \"./config.ts\";\n\nfunction trace(message: string): void {\n\tif (!process.env.NEXUS_TRACE) return;\n\ttry {\n\t\tappendFileSync(\"/tmp/nexus-observatory-trace.log\", `${new Date().toISOString()} ${message}\\n`);\n\t} catch {\n\t\t// tracing is best-effort\n\t}\n}\n\n/**\n * Startup graph observatory: fetches the community map of the current repo\n * (community sizes, cross-community call edges, flow count) and feeds the\n * rail store. Fire-and-forget — failures leave the observatory empty and\n * never surface to the user.\n */\n\nconst COMMUNITY_QUERY =\n\t\"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) \" +\n\t\"RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\";\n\nconst EDGE_QUERY =\n\t\"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), \" +\n\t\"(b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), \" +\n\t\"(a)-[r:CodeRelation {type:'CALLS'}]->(b) \" +\n\t\"WHERE ca.heuristicLabel <> cb.heuristicLabel \" +\n\t\"RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight \" +\n\t\"ORDER BY weight DESC LIMIT 16\";\n\nconst FLOW_QUERY = \"MATCH (p:Process) RETURN count(p) AS flows\";\n\nconst TIMEOUT_MS = 20_000;\n\n/** Parse the hub cypher result: `{\"markdown\": \"| a | b |\\n| --- |...\", \"row_count\": n}`. */\nexport function parseCypherRows(text: string): string[][] {\n\tlet markdown: string;\n\ttry {\n\t\tconst parsed = JSON.parse(text.split(\"\\n\\n---\")[0]?.trim() ?? \"\") as { markdown?: string };\n\t\tif (typeof parsed.markdown !== \"string\") return [];\n\t\tmarkdown = parsed.markdown;\n\t} catch {\n\t\treturn [];\n\t}\n\tconst lines = markdown.split(\"\\n\").filter((line) => line.trim().startsWith(\"|\"));\n\t// Drop header + separator rows.\n\treturn lines.slice(2).map((line) =>\n\t\tline\n\t\t\t.split(\"|\")\n\t\t\t.slice(1, -1)\n\t\t\t.map((cell) => cell.trim()),\n\t);\n}\n\nasync function runQuery(client: GitNexusClient, repo: string, query: string, label: string): Promise<string[][]> {\n\tconst start = Date.now();\n\ttrace(`${label}: start`);\n\ttry {\n\t\tconst result = await client.callTool(\"cypher\", { query, repo }, AbortSignal.timeout(TIMEOUT_MS));\n\t\ttrace(`${label}: ok in ${Date.now() - start}ms`);\n\t\treturn parseCypherRows(GitNexusClient.text(result));\n\t} catch (error) {\n\t\ttrace(`${label}: failed in ${Date.now() - start}ms: ${String(error)}`);\n\t\tthrow error;\n\t}\n}\n\nexport async function fetchObservatory(client: GitNexusClient, repo: string): Promise<void> {\n\ttry {\n\t\ttrace(`fetching for ${repo}`);\n\t\t// Sequential on purpose: parallel POSTs against one hub MCP session stall\n\t\t// behind per-session serialization; three sequential cyphers finish in ~2s.\n\t\tconst communityRows = await runQuery(client, repo, COMMUNITY_QUERY, \"communities\");\n\t\tconst edgeRows = await runQuery(client, repo, EDGE_QUERY, \"edges\");\n\t\tconst flowRows = await runQuery(client, repo, FLOW_QUERY, \"flows\");\n\t\ttrace(`rows: communities=${communityRows.length} edges=${edgeRows.length} flows=${flowRows.length}`);\n\t\tconst communities = communityRows\n\t\t\t.filter((row) => row[0])\n\t\t\t.map((row) => ({ name: row[0], members: Number(row[1]) || 0 }));\n\t\tif (communities.length === 0) return;\n\t\tconst known = new Set(communities.map((c) => c.name));\n\t\tconst edges = edgeRows\n\t\t\t.filter((row) => row[0] && row[1] && known.has(row[0]) && known.has(row[1]))\n\t\t\t.map((row) => ({ source: row[0], target: row[1], weight: Number(row[2]) || 0 }));\n\t\tconst flowCount = Number(flowRows[0]?.[0]);\n\t\tgraphRailStore.setObservatory({\n\t\t\tcommunities,\n\t\t\tedges,\n\t\t\t...(Number.isNaN(flowCount) ? {} : { flowCount }),\n\t\t});\n\t} catch (error) {\n\t\t// observatory is cosmetic — startup must never depend on it\n\t\ttrace(`fetch failed: ${String(error)}`);\n\t}\n}\n\n/**\n * Fetch the observatory in a detached child process and hand the result over\n * via a temp file (picked up lazily by the landing panel / rail). In-process\n * hub calls issued after startup can starve behind the session's own HTTP\n * machinery in some environments; a fresh process never does.\n */\nexport function spawnObservatoryFetch(config: GitNexusConfig, repo: string): void {\n\ttry {\n\t\tconst script = buildFetchScript();\n\t\tconst child = spawn(process.execPath, [\"-e\", script], {\n\t\t\tdetached: true,\n\t\t\tstdio: \"ignore\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tNEXUS_OBS_HUB_URL: config.hubUrl,\n\t\t\t\tNEXUS_OBS_TOKEN: config.token,\n\t\t\t\tNEXUS_OBS_REPO: repo,\n\t\t\t\tNEXUS_OBS_OUT: observatoryCachePath(repo),\n\t\t\t},\n\t\t});\n\t\tchild.unref();\n\t\ttrace(`spawned observatory fetcher pid=${child.pid ?? \"?\"} for ${repo}`);\n\t} catch (error) {\n\t\ttrace(`spawn failed: ${String(error)}`);\n\t}\n}\n\n/** Self-contained fetch script (plain node, no imports beyond fs). */\nfunction buildFetchScript(): string {\n\treturn `\nconst { writeFileSync } = require(\"node:fs\");\nconst hubUrl = process.env.NEXUS_OBS_HUB_URL;\nconst token = process.env.NEXUS_OBS_TOKEN;\nconst repo = process.env.NEXUS_OBS_REPO;\nconst out = process.env.NEXUS_OBS_OUT;\nlet sessionId;\nasync function post(body) {\n\tconst res = await fetch(hubUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t\taccept: \"application/json, text/event-stream\",\n\t\t\tauthorization: \"Bearer \" + token,\n\t\t\t...(sessionId ? { \"mcp-session-id\": sessionId } : {}),\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t\tsignal: AbortSignal.timeout(20000),\n\t});\n\tconst sid = res.headers.get(\"mcp-session-id\");\n\tif (sid) sessionId = sid;\n\tconst text = await res.text();\n\tif (res.status === 202) return undefined;\n\tif (!res.ok) throw new Error(\"hub HTTP \" + res.status);\n\tif ((res.headers.get(\"content-type\") || \"\").includes(\"text/event-stream\")) {\n\t\tlet last;\n\t\tfor (const ev of text.split(\"\\\\n\\\\n\")) {\n\t\t\tconst data = ev.split(\"\\\\n\").filter((l) => l.startsWith(\"data:\")).map((l) => l.slice(5).trim()).join(\"\\\\n\");\n\t\t\tif (!data) continue;\n\t\t\ttry { const p = JSON.parse(data); if (p.id !== undefined && p.id !== null) last = p; } catch {}\n\t\t}\n\t\treturn last;\n\t}\n\treturn text ? JSON.parse(text) : undefined;\n}\nlet nextId = 1;\nasync function call(name, args) {\n\tconst r = await post({ jsonrpc: \"2.0\", id: nextId++, method: \"tools/call\", params: { name, arguments: args } });\n\tif (!r || r.error) throw new Error(r && r.error ? r.error.message : \"empty response\");\n\tconst parts = (r.result && r.result.content) || [];\n\treturn parts.filter((p) => p.type === \"text\").map((p) => p.text).join(\"\\\\n\");\n}\nfunction rows(text) {\n\tlet md;\n\ttry { md = JSON.parse((text.split(\"\\\\n\\\\n---\")[0] || \"\").trim()).markdown; } catch { return []; }\n\tif (typeof md !== \"string\") return [];\n\tconst lines = md.split(\"\\\\n\").filter((l) => l.trim().startsWith(\"|\"));\n\treturn lines.slice(2).map((l) => l.split(\"|\").slice(1, -1).map((c) => c.trim()));\n}\n(async () => {\n\tawait post({ jsonrpc: \"2.0\", id: nextId++, method: \"initialize\", params: { protocolVersion: \"2025-06-18\", capabilities: {}, clientInfo: { name: \"nexus-observatory\", version: \"0.1.0\" } } });\n\tawait post({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\tconst communityRows = rows(await call(\"cypher\", { repo, query: \"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\" }));\n\tconst edgeRows = rows(await call(\"cypher\", { repo, query: \"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), (b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), (a)-[r:CodeRelation {type:'CALLS'}]->(b) WHERE ca.heuristicLabel <> cb.heuristicLabel RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight ORDER BY weight DESC LIMIT 16\" }));\n\tconst flowRows = rows(await call(\"cypher\", { repo, query: \"MATCH (p:Process) RETURN count(p) AS flows\" }));\n\tconst communities = communityRows.filter((r) => r[0]).map((r) => ({ name: r[0], members: Number(r[1]) || 0 }));\n\tif (!communities.length) process.exit(0);\n\tconst known = new Set(communities.map((c) => c.name));\n\tconst edges = edgeRows.filter((r) => r[0] && r[1] && known.has(r[0]) && known.has(r[1])).map((r) => ({ source: r[0], target: r[1], weight: Number(r[2]) || 0 }));\n\tconst flowCount = Number(flowRows[0] && flowRows[0][0]);\n\tconst payload = { communities, edges, ...(Number.isNaN(flowCount) ? {} : { flowCount }), fetchedAt: Date.now() };\n\twriteFileSync(out, JSON.stringify(payload));\n\tprocess.exit(0);\n})().catch(() => process.exit(1));\n`;\n}\n"]}
1
+ {"version":3,"file":"observatory.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/observatory.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkClD,4FAA4F;AAC5F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAiBxD;AAeD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B1F;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAmBhF","sourcesContent":["import { appendFileSync } from \"node:fs\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { observatoryCachePath } from \"../../core/observatory-cache.ts\";\nimport { spawnWithoutConsole } from \"../../utils/hidden-spawn.ts\";\nimport { GitNexusClient } from \"./client.ts\";\nimport type { GitNexusConfig } from \"./config.ts\";\n\nfunction trace(message: string): void {\n\tif (!process.env.NEXUS_TRACE) return;\n\ttry {\n\t\tappendFileSync(\"/tmp/nexus-observatory-trace.log\", `${new Date().toISOString()} ${message}\\n`);\n\t} catch {\n\t\t// tracing is best-effort\n\t}\n}\n\n/**\n * Startup graph observatory: fetches the community map of the current repo\n * (community sizes, cross-community call edges, flow count) and feeds the\n * rail store. Fire-and-forget — failures leave the observatory empty and\n * never surface to the user.\n */\n\nconst COMMUNITY_QUERY =\n\t\"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) \" +\n\t\"RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\";\n\nconst EDGE_QUERY =\n\t\"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), \" +\n\t\"(b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), \" +\n\t\"(a)-[r:CodeRelation {type:'CALLS'}]->(b) \" +\n\t\"WHERE ca.heuristicLabel <> cb.heuristicLabel \" +\n\t\"RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight \" +\n\t\"ORDER BY weight DESC LIMIT 16\";\n\nconst FLOW_QUERY = \"MATCH (p:Process) RETURN count(p) AS flows\";\n\nconst TIMEOUT_MS = 20_000;\n\n/** Parse the hub cypher result: `{\"markdown\": \"| a | b |\\n| --- |...\", \"row_count\": n}`. */\nexport function parseCypherRows(text: string): string[][] {\n\tlet markdown: string;\n\ttry {\n\t\tconst parsed = JSON.parse(text.split(\"\\n\\n---\")[0]?.trim() ?? \"\") as { markdown?: string };\n\t\tif (typeof parsed.markdown !== \"string\") return [];\n\t\tmarkdown = parsed.markdown;\n\t} catch {\n\t\treturn [];\n\t}\n\tconst lines = markdown.split(\"\\n\").filter((line) => line.trim().startsWith(\"|\"));\n\t// Drop header + separator rows.\n\treturn lines.slice(2).map((line) =>\n\t\tline\n\t\t\t.split(\"|\")\n\t\t\t.slice(1, -1)\n\t\t\t.map((cell) => cell.trim()),\n\t);\n}\n\nasync function runQuery(client: GitNexusClient, repo: string, query: string, label: string): Promise<string[][]> {\n\tconst start = Date.now();\n\ttrace(`${label}: start`);\n\ttry {\n\t\tconst result = await client.callTool(\"cypher\", { query, repo }, AbortSignal.timeout(TIMEOUT_MS));\n\t\ttrace(`${label}: ok in ${Date.now() - start}ms`);\n\t\treturn parseCypherRows(GitNexusClient.text(result));\n\t} catch (error) {\n\t\ttrace(`${label}: failed in ${Date.now() - start}ms: ${String(error)}`);\n\t\tthrow error;\n\t}\n}\n\nexport async function fetchObservatory(client: GitNexusClient, repo: string): Promise<void> {\n\ttry {\n\t\ttrace(`fetching for ${repo}`);\n\t\t// Sequential on purpose: parallel POSTs against one hub MCP session stall\n\t\t// behind per-session serialization; three sequential cyphers finish in ~2s.\n\t\tconst communityRows = await runQuery(client, repo, COMMUNITY_QUERY, \"communities\");\n\t\tconst edgeRows = await runQuery(client, repo, EDGE_QUERY, \"edges\");\n\t\tconst flowRows = await runQuery(client, repo, FLOW_QUERY, \"flows\");\n\t\ttrace(`rows: communities=${communityRows.length} edges=${edgeRows.length} flows=${flowRows.length}`);\n\t\tconst communities = communityRows\n\t\t\t.filter((row) => row[0])\n\t\t\t.map((row) => ({ name: row[0], members: Number(row[1]) || 0 }));\n\t\tif (communities.length === 0) return;\n\t\tconst known = new Set(communities.map((c) => c.name));\n\t\tconst edges = edgeRows\n\t\t\t.filter((row) => row[0] && row[1] && known.has(row[0]) && known.has(row[1]))\n\t\t\t.map((row) => ({ source: row[0], target: row[1], weight: Number(row[2]) || 0 }));\n\t\tconst flowCount = Number(flowRows[0]?.[0]);\n\t\tgraphRailStore.setObservatory({\n\t\t\tcommunities,\n\t\t\tedges,\n\t\t\t...(Number.isNaN(flowCount) ? {} : { flowCount }),\n\t\t});\n\t} catch (error) {\n\t\t// observatory is cosmetic — startup must never depend on it\n\t\ttrace(`fetch failed: ${String(error)}`);\n\t}\n}\n\n/**\n * Fetch the observatory in a detached child process and hand the result over\n * via a temp file (picked up lazily by the landing panel / rail). In-process\n * hub calls issued after startup can starve behind the session's own HTTP\n * machinery in some environments; a fresh process never does.\n */\nexport function spawnObservatoryFetch(config: GitNexusConfig, repo: string): void {\n\ttry {\n\t\tconst script = buildFetchScript();\n\t\tconst child = spawnWithoutConsole(process.execPath, [\"-e\", script], {\n\t\t\tdetached: true,\n\t\t\tstdio: \"ignore\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tNEXUS_OBS_HUB_URL: config.hubUrl,\n\t\t\t\tNEXUS_OBS_TOKEN: config.token,\n\t\t\t\tNEXUS_OBS_REPO: repo,\n\t\t\t\tNEXUS_OBS_OUT: observatoryCachePath(repo),\n\t\t\t},\n\t\t});\n\t\tchild.unref();\n\t\ttrace(`spawned observatory fetcher pid=${child.pid ?? \"?\"} for ${repo}`);\n\t} catch (error) {\n\t\ttrace(`spawn failed: ${String(error)}`);\n\t}\n}\n\n/** Self-contained fetch script (plain node, no imports beyond fs). */\nfunction buildFetchScript(): string {\n\treturn `\nconst { writeFileSync } = require(\"node:fs\");\nconst hubUrl = process.env.NEXUS_OBS_HUB_URL;\nconst token = process.env.NEXUS_OBS_TOKEN;\nconst repo = process.env.NEXUS_OBS_REPO;\nconst out = process.env.NEXUS_OBS_OUT;\nlet sessionId;\nasync function post(body) {\n\tconst res = await fetch(hubUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t\taccept: \"application/json, text/event-stream\",\n\t\t\tauthorization: \"Bearer \" + token,\n\t\t\t...(sessionId ? { \"mcp-session-id\": sessionId } : {}),\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t\tsignal: AbortSignal.timeout(20000),\n\t});\n\tconst sid = res.headers.get(\"mcp-session-id\");\n\tif (sid) sessionId = sid;\n\tconst text = await res.text();\n\tif (res.status === 202) return undefined;\n\tif (!res.ok) throw new Error(\"hub HTTP \" + res.status);\n\tif ((res.headers.get(\"content-type\") || \"\").includes(\"text/event-stream\")) {\n\t\tlet last;\n\t\tfor (const ev of text.split(\"\\\\n\\\\n\")) {\n\t\t\tconst data = ev.split(\"\\\\n\").filter((l) => l.startsWith(\"data:\")).map((l) => l.slice(5).trim()).join(\"\\\\n\");\n\t\t\tif (!data) continue;\n\t\t\ttry { const p = JSON.parse(data); if (p.id !== undefined && p.id !== null) last = p; } catch {}\n\t\t}\n\t\treturn last;\n\t}\n\treturn text ? JSON.parse(text) : undefined;\n}\nlet nextId = 1;\nasync function call(name, args) {\n\tconst r = await post({ jsonrpc: \"2.0\", id: nextId++, method: \"tools/call\", params: { name, arguments: args } });\n\tif (!r || r.error) throw new Error(r && r.error ? r.error.message : \"empty response\");\n\tconst parts = (r.result && r.result.content) || [];\n\treturn parts.filter((p) => p.type === \"text\").map((p) => p.text).join(\"\\\\n\");\n}\nfunction rows(text) {\n\tlet md;\n\ttry { md = JSON.parse((text.split(\"\\\\n\\\\n---\")[0] || \"\").trim()).markdown; } catch { return []; }\n\tif (typeof md !== \"string\") return [];\n\tconst lines = md.split(\"\\\\n\").filter((l) => l.trim().startsWith(\"|\"));\n\treturn lines.slice(2).map((l) => l.split(\"|\").slice(1, -1).map((c) => c.trim()));\n}\n(async () => {\n\tawait post({ jsonrpc: \"2.0\", id: nextId++, method: \"initialize\", params: { protocolVersion: \"2025-06-18\", capabilities: {}, clientInfo: { name: \"nexus-observatory\", version: \"0.1.0\" } } });\n\tawait post({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\tconst communityRows = rows(await call(\"cypher\", { repo, query: \"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\" }));\n\tconst edgeRows = rows(await call(\"cypher\", { repo, query: \"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), (b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), (a)-[r:CodeRelation {type:'CALLS'}]->(b) WHERE ca.heuristicLabel <> cb.heuristicLabel RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight ORDER BY weight DESC LIMIT 16\" }));\n\tconst flowRows = rows(await call(\"cypher\", { repo, query: \"MATCH (p:Process) RETURN count(p) AS flows\" }));\n\tconst communities = communityRows.filter((r) => r[0]).map((r) => ({ name: r[0], members: Number(r[1]) || 0 }));\n\tif (!communities.length) process.exit(0);\n\tconst known = new Set(communities.map((c) => c.name));\n\tconst edges = edgeRows.filter((r) => r[0] && r[1] && known.has(r[0]) && known.has(r[1])).map((r) => ({ source: r[0], target: r[1], weight: Number(r[2]) || 0 }));\n\tconst flowCount = Number(flowRows[0] && flowRows[0][0]);\n\tconst payload = { communities, edges, ...(Number.isNaN(flowCount) ? {} : { flowCount }), fetchedAt: Date.now() };\n\twriteFileSync(out, JSON.stringify(payload));\n\tprocess.exit(0);\n})().catch(() => process.exit(1));\n`;\n}\n"]}
@@ -1,7 +1,7 @@
1
- import { spawn } from "node:child_process";
2
1
  import { appendFileSync } from "node:fs";
3
2
  import { graphRailStore } from "../../core/graph-rail-store.js";
4
3
  import { observatoryCachePath } from "../../core/observatory-cache.js";
4
+ import { spawnWithoutConsole } from "../../utils/hidden-spawn.js";
5
5
  import { GitNexusClient } from "./client.js";
6
6
  function trace(message) {
7
7
  if (!process.env.NEXUS_TRACE)
@@ -100,7 +100,7 @@ export async function fetchObservatory(client, repo) {
100
100
  export function spawnObservatoryFetch(config, repo) {
101
101
  try {
102
102
  const script = buildFetchScript();
103
- const child = spawn(process.execPath, ["-e", script], {
103
+ const child = spawnWithoutConsole(process.execPath, ["-e", script], {
104
104
  detached: true,
105
105
  stdio: "ignore",
106
106
  env: {
@@ -1 +1 @@
1
- {"version":3,"file":"observatory.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/observatory.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { appendFileSync } from \"node:fs\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { observatoryCachePath } from \"../../core/observatory-cache.ts\";\nimport { GitNexusClient } from \"./client.ts\";\nimport type { GitNexusConfig } from \"./config.ts\";\n\nfunction trace(message: string): void {\n\tif (!process.env.NEXUS_TRACE) return;\n\ttry {\n\t\tappendFileSync(\"/tmp/nexus-observatory-trace.log\", `${new Date().toISOString()} ${message}\\n`);\n\t} catch {\n\t\t// tracing is best-effort\n\t}\n}\n\n/**\n * Startup graph observatory: fetches the community map of the current repo\n * (community sizes, cross-community call edges, flow count) and feeds the\n * rail store. Fire-and-forget — failures leave the observatory empty and\n * never surface to the user.\n */\n\nconst COMMUNITY_QUERY =\n\t\"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) \" +\n\t\"RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\";\n\nconst EDGE_QUERY =\n\t\"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), \" +\n\t\"(b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), \" +\n\t\"(a)-[r:CodeRelation {type:'CALLS'}]->(b) \" +\n\t\"WHERE ca.heuristicLabel <> cb.heuristicLabel \" +\n\t\"RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight \" +\n\t\"ORDER BY weight DESC LIMIT 16\";\n\nconst FLOW_QUERY = \"MATCH (p:Process) RETURN count(p) AS flows\";\n\nconst TIMEOUT_MS = 20_000;\n\n/** Parse the hub cypher result: `{\"markdown\": \"| a | b |\\n| --- |...\", \"row_count\": n}`. */\nexport function parseCypherRows(text: string): string[][] {\n\tlet markdown: string;\n\ttry {\n\t\tconst parsed = JSON.parse(text.split(\"\\n\\n---\")[0]?.trim() ?? \"\") as { markdown?: string };\n\t\tif (typeof parsed.markdown !== \"string\") return [];\n\t\tmarkdown = parsed.markdown;\n\t} catch {\n\t\treturn [];\n\t}\n\tconst lines = markdown.split(\"\\n\").filter((line) => line.trim().startsWith(\"|\"));\n\t// Drop header + separator rows.\n\treturn lines.slice(2).map((line) =>\n\t\tline\n\t\t\t.split(\"|\")\n\t\t\t.slice(1, -1)\n\t\t\t.map((cell) => cell.trim()),\n\t);\n}\n\nasync function runQuery(client: GitNexusClient, repo: string, query: string, label: string): Promise<string[][]> {\n\tconst start = Date.now();\n\ttrace(`${label}: start`);\n\ttry {\n\t\tconst result = await client.callTool(\"cypher\", { query, repo }, AbortSignal.timeout(TIMEOUT_MS));\n\t\ttrace(`${label}: ok in ${Date.now() - start}ms`);\n\t\treturn parseCypherRows(GitNexusClient.text(result));\n\t} catch (error) {\n\t\ttrace(`${label}: failed in ${Date.now() - start}ms: ${String(error)}`);\n\t\tthrow error;\n\t}\n}\n\nexport async function fetchObservatory(client: GitNexusClient, repo: string): Promise<void> {\n\ttry {\n\t\ttrace(`fetching for ${repo}`);\n\t\t// Sequential on purpose: parallel POSTs against one hub MCP session stall\n\t\t// behind per-session serialization; three sequential cyphers finish in ~2s.\n\t\tconst communityRows = await runQuery(client, repo, COMMUNITY_QUERY, \"communities\");\n\t\tconst edgeRows = await runQuery(client, repo, EDGE_QUERY, \"edges\");\n\t\tconst flowRows = await runQuery(client, repo, FLOW_QUERY, \"flows\");\n\t\ttrace(`rows: communities=${communityRows.length} edges=${edgeRows.length} flows=${flowRows.length}`);\n\t\tconst communities = communityRows\n\t\t\t.filter((row) => row[0])\n\t\t\t.map((row) => ({ name: row[0], members: Number(row[1]) || 0 }));\n\t\tif (communities.length === 0) return;\n\t\tconst known = new Set(communities.map((c) => c.name));\n\t\tconst edges = edgeRows\n\t\t\t.filter((row) => row[0] && row[1] && known.has(row[0]) && known.has(row[1]))\n\t\t\t.map((row) => ({ source: row[0], target: row[1], weight: Number(row[2]) || 0 }));\n\t\tconst flowCount = Number(flowRows[0]?.[0]);\n\t\tgraphRailStore.setObservatory({\n\t\t\tcommunities,\n\t\t\tedges,\n\t\t\t...(Number.isNaN(flowCount) ? {} : { flowCount }),\n\t\t});\n\t} catch (error) {\n\t\t// observatory is cosmetic — startup must never depend on it\n\t\ttrace(`fetch failed: ${String(error)}`);\n\t}\n}\n\n/**\n * Fetch the observatory in a detached child process and hand the result over\n * via a temp file (picked up lazily by the landing panel / rail). In-process\n * hub calls issued after startup can starve behind the session's own HTTP\n * machinery in some environments; a fresh process never does.\n */\nexport function spawnObservatoryFetch(config: GitNexusConfig, repo: string): void {\n\ttry {\n\t\tconst script = buildFetchScript();\n\t\tconst child = spawn(process.execPath, [\"-e\", script], {\n\t\t\tdetached: true,\n\t\t\tstdio: \"ignore\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tNEXUS_OBS_HUB_URL: config.hubUrl,\n\t\t\t\tNEXUS_OBS_TOKEN: config.token,\n\t\t\t\tNEXUS_OBS_REPO: repo,\n\t\t\t\tNEXUS_OBS_OUT: observatoryCachePath(repo),\n\t\t\t},\n\t\t});\n\t\tchild.unref();\n\t\ttrace(`spawned observatory fetcher pid=${child.pid ?? \"?\"} for ${repo}`);\n\t} catch (error) {\n\t\ttrace(`spawn failed: ${String(error)}`);\n\t}\n}\n\n/** Self-contained fetch script (plain node, no imports beyond fs). */\nfunction buildFetchScript(): string {\n\treturn `\nconst { writeFileSync } = require(\"node:fs\");\nconst hubUrl = process.env.NEXUS_OBS_HUB_URL;\nconst token = process.env.NEXUS_OBS_TOKEN;\nconst repo = process.env.NEXUS_OBS_REPO;\nconst out = process.env.NEXUS_OBS_OUT;\nlet sessionId;\nasync function post(body) {\n\tconst res = await fetch(hubUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t\taccept: \"application/json, text/event-stream\",\n\t\t\tauthorization: \"Bearer \" + token,\n\t\t\t...(sessionId ? { \"mcp-session-id\": sessionId } : {}),\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t\tsignal: AbortSignal.timeout(20000),\n\t});\n\tconst sid = res.headers.get(\"mcp-session-id\");\n\tif (sid) sessionId = sid;\n\tconst text = await res.text();\n\tif (res.status === 202) return undefined;\n\tif (!res.ok) throw new Error(\"hub HTTP \" + res.status);\n\tif ((res.headers.get(\"content-type\") || \"\").includes(\"text/event-stream\")) {\n\t\tlet last;\n\t\tfor (const ev of text.split(\"\\\\n\\\\n\")) {\n\t\t\tconst data = ev.split(\"\\\\n\").filter((l) => l.startsWith(\"data:\")).map((l) => l.slice(5).trim()).join(\"\\\\n\");\n\t\t\tif (!data) continue;\n\t\t\ttry { const p = JSON.parse(data); if (p.id !== undefined && p.id !== null) last = p; } catch {}\n\t\t}\n\t\treturn last;\n\t}\n\treturn text ? JSON.parse(text) : undefined;\n}\nlet nextId = 1;\nasync function call(name, args) {\n\tconst r = await post({ jsonrpc: \"2.0\", id: nextId++, method: \"tools/call\", params: { name, arguments: args } });\n\tif (!r || r.error) throw new Error(r && r.error ? r.error.message : \"empty response\");\n\tconst parts = (r.result && r.result.content) || [];\n\treturn parts.filter((p) => p.type === \"text\").map((p) => p.text).join(\"\\\\n\");\n}\nfunction rows(text) {\n\tlet md;\n\ttry { md = JSON.parse((text.split(\"\\\\n\\\\n---\")[0] || \"\").trim()).markdown; } catch { return []; }\n\tif (typeof md !== \"string\") return [];\n\tconst lines = md.split(\"\\\\n\").filter((l) => l.trim().startsWith(\"|\"));\n\treturn lines.slice(2).map((l) => l.split(\"|\").slice(1, -1).map((c) => c.trim()));\n}\n(async () => {\n\tawait post({ jsonrpc: \"2.0\", id: nextId++, method: \"initialize\", params: { protocolVersion: \"2025-06-18\", capabilities: {}, clientInfo: { name: \"nexus-observatory\", version: \"0.1.0\" } } });\n\tawait post({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\tconst communityRows = rows(await call(\"cypher\", { repo, query: \"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\" }));\n\tconst edgeRows = rows(await call(\"cypher\", { repo, query: \"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), (b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), (a)-[r:CodeRelation {type:'CALLS'}]->(b) WHERE ca.heuristicLabel <> cb.heuristicLabel RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight ORDER BY weight DESC LIMIT 16\" }));\n\tconst flowRows = rows(await call(\"cypher\", { repo, query: \"MATCH (p:Process) RETURN count(p) AS flows\" }));\n\tconst communities = communityRows.filter((r) => r[0]).map((r) => ({ name: r[0], members: Number(r[1]) || 0 }));\n\tif (!communities.length) process.exit(0);\n\tconst known = new Set(communities.map((c) => c.name));\n\tconst edges = edgeRows.filter((r) => r[0] && r[1] && known.has(r[0]) && known.has(r[1])).map((r) => ({ source: r[0], target: r[1], weight: Number(r[2]) || 0 }));\n\tconst flowCount = Number(flowRows[0] && flowRows[0][0]);\n\tconst payload = { communities, edges, ...(Number.isNaN(flowCount) ? {} : { flowCount }), fetchedAt: Date.now() };\n\twriteFileSync(out, JSON.stringify(payload));\n\tprocess.exit(0);\n})().catch(() => process.exit(1));\n`;\n}\n"]}
1
+ {"version":3,"file":"observatory.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/observatory.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync } from \"node:fs\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { observatoryCachePath } from \"../../core/observatory-cache.ts\";\nimport { spawnWithoutConsole } from \"../../utils/hidden-spawn.ts\";\nimport { GitNexusClient } from \"./client.ts\";\nimport type { GitNexusConfig } from \"./config.ts\";\n\nfunction trace(message: string): void {\n\tif (!process.env.NEXUS_TRACE) return;\n\ttry {\n\t\tappendFileSync(\"/tmp/nexus-observatory-trace.log\", `${new Date().toISOString()} ${message}\\n`);\n\t} catch {\n\t\t// tracing is best-effort\n\t}\n}\n\n/**\n * Startup graph observatory: fetches the community map of the current repo\n * (community sizes, cross-community call edges, flow count) and feeds the\n * rail store. Fire-and-forget — failures leave the observatory empty and\n * never surface to the user.\n */\n\nconst COMMUNITY_QUERY =\n\t\"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) \" +\n\t\"RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\";\n\nconst EDGE_QUERY =\n\t\"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), \" +\n\t\"(b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), \" +\n\t\"(a)-[r:CodeRelation {type:'CALLS'}]->(b) \" +\n\t\"WHERE ca.heuristicLabel <> cb.heuristicLabel \" +\n\t\"RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight \" +\n\t\"ORDER BY weight DESC LIMIT 16\";\n\nconst FLOW_QUERY = \"MATCH (p:Process) RETURN count(p) AS flows\";\n\nconst TIMEOUT_MS = 20_000;\n\n/** Parse the hub cypher result: `{\"markdown\": \"| a | b |\\n| --- |...\", \"row_count\": n}`. */\nexport function parseCypherRows(text: string): string[][] {\n\tlet markdown: string;\n\ttry {\n\t\tconst parsed = JSON.parse(text.split(\"\\n\\n---\")[0]?.trim() ?? \"\") as { markdown?: string };\n\t\tif (typeof parsed.markdown !== \"string\") return [];\n\t\tmarkdown = parsed.markdown;\n\t} catch {\n\t\treturn [];\n\t}\n\tconst lines = markdown.split(\"\\n\").filter((line) => line.trim().startsWith(\"|\"));\n\t// Drop header + separator rows.\n\treturn lines.slice(2).map((line) =>\n\t\tline\n\t\t\t.split(\"|\")\n\t\t\t.slice(1, -1)\n\t\t\t.map((cell) => cell.trim()),\n\t);\n}\n\nasync function runQuery(client: GitNexusClient, repo: string, query: string, label: string): Promise<string[][]> {\n\tconst start = Date.now();\n\ttrace(`${label}: start`);\n\ttry {\n\t\tconst result = await client.callTool(\"cypher\", { query, repo }, AbortSignal.timeout(TIMEOUT_MS));\n\t\ttrace(`${label}: ok in ${Date.now() - start}ms`);\n\t\treturn parseCypherRows(GitNexusClient.text(result));\n\t} catch (error) {\n\t\ttrace(`${label}: failed in ${Date.now() - start}ms: ${String(error)}`);\n\t\tthrow error;\n\t}\n}\n\nexport async function fetchObservatory(client: GitNexusClient, repo: string): Promise<void> {\n\ttry {\n\t\ttrace(`fetching for ${repo}`);\n\t\t// Sequential on purpose: parallel POSTs against one hub MCP session stall\n\t\t// behind per-session serialization; three sequential cyphers finish in ~2s.\n\t\tconst communityRows = await runQuery(client, repo, COMMUNITY_QUERY, \"communities\");\n\t\tconst edgeRows = await runQuery(client, repo, EDGE_QUERY, \"edges\");\n\t\tconst flowRows = await runQuery(client, repo, FLOW_QUERY, \"flows\");\n\t\ttrace(`rows: communities=${communityRows.length} edges=${edgeRows.length} flows=${flowRows.length}`);\n\t\tconst communities = communityRows\n\t\t\t.filter((row) => row[0])\n\t\t\t.map((row) => ({ name: row[0], members: Number(row[1]) || 0 }));\n\t\tif (communities.length === 0) return;\n\t\tconst known = new Set(communities.map((c) => c.name));\n\t\tconst edges = edgeRows\n\t\t\t.filter((row) => row[0] && row[1] && known.has(row[0]) && known.has(row[1]))\n\t\t\t.map((row) => ({ source: row[0], target: row[1], weight: Number(row[2]) || 0 }));\n\t\tconst flowCount = Number(flowRows[0]?.[0]);\n\t\tgraphRailStore.setObservatory({\n\t\t\tcommunities,\n\t\t\tedges,\n\t\t\t...(Number.isNaN(flowCount) ? {} : { flowCount }),\n\t\t});\n\t} catch (error) {\n\t\t// observatory is cosmetic — startup must never depend on it\n\t\ttrace(`fetch failed: ${String(error)}`);\n\t}\n}\n\n/**\n * Fetch the observatory in a detached child process and hand the result over\n * via a temp file (picked up lazily by the landing panel / rail). In-process\n * hub calls issued after startup can starve behind the session's own HTTP\n * machinery in some environments; a fresh process never does.\n */\nexport function spawnObservatoryFetch(config: GitNexusConfig, repo: string): void {\n\ttry {\n\t\tconst script = buildFetchScript();\n\t\tconst child = spawnWithoutConsole(process.execPath, [\"-e\", script], {\n\t\t\tdetached: true,\n\t\t\tstdio: \"ignore\",\n\t\t\tenv: {\n\t\t\t\t...process.env,\n\t\t\t\tNEXUS_OBS_HUB_URL: config.hubUrl,\n\t\t\t\tNEXUS_OBS_TOKEN: config.token,\n\t\t\t\tNEXUS_OBS_REPO: repo,\n\t\t\t\tNEXUS_OBS_OUT: observatoryCachePath(repo),\n\t\t\t},\n\t\t});\n\t\tchild.unref();\n\t\ttrace(`spawned observatory fetcher pid=${child.pid ?? \"?\"} for ${repo}`);\n\t} catch (error) {\n\t\ttrace(`spawn failed: ${String(error)}`);\n\t}\n}\n\n/** Self-contained fetch script (plain node, no imports beyond fs). */\nfunction buildFetchScript(): string {\n\treturn `\nconst { writeFileSync } = require(\"node:fs\");\nconst hubUrl = process.env.NEXUS_OBS_HUB_URL;\nconst token = process.env.NEXUS_OBS_TOKEN;\nconst repo = process.env.NEXUS_OBS_REPO;\nconst out = process.env.NEXUS_OBS_OUT;\nlet sessionId;\nasync function post(body) {\n\tconst res = await fetch(hubUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t\taccept: \"application/json, text/event-stream\",\n\t\t\tauthorization: \"Bearer \" + token,\n\t\t\t...(sessionId ? { \"mcp-session-id\": sessionId } : {}),\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t\tsignal: AbortSignal.timeout(20000),\n\t});\n\tconst sid = res.headers.get(\"mcp-session-id\");\n\tif (sid) sessionId = sid;\n\tconst text = await res.text();\n\tif (res.status === 202) return undefined;\n\tif (!res.ok) throw new Error(\"hub HTTP \" + res.status);\n\tif ((res.headers.get(\"content-type\") || \"\").includes(\"text/event-stream\")) {\n\t\tlet last;\n\t\tfor (const ev of text.split(\"\\\\n\\\\n\")) {\n\t\t\tconst data = ev.split(\"\\\\n\").filter((l) => l.startsWith(\"data:\")).map((l) => l.slice(5).trim()).join(\"\\\\n\");\n\t\t\tif (!data) continue;\n\t\t\ttry { const p = JSON.parse(data); if (p.id !== undefined && p.id !== null) last = p; } catch {}\n\t\t}\n\t\treturn last;\n\t}\n\treturn text ? JSON.parse(text) : undefined;\n}\nlet nextId = 1;\nasync function call(name, args) {\n\tconst r = await post({ jsonrpc: \"2.0\", id: nextId++, method: \"tools/call\", params: { name, arguments: args } });\n\tif (!r || r.error) throw new Error(r && r.error ? r.error.message : \"empty response\");\n\tconst parts = (r.result && r.result.content) || [];\n\treturn parts.filter((p) => p.type === \"text\").map((p) => p.text).join(\"\\\\n\");\n}\nfunction rows(text) {\n\tlet md;\n\ttry { md = JSON.parse((text.split(\"\\\\n\\\\n---\")[0] || \"\").trim()).markdown; } catch { return []; }\n\tif (typeof md !== \"string\") return [];\n\tconst lines = md.split(\"\\\\n\").filter((l) => l.trim().startsWith(\"|\"));\n\treturn lines.slice(2).map((l) => l.split(\"|\").slice(1, -1).map((c) => c.trim()));\n}\n(async () => {\n\tawait post({ jsonrpc: \"2.0\", id: nextId++, method: \"initialize\", params: { protocolVersion: \"2025-06-18\", capabilities: {}, clientInfo: { name: \"nexus-observatory\", version: \"0.1.0\" } } });\n\tawait post({ jsonrpc: \"2.0\", method: \"notifications/initialized\" });\n\tconst communityRows = rows(await call(\"cypher\", { repo, query: \"MATCH (f)-[:CodeRelation {type:'MEMBER_OF'}]->(c:Community) RETURN c.heuristicLabel AS name, count(f) AS members ORDER BY members DESC LIMIT 8\" }));\n\tconst edgeRows = rows(await call(\"cypher\", { repo, query: \"MATCH (a)-[:CodeRelation {type:'MEMBER_OF'}]->(ca:Community), (b)-[:CodeRelation {type:'MEMBER_OF'}]->(cb:Community), (a)-[r:CodeRelation {type:'CALLS'}]->(b) WHERE ca.heuristicLabel <> cb.heuristicLabel RETURN ca.heuristicLabel AS source, cb.heuristicLabel AS target, count(r) AS weight ORDER BY weight DESC LIMIT 16\" }));\n\tconst flowRows = rows(await call(\"cypher\", { repo, query: \"MATCH (p:Process) RETURN count(p) AS flows\" }));\n\tconst communities = communityRows.filter((r) => r[0]).map((r) => ({ name: r[0], members: Number(r[1]) || 0 }));\n\tif (!communities.length) process.exit(0);\n\tconst known = new Set(communities.map((c) => c.name));\n\tconst edges = edgeRows.filter((r) => r[0] && r[1] && known.has(r[0]) && known.has(r[1])).map((r) => ({ source: r[0], target: r[1], weight: Number(r[2]) || 0 }));\n\tconst flowCount = Number(flowRows[0] && flowRows[0][0]);\n\tconst payload = { communities, edges, ...(Number.isNaN(flowCount) ? {} : { flowCount }), fetchedAt: Date.now() };\n\twriteFileSync(out, JSON.stringify(payload));\n\tprocess.exit(0);\n})().catch(() => process.exit(1));\n`;\n}\n"]}
@@ -13,8 +13,17 @@ export interface RepoInventory {
13
13
  repos: IndexedRepo[];
14
14
  groups: IndexedGroup[];
15
15
  }
16
+ export interface GitHubRemote {
17
+ name: string;
18
+ fullName: string;
19
+ cloneUrl: string;
20
+ }
16
21
  /** Parse the markdown returned by the hub list_repos tool. */
17
22
  export declare function parseRepoInventory(markdown: string): RepoInventory;
23
+ /** Parse a supported GitHub origin without preserving credentials or URL parameters. */
24
+ export declare function parseGitHubRemote(remoteUrl: string): GitHubRemote | undefined;
25
+ /** Resolve the current repository's origin as a canonical GitHub remote. */
26
+ export declare function currentGitHubRemote(cwd: string): GitHubRemote | undefined;
18
27
  /** Read the git remote of cwd and normalize to "owner/name" (undefined outside a repo). */
19
28
  export declare function gitRemoteSlug(cwd: string): string | undefined;
20
29
  /** Match the cwd's remote against the hub inventory. Exact slug first, then name suffix. */
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/resolve.ts"],"names":[],"mappings":"AAEA,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAuBlE;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAY7D;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAO5G","sourcesContent":["import { spawnSync } from \"node:child_process\";\n\n/** One indexed repo as reported by the hub's list_repos tool. */\nexport interface IndexedRepo {\n\tname: string;\n\tsymbols: string;\n\trelationships: string;\n\tindexedAt: string;\n}\n\nexport interface IndexedGroup {\n\tname: string;\n\trepoCount: string;\n}\n\nexport interface RepoInventory {\n\trepos: IndexedRepo[];\n\tgroups: IndexedGroup[];\n}\n\n/** Parse the markdown returned by the hub list_repos tool. */\nexport function parseRepoInventory(markdown: string): RepoInventory {\n\tconst repos: IndexedRepo[] = [];\n\tconst groups: IndexedGroup[] = [];\n\tconst repoRe = /^- \\*\\*([^*]+)\\*\\* \\(([\\d,]+) symbols, ([\\d,]+) relationships\\) — indexed (.+)$/;\n\tconst groupRe = /^- \\*\\*(@[^*]+)\\*\\* \\((\\d+) repos?/;\n\tfor (const line of markdown.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tconst repoMatch = repoRe.exec(trimmed);\n\t\tif (repoMatch?.[1] && repoMatch[2] && repoMatch[3] && repoMatch[4]) {\n\t\t\trepos.push({\n\t\t\t\tname: repoMatch[1],\n\t\t\t\tsymbols: repoMatch[2],\n\t\t\t\trelationships: repoMatch[3],\n\t\t\t\tindexedAt: repoMatch[4],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst groupMatch = groupRe.exec(trimmed);\n\t\tif (groupMatch?.[1] && groupMatch[2]) {\n\t\t\tgroups.push({ name: groupMatch[1], repoCount: groupMatch[2] });\n\t\t}\n\t}\n\treturn { repos, groups };\n}\n\n/** Read the git remote of cwd and normalize to \"owner/name\" (undefined outside a repo). */\nexport function gitRemoteSlug(cwd: string): string | undefined {\n\tconst result = spawnSync(\"git\", [\"remote\", \"get-url\", \"origin\"], {\n\t\tcwd,\n\t\tencoding: \"utf8\",\n\t\ttimeout: 3000,\n\t});\n\tif (result.status !== 0) return undefined;\n\tconst url = (result.stdout ?? \"\").trim();\n\tif (!url) return undefined;\n\t// https://github.com/owner/name(.git) | git@github.com:owner/name(.git)\n\tconst match = /[/:]([^/:]+\\/[^/:]+?)(?:\\.git)?$/.exec(url);\n\treturn match?.[1];\n}\n\n/** Match the cwd's remote against the hub inventory. Exact slug first, then name suffix. */\nexport function matchCurrentRepo(inventory: RepoInventory, slug: string | undefined): IndexedRepo | undefined {\n\tif (!slug) return undefined;\n\tconst lower = slug.toLowerCase();\n\tconst exact = inventory.repos.find((r) => r.name.toLowerCase() === lower);\n\tif (exact) return exact;\n\tconst name = lower.split(\"/\")[1] ?? lower;\n\treturn inventory.repos.find((r) => r.name.toLowerCase().endsWith(`/${name}`));\n}\n"]}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/resolve.ts"],"names":[],"mappings":"AAEA,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAuBlE;AAaD,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAwB7E;AAUD,4EAA4E;AAC5E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAGzE;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM7D;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAO5G","sourcesContent":["import { spawnSync } from \"node:child_process\";\n\n/** One indexed repo as reported by the hub's list_repos tool. */\nexport interface IndexedRepo {\n\tname: string;\n\tsymbols: string;\n\trelationships: string;\n\tindexedAt: string;\n}\n\nexport interface IndexedGroup {\n\tname: string;\n\trepoCount: string;\n}\n\nexport interface RepoInventory {\n\trepos: IndexedRepo[];\n\tgroups: IndexedGroup[];\n}\n\nexport interface GitHubRemote {\n\tname: string;\n\tfullName: string;\n\tcloneUrl: string;\n}\n\n/** Parse the markdown returned by the hub list_repos tool. */\nexport function parseRepoInventory(markdown: string): RepoInventory {\n\tconst repos: IndexedRepo[] = [];\n\tconst groups: IndexedGroup[] = [];\n\tconst repoRe = /^- \\*\\*([^*]+)\\*\\* \\(([\\d,]+) symbols, ([\\d,]+) relationships\\) — indexed (.+)$/;\n\tconst groupRe = /^- \\*\\*(@[^*]+)\\*\\* \\((\\d+) repos?/;\n\tfor (const line of markdown.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tconst repoMatch = repoRe.exec(trimmed);\n\t\tif (repoMatch?.[1] && repoMatch[2] && repoMatch[3] && repoMatch[4]) {\n\t\t\trepos.push({\n\t\t\t\tname: repoMatch[1],\n\t\t\t\tsymbols: repoMatch[2],\n\t\t\t\trelationships: repoMatch[3],\n\t\t\t\tindexedAt: repoMatch[4],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tconst groupMatch = groupRe.exec(trimmed);\n\t\tif (groupMatch?.[1] && groupMatch[2]) {\n\t\t\tgroups.push({ name: groupMatch[1], repoCount: groupMatch[2] });\n\t\t}\n\t}\n\treturn { repos, groups };\n}\n\nfunction gitOriginUrl(cwd: string): string | undefined {\n\tconst result = spawnSync(\"git\", [\"remote\", \"get-url\", \"origin\"], {\n\t\tcwd,\n\t\tencoding: \"utf8\",\n\t\ttimeout: 3000,\n\t});\n\tif (result.status !== 0) return undefined;\n\tconst url = (result.stdout ?? \"\").trim();\n\treturn url || undefined;\n}\n\n/** Parse a supported GitHub origin without preserving credentials or URL parameters. */\nexport function parseGitHubRemote(remoteUrl: string): GitHubRemote | undefined {\n\tif (!remoteUrl || /\\s/.test(remoteUrl)) return undefined;\n\n\tconst scpMatch = /^git@github\\.com:([^/]+)\\/([^/]+)$/.exec(remoteUrl);\n\tif (scpMatch?.[1] && scpMatch[2]) return normalizeGitHubRemote(scpMatch[1], scpMatch[2]);\n\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(remoteUrl);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (parsed.hostname !== \"github.com\" || parsed.port || parsed.search || parsed.hash) return undefined;\n\tif (parsed.protocol === \"https:\") {\n\t\tif (parsed.username || parsed.password) return undefined;\n\t} else if (parsed.protocol === \"ssh:\") {\n\t\tif (parsed.username !== \"git\" || parsed.password) return undefined;\n\t} else {\n\t\treturn undefined;\n\t}\n\n\tconst pathMatch = /^\\/([^/]+)\\/([^/]+)$/.exec(parsed.pathname);\n\tif (!pathMatch?.[1] || !pathMatch[2]) return undefined;\n\treturn normalizeGitHubRemote(pathMatch[1], pathMatch[2]);\n}\n\nfunction normalizeGitHubRemote(owner: string, rawName: string): GitHubRemote | undefined {\n\tconst name = rawName.endsWith(\".git\") ? rawName.slice(0, -4) : rawName;\n\tif (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(owner)) return undefined;\n\tif (name.length > 100 || !/^[A-Za-z0-9._-]+$/.test(name) || name === \".\" || name === \"..\") return undefined;\n\tconst fullName = `${owner}/${name}`;\n\treturn { name, fullName, cloneUrl: `https://github.com/${fullName}.git` };\n}\n\n/** Resolve the current repository's origin as a canonical GitHub remote. */\nexport function currentGitHubRemote(cwd: string): GitHubRemote | undefined {\n\tconst remoteUrl = gitOriginUrl(cwd);\n\treturn remoteUrl ? parseGitHubRemote(remoteUrl) : undefined;\n}\n\n/** Read the git remote of cwd and normalize to \"owner/name\" (undefined outside a repo). */\nexport function gitRemoteSlug(cwd: string): string | undefined {\n\tconst url = gitOriginUrl(cwd);\n\tif (!url) return undefined;\n\t// https://github.com/owner/name(.git) | git@github.com:owner/name(.git)\n\tconst match = /[/:]([^/:]+\\/[^/:]+?)(?:\\.git)?$/.exec(url);\n\treturn match?.[1];\n}\n\n/** Match the cwd's remote against the hub inventory. Exact slug first, then name suffix. */\nexport function matchCurrentRepo(inventory: RepoInventory, slug: string | undefined): IndexedRepo | undefined {\n\tif (!slug) return undefined;\n\tconst lower = slug.toLowerCase();\n\tconst exact = inventory.repos.find((r) => r.name.toLowerCase() === lower);\n\tif (exact) return exact;\n\tconst name = lower.split(\"/\")[1] ?? lower;\n\treturn inventory.repos.find((r) => r.name.toLowerCase().endsWith(`/${name}`));\n}\n"]}
@@ -24,8 +24,7 @@ export function parseRepoInventory(markdown) {
24
24
  }
25
25
  return { repos, groups };
26
26
  }
27
- /** Read the git remote of cwd and normalize to "owner/name" (undefined outside a repo). */
28
- export function gitRemoteSlug(cwd) {
27
+ function gitOriginUrl(cwd) {
29
28
  const result = spawnSync("git", ["remote", "get-url", "origin"], {
30
29
  cwd,
31
30
  encoding: "utf8",
@@ -34,6 +33,57 @@ export function gitRemoteSlug(cwd) {
34
33
  if (result.status !== 0)
35
34
  return undefined;
36
35
  const url = (result.stdout ?? "").trim();
36
+ return url || undefined;
37
+ }
38
+ /** Parse a supported GitHub origin without preserving credentials or URL parameters. */
39
+ export function parseGitHubRemote(remoteUrl) {
40
+ if (!remoteUrl || /\s/.test(remoteUrl))
41
+ return undefined;
42
+ const scpMatch = /^git@github\.com:([^/]+)\/([^/]+)$/.exec(remoteUrl);
43
+ if (scpMatch?.[1] && scpMatch[2])
44
+ return normalizeGitHubRemote(scpMatch[1], scpMatch[2]);
45
+ let parsed;
46
+ try {
47
+ parsed = new URL(remoteUrl);
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ if (parsed.hostname !== "github.com" || parsed.port || parsed.search || parsed.hash)
53
+ return undefined;
54
+ if (parsed.protocol === "https:") {
55
+ if (parsed.username || parsed.password)
56
+ return undefined;
57
+ }
58
+ else if (parsed.protocol === "ssh:") {
59
+ if (parsed.username !== "git" || parsed.password)
60
+ return undefined;
61
+ }
62
+ else {
63
+ return undefined;
64
+ }
65
+ const pathMatch = /^\/([^/]+)\/([^/]+)$/.exec(parsed.pathname);
66
+ if (!pathMatch?.[1] || !pathMatch[2])
67
+ return undefined;
68
+ return normalizeGitHubRemote(pathMatch[1], pathMatch[2]);
69
+ }
70
+ function normalizeGitHubRemote(owner, rawName) {
71
+ const name = rawName.endsWith(".git") ? rawName.slice(0, -4) : rawName;
72
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(owner))
73
+ return undefined;
74
+ if (name.length > 100 || !/^[A-Za-z0-9._-]+$/.test(name) || name === "." || name === "..")
75
+ return undefined;
76
+ const fullName = `${owner}/${name}`;
77
+ return { name, fullName, cloneUrl: `https://github.com/${fullName}.git` };
78
+ }
79
+ /** Resolve the current repository's origin as a canonical GitHub remote. */
80
+ export function currentGitHubRemote(cwd) {
81
+ const remoteUrl = gitOriginUrl(cwd);
82
+ return remoteUrl ? parseGitHubRemote(remoteUrl) : undefined;
83
+ }
84
+ /** Read the git remote of cwd and normalize to "owner/name" (undefined outside a repo). */
85
+ export function gitRemoteSlug(cwd) {
86
+ const url = gitOriginUrl(cwd);
37
87
  if (!url)
38
88
  return undefined;
39
89
  // https://github.com/owner/name(.git) | git@github.com:owner/name(.git)