vigthoria-cli 1.13.18 → 1.13.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/completions/_vigthoria +49 -0
  2. package/completions/vigthoria.bash +6 -0
  3. package/completions/vigthoria.fish +41 -0
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +84 -135
  6. package/dist/commands/background.d.ts +0 -2
  7. package/dist/commands/background.js +26 -34
  8. package/dist/commands/bridge.js +45 -41
  9. package/dist/commands/cancel.d.ts +1 -0
  10. package/dist/commands/cancel.js +22 -16
  11. package/dist/commands/chat.d.ts +5 -42
  12. package/dist/commands/chat.js +462 -1122
  13. package/dist/commands/coding-registration.d.ts +4 -0
  14. package/dist/commands/coding-registration.js +24 -0
  15. package/dist/commands/config.js +32 -42
  16. package/dist/commands/conversation-registration.d.ts +4 -0
  17. package/dist/commands/conversation-registration.js +122 -0
  18. package/dist/commands/deploy.d.ts +8 -2
  19. package/dist/commands/deploy.js +92 -90
  20. package/dist/commands/device.d.ts +1 -2
  21. package/dist/commands/device.js +37 -21
  22. package/dist/commands/doctor-registration.d.ts +4 -0
  23. package/dist/commands/doctor-registration.js +200 -0
  24. package/dist/commands/edit.js +22 -33
  25. package/dist/commands/explain.js +7 -10
  26. package/dist/commands/fork.d.ts +1 -2
  27. package/dist/commands/fork.js +72 -22
  28. package/dist/commands/game.d.ts +1 -1
  29. package/dist/commands/game.js +17 -6
  30. package/dist/commands/generate.js +7 -9
  31. package/dist/commands/history.js +11 -7
  32. package/dist/commands/hub.d.ts +2 -2
  33. package/dist/commands/hub.js +34 -35
  34. package/dist/commands/legion.d.ts +0 -1
  35. package/dist/commands/legion.js +62 -79
  36. package/dist/commands/music.d.ts +1 -2
  37. package/dist/commands/music.js +62 -27
  38. package/dist/commands/platform-registration.d.ts +4 -0
  39. package/dist/commands/platform-registration.js +45 -0
  40. package/dist/commands/preview.d.ts +0 -1
  41. package/dist/commands/preview.js +82 -35
  42. package/dist/commands/product-run-registration.d.ts +4 -0
  43. package/dist/commands/product-run-registration.js +490 -0
  44. package/dist/commands/replay.d.ts +1 -2
  45. package/dist/commands/replay.js +15 -11
  46. package/dist/commands/repo.d.ts +14 -7
  47. package/dist/commands/repo.js +245 -196
  48. package/dist/commands/review.js +9 -12
  49. package/dist/commands/security.d.ts +1 -2
  50. package/dist/commands/security.js +12 -6
  51. package/dist/commands/update-registration.d.ts +3 -0
  52. package/dist/commands/update-registration.js +338 -0
  53. package/dist/commands/v4-registration.d.ts +4 -0
  54. package/dist/commands/v4-registration.js +24 -0
  55. package/dist/commands/v4.d.ts +1 -1
  56. package/dist/commands/v4.js +46 -8
  57. package/dist/commands/wallet.d.ts +1 -3
  58. package/dist/commands/wallet.js +18 -45
  59. package/dist/commands/workflow.d.ts +2 -0
  60. package/dist/commands/workflow.js +52 -39
  61. package/dist/index.js +122 -1498
  62. package/dist/utils/agent-stream-state.d.ts +27 -0
  63. package/dist/utils/agent-stream-state.js +93 -0
  64. package/dist/utils/agentRoute.d.ts +2 -1
  65. package/dist/utils/agentRoute.js +22 -9
  66. package/dist/utils/agentRunOutcome.d.ts +16 -0
  67. package/dist/utils/agentRunOutcome.js +37 -2
  68. package/dist/utils/api-client-factory.d.ts +8 -0
  69. package/dist/utils/api-client-factory.js +8 -0
  70. package/dist/utils/api.d.ts +45 -269
  71. package/dist/utils/api.js +664 -3210
  72. package/dist/utils/auth-session.d.ts +41 -0
  73. package/dist/utils/auth-session.js +224 -0
  74. package/dist/utils/brain-hub-client.d.ts +0 -4
  75. package/dist/utils/brain-hub-client.js +6 -3
  76. package/dist/utils/bridge-client.js +2 -0
  77. package/dist/utils/capability-health-service.d.ts +48 -0
  78. package/dist/utils/capability-health-service.js +64 -0
  79. package/dist/utils/chat-prompt-policy.d.ts +18 -0
  80. package/dist/utils/chat-prompt-policy.js +61 -0
  81. package/dist/utils/chat-tool-call-parser.d.ts +11 -0
  82. package/dist/utils/chat-tool-call-parser.js +71 -0
  83. package/dist/utils/cli-state.d.ts +22 -4
  84. package/dist/utils/cli-state.js +105 -47
  85. package/dist/utils/code-operations-service.d.ts +104 -0
  86. package/dist/utils/code-operations-service.js +837 -0
  87. package/dist/utils/codebase-indexer.d.ts +1 -0
  88. package/dist/utils/codebase-indexer.js +28 -8
  89. package/dist/utils/command-contract.d.ts +61 -0
  90. package/dist/utils/command-contract.js +149 -0
  91. package/dist/utils/command-policy.d.ts +10 -0
  92. package/dist/utils/command-policy.js +54 -0
  93. package/dist/utils/config.d.ts +13 -5
  94. package/dist/utils/config.js +227 -99
  95. package/dist/utils/context-ranker.js +11 -2
  96. package/dist/utils/context-state.d.ts +39 -0
  97. package/dist/utils/context-state.js +52 -0
  98. package/dist/utils/deckEvents.d.ts +0 -5
  99. package/dist/utils/deckEvents.js +3 -2
  100. package/dist/utils/desktop-bridge-client.js +2 -0
  101. package/dist/utils/direct-output-policy.d.ts +3 -0
  102. package/dist/utils/direct-output-policy.js +76 -0
  103. package/dist/utils/durable-json.d.ts +57 -0
  104. package/dist/utils/durable-json.js +319 -0
  105. package/dist/utils/fastAgentRouter.d.ts +6 -7
  106. package/dist/utils/fastAgentRouter.js +21 -30
  107. package/dist/utils/files.js +21 -13
  108. package/dist/utils/frontend-preview-service.d.ts +76 -0
  109. package/dist/utils/frontend-preview-service.js +482 -0
  110. package/dist/utils/goaEvents.d.ts +0 -4
  111. package/dist/utils/goaEvents.js +5 -2
  112. package/dist/utils/logger.d.ts +2 -1
  113. package/dist/utils/logger.js +27 -20
  114. package/dist/utils/mcp-context-client.d.ts +24 -0
  115. package/dist/utils/mcp-context-client.js +58 -0
  116. package/dist/utils/model-governance.d.ts +18 -0
  117. package/dist/utils/model-governance.js +75 -0
  118. package/dist/utils/model-transport-service.d.ts +44 -0
  119. package/dist/utils/model-transport-service.js +111 -0
  120. package/dist/utils/mutation-journal.d.ts +42 -0
  121. package/dist/utils/mutation-journal.js +387 -0
  122. package/dist/utils/network-policy.d.ts +24 -0
  123. package/dist/utils/network-policy.js +202 -0
  124. package/dist/utils/operator-client.d.ts +45 -0
  125. package/dist/utils/operator-client.js +159 -0
  126. package/dist/utils/post-write-validator.js +7 -9
  127. package/dist/utils/preview-screenshot-adapter.d.ts +13 -0
  128. package/dist/utils/preview-screenshot-adapter.js +36 -0
  129. package/dist/utils/process-policy.d.ts +23 -0
  130. package/dist/utils/process-policy.js +284 -0
  131. package/dist/utils/project-memory.d.ts +13 -3
  132. package/dist/utils/project-memory.js +70 -17
  133. package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
  134. package/dist/utils/prompt-workspace-resolver.js +64 -0
  135. package/dist/utils/release-install.d.ts +11 -0
  136. package/dist/utils/release-install.js +155 -0
  137. package/dist/utils/release-policy.d.ts +64 -0
  138. package/dist/utils/release-policy.js +293 -0
  139. package/dist/utils/requestIntent.js +22 -3
  140. package/dist/utils/runtime-capability.d.ts +8 -0
  141. package/dist/utils/runtime-capability.js +16 -0
  142. package/dist/utils/secret-policy.d.ts +20 -0
  143. package/dist/utils/secret-policy.js +160 -0
  144. package/dist/utils/session.d.ts +34 -2
  145. package/dist/utils/session.js +354 -110
  146. package/dist/utils/tool-approval-service.d.ts +17 -0
  147. package/dist/utils/tool-approval-service.js +78 -0
  148. package/dist/utils/tool-capability-providers.d.ts +57 -0
  149. package/dist/utils/tool-capability-providers.js +57 -0
  150. package/dist/utils/tools.d.ts +2 -23
  151. package/dist/utils/tools.js +216 -508
  152. package/dist/utils/update-policy.d.ts +18 -0
  153. package/dist/utils/update-policy.js +137 -0
  154. package/dist/utils/v3-agent-client.d.ts +40 -0
  155. package/dist/utils/v3-agent-client.js +165 -0
  156. package/dist/utils/v3-workspace-service.d.ts +28 -0
  157. package/dist/utils/v3-workspace-service.js +279 -0
  158. package/dist/utils/vigflow-client.d.ts +95 -0
  159. package/dist/utils/vigflow-client.js +105 -0
  160. package/dist/utils/workspace-boundary.d.ts +45 -0
  161. package/dist/utils/workspace-boundary.js +377 -0
  162. package/dist/utils/workspace-brain-service.d.ts +2 -0
  163. package/dist/utils/workspace-brain-service.js +11 -1
  164. package/dist/utils/workspace-cache.js +37 -15
  165. package/dist/utils/workspace-stream.js +35 -10
  166. package/install.ps1 +86 -143
  167. package/install.sh +76 -81
  168. package/package.json +44 -9
  169. package/release-policy.json +43 -0
  170. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
  171. package/scripts/release/generate-completions.mjs +24 -0
  172. package/scripts/release/install-release.mjs +10 -0
  173. package/scripts/release/publish-cli-release.mjs +116 -0
  174. package/scripts/release/publish-cli-release.sh +2 -70
  175. package/scripts/release/resolve-release-manifest.mjs +26 -0
  176. package/scripts/release/test-balanced-model-live.sh +42 -0
  177. package/scripts/release/validate-live-service-gates.sh +90 -0
  178. package/scripts/release/validate-no-go-gates.sh +135 -172
  179. package/scripts/release/verify-runtime-consistency.mjs +4 -1
  180. package/dist/commands/index.d.ts +0 -12
  181. package/dist/commands/index.js +0 -194
  182. package/dist/commands/update.d.ts +0 -9
  183. package/dist/commands/update.js +0 -301
  184. package/dist/utils/templateInstantPath.d.ts +0 -18
  185. package/dist/utils/templateInstantPath.js +0 -228
