vigthoria-cli 1.13.26 → 1.13.29

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 (51) hide show
  1. package/dist/commands/chat.js +73 -26
  2. package/dist/commands/config.js +4 -4
  3. package/dist/commands/fork.d.ts +3 -2
  4. package/dist/commands/fork.js +124 -123
  5. package/dist/commands/game.d.ts +8 -0
  6. package/dist/commands/game.js +113 -9
  7. package/dist/commands/history.d.ts +0 -1
  8. package/dist/commands/history.js +8 -22
  9. package/dist/commands/hub.d.ts +20 -0
  10. package/dist/commands/hub.js +17 -3
  11. package/dist/commands/preview.js +7 -2
  12. package/dist/commands/product-run-registration.js +1 -1
  13. package/dist/commands/replay.d.ts +0 -1
  14. package/dist/commands/replay.js +10 -19
  15. package/dist/commands/repo.js +16 -4
  16. package/dist/commands/update-registration.js +2 -2
  17. package/dist/commands/workflow.d.ts +4 -0
  18. package/dist/commands/workflow.js +27 -0
  19. package/dist/index.js +6 -4
  20. package/dist/utils/agentRunOutcome.d.ts +7 -0
  21. package/dist/utils/agentRunOutcome.js +13 -0
  22. package/dist/utils/api.d.ts +20 -5
  23. package/dist/utils/api.js +428 -43
  24. package/dist/utils/command-policy.js +1 -1
  25. package/dist/utils/config.d.ts +2 -0
  26. package/dist/utils/config.js +8 -3
  27. package/dist/utils/frontend-preview-service.d.ts +1 -0
  28. package/dist/utils/frontend-preview-service.js +54 -5
  29. package/dist/utils/model-governance.js +23 -14
  30. package/dist/utils/model-transport-service.js +1 -1
  31. package/dist/utils/network-policy.js +15 -3
  32. package/dist/utils/operator-client.js +23 -4
  33. package/dist/utils/post-write-validator.js +7 -3
  34. package/dist/utils/preview-screenshot-adapter.d.ts +16 -41
  35. package/dist/utils/preview-screenshot-adapter.js +273 -64
  36. package/dist/utils/runtime-capability.d.ts +7 -0
  37. package/dist/utils/runtime-capability.js +11 -0
  38. package/dist/utils/runtime-temp.d.ts +5 -2
  39. package/dist/utils/runtime-temp.js +125 -29
  40. package/dist/utils/tools.js +1 -1
  41. package/dist/utils/v3-stream-events.js +10 -2
  42. package/dist/utils/v3-workspace-service.d.ts +1 -0
  43. package/dist/utils/v3-workspace-service.js +38 -1
  44. package/dist/utils/vigflow-client.d.ts +9 -0
  45. package/dist/utils/vigflow-client.js +48 -2
  46. package/dist/utils/workspace-reference.d.ts +8 -0
  47. package/dist/utils/workspace-reference.js +21 -0
  48. package/package.json +4 -6
  49. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +2 -2
  50. package/scripts/release/validate-live-service-gates.sh +3 -3
  51. package/scripts/release/validate-no-go-gates.sh +2 -0
@@ -4,11 +4,14 @@ import * as path from 'node:path';
4
4
  const MIB = 1024 * 1024;
5
5
  const DEFAULT_MAX_BYTES = 1024 * MIB;
6
6
  const DEFAULT_MIN_FREE_BYTES = 128 * MIB;
7
+ const DEFAULT_ALLOCATION_MAX_BYTES = 64 * MIB;
7
8
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
8
9
  const CLEANUP_LOCK_MAX_AGE_MS = 10 * 60 * 1000;
10
+ const LOCK_RETRY_WAIT_MS = 20;
9
11
  const MAX_SCAN_ENTRIES = 100_000;
10
12
  const LEASE_FILE = '.vigthoria-temp-lease.json';
11
13
  const CLEANUP_LOCK = '.vigthoria-temp-cleanup.lock';
14
+ const ALLOCATION_LOCK = '.vigthoria-temp-allocation.lock';
12
15
  const ROOT_MARKER = '.vigthoria-temp-root.json';
13
16
  const LEGACY_CLEANUP_MARKER = '.vigthoria-legacy-temp-cleanup-v1';
