@vibecodr/cli 0.2.11 → 1.0.0-rc.0
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 +53 -23
- package/MIGRATION.md +73 -0
- package/README.md +89 -72
- package/dist/auth/official-client.d.ts +6 -0
- package/dist/auth/official-client.d.ts.map +1 -0
- package/dist/auth/official-client.js +1 -0
- package/dist/auth/official-client.js.map +1 -0
- package/dist/auth/token-manager.d.ts +40 -0
- package/dist/auth/token-manager.d.ts.map +1 -0
- package/dist/auth/token-manager.js +1 -2
- package/dist/auth/token-manager.js.map +1 -0
- package/dist/bin/vc-tools.d.ts +3 -0
- package/dist/bin/vc-tools.d.ts.map +1 -0
- package/dist/bin/vc-tools.js +7 -0
- package/dist/bin/vc-tools.js.map +1 -0
- package/dist/bin/vibecodr-mcp.d.ts +3 -0
- package/dist/bin/vibecodr-mcp.d.ts.map +1 -0
- package/dist/bin/vibecodr-mcp.js +37 -0
- package/dist/bin/vibecodr-mcp.js.map +1 -0
- package/dist/cli/errors.d.ts +28 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +1 -0
- package/dist/cli/errors.js.map +1 -0
- package/dist/cli/output.d.ts +16 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/parse.d.ts +18 -0
- package/dist/cli/parse.d.ts.map +1 -0
- package/dist/cli/parse.js +1 -0
- package/dist/cli/parse.js.map +1 -0
- package/dist/clients/base.d.ts +20 -0
- package/dist/clients/base.d.ts.map +1 -0
- package/dist/clients/base.js +1 -0
- package/dist/clients/base.js.map +1 -0
- package/dist/clients/claude-code.d.ts +5 -0
- package/dist/clients/claude-code.d.ts.map +1 -0
- package/dist/clients/claude-code.js +88 -0
- package/dist/clients/claude-code.js.map +1 -0
- package/dist/clients/claude-desktop.d.ts +5 -0
- package/dist/clients/claude-desktop.d.ts.map +1 -0
- package/dist/clients/claude-desktop.js +97 -0
- package/dist/clients/claude-desktop.js.map +1 -0
- package/dist/clients/codex.d.ts +5 -0
- package/dist/clients/codex.d.ts.map +1 -0
- package/dist/clients/codex.js +1 -0
- package/dist/clients/codex.js.map +1 -0
- package/dist/clients/cursor.d.ts +5 -0
- package/dist/clients/cursor.d.ts.map +1 -0
- package/dist/clients/cursor.js +1 -1
- package/dist/clients/cursor.js.map +1 -0
- package/dist/clients/vscode.d.ts +5 -0
- package/dist/clients/vscode.d.ts.map +1 -0
- package/dist/clients/vscode.js +5 -1
- package/dist/clients/vscode.js.map +1 -0
- package/dist/clients/windsurf.d.ts +5 -0
- package/dist/clients/windsurf.d.ts.map +1 -0
- package/dist/clients/windsurf.js +1 -0
- package/dist/clients/windsurf.js.map +1 -0
- package/dist/commands/call.d.ts +9 -0
- package/dist/commands/call.d.ts.map +1 -0
- package/dist/commands/call.js +1 -0
- package/dist/commands/call.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +1 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/context.d.ts +15 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +2 -5
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +1 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/install.d.ts +3 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +23 -5
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +1 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +1 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/pulse-publish.d.ts +3 -0
- package/dist/commands/pulse-publish.d.ts.map +1 -0
- package/dist/commands/pulse-publish.js +1 -0
- package/dist/commands/pulse-publish.js.map +1 -0
- package/dist/commands/pulse-setup.d.ts +3 -0
- package/dist/commands/pulse-setup.d.ts.map +1 -0
- package/dist/commands/pulse-setup.js +5 -3
- package/dist/commands/pulse-setup.js.map +1 -0
- package/dist/commands/pulse.d.ts +3 -0
- package/dist/commands/pulse.d.ts.map +1 -0
- package/dist/commands/pulse.js +1 -0
- package/dist/commands/pulse.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +1 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +1 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commands/uninstall.d.ts +3 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +12 -4
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/upload.d.ts +3 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +1 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +82 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/core/interactive-input.d.ts +7 -0
- package/dist/core/interactive-input.d.ts.map +1 -0
- package/dist/core/interactive-input.js +1 -0
- package/dist/core/interactive-input.js.map +1 -0
- package/dist/core/mcp-client.d.ts +17 -0
- package/dist/core/mcp-client.d.ts.map +1 -0
- package/dist/core/mcp-client.js +1 -0
- package/dist/core/mcp-client.js.map +1 -0
- package/dist/core/redaction.d.ts +2 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +36 -2
- package/dist/core/redaction.js.map +1 -0
- package/dist/core/renderers.d.ts +8 -0
- package/dist/core/renderers.d.ts.map +1 -0
- package/dist/core/renderers.js +1 -0
- package/dist/core/renderers.js.map +1 -0
- package/dist/doctor/run.d.ts +10 -0
- package/dist/doctor/run.d.ts.map +1 -0
- package/dist/doctor/run.js +12 -3
- package/dist/doctor/run.js.map +1 -0
- package/dist/legacy/cli/errors.d.ts +9 -0
- package/dist/legacy/cli/errors.d.ts.map +1 -0
- package/dist/legacy/cli/errors.js +23 -0
- package/dist/legacy/cli/errors.js.map +1 -0
- package/dist/legacy/cli/install.d.ts +24 -0
- package/dist/legacy/cli/install.d.ts.map +1 -0
- package/dist/legacy/cli/install.js +307 -0
- package/dist/legacy/cli/install.js.map +1 -0
- package/dist/legacy/cli/output.d.ts +17 -0
- package/dist/legacy/cli/output.d.ts.map +1 -0
- package/dist/legacy/cli/output.js +36 -0
- package/dist/legacy/cli/output.js.map +1 -0
- package/dist/legacy/cli/parser.d.ts +33 -0
- package/dist/legacy/cli/parser.d.ts.map +1 -0
- package/dist/legacy/cli/parser.js +177 -0
- package/dist/legacy/cli/parser.js.map +1 -0
- package/dist/legacy/cli/run.d.ts +11 -0
- package/dist/legacy/cli/run.d.ts.map +1 -0
- package/dist/legacy/cli/run.js +2947 -0
- package/dist/legacy/cli/run.js.map +1 -0
- package/dist/legacy/config/credential-store.d.ts +8 -0
- package/dist/legacy/config/credential-store.d.ts.map +1 -0
- package/dist/legacy/config/credential-store.js +52 -0
- package/dist/legacy/config/credential-store.js.map +1 -0
- package/dist/legacy/config/store.d.ts +63 -0
- package/dist/legacy/config/store.d.ts.map +1 -0
- package/dist/legacy/config/store.js +311 -0
- package/dist/legacy/config/store.js.map +1 -0
- package/dist/legacy/core/api-client.d.ts +45 -0
- package/dist/legacy/core/api-client.d.ts.map +1 -0
- package/dist/legacy/core/api-client.js +204 -0
- package/dist/legacy/core/api-client.js.map +1 -0
- package/dist/legacy/core/contracts.d.ts +488 -0
- package/dist/legacy/core/contracts.d.ts.map +1 -0
- package/dist/legacy/core/contracts.js +386 -0
- package/dist/legacy/core/contracts.js.map +1 -0
- package/dist/legacy/core/goal-coverage.d.ts +15 -0
- package/dist/legacy/core/goal-coverage.d.ts.map +1 -0
- package/dist/legacy/core/goal-coverage.js +169 -0
- package/dist/legacy/core/goal-coverage.js.map +1 -0
- package/dist/legacy/core/redaction.d.ts +4 -0
- package/dist/legacy/core/redaction.d.ts.map +1 -0
- package/dist/legacy/core/redaction.js +121 -0
- package/dist/legacy/core/redaction.js.map +1 -0
- package/dist/legacy/core/validators.d.ts +8 -0
- package/dist/legacy/core/validators.d.ts.map +1 -0
- package/dist/legacy/core/validators.js +102 -0
- package/dist/legacy/core/validators.js.map +1 -0
- package/dist/legacy/core/version.d.ts +3 -0
- package/dist/legacy/core/version.d.ts.map +1 -0
- package/dist/legacy/core/version.js +3 -0
- package/dist/legacy/core/version.js.map +1 -0
- package/dist/legacy/index.d.ts +8 -0
- package/dist/legacy/index.d.ts.map +1 -0
- package/dist/legacy/index.js +8 -0
- package/dist/legacy/index.js.map +1 -0
- package/dist/platform/browser.d.ts +7 -0
- package/dist/platform/browser.d.ts.map +1 -0
- package/dist/platform/browser.js +1 -0
- package/dist/platform/browser.js.map +1 -0
- package/dist/platform/exec.d.ts +3 -0
- package/dist/platform/exec.d.ts.map +1 -0
- package/dist/platform/exec.js +10 -1
- package/dist/platform/exec.js.map +1 -0
- package/dist/platform/paths.d.ts +9 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +13 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/prompt.d.ts +5 -0
- package/dist/platform/prompt.d.ts.map +1 -0
- package/dist/platform/prompt.js +1 -0
- package/dist/platform/prompt.js.map +1 -0
- package/dist/storage/config-store.d.ts +15 -0
- package/dist/storage/config-store.d.ts.map +1 -0
- package/dist/storage/config-store.js +1 -0
- package/dist/storage/config-store.js.map +1 -0
- package/dist/storage/file-lock.d.ts +7 -0
- package/dist/storage/file-lock.d.ts.map +1 -0
- package/dist/storage/file-lock.js +1 -0
- package/dist/storage/file-lock.js.map +1 -0
- package/dist/storage/install-manifest.d.ts +12 -0
- package/dist/storage/install-manifest.d.ts.map +1 -0
- package/dist/storage/install-manifest.js +1 -0
- package/dist/storage/install-manifest.js.map +1 -0
- package/dist/storage/secret-store.d.ts +36 -0
- package/dist/storage/secret-store.d.ts.map +1 -0
- package/dist/storage/secret-store.js +1 -0
- package/dist/storage/secret-store.js.map +1 -0
- package/dist/types/auth.d.ts +55 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +1 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/config.d.ts +29 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +1 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/install.d.ts +26 -0
- package/dist/types/install.d.ts.map +1 -0
- package/dist/types/install.js +1 -0
- package/dist/types/install.js.map +1 -0
- package/docs/API-CONTRACT.md +606 -0
- package/docs/CLOUDFLARE-PRIMITIVE-FIT.md +212 -0
- package/docs/RELEASE-CHECKLIST.md +297 -0
- package/docs/SECURITY.md +227 -0
- package/docs/VALIDATION-MATRIX.md +58 -0
- package/docs/commands.md +49 -29
- package/docs/legacy/AGENT-TOOLKIT-RFC.md +1395 -0
- package/docs/legacy/CLI-GUIDELINES-AUDIT.md +95 -0
- package/docs/legacy/COMPLETION-AUDIT.md +542 -0
- package/docs/legacy/vc-tools-finetune.md +982 -0
- package/docs/legacy/vc-tools-goal-browser-run-containers.md +465 -0
- package/docs/legacy/vc-tools-goal-original.md +249 -0
- package/package.json +37 -8
|
@@ -0,0 +1,2947 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ConfigStore, DEFAULT_API_URL, resolveConfigDir } from "../config/store.js";
|
|
6
|
+
import { createApiClient, createBaseClient, encodePathSegment, normalizeBaseUrl } from "../core/api-client.js";
|
|
7
|
+
import { CAPABILITIES, DASHBOARD_SECTIONS, DEFAULT_PLANS, LAUNCH_POLICIES, LAUNCH_TOOL_GRANTS, LAUNCH_WORKFLOWS, OVERAGE_METERS, PUBLIC_OFFERING_CLASSIFICATIONS } from "../core/contracts.js";
|
|
8
|
+
import { GOAL_INSPECTIONS, goalCoverageSummary } from "../core/goal-coverage.js";
|
|
9
|
+
import { VC_TOOLS_VERSION } from "../core/version.js";
|
|
10
|
+
import { normalizeCapabilityName, sanitizeFilename, validateBrowserUrl, validateEntityId, validatePositiveInt, validateSandboxCommand } from "../core/validators.js";
|
|
11
|
+
import { CliError, toCliError } from "./errors.js";
|
|
12
|
+
import { installClient, isInstallableClient } from "./install.js";
|
|
13
|
+
import { writeError, writeResult } from "./output.js";
|
|
14
|
+
import { getBooleanFlag, getStringFlag, parseArgv, parseCommandOptions } from "./parser.js";
|
|
15
|
+
const VERSION = VC_TOOLS_VERSION;
|
|
16
|
+
const MAX_CREDENTIAL_BYTES = 64 * 1024;
|
|
17
|
+
const DEFAULT_AUTH_API_URL = "https://api.vibecodr.space";
|
|
18
|
+
const GRANT_REFRESH_SKEW_SECONDS = 60;
|
|
19
|
+
const ARTIFACT_OUTPUT_WORKSPACE_MESSAGE = "Artifact output is workspace-bounded so downloaded bytes can only be written to files you intentionally target inside this workspace. Use --out ./artifacts, --out ./artifacts/report.pdf, or cd to the intended workspace and use --out .";
|
|
20
|
+
const ARTIFACT_INPUT_WORKSPACE_MESSAGE = "Artifact upload sources are workspace-bounded so the CLI only reads files you intentionally target inside this workspace. Move the file into this workspace, or cd to the workspace that contains it.";
|
|
21
|
+
export async function runCli(argv, options = {}) {
|
|
22
|
+
const stdout = options.stdout ?? process.stdout;
|
|
23
|
+
const stderr = options.stderr ?? process.stderr;
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = parseArgv(argv);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const cliError = toCliError(error);
|
|
30
|
+
writeError(cliError, { json: false, quiet: false, stdout, stderr });
|
|
31
|
+
return cliError.exitCode;
|
|
32
|
+
}
|
|
33
|
+
const { globals, commandArgs } = parsed;
|
|
34
|
+
try {
|
|
35
|
+
if (globals.version) {
|
|
36
|
+
writeResult({ message: `vc-tools ${VERSION}`, data: { version: VERSION }, humanData: "hide" }, { json: globals.json, quiet: globals.quiet, stdout, stderr });
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
if (globals.help || commandArgs.length === 0) {
|
|
40
|
+
const help = helpResult(commandArgs);
|
|
41
|
+
writeResult(help, { json: globals.json, quiet: globals.quiet, stdout, stderr });
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
const env = options.env ?? process.env;
|
|
45
|
+
const context = {
|
|
46
|
+
env,
|
|
47
|
+
cwd: options.cwd ?? process.cwd(),
|
|
48
|
+
globals,
|
|
49
|
+
store: ConfigStore.resolve(env, globals.configDir),
|
|
50
|
+
stdin: options.stdin ?? process.stdin,
|
|
51
|
+
fetchImpl: options.fetchImpl,
|
|
52
|
+
stderr
|
|
53
|
+
};
|
|
54
|
+
const result = await dispatch(context, commandArgs);
|
|
55
|
+
writeResult(result, { json: globals.json, quiet: globals.quiet, stdout, stderr });
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const cliError = toCliError(error);
|
|
60
|
+
writeError(cliError, { json: globals.json, quiet: globals.quiet, stdout, stderr });
|
|
61
|
+
return cliError.exitCode;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function dispatch(context, args) {
|
|
65
|
+
const [command, subcommand, ...rest] = args;
|
|
66
|
+
switch (command) {
|
|
67
|
+
case "start":
|
|
68
|
+
case "setup":
|
|
69
|
+
return commandStart(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
70
|
+
case "login":
|
|
71
|
+
return commandLogin(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
72
|
+
case "logout":
|
|
73
|
+
return commandLogout(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
74
|
+
case "status":
|
|
75
|
+
return commandStatus(context);
|
|
76
|
+
case "whoami":
|
|
77
|
+
return commandWhoami(context);
|
|
78
|
+
case "connect":
|
|
79
|
+
return commandConnect(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
80
|
+
case "agent":
|
|
81
|
+
return commandAgent(context, subcommand, rest);
|
|
82
|
+
case "auth":
|
|
83
|
+
return commandAuth(context, subcommand, rest);
|
|
84
|
+
case "computer":
|
|
85
|
+
return commandComputer(context, subcommand, rest);
|
|
86
|
+
case "browser":
|
|
87
|
+
return commandBrowser(context, subcommand, rest);
|
|
88
|
+
case "try":
|
|
89
|
+
return commandTry(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
90
|
+
case "work":
|
|
91
|
+
return commandWork(context, subcommand, rest);
|
|
92
|
+
case "proof":
|
|
93
|
+
return commandProof(context, subcommand, rest);
|
|
94
|
+
case "tools":
|
|
95
|
+
return commandTools(context, subcommand, rest);
|
|
96
|
+
case "jobs":
|
|
97
|
+
return commandJobs(context, subcommand, rest);
|
|
98
|
+
case "artifacts":
|
|
99
|
+
return commandArtifacts(context, subcommand, rest);
|
|
100
|
+
case "usage":
|
|
101
|
+
return commandUsage(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
102
|
+
case "limits":
|
|
103
|
+
return commandUsage(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
104
|
+
case "grants":
|
|
105
|
+
return commandGrants(context, subcommand, rest);
|
|
106
|
+
case "retention":
|
|
107
|
+
return commandRetention(context, subcommand, rest);
|
|
108
|
+
case "scheduled-qa":
|
|
109
|
+
return commandScheduledQa(context, subcommand, rest);
|
|
110
|
+
case "plans":
|
|
111
|
+
return commandPlans(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
112
|
+
case "dashboard":
|
|
113
|
+
return commandDashboard(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
114
|
+
case "inspect":
|
|
115
|
+
return commandInspect();
|
|
116
|
+
case "doctor":
|
|
117
|
+
return commandDoctor(context, parseCommandOptions(withOptionalHead(subcommand, rest)));
|
|
118
|
+
case "help":
|
|
119
|
+
return helpResult(withOptionalHead(subcommand, rest));
|
|
120
|
+
default:
|
|
121
|
+
throw unknownCommandError(command);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function commandInspect() {
|
|
125
|
+
const summary = goalCoverageSummary();
|
|
126
|
+
return {
|
|
127
|
+
message: summary.hostedRequired === 0
|
|
128
|
+
? `vc-tools goal coverage: ${summary.localVerified}/${summary.total} inspections verified.`
|
|
129
|
+
: `vc-tools goal coverage: ${summary.localVerified}/${summary.total} locally verified, ${summary.hostedRequired} hosted-service check pending.`,
|
|
130
|
+
data: {
|
|
131
|
+
summary,
|
|
132
|
+
inspections: GOAL_INSPECTIONS
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function commandStart(context, parsed) {
|
|
137
|
+
const clientName = getStringFlag(parsed.flags, "client") ?? parsed.positionals[0] ?? "generic";
|
|
138
|
+
const surface = outputSurface(parsed);
|
|
139
|
+
let token = await resolveToken(context, false);
|
|
140
|
+
let login;
|
|
141
|
+
if (!token) {
|
|
142
|
+
if (context.globals.noInput) {
|
|
143
|
+
throw new CliError("auth.approval_required", "This Agent Computer is not connected yet. Run vc-tools start without --no-input to open Vibecodr approval, or use an advanced file/stdin credential source for automation.", 3);
|
|
144
|
+
}
|
|
145
|
+
login = await commandLogin(context, parseCommandOptions([]));
|
|
146
|
+
token = await resolveToken(context, true);
|
|
147
|
+
}
|
|
148
|
+
const { profile } = await getOptionalProfile(context);
|
|
149
|
+
let client = createClient(context, profile, token);
|
|
150
|
+
let readiness;
|
|
151
|
+
try {
|
|
152
|
+
readiness = await readStartReadiness(client, clientName, surface);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (toCliError(error).code !== "auth.denied") {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
token = await resolveToken(context, true, { forceRefresh: true });
|
|
159
|
+
client = createClient(context, profile, token);
|
|
160
|
+
readiness = await readStartReadiness(client, clientName, surface);
|
|
161
|
+
}
|
|
162
|
+
const [me, health, connection, usage] = readiness;
|
|
163
|
+
const data = surface.details || surface.operator
|
|
164
|
+
? {
|
|
165
|
+
...publicStartPayload(me, health, connection, usage, login !== undefined),
|
|
166
|
+
details: {
|
|
167
|
+
account: me,
|
|
168
|
+
health: publicHealthPayload(health),
|
|
169
|
+
agentConnection: publicConnectionPayload(connection),
|
|
170
|
+
usage: publicUsagePayload(usage)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
: publicStartPayload(me, health, connection, usage, login !== undefined);
|
|
174
|
+
return {
|
|
175
|
+
message: formatStartSummary(me, health, connection, login !== undefined),
|
|
176
|
+
data,
|
|
177
|
+
humanData: surface.details || surface.operator ? "show" : "hide"
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async function readStartReadiness(client, clientName, surface) {
|
|
181
|
+
return await Promise.all([
|
|
182
|
+
client.request("GET", "me"),
|
|
183
|
+
client.request("GET", "health", { auth: false, query: queryForSurface(surface) }),
|
|
184
|
+
client.request("GET", "mcp/connection", { query: { client: clientName, ...queryForSurface(surface) } }),
|
|
185
|
+
client.request("GET", "usage", { query: queryForSurface(surface) }).catch((error) => ({ unavailable: true, message: toCliError(error).message }))
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
async function commandTry(context, parsed) {
|
|
189
|
+
await commandStart(context, parsed);
|
|
190
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
191
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
192
|
+
const proofDir = getStringFlag(parsed.flags, "out") ?? "vc-tools-proof";
|
|
193
|
+
const browserParsed = {
|
|
194
|
+
positionals: ["https://example.com"],
|
|
195
|
+
flags: {
|
|
196
|
+
...parsed.flags,
|
|
197
|
+
out: proofDir,
|
|
198
|
+
filename: "browser-read.md",
|
|
199
|
+
pollIntervalMs: getStringFlag(parsed.flags, "pollIntervalMs") ?? "250"
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const computerParsed = {
|
|
203
|
+
positionals: [],
|
|
204
|
+
flags: {
|
|
205
|
+
...parsed.flags,
|
|
206
|
+
command: "node -e \"console.log('vc-tools computer ok')\"",
|
|
207
|
+
out: proofDir,
|
|
208
|
+
filename: "computer-run.json",
|
|
209
|
+
pollIntervalMs: getStringFlag(parsed.flags, "pollIntervalMs") ?? "250"
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const checks = {
|
|
213
|
+
auth: "ok",
|
|
214
|
+
hostedApi: "ok",
|
|
215
|
+
browser: "failed",
|
|
216
|
+
computer: "failed",
|
|
217
|
+
proof: "failed",
|
|
218
|
+
usage: "failed"
|
|
219
|
+
};
|
|
220
|
+
const warnings = [];
|
|
221
|
+
const browserPayload = buildToolTestPayload("browser.extract_markdown", browserParsed.positionals[0], browserParsed);
|
|
222
|
+
const browserWork = await client.request("POST", "tools/test", { body: browserPayload });
|
|
223
|
+
const browserResult = await followSubmittedWork(context, client, "browser.extract_markdown", browserWork, browserParsed);
|
|
224
|
+
if (isRecord(browserResult.data) && browserResult.data.status === "completed") {
|
|
225
|
+
checks.browser = "ok";
|
|
226
|
+
}
|
|
227
|
+
const browserProof = isRecord(browserResult.data) && isRecord(browserResult.data.proof) ? browserResult.data.proof : undefined;
|
|
228
|
+
if (isRecord(browserProof) && typeof browserProof.path === "string") {
|
|
229
|
+
checks.proof = "ok";
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const computerPayload = buildToolTestPayload("sandbox.run_command", undefined, computerParsed);
|
|
233
|
+
const computerWork = await client.request("POST", "tools/test", { body: computerPayload });
|
|
234
|
+
const computerResult = await followSubmittedWork(context, client, "sandbox.run_command", computerWork, computerParsed);
|
|
235
|
+
if (isRecord(computerResult.data) && computerResult.data.status === "completed") {
|
|
236
|
+
checks.computer = "ok";
|
|
237
|
+
}
|
|
238
|
+
const computerProof = isRecord(computerResult.data) && isRecord(computerResult.data.proof) ? computerResult.data.proof : undefined;
|
|
239
|
+
if (isRecord(computerProof) && typeof computerProof.path === "string") {
|
|
240
|
+
checks.proof = "ok";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
warnings.push(`Computer check did not complete: ${toCliError(error).message}`);
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
await client.request("GET", "usage");
|
|
248
|
+
checks.usage = "ok";
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
warnings.push(`Usage check did not complete: ${toCliError(error).message}`);
|
|
252
|
+
}
|
|
253
|
+
const ready = Object.values(checks).every((status) => status === "ok");
|
|
254
|
+
return {
|
|
255
|
+
message: ready
|
|
256
|
+
? `Vibecodr Agent Computer check passed.\nProof saved: ${path.resolve(context.cwd, proofDir)}`
|
|
257
|
+
: `Vibecodr Agent Computer check finished with attention needed.\nProof path: ${path.resolve(context.cwd, proofDir)}`,
|
|
258
|
+
data: {
|
|
259
|
+
ready,
|
|
260
|
+
checks,
|
|
261
|
+
proofPath: path.resolve(context.cwd, proofDir)
|
|
262
|
+
},
|
|
263
|
+
warnings,
|
|
264
|
+
humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function commandDashboard(context, parsed) {
|
|
268
|
+
const section = getStringFlag(parsed.flags, "section") ?? parsed.positionals[0] ?? "overview";
|
|
269
|
+
const sections = DASHBOARD_SECTIONS.map((item) => item.id);
|
|
270
|
+
if (!sections.includes(section)) {
|
|
271
|
+
throw new CliError("input.invalid_dashboard_section", `Dashboard section must be one of: ${sections.join(", ")}.`, 2);
|
|
272
|
+
}
|
|
273
|
+
const { profile } = await getOptionalProfile(context);
|
|
274
|
+
const url = normalizeBaseUrl(context.globals.apiUrl ?? context.env.VC_TOOLS_API_URL ?? profile.apiUrl, allowInsecureLocalApi(context));
|
|
275
|
+
url.pathname = section === "overview" ? "/dashboard/" : `/dashboard/${section}/`;
|
|
276
|
+
url.search = "";
|
|
277
|
+
url.hash = "";
|
|
278
|
+
const urlString = url.toString();
|
|
279
|
+
const skipOpen = getBooleanFlag(parsed.flags, "noOpen") ||
|
|
280
|
+
parsed.flags.open === false ||
|
|
281
|
+
context.globals.json ||
|
|
282
|
+
context.globals.quiet ||
|
|
283
|
+
context.globals.noInput;
|
|
284
|
+
let opened = false;
|
|
285
|
+
if (!skipOpen) {
|
|
286
|
+
opened = await maybeOpenBrowser(context, parsed, urlString);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
message: opened
|
|
290
|
+
? `Opened the Vibecodr Tools dashboard: ${urlString}`
|
|
291
|
+
: `Vibecodr Tools dashboard: ${urlString}\nUse vc-tools dashboard --no-open to suppress opening, or --json for machine-readable metadata.`,
|
|
292
|
+
data: {
|
|
293
|
+
url: urlString,
|
|
294
|
+
section,
|
|
295
|
+
opened,
|
|
296
|
+
sections,
|
|
297
|
+
sectionContract: DASHBOARD_SECTIONS.find((item) => item.id === section)
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
async function commandLogin(context, parsed) {
|
|
302
|
+
const credential = await resolveLoginCredential(context, parsed, true);
|
|
303
|
+
let exchange;
|
|
304
|
+
let token;
|
|
305
|
+
let authMode;
|
|
306
|
+
let browserLogin;
|
|
307
|
+
if (credential === undefined) {
|
|
308
|
+
if (context.globals.noInput) {
|
|
309
|
+
throw new CliError("auth.token_required", "Browser login needs interactive approval. Run vc-tools login without --no-input, or use an automation-safe credential source such as Get-Clipboard | vc-tools login --credential-stdin or vc-tools login --credential-file <path>.", 3);
|
|
310
|
+
}
|
|
311
|
+
const browserExchange = await completeBrowserDeviceLogin(context, parsed);
|
|
312
|
+
exchange = browserExchange.exchange;
|
|
313
|
+
token = exchange.access_token;
|
|
314
|
+
authMode = "browser_device";
|
|
315
|
+
browserLogin = browserExchange.browserLogin;
|
|
316
|
+
validateTokenShape(token);
|
|
317
|
+
}
|
|
318
|
+
else if (credential.mode === "token") {
|
|
319
|
+
token = credential.value;
|
|
320
|
+
authMode = credential.mode;
|
|
321
|
+
validateTokenShape(token);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
authMode = credential.mode;
|
|
325
|
+
validateCredentialShape(credential.value, credential.mode === "oauth" ? "OAuth token" : "API key");
|
|
326
|
+
exchange = await exchangeCredentialForGrant(context, parsed, credential);
|
|
327
|
+
token = exchange.access_token;
|
|
328
|
+
validateTokenShape(token);
|
|
329
|
+
}
|
|
330
|
+
const apiUrl = context.globals.apiUrl ?? getStringFlag(parsed.flags, "apiUrl") ?? context.env.VC_TOOLS_API_URL ?? DEFAULT_API_URL;
|
|
331
|
+
const skipVerify = getBooleanFlag(parsed.flags, "skipVerify");
|
|
332
|
+
const client = createApiClient({
|
|
333
|
+
baseUrl: versionedApiUrl(apiUrl, allowInsecureLocalApi(context)),
|
|
334
|
+
token,
|
|
335
|
+
timeoutMs: context.globals.timeoutMs,
|
|
336
|
+
allowInsecureLocalApi: allowInsecureLocalApi(context),
|
|
337
|
+
fetchImpl: context.fetchImpl
|
|
338
|
+
});
|
|
339
|
+
let me;
|
|
340
|
+
if (!skipVerify) {
|
|
341
|
+
me = await client.request("GET", "me");
|
|
342
|
+
}
|
|
343
|
+
const workspaceId = me?.workspace?.id;
|
|
344
|
+
await context.store.saveProfile(context.globals.profile, workspaceId ? { apiUrl, workspaceId } : { apiUrl });
|
|
345
|
+
await storeLoginAuth(context, authMode, credential, exchange, token);
|
|
346
|
+
return {
|
|
347
|
+
message: skipVerify
|
|
348
|
+
? "Saved the Agent Computer credential without live verification."
|
|
349
|
+
: `Approved this Vibecodr Agent Computer for ${formatMaybeAccountLabel(me)}.`,
|
|
350
|
+
data: {
|
|
351
|
+
apiUrl,
|
|
352
|
+
authMode,
|
|
353
|
+
storedAuth: storedCredentialSummary(authMode, credential, exchange),
|
|
354
|
+
grantExpiresAt: exchange?.expires_at,
|
|
355
|
+
grantProfile: exchange?.grant_profile,
|
|
356
|
+
grantScopes: exchange?.scopes,
|
|
357
|
+
browserLogin,
|
|
358
|
+
verified: !skipVerify,
|
|
359
|
+
user: me?.user,
|
|
360
|
+
workspace: me?.workspace,
|
|
361
|
+
plan: me?.plan
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async function commandLogout(context, parsed) {
|
|
366
|
+
const yes = getBooleanFlag(parsed.flags, "yes");
|
|
367
|
+
if (!yes) {
|
|
368
|
+
throw new CliError("confirm.required", "Logout removes the stored Agent Computer credential. Re-run with --yes to confirm.", 4);
|
|
369
|
+
}
|
|
370
|
+
const cleared = await context.store.clearToken(context.globals.profile);
|
|
371
|
+
return {
|
|
372
|
+
message: cleared ? "Removed this Agent Computer credential." : "No Agent Computer credential was stored.",
|
|
373
|
+
data: { cleared }
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async function storeLoginAuth(context, authMode, credential, exchange, token) {
|
|
377
|
+
const savedAt = new Date().toISOString();
|
|
378
|
+
const grant = {
|
|
379
|
+
token,
|
|
380
|
+
savedAt,
|
|
381
|
+
source: authMode === "browser_device" ? "browser_device" : authMode === "token" ? "token" : "exchange",
|
|
382
|
+
expiresAt: exchange?.expires_at
|
|
383
|
+
};
|
|
384
|
+
const durableFromDevice = exchange?.durable_credential;
|
|
385
|
+
if (durableFromDevice) {
|
|
386
|
+
await context.store.saveDurableCredential({
|
|
387
|
+
mode: "api_key",
|
|
388
|
+
value: durableFromDevice.api_key,
|
|
389
|
+
savedAt,
|
|
390
|
+
source: "browser_device",
|
|
391
|
+
expiresAt: durableFromDevice.expires_at
|
|
392
|
+
}, grant);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (credential?.mode === "api_key" || credential?.mode === "oauth") {
|
|
396
|
+
await context.store.saveDurableCredential({
|
|
397
|
+
mode: credential.mode,
|
|
398
|
+
value: credential.value,
|
|
399
|
+
savedAt,
|
|
400
|
+
source: credential.source,
|
|
401
|
+
expiresAt: exchange?.durable_credential?.expires_at
|
|
402
|
+
}, grant);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
await context.store.saveGrant(grant);
|
|
406
|
+
}
|
|
407
|
+
function storedCredentialSummary(authMode, credential, exchange) {
|
|
408
|
+
const durableFromDevice = exchange?.durable_credential;
|
|
409
|
+
if (durableFromDevice) {
|
|
410
|
+
return {
|
|
411
|
+
kind: "durable",
|
|
412
|
+
mode: "api_key",
|
|
413
|
+
source: "browser_device",
|
|
414
|
+
name: durableFromDevice.name,
|
|
415
|
+
expiresAt: durableFromDevice.expires_at
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (credential?.mode === "api_key" || credential?.mode === "oauth") {
|
|
419
|
+
return {
|
|
420
|
+
kind: "durable",
|
|
421
|
+
mode: credential.mode,
|
|
422
|
+
source: credential.source
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
kind: "grant_cache",
|
|
427
|
+
mode: authMode === "browser_device" ? "browser_device" : "token"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function commandStatus(context) {
|
|
431
|
+
const auth = await inspectAuthState(context);
|
|
432
|
+
const { profile } = await getOptionalProfile(context);
|
|
433
|
+
let health;
|
|
434
|
+
const warnings = [...auth.warnings];
|
|
435
|
+
try {
|
|
436
|
+
const client = createClient(context, profile, auth.token);
|
|
437
|
+
health = await client.request("GET", "health", { auth: false });
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
warnings.push(toCliError(error).message);
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
message: auth.token
|
|
444
|
+
? `This Vibecodr Agent Computer has a credential available from ${auth.credential.winning?.label ?? "stored credentials"}. Run vc-tools agent status for account and connection details.`
|
|
445
|
+
: "This Vibecodr Agent Computer is not connected yet. Run vc-tools start to connect it.",
|
|
446
|
+
warnings,
|
|
447
|
+
data: {
|
|
448
|
+
apiUrl: profile.apiUrl,
|
|
449
|
+
config: auth.config,
|
|
450
|
+
authSources: auth.credential,
|
|
451
|
+
authenticated: Boolean(auth.token),
|
|
452
|
+
health
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async function commandWhoami(context) {
|
|
457
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
458
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
459
|
+
const me = await client.request("GET", "me");
|
|
460
|
+
return {
|
|
461
|
+
message: formatWhoamiSummary(me),
|
|
462
|
+
data: me
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async function commandConnect(context, parsed) {
|
|
466
|
+
const clientName = getStringFlag(parsed.flags, "client") ?? parsed.positionals[0] ?? "generic";
|
|
467
|
+
const serverName = getStringFlag(parsed.flags, "name") ?? "vc-tools";
|
|
468
|
+
const surface = outputSurface(parsed);
|
|
469
|
+
const printOnly = getBooleanFlag(parsed.flags, "print") || parsed.flags.install === false;
|
|
470
|
+
const dryRun = getBooleanFlag(parsed.flags, "dryRun") || parsed.flags.dryRun === true;
|
|
471
|
+
const overwrite = getBooleanFlag(parsed.flags, "overwrite");
|
|
472
|
+
const installDir = getStringFlag(parsed.flags, "installDir");
|
|
473
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
474
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
475
|
+
const connection = await client.request("GET", "mcp/connection", {
|
|
476
|
+
query: { client: clientName, ...queryForSurface(surface) }
|
|
477
|
+
});
|
|
478
|
+
const publicConnection = publicConnectionPayload(connection);
|
|
479
|
+
const url = typeof publicConnection.url === "string" ? publicConnection.url : undefined;
|
|
480
|
+
const warnings = [];
|
|
481
|
+
let installResult;
|
|
482
|
+
if (!printOnly && url && isInstallableClient(clientName)) {
|
|
483
|
+
try {
|
|
484
|
+
installResult = await installClient({
|
|
485
|
+
client: clientName,
|
|
486
|
+
serverUrl: url,
|
|
487
|
+
serverName,
|
|
488
|
+
overwrite,
|
|
489
|
+
dryRun,
|
|
490
|
+
cwd: context.cwd,
|
|
491
|
+
env: context.env,
|
|
492
|
+
installDir
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
warnings.push(`${toCliError(error).message} Falling back to copy-paste config; pass --print to skip install attempts.`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const message = formatAgentConnectionSummary(clientName, publicConnection, serverName, installResult);
|
|
500
|
+
const data = surface.details || surface.operator
|
|
501
|
+
? { ...publicConnection, details: connection, install: installResult }
|
|
502
|
+
: installResult
|
|
503
|
+
? { ...publicConnection, install: installResult }
|
|
504
|
+
: publicConnection;
|
|
505
|
+
return {
|
|
506
|
+
message,
|
|
507
|
+
data,
|
|
508
|
+
warnings,
|
|
509
|
+
humanData: surface.details || surface.operator ? "show" : "hide"
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
async function commandAgent(context, subcommand, rest) {
|
|
513
|
+
switch (subcommand ?? "connect") {
|
|
514
|
+
case "connect":
|
|
515
|
+
case "instructions":
|
|
516
|
+
return commandConnect(context, parseCommandOptions(rest));
|
|
517
|
+
case "status":
|
|
518
|
+
return commandStart(context, parseCommandOptions(rest));
|
|
519
|
+
default:
|
|
520
|
+
throw unknownSubcommandError("agent", subcommand, ["connect", "instructions", "status"], "Use vc-tools agent connect [--client codex] or vc-tools agent status.");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async function commandAuth(context, subcommand, rest) {
|
|
524
|
+
switch (subcommand ?? "diagnose") {
|
|
525
|
+
case "diagnose":
|
|
526
|
+
return commandAuthDiagnose(context);
|
|
527
|
+
case "status":
|
|
528
|
+
return commandStatus(context);
|
|
529
|
+
case "export-agent-env":
|
|
530
|
+
return commandAuthExportAgentEnv(context, parseCommandOptions(rest));
|
|
531
|
+
default:
|
|
532
|
+
throw unknownSubcommandError("auth", subcommand, ["diagnose", "status", "export-agent-env"], "Use vc-tools auth diagnose or vc-tools auth export-agent-env --out <file> --yes.");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function commandAuthDiagnose(context) {
|
|
536
|
+
const auth = await inspectAuthState(context);
|
|
537
|
+
const { profile } = await getOptionalProfile(context);
|
|
538
|
+
let verification;
|
|
539
|
+
const warnings = [...auth.warnings];
|
|
540
|
+
if (auth.token) {
|
|
541
|
+
try {
|
|
542
|
+
const client = createClient(context, profile, auth.token);
|
|
543
|
+
const me = await client.request("GET", "me");
|
|
544
|
+
verification = {
|
|
545
|
+
ok: true,
|
|
546
|
+
account: {
|
|
547
|
+
label: formatAccountLabel(me),
|
|
548
|
+
plan: me.plan?.name,
|
|
549
|
+
workspace: me.workspace?.name ?? me.workspace?.id
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
const cliError = toCliError(error);
|
|
555
|
+
verification = {
|
|
556
|
+
ok: false,
|
|
557
|
+
code: cliError.code,
|
|
558
|
+
message: cliError.message
|
|
559
|
+
};
|
|
560
|
+
warnings.push(cliError.message);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const message = auth.token
|
|
564
|
+
? `Auth diagnose: credential source is ${auth.credential.winning?.label ?? "stored credentials"}.`
|
|
565
|
+
: "Auth diagnose: no usable credential source found. Run vc-tools start.";
|
|
566
|
+
return {
|
|
567
|
+
message,
|
|
568
|
+
warnings,
|
|
569
|
+
data: {
|
|
570
|
+
apiUrl: profile.apiUrl,
|
|
571
|
+
os: {
|
|
572
|
+
platform: process.platform,
|
|
573
|
+
user: safeOsUser()
|
|
574
|
+
},
|
|
575
|
+
config: auth.config,
|
|
576
|
+
authSources: auth.credential,
|
|
577
|
+
verification,
|
|
578
|
+
next: auth.token
|
|
579
|
+
? ["Use vc-tools agent status to verify the full Agent Computer connection."]
|
|
580
|
+
: ["Run vc-tools start to connect this Agent Computer.", "If this is an isolated agent, check VC_TOOLS_CONFIG_DIR and VC_TOOLS_CREDENTIAL_FILE."]
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
async function commandAuthExportAgentEnv(context, parsed) {
|
|
585
|
+
const out = getStringFlag(parsed.flags, "out") ?? parsed.positionals[0];
|
|
586
|
+
if (!out) {
|
|
587
|
+
throw new CliError("input.output_required", "auth export-agent-env requires --out <file>.", 2);
|
|
588
|
+
}
|
|
589
|
+
if (!getBooleanFlag(parsed.flags, "yes")) {
|
|
590
|
+
throw new CliError("confirm.required", "Exporting an agent credential writes a credential file to disk. Re-run with --yes to confirm.", 4);
|
|
591
|
+
}
|
|
592
|
+
const auth = await inspectAuthState(context);
|
|
593
|
+
const authState = await context.store.readAuthState().catch(() => ({ version: 2 }));
|
|
594
|
+
const durableCredential = authState.credential?.mode === "api_key" || authState.credential?.mode === "oauth"
|
|
595
|
+
? authState.credential
|
|
596
|
+
: undefined;
|
|
597
|
+
if (!auth.token) {
|
|
598
|
+
throw new CliError("auth.missing", "No vc-tools approval is available to export. Run vc-tools start first.", 3);
|
|
599
|
+
}
|
|
600
|
+
const target = path.resolve(context.cwd, out);
|
|
601
|
+
await ensureOutputPathAllowed(context.cwd, target);
|
|
602
|
+
if (await pathExists(target) && !getBooleanFlag(parsed.flags, "overwrite")) {
|
|
603
|
+
throw new CliError("file.exists", `Refusing to overwrite existing credential file: ${target}. Use --overwrite if intended.`, 5);
|
|
604
|
+
}
|
|
605
|
+
await fs.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
606
|
+
const exportedValue = durableCredential?.value ?? auth.token;
|
|
607
|
+
await fs.writeFile(target, `${exportedValue}\n`, { encoding: "utf8", mode: 0o600 });
|
|
608
|
+
try {
|
|
609
|
+
await fs.chmod(target, 0o600);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// Windows may not honor POSIX modes, but Node still requests the narrowest practical mode.
|
|
613
|
+
}
|
|
614
|
+
const envName = durableCredential ? "VC_TOOLS_CREDENTIAL_FILE" : "VC_TOOLS_TOKEN_FILE";
|
|
615
|
+
const exportedKind = durableCredential ? formatCredentialMode(durableCredential.mode) : "short-lived vc-tools grant";
|
|
616
|
+
return {
|
|
617
|
+
message: `Wrote an agent credential file (${exportedKind}). Set ${envName}=${target} for the agent process.`,
|
|
618
|
+
data: {
|
|
619
|
+
file: target,
|
|
620
|
+
env: {
|
|
621
|
+
name: envName,
|
|
622
|
+
value: target,
|
|
623
|
+
assignment: `${envName}=${target}`
|
|
624
|
+
},
|
|
625
|
+
durable: Boolean(durableCredential),
|
|
626
|
+
source: auth.credential.winning,
|
|
627
|
+
note: "Do not commit this file, paste it into prompts, or share it outside the intended agent process."
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
async function commandBrowser(context, subcommand, rest) {
|
|
632
|
+
const parsed = parseCommandOptions(rest);
|
|
633
|
+
switch (subcommand) {
|
|
634
|
+
case "render":
|
|
635
|
+
return submitHostedCapability(context, "browser.render", parsed, "Asked the hosted Browser to render the public page.", { autoFollow: true });
|
|
636
|
+
case "screenshot":
|
|
637
|
+
return submitHostedCapability(context, "browser.screenshot", parsed, "Asked the hosted Browser to capture a screenshot.", { autoFollow: true });
|
|
638
|
+
case "read":
|
|
639
|
+
case "markdown":
|
|
640
|
+
return submitHostedCapability(context, "browser.read", parsed, "Asked the hosted Browser to read the public page.", { autoFollow: true });
|
|
641
|
+
case "pdf":
|
|
642
|
+
return submitHostedCapability(context, "browser.pdf", parsed, "Asked the hosted Browser to create a PDF.", { autoFollow: true });
|
|
643
|
+
case "crawl":
|
|
644
|
+
return submitHostedCapability(context, "browser.crawl", parsed, "Asked the hosted Browser to crawl the public site.", { autoFollow: true });
|
|
645
|
+
case "snapshot":
|
|
646
|
+
case "ask": {
|
|
647
|
+
const normalized = normalizeBrowserAskOptions(parsed);
|
|
648
|
+
return submitHostedCapability(context, "browser.ask", normalized, "Asked the hosted Browser to capture an inspection snapshot for your agent.", { autoFollow: true });
|
|
649
|
+
}
|
|
650
|
+
default:
|
|
651
|
+
throw unknownSubcommandError("browser", subcommand, ["render", "screenshot", "read", "markdown", "pdf", "crawl", "snapshot", "ask"], "Use vc-tools browser screenshot <https-url>, browser read <https-url>, or browser snapshot <https-url> --instructions <text>.");
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function commandComputer(context, subcommand, rest) {
|
|
655
|
+
switch (subcommand ?? "status") {
|
|
656
|
+
case "start":
|
|
657
|
+
case "status":
|
|
658
|
+
return commandStart(context, parseCommandOptions(rest));
|
|
659
|
+
case "run": {
|
|
660
|
+
const parsed = normalizeComputerCommandOptions(parseCommandOptions(rest), "computer run requires a command, for example: vc-tools computer run \"npm test\".");
|
|
661
|
+
return submitHostedCapability(context, "computer.run", parsed, "Submitted work to the hosted Agent Computer.", { autoFollow: true });
|
|
662
|
+
}
|
|
663
|
+
case "test":
|
|
664
|
+
case "tests": {
|
|
665
|
+
const parsed = normalizeComputerCommandOptions(parseCommandOptions(rest), "computer test requires a command, for example: vc-tools computer test \"npm test\".");
|
|
666
|
+
return submitHostedCapability(context, "computer.test", parsed, "Submitted tests to the hosted Agent Computer.", { autoFollow: true });
|
|
667
|
+
}
|
|
668
|
+
default:
|
|
669
|
+
throw unknownSubcommandError("computer", subcommand, ["start", "status", "run", "test"], "Use vc-tools computer start, computer status, computer run \"<command>\", or computer test \"<command>\".");
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function commandWork(context, subcommand, rest) {
|
|
673
|
+
switch (subcommand ?? "list") {
|
|
674
|
+
case "list":
|
|
675
|
+
return commandJobs(context, "list", rest);
|
|
676
|
+
case "show":
|
|
677
|
+
case "status":
|
|
678
|
+
return commandJobs(context, "status", rest);
|
|
679
|
+
case "follow":
|
|
680
|
+
return commandWorkFollow(context, parseCommandOptions(rest));
|
|
681
|
+
case "cancel":
|
|
682
|
+
return commandJobs(context, "cancel", rest);
|
|
683
|
+
default:
|
|
684
|
+
throw unknownSubcommandError("work", subcommand, ["list", "show", "status", "follow", "cancel"], "Use vc-tools work list, work show <jobId>, work follow <jobId>, or work cancel <jobId> --yes.");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function commandProof(context, subcommand, rest) {
|
|
688
|
+
switch (subcommand ?? "list") {
|
|
689
|
+
case "list":
|
|
690
|
+
return commandArtifacts(context, "list", rest);
|
|
691
|
+
case "show":
|
|
692
|
+
case "get":
|
|
693
|
+
return commandArtifacts(context, "get", rest);
|
|
694
|
+
case "save":
|
|
695
|
+
case "pull":
|
|
696
|
+
return commandArtifacts(context, "pull", rest);
|
|
697
|
+
case "delete":
|
|
698
|
+
return commandArtifacts(context, "delete", rest);
|
|
699
|
+
default:
|
|
700
|
+
throw unknownSubcommandError("proof", subcommand, ["list", "show", "save", "delete"], "Use vc-tools proof list, proof show <artifactId>, proof save <artifactId> --out ./artifacts, or proof delete <artifactId> --yes.");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async function commandTools(context, subcommand, rest) {
|
|
704
|
+
switch (subcommand) {
|
|
705
|
+
case "list": {
|
|
706
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
707
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
708
|
+
const tools = await client.request("GET", "tools");
|
|
709
|
+
return { message: "Fetched granted vc-tools capabilities.", data: tools };
|
|
710
|
+
}
|
|
711
|
+
case "test":
|
|
712
|
+
return commandToolsTest(context, parseCommandOptions(rest));
|
|
713
|
+
default:
|
|
714
|
+
throw unknownSubcommandError("tools", subcommand, ["list", "test"], "Use vc-tools tools list or vc-tools tools test <capability>.");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function commandToolsTest(context, parsed) {
|
|
718
|
+
const [capabilityInput, target] = parsed.positionals;
|
|
719
|
+
if (!capabilityInput) {
|
|
720
|
+
throw new CliError("input.capability_required", "tools test requires a capability name.", 2);
|
|
721
|
+
}
|
|
722
|
+
return submitHostedCapability(context, capabilityInput, { positionals: target === undefined ? [] : [target], flags: parsed.flags });
|
|
723
|
+
}
|
|
724
|
+
async function submitHostedCapability(context, capabilityInput, parsed, successMessage, options = {}) {
|
|
725
|
+
const capability = normalizeCapabilityName(capabilityInput);
|
|
726
|
+
const payload = buildToolTestPayload(capability, parsed.positionals[0], parsed, context.globals.timeoutMs === 30_000 ? undefined : context.globals.timeoutMs);
|
|
727
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
728
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
729
|
+
const response = await client.request("POST", "tools/test", {
|
|
730
|
+
body: payload
|
|
731
|
+
});
|
|
732
|
+
if (options.autoFollow === true && !shouldSkipWait(parsed)) {
|
|
733
|
+
return followSubmittedWork(context, client, capability, response, parsed);
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
message: successMessage ?? (capability === "usage.read" ? "Read usage and limits from hosted vc-tools." : `Submitted ${capability} test to hosted vc-tools.`),
|
|
737
|
+
data: response
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function shouldSkipWait(parsed) {
|
|
741
|
+
return getBooleanFlag(parsed.flags, "noWait") || parsed.flags.wait === false;
|
|
742
|
+
}
|
|
743
|
+
async function followSubmittedWork(context, client, capability, submitted, parsed) {
|
|
744
|
+
const jobId = jobIdFromWork(submitted);
|
|
745
|
+
if (!jobId) {
|
|
746
|
+
return {
|
|
747
|
+
message: completedCapabilityMessage(capability),
|
|
748
|
+
data: publicWorkResult(capability, submitted, parsed),
|
|
749
|
+
humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const terminal = isTerminalWork(submitted)
|
|
753
|
+
? submitted
|
|
754
|
+
: await pollWorkUntilTerminal(client, jobId, parsed);
|
|
755
|
+
const artifactId = artifactIdFromWork(terminal);
|
|
756
|
+
const proof = artifactId && getStringFlag(parsed.flags, "out") !== undefined
|
|
757
|
+
? await saveArtifact(context, client, artifactId, parsed)
|
|
758
|
+
: undefined;
|
|
759
|
+
return {
|
|
760
|
+
message: formatCompletedWorkMessage(capability, terminal, proof),
|
|
761
|
+
data: publicWorkResult(capability, terminal, parsed, proof),
|
|
762
|
+
humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
async function commandWorkFollow(context, parsed) {
|
|
766
|
+
const jobId = validateEntityId(requiredPositional(parsed, 0, "work follow requires a job id."), "job id");
|
|
767
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
768
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
769
|
+
const job = await pollWorkUntilTerminal(client, jobId, parsed);
|
|
770
|
+
const artifactId = artifactIdFromWork(job);
|
|
771
|
+
const proof = artifactId && getStringFlag(parsed.flags, "out") !== undefined
|
|
772
|
+
? await saveArtifact(context, client, artifactId, parsed)
|
|
773
|
+
: undefined;
|
|
774
|
+
return {
|
|
775
|
+
message: formatCompletedWorkMessage(undefined, job, proof),
|
|
776
|
+
data: publicWorkResult(undefined, job, parsed, proof),
|
|
777
|
+
humanData: getBooleanFlag(parsed.flags, "details") ? "show" : "hide"
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
async function pollWorkUntilTerminal(client, jobId, parsed) {
|
|
781
|
+
const timeoutMs = validatePositiveInt(getStringFlag(parsed.flags, "waitTimeoutMs") ?? getStringFlag(parsed.flags, "timeoutMs") ?? "180000", "--wait-timeout-ms", 1000, 3_600_000) ?? 180_000;
|
|
782
|
+
const pollIntervalMs = validatePositiveInt(getStringFlag(parsed.flags, "pollIntervalMs") ?? "250", "--poll-interval-ms", 100, 30_000) ?? 250;
|
|
783
|
+
const deadline = Date.now() + timeoutMs;
|
|
784
|
+
let latest = await client.request("GET", `jobs/${encodePathSegment(jobId)}`);
|
|
785
|
+
while (!isTerminalWork(latest)) {
|
|
786
|
+
if (Date.now() >= deadline) {
|
|
787
|
+
return latest;
|
|
788
|
+
}
|
|
789
|
+
await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
|
|
790
|
+
latest = await client.request("GET", `jobs/${encodePathSegment(jobId)}`);
|
|
791
|
+
}
|
|
792
|
+
return latest;
|
|
793
|
+
}
|
|
794
|
+
function isTerminalWork(value) {
|
|
795
|
+
const status = workStatus(value);
|
|
796
|
+
return status === "completed" || status === "failed" || status === "cancelled" || status === "canceled";
|
|
797
|
+
}
|
|
798
|
+
function jobIdFromWork(value) {
|
|
799
|
+
if (!isRecord(value)) {
|
|
800
|
+
return undefined;
|
|
801
|
+
}
|
|
802
|
+
return typeof value.id === "string"
|
|
803
|
+
? value.id
|
|
804
|
+
: typeof value.jobId === "string"
|
|
805
|
+
? value.jobId
|
|
806
|
+
: undefined;
|
|
807
|
+
}
|
|
808
|
+
function workStatus(value) {
|
|
809
|
+
return isRecord(value) && typeof value.status === "string" ? value.status : undefined;
|
|
810
|
+
}
|
|
811
|
+
function artifactIdFromWork(value) {
|
|
812
|
+
if (!isRecord(value)) {
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
if (typeof value.artifactId === "string") {
|
|
816
|
+
return value.artifactId;
|
|
817
|
+
}
|
|
818
|
+
if (isRecord(value.result) && typeof value.result.artifactId === "string") {
|
|
819
|
+
return value.result.artifactId;
|
|
820
|
+
}
|
|
821
|
+
if (Array.isArray(value.artifacts)) {
|
|
822
|
+
const first = value.artifacts.find((item) => isRecord(item) && typeof item.id === "string");
|
|
823
|
+
return isRecord(first) && typeof first.id === "string" ? first.id : undefined;
|
|
824
|
+
}
|
|
825
|
+
return undefined;
|
|
826
|
+
}
|
|
827
|
+
function formatCompletedWorkMessage(capability, work, proof) {
|
|
828
|
+
const status = workStatus(work) ?? "completed";
|
|
829
|
+
if (status === "completed") {
|
|
830
|
+
const saved = proof ? `\nProof saved: ${proof.path}` : "";
|
|
831
|
+
return `${completedCapabilityMessage(capability)}${saved}`;
|
|
832
|
+
}
|
|
833
|
+
if (status === "queued" || status === "running") {
|
|
834
|
+
const id = jobIdFromWork(work);
|
|
835
|
+
const follow = id ? `\nFollow it: vc-tools work follow ${id}` : "";
|
|
836
|
+
return `Work accepted and still ${status}.${follow}`;
|
|
837
|
+
}
|
|
838
|
+
const error = isRecord(work) && isRecord(work.error) && typeof work.error.message === "string"
|
|
839
|
+
? `: ${work.error.message}`
|
|
840
|
+
: "";
|
|
841
|
+
return `Hosted work ${status}${error}`;
|
|
842
|
+
}
|
|
843
|
+
function completedCapabilityMessage(capability) {
|
|
844
|
+
switch (capability) {
|
|
845
|
+
case "browser.screenshot_url":
|
|
846
|
+
return "Browser screenshot completed.";
|
|
847
|
+
case "browser.extract_markdown":
|
|
848
|
+
return "Browser read completed.";
|
|
849
|
+
case "browser.render_pdf":
|
|
850
|
+
return "Browser PDF completed.";
|
|
851
|
+
case "browser.render_url":
|
|
852
|
+
return "Browser render completed.";
|
|
853
|
+
case "browser.crawl_site":
|
|
854
|
+
return "Browser crawl completed.";
|
|
855
|
+
case "browser.agent_task":
|
|
856
|
+
return "Browser snapshot completed.";
|
|
857
|
+
case "sandbox.run_command":
|
|
858
|
+
return "Agent Computer run completed.";
|
|
859
|
+
case "sandbox.run_tests":
|
|
860
|
+
return "Agent Computer tests completed.";
|
|
861
|
+
default:
|
|
862
|
+
return "Hosted work completed.";
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function publicWorkResult(capability, work, parsed, proof) {
|
|
866
|
+
const details = getBooleanFlag(parsed.flags, "details");
|
|
867
|
+
const result = {
|
|
868
|
+
status: workStatus(work) ?? "completed"
|
|
869
|
+
};
|
|
870
|
+
if (capability) {
|
|
871
|
+
result.tool = userToolName(capability);
|
|
872
|
+
}
|
|
873
|
+
if (proof) {
|
|
874
|
+
result.proof = {
|
|
875
|
+
path: proof.path,
|
|
876
|
+
bytes: proof.bytes,
|
|
877
|
+
contentType: proof.contentType
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
if (details) {
|
|
881
|
+
result.work = work;
|
|
882
|
+
}
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
885
|
+
function userToolName(capability) {
|
|
886
|
+
if (capability === "browser.screenshot_url")
|
|
887
|
+
return "browser.screenshot";
|
|
888
|
+
if (capability === "browser.extract_markdown")
|
|
889
|
+
return "browser.read";
|
|
890
|
+
if (capability === "browser.render_pdf")
|
|
891
|
+
return "browser.pdf";
|
|
892
|
+
if (capability === "browser.render_url")
|
|
893
|
+
return "browser.render";
|
|
894
|
+
if (capability === "browser.crawl_site")
|
|
895
|
+
return "browser.crawl";
|
|
896
|
+
if (capability === "browser.agent_task")
|
|
897
|
+
return "browser.snapshot";
|
|
898
|
+
if (capability === "sandbox.run_command")
|
|
899
|
+
return "computer.run";
|
|
900
|
+
if (capability === "sandbox.run_tests")
|
|
901
|
+
return "computer.test";
|
|
902
|
+
return capability;
|
|
903
|
+
}
|
|
904
|
+
function normalizeBrowserAskOptions(parsed) {
|
|
905
|
+
const [url, ...instructionParts] = parsed.positionals;
|
|
906
|
+
const flags = { ...parsed.flags };
|
|
907
|
+
if (getStringFlag(flags, "instructions") === undefined && instructionParts.length > 0) {
|
|
908
|
+
flags.instructions = instructionParts.join(" ");
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
positionals: url === undefined ? [] : [url],
|
|
912
|
+
flags
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function normalizeComputerCommandOptions(parsed, missingMessage) {
|
|
916
|
+
const command = getStringFlag(parsed.flags, "command") ?? parsed.positionals.join(" ").trim();
|
|
917
|
+
if (!command) {
|
|
918
|
+
throw new CliError("input.command_required", missingMessage, 2);
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
positionals: [],
|
|
922
|
+
flags: {
|
|
923
|
+
...parsed.flags,
|
|
924
|
+
command
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
async function commandJobs(context, subcommand, rest) {
|
|
929
|
+
const parsed = parseCommandOptions(rest);
|
|
930
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
931
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
932
|
+
switch (subcommand) {
|
|
933
|
+
case "list": {
|
|
934
|
+
const limit = validatePositiveInt(getStringFlag(parsed.flags, "limit") ?? "20", "--limit", 1, 100) ?? 20;
|
|
935
|
+
const jobs = await client.request("GET", "jobs", {
|
|
936
|
+
query: { limit }
|
|
937
|
+
});
|
|
938
|
+
return { message: "Fetched recent hosted work.", data: jobs };
|
|
939
|
+
}
|
|
940
|
+
case "status": {
|
|
941
|
+
const jobId = validateEntityId(requiredPositional(parsed, 0, "jobs status requires a job id."), "job id");
|
|
942
|
+
const job = await client.request("GET", `jobs/${encodePathSegment(jobId)}`);
|
|
943
|
+
return { message: formatJobStatusMessage(job, jobId), data: job };
|
|
944
|
+
}
|
|
945
|
+
case "cancel": {
|
|
946
|
+
const jobId = validateEntityId(requiredPositional(parsed, 0, "jobs cancel requires a job id."), "job id");
|
|
947
|
+
if (!getBooleanFlag(parsed.flags, "yes")) {
|
|
948
|
+
throw new CliError("confirm.required", "Canceling a job mutates hosted state. Re-run with --yes to confirm.", 4);
|
|
949
|
+
}
|
|
950
|
+
const job = await client.request("POST", `jobs/${encodePathSegment(jobId)}/cancel`);
|
|
951
|
+
return { message: `Canceled job ${jobId}.`, data: job };
|
|
952
|
+
}
|
|
953
|
+
default:
|
|
954
|
+
throw unknownSubcommandError("jobs", subcommand, ["list", "status", "cancel"], "Use vc-tools jobs list, jobs status <jobId>, or jobs cancel <jobId> --yes.");
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async function commandArtifacts(context, subcommand, rest) {
|
|
958
|
+
const parsed = parseCommandOptions(rest);
|
|
959
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
960
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
961
|
+
switch (subcommand) {
|
|
962
|
+
case "list": {
|
|
963
|
+
const limit = validatePositiveInt(getStringFlag(parsed.flags, "limit") ?? "20", "--limit", 1, 100) ?? 20;
|
|
964
|
+
const artifacts = await client.request("GET", "artifacts", {
|
|
965
|
+
query: { limit }
|
|
966
|
+
});
|
|
967
|
+
return { message: "Fetched artifacts.", data: artifacts };
|
|
968
|
+
}
|
|
969
|
+
case "get": {
|
|
970
|
+
const artifactId = validateEntityId(requiredPositional(parsed, 0, "artifacts get requires an artifact id."), "artifact id");
|
|
971
|
+
const artifact = await client.request("GET", `artifacts/${encodePathSegment(artifactId)}`);
|
|
972
|
+
return { message: `Fetched artifact ${artifactId}.`, data: artifact };
|
|
973
|
+
}
|
|
974
|
+
case "pull":
|
|
975
|
+
return commandArtifactsPull(context, client, parsed);
|
|
976
|
+
case "create":
|
|
977
|
+
return commandArtifactsCreate(context, client, parsed);
|
|
978
|
+
case "delete": {
|
|
979
|
+
const artifactId = validateEntityId(requiredPositional(parsed, 0, "artifacts delete requires an artifact id."), "artifact id");
|
|
980
|
+
if (!getBooleanFlag(parsed.flags, "yes")) {
|
|
981
|
+
throw new CliError("confirm.required", "Deleting an artifact removes hosted shelf metadata and bytes. Re-run with --yes to confirm.", 4);
|
|
982
|
+
}
|
|
983
|
+
const artifact = await client.request("DELETE", `artifacts/${encodePathSegment(artifactId)}`);
|
|
984
|
+
return { message: `Deleted artifact ${artifactId}.`, data: artifact };
|
|
985
|
+
}
|
|
986
|
+
default:
|
|
987
|
+
throw unknownSubcommandError("artifacts", subcommand, ["list", "get", "pull", "create", "delete"], "Use vc-tools artifacts list, get, pull, create, or delete.");
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function commandArtifactsPull(context, client, parsed) {
|
|
991
|
+
const artifactId = validateEntityId(requiredPositional(parsed, 0, "artifacts pull requires an artifact id."), "artifact id");
|
|
992
|
+
const saved = await saveArtifact(context, client, artifactId, parsed);
|
|
993
|
+
return {
|
|
994
|
+
message: `Pulled artifact ${artifactId} to ${saved.path}.`,
|
|
995
|
+
data: saved
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
async function saveArtifact(context, client, artifactId, parsed) {
|
|
999
|
+
const out = getStringFlag(parsed.flags, "out") ?? ".";
|
|
1000
|
+
const outPath = path.resolve(context.cwd, out);
|
|
1001
|
+
await ensureOutputPathAllowed(context.cwd, outPath);
|
|
1002
|
+
const output = await resolveArtifactOutput(outPath);
|
|
1003
|
+
const requestedFilename = getStringFlag(parsed.flags, "filename");
|
|
1004
|
+
let download;
|
|
1005
|
+
let target;
|
|
1006
|
+
if (output.kind === "file") {
|
|
1007
|
+
target = output.target;
|
|
1008
|
+
await assertArtifactTargetWritable(context.cwd, target, parsed);
|
|
1009
|
+
download = await client.download(`artifacts/${encodePathSegment(artifactId)}/download`);
|
|
1010
|
+
}
|
|
1011
|
+
else if (requestedFilename) {
|
|
1012
|
+
target = path.join(output.directory, sanitizeFilename(requestedFilename, `${artifactId}.bin`));
|
|
1013
|
+
await assertArtifactTargetWritable(context.cwd, target, parsed);
|
|
1014
|
+
download = await client.download(`artifacts/${encodePathSegment(artifactId)}/download`);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
download = await client.download(`artifacts/${encodePathSegment(artifactId)}/download`);
|
|
1018
|
+
const filename = sanitizeFilename(download.filename, `${artifactId}.bin`);
|
|
1019
|
+
target = path.join(output.directory, filename);
|
|
1020
|
+
await assertArtifactTargetWritable(context.cwd, target, parsed);
|
|
1021
|
+
}
|
|
1022
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
1023
|
+
await ensureOutputPathAllowed(context.cwd, target);
|
|
1024
|
+
await fs.writeFile(target, download.bytes);
|
|
1025
|
+
return {
|
|
1026
|
+
artifactId,
|
|
1027
|
+
path: target,
|
|
1028
|
+
bytes: download.bytes.byteLength,
|
|
1029
|
+
contentType: download.contentType
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
async function assertArtifactTargetWritable(cwd, target, parsed) {
|
|
1033
|
+
await ensureOutputPathAllowed(cwd, target);
|
|
1034
|
+
if (await pathExists(target) && !getBooleanFlag(parsed.flags, "overwrite")) {
|
|
1035
|
+
throw new CliError("file.exists", `Refusing to overwrite ${target}. Re-run with --overwrite.`, 5);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async function resolveArtifactOutput(outPath) {
|
|
1039
|
+
const stat = await fs.stat(outPath).catch(() => undefined);
|
|
1040
|
+
if (stat?.isFile()) {
|
|
1041
|
+
return { kind: "file", target: outPath };
|
|
1042
|
+
}
|
|
1043
|
+
if (stat?.isDirectory()) {
|
|
1044
|
+
return { kind: "directory", directory: outPath };
|
|
1045
|
+
}
|
|
1046
|
+
if (path.extname(outPath)) {
|
|
1047
|
+
return { kind: "file", target: outPath };
|
|
1048
|
+
}
|
|
1049
|
+
return { kind: "directory", directory: outPath };
|
|
1050
|
+
}
|
|
1051
|
+
async function commandArtifactsCreate(context, client, parsed) {
|
|
1052
|
+
const fileInput = getStringFlag(parsed.flags, "file") ?? parsed.positionals[0];
|
|
1053
|
+
if (!fileInput) {
|
|
1054
|
+
throw new CliError("input.file_required", "artifacts create requires --file <path>.", 2);
|
|
1055
|
+
}
|
|
1056
|
+
if (!getBooleanFlag(parsed.flags, "yes")) {
|
|
1057
|
+
throw new CliError("confirm.required", "Creating an artifact uploads a local file. Re-run with --yes to confirm.", 4);
|
|
1058
|
+
}
|
|
1059
|
+
const filePath = path.resolve(context.cwd, fileInput);
|
|
1060
|
+
await ensureInputPathAllowed(context.cwd, filePath);
|
|
1061
|
+
const stat = await fs.stat(filePath).catch(() => undefined);
|
|
1062
|
+
if (!stat?.isFile()) {
|
|
1063
|
+
throw new CliError("file.not_found", `Artifact file does not exist: ${filePath}`, 5);
|
|
1064
|
+
}
|
|
1065
|
+
const kind = getStringFlag(parsed.flags, "kind") ?? "file";
|
|
1066
|
+
const bytes = await fs.readFile(filePath);
|
|
1067
|
+
const form = new FormData();
|
|
1068
|
+
form.set("file", new Blob([bytes]), path.basename(filePath));
|
|
1069
|
+
form.set("kind", kind);
|
|
1070
|
+
const result = await client.upload("artifacts", form);
|
|
1071
|
+
return { message: `Uploaded artifact ${path.basename(filePath)}.`, data: result };
|
|
1072
|
+
}
|
|
1073
|
+
async function commandUsage(context, parsed) {
|
|
1074
|
+
const surface = outputSurface(parsed);
|
|
1075
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
1076
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
1077
|
+
const usage = await client.request("GET", "usage", { query: queryForSurface(surface) });
|
|
1078
|
+
const data = surface.details || surface.operator ? usage : publicUsagePayload(usage);
|
|
1079
|
+
return {
|
|
1080
|
+
message: formatUsageSummary(data),
|
|
1081
|
+
data,
|
|
1082
|
+
humanData: surface.details || surface.operator ? "show" : "hide"
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
async function commandGrants(context, subcommand, rest) {
|
|
1086
|
+
const selectedSubcommand = subcommand ?? "list";
|
|
1087
|
+
if (selectedSubcommand !== "list") {
|
|
1088
|
+
throw unknownSubcommandError("grants", subcommand, ["list"], "vc-tools grants lists effective grants by default. Use vc-tools grants list [--project <id>] [--user <id>] for explicit filters.");
|
|
1089
|
+
}
|
|
1090
|
+
const parsed = parseCommandOptions(rest);
|
|
1091
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
1092
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
1093
|
+
const grants = await client.request("GET", "grants", {
|
|
1094
|
+
query: {
|
|
1095
|
+
project: getStringFlag(parsed.flags, "project"),
|
|
1096
|
+
user: getStringFlag(parsed.flags, "user")
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
return { message: formatGrantsSummary(grants), data: grants };
|
|
1100
|
+
}
|
|
1101
|
+
async function commandRetention(context, subcommand, rest) {
|
|
1102
|
+
const parsed = parseCommandOptions(rest);
|
|
1103
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
1104
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
1105
|
+
switch (subcommand) {
|
|
1106
|
+
case "show": {
|
|
1107
|
+
const retention = await client.request("GET", "retention");
|
|
1108
|
+
return { message: "Fetched retention policy.", data: retention };
|
|
1109
|
+
}
|
|
1110
|
+
case "set": {
|
|
1111
|
+
if (!getBooleanFlag(parsed.flags, "yes")) {
|
|
1112
|
+
throw new CliError("confirm.required", "Updating retention mutates hosted policy. Re-run with --yes to confirm.", 4);
|
|
1113
|
+
}
|
|
1114
|
+
const logsDays = validatePositiveInt(getStringFlag(parsed.flags, "logsDays"), "--logs-days", 1, 365);
|
|
1115
|
+
const artifactsDays = validatePositiveInt(getStringFlag(parsed.flags, "artifactsDays"), "--artifacts-days", 1, 365);
|
|
1116
|
+
const recordings = getStringFlag(parsed.flags, "recordings");
|
|
1117
|
+
if (recordings !== undefined && !["off", "opt-in", "admin"].includes(recordings)) {
|
|
1118
|
+
throw new CliError("input.invalid_recordings", "--recordings must be off, opt-in, or admin.", 2);
|
|
1119
|
+
}
|
|
1120
|
+
if (logsDays === undefined && artifactsDays === undefined && recordings === undefined) {
|
|
1121
|
+
throw new CliError("input.empty_retention_update", "Provide at least one retention field to update.", 2);
|
|
1122
|
+
}
|
|
1123
|
+
const retention = await client.request("PATCH", "retention", {
|
|
1124
|
+
body: { logsDays, artifactsDays, recordings }
|
|
1125
|
+
});
|
|
1126
|
+
return { message: "Updated retention policy.", data: retention };
|
|
1127
|
+
}
|
|
1128
|
+
default:
|
|
1129
|
+
throw unknownSubcommandError("retention", subcommand, ["show", "set"], "Use vc-tools retention show or retention set.");
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async function commandScheduledQa(context, subcommand, rest) {
|
|
1133
|
+
const parsed = parseCommandOptions(rest);
|
|
1134
|
+
const { profile } = await context.store.getProfile(context.globals.profile);
|
|
1135
|
+
const client = createClient(context, profile, await resolveToken(context, true));
|
|
1136
|
+
switch (subcommand) {
|
|
1137
|
+
case "list": {
|
|
1138
|
+
const scheduled = await client.request("GET", "scheduled-qa");
|
|
1139
|
+
return { message: "Fetched scheduled QA checks.", data: scheduled };
|
|
1140
|
+
}
|
|
1141
|
+
case "create": {
|
|
1142
|
+
const target = requiredPositional(parsed, 0, "scheduled-qa create requires an HTTPS URL target.");
|
|
1143
|
+
if (!getBooleanFlag(parsed.flags, "yes")) {
|
|
1144
|
+
throw new CliError("confirm.required", "Creating scheduled QA mutates hosted state and may spend future Browser Run quota. Re-run with --yes to confirm.", 4);
|
|
1145
|
+
}
|
|
1146
|
+
const body = buildScheduledQaPayload(target, parsed, context.globals.timeoutMs === 30_000 ? undefined : context.globals.timeoutMs);
|
|
1147
|
+
const created = await client.request("POST", "scheduled-qa", { body });
|
|
1148
|
+
return { message: "Created scheduled QA check.", data: created };
|
|
1149
|
+
}
|
|
1150
|
+
case "pause":
|
|
1151
|
+
case "resume": {
|
|
1152
|
+
const id = validateEntityId(requiredPositional(parsed, 0, `scheduled-qa ${subcommand} requires a scheduled QA id.`), "scheduled QA id");
|
|
1153
|
+
if (!getBooleanFlag(parsed.flags, "yes")) {
|
|
1154
|
+
throw new CliError("confirm.required", `Updating scheduled QA mutates hosted state. Re-run with --yes to confirm.`, 4);
|
|
1155
|
+
}
|
|
1156
|
+
const updated = await client.request("PATCH", `scheduled-qa/${encodePathSegment(id)}`, {
|
|
1157
|
+
body: { enabled: subcommand === "resume", runNow: subcommand === "resume" && getBooleanFlag(parsed.flags, "runNow") }
|
|
1158
|
+
});
|
|
1159
|
+
return { message: `${subcommand === "resume" ? "Resumed" : "Paused"} scheduled QA check ${id}.`, data: updated };
|
|
1160
|
+
}
|
|
1161
|
+
case "delete": {
|
|
1162
|
+
const id = validateEntityId(requiredPositional(parsed, 0, "scheduled-qa delete requires a scheduled QA id."), "scheduled QA id");
|
|
1163
|
+
if (!getBooleanFlag(parsed.flags, "yes")) {
|
|
1164
|
+
throw new CliError("confirm.required", "Deleting scheduled QA mutates hosted state. Re-run with --yes to confirm.", 4);
|
|
1165
|
+
}
|
|
1166
|
+
const deleted = await client.request("DELETE", `scheduled-qa/${encodePathSegment(id)}`);
|
|
1167
|
+
return { message: `Deleted scheduled QA check ${id}.`, data: deleted };
|
|
1168
|
+
}
|
|
1169
|
+
default:
|
|
1170
|
+
throw unknownSubcommandError("scheduled-qa", subcommand, ["list", "create", "pause", "resume", "delete"], "Use vc-tools scheduled-qa list, create <url>, pause <id>, resume <id>, or delete <id>.");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
async function commandPlans(context, parsed) {
|
|
1174
|
+
const surface = outputSurface(parsed);
|
|
1175
|
+
const { profile } = await getOptionalProfile(context);
|
|
1176
|
+
const token = await resolveToken(context, false);
|
|
1177
|
+
const warnings = [];
|
|
1178
|
+
if (token) {
|
|
1179
|
+
try {
|
|
1180
|
+
const client = createClient(context, profile, token);
|
|
1181
|
+
const plans = await client.request("GET", "plans", { query: queryForSurface(surface) });
|
|
1182
|
+
const data = surface.details || surface.operator ? plans : publicPlansPayload(plans);
|
|
1183
|
+
return {
|
|
1184
|
+
message: formatPlansSummary(data),
|
|
1185
|
+
data,
|
|
1186
|
+
humanData: surface.details || surface.operator ? "show" : "hide"
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
warnings.push(`Using local fallback plans because hosted plans failed: ${toCliError(error).message}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
warnings.push("Local fallback plan packaging is informational; it cannot change hosted entitlement, quota, billing, or enforcement.");
|
|
1194
|
+
const localPlans = {
|
|
1195
|
+
plans: DEFAULT_PLANS,
|
|
1196
|
+
authority: localPlanPackagingAuthority(),
|
|
1197
|
+
...(surface.operator ? { overageMeters: OVERAGE_METERS, offeringClassifications: PUBLIC_OFFERING_CLASSIFICATIONS, policies: LAUNCH_POLICIES } : {})
|
|
1198
|
+
};
|
|
1199
|
+
const data = surface.details || surface.operator ? localPlans : publicPlansPayload(localPlans);
|
|
1200
|
+
return {
|
|
1201
|
+
message: formatPlansSummary(data),
|
|
1202
|
+
warnings,
|
|
1203
|
+
data,
|
|
1204
|
+
humanData: surface.details || surface.operator ? "show" : "hide"
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
async function commandDoctor(context, parsed) {
|
|
1208
|
+
const surface = outputSurface(parsed);
|
|
1209
|
+
const local = await context.store.inspect();
|
|
1210
|
+
const { profile } = await getOptionalProfile(context);
|
|
1211
|
+
const token = await resolveToken(context, false);
|
|
1212
|
+
const checks = [
|
|
1213
|
+
{ name: "node", ok: nodeMajor() >= 22 && nodeMajor() < 26, detail: process.version },
|
|
1214
|
+
{ name: "config", ok: true, detail: local.dir },
|
|
1215
|
+
{ name: "approval", ok: Boolean(token), detail: token ? "saved for this OS user or provided by automation" : "missing; run vc-tools start" },
|
|
1216
|
+
{
|
|
1217
|
+
name: "apiUrl",
|
|
1218
|
+
ok: profile.apiUrl.startsWith("https://") || (profile.apiUrl.startsWith("http://localhost") && allowInsecureLocalApi(context)),
|
|
1219
|
+
detail: profile.apiUrl
|
|
1220
|
+
},
|
|
1221
|
+
{ name: "agentComputer", ok: true, detail: "hosted Browser, Computer, Work, Proof, and Usage tools" }
|
|
1222
|
+
];
|
|
1223
|
+
try {
|
|
1224
|
+
const health = await createClient(context, profile, token).request("GET", "health", { auth: false, query: queryForSurface(surface) });
|
|
1225
|
+
checks.push({ name: "hostedApi", ok: health.ok !== false, detail: health.service ?? "reachable" });
|
|
1226
|
+
}
|
|
1227
|
+
catch (error) {
|
|
1228
|
+
checks.push({ name: "hostedApi", ok: false, detail: toCliError(error).message });
|
|
1229
|
+
}
|
|
1230
|
+
return {
|
|
1231
|
+
message: checks.every((check) => check.ok)
|
|
1232
|
+
? "Agent Computer checks passed. Your agent can use the hosted Vibecodr computer."
|
|
1233
|
+
: "Agent Computer needs attention. Run vc-tools start to approve access, then retry the agent.",
|
|
1234
|
+
data: {
|
|
1235
|
+
checks,
|
|
1236
|
+
...(surface.details || surface.operator ? { config: { dir: local.dir, credentialStore: local.credentialStore } } : {}),
|
|
1237
|
+
nextActions: checks.every((check) => check.ok)
|
|
1238
|
+
? ["Connect the agent with vc-tools agent connect.", "Use --json when an agent needs stable machine-readable output."]
|
|
1239
|
+
: ["Run vc-tools start.", "If this is CI or an isolated agent, use an advanced file/stdin credential source."]
|
|
1240
|
+
},
|
|
1241
|
+
humanData: surface.details || surface.operator ? "show" : "hide"
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function outputSurface(parsed) {
|
|
1245
|
+
return {
|
|
1246
|
+
details: getBooleanFlag(parsed.flags, "details"),
|
|
1247
|
+
operator: getBooleanFlag(parsed.flags, "operator")
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function queryForSurface(surface) {
|
|
1251
|
+
return {
|
|
1252
|
+
details: surface.details || undefined,
|
|
1253
|
+
operator: surface.operator || undefined
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function publicStartPayload(me, health, connection, usage, loginStarted) {
|
|
1257
|
+
return {
|
|
1258
|
+
ready: health.ok !== false,
|
|
1259
|
+
loginStarted,
|
|
1260
|
+
account: {
|
|
1261
|
+
label: formatAccountLabel(me),
|
|
1262
|
+
workspace: me.workspace?.name ?? me.workspace?.id,
|
|
1263
|
+
plan: me.plan?.name ?? "unknown"
|
|
1264
|
+
},
|
|
1265
|
+
connection: publicConnectionPayload(connection),
|
|
1266
|
+
health: publicHealthPayload(health),
|
|
1267
|
+
usage: publicUsagePayload(usage),
|
|
1268
|
+
nextActions: [
|
|
1269
|
+
"Connect your agent with vc-tools agent connect --client codex.",
|
|
1270
|
+
"Run vc-tools try to prove browser, computer, and proof are working."
|
|
1271
|
+
]
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function publicConnectionPayload(connection) {
|
|
1275
|
+
const tools = Array.isArray(connection.tools)
|
|
1276
|
+
? connection.tools
|
|
1277
|
+
.filter(isRecord)
|
|
1278
|
+
.map((tool) => typeof tool.name === "string" ? tool.name : undefined)
|
|
1279
|
+
.filter((name) => name !== undefined)
|
|
1280
|
+
: undefined;
|
|
1281
|
+
return {
|
|
1282
|
+
transport: typeof connection.transport === "string" ? connection.transport : "streamable_http",
|
|
1283
|
+
url: typeof connection.url === "string" ? connection.url : undefined,
|
|
1284
|
+
protocolVersion: typeof connection.protocolVersion === "string" ? connection.protocolVersion : undefined,
|
|
1285
|
+
tools
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function publicHealthPayload(health) {
|
|
1289
|
+
const record = isRecord(health) ? health : {};
|
|
1290
|
+
const live = isRecord(record.live) ? record.live : {};
|
|
1291
|
+
return {
|
|
1292
|
+
ok: health.ok !== false,
|
|
1293
|
+
service: typeof health.service === "string" ? health.service : undefined,
|
|
1294
|
+
version: typeof record.version === "string" ? record.version : undefined,
|
|
1295
|
+
requestId: typeof record.requestId === "string" ? record.requestId : undefined,
|
|
1296
|
+
network: publicNetworkPayload(live)
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
function publicNetworkPayload(live) {
|
|
1300
|
+
const network = isRecord(live.network) ? live.network : {};
|
|
1301
|
+
return {
|
|
1302
|
+
browserPublicHttps: typeof network.browserPublicHttps === "string" ? network.browserPublicHttps : "available",
|
|
1303
|
+
computerPublicHttps: typeof network.computerPublicHttps === "string" ? network.computerPublicHttps : "available",
|
|
1304
|
+
privateLocalNetworks: typeof network.privateLocalNetworks === "string" ? network.privateLocalNetworks : "blocked",
|
|
1305
|
+
metadataServices: typeof network.metadataServices === "string" ? network.metadataServices : "blocked",
|
|
1306
|
+
rawNetwork: typeof network.rawNetwork === "string" ? network.rawNetwork : "restricted"
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function publicUsagePayload(usage) {
|
|
1310
|
+
const data = isRecord(usage) ? usage : {};
|
|
1311
|
+
return {
|
|
1312
|
+
plan: typeof data.plan === "string" ? data.plan : "unknown",
|
|
1313
|
+
monthlyCredits: quotaValue(data.vcToolCredits),
|
|
1314
|
+
dailyCredits: quotaValue(data.dailyVcToolCredits),
|
|
1315
|
+
runningNow: quotaValue(data.concurrentRuns),
|
|
1316
|
+
browserWork: quotaValue(data.browserJobs),
|
|
1317
|
+
computerWork: quotaValue(data.sandboxJobs),
|
|
1318
|
+
proofStorage: quotaValue(data.artifactStorageGb, "GB")
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function publicPlansPayload(plans) {
|
|
1322
|
+
const data = isRecord(plans) ? plans : {};
|
|
1323
|
+
const rows = Array.isArray(data.plans) ? data.plans.filter(isRecord) : [];
|
|
1324
|
+
return {
|
|
1325
|
+
plans: rows.map(publicPlanPayload),
|
|
1326
|
+
note: "Plan packaging is public product information. Use vc-tools usage for your actual account capacity."
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function publicPlanPayload(plan) {
|
|
1330
|
+
const limits = isRecord(plan.limits) ? plan.limits : {};
|
|
1331
|
+
const browser = isRecord(plan.browser) ? plan.browser : isRecord(limits.browser) ? limits.browser : {};
|
|
1332
|
+
const sandbox = isRecord(plan.computer) ? plan.computer : isRecord(limits.sandbox) ? limits.sandbox : {};
|
|
1333
|
+
const monthlyCredits = numberValue(plan.monthlyCredits) ?? numberValue(limits.monthlyCredits);
|
|
1334
|
+
const dailyCredits = numberValue(plan.dailyCredits) ?? numberValue(limits.dailyCredits);
|
|
1335
|
+
const runningLimit = numberValue(plan.runningLimit) ?? numberValue(limits.maxConcurrentRuns);
|
|
1336
|
+
const browserMonthlyJobs = numberValue(browser.monthlyJobs) ?? numberValue(limits.browserRenderJobsMonthly);
|
|
1337
|
+
const browserMaxSeconds = numberValue(browser.maxSecondsPerRun) ?? numberValue(browser.maxBrowserSecondsPerRun);
|
|
1338
|
+
const agentBrowserMaxSeconds = numberValue(browser.agentBrowserMaxSeconds) ?? numberValue(browser.maxBrowserSessionSeconds);
|
|
1339
|
+
const computerMonthlyJobs = numberValue(sandbox.monthlyJobs) ?? numberValue(limits.sandboxJobsMonthly);
|
|
1340
|
+
const computerMaxSeconds = numberValue(sandbox.maxTaskSeconds) ?? numberValue(sandbox.maxSandboxTaskSeconds);
|
|
1341
|
+
return {
|
|
1342
|
+
name: typeof plan.name === "string" ? plan.name : "Unknown",
|
|
1343
|
+
priceUsdMonthly: numberValue(plan.priceUsdMonthly),
|
|
1344
|
+
monthlyCredits,
|
|
1345
|
+
dailyCredits,
|
|
1346
|
+
runningLimit,
|
|
1347
|
+
browser: {
|
|
1348
|
+
monthlyJobs: browserMonthlyJobs,
|
|
1349
|
+
maxSecondsPerRun: browserMaxSeconds,
|
|
1350
|
+
agentBrowserTasks: typeof browser.agentBrowserTasks === "string" ? browser.agentBrowserTasks : browser.allowBrowserSessions === true ? "included" : "not included",
|
|
1351
|
+
...(agentBrowserMaxSeconds !== undefined ? { agentBrowserMaxSeconds } : {})
|
|
1352
|
+
},
|
|
1353
|
+
computer: {
|
|
1354
|
+
monthlyJobs: computerMonthlyJobs,
|
|
1355
|
+
maxTaskSeconds: computerMaxSeconds,
|
|
1356
|
+
publicHttpEgress: typeof sandbox.publicHttpEgress === "string" ? sandbox.publicHttpEgress : computerMonthlyJobs !== undefined && computerMonthlyJobs > 0 ? "available" : "not included"
|
|
1357
|
+
},
|
|
1358
|
+
proofStorageGb: numberValue(plan.proofStorageGb) ?? numberValue(limits.artifactStorageGb)
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
function quotaValue(value, unit) {
|
|
1362
|
+
if (!isRecord(value)) {
|
|
1363
|
+
return undefined;
|
|
1364
|
+
}
|
|
1365
|
+
const used = numberValue(value.used);
|
|
1366
|
+
const included = numberValue(value.included);
|
|
1367
|
+
if (used === undefined && included === undefined) {
|
|
1368
|
+
return undefined;
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
...(used !== undefined ? { used } : {}),
|
|
1372
|
+
...(included !== undefined ? { included } : {}),
|
|
1373
|
+
...(unit !== undefined ? { unit } : {})
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function formatUsageSummary(usage) {
|
|
1377
|
+
const data = isRecord(usage) ? usage : {};
|
|
1378
|
+
const plan = typeof data.plan === "string" ? data.plan : "unknown";
|
|
1379
|
+
const lines = [
|
|
1380
|
+
"Agent Computer capacity",
|
|
1381
|
+
`Plan: ${plan}`,
|
|
1382
|
+
"",
|
|
1383
|
+
"Limit Used / Included Progress"
|
|
1384
|
+
];
|
|
1385
|
+
for (const row of usageRows(data)) {
|
|
1386
|
+
const used = row.used ?? 0;
|
|
1387
|
+
const included = row.included ?? 0;
|
|
1388
|
+
const percent = quotaPercent(used, included);
|
|
1389
|
+
const amount = `${formatUsageNumber(used)} / ${formatUsageNumber(included)}${row.unit ? ` ${row.unit}` : ""}`;
|
|
1390
|
+
lines.push(`${row.label.padEnd(29)} ${amount.padEnd(22)} ${usageBar(percent)} ${percent}%`);
|
|
1391
|
+
}
|
|
1392
|
+
const hardCap = typeof data.hardCap === "boolean" ? data.hardCap : undefined;
|
|
1393
|
+
if (hardCap !== undefined) {
|
|
1394
|
+
lines.push("", `Spend cap: ${hardCap ? "hard" : "soft"}`);
|
|
1395
|
+
}
|
|
1396
|
+
lines.push("Alias: vc-tools limits");
|
|
1397
|
+
return lines.join("\n");
|
|
1398
|
+
}
|
|
1399
|
+
function formatWhoamiSummary(me) {
|
|
1400
|
+
const user = formatAccountLabel(me);
|
|
1401
|
+
const workspace = me.workspace?.name ?? me.workspace?.id ?? "none returned";
|
|
1402
|
+
const plan = me.plan?.name ?? "unknown";
|
|
1403
|
+
return [
|
|
1404
|
+
"Vibecodr Agent Computer",
|
|
1405
|
+
`Account: ${user}`,
|
|
1406
|
+
`Workspace: ${workspace}`,
|
|
1407
|
+
`Plan: ${plan}`,
|
|
1408
|
+
"Agent access: ready"
|
|
1409
|
+
].join("\n");
|
|
1410
|
+
}
|
|
1411
|
+
function formatGrantsSummary(grants) {
|
|
1412
|
+
const data = isRecord(grants) ? grants : {};
|
|
1413
|
+
const rows = Array.isArray(data.grants) ? data.grants.filter(isRecord) : [];
|
|
1414
|
+
const granted = rows.filter((row) => row.granted === true).length;
|
|
1415
|
+
const providerMode = typeof data.providerMode === "string" ? ` (${data.providerMode})` : "";
|
|
1416
|
+
if (rows.length === 0) {
|
|
1417
|
+
return `vc-tools grants${providerMode}\nNo tool grants were returned. The full hosted response follows.`;
|
|
1418
|
+
}
|
|
1419
|
+
return [
|
|
1420
|
+
`vc-tools grants${providerMode}`,
|
|
1421
|
+
`${granted}/${rows.length} tool grants are enabled for the active account/plan.`,
|
|
1422
|
+
"The full hosted grant payload follows."
|
|
1423
|
+
].join("\n");
|
|
1424
|
+
}
|
|
1425
|
+
function formatPlansSummary(plans) {
|
|
1426
|
+
const data = isRecord(plans) ? plans : {};
|
|
1427
|
+
const rows = Array.isArray(data.plans) ? data.plans.filter(isRecord) : [];
|
|
1428
|
+
if (rows.length === 0) {
|
|
1429
|
+
return [
|
|
1430
|
+
"Vibecodr Agent Computer plans",
|
|
1431
|
+
"No plan packaging was returned.",
|
|
1432
|
+
"Run vc-tools usage for your actual account capacity."
|
|
1433
|
+
].join("\n");
|
|
1434
|
+
}
|
|
1435
|
+
const lines = ["Vibecodr Agent Computer plans", ""];
|
|
1436
|
+
for (const row of rows) {
|
|
1437
|
+
lines.push(...formatPlanBullets(row));
|
|
1438
|
+
lines.push("");
|
|
1439
|
+
}
|
|
1440
|
+
lines.push("Run vc-tools usage for your actual account capacity.");
|
|
1441
|
+
lines.push("Run vc-tools plans --details for the full entitlement schema.");
|
|
1442
|
+
return lines.join("\n").replace(/\n+$/, "");
|
|
1443
|
+
}
|
|
1444
|
+
function formatPlanBullets(plan) {
|
|
1445
|
+
const name = typeof plan.name === "string" ? plan.name : "Plan";
|
|
1446
|
+
const price = numberValue(plan.priceUsdMonthly);
|
|
1447
|
+
const header = price === undefined || price === 0
|
|
1448
|
+
? name === "Free" ? "Free" : `${name} - free`
|
|
1449
|
+
: `${name} - $${price}/mo`;
|
|
1450
|
+
const browser = isRecord(plan.browser) ? plan.browser : {};
|
|
1451
|
+
const computer = isRecord(plan.computer) ? plan.computer : {};
|
|
1452
|
+
const monthlyCredits = numberValue(plan.monthlyCredits);
|
|
1453
|
+
const dailyCredits = numberValue(plan.dailyCredits);
|
|
1454
|
+
const runningLimit = numberValue(plan.runningLimit);
|
|
1455
|
+
const browserMonthlyJobs = numberValue(browser.monthlyJobs);
|
|
1456
|
+
const browserMaxSeconds = numberValue(browser.maxSecondsPerRun);
|
|
1457
|
+
const agentBrowserTasks = typeof browser.agentBrowserTasks === "string" ? browser.agentBrowserTasks : undefined;
|
|
1458
|
+
const agentBrowserMaxSeconds = numberValue(browser.agentBrowserMaxSeconds);
|
|
1459
|
+
const computerMonthlyJobs = numberValue(computer.monthlyJobs);
|
|
1460
|
+
const computerMaxTaskSeconds = numberValue(computer.maxTaskSeconds);
|
|
1461
|
+
const computerPublicEgress = typeof computer.publicHttpEgress === "string" ? computer.publicHttpEgress : undefined;
|
|
1462
|
+
const proofStorageGb = numberValue(plan.proofStorageGb);
|
|
1463
|
+
const bullets = [];
|
|
1464
|
+
bullets.push(planBrowserBullet(browserMonthlyJobs, browserMaxSeconds));
|
|
1465
|
+
bullets.push(planComputerBullet(computerMonthlyJobs, computerMaxTaskSeconds, computerPublicEgress));
|
|
1466
|
+
if (monthlyCredits !== undefined) {
|
|
1467
|
+
bullets.push(`${formatPlanCount(monthlyCredits)} monthly credits${dailyCredits !== undefined ? ` (${formatPlanCount(dailyCredits)} per day)` : ""}`);
|
|
1468
|
+
}
|
|
1469
|
+
if (runningLimit !== undefined) {
|
|
1470
|
+
bullets.push(`${formatPlanCount(runningLimit)} concurrent runs`);
|
|
1471
|
+
}
|
|
1472
|
+
bullets.push(planProofStorageBullet(proofStorageGb));
|
|
1473
|
+
bullets.push(planAgentBrowserBullet(agentBrowserTasks, agentBrowserMaxSeconds));
|
|
1474
|
+
return [header, ...bullets.filter((bullet) => bullet.length > 0).map((bullet) => ` ${bullet}`)];
|
|
1475
|
+
}
|
|
1476
|
+
function planBrowserBullet(monthlyJobs, maxSeconds) {
|
|
1477
|
+
if (monthlyJobs === undefined || monthlyJobs <= 0) {
|
|
1478
|
+
return "Public browser checks: limited";
|
|
1479
|
+
}
|
|
1480
|
+
const secondsHint = maxSeconds !== undefined && maxSeconds > 0 ? ` up to ${maxSeconds}s each` : "";
|
|
1481
|
+
return `Public browser checks${secondsHint}`;
|
|
1482
|
+
}
|
|
1483
|
+
function planComputerBullet(monthlyJobs, maxTaskSeconds, publicHttpEgress) {
|
|
1484
|
+
if (monthlyJobs === undefined || monthlyJobs <= 0) {
|
|
1485
|
+
return "Hosted computer runs: not included";
|
|
1486
|
+
}
|
|
1487
|
+
const minutes = maxTaskSeconds !== undefined && maxTaskSeconds > 0 ? ` up to ${Math.round(maxTaskSeconds / 60)} min each` : "";
|
|
1488
|
+
const network = publicHttpEgress === "available" ? "; public HTTP(S) available" : "";
|
|
1489
|
+
return `Hosted computer runs${minutes}${network}`;
|
|
1490
|
+
}
|
|
1491
|
+
function planProofStorageBullet(proofStorageGb) {
|
|
1492
|
+
if (proofStorageGb === undefined || proofStorageGb <= 0) {
|
|
1493
|
+
return "Saved proof storage: not included";
|
|
1494
|
+
}
|
|
1495
|
+
return `${proofStorageGb} GB proof storage`;
|
|
1496
|
+
}
|
|
1497
|
+
function planAgentBrowserBullet(agentBrowserTasks, maxSeconds) {
|
|
1498
|
+
if (agentBrowserTasks === "included") {
|
|
1499
|
+
if (maxSeconds !== undefined && maxSeconds >= 3600) {
|
|
1500
|
+
return "Browser agent tasks up to 1 hour";
|
|
1501
|
+
}
|
|
1502
|
+
if (maxSeconds !== undefined && maxSeconds >= 60) {
|
|
1503
|
+
return `Browser agent tasks up to ${Math.round(maxSeconds / 60)} min`;
|
|
1504
|
+
}
|
|
1505
|
+
return "Browser agent tasks included";
|
|
1506
|
+
}
|
|
1507
|
+
return "";
|
|
1508
|
+
}
|
|
1509
|
+
function formatPlanCount(value) {
|
|
1510
|
+
if (value >= 1000) {
|
|
1511
|
+
return value.toLocaleString("en-US");
|
|
1512
|
+
}
|
|
1513
|
+
return String(value);
|
|
1514
|
+
}
|
|
1515
|
+
function formatStartSummary(me, health, connection, loggedInNow) {
|
|
1516
|
+
const connectionUrl = typeof connection.url === "string" ? connection.url : "hosted MCP URL returned in the payload";
|
|
1517
|
+
const plan = me.plan?.name ?? "unknown";
|
|
1518
|
+
const healthLabel = health.ok === false ? "needs attention" : "reachable";
|
|
1519
|
+
return [
|
|
1520
|
+
"Vibecodr Agent Computer is ready.",
|
|
1521
|
+
loggedInNow ? "Approval: completed in this run" : "Approval: already saved",
|
|
1522
|
+
`Account: ${formatAccountLabel(me)}`,
|
|
1523
|
+
`Plan: ${plan}`,
|
|
1524
|
+
`Hosted service: ${healthLabel}`,
|
|
1525
|
+
`Agent connection: ${connectionUrl}`,
|
|
1526
|
+
"Next: connect the agent to this URL and let it use browser.*, computer.*, work.*, proof.*, and usage.status."
|
|
1527
|
+
].join("\n");
|
|
1528
|
+
}
|
|
1529
|
+
function formatAgentConnectionSummary(clientName, connection, serverName = "vc-tools", installResult) {
|
|
1530
|
+
const url = typeof connection.url === "string" ? connection.url : "hosted MCP URL returned in the payload";
|
|
1531
|
+
const tools = Array.isArray(connection.tools) ? connection.tools.filter((tool) => typeof tool === "string") : [];
|
|
1532
|
+
const toolLine = tools.length > 0
|
|
1533
|
+
? `Tools: ${tools.slice(0, 8).join(", ")}${tools.length > 8 ? ", ..." : ""}`
|
|
1534
|
+
: "Tools: browser.*, computer.*, work.*, proof.*, and usage.status";
|
|
1535
|
+
const headline = clientName === "generic"
|
|
1536
|
+
? "Agent connection ready."
|
|
1537
|
+
: `${clientLabel(clientName)} connection ready.`;
|
|
1538
|
+
const lines = [headline, "", `MCP URL: ${url}`, toolLine];
|
|
1539
|
+
if (installResult) {
|
|
1540
|
+
lines.push("");
|
|
1541
|
+
if (installResult.changed) {
|
|
1542
|
+
lines.push(installResult.method === "cli"
|
|
1543
|
+
? `Installed via ${installResult.location}.`
|
|
1544
|
+
: `Wrote ${clientLabel(clientName)} MCP config: ${installResult.location}`);
|
|
1545
|
+
}
|
|
1546
|
+
else {
|
|
1547
|
+
lines.push(`${clientLabel(clientName)} MCP config already pointed at this Agent Computer (${installResult.location}).`);
|
|
1548
|
+
}
|
|
1549
|
+
if (installResult.backupPath) {
|
|
1550
|
+
lines.push(`Previous config backed up to: ${installResult.backupPath}`);
|
|
1551
|
+
}
|
|
1552
|
+
lines.push(installResult.nextStep);
|
|
1553
|
+
return lines.join("\n");
|
|
1554
|
+
}
|
|
1555
|
+
const snippet = clientConfigSnippet(clientName, url, serverName);
|
|
1556
|
+
if (snippet) {
|
|
1557
|
+
lines.push("", `Add this to ${snippet.label}:`, "", snippet.code);
|
|
1558
|
+
lines.push("", snippet.nextStep);
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1561
|
+
lines.push("", "Next: add this MCP URL to the agent client, then ask it to use the Vibecodr Agent Computer.");
|
|
1562
|
+
}
|
|
1563
|
+
return lines.join("\n");
|
|
1564
|
+
}
|
|
1565
|
+
function clientLabel(clientName) {
|
|
1566
|
+
switch (clientName.toLowerCase()) {
|
|
1567
|
+
case "codex": return "Codex";
|
|
1568
|
+
case "cursor": return "Cursor";
|
|
1569
|
+
case "vscode": return "VS Code";
|
|
1570
|
+
case "windsurf": return "Windsurf";
|
|
1571
|
+
case "claude":
|
|
1572
|
+
case "claude-desktop": return "Claude Desktop";
|
|
1573
|
+
case "claude-code": return "Claude Code";
|
|
1574
|
+
default: return clientName;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function clientConfigSnippet(clientName, url, serverName) {
|
|
1578
|
+
switch (clientName.toLowerCase()) {
|
|
1579
|
+
case "codex": {
|
|
1580
|
+
const code = [
|
|
1581
|
+
`[mcp_servers.${serverName}]`,
|
|
1582
|
+
`url = "${url}"`
|
|
1583
|
+
].join("\n");
|
|
1584
|
+
return {
|
|
1585
|
+
label: "your Codex MCP config (~/.codex/config.toml)",
|
|
1586
|
+
code,
|
|
1587
|
+
nextStep: "Then restart or open a new Codex session."
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
case "cursor": {
|
|
1591
|
+
const code = JSON.stringify({
|
|
1592
|
+
mcpServers: {
|
|
1593
|
+
[serverName]: { url }
|
|
1594
|
+
}
|
|
1595
|
+
}, null, 2);
|
|
1596
|
+
return {
|
|
1597
|
+
label: "your Cursor MCP config (~/.cursor/mcp.json)",
|
|
1598
|
+
code,
|
|
1599
|
+
nextStep: "Then open Cursor and trigger the Vibecodr Agent Computer."
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
case "vscode": {
|
|
1603
|
+
const code = JSON.stringify({
|
|
1604
|
+
servers: {
|
|
1605
|
+
[serverName]: { type: "http", url }
|
|
1606
|
+
}
|
|
1607
|
+
}, null, 2);
|
|
1608
|
+
return {
|
|
1609
|
+
label: "your VS Code MCP config (workspace .vscode/mcp.json or user settings)",
|
|
1610
|
+
code,
|
|
1611
|
+
nextStep: "Then reload the VS Code MCP servers and connect."
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
case "windsurf": {
|
|
1615
|
+
const code = JSON.stringify({
|
|
1616
|
+
mcpServers: {
|
|
1617
|
+
[serverName]: { serverUrl: url }
|
|
1618
|
+
}
|
|
1619
|
+
}, null, 2);
|
|
1620
|
+
return {
|
|
1621
|
+
label: "your Windsurf MCP config (~/.codeium/windsurf/mcp_config.json)",
|
|
1622
|
+
code,
|
|
1623
|
+
nextStep: "Then restart Windsurf and connect."
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
case "claude":
|
|
1627
|
+
case "claude-desktop": {
|
|
1628
|
+
const code = JSON.stringify({
|
|
1629
|
+
mcpServers: {
|
|
1630
|
+
[serverName]: {
|
|
1631
|
+
command: "npx",
|
|
1632
|
+
args: ["mcp-remote", url]
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}, null, 2);
|
|
1636
|
+
return {
|
|
1637
|
+
label: "your Claude Desktop config (claude_desktop_config.json). Claude Desktop does not load remote HTTP MCP servers directly; this uses the mcp-remote stdio proxy via npx",
|
|
1638
|
+
code,
|
|
1639
|
+
nextStep: "Restart Claude Desktop (Node.js / npx must be installed). Alternatively, add the MCP URL via Settings -> Connectors -> Add custom connector."
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
case "claude-code": {
|
|
1643
|
+
return {
|
|
1644
|
+
label: "Claude Code",
|
|
1645
|
+
code: `claude mcp add ${serverName} --url ${url}`,
|
|
1646
|
+
nextStep: "Then start a new Claude Code session in this workspace."
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
default:
|
|
1650
|
+
return undefined;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function formatMaybeAccountLabel(me) {
|
|
1654
|
+
return me === undefined ? "the verified Vibecodr account" : formatAccountLabel(me);
|
|
1655
|
+
}
|
|
1656
|
+
function formatAccountLabel(me) {
|
|
1657
|
+
if (me.user.email && !me.user.email.endsWith("@vibecodr.local")) {
|
|
1658
|
+
return me.user.email;
|
|
1659
|
+
}
|
|
1660
|
+
return me.workspace?.name ?? me.workspace?.id ?? me.user.id;
|
|
1661
|
+
}
|
|
1662
|
+
function localPlanPackagingAuthority() {
|
|
1663
|
+
return {
|
|
1664
|
+
source: "local-package-fallback",
|
|
1665
|
+
accountEntitlementsAuthoritative: false,
|
|
1666
|
+
localFallbackAuthoritative: false,
|
|
1667
|
+
accountStateEndpoint: "/v1/usage",
|
|
1668
|
+
enforcement: "server-side",
|
|
1669
|
+
message: "Local plan packaging is informational only. Hosted usage and hosted quota checks decide real account entitlement, usage, billing, and enforcement."
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
function formatPlanPackagingAuthoritySummary(value) {
|
|
1673
|
+
if (!isRecord(value)) {
|
|
1674
|
+
return "informational packaging; hosted usage and quota checks decide account entitlement";
|
|
1675
|
+
}
|
|
1676
|
+
const source = typeof value.source === "string" ? value.source : "unknown";
|
|
1677
|
+
const accountAuthoritative = value.accountEntitlementsAuthoritative === true;
|
|
1678
|
+
const localAuthoritative = value.localFallbackAuthoritative === true;
|
|
1679
|
+
if (accountAuthoritative || localAuthoritative) {
|
|
1680
|
+
return `${source} (unexpectedly authoritative; verify hosted account state before relying on this)`;
|
|
1681
|
+
}
|
|
1682
|
+
return `${source} (informational only; not billing or enforcement authority)`;
|
|
1683
|
+
}
|
|
1684
|
+
function usageRows(data) {
|
|
1685
|
+
return [
|
|
1686
|
+
usageRow(data, "Monthly credits", "monthlyCredits") ?? usageRow(data, "Monthly credits", "vcToolCredits"),
|
|
1687
|
+
usageRow(data, "Daily credits", "dailyCredits") ?? usageRow(data, "Daily credits", "dailyVcToolCredits"),
|
|
1688
|
+
usageRow(data, "Browser work", "browserWork") ?? usageRow(data, "Browser work", "browserJobs"),
|
|
1689
|
+
usageRow(data, "Computer work", "computerWork") ?? usageRow(data, "Computer work", "sandboxJobs"),
|
|
1690
|
+
usageRow(data, "Browser seconds", "browserSeconds", "s"),
|
|
1691
|
+
usageRow(data, "Daily browser seconds", "dailyBrowserSeconds", "s"),
|
|
1692
|
+
usageRow(data, "Sandbox minutes", "sandboxMinutes", "min"),
|
|
1693
|
+
usageRow(data, "Proof storage", "proofStorage", "GB") ?? usageRow(data, "Proof storage", "artifactStorageGb", "GB"),
|
|
1694
|
+
usageRow(data, "Running now", "runningNow") ?? usageRow(data, "Running now", "concurrentRuns"),
|
|
1695
|
+
usageRow(data, "Active browser sessions", "browserSessionConcurrency"),
|
|
1696
|
+
usageRow(data, "Active sandbox tasks", "sandboxConcurrency")
|
|
1697
|
+
].filter((row) => row !== undefined);
|
|
1698
|
+
}
|
|
1699
|
+
function usageRow(data, label, key, unit) {
|
|
1700
|
+
const value = data[key];
|
|
1701
|
+
if (!isRecord(value)) {
|
|
1702
|
+
return undefined;
|
|
1703
|
+
}
|
|
1704
|
+
const used = numberValue(value.used);
|
|
1705
|
+
const included = numberValue(value.included);
|
|
1706
|
+
if (used === undefined && included === undefined) {
|
|
1707
|
+
return undefined;
|
|
1708
|
+
}
|
|
1709
|
+
const row = { label };
|
|
1710
|
+
if (used !== undefined)
|
|
1711
|
+
row.used = used;
|
|
1712
|
+
if (included !== undefined)
|
|
1713
|
+
row.included = included;
|
|
1714
|
+
if (unit !== undefined)
|
|
1715
|
+
row.unit = unit;
|
|
1716
|
+
return row;
|
|
1717
|
+
}
|
|
1718
|
+
function numberValue(value) {
|
|
1719
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1720
|
+
}
|
|
1721
|
+
function formatUsageNumber(value) {
|
|
1722
|
+
if (Number.isInteger(value)) {
|
|
1723
|
+
return String(value);
|
|
1724
|
+
}
|
|
1725
|
+
return value.toFixed(2).replace(/\.?0+$/, "");
|
|
1726
|
+
}
|
|
1727
|
+
function formatJobStatusMessage(job, fallbackId) {
|
|
1728
|
+
if (!isRecord(job)) {
|
|
1729
|
+
return `Fetched work ${fallbackId}.`;
|
|
1730
|
+
}
|
|
1731
|
+
const id = typeof job.id === "string" ? job.id : fallbackId;
|
|
1732
|
+
const status = typeof job.status === "string" ? job.status : "unknown";
|
|
1733
|
+
const queue = isRecord(job.queue) ? job.queue : undefined;
|
|
1734
|
+
const delay = typeof queue?.fairDelaySeconds === "number" ? queue.fairDelaySeconds : 0;
|
|
1735
|
+
if (status === "queued" && delay > 0) {
|
|
1736
|
+
return `Work ${id} is queued with a ${delay}s fairness delay so one account cannot monopolize the hosted computer.`;
|
|
1737
|
+
}
|
|
1738
|
+
return `Work ${id} is ${status}.`;
|
|
1739
|
+
}
|
|
1740
|
+
function quotaPercent(used, included) {
|
|
1741
|
+
if (included <= 0) {
|
|
1742
|
+
return used > 0 ? 100 : 0;
|
|
1743
|
+
}
|
|
1744
|
+
return Math.max(0, Math.min(100, Math.round((used / included) * 100)));
|
|
1745
|
+
}
|
|
1746
|
+
function usageBar(percent) {
|
|
1747
|
+
const width = 10;
|
|
1748
|
+
const filled = Math.max(0, Math.min(width, Math.round((percent / 100) * width)));
|
|
1749
|
+
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
|
|
1750
|
+
}
|
|
1751
|
+
function buildToolTestPayload(capability, target, parsed, globalToolTimeoutMs) {
|
|
1752
|
+
if (capability.startsWith("browser.")) {
|
|
1753
|
+
if (!target) {
|
|
1754
|
+
throw new CliError("input.url_required", `${capability} requires an HTTPS URL target.`, 2);
|
|
1755
|
+
}
|
|
1756
|
+
const input = {
|
|
1757
|
+
url: validateBrowserUrl(target)
|
|
1758
|
+
};
|
|
1759
|
+
const format = getStringFlag(parsed.flags, "format");
|
|
1760
|
+
const timeoutInput = getStringFlag(parsed.flags, "timeoutMs") ?? (globalToolTimeoutMs === undefined ? undefined : String(globalToolTimeoutMs));
|
|
1761
|
+
const timeoutMs = validatePositiveInt(timeoutInput, "--timeout-ms", 1000, capability === "browser.agent_task" ? 3600000 : 180000);
|
|
1762
|
+
if (capability === "browser.agent_task") {
|
|
1763
|
+
const instructions = getStringFlag(parsed.flags, "instructions");
|
|
1764
|
+
const idleTimeoutMs = validatePositiveInt(getStringFlag(parsed.flags, "idleTimeoutMs"), "--idle-timeout-ms", 1000, 600000);
|
|
1765
|
+
if (instructions !== undefined)
|
|
1766
|
+
input.instructions = instructions.slice(0, 4000);
|
|
1767
|
+
if (idleTimeoutMs !== undefined)
|
|
1768
|
+
input.idleTimeoutMs = idleTimeoutMs;
|
|
1769
|
+
}
|
|
1770
|
+
else if (capability === "browser.crawl_site") {
|
|
1771
|
+
const maxPages = validatePositiveInt(getStringFlag(parsed.flags, "maxPages"), "--max-pages", 1, 250);
|
|
1772
|
+
const maxDepth = validatePositiveInt(getStringFlag(parsed.flags, "maxDepth"), "--max-depth", 0, 4);
|
|
1773
|
+
const render = parsed.flags.render;
|
|
1774
|
+
if (maxPages !== undefined)
|
|
1775
|
+
input.maxPages = maxPages;
|
|
1776
|
+
if (maxDepth !== undefined)
|
|
1777
|
+
input.maxDepth = maxDepth;
|
|
1778
|
+
if (typeof render === "boolean")
|
|
1779
|
+
input.render = render;
|
|
1780
|
+
if (format !== undefined)
|
|
1781
|
+
input.format = validateCrawlFormat(format);
|
|
1782
|
+
}
|
|
1783
|
+
else if (format !== undefined) {
|
|
1784
|
+
input.format = format;
|
|
1785
|
+
}
|
|
1786
|
+
if (timeoutMs !== undefined)
|
|
1787
|
+
input.timeoutMs = timeoutMs;
|
|
1788
|
+
return {
|
|
1789
|
+
capability,
|
|
1790
|
+
input
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
if (capability.startsWith("sandbox.")) {
|
|
1794
|
+
const command = getStringFlag(parsed.flags, "command") ?? parsed.positionals[0];
|
|
1795
|
+
if (!command) {
|
|
1796
|
+
throw new CliError("input.command_required", `${capability} requires --command <command>.`, 2);
|
|
1797
|
+
}
|
|
1798
|
+
const input = {
|
|
1799
|
+
command: validateSandboxCommand(command),
|
|
1800
|
+
network: normalizeComputerNetworkFlag(parsed)
|
|
1801
|
+
};
|
|
1802
|
+
const timeoutInput = getStringFlag(parsed.flags, "timeoutMs") ?? (globalToolTimeoutMs === undefined ? undefined : String(globalToolTimeoutMs));
|
|
1803
|
+
const timeoutMs = validatePositiveInt(timeoutInput, "--timeout-ms", 1000, 1800000);
|
|
1804
|
+
if (timeoutMs !== undefined)
|
|
1805
|
+
input.timeoutMs = timeoutMs;
|
|
1806
|
+
return {
|
|
1807
|
+
capability,
|
|
1808
|
+
input
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
if (capability === "artifact.get") {
|
|
1812
|
+
return {
|
|
1813
|
+
capability,
|
|
1814
|
+
input: {
|
|
1815
|
+
artifactId: validateEntityId(target ?? "", "artifact id")
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
if (capability === "artifact.create") {
|
|
1820
|
+
throw new CliError("input.use_artifacts_create", "Use vc-tools artifacts create --file <path> --yes to create artifacts.", 2);
|
|
1821
|
+
}
|
|
1822
|
+
if (capability === "job.status" || capability === "job.cancel") {
|
|
1823
|
+
return {
|
|
1824
|
+
capability,
|
|
1825
|
+
input: {
|
|
1826
|
+
jobId: validateEntityId(target ?? "", "job id"),
|
|
1827
|
+
confirmed: capability === "job.cancel" ? getBooleanFlag(parsed.flags, "yes") : undefined
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
return { capability, input: {} };
|
|
1832
|
+
}
|
|
1833
|
+
function buildScheduledQaPayload(target, parsed, globalToolTimeoutMs) {
|
|
1834
|
+
const capability = normalizeScheduledQaCliCapability(getStringFlag(parsed.flags, "capability") ?? getStringFlag(parsed.flags, "tool") ?? "browser.render_url");
|
|
1835
|
+
const timeoutInput = getStringFlag(parsed.flags, "timeoutMs") ?? (globalToolTimeoutMs === undefined ? undefined : String(globalToolTimeoutMs));
|
|
1836
|
+
const timeoutMs = validatePositiveInt(timeoutInput, "--timeout-ms", 1000, 180000);
|
|
1837
|
+
const intervalMinutes = validatePositiveInt(getStringFlag(parsed.flags, "intervalMinutes"), "--interval-minutes", 1, 30 * 24 * 60);
|
|
1838
|
+
const input = {
|
|
1839
|
+
url: validateBrowserUrl(target)
|
|
1840
|
+
};
|
|
1841
|
+
const format = getStringFlag(parsed.flags, "format");
|
|
1842
|
+
if (format !== undefined && capability !== "browser.extract_markdown") {
|
|
1843
|
+
input.format = format;
|
|
1844
|
+
}
|
|
1845
|
+
if (timeoutMs !== undefined)
|
|
1846
|
+
input.timeoutMs = timeoutMs;
|
|
1847
|
+
const body = {
|
|
1848
|
+
capability,
|
|
1849
|
+
input
|
|
1850
|
+
};
|
|
1851
|
+
if (intervalMinutes !== undefined)
|
|
1852
|
+
body.intervalMinutes = intervalMinutes;
|
|
1853
|
+
const label = getStringFlag(parsed.flags, "label");
|
|
1854
|
+
if (label !== undefined)
|
|
1855
|
+
body.label = label;
|
|
1856
|
+
if (getBooleanFlag(parsed.flags, "runNow"))
|
|
1857
|
+
body.runNow = true;
|
|
1858
|
+
return body;
|
|
1859
|
+
}
|
|
1860
|
+
function normalizeScheduledQaCliCapability(input) {
|
|
1861
|
+
const capability = normalizeCapabilityName(input);
|
|
1862
|
+
if (!["browser.render_url", "browser.screenshot_url", "browser.extract_markdown", "browser.render_pdf"].includes(capability)) {
|
|
1863
|
+
throw new CliError("input.unsupported_scheduled_qa_capability", "Scheduled QA supports browser.render, browser.screenshot, browser.markdown, and browser.pdf.", 2);
|
|
1864
|
+
}
|
|
1865
|
+
return capability;
|
|
1866
|
+
}
|
|
1867
|
+
function validateCrawlFormat(format) {
|
|
1868
|
+
if (format !== "markdown" && format !== "html") {
|
|
1869
|
+
throw new CliError("input.invalid_format", "--format for browser.crawl must be markdown or html.", 2);
|
|
1870
|
+
}
|
|
1871
|
+
return format;
|
|
1872
|
+
}
|
|
1873
|
+
function normalizeComputerNetworkFlag(parsed) {
|
|
1874
|
+
const value = parsed.flags.network;
|
|
1875
|
+
if (value === undefined || value === true) {
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
if (value === false) {
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
if (value === "public") {
|
|
1882
|
+
return true;
|
|
1883
|
+
}
|
|
1884
|
+
if (value === "off" || value === "none" || value === "false") {
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
throw new CliError("input.invalid_network", "--network must be public or off. Public HTTP(S) is available by default; private, local, metadata, and internal destinations remain blocked by hosted policy.", 2);
|
|
1888
|
+
}
|
|
1889
|
+
async function inspectAuthState(context) {
|
|
1890
|
+
const warnings = [];
|
|
1891
|
+
const local = await context.store.inspect();
|
|
1892
|
+
const explicitCredentials = credentialDescriptors(context, undefined, false).map((descriptor) => credentialSummary(descriptor, context.cwd));
|
|
1893
|
+
const ambiguous = explicitCredentials.length > 1;
|
|
1894
|
+
const configDirSource = context.globals.configDir
|
|
1895
|
+
? "--config-dir"
|
|
1896
|
+
: context.env.VC_TOOLS_CONFIG_DIR
|
|
1897
|
+
? "VC_TOOLS_CONFIG_DIR"
|
|
1898
|
+
: "default";
|
|
1899
|
+
const config = {
|
|
1900
|
+
...local,
|
|
1901
|
+
dirSource: configDirSource
|
|
1902
|
+
};
|
|
1903
|
+
if (configDirSource !== "default") {
|
|
1904
|
+
const envWithoutOverride = { ...context.env };
|
|
1905
|
+
delete envWithoutOverride.VC_TOOLS_CONFIG_DIR;
|
|
1906
|
+
const defaultStore = new ConfigStore(resolveConfigDir(envWithoutOverride), envWithoutOverride);
|
|
1907
|
+
const defaultLocal = await defaultStore.inspect();
|
|
1908
|
+
config.defaultDir = defaultLocal.dir;
|
|
1909
|
+
config.defaultConfigExists = defaultLocal.configExists;
|
|
1910
|
+
config.defaultCredentialsExist = defaultLocal.credentialsExist;
|
|
1911
|
+
warnings.push(`${configDirSource} is set, so this session is isolated from the normal vc-tools config directory.`);
|
|
1912
|
+
}
|
|
1913
|
+
let storedAuth = { version: 2 };
|
|
1914
|
+
let storedStatus = "missing";
|
|
1915
|
+
let storedErrorCode;
|
|
1916
|
+
let storedAuthCleared = false;
|
|
1917
|
+
try {
|
|
1918
|
+
storedAuth = await context.store.readAuthState();
|
|
1919
|
+
storedStatus = storedAuth.credential || storedAuth.grant ? "present" : "missing";
|
|
1920
|
+
}
|
|
1921
|
+
catch (error) {
|
|
1922
|
+
const cliError = toCliError(error);
|
|
1923
|
+
if (isRecoverableStoredAuthError(cliError)) {
|
|
1924
|
+
await clearRecoverableStoredAuthState(context, warnings);
|
|
1925
|
+
storedStatus = "missing";
|
|
1926
|
+
storedAuthCleared = true;
|
|
1927
|
+
}
|
|
1928
|
+
else {
|
|
1929
|
+
storedStatus = cliError.code === "storage.native_credentials_unavailable" ? "unavailable" : "error";
|
|
1930
|
+
storedErrorCode = cliError.code;
|
|
1931
|
+
warnings.push(cliError.message);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
let token;
|
|
1935
|
+
let winning;
|
|
1936
|
+
if (ambiguous) {
|
|
1937
|
+
warnings.push(`Multiple explicit credential sources are set: ${explicitCredentials.map((item) => item.label).join(", ")}.`);
|
|
1938
|
+
}
|
|
1939
|
+
else if (explicitCredentials[0]) {
|
|
1940
|
+
winning = { ...explicitCredentials[0], kind: "explicit" };
|
|
1941
|
+
try {
|
|
1942
|
+
token = await resolveToken(context, false);
|
|
1943
|
+
}
|
|
1944
|
+
catch (error) {
|
|
1945
|
+
warnings.push(toCliError(error).message);
|
|
1946
|
+
}
|
|
1947
|
+
if (storedStatus === "present") {
|
|
1948
|
+
warnings.push(`A stored approval exists, but ${explicitCredentials[0].label} is taking precedence.`);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
else if (storedStatus === "present") {
|
|
1952
|
+
token = await resolveToken(context, false).catch((error) => {
|
|
1953
|
+
warnings.push(toCliError(error).message);
|
|
1954
|
+
return undefined;
|
|
1955
|
+
});
|
|
1956
|
+
winning = {
|
|
1957
|
+
mode: storedAuth.credential?.mode ?? "token",
|
|
1958
|
+
source: local.credentialStore === "native" ? "native" : "stored",
|
|
1959
|
+
label: storedAuth.credential
|
|
1960
|
+
? `stored ${formatCredentialMode(storedAuth.credential.mode)}`
|
|
1961
|
+
: "cached vc-tools grant",
|
|
1962
|
+
kind: "stored"
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
return {
|
|
1966
|
+
token,
|
|
1967
|
+
warnings,
|
|
1968
|
+
config: {
|
|
1969
|
+
...config,
|
|
1970
|
+
credentialsExist: (storedAuthCleared ? false : config.credentialsExist) || storedStatus === "present" || explicitCredentials.length > 0
|
|
1971
|
+
},
|
|
1972
|
+
credential: {
|
|
1973
|
+
envOverrides: explicitCredentials,
|
|
1974
|
+
stored: {
|
|
1975
|
+
status: storedStatus,
|
|
1976
|
+
credentialStore: local.credentialStore,
|
|
1977
|
+
credentialMode: storedAuth.credential?.mode,
|
|
1978
|
+
grantStatus: storedAuth.grant ? (isGrantFresh(storedAuth.grant) ? "fresh" : "expired") : "missing",
|
|
1979
|
+
refreshable: Boolean(storedAuth.credential && storedAuth.credential.mode !== "token"),
|
|
1980
|
+
errorCode: storedErrorCode
|
|
1981
|
+
},
|
|
1982
|
+
winning,
|
|
1983
|
+
ambiguous
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function credentialSummary(descriptor, cwd) {
|
|
1988
|
+
const summary = {
|
|
1989
|
+
mode: descriptor.mode,
|
|
1990
|
+
source: descriptor.source,
|
|
1991
|
+
label: descriptor.label
|
|
1992
|
+
};
|
|
1993
|
+
if (descriptor.file) {
|
|
1994
|
+
summary.file = path.resolve(cwd, descriptor.file);
|
|
1995
|
+
}
|
|
1996
|
+
return summary;
|
|
1997
|
+
}
|
|
1998
|
+
function safeOsUser() {
|
|
1999
|
+
try {
|
|
2000
|
+
return os.userInfo().username;
|
|
2001
|
+
}
|
|
2002
|
+
catch {
|
|
2003
|
+
return "unknown";
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
async function getOptionalProfile(context) {
|
|
2007
|
+
try {
|
|
2008
|
+
return await context.store.getProfile(context.globals.profile);
|
|
2009
|
+
}
|
|
2010
|
+
catch {
|
|
2011
|
+
return { name: context.globals.profile, profile: { apiUrl: context.globals.apiUrl ?? context.env.VC_TOOLS_API_URL ?? DEFAULT_API_URL } };
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
async function resolveToken(context, required, options = {}) {
|
|
2015
|
+
const envCredential = await resolveLoginCredential(context, undefined, false);
|
|
2016
|
+
if (envCredential?.mode === "token") {
|
|
2017
|
+
validateTokenShape(envCredential.value);
|
|
2018
|
+
return envCredential.value;
|
|
2019
|
+
}
|
|
2020
|
+
if (envCredential?.mode === "oauth" || envCredential?.mode === "api_key") {
|
|
2021
|
+
validateCredentialShape(envCredential.value, envCredential.mode === "oauth" ? "OAuth token" : "API key");
|
|
2022
|
+
const exchange = await exchangeCredentialForGrant(context, undefined, envCredential);
|
|
2023
|
+
validateTokenShape(exchange.access_token);
|
|
2024
|
+
return exchange.access_token;
|
|
2025
|
+
}
|
|
2026
|
+
try {
|
|
2027
|
+
const state = await context.store.readAuthState();
|
|
2028
|
+
const token = await resolveStoredToken(context, state, options.forceRefresh === true);
|
|
2029
|
+
if (token) {
|
|
2030
|
+
return token;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
catch (error) {
|
|
2034
|
+
const cliError = toCliError(error);
|
|
2035
|
+
if (isRecoverableStoredAuthError(cliError)) {
|
|
2036
|
+
await clearRecoverableStoredAuthState(context);
|
|
2037
|
+
}
|
|
2038
|
+
else if (required || cliError.code !== "storage.native_credentials_unavailable") {
|
|
2039
|
+
throw cliError;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
if (required) {
|
|
2043
|
+
throw new CliError("auth.missing", "Run vc-tools start, pass a credential with --credential-file or --credential-stdin, or set VC_TOOLS_CREDENTIAL_FILE for an isolated agent.", 3);
|
|
2044
|
+
}
|
|
2045
|
+
return undefined;
|
|
2046
|
+
}
|
|
2047
|
+
async function resolveStoredToken(context, state, forceRefresh) {
|
|
2048
|
+
if (!forceRefresh && state.grant && isGrantFresh(state.grant)) {
|
|
2049
|
+
return state.grant.token;
|
|
2050
|
+
}
|
|
2051
|
+
const credential = state.credential;
|
|
2052
|
+
if (credential?.mode === "api_key" || credential?.mode === "oauth") {
|
|
2053
|
+
validateCredentialShape(credential.value, credential.mode === "oauth" ? "OAuth token" : "API key");
|
|
2054
|
+
const exchange = await exchangeCredentialForGrant(context, undefined, {
|
|
2055
|
+
mode: credential.mode,
|
|
2056
|
+
value: credential.value,
|
|
2057
|
+
source: storedCredentialSourceToCredentialSource(credential.source)
|
|
2058
|
+
});
|
|
2059
|
+
validateTokenShape(exchange.access_token);
|
|
2060
|
+
await context.store.saveGrant({
|
|
2061
|
+
token: exchange.access_token,
|
|
2062
|
+
expiresAt: exchange.expires_at,
|
|
2063
|
+
savedAt: new Date().toISOString(),
|
|
2064
|
+
source: "exchange"
|
|
2065
|
+
});
|
|
2066
|
+
return exchange.access_token;
|
|
2067
|
+
}
|
|
2068
|
+
if (credential?.mode === "token") {
|
|
2069
|
+
validateTokenShape(credential.value);
|
|
2070
|
+
return credential.value;
|
|
2071
|
+
}
|
|
2072
|
+
return !forceRefresh && state.grant?.token ? state.grant.token : undefined;
|
|
2073
|
+
}
|
|
2074
|
+
function isRecoverableStoredAuthError(error) {
|
|
2075
|
+
return error.code === "config.credentials_invalid_shape";
|
|
2076
|
+
}
|
|
2077
|
+
async function clearRecoverableStoredAuthState(context, warnings) {
|
|
2078
|
+
try {
|
|
2079
|
+
await context.store.clearToken(context.globals.profile);
|
|
2080
|
+
warnings?.push("Removed an unreadable stored vc-tools approval. Run vc-tools start to connect this Agent Computer.");
|
|
2081
|
+
}
|
|
2082
|
+
catch (error) {
|
|
2083
|
+
const cliError = toCliError(error);
|
|
2084
|
+
warnings?.push(`Could not remove an unreadable stored vc-tools approval: ${cliError.message}`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
function isGrantFresh(grant, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
2088
|
+
return typeof grant.expiresAt !== "number" || grant.expiresAt - nowSeconds > GRANT_REFRESH_SKEW_SECONDS;
|
|
2089
|
+
}
|
|
2090
|
+
function formatCredentialMode(mode) {
|
|
2091
|
+
if (mode === "api_key") {
|
|
2092
|
+
return "API key";
|
|
2093
|
+
}
|
|
2094
|
+
if (mode === "oauth") {
|
|
2095
|
+
return "OAuth token";
|
|
2096
|
+
}
|
|
2097
|
+
return "vc-tools grant";
|
|
2098
|
+
}
|
|
2099
|
+
function storedCredentialSourceToCredentialSource(source) {
|
|
2100
|
+
return source === "env" || source === "stdin" || source === "file" || source === "flag" ? source : "file";
|
|
2101
|
+
}
|
|
2102
|
+
function createClient(context, profile, token) {
|
|
2103
|
+
return createApiClient({
|
|
2104
|
+
baseUrl: versionedApiUrl(context.globals.apiUrl ?? context.env.VC_TOOLS_API_URL ?? profile.apiUrl, allowInsecureLocalApi(context)),
|
|
2105
|
+
token,
|
|
2106
|
+
timeoutMs: context.globals.timeoutMs,
|
|
2107
|
+
allowInsecureLocalApi: allowInsecureLocalApi(context),
|
|
2108
|
+
fetchImpl: context.fetchImpl
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
function allowInsecureLocalApi(context) {
|
|
2112
|
+
return context.globals.allowInsecureLocalApi || context.env.VC_TOOLS_ALLOW_INSECURE_LOCAL_API === "true";
|
|
2113
|
+
}
|
|
2114
|
+
async function exchangeCredentialForGrant(context, parsed, credential) {
|
|
2115
|
+
const authApiUrl = getStringFlag(parsed?.flags ?? {}, "authApiUrl") ?? context.env.VC_TOOLS_AUTH_API_URL ?? DEFAULT_AUTH_API_URL;
|
|
2116
|
+
const body = credential.mode === "oauth"
|
|
2117
|
+
? { access_token: credential.value, grant_profile: "vc_tools" }
|
|
2118
|
+
: { api_key: credential.value, grant_profile: "vc_tools" };
|
|
2119
|
+
const authClient = createBaseClient({
|
|
2120
|
+
baseUrl: authApiUrl,
|
|
2121
|
+
timeoutMs: context.globals.timeoutMs,
|
|
2122
|
+
allowInsecureLocalApi: allowInsecureLocalApi(context),
|
|
2123
|
+
fetchImpl: context.fetchImpl,
|
|
2124
|
+
serviceName: "Vibecodr Auth API",
|
|
2125
|
+
redactResponses: false
|
|
2126
|
+
});
|
|
2127
|
+
const parsedResponse = await authClient.request("POST", "auth/cli/exchange", { body });
|
|
2128
|
+
if (!isRecord(parsedResponse)) {
|
|
2129
|
+
throw new CliError("auth.invalid_exchange_response", "Vibecodr Auth API returned an invalid CLI grant response.", 6);
|
|
2130
|
+
}
|
|
2131
|
+
const accessToken = parsedResponse.access_token;
|
|
2132
|
+
const expiresAt = parsedResponse.expires_at;
|
|
2133
|
+
const userId = parsedResponse.user_id;
|
|
2134
|
+
if (parsedResponse.token_type !== "Bearer" || typeof accessToken !== "string" || typeof expiresAt !== "number" || typeof userId !== "string") {
|
|
2135
|
+
throw new CliError("auth.invalid_exchange_response", "Vibecodr Auth API returned an invalid CLI grant response.", 6);
|
|
2136
|
+
}
|
|
2137
|
+
return {
|
|
2138
|
+
token_type: "Bearer",
|
|
2139
|
+
access_token: accessToken,
|
|
2140
|
+
expires_at: expiresAt,
|
|
2141
|
+
user_id: userId,
|
|
2142
|
+
user_handle: typeof parsedResponse.user_handle === "string" ? parsedResponse.user_handle : undefined,
|
|
2143
|
+
credential_type: typeof parsedResponse.credential_type === "string" ? parsedResponse.credential_type : undefined,
|
|
2144
|
+
grant_profile: typeof parsedResponse.grant_profile === "string" ? parsedResponse.grant_profile : undefined,
|
|
2145
|
+
scopes: Array.isArray(parsedResponse.scopes) ? parsedResponse.scopes.filter((scope) => typeof scope === "string") : undefined,
|
|
2146
|
+
durable_credential: normalizeDurableCredentialResponse(parsedResponse.durable_credential)
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
async function completeBrowserDeviceLogin(context, parsed) {
|
|
2150
|
+
const authClient = createAuthClient(context, parsed);
|
|
2151
|
+
const start = await startBrowserDeviceLogin(authClient);
|
|
2152
|
+
const verificationUri = start.verification_uri_complete ?? start.verification_uri;
|
|
2153
|
+
const openedBrowser = await maybeOpenBrowser(context, parsed, verificationUri);
|
|
2154
|
+
if (!context.globals.json && !context.globals.quiet) {
|
|
2155
|
+
context.stderr.write([
|
|
2156
|
+
`Open this URL to approve vc-tools login: ${verificationUri}`,
|
|
2157
|
+
`Code: ${start.user_code}`,
|
|
2158
|
+
"Only approve the browser page if the code shown there matches this terminal.",
|
|
2159
|
+
openedBrowser ? "A browser window was opened for you." : "The browser was not opened automatically; paste the URL above.",
|
|
2160
|
+
""
|
|
2161
|
+
].join("\n"));
|
|
2162
|
+
}
|
|
2163
|
+
const exchange = await pollBrowserDeviceLogin(context, authClient, start);
|
|
2164
|
+
validateTokenShape(exchange.access_token);
|
|
2165
|
+
return {
|
|
2166
|
+
exchange,
|
|
2167
|
+
browserLogin: {
|
|
2168
|
+
userCode: start.user_code,
|
|
2169
|
+
verificationUri,
|
|
2170
|
+
openedBrowser
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
function createAuthClient(context, parsed) {
|
|
2175
|
+
const authApiUrl = getStringFlag(parsed?.flags ?? {}, "authApiUrl") ?? context.env.VC_TOOLS_AUTH_API_URL ?? DEFAULT_AUTH_API_URL;
|
|
2176
|
+
return createBaseClient({
|
|
2177
|
+
baseUrl: authApiUrl,
|
|
2178
|
+
timeoutMs: context.globals.timeoutMs,
|
|
2179
|
+
allowInsecureLocalApi: allowInsecureLocalApi(context),
|
|
2180
|
+
fetchImpl: context.fetchImpl,
|
|
2181
|
+
serviceName: "Vibecodr Auth API",
|
|
2182
|
+
redactResponses: false
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
async function startBrowserDeviceLogin(authClient) {
|
|
2186
|
+
const response = await authClient.request("POST", "auth/vc-tools/device/start", {
|
|
2187
|
+
body: {
|
|
2188
|
+
client_name: "vc-tools",
|
|
2189
|
+
version: VERSION
|
|
2190
|
+
}
|
|
2191
|
+
});
|
|
2192
|
+
if (!isDeviceStartResponse(response)) {
|
|
2193
|
+
throw new CliError("auth.invalid_device_response", "Vibecodr Auth API returned an invalid vc-tools browser-login response.", 6);
|
|
2194
|
+
}
|
|
2195
|
+
return response;
|
|
2196
|
+
}
|
|
2197
|
+
async function pollBrowserDeviceLogin(context, authClient, start) {
|
|
2198
|
+
let intervalMs = clampPollIntervalMs(start.interval);
|
|
2199
|
+
const deadlineMs = start.expires_at * 1000;
|
|
2200
|
+
while (Date.now() < deadlineMs) {
|
|
2201
|
+
const response = await authClient.request("POST", "auth/vc-tools/device/token", {
|
|
2202
|
+
body: { device_code: start.device_code }
|
|
2203
|
+
});
|
|
2204
|
+
const parsed = parseDevicePollResponse(response);
|
|
2205
|
+
if ("access_token" in parsed) {
|
|
2206
|
+
return parsed;
|
|
2207
|
+
}
|
|
2208
|
+
if (!context.globals.json && !context.globals.quiet && parsed.message) {
|
|
2209
|
+
context.stderr.write(`${parsed.message}\n`);
|
|
2210
|
+
}
|
|
2211
|
+
intervalMs = clampPollIntervalMs(parsed.interval ?? intervalMs / 1000);
|
|
2212
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadlineMs - Date.now())));
|
|
2213
|
+
}
|
|
2214
|
+
throw new CliError("auth.device_login_expired", "vc-tools browser login expired before approval. Run vc-tools login again.", 3);
|
|
2215
|
+
}
|
|
2216
|
+
function parseDevicePollResponse(value) {
|
|
2217
|
+
if (isCliGrantExchangeResponse(value)) {
|
|
2218
|
+
return normalizeCliGrantExchangeResponse(value);
|
|
2219
|
+
}
|
|
2220
|
+
if (isRecord(value) && value.status === "authorization_pending") {
|
|
2221
|
+
return {
|
|
2222
|
+
status: "authorization_pending",
|
|
2223
|
+
interval: typeof value.interval === "number" ? value.interval : undefined,
|
|
2224
|
+
expires_at: typeof value.expires_at === "number" ? value.expires_at : undefined,
|
|
2225
|
+
message: typeof value.message === "string" ? value.message : undefined
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
throw new CliError("auth.invalid_device_response", "Vibecodr Auth API returned an invalid vc-tools browser-login polling response.", 6);
|
|
2229
|
+
}
|
|
2230
|
+
function clampPollIntervalMs(intervalSeconds) {
|
|
2231
|
+
if (!Number.isFinite(intervalSeconds)) {
|
|
2232
|
+
return 5_000;
|
|
2233
|
+
}
|
|
2234
|
+
return Math.max(0, Math.min(30_000, Math.floor(intervalSeconds * 1000)));
|
|
2235
|
+
}
|
|
2236
|
+
function sleep(ms) {
|
|
2237
|
+
if (ms <= 0) {
|
|
2238
|
+
return Promise.resolve();
|
|
2239
|
+
}
|
|
2240
|
+
return new Promise((resolve) => {
|
|
2241
|
+
setTimeout(resolve, ms);
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
async function maybeOpenBrowser(context, parsed, url) {
|
|
2245
|
+
if (parsed.flags.browser === false || context.env.VC_TOOLS_BROWSER_OPEN === "false") {
|
|
2246
|
+
return false;
|
|
2247
|
+
}
|
|
2248
|
+
const parsedUrl = validateBrowserLoginUrl(url);
|
|
2249
|
+
const command = process.platform === "win32"
|
|
2250
|
+
? { file: "cmd", args: ["/c", "start", "", parsedUrl.toString()] }
|
|
2251
|
+
: process.platform === "darwin"
|
|
2252
|
+
? { file: "open", args: [parsedUrl.toString()] }
|
|
2253
|
+
: { file: "xdg-open", args: [parsedUrl.toString()] };
|
|
2254
|
+
try {
|
|
2255
|
+
const child = spawn(command.file, command.args, {
|
|
2256
|
+
detached: true,
|
|
2257
|
+
stdio: "ignore",
|
|
2258
|
+
windowsHide: true
|
|
2259
|
+
});
|
|
2260
|
+
child.unref();
|
|
2261
|
+
return true;
|
|
2262
|
+
}
|
|
2263
|
+
catch {
|
|
2264
|
+
return false;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
function validateBrowserLoginUrl(value) {
|
|
2268
|
+
let url;
|
|
2269
|
+
try {
|
|
2270
|
+
url = new URL(value);
|
|
2271
|
+
}
|
|
2272
|
+
catch {
|
|
2273
|
+
throw new CliError("auth.invalid_device_response", "Vibecodr Auth API returned an invalid browser-login URL.", 6);
|
|
2274
|
+
}
|
|
2275
|
+
if (url.username || url.password) {
|
|
2276
|
+
throw new CliError("auth.invalid_device_response", "Browser-login URL must not include credentials.", 6);
|
|
2277
|
+
}
|
|
2278
|
+
if (url.protocol !== "https:" && !["localhost", "127.0.0.1", "::1"].includes(url.hostname.toLowerCase().replace(/^\[|\]$/g, ""))) {
|
|
2279
|
+
throw new CliError("auth.invalid_device_response", "Browser-login URL must use HTTPS outside local development.", 6);
|
|
2280
|
+
}
|
|
2281
|
+
return url;
|
|
2282
|
+
}
|
|
2283
|
+
function isDeviceStartResponse(value) {
|
|
2284
|
+
return (isRecord(value) &&
|
|
2285
|
+
typeof value.device_code === "string" &&
|
|
2286
|
+
typeof value.user_code === "string" &&
|
|
2287
|
+
typeof value.verification_uri === "string" &&
|
|
2288
|
+
(value.verification_uri_complete === undefined || typeof value.verification_uri_complete === "string") &&
|
|
2289
|
+
typeof value.expires_at === "number" &&
|
|
2290
|
+
typeof value.interval === "number");
|
|
2291
|
+
}
|
|
2292
|
+
function isCliGrantExchangeResponse(value) {
|
|
2293
|
+
return (isRecord(value) &&
|
|
2294
|
+
value.token_type === "Bearer" &&
|
|
2295
|
+
typeof value.access_token === "string" &&
|
|
2296
|
+
typeof value.expires_at === "number" &&
|
|
2297
|
+
typeof value.user_id === "string");
|
|
2298
|
+
}
|
|
2299
|
+
function normalizeCliGrantExchangeResponse(value) {
|
|
2300
|
+
const durable = normalizeDurableCredentialResponse(value.durable_credential);
|
|
2301
|
+
return {
|
|
2302
|
+
token_type: "Bearer",
|
|
2303
|
+
access_token: String(value.access_token),
|
|
2304
|
+
expires_at: Number(value.expires_at),
|
|
2305
|
+
user_id: String(value.user_id),
|
|
2306
|
+
user_handle: typeof value.user_handle === "string" ? value.user_handle : undefined,
|
|
2307
|
+
credential_type: typeof value.credential_type === "string" ? value.credential_type : undefined,
|
|
2308
|
+
grant_profile: typeof value.grant_profile === "string" ? value.grant_profile : undefined,
|
|
2309
|
+
scopes: Array.isArray(value.scopes) ? value.scopes.filter((scope) => typeof scope === "string") : undefined,
|
|
2310
|
+
durable_credential: durable
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
function normalizeDurableCredentialResponse(value) {
|
|
2314
|
+
if (!isRecord(value) || value.type !== "api_key" || typeof value.api_key !== "string") {
|
|
2315
|
+
return undefined;
|
|
2316
|
+
}
|
|
2317
|
+
validateCredentialShape(value.api_key, "API key");
|
|
2318
|
+
return {
|
|
2319
|
+
type: "api_key",
|
|
2320
|
+
api_key: value.api_key,
|
|
2321
|
+
id: typeof value.id === "string" ? value.id : undefined,
|
|
2322
|
+
name: typeof value.name === "string" ? value.name : undefined,
|
|
2323
|
+
expires_at: typeof value.expires_at === "number" ? value.expires_at : undefined
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
function versionedApiUrl(apiUrl, allowInsecure = false) {
|
|
2327
|
+
const url = normalizeBaseUrl(apiUrl, allowInsecure);
|
|
2328
|
+
const pathname = url.pathname.replace(/\/+$/, "");
|
|
2329
|
+
if (!pathname.endsWith("/v1")) {
|
|
2330
|
+
url.pathname = `${pathname}/v1/`.replace(/^\/?/, "/");
|
|
2331
|
+
}
|
|
2332
|
+
return url.toString();
|
|
2333
|
+
}
|
|
2334
|
+
function validateTokenShape(token) {
|
|
2335
|
+
if (token.length < 12 || /\s/.test(token)) {
|
|
2336
|
+
throw new CliError("auth.invalid_token", "Token must be at least 12 characters and contain no whitespace.", 2);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
function validateCredentialShape(value, label) {
|
|
2340
|
+
if (value.length < 8 || /\s/.test(value)) {
|
|
2341
|
+
throw new CliError("auth.invalid_credential", `${label} must be at least 8 characters and contain no whitespace.`, 2);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
async function resolveLoginCredential(context, parsed, includeCommandFlags) {
|
|
2345
|
+
rejectCredentialTypeFlags(parsed);
|
|
2346
|
+
const descriptors = credentialDescriptors(context, parsed, includeCommandFlags);
|
|
2347
|
+
if (descriptors.length > 1) {
|
|
2348
|
+
throw new CliError("auth.ambiguous_credentials", `Provide only one vc-tools credential source. Received: ${descriptors.map((descriptor) => descriptor.label).join(", ")}.`, 3);
|
|
2349
|
+
}
|
|
2350
|
+
const descriptor = descriptors[0];
|
|
2351
|
+
if (descriptor === undefined) {
|
|
2352
|
+
return undefined;
|
|
2353
|
+
}
|
|
2354
|
+
const value = await readCredentialDescriptor(context, descriptor);
|
|
2355
|
+
const mode = descriptor.mode === "auto" ? inferCredentialMode(value) : descriptor.mode;
|
|
2356
|
+
return { mode, source: descriptor.source, value };
|
|
2357
|
+
}
|
|
2358
|
+
function rejectCredentialTypeFlags(parsed) {
|
|
2359
|
+
if (!parsed) {
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
const removedFlags = [
|
|
2363
|
+
"apiKey",
|
|
2364
|
+
"apiKeyFile",
|
|
2365
|
+
"apiKeyStdin",
|
|
2366
|
+
"oauthToken",
|
|
2367
|
+
"oauthTokenFile",
|
|
2368
|
+
"oauthTokenStdin"
|
|
2369
|
+
].filter((flag) => Object.prototype.hasOwnProperty.call(parsed.flags, flag));
|
|
2370
|
+
if (removedFlags.length > 0) {
|
|
2371
|
+
throw new CliError("input.unsupported_credential_flag", "Use --credential, --credential-file, or --credential-stdin. vc-tools now infers whether the credential is a grant, Clerk API key, or Clerk OAuth token.", 2);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
function credentialDescriptors(context, parsed, includeCommandFlags) {
|
|
2375
|
+
const descriptors = [];
|
|
2376
|
+
const flags = parsed?.flags ?? {};
|
|
2377
|
+
addCredentialValue(descriptors, "auto", "flag", "--credential", context.globals.credential);
|
|
2378
|
+
addCredentialFile(descriptors, "auto", "flag", "--credential-file", context.globals.credentialFile);
|
|
2379
|
+
addCredentialStdin(descriptors, "auto", "stdin", "--credential-stdin", context.globals.credentialStdin);
|
|
2380
|
+
addCredentialValue(descriptors, "token", "flag", "--token", context.globals.token);
|
|
2381
|
+
addCredentialFile(descriptors, "token", "flag", "--token-file", context.globals.tokenFile);
|
|
2382
|
+
addCredentialStdin(descriptors, "token", "stdin", "--token-stdin", context.globals.tokenStdin);
|
|
2383
|
+
if (includeCommandFlags) {
|
|
2384
|
+
addCredentialValue(descriptors, "auto", "flag", "--credential", getStringFlag(flags, "credential"));
|
|
2385
|
+
addCredentialFile(descriptors, "auto", "flag", "--credential-file", getStringFlag(flags, "credentialFile"));
|
|
2386
|
+
addCredentialStdin(descriptors, "auto", "stdin", "--credential-stdin", getBooleanFlag(flags, "credentialStdin"));
|
|
2387
|
+
addCredentialValue(descriptors, "token", "flag", "--token", getStringFlag(flags, "token"));
|
|
2388
|
+
addCredentialFile(descriptors, "token", "flag", "--token-file", getStringFlag(flags, "tokenFile"));
|
|
2389
|
+
addCredentialStdin(descriptors, "token", "stdin", "--token-stdin", getBooleanFlag(flags, "tokenStdin"));
|
|
2390
|
+
}
|
|
2391
|
+
addCredentialValue(descriptors, "auto", "env", "VC_TOOLS_CREDENTIAL", context.env.VC_TOOLS_CREDENTIAL);
|
|
2392
|
+
addCredentialFile(descriptors, "auto", "file", "VC_TOOLS_CREDENTIAL_FILE", context.env.VC_TOOLS_CREDENTIAL_FILE);
|
|
2393
|
+
addCredentialValue(descriptors, "token", "env", "VC_TOOLS_TOKEN", context.env.VC_TOOLS_TOKEN);
|
|
2394
|
+
addCredentialFile(descriptors, "token", "file", "VC_TOOLS_TOKEN_FILE", context.env.VC_TOOLS_TOKEN_FILE);
|
|
2395
|
+
return descriptors;
|
|
2396
|
+
}
|
|
2397
|
+
function addCredentialValue(descriptors, mode, source, label, value) {
|
|
2398
|
+
if (value !== undefined && value !== "") {
|
|
2399
|
+
descriptors.push({ mode, source, label, value });
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
function addCredentialFile(descriptors, mode, source, label, file) {
|
|
2403
|
+
if (file !== undefined && file !== "") {
|
|
2404
|
+
descriptors.push({ mode, source, label, file });
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
function addCredentialStdin(descriptors, mode, source, label, enabled) {
|
|
2408
|
+
if (enabled) {
|
|
2409
|
+
descriptors.push({ mode, source, label });
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
function inferCredentialMode(value) {
|
|
2413
|
+
if (/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value)) {
|
|
2414
|
+
return "token";
|
|
2415
|
+
}
|
|
2416
|
+
if (value.startsWith("ak_")) {
|
|
2417
|
+
return "api_key";
|
|
2418
|
+
}
|
|
2419
|
+
if (value.startsWith("oat_") || value.startsWith("oauth_")) {
|
|
2420
|
+
return "oauth";
|
|
2421
|
+
}
|
|
2422
|
+
throw new CliError("auth.credential_type_unknown", "Could not identify the credential type. Use a vc-tools grant token, a Clerk API key starting with ak_, or a Clerk OAuth token starting with oat_.", 2);
|
|
2423
|
+
}
|
|
2424
|
+
async function readCredentialDescriptor(context, descriptor) {
|
|
2425
|
+
if (descriptor.value !== undefined) {
|
|
2426
|
+
return descriptor.value.trim();
|
|
2427
|
+
}
|
|
2428
|
+
if (descriptor.file !== undefined) {
|
|
2429
|
+
const filePath = path.resolve(context.cwd, descriptor.file);
|
|
2430
|
+
const stat = await fs.stat(filePath).catch(() => undefined);
|
|
2431
|
+
if (!stat?.isFile()) {
|
|
2432
|
+
throw new CliError("auth.credential_file_missing", `Credential file does not exist: ${filePath}`, 5);
|
|
2433
|
+
}
|
|
2434
|
+
if (stat.size > MAX_CREDENTIAL_BYTES) {
|
|
2435
|
+
throw new CliError("auth.credential_file_too_large", "Credential files must be 64 KiB or smaller.", 5);
|
|
2436
|
+
}
|
|
2437
|
+
const value = (await fs.readFile(filePath, "utf8")).trim();
|
|
2438
|
+
if (!value) {
|
|
2439
|
+
throw new CliError("auth.empty_credential", `Credential file is empty: ${filePath}`, 2);
|
|
2440
|
+
}
|
|
2441
|
+
return value;
|
|
2442
|
+
}
|
|
2443
|
+
if (isInteractiveStdin(context.stdin)) {
|
|
2444
|
+
throw new CliError("auth.stdin_interactive", `${descriptor.label} reads from stdin. Pipe the credential or use a credential file.`, 2);
|
|
2445
|
+
}
|
|
2446
|
+
const value = (await readStdin(context.stdin)).trim();
|
|
2447
|
+
if (!value) {
|
|
2448
|
+
throw new CliError("auth.empty_credential", `${descriptor.label} received no stdin data.`, 2);
|
|
2449
|
+
}
|
|
2450
|
+
return value;
|
|
2451
|
+
}
|
|
2452
|
+
async function readStdin(stdin) {
|
|
2453
|
+
const chunks = [];
|
|
2454
|
+
let total = 0;
|
|
2455
|
+
for await (const chunk of stdin) {
|
|
2456
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
2457
|
+
total += buffer.byteLength;
|
|
2458
|
+
if (total > MAX_CREDENTIAL_BYTES) {
|
|
2459
|
+
throw new CliError("auth.stdin_too_large", "Credential stdin must be 64 KiB or smaller.", 2);
|
|
2460
|
+
}
|
|
2461
|
+
chunks.push(buffer);
|
|
2462
|
+
}
|
|
2463
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2464
|
+
}
|
|
2465
|
+
function isInteractiveStdin(stdin) {
|
|
2466
|
+
return Boolean(stdin.isTTY);
|
|
2467
|
+
}
|
|
2468
|
+
function requiredPositional(parsed, index, message) {
|
|
2469
|
+
const value = parsed.positionals[index];
|
|
2470
|
+
if (!value) {
|
|
2471
|
+
throw new CliError("input.missing_argument", message, 2);
|
|
2472
|
+
}
|
|
2473
|
+
return value;
|
|
2474
|
+
}
|
|
2475
|
+
async function ensureOutputPathAllowed(cwd, outPath) {
|
|
2476
|
+
const lexicalCwd = path.resolve(cwd);
|
|
2477
|
+
assertPathInsideWorkspace(lexicalCwd, path.resolve(outPath));
|
|
2478
|
+
const realCwd = await fs.realpath(cwd);
|
|
2479
|
+
const realCandidate = await realpathOrNearestExistingParent(outPath);
|
|
2480
|
+
if (realCandidate === undefined || isPathOutside(realCwd, realCandidate)) {
|
|
2481
|
+
throw new CliError("file.outside_workspace", ARTIFACT_OUTPUT_WORKSPACE_MESSAGE, 5);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
async function ensureInputPathAllowed(cwd, inputPath) {
|
|
2485
|
+
const [resolvedCwd, resolvedInput] = await Promise.all([
|
|
2486
|
+
fs.realpath(cwd),
|
|
2487
|
+
fs.realpath(inputPath)
|
|
2488
|
+
]);
|
|
2489
|
+
const relative = path.relative(resolvedCwd, resolvedInput);
|
|
2490
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
2491
|
+
throw new CliError("file.outside_workspace", ARTIFACT_INPUT_WORKSPACE_MESSAGE, 5);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
async function realpathOrNearestExistingParent(inputPath) {
|
|
2495
|
+
let current = path.resolve(inputPath);
|
|
2496
|
+
while (true) {
|
|
2497
|
+
const real = await fs.realpath(current).catch(() => undefined);
|
|
2498
|
+
if (real !== undefined) {
|
|
2499
|
+
return real;
|
|
2500
|
+
}
|
|
2501
|
+
const parent = path.dirname(current);
|
|
2502
|
+
if (parent === current) {
|
|
2503
|
+
return undefined;
|
|
2504
|
+
}
|
|
2505
|
+
current = parent;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
function assertPathInsideWorkspace(cwd, candidate) {
|
|
2509
|
+
if (isPathOutside(cwd, candidate)) {
|
|
2510
|
+
throw new CliError("file.outside_workspace", ARTIFACT_OUTPUT_WORKSPACE_MESSAGE, 5);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
function isPathOutside(cwd, candidate) {
|
|
2514
|
+
const relative = path.relative(cwd, candidate);
|
|
2515
|
+
return relative.startsWith("..") || path.isAbsolute(relative);
|
|
2516
|
+
}
|
|
2517
|
+
async function pathExists(file) {
|
|
2518
|
+
try {
|
|
2519
|
+
await fs.access(file);
|
|
2520
|
+
return true;
|
|
2521
|
+
}
|
|
2522
|
+
catch {
|
|
2523
|
+
return false;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function nodeMajor() {
|
|
2527
|
+
return Number(process.version.replace(/^v/, "").split(".")[0] ?? "0");
|
|
2528
|
+
}
|
|
2529
|
+
function isRecord(value) {
|
|
2530
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2531
|
+
}
|
|
2532
|
+
function withOptionalHead(head, tail) {
|
|
2533
|
+
return head === undefined ? tail : [head, ...tail];
|
|
2534
|
+
}
|
|
2535
|
+
const TOP_LEVEL_COMMANDS = ["start", "setup", "try", "agent", "computer", "browser", "work", "proof", "usage", "limits", "dashboard", "doctor", "auth", "login", "logout", "status", "whoami", "connect", "tools", "jobs", "artifacts", "grants", "retention", "scheduled-qa", "plans", "inspect"];
|
|
2536
|
+
function helpResult(args = []) {
|
|
2537
|
+
const topic = args.filter((arg) => arg !== "--").join(" ").trim();
|
|
2538
|
+
return {
|
|
2539
|
+
message: topic ? commandHelpText(args) : helpText(),
|
|
2540
|
+
data: helpData(topic || undefined),
|
|
2541
|
+
humanData: "hide"
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
function helpText() {
|
|
2545
|
+
return `vc-tools ${VERSION}
|
|
2546
|
+
|
|
2547
|
+
The hosted Vibecodr computer for agents.
|
|
2548
|
+
|
|
2549
|
+
Examples:
|
|
2550
|
+
vc-tools start
|
|
2551
|
+
vc-tools try
|
|
2552
|
+
vc-tools agent connect --client codex
|
|
2553
|
+
vc-tools computer status
|
|
2554
|
+
vc-tools browser screenshot https://example.com --format png --out ./proof
|
|
2555
|
+
vc-tools browser read https://example.com
|
|
2556
|
+
vc-tools computer run "npm test" --out ./proof
|
|
2557
|
+
vc-tools work follow job_123
|
|
2558
|
+
vc-tools proof save art_123 --out ./artifacts
|
|
2559
|
+
|
|
2560
|
+
Usage:
|
|
2561
|
+
vc-tools <command> [options]
|
|
2562
|
+
vc-tools help <command>
|
|
2563
|
+
|
|
2564
|
+
Commands:
|
|
2565
|
+
start Connect and verify the Agent Computer, then return agent connection details.
|
|
2566
|
+
setup Alias for start.
|
|
2567
|
+
try Run a small browser, computer, proof, and usage check.
|
|
2568
|
+
agent Connect an agent to the hosted computer or check readiness.
|
|
2569
|
+
computer Start/status/run commands on the hosted Agent Computer.
|
|
2570
|
+
browser Ask the hosted Browser to render, read, screenshot, crawl, or inspect public HTTPS pages.
|
|
2571
|
+
work List, follow, show, or cancel hosted work.
|
|
2572
|
+
proof List, show, save, or delete saved outputs and artifacts.
|
|
2573
|
+
usage Show account-scoped Agent Computer capacity and quota progress.
|
|
2574
|
+
limits Alias for usage.
|
|
2575
|
+
dashboard Print the hosted supervision dashboard URL.
|
|
2576
|
+
doctor Explain whether the Agent Computer is ready and what to do next.
|
|
2577
|
+
|
|
2578
|
+
Advanced/debug commands:
|
|
2579
|
+
auth, login, logout, status, whoami, connect, tools, jobs, artifacts, grants, retention, scheduled-qa, plans, inspect
|
|
2580
|
+
|
|
2581
|
+
Global flags:
|
|
2582
|
+
--json Stable machine-readable output.
|
|
2583
|
+
-q, --quiet Suppress non-essential human success output.
|
|
2584
|
+
-h, --help Show help. Works after subcommands too.
|
|
2585
|
+
--version Show version.
|
|
2586
|
+
--api-url <url> Hosted Tools API URL. HTTPS unless local dev is explicitly allowed.
|
|
2587
|
+
--allow-insecure-local-api Allow http://localhost API URLs for local development.
|
|
2588
|
+
--timeout-ms <ms> Network timeout from 1000 to 300000.
|
|
2589
|
+
--no-input Disable browser/device login for automation.
|
|
2590
|
+
--no-color Accepted for CLI convention compatibility. vc-tools emits no color by default.
|
|
2591
|
+
|
|
2592
|
+
Advanced credential/config flags:
|
|
2593
|
+
--credential-file <path> Read a vc-tools grant, Clerk API key, or Clerk OAuth token from a file.
|
|
2594
|
+
--credential-stdin Read a vc-tools grant, Clerk API key, or Clerk OAuth token from stdin.
|
|
2595
|
+
--config-dir <dir> Isolated config directory override for tests/automation.
|
|
2596
|
+
|
|
2597
|
+
Docs:
|
|
2598
|
+
https://vibecodr.space/docs/vc-tools
|
|
2599
|
+
Overview:
|
|
2600
|
+
https://vibecodr.space/vc-tools
|
|
2601
|
+
Support:
|
|
2602
|
+
https://vibecodr.space/support
|
|
2603
|
+
`;
|
|
2604
|
+
}
|
|
2605
|
+
function commandHelpText(args) {
|
|
2606
|
+
const [command, subcommand] = args;
|
|
2607
|
+
if (command === undefined) {
|
|
2608
|
+
return helpText();
|
|
2609
|
+
}
|
|
2610
|
+
switch (command) {
|
|
2611
|
+
case "start":
|
|
2612
|
+
case "setup":
|
|
2613
|
+
return `vc-tools start
|
|
2614
|
+
|
|
2615
|
+
Connect and verify the hosted Vibecodr Agent Computer, then return the connection details an agent needs.
|
|
2616
|
+
|
|
2617
|
+
Examples:
|
|
2618
|
+
vc-tools start
|
|
2619
|
+
vc-tools start --client codex
|
|
2620
|
+
|
|
2621
|
+
Usage:
|
|
2622
|
+
vc-tools start [--client generic]
|
|
2623
|
+
vc-tools setup [--client generic]
|
|
2624
|
+
`;
|
|
2625
|
+
case "agent":
|
|
2626
|
+
return `vc-tools agent
|
|
2627
|
+
|
|
2628
|
+
Connect an agent to the hosted Vibecodr computer or check whether the computer is ready.
|
|
2629
|
+
|
|
2630
|
+
Usage:
|
|
2631
|
+
vc-tools agent connect [--client generic]
|
|
2632
|
+
vc-tools agent instructions [--client generic]
|
|
2633
|
+
vc-tools agent status
|
|
2634
|
+
`;
|
|
2635
|
+
case "try":
|
|
2636
|
+
return `vc-tools try
|
|
2637
|
+
|
|
2638
|
+
Run a small end-to-end Agent Computer check: auth, hosted API, public Browser read, hosted computer run, proof saving, and usage.
|
|
2639
|
+
|
|
2640
|
+
Usage:
|
|
2641
|
+
vc-tools try [--out ./vc-tools-proof] [--details]
|
|
2642
|
+
`;
|
|
2643
|
+
case "computer":
|
|
2644
|
+
return `vc-tools computer
|
|
2645
|
+
|
|
2646
|
+
Use the hosted Agent Computer. Commands are submitted to Vibecodr Tools Cloud; nothing is executed locally.
|
|
2647
|
+
Public HTTP(S) package/docs access is available by default; private, local, and internal destinations are blocked by hosted policy.
|
|
2648
|
+
|
|
2649
|
+
Usage:
|
|
2650
|
+
vc-tools computer start
|
|
2651
|
+
vc-tools computer status
|
|
2652
|
+
vc-tools computer run "<command>" [--timeout-ms <ms>] [--network public|off] [--out ./proof] [--no-wait] [--details]
|
|
2653
|
+
vc-tools computer test "<command>" [--timeout-ms <ms>] [--network public|off] [--out ./proof] [--no-wait] [--details]
|
|
2654
|
+
`;
|
|
2655
|
+
case "browser":
|
|
2656
|
+
return `vc-tools browser
|
|
2657
|
+
|
|
2658
|
+
Use the hosted Browser against public HTTPS pages. Localhost, private networks, URL credentials, and internal hosts are blocked before hosted work is submitted.
|
|
2659
|
+
|
|
2660
|
+
Usage:
|
|
2661
|
+
vc-tools browser screenshot <https-url> [--format png] [--out ./proof] [--no-wait] [--details]
|
|
2662
|
+
vc-tools browser read <https-url> [--out ./proof] [--no-wait] [--details]
|
|
2663
|
+
vc-tools browser render <https-url> [--out ./proof] [--no-wait] [--details]
|
|
2664
|
+
vc-tools browser pdf <https-url> [--out ./proof] [--no-wait] [--details]
|
|
2665
|
+
vc-tools browser crawl <https-url> [--max-pages n] [--max-depth n]
|
|
2666
|
+
vc-tools browser snapshot <https-url> [--instructions <text>]
|
|
2667
|
+
vc-tools browser ask <https-url> --instructions <text>
|
|
2668
|
+
|
|
2669
|
+
Notes:
|
|
2670
|
+
browser snapshot captures a bounded inspection snapshot for your agent to analyze. browser ask is a compatibility alias; it is not a separate chat answerer.
|
|
2671
|
+
`;
|
|
2672
|
+
case "work":
|
|
2673
|
+
return `vc-tools work
|
|
2674
|
+
|
|
2675
|
+
Inspect hosted work the agent has submitted.
|
|
2676
|
+
|
|
2677
|
+
Usage:
|
|
2678
|
+
vc-tools work list [--limit 20]
|
|
2679
|
+
vc-tools work show <jobId>
|
|
2680
|
+
vc-tools work follow <jobId> [--out ./proof] [--details]
|
|
2681
|
+
vc-tools work cancel <jobId> --yes
|
|
2682
|
+
`;
|
|
2683
|
+
case "proof":
|
|
2684
|
+
return `vc-tools proof
|
|
2685
|
+
|
|
2686
|
+
List, inspect, save, or delete outputs saved by hosted work.
|
|
2687
|
+
|
|
2688
|
+
Usage:
|
|
2689
|
+
vc-tools proof list [--limit 20]
|
|
2690
|
+
vc-tools proof show <artifactId>
|
|
2691
|
+
vc-tools proof save <artifactId> [--out <dir|file>] [--filename <name>] [--overwrite]
|
|
2692
|
+
vc-tools proof delete <artifactId> --yes
|
|
2693
|
+
`;
|
|
2694
|
+
case "login":
|
|
2695
|
+
return `vc-tools login
|
|
2696
|
+
|
|
2697
|
+
Approve this machine to use Vibecodr Tools Cloud. Plain login opens the browser/device approval flow.
|
|
2698
|
+
|
|
2699
|
+
Examples:
|
|
2700
|
+
vc-tools login
|
|
2701
|
+
vc-tools login --credential-file ./vc-tools-credential.txt
|
|
2702
|
+
vc-tools login --credential-stdin
|
|
2703
|
+
|
|
2704
|
+
Usage:
|
|
2705
|
+
vc-tools login [--no-browser]
|
|
2706
|
+
vc-tools login (--credential-file <path> | --credential-stdin)
|
|
2707
|
+
|
|
2708
|
+
Options:
|
|
2709
|
+
--no-browser Print the approval URL and code without opening a browser.
|
|
2710
|
+
--skip-verify Save without calling /v1/me.
|
|
2711
|
+
--auth-api-url <url> Override the Vibecodr Auth API exchange URL.
|
|
2712
|
+
--api-url <url> Hosted Tools API URL saved for this approval.
|
|
2713
|
+
`;
|
|
2714
|
+
case "auth":
|
|
2715
|
+
return `vc-tools auth
|
|
2716
|
+
|
|
2717
|
+
Diagnose or export the current Agent Computer approval without printing secrets.
|
|
2718
|
+
|
|
2719
|
+
Usage:
|
|
2720
|
+
vc-tools auth diagnose [--json]
|
|
2721
|
+
vc-tools auth status [--json]
|
|
2722
|
+
vc-tools auth export-agent-env --out <file> --yes [--overwrite]
|
|
2723
|
+
|
|
2724
|
+
Notes:
|
|
2725
|
+
diagnose shows which credential source is winning, whether VC_TOOLS_CONFIG_DIR isolates this session, and whether the stored credential store is readable.
|
|
2726
|
+
export-agent-env writes the durable local credential when available, otherwise a bearer grant, and returns the matching file env var. The secret value is never printed.
|
|
2727
|
+
`;
|
|
2728
|
+
case "logout":
|
|
2729
|
+
return `vc-tools logout
|
|
2730
|
+
|
|
2731
|
+
Remove the saved Agent Computer approval.
|
|
2732
|
+
|
|
2733
|
+
Usage:
|
|
2734
|
+
vc-tools logout --yes
|
|
2735
|
+
`;
|
|
2736
|
+
case "status":
|
|
2737
|
+
return `vc-tools status
|
|
2738
|
+
|
|
2739
|
+
Show whether this shell/agent has an Agent Computer approval saved, without requiring auth.
|
|
2740
|
+
|
|
2741
|
+
Usage:
|
|
2742
|
+
vc-tools status [--json]
|
|
2743
|
+
`;
|
|
2744
|
+
case "whoami":
|
|
2745
|
+
return `vc-tools whoami
|
|
2746
|
+
|
|
2747
|
+
Show the Vibecodr account and plan for the approved Agent Computer.
|
|
2748
|
+
|
|
2749
|
+
Usage:
|
|
2750
|
+
vc-tools whoami [--json]
|
|
2751
|
+
`;
|
|
2752
|
+
case "connect":
|
|
2753
|
+
return `vc-tools connect
|
|
2754
|
+
|
|
2755
|
+
Fetch hosted MCP connection metadata for an agent client. Most users should use vc-tools agent connect.
|
|
2756
|
+
|
|
2757
|
+
Usage:
|
|
2758
|
+
vc-tools connect [--client generic]
|
|
2759
|
+
`;
|
|
2760
|
+
case "tools":
|
|
2761
|
+
if (subcommand === "test") {
|
|
2762
|
+
return `vc-tools tools test
|
|
2763
|
+
|
|
2764
|
+
Submit a no-local-execution hosted tool test after validating local inputs.
|
|
2765
|
+
|
|
2766
|
+
Examples:
|
|
2767
|
+
vc-tools tools test browser.render https://example.com
|
|
2768
|
+
vc-tools tools test browser.agent https://example.com --timeout-ms 1200000 --idle-timeout-ms 600000
|
|
2769
|
+
vc-tools tools test browser.crawl https://example.com/docs --max-pages 10 --max-depth 1
|
|
2770
|
+
vc-tools tools test sandbox.run --command "npm test"
|
|
2771
|
+
vc-tools tools test usage
|
|
2772
|
+
|
|
2773
|
+
Usage:
|
|
2774
|
+
vc-tools tools test <capability> [target] [--command <cmd>] [--timeout-ms <ms>] [--max-pages n] [--max-depth n] [--no-render]
|
|
2775
|
+
|
|
2776
|
+
Notes:
|
|
2777
|
+
Browser Quick Actions accept up to 180000 ms. Browser agent tasks accept up to 3600000 ms and are plan-capped by the hosted service. Sandbox tasks accept up to 1800000 ms and are plan-capped by the hosted service.
|
|
2778
|
+
`;
|
|
2779
|
+
}
|
|
2780
|
+
return `vc-tools tools
|
|
2781
|
+
|
|
2782
|
+
Advanced: list granted low-level capabilities or submit a hosted capability test.
|
|
2783
|
+
|
|
2784
|
+
Usage:
|
|
2785
|
+
vc-tools tools list
|
|
2786
|
+
vc-tools tools test <capability> [target] [--command <cmd>]
|
|
2787
|
+
`;
|
|
2788
|
+
case "jobs":
|
|
2789
|
+
return `vc-tools jobs
|
|
2790
|
+
|
|
2791
|
+
Advanced alias for vc-tools work.
|
|
2792
|
+
|
|
2793
|
+
Usage:
|
|
2794
|
+
vc-tools jobs list [--limit 20]
|
|
2795
|
+
vc-tools jobs status <jobId>
|
|
2796
|
+
vc-tools jobs cancel <jobId> --yes
|
|
2797
|
+
`;
|
|
2798
|
+
case "artifacts":
|
|
2799
|
+
return `vc-tools artifacts
|
|
2800
|
+
|
|
2801
|
+
Advanced alias for vc-tools proof. Pulls and uploads are bounded to the current workspace.
|
|
2802
|
+
|
|
2803
|
+
Usage:
|
|
2804
|
+
vc-tools artifacts list [--limit 20]
|
|
2805
|
+
vc-tools artifacts get <artifactId>
|
|
2806
|
+
vc-tools artifacts pull <artifactId> [--out <dir|file>] [--filename <name>] [--overwrite]
|
|
2807
|
+
vc-tools artifacts create --file <path> [--kind <kind>] --yes
|
|
2808
|
+
vc-tools artifacts delete <artifactId> --yes
|
|
2809
|
+
|
|
2810
|
+
Notes:
|
|
2811
|
+
Delete removes the hosted shelf row and R2 bytes for the authenticated actor.
|
|
2812
|
+
Pull output must stay inside the current workspace. Use --out ./artifacts for a directory, --out ./artifacts/report.pdf for an explicit file target, or --filename <name> to choose a file name inside a directory output.
|
|
2813
|
+
`;
|
|
2814
|
+
case "usage":
|
|
2815
|
+
return `vc-tools usage
|
|
2816
|
+
|
|
2817
|
+
Show account-scoped Agent Computer capacity and quota progress.
|
|
2818
|
+
|
|
2819
|
+
Usage:
|
|
2820
|
+
vc-tools usage [--json]
|
|
2821
|
+
|
|
2822
|
+
Alias:
|
|
2823
|
+
vc-tools limits
|
|
2824
|
+
`;
|
|
2825
|
+
case "limits":
|
|
2826
|
+
return `vc-tools limits
|
|
2827
|
+
|
|
2828
|
+
Alias for vc-tools usage. Shows account-scoped Agent Computer capacity and quota progress.
|
|
2829
|
+
|
|
2830
|
+
Usage:
|
|
2831
|
+
vc-tools limits [--json]
|
|
2832
|
+
`;
|
|
2833
|
+
case "grants":
|
|
2834
|
+
return `vc-tools grants
|
|
2835
|
+
|
|
2836
|
+
Show effective tool grants. With no subcommand, this defaults to list.
|
|
2837
|
+
|
|
2838
|
+
Usage:
|
|
2839
|
+
vc-tools grants
|
|
2840
|
+
vc-tools grants list [--project <id>] [--user <id>]
|
|
2841
|
+
`;
|
|
2842
|
+
case "retention":
|
|
2843
|
+
return `vc-tools retention
|
|
2844
|
+
|
|
2845
|
+
Show or update retention policy. Updates mutate hosted policy and require --yes.
|
|
2846
|
+
|
|
2847
|
+
Usage:
|
|
2848
|
+
vc-tools retention show
|
|
2849
|
+
vc-tools retention set [--logs-days n] [--artifacts-days n] [--recordings off|opt-in|admin] --yes
|
|
2850
|
+
`;
|
|
2851
|
+
case "scheduled-qa":
|
|
2852
|
+
return `vc-tools scheduled-qa
|
|
2853
|
+
|
|
2854
|
+
Create and manage plan-capped scheduled Browser Quick Action checks. Scheduled QA only accepts public HTTPS browser render, screenshot, markdown, and PDF checks.
|
|
2855
|
+
|
|
2856
|
+
Usage:
|
|
2857
|
+
vc-tools scheduled-qa list
|
|
2858
|
+
vc-tools scheduled-qa create <https-url> [--capability browser.render|browser.screenshot|browser.markdown|browser.pdf] [--interval-minutes n] [--label text] [--run-now] --yes
|
|
2859
|
+
vc-tools scheduled-qa pause <id> --yes
|
|
2860
|
+
vc-tools scheduled-qa resume <id> [--run-now] --yes
|
|
2861
|
+
vc-tools scheduled-qa delete <id> --yes
|
|
2862
|
+
`;
|
|
2863
|
+
case "plans":
|
|
2864
|
+
return `vc-tools plans
|
|
2865
|
+
|
|
2866
|
+
Show public Free, Creator, and Pro packaging. Works offline with local fallback data.
|
|
2867
|
+
|
|
2868
|
+
Usage:
|
|
2869
|
+
vc-tools plans [--json]
|
|
2870
|
+
`;
|
|
2871
|
+
case "dashboard":
|
|
2872
|
+
return `vc-tools dashboard
|
|
2873
|
+
|
|
2874
|
+
Print a hosted supervision dashboard URL. Sections: ${DASHBOARD_SECTIONS.map((item) => item.id).join(", ")}.
|
|
2875
|
+
|
|
2876
|
+
Usage:
|
|
2877
|
+
vc-tools dashboard [section]
|
|
2878
|
+
`;
|
|
2879
|
+
case "inspect":
|
|
2880
|
+
return `vc-tools inspect
|
|
2881
|
+
|
|
2882
|
+
Show goal-coverage inspections for local release readiness.
|
|
2883
|
+
|
|
2884
|
+
Usage:
|
|
2885
|
+
vc-tools inspect [--json]
|
|
2886
|
+
`;
|
|
2887
|
+
case "doctor":
|
|
2888
|
+
return `vc-tools doctor
|
|
2889
|
+
|
|
2890
|
+
Explain whether the Agent Computer is ready and what to do next.
|
|
2891
|
+
|
|
2892
|
+
Usage:
|
|
2893
|
+
vc-tools doctor [--json]
|
|
2894
|
+
`;
|
|
2895
|
+
default:
|
|
2896
|
+
throw unknownCommandError(command);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
function helpData(topic) {
|
|
2900
|
+
return {
|
|
2901
|
+
version: VERSION,
|
|
2902
|
+
binary: "vc-tools",
|
|
2903
|
+
package: "@vibecodr/vc-tools",
|
|
2904
|
+
topic,
|
|
2905
|
+
capabilities: CAPABILITIES,
|
|
2906
|
+
grants: LAUNCH_TOOL_GRANTS,
|
|
2907
|
+
workflows: LAUNCH_WORKFLOWS,
|
|
2908
|
+
commands: TOP_LEVEL_COMMANDS,
|
|
2909
|
+
docs: "https://vibecodr.space/docs/vc-tools",
|
|
2910
|
+
overview: "https://vibecodr.space/vc-tools",
|
|
2911
|
+
support: "https://vibecodr.space/support"
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
function unknownCommandError(command) {
|
|
2915
|
+
const suggestion = command === undefined ? undefined : suggest(command, TOP_LEVEL_COMMANDS);
|
|
2916
|
+
const suggestionText = suggestion ? ` Did you mean "vc-tools ${suggestion}"?` : "";
|
|
2917
|
+
return new CliError("input.unknown_command", `Unknown command "${command ?? ""}".${suggestionText} Run vc-tools --help.`, 2);
|
|
2918
|
+
}
|
|
2919
|
+
function unknownSubcommandError(command, subcommand, allowed, usage) {
|
|
2920
|
+
const suggestion = subcommand === undefined ? undefined : suggest(subcommand, allowed);
|
|
2921
|
+
const suggestionText = suggestion ? ` Did you mean "vc-tools ${command} ${suggestion}"?` : "";
|
|
2922
|
+
return new CliError("input.unknown_subcommand", `Unknown ${command} subcommand "${subcommand ?? ""}".${suggestionText} ${usage}`, 2);
|
|
2923
|
+
}
|
|
2924
|
+
function suggest(input, candidates) {
|
|
2925
|
+
const normalized = input.replace(/^-+/, "").toLowerCase();
|
|
2926
|
+
let best;
|
|
2927
|
+
for (const candidate of candidates) {
|
|
2928
|
+
const distance = levenshtein(normalized, candidate.toLowerCase());
|
|
2929
|
+
if (distance <= 3 && (best === undefined || distance < best.distance)) {
|
|
2930
|
+
best = { candidate, distance };
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return best?.candidate;
|
|
2934
|
+
}
|
|
2935
|
+
function levenshtein(left, right) {
|
|
2936
|
+
const previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
|
|
2937
|
+
for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
|
|
2938
|
+
const current = [leftIndex + 1];
|
|
2939
|
+
for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
|
|
2940
|
+
const replaceCost = left[leftIndex] === right[rightIndex] ? 0 : 1;
|
|
2941
|
+
current[rightIndex + 1] = Math.min((current[rightIndex] ?? 0) + 1, (previous[rightIndex + 1] ?? 0) + 1, (previous[rightIndex] ?? 0) + replaceCost);
|
|
2942
|
+
}
|
|
2943
|
+
previous.splice(0, previous.length, ...current);
|
|
2944
|
+
}
|
|
2945
|
+
return previous[right.length] ?? left.length;
|
|
2946
|
+
}
|
|
2947
|
+
//# sourceMappingURL=run.js.map
|