@vibecodr/cli 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +201 -0
  3. package/README.md +66 -0
  4. package/dist/auth/official-client.js +10 -0
  5. package/dist/auth/token-manager.js +424 -0
  6. package/dist/bin/vibecodr-mcp.js +101 -0
  7. package/dist/cli/errors.js +38 -0
  8. package/dist/cli/output.js +52 -0
  9. package/dist/cli/parse.js +84 -0
  10. package/dist/clients/base.js +30 -0
  11. package/dist/clients/codex.js +136 -0
  12. package/dist/clients/cursor.js +91 -0
  13. package/dist/clients/vscode.js +138 -0
  14. package/dist/clients/windsurf.js +81 -0
  15. package/dist/commands/call.js +123 -0
  16. package/dist/commands/config.js +124 -0
  17. package/dist/commands/context.js +5 -0
  18. package/dist/commands/doctor.js +17 -0
  19. package/dist/commands/install.js +63 -0
  20. package/dist/commands/login.js +41 -0
  21. package/dist/commands/logout.js +26 -0
  22. package/dist/commands/pulse-setup.js +82 -0
  23. package/dist/commands/status.js +64 -0
  24. package/dist/commands/tools.js +82 -0
  25. package/dist/commands/uninstall.js +55 -0
  26. package/dist/core/interactive-input.js +114 -0
  27. package/dist/core/mcp-client.js +82 -0
  28. package/dist/core/renderers.js +34 -0
  29. package/dist/doctor/run.js +132 -0
  30. package/dist/platform/browser.js +79 -0
  31. package/dist/platform/exec.js +23 -0
  32. package/dist/platform/paths.js +36 -0
  33. package/dist/platform/prompt.js +19 -0
  34. package/dist/storage/config-store.js +72 -0
  35. package/dist/storage/file-lock.js +41 -0
  36. package/dist/storage/install-manifest.js +80 -0
  37. package/dist/storage/secret-store.js +301 -0
  38. package/dist/types/auth.js +1 -0
  39. package/dist/types/config.js +21 -0
  40. package/dist/types/install.js +1 -0
  41. package/docs/architecture.md +35 -0
  42. package/docs/auth.md +66 -0
  43. package/docs/clients.md +42 -0
  44. package/docs/commands.md +134 -0
  45. package/docs/contributors.md +68 -0
  46. package/docs/install.md +90 -0
  47. package/docs/licensing.md +20 -0
  48. package/docs/troubleshooting.md +97 -0
  49. package/package.json +40 -0