@@ -0,0 +1,18 @@
1
+ export type ResolvedUpdateSource = {
2
+ kind: 'registry';
3
+ installTarget: string;
4
+ expectedSha256: null;
5
+ } | {
6
+ kind: 'local';
7
+ installTarget: string;
8
+ expectedSha256: string | null;
9
+ } | {
10
+ kind: 'remote';
11
+ downloadUrl: string;
12
+ expectedSha256: string;
13
+ };
14
+ export declare function compareSemanticVersions(leftRaw: string, rightRaw: string): number;
15
+ export declare function resolveUpdateSource(rawTarget: string, options?: {
16
+ sha256?: unknown;
17
+ cwd?: string;
18
+ }): ResolvedUpdateSource;
@@ -0,0 +1,137 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { CliCommandError } from './command-contract.js';
4
+ import { assertTrustedEndpoint } from './network-policy.js';
5
+ const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
6
+ const SHA256 = /^[a-f0-9]{64}$/i;
7
+ function parseSemanticVersion(raw) {
8
+ const value = String(raw || '').trim();
9
+ const match = SEMVER.exec(value);
10
+ if (!match) {
11
+ throw new CliCommandError(`Invalid semantic version: ${value || '(empty)'}`, {
12
+ code: 'UPDATE_VERSION_INVALID',
13
+ category: 'usage',
14
+ });
15
+ }
16
+ const prerelease = match[4] ? match[4].split('.') : [];
17
+ if (prerelease.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith('0'))) {
18
+ throw new CliCommandError(`Invalid semantic version: ${value}`, {
19
+ code: 'UPDATE_VERSION_INVALID',
20
+ category: 'usage',
21
+ });
22
+ }
23
+ return {
24
+ major: Number(match[1]),
25
+ minor: Number(match[2]),
26
+ patch: Number(match[3]),
27
+ prerelease,
28
+ };
29
+ }
30
+ export function compareSemanticVersions(leftRaw, rightRaw) {
31
+ const left = parseSemanticVersion(leftRaw);
32
+ const right = parseSemanticVersion(rightRaw);
33
+ for (const key of ['major', 'minor', 'patch']) {
34
+ if (left[key] !== right[key])
35
+ return left[key] < right[key] ? -1 : 1;
36
+ }
37
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
38
+ if (left.prerelease.length === right.prerelease.length)
39
+ return 0;
40
+ return left.prerelease.length === 0 ? 1 : -1;
41
+ }
42
+ const length = Math.max(left.prerelease.length, right.prerelease.length);
43
+ for (let index = 0; index < length; index += 1) {
44
+ const leftPart = left.prerelease[index];
45
+ const rightPart = right.prerelease[index];
46
+ if (leftPart === undefined || rightPart === undefined)
47
+ return leftPart === undefined ? -1 : 1;
48
+ if (leftPart === rightPart)
49
+ continue;
50
+ const leftNumeric = /^\d+$/.test(leftPart);
51
+ const rightNumeric = /^\d+$/.test(rightPart);
52
+ if (leftNumeric && rightNumeric)
53
+ return Number(leftPart) < Number(rightPart) ? -1 : 1;
54
+ if (leftNumeric !== rightNumeric)
55
+ return leftNumeric ? -1 : 1;
56
+ return leftPart < rightPart ? -1 : 1;
57
+ }
58
+ return 0;
59
+ }
60
+ function normalizeChecksum(raw) {
61
+ const value = String(raw || '').trim().toLowerCase();
62
+ if (!value)
63
+ return null;
64
+ if (!SHA256.test(value)) {
65
+ throw new CliCommandError('Update SHA-256 must contain exactly 64 hexadecimal characters.', {
66
+ code: 'UPDATE_CHECKSUM_INVALID',
67
+ category: 'usage',
68
+ });
69
+ }
70
+ return value;
71
+ }
72
+ export function resolveUpdateSource(rawTarget, options = {}) {
73
+ const target = String(rawTarget || '').trim();
74
+ const expectedSha256 = normalizeChecksum(options.sha256);
75
+ if (!target) {
76
+ throw new CliCommandError('Update source is required.', {
77
+ code: 'UPDATE_SOURCE_REQUIRED',
78
+ category: 'usage',
79
+ });
80
+ }
81
+ if (/^https?:\/\//i.test(target)) {
82
+ const endpoint = assertTrustedEndpoint(target, { audience: 'release' });
83
+ if (!endpoint.pathname.toLowerCase().endsWith('.tgz')) {
84
+ throw new CliCommandError('Remote update source must identify a .tgz archive.', {
85
+ code: 'UPDATE_ARCHIVE_INVALID',
86
+ category: 'usage',
87
+ });
88
+ }
89
+ if (!expectedSha256) {
90
+ throw new CliCommandError('Remote update sources require --sha256 so the downloaded package can be verified.', {
91
+ code: 'UPDATE_CHECKSUM_REQUIRED',
92
+ category: 'usage',
93
+ });
94
+ }
95
+ return { kind: 'remote', downloadUrl: endpoint.toString(), expectedSha256 };
96
+ }
97
+ if (target.includes('://') || /^git(?:\+|@)/i.test(target)) {
98
+ throw new CliCommandError('Git and non-HTTPS update sources are not supported.', {
99
+ code: 'UPDATE_SOURCE_UNTRUSTED',
100
+ category: 'usage',
101
+ });
102
+ }
103
+ if (target.startsWith('vigthoria-cli@')) {
104
+ parseSemanticVersion(target.slice('vigthoria-cli@'.length));
105
+ if (expectedSha256) {
106
+ throw new CliCommandError('--sha256 applies only to local or HTTPS archive sources.', {
107
+ code: 'UPDATE_CHECKSUM_NOT_APPLICABLE',
108
+ category: 'usage',
109
+ });
110
+ }
111
+ return { kind: 'registry', installTarget: target, expectedSha256: null };
112
+ }
113
+ const candidatePath = path.resolve(options.cwd || process.cwd(), target);
114
+ if (target.toLowerCase().endsWith('.tgz') || fs.existsSync(candidatePath)) {
115
+ let stat;
116
+ try {
117
+ stat = fs.statSync(candidatePath);
118
+ }
119
+ catch {
120
+ throw new CliCommandError(`Local update archive does not exist: ${candidatePath}`, {
121
+ code: 'UPDATE_ARCHIVE_NOT_FOUND',
122
+ category: 'configuration',
123
+ });
124
+ }
125
+ if (!stat.isFile() || !candidatePath.toLowerCase().endsWith('.tgz')) {
126
+ throw new CliCommandError('Local update source must be an existing .tgz file.', {
127
+ code: 'UPDATE_ARCHIVE_INVALID',
128
+ category: 'configuration',
129
+ });
130
+ }
131
+ return { kind: 'local', installTarget: fs.realpathSync(candidatePath), expectedSha256 };
132
+ }
133
+ throw new CliCommandError('Update source must be an exact vigthoria-cli@<version> package or a local/HTTPS .tgz archive.', {
134
+ code: 'UPDATE_SOURCE_INVALID',
135
+ category: 'usage',
136
+ });
137
+ }
@@ -0,0 +1,40 @@
1
+ export interface V3HealthResult {
2
+ healthy: boolean;
3
+ endpoint: string;
4
+ error?: string;
5
+ }
6
+ export interface V3PreflightResult {
7
+ ok: boolean;
8
+ status: number;
9
+ endpoint: string;
10
+ reason?: string;
11
+ }
12
+ export interface V3AgentClientDependencies {
13
+ getConfiguredApiUrl(): string;
14
+ getAccessToken(): string | null;
15
+ getServiceKey(): string | null;
16
+ setServiceKey(key: string): void;
17
+ fetchServiceKey(token: string): Promise<string | null>;
18
+ refreshToken(): Promise<boolean>;
19
+ fetch(input: string, init: RequestInit): Promise<Response>;
20
+ sanitizeError(text: string): string;
21
+ debug(message: string): void;
22
+ }
23
+ export declare class V3AgentClient {
24
+ private readonly dependencies;
25
+ constructor(dependencies: V3AgentClientDependencies);
26
+ isDirectBaseUrl(baseUrl: string): boolean;
27
+ getBaseUrls(preferLocal?: boolean): string[];
28
+ runUrl(baseUrl: string): string;
29
+ continueUrl(baseUrl: string): string;
30
+ backgroundUrl(baseUrl: string, suffix?: string): string;
31
+ approvalUrl(baseUrl: string): string;
32
+ healthUrl(baseUrl: string): string;
33
+ headers(): Record<string, string>;
34
+ ensureServiceKey(): Promise<void>;
35
+ health(): Promise<V3HealthResult>;
36
+ preflight(baseUrl: string, context: Record<string, any>): Promise<V3PreflightResult>;
37
+ run(baseUrl: string, body: Record<string, any>, context: Record<string, any>, signal: AbortSignal): Promise<Response>;
38
+ requestBackground<T>(method: 'GET' | 'POST', suffix?: string, body?: unknown, query?: string): Promise<T>;
39
+ resolveApproval(body: Record<string, unknown>): Promise<any>;
40
+ }
@@ -0,0 +1,165 @@
1
+ import { hasLocalV3AgentCapability } from './runtime-capability.js';
2
+ import { isLocalTestfarmMode } from './localTestMode.js';
3
+ import { scanOutboundContext } from './secret-policy.js';
4
+ export class V3AgentClient {
5
+ dependencies;
6
+ constructor(dependencies) {
7
+ this.dependencies = dependencies;
8
+ }
9
+ isDirectBaseUrl(baseUrl) {
10
+ try {
11
+ return new URL(String(baseUrl)).port.endsWith('8030');
12
+ }
13
+ catch {
14
+ return /(?:127\.0\.0\.1|localhost|172\.19\.0\.1|host\.lan):(?:\d*8030|8030)/.test(String(baseUrl));
15
+ }
16
+ }
17
+ getBaseUrls(preferLocal = false) {
18
+ const configured = String(this.dependencies.getConfiguredApiUrl() || 'https://coder.vigthoria.io').replace(/\/$/, '');
19
+ const localTestMode = isLocalTestfarmMode();
20
+ const explicit = Boolean(process.env.VIGTHORIA_V3_AGENT_URL || process.env.V3_AGENT_URL);
21
+ const local = [
22
+ process.env.VIGTHORIA_V3_AGENT_URL,
23
+ process.env.V3_AGENT_URL,
24
+ ...(hasLocalV3AgentCapability() ? ['http://127.0.0.1:8030'] : []),
25
+ ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
26
+ const remote = (localTestMode ? [configured] : [configured, 'https://coder.vigthoria.io'])
27
+ .filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
28
+ return [...new Set((preferLocal || localTestMode || explicit) && local.length ? [...local, ...remote] : [...remote, ...local])];
29
+ }
30
+ runUrl(baseUrl) { return `${baseUrl}${this.isDirectBaseUrl(baseUrl) ? '/api/agent/run' : '/api/v3-agent/run'}`; }
31
+ continueUrl(baseUrl) { return `${baseUrl}${this.isDirectBaseUrl(baseUrl) ? '/api/agent/continue' : '/api/v3-agent/continue'}`; }
32
+ backgroundUrl(baseUrl, suffix = '') {
33
+ const clean = suffix ? `/${suffix.replace(/^\/+/, '')}` : '';
34
+ return `${baseUrl}${this.isDirectBaseUrl(baseUrl) ? '/api/agent/background' : '/api/v3-agent/background'}${clean}`;
35
+ }
36
+ approvalUrl(baseUrl) { return `${baseUrl}${this.isDirectBaseUrl(baseUrl) ? '/api/agent/approval' : '/api/v3-agent/approval'}`; }
37
+ healthUrl(baseUrl) { return `${baseUrl}${this.isDirectBaseUrl(baseUrl) ? '/health' : '/api/v3-agent/health'}`; }
38
+ headers() {
39
+ const headers = { 'Content-Type': 'application/json', Accept: 'text/event-stream' };
40
+ const token = this.dependencies.getAccessToken();
41
+ if (token) {
42
+ headers.Authorization = `Bearer ${token}`;
43
+ headers.Cookie = `vigthoria-auth-token=${token}`;
44
+ }
45
+ const key = process.env.VIGTHORIA_V3_SERVICE_KEY || process.env.V3_SERVICE_KEY || process.env.HYPERLOOP_SERVICE_KEY || this.dependencies.getServiceKey();
46
+ if (key)
47
+ headers['X-Service-Key'] = key;
48
+ return headers;
49
+ }
50
+ async ensureServiceKey() {
51
+ if (process.env.VIGTHORIA_V3_SERVICE_KEY || process.env.V3_SERVICE_KEY || process.env.HYPERLOOP_SERVICE_KEY || this.dependencies.getServiceKey())
52
+ return;
53
+ const token = this.dependencies.getAccessToken();
54
+ if (!token)
55
+ return;
56
+ try {
57
+ const key = await this.dependencies.fetchServiceKey(token);
58
+ if (key) {
59
+ this.dependencies.setServiceKey(key);
60
+ this.dependencies.debug('V3 service key retrieved and cached from Coder API.');
61
+ }
62
+ }
63
+ catch { /* health and run calls surface unavailable authentication */ }
64
+ }
65
+ async health() {
66
+ const endpoints = this.getBaseUrls(false);
67
+ const failures = [];
68
+ for (const baseUrl of endpoints) {
69
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
70
+ try {
71
+ const response = await this.dependencies.fetch(this.healthUrl(baseUrl), { method: 'GET', headers: this.headers(), signal: AbortSignal.timeout(8000) });
72
+ if (response.status === 401 || response.status === 403)
73
+ return { healthy: false, endpoint: this.runUrl(baseUrl), error: 'V3 service rejected authentication. Please re-login to refresh your token.' };
74
+ if (response.ok || response.status === 405)
75
+ return { healthy: true, endpoint: this.runUrl(baseUrl) };
76
+ failures.push(`${this.healthUrl(baseUrl)} attempt ${attempt}: HTTP ${response.status}`);
77
+ }
78
+ catch (error) {
79
+ failures.push(`${this.healthUrl(baseUrl)} attempt ${attempt}: ${this.dependencies.sanitizeError(error?.message || String(error)).slice(0, 160) || 'network error'}`);
80
+ }
81
+ if (attempt < 3)
82
+ await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
83
+ }
84
+ }
85
+ return { healthy: false, endpoint: endpoints[0] ? this.runUrl(endpoints[0]) : 'unknown', error: `V3 service is not reachable during startup preflight. Checked ${endpoints.length} endpoint(s). ${failures.slice(-3).join(' | ')}` };
86
+ }
87
+ async preflight(baseUrl, context) {
88
+ const endpoint = this.healthUrl(baseUrl);
89
+ const headers = this.headers();
90
+ headers['X-Vigthoria-Preflight'] = '1';
91
+ if (context.mcpContextId)
92
+ headers['X-MCP-Context-Id'] = String(context.mcpContextId);
93
+ const controller = new AbortController();
94
+ const timer = setTimeout(() => controller.abort(), 12_000);
95
+ try {
96
+ const response = await this.dependencies.fetch(endpoint, { method: 'GET', headers, signal: controller.signal });
97
+ if (response.status === 401 || response.status === 403) {
98
+ const detail = await response.text().catch(() => '');
99
+ return { ok: false, status: response.status, endpoint, reason: this.dependencies.sanitizeError(detail).slice(0, 200) || 'Unauthorized' };
100
+ }
101
+ return { ok: true, status: response.status, endpoint };
102
+ }
103
+ catch (error) {
104
+ const reason = this.dependencies.sanitizeError(error?.message || String(error)).slice(0, 200);
105
+ return { ok: /aborted|timeout|timed out|network/i.test(reason), status: 0, endpoint, reason };
106
+ }
107
+ finally {
108
+ clearTimeout(timer);
109
+ }
110
+ }
111
+ async run(baseUrl, body, context, signal) {
112
+ const request = () => {
113
+ const headers = this.headers();
114
+ if (context.mcpContextId)
115
+ headers['X-MCP-Context-Id'] = String(context.mcpContextId);
116
+ if (context.operationId) {
117
+ headers['Idempotency-Key'] = String(context.operationId);
118
+ headers['X-Vigthoria-Operation-Id'] = String(context.operationId);
119
+ }
120
+ return this.dependencies.fetch(this.runUrl(baseUrl), { method: 'POST', headers, body: JSON.stringify(scanOutboundContext(body).value), signal });
121
+ };
122
+ let response = await request();
123
+ if (response.status === 401 && await this.dependencies.refreshToken())
124
+ response = await request();
125
+ return response;
126
+ }
127
+ async requestBackground(method, suffix = '', body, query = '') {
128
+ await this.ensureServiceKey();
129
+ const errors = [];
130
+ for (const baseUrl of this.getBaseUrls(false)) {
131
+ try {
132
+ const response = await this.dependencies.fetch(`${this.backgroundUrl(baseUrl, suffix)}${query}`, {
133
+ method, headers: this.headers(), ...(body === undefined ? {} : { body: JSON.stringify(scanOutboundContext(body).value) }),
134
+ });
135
+ if (!response.ok) {
136
+ errors.push(`${baseUrl}: ${response.status}`);
137
+ continue;
138
+ }
139
+ return await response.json();
140
+ }
141
+ catch (error) {
142
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
143
+ }
144
+ }
145
+ throw new Error(`V3 background request failed: ${errors.join('; ')}`);
146
+ }
147
+ async resolveApproval(body) {
148
+ await this.ensureServiceKey();
149
+ const errors = [];
150
+ for (const baseUrl of this.getBaseUrls(false)) {
151
+ try {
152
+ const response = await this.dependencies.fetch(this.approvalUrl(baseUrl), { method: 'POST', headers: this.headers(), body: JSON.stringify(body) });
153
+ if (!response.ok) {
154
+ errors.push(`${baseUrl}: ${response.status}`);
155
+ continue;
156
+ }
157
+ return await response.json();
158
+ }
159
+ catch (error) {
160
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
161
+ }
162
+ }
163
+ throw new Error(`Unable to resolve background approval: ${errors.join('; ')}`);
164
+ }
165
+ }
@@ -0,0 +1,28 @@
1
+ export interface V3WorkspaceSnapshot {
2
+ fileCount: number;
3
+ paths: string[];
4
+ signature: string;
5
+ }
6
+ export interface V3WorkspaceServiceDependencies {
7
+ resolveTargetPath(context: Record<string, any>): string | null;
8
+ warn(message: string): void;
9
+ }
10
+ export declare class V3WorkspaceService {
11
+ private readonly dependencies;
12
+ constructor(dependencies: V3WorkspaceServiceDependencies);
13
+ collectFileContents(rootPath: string, filePaths: string[]): Record<string, string>;
14
+ hasOutput(context?: Record<string, any>): boolean;
15
+ snapshot(rootPath: string): V3WorkspaceSnapshot;
16
+ waitForSettle(context?: Record<string, any>, options?: Record<string, any>): Promise<void>;
17
+ extractExpectedFiles(message?: string, context?: Record<string, any>): string[];
18
+ captureMutation(event: any, streamedFiles: Record<string, string>, serverRoot?: string | null): void;
19
+ applyStreamEvent(event: any, context?: Record<string, any>, serverRoot?: string | null): boolean;
20
+ resolveClientToolPath(rootPath: string, rawPath: unknown, sourceRoots?: string[]): {
21
+ relativePath: string;
22
+ absolutePath: string;
23
+ } | null;
24
+ writeFile(rootPath: string, rawPath: string, content: string, sourceRoot?: string): boolean;
25
+ deleteFile(rootPath: string, rawPath: string): boolean;
26
+ recover(context?: Record<string, any>, streamedFiles?: Record<string, string>, expectedFiles?: string[]): void;
27
+ normalizeRelativePath(rawPath: string, rootPath?: string): string;
28
+ }
@@ -0,0 +1,279 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { containsHighConfidenceSecret, isSensitivePath } from './secret-policy.js';
4
+ import { resolveWorkspacePath } from './workspace-boundary.js';
5
+ import { isV3WorkspaceRootAliasPath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
6
+ const BINARY_EXTENSIONS = new Set([
7
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.avif',
8
+ '.mp3', '.mp4', '.wav', '.ogg', '.webm', '.flac', '.aac',
9
+ '.zip', '.gz', '.tar', '.rar', '.7z', '.bz2', '.exe', '.dll', '.so',
10
+ '.dylib', '.bin', '.dat', '.woff', '.woff2', '.ttf', '.eot', '.otf',
11
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.db',
12
+ '.sqlite', '.sqlite3', '.pyc', '.pyo', '.class', '.o', '.obj', '.DS_Store', '.lock',
13
+ ]);
14
+ export class V3WorkspaceService {
15
+ dependencies;
16
+ constructor(dependencies) {
17
+ this.dependencies = dependencies;
18
+ }
19
+ collectFileContents(rootPath, filePaths) {
20
+ const result = {};
21
+ let totalBytes = 0;
22
+ for (const suppliedPath of filePaths) {
23
+ if (totalBytes >= 2 * 1024 * 1024)
24
+ break;
25
+ const relativePath = suppliedPath.replace(/\\/g, '/');
26
+ if (BINARY_EXTENSIONS.has(path.extname(relativePath).toLowerCase()))
27
+ continue;
28
+ if (/(^|\/)\.(git|hg)(\/|$)/.test(relativePath) || isSensitivePath(relativePath))
29
+ continue;
30
+ const agentState = /^\.vigthoria\/agent-state\//i.test(relativePath);
31
+ if (!agentState && /(^|\/)\.vigthoria(\/|$)/.test(relativePath))
32
+ continue;
33
+ try {
34
+ const absolutePath = resolveWorkspacePath(rootPath, relativePath);
35
+ const stat = fs.statSync(absolutePath);
36
+ if (!stat.isFile() || stat.size > 200 * 1024 || stat.size === 0)
37
+ continue;
38
+ const content = fs.readFileSync(absolutePath, 'utf8');
39
+ if (containsHighConfidenceSecret(content) || /[\x00-\x08\x0e-\x1f]/.test(content.slice(0, 512)))
40
+ continue;
41
+ result[suppliedPath] = content;
42
+ totalBytes += Buffer.byteLength(content, 'utf8');
43
+ }
44
+ catch {
45
+ // Unreadable or escaped paths are not outbound context.
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ hasOutput(context = {}) {
51
+ try {
52
+ const root = this.dependencies.resolveTargetPath(context);
53
+ if (!root || !fs.existsSync(root))
54
+ return false;
55
+ const stack = [fs.realpathSync(root)];
56
+ while (stack.length > 0) {
57
+ const current = stack.pop();
58
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
59
+ if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.vigthoria')
60
+ continue;
61
+ const fullPath = path.join(current, entry.name);
62
+ if (entry.isSymbolicLink())
63
+ continue;
64
+ if (entry.isFile())
65
+ return true;
66
+ if (entry.isDirectory())
67
+ stack.push(fullPath);
68
+ }
69
+ }
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ return false;
75
+ }
76
+ snapshot(rootPath) {
77
+ const canonicalRoot = fs.realpathSync(rootPath);
78
+ const stack = [canonicalRoot];
79
+ const entries = [];
80
+ while (stack.length > 0) {
81
+ const current = stack.pop();
82
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
83
+ if (entry.name === '.git' || entry.name === 'node_modules' || entry.isSymbolicLink())
84
+ continue;
85
+ const fullPath = path.join(current, entry.name);
86
+ const relativePath = path.relative(canonicalRoot, fullPath).replace(/\\/g, '/');
87
+ if (isSensitivePath(relativePath))
88
+ continue;
89
+ if (entry.isDirectory())
90
+ stack.push(fullPath);
91
+ else if (entry.isFile()) {
92
+ const stat = fs.statSync(fullPath);
93
+ entries.push(`${relativePath}:${stat.size}:${stat.mtimeMs}`);
94
+ }
95
+ }
96
+ }
97
+ entries.sort();
98
+ return { fileCount: entries.length, paths: entries.map((entry) => entry.split(':', 1)[0]), signature: entries.join('|') };
99
+ }
100
+ async waitForSettle(context = {}, options = {}) {
101
+ const rootPath = this.dependencies.resolveTargetPath(context);
102
+ if (!rootPath || !fs.existsSync(rootPath))
103
+ return;
104
+ const timeoutMs = options.timeoutMs || 15000;
105
+ const pollMs = options.pollMs || 300;
106
+ const stableMs = options.stableMs || 1200;
107
+ const expectedFiles = Array.isArray(options.expectedFiles)
108
+ ? options.expectedFiles.map((entry) => String(entry || '').trim()).filter(Boolean)
109
+ : [];
110
+ const start = Date.now();
111
+ let stableSince = 0;
112
+ let lastSignature = '';
113
+ while (Date.now() - start < timeoutMs) {
114
+ const current = this.snapshot(rootPath);
115
+ if (current.fileCount > 0 && expectedFiles.every((filePath) => current.paths.includes(filePath))) {
116
+ if (current.signature === lastSignature) {
117
+ if (!stableSince)
118
+ stableSince = Date.now();
119
+ if (Date.now() - stableSince >= stableMs)
120
+ return;
121
+ }
122
+ else {
123
+ lastSignature = current.signature;
124
+ stableSince = Date.now();
125
+ }
126
+ }
127
+ else {
128
+ lastSignature = current.signature;
129
+ stableSince = 0;
130
+ }
131
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
132
+ }
133
+ }
134
+ extractExpectedFiles(message = '', context = {}) {
135
+ const candidates = new Set();
136
+ for (const value of [message, context.rawMessage, context.agentPrompt]) {
137
+ const text = String(value || '');
138
+ for (const pattern of [/`([^`]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))`/gi, /\b([A-Za-z0-9_./-]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))\b/g]) {
139
+ let match;
140
+ while ((match = pattern.exec(text)) !== null) {
141
+ const filePath = match[1].trim().replace(/^\.\//, '');
142
+ if (filePath && !/^https?:\/\//i.test(filePath) && !isSensitivePath(filePath))
143
+ candidates.add(filePath);
144
+ }
145
+ }
146
+ }
147
+ return [...candidates];
148
+ }
149
+ captureMutation(event, streamedFiles, serverRoot) {
150
+ if (!event || !streamedFiles)
151
+ return;
152
+ if (event.type === 'file_mutation' && typeof event.path === 'string') {
153
+ const filePath = this.normalizeRelativePath(event.path, serverRoot || undefined);
154
+ if (!filePath || isSensitivePath(filePath))
155
+ return;
156
+ if (event.action === 'delete')
157
+ delete streamedFiles[filePath];
158
+ else if (typeof event.content === 'string')
159
+ streamedFiles[filePath] = event.content;
160
+ return;
161
+ }
162
+ if (event.type === 'tool_result' && event.success) {
163
+ const name = String(event.name || event.tool || '').trim();
164
+ const args = event.arguments || {};
165
+ const targetPath = typeof event.target === 'string' ? event.target : (typeof args.path === 'string' ? args.path : '');
166
+ const filePath = this.normalizeRelativePath(targetPath, serverRoot || undefined);
167
+ if ((name === 'write_file' || name === 'edit_file') && filePath && !isSensitivePath(filePath) && typeof args.content === 'string') {
168
+ streamedFiles[filePath] = args.content;
169
+ }
170
+ }
171
+ }
172
+ applyStreamEvent(event, context = {}, serverRoot) {
173
+ if (!event || !['file_mutation', 'workspace_snapshot'].includes(String(event.type || '')))
174
+ return false;
175
+ const rootPath = this.dependencies.resolveTargetPath(context);
176
+ if (!rootPath)
177
+ return false;
178
+ if (event.type === 'workspace_snapshot' && event.files && typeof event.files === 'object') {
179
+ let applied = false;
180
+ for (const [rawPath, content] of Object.entries(event.files)) {
181
+ if (typeof content === 'string')
182
+ applied = this.writeFile(rootPath, rawPath, content, serverRoot || undefined) || applied;
183
+ }
184
+ return applied;
185
+ }
186
+ const relativePath = this.normalizeRelativePath(event.path, serverRoot || undefined);
187
+ if (!relativePath)
188
+ return false;
189
+ if (event.action === 'delete')
190
+ return this.deleteFile(rootPath, relativePath);
191
+ return typeof event.content === 'string' && this.writeFile(rootPath, relativePath, event.content, rootPath);
192
+ }
193
+ resolveClientToolPath(rootPath, rawPath, sourceRoots = []) {
194
+ const raw = String(rawPath || '.').trim();
195
+ const roots = [rootPath, ...sourceRoots.filter(Boolean)];
196
+ let relativePath = '';
197
+ for (const sourceRoot of roots) {
198
+ relativePath = this.normalizeRelativePath(raw, sourceRoot);
199
+ if (relativePath || raw === '.' || raw === '')
200
+ break;
201
+ }
202
+ if (!relativePath && raw !== '.' && raw !== '' && !roots.some((sourceRoot) => isV3WorkspaceRootAliasPath(raw, sourceRoot)))
203
+ return null;
204
+ if (relativePath && isSensitivePath(relativePath))
205
+ return null;
206
+ try {
207
+ const absolutePath = relativePath
208
+ ? resolveWorkspacePath(rootPath, relativePath, { allowMissing: true })
209
+ : fs.realpathSync(path.resolve(rootPath));
210
+ return { relativePath: relativePath || '.', absolutePath };
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ writeFile(rootPath, rawPath, content, sourceRoot) {
217
+ const relativePath = this.normalizeRelativePath(rawPath, sourceRoot || rootPath);
218
+ if (!relativePath || isSensitivePath(relativePath)) {
219
+ this.dependencies.warn(`Refusing to write V3 path: ${path.basename(relativePath || rawPath)}`);
220
+ return false;
221
+ }
222
+ let absolutePath;
223
+ try {
224
+ absolutePath = resolveWorkspacePath(rootPath, relativePath, { allowMissing: true });
225
+ }
226
+ catch {
227
+ this.dependencies.warn(`Refusing to write V3 file outside workspace: ${rawPath}`);
228
+ return false;
229
+ }
230
+ try {
231
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
232
+ if (fs.existsSync(absolutePath) && fs.readFileSync(absolutePath, 'utf8') === content)
233
+ return false;
234
+ fs.writeFileSync(absolutePath, content, 'utf8');
235
+ return true;
236
+ }
237
+ catch (error) {
238
+ this.dependencies.warn(`Failed to write V3 file ${relativePath}: ${error.message}`);
239
+ return false;
240
+ }
241
+ }
242
+ deleteFile(rootPath, rawPath) {
243
+ const relativePath = this.normalizeRelativePath(rawPath, rootPath);
244
+ if (!relativePath || isSensitivePath(relativePath))
245
+ return false;
246
+ try {
247
+ const absolutePath = resolveWorkspacePath(rootPath, relativePath);
248
+ if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
249
+ fs.unlinkSync(absolutePath);
250
+ return true;
251
+ }
252
+ }
253
+ catch (error) {
254
+ this.dependencies.warn(`Failed to delete V3 file ${relativePath}: ${error.message}`);
255
+ }
256
+ return false;
257
+ }
258
+ recover(context = {}, streamedFiles = {}, expectedFiles = []) {
259
+ const rootPath = this.dependencies.resolveTargetPath(context);
260
+ if (!rootPath || Object.keys(streamedFiles).length === 0)
261
+ return;
262
+ try {
263
+ fs.mkdirSync(rootPath, { recursive: true });
264
+ }
265
+ catch {
266
+ return;
267
+ }
268
+ const targets = expectedFiles.length > 0 ? [...new Set([...expectedFiles, ...Object.keys(streamedFiles)])] : Object.keys(streamedFiles);
269
+ for (const targetPath of targets) {
270
+ const relativePath = this.normalizeRelativePath(targetPath, rootPath);
271
+ const content = streamedFiles[relativePath];
272
+ if (relativePath && typeof content === 'string')
273
+ this.writeFile(rootPath, relativePath, content, rootPath);
274
+ }
275
+ }
276
+ normalizeRelativePath(rawPath, rootPath) {
277
+ return normalizeV3WorkspaceRelativePath(rawPath, rootPath);
278
+ }
279
+ }