14
17
  const LEGACY_PREFIXES = [
@@ -18,6 +21,12 @@ const LEGACY_PREFIXES = [
18
21
  'vigthoria-cli-install-',
19
22
  'puppeteer_dev_chrome_profile-',
20
23
  ];
24
+ // Capture the operating-system temp root before CLI initialization rewrites
25
+ // TMP/TEMP/TMPDIR to the managed per-user root. `os.tmpdir()` is dynamic on
26
+ // Node: after the rewrite it returns the managed root, which previously meant
27
+ // a second public RuntimeTempManager instance could mistake the original
28
+ // shared %TEMP% for a safe explicit override.
29
+ const PROCESS_START_SYSTEM_TEMP = os.tmpdir();
21
30
  export class RuntimeTempError extends Error {
22
31
  code;
23
32
  details;
@@ -51,7 +60,15 @@ function rejectUnsafeWindowsPath(value) {
51
60
  }
52
61
  function isKnownSharedPosixTemp(value) {
53
62
  const normalized = path.posix.resolve(value);
54
- return normalized === '/tmp' || normalized === '/var/tmp' || normalized === '/usr/tmp';
63
+ return ['/tmp', '/var/tmp', '/usr/tmp'].some((root) => pathIsContained(root, normalized, path.posix));
64
+ }
65
+ function isWithinSharedTemp(candidate, root, platform) {
66
+ const pathApi = platform === 'win32' ? path.win32 : path.posix;
67
+ const normalize = (value) => {
68
+ const resolved = pathApi.resolve(value);
69
+ return platform === 'win32' ? resolved.toLocaleLowerCase('en-US') : resolved;
70
+ };
71
+ return pathIsContained(normalize(root), normalize(candidate), pathApi);
55
72
  }
56
73
  function pathsEqual(left, right, platform) {
57
74
  const pathApi = platform === 'win32' ? path.win32 : path.posix;
@@ -91,6 +108,10 @@ function defaultProcessAlive(pid) {
91
108
  return error?.code === 'EPERM';
92
109
  }
93
110
  }
111
+ function synchronousWait(milliseconds) {
112
+ const signal = new Int32Array(new SharedArrayBuffer(4));
113
+ Atomics.wait(signal, 0, 0, milliseconds);
114
+ }
94
115
  export class RuntimeTempManager {
95
116
  environment;
96
117
  platform;
@@ -104,6 +125,7 @@ export class RuntimeTempManager {
104
125
  source;
105
126
  maxBytes;
106
127
  minimumFreeBytes;
128
+ allocationMaxBytes;
107
129
  ttlMs;
108
130
  initializedRoot = null;
109
131
  initializedIdentity = null;
@@ -125,20 +147,21 @@ export class RuntimeTempManager {
125
147
  const platformTemp = this.platform === 'win32'
126
148
  ? String(this.environment.TEMP || this.environment.TMP || '').trim()
127
149
  : String(this.environment.TMPDIR || '').trim();
128
- this.systemTempRoot = options.systemTempDirectory || platformTemp || os.tmpdir();
150
+ this.systemTempRoot = options.systemTempDirectory || platformTemp || (this.platform === process.platform ? PROCESS_START_SYSTEM_TEMP : os.tmpdir());
129
151
  this.sharedTempRoots = [
130
152
  options.systemTempDirectory,
131
153
  platformTemp,
132
154
  this.environment.TEMP,
133
155
  this.environment.TMP,
134
156
  this.environment.TMPDIR,
135
- this.platform === process.platform ? os.tmpdir() : undefined,
157
+ this.platform === process.platform ? PROCESS_START_SYSTEM_TEMP : undefined,
136
158
  ].filter((value) => typeof value === 'string' && value.trim().length > 0);
137
159
  const resolved = resolveRuntimeTempRoot({ ...options, environment: this.environment, platform: this.platform, homeDirectory: this.homeDirectory });
138
160
  this.configuredRoot = resolved.root;
139
161
  this.source = resolved.source;
140
162
  this.maxBytes = numericSetting('VIGTHORIA_TEMP_MAX_BYTES', this.environment.VIGTHORIA_TEMP_MAX_BYTES, DEFAULT_MAX_BYTES, 64 * MIB, 64 * 1024 * MIB);
141
163
  this.minimumFreeBytes = numericSetting('VIGTHORIA_TEMP_MIN_FREE_BYTES', this.environment.VIGTHORIA_TEMP_MIN_FREE_BYTES, DEFAULT_MIN_FREE_BYTES, 64 * MIB, 64 * 1024 * MIB);
164
+ this.allocationMaxBytes = numericSetting('VIGTHORIA_TEMP_ALLOCATION_MAX_BYTES', this.environment.VIGTHORIA_TEMP_ALLOCATION_MAX_BYTES, DEFAULT_ALLOCATION_MAX_BYTES, MIB, this.maxBytes);
142
165
  const ttlHours = numericSetting('VIGTHORIA_TEMP_TTL_HOURS', this.environment.VIGTHORIA_TEMP_TTL_HOURS, DEFAULT_TTL_MS / 3_600_000, 1, 720);
143
166
  this.ttlMs = ttlHours * 3_600_000;
144
167
  }
@@ -150,7 +173,7 @@ export class RuntimeTempManager {
150
173
  // also what makes cross-platform policy probes deterministic: a Windows
151
174
  // policy instance running in a non-Windows test host must not attempt to
152
175
  // create a drive-letter path before it can reject `%TEMP%`.
153
- if (this.sharedTempRoots.some((root) => pathsEqual(this.configuredRoot, root, this.platform))
176
+ if (this.sharedTempRoots.some((root) => isWithinSharedTemp(this.configuredRoot, root, this.platform))
154
177
  || (this.platform !== 'win32' && isKnownSharedPosixTemp(this.configuredRoot))) {
155
178
  throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
156
179
  }
@@ -174,7 +197,7 @@ export class RuntimeTempManager {
174
197
  }
175
198
  }
176
199
  const pathApi = this.platform === 'win32' ? path.win32 : path.posix;
177
- if (this.sharedTempRoots.some((root) => pathsEqual(realRoot, root, this.platform))
200
+ if (this.sharedTempRoots.some((root) => isWithinSharedTemp(realRoot, root, this.platform))
178
201
  || (this.platform !== 'win32' && isKnownSharedPosixTemp(realRoot))) {
179
202
  throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
180
203
  }
@@ -218,31 +241,53 @@ export class RuntimeTempManager {
218
241
  }
219
242
  return this.status();
220
243
  }
221
- createDirectory(prefix = 'run-') {
244
+ createDirectory(prefix = 'run-', requestedMaxBytes = this.allocationMaxBytes) {
222
245
  this.initialize();
223
246
  const root = this.requireRoot();
224
247
  if (!/^[a-z0-9][a-z0-9-]{0,47}-$/i.test(prefix)) {
225
248
  throw new RuntimeTempError('Temporary directory prefix is invalid.', 'TEMP_PREFIX_INVALID');
226
249
  }
250
+ if (!Number.isSafeInteger(requestedMaxBytes) || requestedMaxBytes < MIB || requestedMaxBytes > this.maxBytes) {
251
+ throw new RuntimeTempError('Temporary allocation reservation is outside the managed root contract.', 'TEMP_RESERVATION_INVALID');
252
+ }
227
253
  this.lastCleanup = this.scavenge({ includeLegacy: false });
228
- const before = this.measure(root);
229
- if (before.usedBytes >= this.maxBytes || (before.freeBytes !== null && before.freeBytes < this.minimumFreeBytes)) {
230
- throw new RuntimeTempError('Vigthoria temporary storage has insufficient capacity after cleanup.', 'TEMP_CAPACITY_EXHAUSTED', {
231
- root,
232
- usedBytes: before.usedBytes,
233
- maxBytes: this.maxBytes,
234
- freeBytes: before.freeBytes,
235
- minimumFreeBytes: this.minimumFreeBytes,
254
+ const allocationLock = this.acquireLock(path.join(root, ALLOCATION_LOCK));
255
+ if (allocationLock === null) {
256
+ throw new RuntimeTempError('Temporary allocation admission is busy.', 'TEMP_ALLOCATION_BUSY');
257
+ }
258
+ try {
259
+ const entries = this.readEntries(root);
260
+ const before = this.measure(root, entries);
261
+ const reservedBytes = entries.reduce((total, entry) => total + Math.max(entry.bytes, entry.active ? entry.reservedBytes : 0), 0);
262
+ if (reservedBytes + requestedMaxBytes > this.maxBytes
263
+ || (before.freeBytes !== null && before.freeBytes - requestedMaxBytes < this.minimumFreeBytes)) {
264
+ throw new RuntimeTempError('Vigthoria temporary storage has insufficient reserved capacity after cleanup.', 'TEMP_CAPACITY_EXHAUSTED', {
265
+ root,
266
+ usedBytes: before.usedBytes,
267
+ reservedBytes,
268
+ requestedMaxBytes,
269
+ maxBytes: this.maxBytes,
270
+ freeBytes: before.freeBytes,
271
+ minimumFreeBytes: this.minimumFreeBytes,
272
+ });
273
+ }
274
+ const directory = fs.mkdtempSync(path.join(root, prefix));
275
+ if (this.platform !== 'win32')
276
+ fs.chmodSync(directory, 0o700);
277
+ fs.writeFileSync(path.join(directory, LEASE_FILE), `${JSON.stringify({
278
+ schemaVersion: 1,
279
+ pid: this.pid,
280
+ createdAt: new Date(this.now()).toISOString(),
281
+ reservedMaxBytes: requestedMaxBytes,
282
+ })}\n`, {
283
+ flag: 'wx',
284
+ mode: 0o600,
236
285
  });
286
+ return directory;
287
+ }
288
+ finally {
289
+ this.releaseLock(path.join(root, ALLOCATION_LOCK), allocationLock);
237
290
  }
238
- const directory = fs.mkdtempSync(path.join(root, prefix));
239
- if (this.platform !== 'win32')
240
- fs.chmodSync(directory, 0o700);
241
- fs.writeFileSync(path.join(directory, LEASE_FILE), `${JSON.stringify({ schemaVersion: 1, pid: this.pid, createdAt: new Date(this.now()).toISOString() })}\n`, {
242
- flag: 'wx',
243
- mode: 0o600,
244
- });
245
- return directory;
246
291
  }
247
292
  removeDirectory(directory) {
248
293
  const root = this.requireRoot();
@@ -250,7 +295,7 @@ export class RuntimeTempManager {
250
295
  if (path.dirname(absolute) !== root || absolute === root) {
251
296
  throw new RuntimeTempError('Refusing to remove a path outside the Vigthoria temporary root.', 'TEMP_CLEANUP_ESCAPE');
252
297
  }
253
- fs.rmSync(absolute, { recursive: true, force: true });
298
+ fs.rmSync(absolute, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
254
299
  }
255
300
  scavenge(options = {}) {
256
301
  const root = this.requireRoot();
@@ -282,6 +327,13 @@ export class RuntimeTempManager {
282
327
  }
283
328
  if (lockAge <= CLEANUP_LOCK_MAX_AGE_MS)
284
329
  return result;
330
+ let ownerPid = 0;
331
+ try {
332
+ ownerPid = Number(fs.readFileSync(lockPath, 'utf8').trim());
333
+ }
334
+ catch { /* malformed stale lock */ }
335
+ if (ownerPid > 0 && this.isProcessAlive(ownerPid))
336
+ return result;
285
337
  try {
286
338
  fs.unlinkSync(lockPath);
287
339
  }
@@ -387,7 +439,7 @@ export class RuntimeTempManager {
387
439
  readEntries(root) {
388
440
  const entries = [];
389
441
  for (const name of fs.readdirSync(root)) {
390
- if (name === CLEANUP_LOCK || name === ROOT_MARKER || name === LEGACY_CLEANUP_MARKER)
442
+ if (name === CLEANUP_LOCK || name === ALLOCATION_LOCK || name === ROOT_MARKER || name === LEGACY_CLEANUP_MARKER)
391
443
  continue;
392
444
  const absolutePath = path.join(root, name);
393
445
  let stat;
@@ -400,15 +452,19 @@ export class RuntimeTempManager {
400
452
  throw error;
401
453
  }
402
454
  let active = false;
455
+ let reservedBytes = 0;
403
456
  if (stat.isDirectory() && !stat.isSymbolicLink()) {
404
457
  const leasePath = path.join(absolutePath, LEASE_FILE);
405
458
  try {
406
459
  const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
407
460
  active = typeof lease.pid === 'number' && this.isProcessAlive(lease.pid);
461
+ if (Number.isSafeInteger(lease.reservedMaxBytes) && Number(lease.reservedMaxBytes) > 0) {
462
+ reservedBytes = Number(lease.reservedMaxBytes);
463
+ }
408
464
  }
409
465
  catch { /* third-party and abandoned directories have no live lease */ }
410
466
  }
411
- entries.push({ absolutePath, name, bytes: this.entrySize(absolutePath), modifiedAt: stat.mtimeMs, active });
467
+ entries.push({ absolutePath, name, bytes: this.entrySize(absolutePath), reservedBytes, modifiedAt: stat.mtimeMs, active });
412
468
  }
413
469
  return entries;
414
470
  }
@@ -456,10 +512,50 @@ export class RuntimeTempManager {
456
512
  if (path.dirname(entry.absolutePath) !== this.requireRoot()) {
457
513
  throw new RuntimeTempError('Temporary cleanup target escaped its root.', 'TEMP_CLEANUP_ESCAPE');
458
514
  }
459
- fs.rmSync(entry.absolutePath, { recursive: true, force: true });
515
+ fs.rmSync(entry.absolutePath, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
460
516
  result.removedEntries += 1;
461
517
  result.removedBytes += entry.bytes;
462
518
  }
519
+ acquireLock(lockPath) {
520
+ for (let attempt = 0; attempt < 100; attempt += 1) {
521
+ try {
522
+ const descriptor = fs.openSync(lockPath, 'wx', 0o600);
523
+ fs.writeFileSync(descriptor, `${this.pid}\n`);
524
+ return descriptor;
525
+ }
526
+ catch (error) {
527
+ if (error?.code !== 'EEXIST')
528
+ throw error;
529
+ try {
530
+ if (this.now() - fs.statSync(lockPath).mtimeMs > CLEANUP_LOCK_MAX_AGE_MS) {
531
+ let ownerPid = 0;
532
+ try {
533
+ ownerPid = Number(fs.readFileSync(lockPath, 'utf8').trim());
534
+ }
535
+ catch { /* malformed stale lock */ }
536
+ if (ownerPid <= 0 || !this.isProcessAlive(ownerPid)) {
537
+ fs.unlinkSync(lockPath);
538
+ continue;
539
+ }
540
+ }
541
+ }
542
+ catch (lockError) {
543
+ if (lockError?.code === 'ENOENT')
544
+ continue;
545
+ throw lockError;
546
+ }
547
+ synchronousWait(LOCK_RETRY_WAIT_MS);
548
+ }
549
+ }
550
+ return null;
551
+ }
552
+ releaseLock(lockPath, descriptor) {
553
+ fs.closeSync(descriptor);
554
+ try {
555
+ fs.unlinkSync(lockPath);
556
+ }
557
+ catch { /* stale-lock recovery may have raced */ }
558
+ }
463
559
  cleanupLegacySystemTemp(result) {
464
560
  const legacyRoot = this.systemTempRoot;
465
561
  if (!fs.existsSync(legacyRoot) || path.resolve(legacyRoot) === this.requireRoot())
@@ -487,7 +583,7 @@ export class RuntimeTempManager {
487
583
  if (this.now() - stat.mtimeMs <= this.ttlMs)
488
584
  continue;
489
585
  const bytes = this.entrySize(candidate);
490
- fs.rmSync(candidate, { recursive: true, force: true });
586
+ fs.rmSync(candidate, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });
491
587
  result.legacyRemovedEntries += 1;
492
588
  result.legacyRemovedBytes += bytes;
493
589
  }
@@ -504,8 +600,8 @@ export function initializeRuntimeTempStorage() {
504
600
  export function runtimeTempStatus() {
505
601
  return getRuntimeTempManager().initialize();
506
602
  }
507
- export function createRuntimeTempDirectory(prefix) {
508
- return getRuntimeTempManager().createDirectory(prefix);
603
+ export function createRuntimeTempDirectory(prefix, requestedMaxBytes) {
604
+ return getRuntimeTempManager().createDirectory(prefix, requestedMaxBytes);
509
605
  }
510
606
  export function removeRuntimeTempDirectory(directory) {
511
607
  getRuntimeTempManager().removeDirectory(directory);
@@ -1344,7 +1344,7 @@ export class AgenticTools {
1344
1344
  validateJavaScriptSyntax(source) {
1345
1345
  let tempDirectory;
1346
1346
  try {
1347
- tempDirectory = createRuntimeTempDirectory('syntax-');
1347
+ tempDirectory = createRuntimeTempDirectory('syntax-', 16 * 1024 * 1024);
1348
1348
  }
1349
1349
  catch (error) {
1350
1350
  return this.formatExternalToolError('syntax_check', 'allocate bounded temporary storage', error);
@@ -61,9 +61,17 @@ export function assertValidAgentPlanEvent(event) {
61
61
  if (!event || typeof event !== 'object')
62
62
  return;
63
63
  const record = event;
64
- if (record.type !== 'plan' || !Array.isArray(record.plan?.tasks))
64
+ if (record.type === 'error' && record.code === 'AGENT_PLAN_INVALID') {
65
+ throw new AgentPlanContractError(String(record.message || 'Agent plan graph is invalid.'));
66
+ }
67
+ if (record.type !== 'plan')
68
+ return;
69
+ // Accept every server envelope used by V3/Operator integrations, while
70
+ // ignoring planning keepalives that intentionally have no task graph.
71
+ const taskCandidate = record.plan?.tasks ?? record.tasks ?? record.data?.tasks;
72
+ if (!Array.isArray(taskCandidate))
65
73
  return;
66
- const tasks = record.plan.tasks;
74
+ const tasks = taskCandidate;
67
75
  const ids = tasks.map((task, index) => {
68
76
  const id = dependencyId(task.id ?? task.task_id ?? task.taskId);
69
77
  if (!id)
@@ -21,6 +21,7 @@ export declare class V3WorkspaceService {
21
21
  relativePath: string;
22
22
  absolutePath: string;
23
23
  } | null;
24
+ private resolveCaseInsensitiveWorkspacePath;
24
25
  writeFile(rootPath: string, rawPath: string, content: string, sourceRoot?: string): boolean;
25
26
  deleteFile(rootPath: string, rawPath: string): boolean;
26
27
  recover(context?: Record<string, any>, streamedFiles?: Record<string, string>, expectedFiles?: string[]): void;
@@ -133,6 +133,14 @@ export class V3WorkspaceService {
133
133
  }
134
134
  extractExpectedFiles(message = '', context = {}) {
135
135
  const candidates = new Set();
136
+ if (Array.isArray(context.expectedFiles)) {
137
+ for (const value of context.expectedFiles) {
138
+ const filePath = String(value || '').trim().replace(/^\.\//, '');
139
+ if (filePath && !/^https?:\/\//i.test(filePath) && !isSensitivePath(filePath)) {
140
+ candidates.add(filePath);
141
+ }
142
+ }
143
+ }
136
144
  for (const value of [message, context.rawMessage, context.agentPrompt]) {
137
145
  const text = String(value || '');
138
146
  const extensions = 'c|cc|cpp|cxx|h|hpp|cs|css|go|html|htm|ini|java|js|jsx|json|kt|kts|md|mjs|cjs|php|ps1|py|rb|rs|scss|sh|sql|svelte|toml|ts|tsx|txt|vue|xml|yaml|yml';
@@ -208,15 +216,44 @@ export class V3WorkspaceService {
208
216
  if (relativePath && isSensitivePath(relativePath))
209
217
  return null;
210
218
  try {
211
- const absolutePath = relativePath
219
+ let absolutePath = relativePath
212
220
  ? resolveWorkspacePath(rootPath, relativePath, { allowMissing: true })
213
221
  : fs.realpathSync(path.resolve(rootPath));
222
+ if (relativePath && !fs.existsSync(absolutePath)) {
223
+ const canonical = this.resolveCaseInsensitiveWorkspacePath(rootPath, relativePath);
224
+ relativePath = canonical.relativePath;
225
+ absolutePath = canonical.absolutePath;
226
+ }
214
227
  return { relativePath: relativePath || '.', absolutePath };
215
228
  }
216
229
  catch {
217
230
  return null;
218
231
  }
219
232
  }
233
+ resolveCaseInsensitiveWorkspacePath(rootPath, relativePath) {
234
+ const suppliedSegments = relativePath.split('/').filter(Boolean);
235
+ const canonicalSegments = [];
236
+ let current = fs.realpathSync(path.resolve(rootPath));
237
+ for (let index = 0; index < suppliedSegments.length; index += 1) {
238
+ const supplied = suppliedSegments[index];
239
+ let selected = supplied;
240
+ if (fs.existsSync(current) && fs.statSync(current).isDirectory()) {
241
+ const exact = path.join(current, supplied);
242
+ if (!fs.existsSync(exact)) {
243
+ const foldedMatches = fs.readdirSync(current).filter((entry) => entry.toLowerCase() === supplied.toLowerCase());
244
+ if (foldedMatches.length === 1)
245
+ selected = foldedMatches[0];
246
+ }
247
+ }
248
+ canonicalSegments.push(selected);
249
+ current = path.join(current, selected);
250
+ }
251
+ const canonicalRelative = canonicalSegments.join('/');
252
+ return {
253
+ relativePath: canonicalRelative,
254
+ absolutePath: resolveWorkspacePath(rootPath, canonicalRelative, { allowMissing: true }),
255
+ };
256
+ }
220
257
  writeFile(rootPath, rawPath, content, sourceRoot) {
221
258
  const relativePath = this.normalizeRelativePath(rawPath, sourceRoot || rootPath);
222
259
  if (!relativePath || isSensitivePath(relativePath)) {
@@ -61,6 +61,9 @@ export interface VigFlowTransport {
61
61
  post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<{
62
62
  data: T;
63
63
  }>;
64
+ delete<T>(url: string, config?: AxiosRequestConfig): Promise<{
65
+ data: T;
66
+ }>;
64
67
  }
65
68
  export interface VigFlowClientDependencies {
66
69
  getBaseUrls(): string[];
@@ -76,6 +79,7 @@ export declare class VigFlowClient {
76
79
  clearCredentials(): void;
77
80
  private accessToken;
78
81
  private withBackend;
82
+ private withMutationBackend;
79
83
  private endpoint;
80
84
  listTemplates(options?: {
81
85
  category?: string;
@@ -92,4 +96,9 @@ export declare class VigFlowClient {
92
96
  executionOptions?: Record<string, unknown>;
93
97
  }): Promise<VigFlowExecutionResult>;
94
98
  executionStatus(executionId: string): Promise<VigFlowExecutionStatus>;
99
+ deleteWorkflow(selector: string): Promise<{
100
+ id: string;
101
+ name?: string;
102
+ alreadyDeleted?: boolean;
103
+ }>;
95
104
  }
@@ -37,6 +37,29 @@ export class VigFlowClient {
37
37
  }
38
38
  throw new Error(`No VigFlow backend available for ${operation}. The workflow service is not deployed or not reachable.`);
39
39
  }
40
+ async withMutationBackend(operation, action) {
41
+ const authenticationErrors = [];
42
+ for (const baseUrl of this.dependencies.getBaseUrls()) {
43
+ let token;
44
+ try {
45
+ token = await this.accessToken(baseUrl);
46
+ }
47
+ catch (error) {
48
+ authenticationErrors.push(`${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
49
+ this.dependencies.debug(`VigFlow ${operation} authentication via ${baseUrl} failed:`, error instanceof Error ? error.message : String(error));
50
+ continue;
51
+ }
52
+ // Once a mutation request is dispatched its outcome can be ambiguous.
53
+ // Never repeat it against a different backend; callers must reconcile
54
+ // the operation by its identifier before choosing to retry.
55
+ return action(baseUrl, {
56
+ 'Content-Type': 'application/json',
57
+ Accept: 'application/json',
58
+ Authorization: `Bearer ${token}`,
59
+ });
60
+ }
61
+ throw new Error(`No VigFlow backend accepted authentication for ${operation}. ${authenticationErrors.join(' | ')}`.trim());
62
+ }
40
63
  endpoint(baseUrl, subPath) {
41
64
  return /\/api\/vigflow\/?$/i.test(baseUrl) ? `${baseUrl.replace(/\/$/, '')}${subPath}` : `${baseUrl}/api${subPath}`;
42
65
  }
@@ -79,7 +102,7 @@ export class VigFlowClient {
79
102
  throw new Error(`No VigFlow workflow matched "${normalized}".`);
80
103
  }
81
104
  useTemplate(templateId, options = {}) {
82
- return this.withBackend('use template', async (baseUrl, headers) => {
105
+ return this.withMutationBackend('use template', async (baseUrl, headers) => {
83
106
  const response = await this.transport.post(this.endpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`), scanOutboundContext({ name: options.name, variables: options.variables || {} }).value, { headers, timeout: 30_000 });
84
107
  if (!response.data.workflow?.id)
85
108
  throw new Error('VigFlow use-template response did not include a workflow id.');
@@ -87,7 +110,7 @@ export class VigFlowClient {
87
110
  });
88
111
  }
89
112
  runWorkflow(workflowId, options = {}) {
90
- return this.withBackend('run workflow', async (baseUrl, headers) => {
113
+ return this.withMutationBackend('run workflow', async (baseUrl, headers) => {
91
114
  const response = await this.transport.post(this.endpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`), scanOutboundContext({ data: options.data || {}, options: options.executionOptions || {} }).value, { headers, timeout: 60_000 });
92
115
  if (!response.data.executionId)
93
116
  throw new Error('VigFlow run response did not include an execution id.');
@@ -102,4 +125,27 @@ export class VigFlowClient {
102
125
  return response.data.execution;
103
126
  });
104
127
  }
128
+ async deleteWorkflow(selector) {
129
+ const normalized = String(selector || '').trim();
130
+ if (!normalized)
131
+ throw new Error('Workflow selector is required. Provide a workflow id or name.');
132
+ // Native VigFlow IDs can be retried idempotently without first listing an
133
+ // object that may already be gone. Human-readable names remain resolved
134
+ // through the ownership-scoped list to avoid ambiguous deletion.
135
+ const target = /^(?:wf_[a-z0-9_-]+|[0-9]+|[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i.test(normalized)
136
+ ? { id: normalized, name: undefined }
137
+ : await this.resolveWorkflow(normalized);
138
+ return this.withMutationBackend('delete workflow', async (baseUrl, headers) => {
139
+ try {
140
+ const response = await this.transport.delete(this.endpoint(baseUrl, `/workflows/${encodeURIComponent(target.id)}`), { headers, timeout: 30_000 });
141
+ return { id: target.id, name: target.name, alreadyDeleted: response.data.alreadyDeleted === true };
142
+ }
143
+ catch (error) {
144
+ if (Number(error?.response?.status) === 404) {
145
+ return { id: target.id, name: target.name, alreadyDeleted: true };
146
+ }
147
+ throw error;
148
+ }
149
+ });
150
+ }
105
151
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Return a stable, non-filesystem workspace label for hosted history APIs.
3
+ *
4
+ * Client absolute paths are private machine metadata and must never be used as
5
+ * server workspace roots. File access is provided separately by the
6
+ * authenticated client-tool bridge.
7
+ */
8
+ export declare function buildLocalWorkspaceReference(workspacePath: string, accountNamespace?: string): string;
@@ -0,0 +1,21 @@
1
+ import path from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ /**
4
+ * Return a stable, non-filesystem workspace label for hosted history APIs.
5
+ *
6
+ * Client absolute paths are private machine metadata and must never be used as
7
+ * server workspace roots. File access is provided separately by the
8
+ * authenticated client-tool bridge.
9
+ */
10
+ export function buildLocalWorkspaceReference(workspacePath, accountNamespace = '') {
11
+ const normalized = String(workspacePath || 'workspace').replace(/\\/g, '/').replace(/\/+$/, '');
12
+ const displayName = path.posix.basename(normalized) || 'workspace';
13
+ const canonicalIdentity = process.platform === 'win32' ? normalized.toLowerCase() : normalized;
14
+ const opaqueId = createHash('sha256')
15
+ .update(String(accountNamespace || 'anonymous'))
16
+ .update('\0')
17
+ .update(canonicalIdentity)
18
+ .digest('hex')
19
+ .slice(0, 24);
20
+ return `vigthoria://local-workspace/${encodeURIComponent(displayName)}?id=${opaqueId}`;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.13.26",
3
+ "version": "1.13.29",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -84,13 +84,14 @@
84
84
  "test:legion:billing:e2e": "npm run build && node scripts/test-legion-godmode-billing-e2e.js",
85
85
  "test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js",
86
86
  "test:v3-workspace-path": "npm run build && node scripts/test-v3-workspace-path.js",
87
+ "test:workspace:reference": "npm run build && node scripts/test-workspace-reference.mjs",
87
88
  "test:v3-server-tool-execution": "npm run build && node scripts/test-v3-server-tool-execution.js",
88
89
  "test:session:project-match": "npm run build && node scripts/test-session-project-match.mjs",
89
90
  "test:external-client-resume": "npm run build && node scripts/test-external-client-resume.mjs",
90
91
  "test:v3-stream-mutation": "npm run build && node scripts/test-v3-stream-mutation.js",
91
92
  "test:context:budget": "npm run build && node scripts/test-context-budget.js",
92
93
  "test:v3-client-tool-quality": "npm run build && node scripts/test-v3-client-tool-quality.mjs",
93
- "test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.js",
94
+ "test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.mjs",
94
95
  "test:game:command": "node scripts/test-game-command.mjs",
95
96
  "test:security:dependencies": "node scripts/test-dependency-security.mjs",
96
97
  "test:security:network": "npm run build && node scripts/test-network-policy.mjs && node scripts/test-installer-trust-policy.mjs && node scripts/test-local-security-service.mjs",
@@ -131,7 +132,6 @@
131
132
  "marked": "^11.0.0",
132
133
  "marked-terminal": "^6.2.0",
133
134
  "ora": "^7.0.1",
134
- "puppeteer": "^24.40.0",
135
135
  "ws": "^8.14.2"
136
136
  },
137
137
  "devDependencies": {
@@ -148,8 +148,6 @@
148
148
  "overrides": {
149
149
  "glob": "^13.0.6",
150
150
  "brace-expansion": "5.0.9",
151
- "fast-uri": "3.1.5",
152
- "ip-address": "10.3.1",
153
- "js-yaml": "4.3.1"
151
+ "fast-uri": "3.1.5"
154
152
  }
155
153
  }
@@ -10,7 +10,7 @@ Use this checklist on real user machines before and after rollout. The goal is t
10
10
  - [ ] Network access to:
11
11
  - `https://coder.vigthoria.io`
12
12
  - `https://api.vigthoria.io`
13
- - [ ] If running browser runtime proofs: Chrome available for Puppeteer
13
+ - [ ] If running browser runtime proofs: Microsoft Edge, Chrome, or Chromium is installed in a standard OS location
14
14
 
15
15
  Commands:
16
16
 
@@ -131,7 +131,7 @@ These are external dependencies, not CLI code regressions:
131
131
  - Live repo/game/platform E2E requires:
132
132
  - `VIGTHORIA_COMMUNITY_EMAIL`
133
133
  - `VIGTHORIA_COMMUNITY_PASSWORD`
134
- - Browser proof requires Chrome installed for Puppeteer runtime
134
+ - Browser proof requires Microsoft Edge, Chrome, or Chromium in a standard OS installation location; the CLI never downloads a browser
135
135
 
136
136
  ## 9. Rollback Readiness
137
137
 
@@ -58,8 +58,8 @@ MODELS_RESULT="$LIVE_TMP/models.json" node --input-type=module - <<'EOF'
58
58
  import fs from 'node:fs';
59
59
  const ids = new Set((JSON.parse(fs.readFileSync(process.env.MODELS_RESULT, 'utf8')).data || []).map((model) => model.id || ''));
60
60
  const hasRouter = ['Vigthoria-v4-Assistant-9B', 'vigthoria-balanced-4b', 'vigthoria-balanced-4b:latest', 'vigthoria-v3-balanced-4b', 'vigthoria-v3-balanced-4b:latest', 'vigthoria-fast-9b', 'vigthoria-fast-9b:latest'].some((model) => ids.has(model));
61
- const hasCreative = ['Vigthoria-v4-Creative-27B', 'vigthoria-creative-9b-v4', 'vigthoria-creative-9b-v4:latest', 'vigthoria-fast-9b', 'vigthoria-fast-9b:latest'].some((model) => ids.has(model));
62
- const hasCode = ids.has('Vigthoria-v4-Code-27B') || [...ids].some((model) => /^vigthoria-v3(?:\.\d+)?-code-35b(?:-|:|$)/.test(model));
61
+ const hasCreative = ['Vigthoria-v4.2-Creative-27B'].some((model) => ids.has(model));
62
+ const hasCode = ids.has('Vigthoria-v4.2-Code-27B') || [...ids].some((model) => /^vigthoria-v3(?:\.\d+)?-code-35b(?:-|:|$)/.test(model));
63
63
  if (!hasRouter || !hasCreative || !hasCode) throw new Error(`required model families missing: ${JSON.stringify([...ids].sort())}`);
64
64
  console.log('[pass] live model inventory gates');
65
65
  EOF
@@ -85,7 +85,7 @@ done
85
85
  CREATIVE_RESULT="$LIVE_TMP/creative.json" node --input-type=module - <<'EOF'
86
86
  import fs from 'node:fs';
87
87
  const result = JSON.parse(fs.readFileSync(process.env.CREATIVE_RESULT, 'utf8'));
88
- if (result.success !== true || result.model !== 'Vigthoria-v4-Code-27B') throw new Error(`unexpected governance result: ${JSON.stringify(result)}`);
88
+ if (result.success !== true || result.model !== 'Vigthoria-v4.2-Code-27B') throw new Error(`unexpected governance result: ${JSON.stringify(result)}`);
89
89
  if (result.metadata?.modelFallback?.reason !== 'governance-blocked-model') throw new Error(`missing governance fallback metadata: ${JSON.stringify(result)}`);
90
90
  console.log('[pass] live governance fallback metadata');
91
91
  EOF
@@ -12,6 +12,7 @@ VALIDATION_TMP="$(mktemp -d "$VALIDATION_TEMP_ROOT/run.XXXXXX")"
12
12
  trap 'rm -rf "$VALIDATION_TMP"' EXIT
13
13
  mkdir -p "$VALIDATION_TMP/home"
14
14
  mkdir -p "$VALIDATION_TMP/runtime-tmp"
15
+ mkdir -p "$VALIDATION_TMP/test-homes"
15
16
  chmod 700 "$VALIDATION_TMP/runtime-tmp"
16
17
 
17
18
  export HOME="$VALIDATION_TMP/home"
@@ -19,6 +20,7 @@ export USERPROFILE="$HOME"
19
20
  export TMPDIR="$VALIDATION_TMP/runtime-tmp"
20
21
  export TMP="$TMPDIR"
21
22
  export TEMP="$TMPDIR"
23
+ export VIGTHORIA_TEST_HOME_ROOT="$VALIDATION_TMP/test-homes"
22
24
  export npm_config_cache="$VALIDATION_TMP/npm-cache"
23
25
  export npm_config_update_notifier=false
24
26
  export VIGTHORIA_NO_BANNER=1