@@ -0,0 +1,124 @@
1
+ import { parseFlags } from "../cli/parse.js";
2
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
3
+ import { defaultProfileConfig } from "../types/config.js";
4
+ function updateProfileKey(profile, key, value) {
5
+ switch (key) {
6
+ case "server-url":
7
+ return { ...profile, serverUrl: value };
8
+ case "browser-mode":
9
+ return { ...profile, browserMode: value };
10
+ case "registration-mode":
11
+ return { ...profile, registrationMode: value };
12
+ case "default-install-scope":
13
+ return { ...profile, defaultInstallScope: value };
14
+ case "log-level":
15
+ return { ...profile, logLevel: value };
16
+ default:
17
+ throw new CliError("usage.unknown_config_key", `Unknown config key: ${key}`, EXIT_CODES.usage);
18
+ }
19
+ }
20
+ function unsetProfileKey(profile, key) {
21
+ switch (key) {
22
+ case "server-url":
23
+ return { ...profile, serverUrl: "https://openai.vibecodr.space/mcp" };
24
+ case "browser-mode":
25
+ return { ...profile, browserMode: "print" };
26
+ case "registration-mode":
27
+ return { ...profile, registrationMode: "auto" };
28
+ case "default-install-scope":
29
+ return { ...profile, defaultInstallScope: "user" };
30
+ case "log-level":
31
+ return { ...profile, logLevel: "normal" };
32
+ default:
33
+ throw new CliError("usage.unknown_config_key", `Unknown config key: ${key}`, EXIT_CODES.usage);
34
+ }
35
+ }
36
+ async function saveConfig(context, config) {
37
+ await context.configStore.save(config);
38
+ }
39
+ export async function runConfigCommand(args, context) {
40
+ const action = args[0];
41
+ const config = await context.configStore.load();
42
+ const currentProfileName = context.globalOptions.profile || config.currentProfile;
43
+ const currentProfile = config.profiles[currentProfileName] || config.profiles[config.currentProfile] || defaultProfileConfig();
44
+ if (action === "path") {
45
+ context.output.success({ schemaVersion: 1, path: context.configStore.path() }, [context.configStore.path()]);
46
+ return;
47
+ }
48
+ if (action === "show") {
49
+ context.output.success({ schemaVersion: 1, config }, [JSON.stringify(config, null, 2)]);
50
+ return;
51
+ }
52
+ if (action === "set") {
53
+ const key = args[1];
54
+ const value = args[2];
55
+ if (!key || !value)
56
+ throw new CliError("usage.config_set", "Usage: config set <key> <value>", EXIT_CODES.usage);
57
+ config.profiles[currentProfileName] = updateProfileKey(currentProfile, key, value);
58
+ await saveConfig(context, config);
59
+ context.output.success({ schemaVersion: 1, profile: currentProfileName, key, value }, [`${currentProfileName}: set ${key}=${value}`]);
60
+ return;
61
+ }
62
+ if (action === "unset") {
63
+ const key = args[1];
64
+ if (!key)
65
+ throw new CliError("usage.config_unset", "Usage: config unset <key>", EXIT_CODES.usage);
66
+ config.profiles[currentProfileName] = unsetProfileKey(currentProfile, key);
67
+ await saveConfig(context, config);
68
+ context.output.success({ schemaVersion: 1, profile: currentProfileName, key }, [`${currentProfileName}: unset ${key}`]);
69
+ return;
70
+ }
71
+ if (action === "profile") {
72
+ const subAction = args[1];
73
+ if (subAction === "list") {
74
+ const profiles = Object.keys(config.profiles);
75
+ context.output.success({ schemaVersion: 1, currentProfile: config.currentProfile, profiles }, profiles);
76
+ return;
77
+ }
78
+ if (subAction === "create") {
79
+ const name = args[2];
80
+ if (!name)
81
+ throw new CliError("usage.profile_create", "Usage: config profile create <name> [--server-url <url>]", EXIT_CODES.usage);
82
+ const { flags } = parseFlags(args.slice(3), { valueFlags: ["server-url"] });
83
+ config.profiles[name] = {
84
+ ...currentProfile,
85
+ ...(typeof flags["server-url"] === "string" ? { serverUrl: flags["server-url"] } : {})
86
+ };
87
+ await saveConfig(context, config);
88
+ context.output.success({ schemaVersion: 1, created: name }, [`Created profile ${name}.`]);
89
+ return;
90
+ }
91
+ if (subAction === "use") {
92
+ const name = args[2];
93
+ if (!name || !config.profiles[name])
94
+ throw new CliError("usage.profile_use", "Usage: config profile use <name>", EXIT_CODES.usage);
95
+ config.currentProfile = name;
96
+ await saveConfig(context, config);
97
+ context.output.success({ schemaVersion: 1, currentProfile: name }, [`Current profile: ${name}`]);
98
+ return;
99
+ }
100
+ if (subAction === "delete") {
101
+ const name = args[2];
102
+ const { flags } = parseFlags(args.slice(3), { booleanFlags: ["force"] });
103
+ if (!name || !config.profiles[name])
104
+ throw new CliError("usage.profile_delete", "Usage: config profile delete <name> [--force]", EXIT_CODES.usage);
105
+ if (config.currentProfile === name && !flags["force"]) {
106
+ throw new CliError("config.profile_in_use", "Cannot delete the current profile without --force.", EXIT_CODES.config);
107
+ }
108
+ delete config.profiles[name];
109
+ if (!Object.keys(config.profiles).length) {
110
+ throw new CliError("config.last_profile", "Cannot delete the last profile.", EXIT_CODES.config);
111
+ }
112
+ if (config.currentProfile === name) {
113
+ const nextProfile = Object.keys(config.profiles)[0];
114
+ if (!nextProfile)
115
+ throw new CliError("config.last_profile", "Cannot delete the last profile.", EXIT_CODES.config);
116
+ config.currentProfile = nextProfile;
117
+ }
118
+ await saveConfig(context, config);
119
+ context.output.success({ schemaVersion: 1, deleted: name }, [`Deleted profile ${name}.`]);
120
+ return;
121
+ }
122
+ }
123
+ throw new CliError("usage.config", "Usage: config path|show|set|unset|profile ...", EXIT_CODES.usage);
124
+ }
@@ -0,0 +1,5 @@
1
+ import { Output } from "../cli/output.js";
2
+ import { TokenManager } from "../auth/token-manager.js";
3
+ import { ConfigStore } from "../storage/config-store.js";
4
+ import { SecretStore } from "../storage/secret-store.js";
5
+ import { McpRuntimeClient } from "../core/mcp-client.js";
@@ -0,0 +1,17 @@
1
+ import { parseFlags } from "../cli/parse.js";
2
+ import { runDoctor } from "../doctor/run.js";
3
+ import { EXIT_CODES } from "../cli/errors.js";
4
+ export async function runDoctorCommand(args, context) {
5
+ const { flags } = parseFlags(args, {
6
+ valueFlags: ["client"]
7
+ });
8
+ const checks = await runDoctor(context.globalOptions, context.tokenManager, context.secretStore, typeof flags["client"] === "string" ? flags["client"] : undefined);
9
+ context.output.success({
10
+ schemaVersion: 1,
11
+ ok: checks.every((check) => check.status === "pass"),
12
+ checks
13
+ }, checks.map((check) => `[${check.status.toUpperCase()}] ${check.id}: ${check.summary}`));
14
+ if (checks.some((check) => check.status === "fail")) {
15
+ process.exitCode = EXIT_CODES.runtime;
16
+ }
17
+ }
@@ -0,0 +1,63 @@
1
+ import { parseFlags } from "../cli/parse.js";
2
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
3
+ import { InstallManifestStore } from "../storage/install-manifest.js";
4
+ import { installCodex } from "../clients/codex.js";
5
+ import { installCursor } from "../clients/cursor.js";
6
+ import { installVsCode } from "../clients/vscode.js";
7
+ import { installWindsurf } from "../clients/windsurf.js";
8
+ function defaultName(serverUrl) {
9
+ return serverUrl.includes("staging") ? "vibecodr-staging" : "vibecodr";
10
+ }
11
+ export async function runInstallCommand(args, context) {
12
+ const client = args[0];
13
+ if (!client || !["codex", "cursor", "vscode", "windsurf"].includes(client)) {
14
+ throw new CliError("usage.install_client", "Usage: install <codex|cursor|vscode|windsurf> [options]", EXIT_CODES.usage);
15
+ }
16
+ const { flags } = parseFlags(args.slice(1), {
17
+ valueFlags: ["scope", "path", "name"],
18
+ booleanFlags: ["open-client", "overwrite", "dry-run"]
19
+ });
20
+ const { serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
21
+ const request = {
22
+ serverUrl,
23
+ name: typeof flags["name"] === "string" ? flags["name"] : defaultName(serverUrl),
24
+ scope: (typeof flags["scope"] === "string" ? flags["scope"] : "user"),
25
+ path: typeof flags["path"] === "string" ? flags["path"] : undefined,
26
+ openClient: Boolean(flags["open-client"]),
27
+ overwrite: Boolean(flags["overwrite"]),
28
+ dryRun: Boolean(flags["dry-run"])
29
+ };
30
+ const result = client === "codex"
31
+ ? await installCodex(request)
32
+ : client === "cursor"
33
+ ? await installCursor(request)
34
+ : client === "vscode"
35
+ ? await installVsCode(request)
36
+ : await installWindsurf(request);
37
+ if (!request.dryRun) {
38
+ const manifest = new InstallManifestStore();
39
+ await manifest.upsert({
40
+ client: result.client,
41
+ scope: result.scope,
42
+ name: result.name,
43
+ location: result.location,
44
+ method: result.method,
45
+ serverUrl,
46
+ installedAt: new Date().toISOString()
47
+ });
48
+ }
49
+ context.output.success({
50
+ schemaVersion: 1,
51
+ ...result
52
+ }, [
53
+ `Client: ${result.client}`,
54
+ `Scope: ${result.scope}`,
55
+ `Method: ${result.method}`,
56
+ `Location: ${result.location}`,
57
+ `Changed: ${result.changed ? "yes" : "no"}`,
58
+ `Managed: yes`,
59
+ result.nextStep,
60
+ ...(result.notes || []),
61
+ `${result.client === "codex" ? "Codex" : result.client === "vscode" ? "VS Code" : result.client === "cursor" ? "Cursor" : "Windsurf"} config install only. CLI auth and installed-client auth remain separate.`
62
+ ]);
63
+ }
@@ -0,0 +1,41 @@
1
+ import { parseFlags } from "../cli/parse.js";
2
+ export async function runLoginCommand(args, context) {
3
+ const { flags } = parseFlags(args, {
4
+ valueFlags: ["scope", "registration", "browser", "timeout-sec"]
5
+ });
6
+ let authorizationUrl;
7
+ let callbackUrl;
8
+ const result = await context.tokenManager.login(context.globalOptions, {
9
+ scope: typeof flags["scope"] === "string" ? flags["scope"] : undefined,
10
+ registrationMode: typeof flags["registration"] === "string" ? flags["registration"] : undefined,
11
+ browserMode: typeof flags["browser"] === "string" ? flags["browser"] : undefined,
12
+ timeoutSec: typeof flags["timeout-sec"] === "string" ? Number(flags["timeout-sec"]) : undefined,
13
+ onLoopbackReady: (url) => {
14
+ callbackUrl = url;
15
+ if (!context.globalOptions.json) {
16
+ context.output.info(`Waiting for OAuth callback on ${url}`);
17
+ context.output.info("Keep this terminal open until login completes.");
18
+ }
19
+ },
20
+ onAuthorizationUrl: (url) => {
21
+ authorizationUrl = url;
22
+ if (!context.globalOptions.json) {
23
+ context.output.info(url);
24
+ }
25
+ }
26
+ });
27
+ context.output.success({
28
+ schemaVersion: 1,
29
+ ...result,
30
+ ...(callbackUrl ? { callbackUrl } : {}),
31
+ ...(context.globalOptions.json && authorizationUrl ? { authorizationUrl } : {})
32
+ }, [
33
+ `Profile: ${result.profile}`,
34
+ `Server URL: ${result.serverUrl}`,
35
+ `Authorization server: ${result.authorizationServerIssuer || "discovered"}`,
36
+ `Registration mode: ${result.registrationMode}`,
37
+ `Expires at: ${result.expiresAt || "unknown"}`,
38
+ `Refresh token: ${result.hasRefreshToken ? "available" : "not issued"}`,
39
+ "CLI login does not log Codex, editors, ChatGPT, or other MCP clients into MCP. Each client owns its own OAuth session."
40
+ ]);
41
+ }
@@ -0,0 +1,26 @@
1
+ import { parseFlags } from "../cli/parse.js";
2
+ export async function runLogoutCommand(args, context) {
3
+ const { flags } = parseFlags(args, {
4
+ booleanFlags: ["all", "no-revoke"]
5
+ });
6
+ const config = await context.configStore.load();
7
+ const targetProfiles = flags["all"]
8
+ ? Object.keys(config.profiles)
9
+ : [context.globalOptions.profile || config.currentProfile];
10
+ const results = [];
11
+ for (const profileName of targetProfiles) {
12
+ results.push({
13
+ profile: profileName,
14
+ ...(await context.tokenManager.logout(profileName, {
15
+ noRevoke: Boolean(flags["no-revoke"])
16
+ }))
17
+ });
18
+ }
19
+ context.output.success({
20
+ schemaVersion: 1,
21
+ results
22
+ }, [
23
+ ...results.map((result) => `${result.profile}: local tokens ${result.localTokensDeleted ? "deleted" : "not present"}, revocation ${result.revocationAttempted ? (result.revocationConfirmed ? "confirmed" : "attempted") : "skipped"}`),
24
+ "Editor registrations are unchanged."
25
+ ]);
26
+ }
@@ -0,0 +1,82 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
3
+ import { parseFlags } from "../cli/parse.js";
4
+ import { renderToolResult } from "../core/renderers.js";
5
+ import { callToolWithRetry } from "./call.js";
6
+ const PULSE_SETUP_TOOL_NAME = "get_pulse_setup_guidance";
7
+ const PULSE_DESCRIPTOR_SOURCE_OF_TRUTH = "PulseDescriptor";
8
+ function readStructuredContent(result) {
9
+ const structuredContent = result && typeof result === "object"
10
+ ? (result.structuredContent ?? result)
11
+ : undefined;
12
+ return structuredContent && typeof structuredContent === "object" && !Array.isArray(structuredContent)
13
+ ? structuredContent
14
+ : undefined;
15
+ }
16
+ function assertDescriptorSetupGuidance(result, options) {
17
+ const structuredContent = readStructuredContent(result);
18
+ const descriptorMetadata = structuredContent
19
+ ? structuredContent["descriptorMetadata"]
20
+ : undefined;
21
+ const metadata = descriptorMetadata && typeof descriptorMetadata === "object"
22
+ ? descriptorMetadata
23
+ : undefined;
24
+ if (metadata?.sourceOfTruth !== PULSE_DESCRIPTOR_SOURCE_OF_TRUTH ||
25
+ metadata.apiVersion !== "pulse/v1") {
26
+ throw new CliError("mcp.pulse_setup_contract", "Pulse setup guidance response is missing PulseDescriptor metadata.", EXIT_CODES.protocol, {
27
+ nextStep: "Run vibecodr doctor to inspect the configured MCP server, then retry."
28
+ });
29
+ }
30
+ if (options?.expectsDescriptorSetup) {
31
+ const descriptorEvaluation = structuredContent?.["descriptorEvaluation"] && typeof structuredContent["descriptorEvaluation"] === "object"
32
+ ? structuredContent["descriptorEvaluation"]
33
+ : undefined;
34
+ if (descriptorEvaluation?.guidanceSource !== "descriptor_setup") {
35
+ throw new CliError("mcp.pulse_setup_contract", "Pulse setup guidance response did not evaluate the supplied descriptorSetup.", EXIT_CODES.protocol, {
36
+ nextStep: "Verify the MCP server supports get_pulse_setup_guidance descriptorSetup projection."
37
+ });
38
+ }
39
+ }
40
+ }
41
+ function parseDescriptorSetup(raw, source) {
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(raw);
45
+ }
46
+ catch (error) {
47
+ throw new CliError("usage.invalid_descriptor_setup", `${source} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.usage);
48
+ }
49
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
50
+ throw new CliError("usage.invalid_descriptor_setup", `${source} must be a PulseDescriptorSetupProjection object.`, EXIT_CODES.usage);
51
+ }
52
+ return parsed;
53
+ }
54
+ async function parsePulseSetupInput(args) {
55
+ const { flags } = parseFlags(args, {
56
+ valueFlags: ["descriptor-setup-json", "descriptor-setup-file"]
57
+ });
58
+ const hasInline = typeof flags["descriptor-setup-json"] === "string";
59
+ const hasFile = typeof flags["descriptor-setup-file"] === "string";
60
+ if (hasInline && hasFile) {
61
+ throw new CliError("usage.duplicate_descriptor_setup", "Use either --descriptor-setup-json or --descriptor-setup-file, not both.", EXIT_CODES.usage);
62
+ }
63
+ if (typeof flags["descriptor-setup-json"] === "string") {
64
+ return { descriptorSetup: parseDescriptorSetup(flags["descriptor-setup-json"], "--descriptor-setup-json") };
65
+ }
66
+ if (typeof flags["descriptor-setup-file"] === "string") {
67
+ const raw = await readFile(flags["descriptor-setup-file"], "utf8");
68
+ return { descriptorSetup: parseDescriptorSetup(raw, "--descriptor-setup-file") };
69
+ }
70
+ return {};
71
+ }
72
+ export async function runPulseSetupCommand(args, context) {
73
+ const input = await parsePulseSetupInput(args);
74
+ const { result } = await callToolWithRetry(context, PULSE_SETUP_TOOL_NAME, input, true);
75
+ assertDescriptorSetupGuidance(result, { expectsDescriptorSetup: Boolean(input["descriptorSetup"]) });
76
+ context.output.success({
77
+ schemaVersion: 1,
78
+ tool: PULSE_SETUP_TOOL_NAME,
79
+ arguments: input,
80
+ result
81
+ }, [renderToolResult(result)]);
82
+ }
@@ -0,0 +1,64 @@
1
+ import { access } from "node:fs/promises";
2
+ import { parseFlags } from "../cli/parse.js";
3
+ import { InstallManifestStore } from "../storage/install-manifest.js";
4
+ async function inspectInstall(entry) {
5
+ if (entry.method !== "file") {
6
+ return {
7
+ ...entry,
8
+ status: "external"
9
+ };
10
+ }
11
+ try {
12
+ await access(entry.location);
13
+ return {
14
+ ...entry,
15
+ status: "configured"
16
+ };
17
+ }
18
+ catch {
19
+ return {
20
+ ...entry,
21
+ status: "missing"
22
+ };
23
+ }
24
+ }
25
+ export async function runStatusCommand(args, context) {
26
+ const { flags } = parseFlags(args, {
27
+ booleanFlags: ["probe", "show-installs"]
28
+ });
29
+ const { profileName, profile, serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
30
+ const session = await context.tokenManager.getSession(profileName);
31
+ const sessionState = context.tokenManager.sessionState(session);
32
+ const installs = flags["show-installs"]
33
+ ? await Promise.all((await new InstallManifestStore().find(() => true)).map((entry) => inspectInstall(entry)))
34
+ : [];
35
+ let probe;
36
+ if (flags["probe"]) {
37
+ const discovery = await context.tokenManager.discover(serverUrl);
38
+ probe = {
39
+ authorizationServerUrl: discovery.authorizationServerUrl,
40
+ pkceS256: Boolean(discovery.authorizationServerMetadata?.code_challenge_methods_supported?.includes("S256"))
41
+ };
42
+ }
43
+ context.output.success({
44
+ schemaVersion: 1,
45
+ profile: profileName,
46
+ serverUrl,
47
+ sessionState,
48
+ registrationMode: session?.registrationMode || profile.registrationMode,
49
+ expiresAt: session?.expiresAt,
50
+ installs,
51
+ ...(probe ? { probe } : {})
52
+ }, [
53
+ `Profile: ${profileName}`,
54
+ `Server URL: ${serverUrl}`,
55
+ `Session state: ${sessionState}`,
56
+ `Registration mode: ${session?.registrationMode || profile.registrationMode}`,
57
+ `Expires at: ${session?.expiresAt || "not logged in"}`,
58
+ ...(flags["show-installs"] ? [`Managed installs: ${installs.length}`] : []),
59
+ ...(flags["show-installs"]
60
+ ? installs.map((install) => `Install: ${install.client} ${install.scope} ${install.location} [${install.status}]`)
61
+ : []),
62
+ ...(probe ? [`Authorization server: ${String(probe["authorizationServerUrl"])}`, `PKCE S256: ${probe["pkceS256"] ? "yes" : "no"}`] : [])
63
+ ]);
64
+ }
@@ -0,0 +1,82 @@
1
+ import { formatJson, summarizeToolSchema } from "../core/renderers.js";
2
+ import { parseFlags } from "../cli/parse.js";
3
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
4
+ function challengedScope(error) {
5
+ if (!error.debugDetails || typeof error.debugDetails !== "object")
6
+ return undefined;
7
+ const scope = error.debugDetails["scope"];
8
+ return typeof scope === "string" && scope.trim() ? scope : undefined;
9
+ }
10
+ async function loadToolsWithRetry(context, allowLogin) {
11
+ const { profileName, serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
12
+ const existingSession = await context.tokenManager.getSession(profileName);
13
+ try {
14
+ return {
15
+ tools: await context.runtimeClient.listTools(serverUrl, existingSession?.accessToken),
16
+ ...(existingSession ? { session: existingSession } : {})
17
+ };
18
+ }
19
+ catch (error) {
20
+ if (!(error instanceof CliError) || !["auth.required", "auth.insufficient_scope"].includes(error.machineCode))
21
+ throw error;
22
+ if (error.machineCode === "auth.required" && existingSession?.refreshToken) {
23
+ const refreshed = await context.tokenManager.refresh(profileName, existingSession);
24
+ return {
25
+ tools: await context.runtimeClient.listTools(serverUrl, refreshed.session.accessToken),
26
+ session: refreshed.session
27
+ };
28
+ }
29
+ if (allowLogin && !context.globalOptions.nonInteractive) {
30
+ await context.tokenManager.login(context.globalOptions, {
31
+ scope: challengedScope(error)
32
+ });
33
+ const nextSession = await context.tokenManager.getSession(profileName);
34
+ return {
35
+ tools: await context.runtimeClient.listTools(serverUrl, nextSession?.accessToken),
36
+ ...(nextSession ? { session: nextSession } : {})
37
+ };
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+ export async function runToolsCommand(args, context) {
43
+ const { flags, positionals } = parseFlags(args, {
44
+ valueFlags: ["search"],
45
+ booleanFlags: ["schema", "no-login"]
46
+ });
47
+ const toolName = positionals[0];
48
+ const { tools, session } = await loadToolsWithRetry(context, !flags["no-login"]);
49
+ const serverUrl = session?.serverUrl || (await context.tokenManager.resolveProfile(context.globalOptions)).serverUrl;
50
+ const sortedTools = tools
51
+ .sort((left, right) => left.name.localeCompare(right.name));
52
+ const search = typeof flags["search"] === "string" ? flags["search"].toLowerCase() : "";
53
+ const filtered = search
54
+ ? sortedTools.filter((tool) => tool.name.toLowerCase().includes(search) || (tool.description || "").toLowerCase().includes(search))
55
+ : sortedTools;
56
+ if (!toolName) {
57
+ context.output.success({
58
+ schemaVersion: 1,
59
+ serverUrl,
60
+ toolCount: filtered.length,
61
+ tools: filtered
62
+ }, filtered.map((tool) => `${tool.name}${tool.description ? `: ${tool.description}` : ""}`));
63
+ return;
64
+ }
65
+ const tool = sortedTools.find((item) => item.name === toolName);
66
+ if (!tool) {
67
+ throw new CliError("tool.not_found", `Tool not found: ${toolName}`, EXIT_CODES.toolFailed);
68
+ }
69
+ const summary = summarizeToolSchema(tool.inputSchema);
70
+ context.output.success({
71
+ schemaVersion: 1,
72
+ tool
73
+ }, [
74
+ `Name: ${tool.name}`,
75
+ `Description: ${tool.description || ""}`,
76
+ `Required: ${summary.required.join(", ") || "none"}`,
77
+ `Optional: ${summary.optional.join(", ") || "none"}`,
78
+ "Input skeleton:",
79
+ formatJson(summary.skeleton),
80
+ ...(flags["schema"] ? ["Schema:", formatJson(tool.inputSchema)] : [])
81
+ ]);
82
+ }
@@ -0,0 +1,55 @@
1
+ import { parseFlags } from "../cli/parse.js";
2
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
3
+ import { InstallManifestStore } from "../storage/install-manifest.js";
4
+ import { uninstallCodex } from "../clients/codex.js";
5
+ import { uninstallCursor } from "../clients/cursor.js";
6
+ import { uninstallVsCode } from "../clients/vscode.js";
7
+ import { uninstallWindsurf } from "../clients/windsurf.js";
8
+ export async function runUninstallCommand(args, context) {
9
+ const client = args[0];
10
+ if (!client || !["codex", "cursor", "vscode", "windsurf"].includes(client)) {
11
+ throw new CliError("usage.uninstall_client", "Usage: uninstall <codex|cursor|vscode|windsurf> [options]", EXIT_CODES.usage);
12
+ }
13
+ const { flags } = parseFlags(args.slice(1), {
14
+ valueFlags: ["scope", "path", "name"],
15
+ booleanFlags: ["dry-run"]
16
+ });
17
+ const { serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
18
+ const scope = (typeof flags["scope"] === "string" ? flags["scope"] : "user");
19
+ const name = typeof flags["name"] === "string" ? flags["name"] : (serverUrl.includes("staging") ? "vibecodr-staging" : "vibecodr");
20
+ const manifest = new InstallManifestStore();
21
+ const managedEntries = await manifest.find((entry) => entry.client === client && entry.scope === scope && entry.name === name);
22
+ const managed = managedEntries[0];
23
+ if (!managed) {
24
+ throw new CliError("install.not_managed", `No CLI-managed ${client} install entry was found for ${name}.`, EXIT_CODES.installConflict, {
25
+ nextStep: "Only CLI-managed installs can be safely removed."
26
+ });
27
+ }
28
+ const request = {
29
+ serverUrl,
30
+ name,
31
+ scope,
32
+ path: typeof flags["path"] === "string" ? flags["path"] : undefined,
33
+ dryRun: Boolean(flags["dry-run"])
34
+ };
35
+ const result = client === "codex"
36
+ ? await uninstallCodex(request, managed.location)
37
+ : client === "cursor"
38
+ ? await uninstallCursor(request, managed.location)
39
+ : client === "vscode"
40
+ ? await uninstallVsCode(request, managed.location, managed.method === "uri" ? "cli" : managed.method)
41
+ : await uninstallWindsurf(request, managed.location);
42
+ if (!request.dryRun && result.changed) {
43
+ await manifest.remove((entry) => entry.client === client && entry.scope === scope && entry.name === name && entry.location === managed.location);
44
+ }
45
+ context.output.success({
46
+ schemaVersion: 1,
47
+ ...result
48
+ }, [
49
+ `Client: ${result.client}`,
50
+ `Scope: ${result.scope}`,
51
+ `Location: ${result.location}`,
52
+ `Changed: ${result.changed ? "yes" : "no"}`,
53
+ result.nextStep
54
+ ]);
55
+ }