@akon-labs/nexus-cli 0.1.7 → 0.1.8
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.
- package/CHANGELOG.md +6 -0
- package/dist/cli.js +1192 -529
- package/dist/extensions/gitnexus/gateway.d.ts +2 -0
- package/dist/extensions/gitnexus/gateway.d.ts.map +1 -1
- package/dist/extensions/gitnexus/gateway.js +27 -0
- package/dist/extensions/gitnexus/gateway.js.map +1 -1
- package/dist/extensions/gitnexus/index.d.ts.map +1 -1
- package/dist/extensions/gitnexus/index.js +16 -1
- package/dist/extensions/gitnexus/index.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +2 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts +110 -0
- package/dist/extensions/subagent/index.d.ts.map +1 -0
- package/dist/extensions/subagent/index.js +568 -0
- package/dist/extensions/subagent/index.js.map +1 -0
- package/dist/index.js +1236 -573
- package/dist/rpc-entry.js +1192 -529
- package/dist/vendor/ai.js +110 -15
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "../../core/extensions/types.ts";
|
|
2
|
+
import type { DelegationCapability } from "../subagent/index.ts";
|
|
2
3
|
import { type GitNexusConfig } from "./config.ts";
|
|
3
4
|
/**
|
|
4
5
|
* Auto mode: models served through the hub's LLM gateway.
|
|
@@ -28,6 +29,7 @@ export interface GatewayPolicy {
|
|
|
28
29
|
limit: number;
|
|
29
30
|
remaining?: number;
|
|
30
31
|
};
|
|
32
|
+
delegation?: DelegationCapability;
|
|
31
33
|
}
|
|
32
34
|
export declare function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined;
|
|
33
35
|
export declare function gatewayCachePath(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,aAAa,CAAC;AAGtE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,aAAa,CAAC;AAGtE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,UAAU,CAAC,EAAE,oBAAoB,CAAC;CAClC;AAwCD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIzF;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,MAA2B,GAAG,aAAa,GAAG,SAAS,CAc7F;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAGzC,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,OAAO,GAAE;IACR,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACd,GACJ,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAwCpC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CACnC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,MAAM,EAAE,aAAa,GAAG,SAAS,GAC/B,OAAO,CAwCT","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from \"../../core/auto-model.ts\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport type { DelegationCapability } from \"../subagent/index.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Auto mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Auto\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Auto entry.\n */\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n\tdelegation?: DelegationCapability;\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nfunction parseDelegation(value: unknown): DelegationCapability | undefined {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst delegation = value as Record<string, unknown>;\n\tif (\n\t\tdelegation.enabled !== true ||\n\t\tdelegation.version !== 1 ||\n\t\ttypeof delegation.maxParallel !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxParallel) ||\n\t\tdelegation.maxParallel < 1 ||\n\t\ttypeof delegation.maxTaskBytes !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxTaskBytes) ||\n\t\tdelegation.maxTaskBytes < 1 ||\n\t\ttypeof delegation.workerTimeoutMs !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.workerTimeoutMs) ||\n\t\tdelegation.workerTimeoutMs < 1\n\t) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tenabled: true,\n\t\tversion: 1,\n\t\tmaxParallel: delegation.maxParallel,\n\t\tmaxTaskBytes: delegation.maxTaskBytes,\n\t\tworkerTimeoutMs: delegation.workerTimeoutMs,\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t\tdelegation: parseDelegation(parsed.delegation),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Auto entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: {\n\t\tfetchImpl?: typeof fetch;\n\t\tcachePath?: string;\n\t\ttimeoutMs?: number;\n\t} = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t\tdelegation: parseDelegation(body.delegation),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Auto entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: an Auto entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Auto\",\n\t\t\t\t// The agent already owns a stable session ID. Forward it only to the\n\t\t\t\t// Hub, which hashes it before applying provider-specific cache affinity.\n\t\t\t\tcompat: {\n\t\t\t\t\tsendSessionAffinityHeaders: true,\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\",\n\t\t\t\t},\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\t// The Nexus Agent beta is text-only at the Hub boundary, regardless\n\t\t\t\t// of the capabilities of the model selected behind Auto.\n\t\t\t\tinput: [\"text\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}
|
|
@@ -15,6 +15,31 @@ function parseUsage(value) {
|
|
|
15
15
|
...(typeof usage.remaining === "number" ? { remaining: usage.remaining } : {}),
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
function parseDelegation(value) {
|
|
19
|
+
if (!value || typeof value !== "object")
|
|
20
|
+
return undefined;
|
|
21
|
+
const delegation = value;
|
|
22
|
+
if (delegation.enabled !== true ||
|
|
23
|
+
delegation.version !== 1 ||
|
|
24
|
+
typeof delegation.maxParallel !== "number" ||
|
|
25
|
+
!Number.isSafeInteger(delegation.maxParallel) ||
|
|
26
|
+
delegation.maxParallel < 1 ||
|
|
27
|
+
typeof delegation.maxTaskBytes !== "number" ||
|
|
28
|
+
!Number.isSafeInteger(delegation.maxTaskBytes) ||
|
|
29
|
+
delegation.maxTaskBytes < 1 ||
|
|
30
|
+
typeof delegation.workerTimeoutMs !== "number" ||
|
|
31
|
+
!Number.isSafeInteger(delegation.workerTimeoutMs) ||
|
|
32
|
+
delegation.workerTimeoutMs < 1) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
enabled: true,
|
|
37
|
+
version: 1,
|
|
38
|
+
maxParallel: delegation.maxParallel,
|
|
39
|
+
maxTaskBytes: delegation.maxTaskBytes,
|
|
40
|
+
workerTimeoutMs: delegation.workerTimeoutMs,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
18
43
|
export function formatCreditBalance(policy) {
|
|
19
44
|
if (!policy?.usage)
|
|
20
45
|
return undefined;
|
|
@@ -35,6 +60,7 @@ export function readGatewayCache(path = gatewayCachePath()) {
|
|
|
35
60
|
contextWindow: parsed.contextWindow,
|
|
36
61
|
maxTokens: parsed.maxTokens,
|
|
37
62
|
usage: parseUsage(parsed.usage),
|
|
63
|
+
delegation: parseDelegation(parsed.delegation),
|
|
38
64
|
};
|
|
39
65
|
}
|
|
40
66
|
catch {
|
|
@@ -84,6 +110,7 @@ config, options = {}) {
|
|
|
84
110
|
contextWindow: body.contextWindow,
|
|
85
111
|
maxTokens: body.maxTokens,
|
|
86
112
|
usage: parseUsage(body.usage),
|
|
113
|
+
delegation: parseDelegation(body.delegation),
|
|
87
114
|
};
|
|
88
115
|
if (!existsSync(dirname(cachePath)))
|
|
89
116
|
mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from \"../../core/auto-model.ts\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Auto mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Auto\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Auto entry.\n */\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Auto entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: {\n\t\tfetchImpl?: typeof fetch;\n\t\tcachePath?: string;\n\t\ttimeoutMs?: number;\n\t} = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Auto entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: an Auto entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Auto\",\n\t\t\t\t// The agent already owns a stable session ID. Forward it only to the\n\t\t\t\t// Hub, which hashes it before applying provider-specific cache affinity.\n\t\t\t\tcompat: {\n\t\t\t\t\tsendSessionAffinityHeaders: true,\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\",\n\t\t\t\t},\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\t// The Nexus Agent beta is text-only at the Hub boundary, regardless\n\t\t\t\t// of the capabilities of the model selected behind Auto.\n\t\t\t\tinput: [\"text\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from \"../../core/auto-model.ts\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport type { DelegationCapability } from \"../subagent/index.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Auto mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Auto\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Auto entry.\n */\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n\tdelegation?: DelegationCapability;\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nfunction parseDelegation(value: unknown): DelegationCapability | undefined {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst delegation = value as Record<string, unknown>;\n\tif (\n\t\tdelegation.enabled !== true ||\n\t\tdelegation.version !== 1 ||\n\t\ttypeof delegation.maxParallel !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxParallel) ||\n\t\tdelegation.maxParallel < 1 ||\n\t\ttypeof delegation.maxTaskBytes !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxTaskBytes) ||\n\t\tdelegation.maxTaskBytes < 1 ||\n\t\ttypeof delegation.workerTimeoutMs !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.workerTimeoutMs) ||\n\t\tdelegation.workerTimeoutMs < 1\n\t) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tenabled: true,\n\t\tversion: 1,\n\t\tmaxParallel: delegation.maxParallel,\n\t\tmaxTaskBytes: delegation.maxTaskBytes,\n\t\tworkerTimeoutMs: delegation.workerTimeoutMs,\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t\tdelegation: parseDelegation(parsed.delegation),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Auto entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: {\n\t\tfetchImpl?: typeof fetch;\n\t\tcachePath?: string;\n\t\ttimeoutMs?: number;\n\t} = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t\tdelegation: parseDelegation(body.delegation),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Auto entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: an Auto entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Auto\",\n\t\t\t\t// The agent already owns a stable session ID. Forward it only to the\n\t\t\t\t// Hub, which hashes it before applying provider-specific cache affinity.\n\t\t\t\tcompat: {\n\t\t\t\t\tsendSessionAffinityHeaders: true,\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\",\n\t\t\t\t},\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\t// The Nexus Agent beta is text-only at the Hub boundary, regardless\n\t\t\t\t// of the capabilities of the model selected behind Auto.\n\t\t\t\tinput: [\"text\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAqBnE,2EAA2E;AAC3E,wBAAgB,wBAAwB,CACvC,SAAS,GAAE,MAAwB,EACnC,UAAU,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAoB,GAChD,MAAM,GAAG,SAAS,CAMpB;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAgMhE","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 { 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 startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Auto mode 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\tlet gatewayPolicy = readGatewayCache();\n\tregisterAutoProvider(pi, resolveGitNexusConfig(), gatewayPolicy);\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\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.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAsBnE,2EAA2E;AAC3E,wBAAgB,wBAAwB,CACvC,SAAS,GAAE,MAAwB,EACnC,UAAU,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAoB,GAChD,MAAM,GAAG,SAAS,CAMpB;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAkNhE","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 { isDelegatedWorkerProcess, registerDelegateTaskTool } 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// Auto mode 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\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\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"]}
|
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { graphRailStore } from "../../core/graph-rail-store.js";
|
|
6
6
|
import { openBrowser } from "../../utils/open-browser.js";
|
|
7
|
+
import { isDelegatedWorkerProcess, registerDelegateTaskTool } from "../subagent/index.js";
|
|
7
8
|
import { registerAugmentedGrep } from "./augment.js";
|
|
8
9
|
import { registerChangeTracking } from "./changes.js";
|
|
9
10
|
import { GitNexusClient, GitNexusClientError } from "./client.js";
|
|
@@ -37,6 +38,7 @@ export default function gitnexusExtension(pi) {
|
|
|
37
38
|
let current;
|
|
38
39
|
let activeClient;
|
|
39
40
|
let toolsRegistered = false;
|
|
41
|
+
let delegationRegistered = false;
|
|
40
42
|
let startupNotice;
|
|
41
43
|
let loginRequired = false;
|
|
42
44
|
const cwd = process.cwd();
|
|
@@ -44,7 +46,19 @@ export default function gitnexusExtension(pi) {
|
|
|
44
46
|
// resolved before session_start fires, and a provider that appears after
|
|
45
47
|
// that is invisible to it. Reads the policy cache the startup gate wrote.
|
|
46
48
|
let gatewayPolicy = readGatewayCache();
|
|
47
|
-
|
|
49
|
+
const initialConfig = resolveGitNexusConfig();
|
|
50
|
+
registerAutoProvider(pi, initialConfig, gatewayPolicy);
|
|
51
|
+
function registerDelegationIfAvailable() {
|
|
52
|
+
if (delegationRegistered ||
|
|
53
|
+
isDelegatedWorkerProcess() ||
|
|
54
|
+
!resolveGitNexusConfig() ||
|
|
55
|
+
!gatewayPolicy?.delegation) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
registerDelegateTaskTool(pi, gatewayPolicy.delegation);
|
|
59
|
+
delegationRegistered = true;
|
|
60
|
+
}
|
|
61
|
+
registerDelegationIfAvailable();
|
|
48
62
|
function updateCreditStatus(ctx) {
|
|
49
63
|
ctx.ui.setStatus("nexus-credits", formatCreditBalance(gatewayPolicy));
|
|
50
64
|
}
|
|
@@ -122,6 +136,7 @@ export default function gitnexusExtension(pi) {
|
|
|
122
136
|
const config = resolveGitNexusConfig();
|
|
123
137
|
if (config)
|
|
124
138
|
gatewayPolicy = await refreshGatewayPolicy(config);
|
|
139
|
+
registerDelegationIfAvailable();
|
|
125
140
|
updateCreditStatus(ctx);
|
|
126
141
|
});
|
|
127
142
|
// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,
|
|
@@ -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 { 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 startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Auto mode 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\tlet gatewayPolicy = readGatewayCache();\n\tregisterAutoProvider(pi, resolveGitNexusConfig(), gatewayPolicy);\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\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 { isDelegatedWorkerProcess, registerDelegateTaskTool } 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// Auto mode 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\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\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAQnE,eAAO,MAAM,iBAAiB,EAAE,eAAe,EAM9C,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport subagentExtension from \"./subagent/index.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t{ name: \"delegated-worker-runtime\", factory: subagentExtension, hidden: true },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
|
package/dist/extensions/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import gitnexusExtension from "./gitnexus/index.js";
|
|
2
2
|
import mcpExtension from "./mcp/index.js";
|
|
3
|
+
import subagentExtension from "./subagent/index.js";
|
|
3
4
|
import webSearchConcurrencyExtension from "./web-search-concurrency.js";
|
|
4
5
|
// llama.cpp support lives in ./llama and is kept in tree but not registered.
|
|
5
6
|
// Re-add `{ name: "llama.cpp", factory: llamaExtension, hidden: true }` to turn it back on.
|
|
6
7
|
export const builtInExtensions = [
|
|
7
8
|
{ name: "gitnexus", factory: gitnexusExtension },
|
|
9
|
+
{ name: "delegated-worker-runtime", factory: subagentExtension, hidden: true },
|
|
8
10
|
// No-op until the user writes an mcp.json; connecting nothing costs nothing.
|
|
9
11
|
{ name: "mcp", factory: mcpExtension },
|
|
10
12
|
{ name: "web-search-concurrency", factory: webSearchConcurrencyExtension },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,6BAA6B,MAAM,6BAA6B,CAAC;AAExE,6EAA6E;AAC7E,4FAA4F;AAC5F,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE;IAChD,6EAA6E;IAC7E,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE;IACtC,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,6BAA6B,EAAE;CAC1E,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extensions/index.ts"],"names":[],"mappings":"AACA,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAC1C,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,6BAA6B,MAAM,6BAA6B,CAAC;AAExE,6EAA6E;AAC7E,4FAA4F;AAC5F,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IACnD,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE;IAChD,EAAE,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE;IAC9E,6EAA6E;IAC7E,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE;IACtC,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,6BAA6B,EAAE;CAC1E,CAAC","sourcesContent":["import type { InlineExtension } from \"../core/extensions/types.ts\";\nimport gitnexusExtension from \"./gitnexus/index.ts\";\nimport mcpExtension from \"./mcp/index.ts\";\nimport subagentExtension from \"./subagent/index.ts\";\nimport webSearchConcurrencyExtension from \"./web-search-concurrency.ts\";\n\n// llama.cpp support lives in ./llama and is kept in tree but not registered.\n// Re-add `{ name: \"llama.cpp\", factory: llamaExtension, hidden: true }` to turn it back on.\nexport const builtInExtensions: InlineExtension[] = [\n\t{ name: \"gitnexus\", factory: gitnexusExtension },\n\t{ name: \"delegated-worker-runtime\", factory: subagentExtension, hidden: true },\n\t// No-op until the user writes an mcp.json; connecting nothing costs nothing.\n\t{ name: \"mcp\", factory: mcpExtension },\n\t{ name: \"web-search-concurrency\", factory: webSearchConcurrencyExtension },\n];\n"]}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal delegated-worker runtime.
|
|
3
|
+
*
|
|
4
|
+
* This file deliberately does not register a user-facing tool by default. The
|
|
5
|
+
* authenticated Hub policy must advertise delegation before the GitNexus
|
|
6
|
+
* extension calls registerDelegateTaskTool(). The CLI owns process isolation
|
|
7
|
+
* and streaming UX; the Hub owns one-time worker authorization and routing.
|
|
8
|
+
*/
|
|
9
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
10
|
+
import type { ExtensionAPI } from "../../core/extensions/types.ts";
|
|
11
|
+
import type { GitNexusConfig } from "../gitnexus/config.ts";
|
|
12
|
+
export declare const DELEGATE_TASK_TOOL_NAME = "delegate_task";
|
|
13
|
+
export declare const INTERNAL_WORKER_MARKER_ENV = "NEXUS_INTERNAL_DELEGATED_WORKER";
|
|
14
|
+
export declare const INTERNAL_WORKER_SESSION_ENV = "NEXUS_INTERNAL_WORKER_SESSION_ID";
|
|
15
|
+
export declare const INTERNAL_WORKER_TASK_ENV = "NEXUS_INTERNAL_WORKER_TASK_ID";
|
|
16
|
+
export declare const INTERNAL_WORKER_RUN_ENV = "NEXUS_INTERNAL_WORKER_RUN_ID";
|
|
17
|
+
export declare const INTERNAL_WORKER_ROOT_REQUEST_ENV = "NEXUS_INTERNAL_WORKER_ROOT_REQUEST_ID";
|
|
18
|
+
export declare const INTERNAL_WORKER_PARENT_REQUEST_ENV = "NEXUS_INTERNAL_WORKER_PARENT_REQUEST_ID";
|
|
19
|
+
export declare const INTERNAL_WORKER_GRANT_HEADER = "x-nexus-worker-grant";
|
|
20
|
+
export declare const INTERNAL_WORKER_TASK_HEADER = "x-nexus-worker-task-id";
|
|
21
|
+
export declare const SESSION_AFFINITY_HEADER = "x-session-affinity";
|
|
22
|
+
export declare const RUN_ID_HEADER = "x-nexus-run-id";
|
|
23
|
+
export declare const REQUEST_ID_HEADER = "x-nexus-request-id";
|
|
24
|
+
export declare const ROOT_REQUEST_ID_HEADER = "x-nexus-root-request-id";
|
|
25
|
+
export declare const PARENT_REQUEST_ID_HEADER = "x-nexus-parent-request-id";
|
|
26
|
+
export declare const DELEGATION_ID_HEADER = "x-nexus-delegation-id";
|
|
27
|
+
export declare const REQUEST_SCOPE_HEADER = "x-nexus-request-scope";
|
|
28
|
+
export declare const MAX_DELEGATED_TURNS = 12;
|
|
29
|
+
export declare const MAX_DELEGATIONS_PER_RUN = 3;
|
|
30
|
+
export declare const DELEGATED_WORKER_TIMEOUT_MS: number;
|
|
31
|
+
export declare const DELEGATED_OUTPUT_CAP_BYTES: number;
|
|
32
|
+
export declare const DELEGATED_EVENT_CAP_BYTES: number;
|
|
33
|
+
export declare const MAX_DELEGATED_TASK_BYTES: number;
|
|
34
|
+
export declare const DELEGATED_WORKER_TOOLS: readonly ["read", "grep", "find", "ls", "bash", "write", "edit", "gitnexus_query", "gitnexus_context", "gitnexus_impact", "gitnexus_detect_changes", "gitnexus_cypher", "gitnexus_list_repos"];
|
|
35
|
+
export type DelegationStatus = "running" | "completed" | "failed" | "aborted" | "timeout" | "turn_limit" | "event_limit";
|
|
36
|
+
export interface DelegationDetails {
|
|
37
|
+
status: DelegationStatus;
|
|
38
|
+
turns: number;
|
|
39
|
+
exitCode?: number;
|
|
40
|
+
outputTruncated?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface WorkerGrant {
|
|
43
|
+
/** Opaque, short-lived grant minted by the Hub for one worker completion. */
|
|
44
|
+
token: string;
|
|
45
|
+
expiresAt: string;
|
|
46
|
+
}
|
|
47
|
+
export interface DelegationCapability {
|
|
48
|
+
enabled: true;
|
|
49
|
+
version: 1;
|
|
50
|
+
maxParallel: number;
|
|
51
|
+
maxTaskBytes: number;
|
|
52
|
+
workerTimeoutMs: number;
|
|
53
|
+
}
|
|
54
|
+
export interface WorkerGrantRequest {
|
|
55
|
+
sessionId: string;
|
|
56
|
+
taskId: string;
|
|
57
|
+
runId: string;
|
|
58
|
+
rootRequestId: string;
|
|
59
|
+
parentRequestId: string;
|
|
60
|
+
requestId: string;
|
|
61
|
+
signal: AbortSignal | undefined;
|
|
62
|
+
}
|
|
63
|
+
export interface DelegationController {
|
|
64
|
+
createWorkerGrant(request: WorkerGrantRequest): Promise<WorkerGrant>;
|
|
65
|
+
}
|
|
66
|
+
export interface DelegationRequest {
|
|
67
|
+
task: string;
|
|
68
|
+
acceptanceCriteria: string[];
|
|
69
|
+
relevantPaths: string[];
|
|
70
|
+
cwd: string;
|
|
71
|
+
signal: AbortSignal | undefined;
|
|
72
|
+
}
|
|
73
|
+
export interface WorkerContext {
|
|
74
|
+
sessionId: string;
|
|
75
|
+
taskId: string;
|
|
76
|
+
runId: string;
|
|
77
|
+
rootRequestId: string;
|
|
78
|
+
parentRequestId: string;
|
|
79
|
+
}
|
|
80
|
+
export interface DelegatedWorkerOptions extends DelegationRequest {
|
|
81
|
+
workerContext: WorkerContext;
|
|
82
|
+
timeoutMs?: number;
|
|
83
|
+
onUpdate?: (result: AgentToolResult<DelegationDetails>) => void;
|
|
84
|
+
}
|
|
85
|
+
export interface DelegatedWorkerResult {
|
|
86
|
+
status: Exclude<DelegationStatus, "running">;
|
|
87
|
+
turns: number;
|
|
88
|
+
exitCode: number;
|
|
89
|
+
output: string;
|
|
90
|
+
outputTruncated: boolean;
|
|
91
|
+
}
|
|
92
|
+
export declare function composeDelegatedTask(task: string, acceptanceCriteria: readonly string[], relevantPaths: readonly string[]): string;
|
|
93
|
+
export declare function buildDelegatedWorkerArgs(task: string, promptPath: string): string[];
|
|
94
|
+
export declare function createHubDelegationController(config: GitNexusConfig, options?: {
|
|
95
|
+
fetchImpl?: typeof fetch;
|
|
96
|
+
timeoutMs?: number;
|
|
97
|
+
}): DelegationController;
|
|
98
|
+
export declare function isDelegatedWorkerProcess(env?: NodeJS.ProcessEnv): boolean;
|
|
99
|
+
/** Capture task routing once, then remove it before worker tools can inherit it. */
|
|
100
|
+
export declare function consumeWorkerContext(env?: NodeJS.ProcessEnv): WorkerContext | undefined;
|
|
101
|
+
export declare function runDelegatedWorker(options: DelegatedWorkerOptions): Promise<DelegatedWorkerResult>;
|
|
102
|
+
/** Register the tool only after the Hub explicitly advertises delegation. */
|
|
103
|
+
export declare function registerDelegateTaskTool(pi: ExtensionAPI, capability: DelegationCapability): void;
|
|
104
|
+
/**
|
|
105
|
+
* Install a fail-closed header hook for one worker task. A fresh one-time grant
|
|
106
|
+
* is minted for each model turn; no grant is inherited by worker tools.
|
|
107
|
+
*/
|
|
108
|
+
export declare function registerWorkerGrantHeaders(pi: ExtensionAPI, workerContext: WorkerContext, controller: DelegationController | undefined): void;
|
|
109
|
+
export default function delegatedWorkerRuntime(pi: ExtensionAPI): void;
|
|
110
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/subagent/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Internal delegated-worker runtime.\n *\n * This file deliberately does not register a user-facing tool by default. The\n * authenticated Hub policy must advertise delegation before the GitNexus\n * extension calls registerDelegateTaskTool(). The CLI owns process isolation\n * and streaming UX; the Hub owns one-time worker authorization and routing.\n */\n\nimport { execFile, spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { AgentToolResult } from \"@earendil-works/pi-agent-core\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport { Type } from \"typebox\";\nimport { AUTO_MODEL_ID, AUTO_PROVIDER_ID, isAutoModel } from \"../../core/auto-model.ts\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { withFileMutationQueue } from \"../../core/tools/index.ts\";\nimport type { GitNexusConfig } from \"../gitnexus/config.ts\";\nimport { resolveGitNexusConfig } from \"../gitnexus/config.ts\";\nimport { hubApiBase } from \"../gitnexus/device-auth.ts\";\n\nexport const DELEGATE_TASK_TOOL_NAME = \"delegate_task\";\nexport const INTERNAL_WORKER_MARKER_ENV = \"NEXUS_INTERNAL_DELEGATED_WORKER\";\nexport const INTERNAL_WORKER_SESSION_ENV = \"NEXUS_INTERNAL_WORKER_SESSION_ID\";\nexport const INTERNAL_WORKER_TASK_ENV = \"NEXUS_INTERNAL_WORKER_TASK_ID\";\nexport const INTERNAL_WORKER_RUN_ENV = \"NEXUS_INTERNAL_WORKER_RUN_ID\";\nexport const INTERNAL_WORKER_ROOT_REQUEST_ENV = \"NEXUS_INTERNAL_WORKER_ROOT_REQUEST_ID\";\nexport const INTERNAL_WORKER_PARENT_REQUEST_ENV = \"NEXUS_INTERNAL_WORKER_PARENT_REQUEST_ID\";\nexport const INTERNAL_WORKER_GRANT_HEADER = \"x-nexus-worker-grant\";\nexport const INTERNAL_WORKER_TASK_HEADER = \"x-nexus-worker-task-id\";\nexport const SESSION_AFFINITY_HEADER = \"x-session-affinity\";\nexport const RUN_ID_HEADER = \"x-nexus-run-id\";\nexport const REQUEST_ID_HEADER = \"x-nexus-request-id\";\nexport const ROOT_REQUEST_ID_HEADER = \"x-nexus-root-request-id\";\nexport const PARENT_REQUEST_ID_HEADER = \"x-nexus-parent-request-id\";\nexport const DELEGATION_ID_HEADER = \"x-nexus-delegation-id\";\nexport const REQUEST_SCOPE_HEADER = \"x-nexus-request-scope\";\nexport const MAX_DELEGATED_TURNS = 12;\nexport const MAX_DELEGATIONS_PER_RUN = 3;\nexport const DELEGATED_WORKER_TIMEOUT_MS = 5 * 60 * 1000;\nexport const DELEGATED_OUTPUT_CAP_BYTES = 32 * 1024;\nexport const DELEGATED_EVENT_CAP_BYTES = 1024 * 1024;\nexport const MAX_DELEGATED_TASK_BYTES = 32 * 1024;\n\nconst WORKER_GRANT_TIMEOUT_MS = 10_000;\nconst INVALID_WORKER_GRANT = \"unavailable\";\nconst MAX_ROOT_REVIEW_TOOL_CALLS = 8;\nconst MAX_PARALLEL_MUTATING_WORKERS = 1;\n\nconst ROOT_REVIEW_TOOLS = new Set([\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"gitnexus_query\",\n\t\"gitnexus_context\",\n\t\"gitnexus_impact\",\n\t\"gitnexus_detect_changes\",\n\t\"gitnexus_cypher\",\n\t\"gitnexus_list_repos\",\n\t\"web_search\",\n\t\"web_fetch\",\n]);\n\nexport const DELEGATED_WORKER_TOOLS = [\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"bash\",\n\t\"write\",\n\t\"edit\",\n\t\"gitnexus_query\",\n\t\"gitnexus_context\",\n\t\"gitnexus_impact\",\n\t\"gitnexus_detect_changes\",\n\t\"gitnexus_cypher\",\n\t\"gitnexus_list_repos\",\n] as const;\n\nconst WORKER_SYSTEM_PROMPT = `You are an internal implementation worker for Nexus Agent.\n\nComplete only the narrowly delegated task. Inspect the repository before editing, follow its local instructions, and verify the changed behavior. Do not delegate work or broaden scope. Do not discuss model names, providers, routing, credits, or internal architecture.\n\nFinish with a concise report containing:\n- changes made\n- verification run and results\n- blockers or remaining risks`;\n\nexport type DelegationStatus =\n\t| \"running\"\n\t| \"completed\"\n\t| \"failed\"\n\t| \"aborted\"\n\t| \"timeout\"\n\t| \"turn_limit\"\n\t| \"event_limit\";\n\nexport interface DelegationDetails {\n\tstatus: DelegationStatus;\n\tturns: number;\n\texitCode?: number;\n\toutputTruncated?: boolean;\n}\n\nexport interface WorkerGrant {\n\t/** Opaque, short-lived grant minted by the Hub for one worker completion. */\n\ttoken: string;\n\texpiresAt: string;\n}\n\nexport interface DelegationCapability {\n\tenabled: true;\n\tversion: 1;\n\tmaxParallel: number;\n\tmaxTaskBytes: number;\n\tworkerTimeoutMs: number;\n}\n\nexport interface WorkerGrantRequest {\n\tsessionId: string;\n\ttaskId: string;\n\trunId: string;\n\trootRequestId: string;\n\tparentRequestId: string;\n\trequestId: string;\n\tsignal: AbortSignal | undefined;\n}\n\nexport interface DelegationController {\n\tcreateWorkerGrant(request: WorkerGrantRequest): Promise<WorkerGrant>;\n}\n\nexport interface DelegationRequest {\n\ttask: string;\n\tacceptanceCriteria: string[];\n\trelevantPaths: string[];\n\tcwd: string;\n\tsignal: AbortSignal | undefined;\n}\n\nexport interface WorkerContext {\n\tsessionId: string;\n\ttaskId: string;\n\trunId: string;\n\trootRequestId: string;\n\tparentRequestId: string;\n}\n\nexport interface DelegatedWorkerOptions extends DelegationRequest {\n\tworkerContext: WorkerContext;\n\ttimeoutMs?: number;\n\tonUpdate?: (result: AgentToolResult<DelegationDetails>) => void;\n}\n\nexport interface DelegatedWorkerResult {\n\tstatus: Exclude<DelegationStatus, \"running\">;\n\tturns: number;\n\texitCode: number;\n\toutput: string;\n\toutputTruncated: boolean;\n}\n\ninterface AssistantSnapshot {\n\ttext: string;\n\tstopReason?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction parseAssistantSnapshot(value: unknown): AssistantSnapshot | undefined {\n\tif (!isRecord(value) || value.role !== \"assistant\" || !Array.isArray(value.content)) return undefined;\n\tconst text = value.content\n\t\t.filter(isRecord)\n\t\t.filter((part) => part.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text as string)\n\t\t.join(\"\\n\");\n\treturn {\n\t\ttext,\n\t\t...(typeof value.stopReason === \"string\" ? { stopReason: value.stopReason } : {}),\n\t};\n}\n\nexport function composeDelegatedTask(\n\ttask: string,\n\tacceptanceCriteria: readonly string[],\n\trelevantPaths: readonly string[],\n): string {\n\tconst sections = [`Task:\\n${task.trim()}`];\n\tif (acceptanceCriteria.length > 0) {\n\t\tsections.push(`Acceptance criteria:\\n${acceptanceCriteria.map((criterion) => `- ${criterion}`).join(\"\\n\")}`);\n\t}\n\tif (relevantPaths.length > 0) {\n\t\tsections.push(`Relevant paths:\\n${relevantPaths.map((filePath) => `- ${filePath}`).join(\"\\n\")}`);\n\t}\n\treturn sections.join(\"\\n\\n\");\n}\n\nexport function buildDelegatedWorkerArgs(task: string, promptPath: string): string[] {\n\treturn [\n\t\t\"--mode\",\n\t\t\"json\",\n\t\t\"-p\",\n\t\t\"--no-session\",\n\t\t\"--model\",\n\t\t`${AUTO_PROVIDER_ID}/${AUTO_MODEL_ID}`,\n\t\t\"--tools\",\n\t\tDELEGATED_WORKER_TOOLS.join(\",\"),\n\t\t\"--exclude-tools\",\n\t\t`${DELEGATE_TASK_TOOL_NAME},subagent`,\n\t\t\"--append-system-prompt\",\n\t\tpromptPath,\n\t\ttask,\n\t];\n}\n\nfunction getNexusInvocation(args: string[]): { command: string; args: string[] } {\n\tconst currentScript = process.argv[1];\n\tconst isBunVirtualScript = currentScript?.startsWith(\"/$bunfs/root/\");\n\tif (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {\n\t\treturn { command: process.execPath, args: [currentScript, ...args] };\n\t}\n\n\tconst execName = path.basename(process.execPath).toLowerCase();\n\tconst isGenericRuntime = /^(node|bun)(\\.exe)?$/.test(execName);\n\treturn isGenericRuntime ? { command: \"nexus\", args } : { command: process.execPath, args };\n}\n\nasync function writeWorkerPrompt(): Promise<{ dir: string; filePath: string }> {\n\tconst dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), \"nexus-worker-\"));\n\tconst filePath = path.join(dir, \"system.md\");\n\tawait withFileMutationQueue(filePath, async () => {\n\t\tawait fs.promises.writeFile(filePath, WORKER_SYSTEM_PROMPT, { encoding: \"utf8\", mode: 0o600 });\n\t});\n\treturn { dir, filePath };\n}\n\nfunction truncateUtf8(value: string, cap: number): { value: string; truncated: boolean } {\n\tif (Buffer.byteLength(value, \"utf8\") <= cap) return { value, truncated: false };\n\tconst suffix = \"\\n\\n[Report truncated]\";\n\tlet truncated = value.slice(0, Math.max(0, cap - Buffer.byteLength(suffix, \"utf8\")));\n\twhile (Buffer.byteLength(`${truncated}${suffix}`, \"utf8\") > cap) truncated = truncated.slice(0, -1);\n\treturn { value: `${truncated}${suffix}`, truncated: true };\n}\n\nexport function createHubDelegationController(\n\tconfig: GitNexusConfig,\n\toptions: { fetchImpl?: typeof fetch; timeoutMs?: number } = {},\n): DelegationController {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\treturn {\n\t\tasync createWorkerGrant(request) {\n\t\t\tconst controller = new AbortController();\n\t\t\tconst onAbort = (): void => controller.abort();\n\t\t\tif (request.signal?.aborted) controller.abort();\n\t\t\telse request.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tconst timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? WORKER_GRANT_TIMEOUT_MS);\n\t\t\ttry {\n\t\t\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/worker-grants`, {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\t\t\t[RUN_ID_HEADER]: request.runId,\n\t\t\t\t\t\t[REQUEST_ID_HEADER]: request.requestId,\n\t\t\t\t\t\t[ROOT_REQUEST_ID_HEADER]: request.rootRequestId,\n\t\t\t\t\t\t[PARENT_REQUEST_ID_HEADER]: request.parentRequestId,\n\t\t\t\t\t\t[DELEGATION_ID_HEADER]: request.taskId,\n\t\t\t\t\t\t[REQUEST_SCOPE_HEADER]: \"worker\",\n\t\t\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify({ sessionId: request.sessionId, taskId: request.taskId }),\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t});\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tawait response.body?.cancel().catch(() => {});\n\t\t\t\t\tthrow new Error(\"Worker authorization unavailable\");\n\t\t\t\t}\n\t\t\t\tconst body: unknown = await response.json();\n\t\t\t\tif (\n\t\t\t\t\t!isRecord(body) ||\n\t\t\t\t\ttypeof body.grant !== \"string\" ||\n\t\t\t\t\t!body.grant.trim() ||\n\t\t\t\t\ttypeof body.expiresAt !== \"string\" ||\n\t\t\t\t\t!body.expiresAt.trim() ||\n\t\t\t\t\t!Number.isFinite(Date.parse(body.expiresAt))\n\t\t\t\t) {\n\t\t\t\t\tthrow new Error(\"Invalid worker authorization response\");\n\t\t\t\t}\n\t\t\t\treturn { token: body.grant, expiresAt: body.expiresAt };\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\trequest.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport function isDelegatedWorkerProcess(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn env[INTERNAL_WORKER_MARKER_ENV] === \"1\";\n}\n\n/** Capture task routing once, then remove it before worker tools can inherit it. */\nexport function consumeWorkerContext(env: NodeJS.ProcessEnv = process.env): WorkerContext | undefined {\n\tif (!isDelegatedWorkerProcess(env)) return undefined;\n\tconst sessionId = env[INTERNAL_WORKER_SESSION_ENV]?.trim();\n\tconst taskId = env[INTERNAL_WORKER_TASK_ENV]?.trim();\n\tconst runId = env[INTERNAL_WORKER_RUN_ENV]?.trim();\n\tconst rootRequestId = env[INTERNAL_WORKER_ROOT_REQUEST_ENV]?.trim();\n\tconst parentRequestId = env[INTERNAL_WORKER_PARENT_REQUEST_ENV]?.trim();\n\tdelete env[INTERNAL_WORKER_SESSION_ENV];\n\tdelete env[INTERNAL_WORKER_TASK_ENV];\n\tdelete env[INTERNAL_WORKER_RUN_ENV];\n\tdelete env[INTERNAL_WORKER_ROOT_REQUEST_ENV];\n\tdelete env[INTERNAL_WORKER_PARENT_REQUEST_ENV];\n\tif (!sessionId || !taskId || !runId || !rootRequestId || !parentRequestId) return undefined;\n\treturn { sessionId, taskId, runId, rootRequestId, parentRequestId };\n}\n\nfunction killProcessTree(pid: number | undefined, signal: NodeJS.Signals): void {\n\tif (pid === undefined) return;\n\tif (process.platform === \"win32\") {\n\t\texecFile(\"taskkill\", [\"/T\", \"/F\", \"/PID\", String(pid)], () => undefined);\n\t\treturn;\n\t}\n\ttry {\n\t\tprocess.kill(-pid, signal);\n\t} catch {\n\t\t// The detached process group may already have exited.\n\t}\n}\n\nexport async function runDelegatedWorker(options: DelegatedWorkerOptions): Promise<DelegatedWorkerResult> {\n\tconst prompt = await writeWorkerPrompt();\n\tconst task = composeDelegatedTask(options.task, options.acceptanceCriteria, options.relevantPaths);\n\tconst args = buildDelegatedWorkerArgs(task, prompt.filePath);\n\tlet output = \"\";\n\tlet turns = 0;\n\tlet terminalStatus: Exclude<DelegationStatus, \"running\" | \"completed\" | \"failed\"> | undefined;\n\tlet assistantFailed = false;\n\tlet eventBytes = 0;\n\n\ttry {\n\t\tconst exitCode = await new Promise<number>((resolve) => {\n\t\t\tconst invocation = getNexusInvocation(args);\n\t\t\tconst child = spawn(invocation.command, invocation.args, {\n\t\t\t\tcwd: options.cwd,\n\t\t\t\tdetached: true,\n\t\t\t\tenv: {\n\t\t\t\t\t...process.env,\n\t\t\t\t\t[INTERNAL_WORKER_MARKER_ENV]: \"1\",\n\t\t\t\t\t[INTERNAL_WORKER_SESSION_ENV]: options.workerContext.sessionId,\n\t\t\t\t\t[INTERNAL_WORKER_TASK_ENV]: options.workerContext.taskId,\n\t\t\t\t\t[INTERNAL_WORKER_RUN_ENV]: options.workerContext.runId,\n\t\t\t\t\t[INTERNAL_WORKER_ROOT_REQUEST_ENV]: options.workerContext.rootRequestId,\n\t\t\t\t\t[INTERNAL_WORKER_PARENT_REQUEST_ENV]: options.workerContext.parentRequestId,\n\t\t\t\t},\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tlet buffer = \"\";\n\t\t\tlet settled = false;\n\t\t\tlet stopped = false;\n\t\t\tlet forceKillTimer: NodeJS.Timeout | undefined;\n\n\t\t\tconst finish = (code: number): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tclearTimeout(timeoutTimer);\n\t\t\t\tclearTimeout(forceKillTimer);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", abortHandler);\n\t\t\t\tresolve(code);\n\t\t\t};\n\n\t\t\tconst stop = (status: \"aborted\" | \"timeout\" | \"turn_limit\" | \"event_limit\"): void => {\n\t\t\t\tif (stopped) return;\n\t\t\t\tstopped = true;\n\t\t\t\tterminalStatus = status;\n\t\t\t\tkillProcessTree(child.pid, \"SIGTERM\");\n\t\t\t\tforceKillTimer = setTimeout(() => {\n\t\t\t\t\tif (process.platform !== \"win32\") killProcessTree(child.pid, \"SIGKILL\");\n\t\t\t\t\tfinish(1);\n\t\t\t\t}, 5000);\n\t\t\t};\n\n\t\t\tconst emitProgress = (): void => {\n\t\t\t\toptions.onUpdate?.({\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work is in progress.\" }],\n\t\t\t\t\tdetails: { status: \"running\", turns },\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tconst processLine = (line: string): void => {\n\t\t\t\tif (!line.trim()) return;\n\t\t\t\tlet parsed: unknown;\n\t\t\t\ttry {\n\t\t\t\t\tparsed = JSON.parse(line);\n\t\t\t\t} catch {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!isRecord(parsed) || parsed.type !== \"message_end\") return;\n\t\t\t\tconst snapshot = parseAssistantSnapshot(parsed.message);\n\t\t\t\tif (!snapshot) return;\n\t\t\t\tturns += 1;\n\t\t\t\tif (snapshot.text) output = snapshot.text;\n\t\t\t\tif (snapshot.stopReason === \"error\" || snapshot.stopReason === \"aborted\") assistantFailed = true;\n\t\t\t\temitProgress();\n\t\t\t\tif (turns >= MAX_DELEGATED_TURNS) stop(\"turn_limit\");\n\t\t\t};\n\n\t\t\tconst abortHandler = (): void => stop(\"aborted\");\n\t\t\tconst timeoutTimer = setTimeout(\n\t\t\t\t() => stop(\"timeout\"),\n\t\t\t\tMath.min(Math.max(1_000, options.timeoutMs ?? DELEGATED_WORKER_TIMEOUT_MS), DELEGATED_WORKER_TIMEOUT_MS),\n\t\t\t);\n\t\t\tchild.stdout.on(\"data\", (chunk: Buffer | string) => {\n\t\t\t\teventBytes += Buffer.byteLength(chunk);\n\t\t\t\tif (eventBytes > DELEGATED_EVENT_CAP_BYTES) {\n\t\t\t\t\tassistantFailed = true;\n\t\t\t\t\tstop(\"event_limit\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tbuffer += chunk.toString();\n\t\t\t\tif (Buffer.byteLength(buffer, \"utf8\") > DELEGATED_EVENT_CAP_BYTES) {\n\t\t\t\t\tbuffer = \"\";\n\t\t\t\t\tassistantFailed = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() ?? \"\";\n\t\t\t\tfor (const line of lines) processLine(line);\n\t\t\t});\n\t\t\t// Consume stderr so a noisy child cannot block. Raw provider/process output\n\t\t\t// is intentionally not retained or exposed to the orchestrator or user.\n\t\t\tchild.stderr.on(\"data\", () => undefined);\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (buffer.trim()) processLine(buffer);\n\t\t\t\tfinish(code ?? (terminalStatus ? 1 : 0));\n\t\t\t});\n\t\t\tchild.on(\"error\", () => finish(1));\n\t\t\tif (options.signal?.aborted) abortHandler();\n\t\t\telse options.signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\t\t});\n\n\t\tconst limitedOutput = truncateUtf8(output, DELEGATED_OUTPUT_CAP_BYTES);\n\t\tconst status = terminalStatus ?? (exitCode === 0 && !assistantFailed ? \"completed\" : \"failed\");\n\t\treturn {\n\t\t\tstatus,\n\t\t\tturns,\n\t\t\texitCode,\n\t\t\toutput: limitedOutput.value,\n\t\t\toutputTruncated: limitedOutput.truncated,\n\t\t};\n\t} finally {\n\t\tawait fs.promises.rm(prompt.dir, { recursive: true, force: true });\n\t}\n}\n\nconst DelegateTaskParameters = Type.Object({\n\ttask: Type.String({ description: \"A narrow implementation task with explicit boundaries\", maxLength: 32_768 }),\n\tacceptanceCriteria: Type.Optional(Type.Array(Type.String({ maxLength: 2000 }), { maxItems: 10 })),\n\trelevantPaths: Type.Optional(Type.Array(Type.String({ maxLength: 1000 }), { maxItems: 20 })),\n});\n\nfunction resultContent(result: DelegatedWorkerResult): string {\n\tconst report = result.output.trim() || \"No implementation report was returned.\";\n\tif (result.status === \"completed\") return `Delegated implementation completed.\\n\\n${report}`;\n\treturn `Delegated implementation did not complete (${result.status}).\\n\\nPartial report:\\n${report}`;\n}\n\n/** Register the tool only after the Hub explicitly advertises delegation. */\nexport function registerDelegateTaskTool(pi: ExtensionAPI, capability: DelegationCapability): void {\n\tconst maxTaskBytes = Math.min(Math.max(1, Math.floor(capability.maxTaskBytes)), MAX_DELEGATED_TASK_BYTES);\n\tconst workerTimeoutMs = Math.min(\n\t\tMath.max(1_000, Math.floor(capability.workerTimeoutMs)),\n\t\tDELEGATED_WORKER_TIMEOUT_MS,\n\t);\n\tconst runId = randomUUID();\n\tlet rootRequestId: string | undefined;\n\tlet latestRootRequestId: string | undefined;\n\tlet activeWorkers = 0;\n\tlet invocationCount = 0;\n\tlet rootReviewToolCalls = 0;\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tinvocationCount = 0;\n\t\trootReviewToolCalls = 0;\n\t\treturn {\n\t\t\tsystemPrompt: `${event.systemPrompt}\\n\\nImplementation policy: perform implementation, mutation, shell, and repeated tool loops through delegate_task. Keep this parent conversation focused on planning and final review. Use only bounded read-only tools to inspect worker changes, verify graph impact, and reach the final answer.`,\n\t\t};\n\t});\n\tpi.on(\"before_provider_headers\", (event, ctx) => {\n\t\tif (!isAutoModel(ctx.model) || isDelegatedWorkerProcess()) return;\n\t\tconst requestId = randomUUID();\n\t\trootRequestId ??= requestId;\n\t\tlatestRootRequestId = requestId;\n\t\tevent.headers[RUN_ID_HEADER] = runId;\n\t\tevent.headers[REQUEST_ID_HEADER] = requestId;\n\t\tevent.headers[ROOT_REQUEST_ID_HEADER] = rootRequestId;\n\t\tevent.headers[REQUEST_SCOPE_HEADER] = \"root\";\n\t});\n\tpi.on(\"tool_call\", (event, ctx) => {\n\t\tif (!isAutoModel(ctx.model) || isDelegatedWorkerProcess() || event.toolName === DELEGATE_TASK_TOOL_NAME) {\n\t\t\treturn;\n\t\t}\n\t\tif (!ROOT_REVIEW_TOOLS.has(event.toolName)) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason:\n\t\t\t\t\t\"This parent run is review-only. Use delegate_task for implementation, mutation, shell commands, tests, or other execution.\",\n\t\t\t};\n\t\t}\n\t\tif (rootReviewToolCalls >= MAX_ROOT_REVIEW_TOOL_CALLS) {\n\t\t\treturn {\n\t\t\t\tblock: true,\n\t\t\t\treason:\n\t\t\t\t\t\"The bounded parent review budget is exhausted. Delegate further investigation or finalize the review.\",\n\t\t\t};\n\t\t}\n\t\trootReviewToolCalls += 1;\n\t});\n\n\tpi.registerTool({\n\t\tname: DELEGATE_TASK_TOOL_NAME,\n\t\tlabel: \"Work\",\n\t\tdescription:\n\t\t\t\"Delegate one narrow implementation task to an isolated internal worker. Keep planning, user communication, and final verification in this conversation.\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use delegate_task for bounded implementation, mutation, command execution, tests, and repeated tool work after you have formed a plan.\",\n\t\t\t\"You remain responsible for reviewing the returned report, verifying the result, and communicating with the user.\",\n\t\t],\n\t\tparameters: DelegateTaskParameters,\n\t\texecutionMode: \"sequential\",\n\t\tasync execute(_toolCallId, params, signal, onUpdate, ctx) {\n\t\t\tif (!isAutoModel(ctx.model)) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work is unavailable for the active mode.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!ctx.isProjectTrusted()) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work requires a trusted project.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (activeWorkers >= MAX_PARALLEL_MUTATING_WORKERS || invocationCount >= MAX_DELEGATIONS_PER_RUN) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work is temporarily unavailable for this run.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst request: DelegationRequest = {\n\t\t\t\ttask: params.task,\n\t\t\t\tacceptanceCriteria: params.acceptanceCriteria ?? [],\n\t\t\t\trelevantPaths: params.relevantPaths ?? [],\n\t\t\t\tcwd: ctx.cwd,\n\t\t\t\tsignal,\n\t\t\t};\n\t\t\tif (\n\t\t\t\tBuffer.byteLength(\n\t\t\t\t\tcomposeDelegatedTask(request.task, request.acceptanceCriteria, request.relevantPaths),\n\t\t\t\t\t\"utf8\",\n\t\t\t\t) > maxTaskBytes\n\t\t\t) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work is too large for one bounded task.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tactiveWorkers += 1;\n\t\t\tinvocationCount += 1;\n\t\t\ttry {\n\t\t\t\tconst delegationId = randomUUID();\n\t\t\t\trootRequestId ??= randomUUID();\n\t\t\t\tconst result = await runDelegatedWorker({\n\t\t\t\t\t...request,\n\t\t\t\t\tworkerContext: {\n\t\t\t\t\t\tsessionId: ctx.sessionManager.getSessionId(),\n\t\t\t\t\t\ttaskId: delegationId,\n\t\t\t\t\t\trunId,\n\t\t\t\t\t\trootRequestId,\n\t\t\t\t\t\tparentRequestId: latestRootRequestId ?? rootRequestId,\n\t\t\t\t\t},\n\t\t\t\t\ttimeoutMs: workerTimeoutMs,\n\t\t\t\t\tonUpdate,\n\t\t\t\t});\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: resultContent(result) }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\tstatus: result.status,\n\t\t\t\t\t\tturns: result.turns,\n\t\t\t\t\t\texitCode: result.exitCode,\n\t\t\t\t\t\toutputTruncated: result.outputTruncated,\n\t\t\t\t\t} satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t} catch {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Delegated work could not be started.\" }],\n\t\t\t\t\tdetails: { status: \"failed\", turns: 0 } satisfies DelegationDetails,\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\tactiveWorkers -= 1;\n\t\t\t}\n\t\t},\n\t\trenderCall(_args, theme) {\n\t\t\treturn new Text(theme.fg(\"toolTitle\", theme.bold(\"Working…\")), 0, 0);\n\t\t},\n\t\trenderResult(result, _options, theme) {\n\t\t\tconst details = result.details as DelegationDetails | undefined;\n\t\t\tif (details?.status === \"running\") return new Text(theme.fg(\"muted\", \"Working…\"), 0, 0);\n\t\t\tif (details?.status === \"completed\") return new Text(theme.fg(\"success\", \"Work completed\"), 0, 0);\n\t\t\treturn new Text(theme.fg(\"error\", \"Work needs attention\"), 0, 0);\n\t\t},\n\t});\n}\n\n/**\n * Install a fail-closed header hook for one worker task. A fresh one-time grant\n * is minted for each model turn; no grant is inherited by worker tools.\n */\nexport function registerWorkerGrantHeaders(\n\tpi: ExtensionAPI,\n\tworkerContext: WorkerContext,\n\tcontroller: DelegationController | undefined,\n): void {\n\tpi.on(\"before_provider_headers\", async (event, ctx) => {\n\t\tif (!isAutoModel(ctx.model)) {\n\t\t\tctx.abort();\n\t\t\treturn;\n\t\t}\n\n\t\t// Set an invalid sentinel first. Extension handler errors are reported and\n\t\t// swallowed by the runner, so this prevents a mint failure from silently\n\t\t// falling back to an ungranted root request.\n\t\tconst requestId = randomUUID();\n\t\tevent.headers[INTERNAL_WORKER_GRANT_HEADER] = INVALID_WORKER_GRANT;\n\t\tevent.headers[INTERNAL_WORKER_TASK_HEADER] = workerContext.taskId;\n\t\tevent.headers[SESSION_AFFINITY_HEADER] = workerContext.sessionId;\n\t\tevent.headers[RUN_ID_HEADER] = workerContext.runId;\n\t\tevent.headers[REQUEST_ID_HEADER] = requestId;\n\t\tevent.headers[ROOT_REQUEST_ID_HEADER] = workerContext.rootRequestId;\n\t\tevent.headers[PARENT_REQUEST_ID_HEADER] = workerContext.parentRequestId;\n\t\tevent.headers[DELEGATION_ID_HEADER] = workerContext.taskId;\n\t\tevent.headers[REQUEST_SCOPE_HEADER] = \"worker\";\n\t\tif (!controller) return;\n\n\t\tconst grant = await controller.createWorkerGrant({\n\t\t\t...workerContext,\n\t\t\trequestId,\n\t\t\tsignal: ctx.signal,\n\t\t});\n\t\tevent.headers[INTERNAL_WORKER_GRANT_HEADER] = grant.token;\n\t});\n}\n\nexport default function delegatedWorkerRuntime(pi: ExtensionAPI): void {\n\tconst isWorker = isDelegatedWorkerProcess();\n\tconst workerContext = consumeWorkerContext();\n\tif (!isWorker) return;\n\tconst config = resolveGitNexusConfig();\n\tconst failClosedContext: WorkerContext = {\n\t\tsessionId: \"unavailable\",\n\t\ttaskId: \"unavailable\",\n\t\trunId: \"unavailable\",\n\t\trootRequestId: \"unavailable\",\n\t\tparentRequestId: \"unavailable\",\n\t};\n\tregisterWorkerGrantHeaders(\n\t\tpi,\n\t\tworkerContext ?? failClosedContext,\n\t\tworkerContext && config ? createHubDelegationController(config) : undefined,\n\t);\n}\n"]}
|