@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,84 @@
1
+ import { CliError, EXIT_CODES } from "./errors.js";
2
+ function normalizeFlagName(flag) {
3
+ return flag.replace(/^--/, "");
4
+ }
5
+ export function parseFlags(args, options) {
6
+ const valueFlags = new Set(options.valueFlags || []);
7
+ const booleanFlags = new Set(options.booleanFlags || []);
8
+ const flags = {};
9
+ const positionals = [];
10
+ for (let index = 0; index < args.length; index += 1) {
11
+ const token = args[index];
12
+ if (token === undefined)
13
+ continue;
14
+ if (!token.startsWith("--")) {
15
+ positionals.push(token);
16
+ continue;
17
+ }
18
+ const name = normalizeFlagName(token);
19
+ if (booleanFlags.has(name)) {
20
+ flags[name] = true;
21
+ continue;
22
+ }
23
+ if (valueFlags.has(name)) {
24
+ const value = args[index + 1];
25
+ if (!value || value.startsWith("--")) {
26
+ throw new CliError("usage.missing_flag_value", `Missing value for --${name}.`, EXIT_CODES.usage);
27
+ }
28
+ flags[name] = value;
29
+ index += 1;
30
+ continue;
31
+ }
32
+ throw new CliError("usage.unknown_flag", `Unknown flag: ${token}`, EXIT_CODES.usage);
33
+ }
34
+ return { flags, positionals };
35
+ }
36
+ export function parseGlobalOptions(argv) {
37
+ const globalOptions = {
38
+ profile: "default",
39
+ json: false,
40
+ verbose: false,
41
+ nonInteractive: false
42
+ };
43
+ let command;
44
+ const commandArgs = [];
45
+ for (let index = 0; index < argv.length; index += 1) {
46
+ const token = argv[index];
47
+ if (token === undefined)
48
+ continue;
49
+ if (token === "--profile") {
50
+ const value = argv[index + 1];
51
+ if (!value)
52
+ throw new CliError("usage.missing_profile", "Missing value for --profile.", EXIT_CODES.usage);
53
+ globalOptions.profile = value;
54
+ index += 1;
55
+ continue;
56
+ }
57
+ if (token === "--server-url") {
58
+ const value = argv[index + 1];
59
+ if (!value)
60
+ throw new CliError("usage.missing_server_url", "Missing value for --server-url.", EXIT_CODES.usage);
61
+ globalOptions.serverUrl = value;
62
+ index += 1;
63
+ continue;
64
+ }
65
+ if (token === "--json") {
66
+ globalOptions.json = true;
67
+ continue;
68
+ }
69
+ if (token === "--verbose") {
70
+ globalOptions.verbose = true;
71
+ continue;
72
+ }
73
+ if (token === "--non-interactive") {
74
+ globalOptions.nonInteractive = true;
75
+ continue;
76
+ }
77
+ if (!command && !token.startsWith("--")) {
78
+ command = token;
79
+ continue;
80
+ }
81
+ commandArgs.push(token);
82
+ }
83
+ return { ...(command ? { command } : {}), commandArgs, globalOptions };
84
+ }
@@ -0,0 +1,30 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
3
+ import { writeFileWithBackup } from "../storage/file-lock.js";
4
+ export async function readJsonFile(path, fallback) {
5
+ try {
6
+ const raw = await readFile(path, "utf8");
7
+ try {
8
+ return JSON.parse(raw);
9
+ }
10
+ catch (error) {
11
+ throw new CliError("install.config_parse", `Existing config at ${path} is not valid JSON.`, EXIT_CODES.installConflict, {
12
+ cause: error,
13
+ nextStep: "Repair the existing config file before retrying."
14
+ });
15
+ }
16
+ }
17
+ catch (error) {
18
+ if (error.code === "ENOENT")
19
+ return fallback;
20
+ throw error;
21
+ }
22
+ }
23
+ export async function writeTextFileAtomic(path, content) {
24
+ await writeFileWithBackup(path, content);
25
+ }
26
+ export function requireScope(actual, supported) {
27
+ if (!supported.includes(actual)) {
28
+ throw new CliError("install.unsupported_scope", `Scope ${actual} is not supported for this client.`, EXIT_CODES.unsupportedClient);
29
+ }
30
+ }
@@ -0,0 +1,136 @@
1
+ import * as TOML from "@iarna/toml";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { codexConfigPath } from "../platform/paths.js";
5
+ import { requireScope, writeTextFileAtomic } from "./base.js";
6
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
7
+ import { commandExists, runCommand } from "../platform/exec.js";
8
+ async function readCodexConfig(path) {
9
+ try {
10
+ const raw = await readFile(path, "utf8");
11
+ try {
12
+ return TOML.parse(raw);
13
+ }
14
+ catch (error) {
15
+ throw new CliError("install.config_parse", `Existing Codex config at ${path} is not valid TOML.`, EXIT_CODES.installConflict, {
16
+ cause: error,
17
+ nextStep: "Repair the existing Codex config before retrying."
18
+ });
19
+ }
20
+ }
21
+ catch (error) {
22
+ if (error.code === "ENOENT")
23
+ return {};
24
+ throw error;
25
+ }
26
+ }
27
+ function resolvedCodexConfigPath(request) {
28
+ return request.path ? join(request.path, "config.toml") : codexConfigPath();
29
+ }
30
+ export async function installCodex(request) {
31
+ requireScope(request.scope, ["user"]);
32
+ if (!request.dryRun && !request.path && commandExists("codex")) {
33
+ try {
34
+ await runCommand("codex", ["mcp", "add", request.name, "--url", request.serverUrl]);
35
+ return {
36
+ client: "codex",
37
+ scope: request.scope,
38
+ name: request.name,
39
+ method: "cli",
40
+ changed: true,
41
+ location: "codex mcp add",
42
+ managed: true,
43
+ nextStep: "Codex is configured. Codex will handle its own OAuth flow on first protected use."
44
+ };
45
+ }
46
+ catch {
47
+ // fall through to TOML merge
48
+ }
49
+ }
50
+ const location = resolvedCodexConfigPath(request);
51
+ const current = await readCodexConfig(location);
52
+ const servers = (current.mcp_servers || {});
53
+ const existing = servers[request.name];
54
+ if (existing?.url && existing.url !== request.serverUrl && !request.overwrite) {
55
+ throw new CliError("install.conflict", `Codex already has an MCP entry named ${request.name} with a different URL.`, EXIT_CODES.installConflict, {
56
+ nextStep: "Retry with --overwrite or choose a different --name."
57
+ });
58
+ }
59
+ const next = {
60
+ ...current,
61
+ mcp_servers: {
62
+ ...servers,
63
+ [request.name]: {
64
+ url: request.serverUrl
65
+ }
66
+ }
67
+ };
68
+ const changed = JSON.stringify(current) !== JSON.stringify(next);
69
+ if (!request.dryRun && changed) {
70
+ await writeTextFileAtomic(location, TOML.stringify(next));
71
+ }
72
+ return {
73
+ client: "codex",
74
+ scope: request.scope,
75
+ name: request.name,
76
+ method: "file",
77
+ changed,
78
+ location,
79
+ managed: true,
80
+ nextStep: "Codex is configured. Codex will handle its own OAuth flow on first protected use."
81
+ };
82
+ }
83
+ export async function uninstallCodex(request, managedLocation) {
84
+ requireScope(request.scope, ["user"]);
85
+ if (!request.dryRun && !request.path && !managedLocation && commandExists("codex")) {
86
+ try {
87
+ await runCommand("codex", ["mcp", "remove", request.name]);
88
+ return {
89
+ client: "codex",
90
+ scope: request.scope,
91
+ name: request.name,
92
+ method: "cli",
93
+ changed: true,
94
+ location: "codex mcp remove",
95
+ managed: true,
96
+ nextStep: "Codex config was updated. Codex-owned auth state is unchanged."
97
+ };
98
+ }
99
+ catch {
100
+ // fall through to file removal
101
+ }
102
+ }
103
+ const location = managedLocation && managedLocation !== "codex mcp add" ? managedLocation : resolvedCodexConfigPath(request);
104
+ const current = await readCodexConfig(location);
105
+ const servers = { ...(current.mcp_servers || {}) };
106
+ if (!servers[request.name]) {
107
+ return {
108
+ client: "codex",
109
+ scope: request.scope,
110
+ name: request.name,
111
+ method: "file",
112
+ changed: false,
113
+ location,
114
+ managed: true,
115
+ nextStep: "No managed Codex entry was present."
116
+ };
117
+ }
118
+ delete servers[request.name];
119
+ const next = {
120
+ ...current,
121
+ mcp_servers: Object.keys(servers).length ? servers : undefined
122
+ };
123
+ if (!request.dryRun) {
124
+ await writeTextFileAtomic(location, TOML.stringify(next));
125
+ }
126
+ return {
127
+ client: "codex",
128
+ scope: request.scope,
129
+ name: request.name,
130
+ method: "file",
131
+ changed: true,
132
+ location,
133
+ managed: true,
134
+ nextStep: "Codex config was updated. Codex-owned auth state is unchanged."
135
+ };
136
+ }
@@ -0,0 +1,91 @@
1
+ import { join } from "node:path";
2
+ import { readJsonFile, requireScope, writeTextFileAtomic } from "./base.js";
3
+ import { cursorUserConfigPath, projectCursorConfigPath } from "../platform/paths.js";
4
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
5
+ import { openExternalUrl } from "../platform/browser.js";
6
+ function configPath(scope, rootPath) {
7
+ if (scope === "user") {
8
+ return rootPath ? join(rootPath, "mcp.json") : cursorUserConfigPath();
9
+ }
10
+ return projectCursorConfigPath(rootPath || process.cwd());
11
+ }
12
+ export async function installCursor(request) {
13
+ requireScope(request.scope, ["user", "project"]);
14
+ const location = configPath(request.scope, request.path);
15
+ const current = await readJsonFile(location, {});
16
+ const next = {
17
+ ...current,
18
+ mcpServers: {
19
+ ...(current.mcpServers || {}),
20
+ [request.name]: {
21
+ type: "http",
22
+ url: request.serverUrl
23
+ }
24
+ }
25
+ };
26
+ const existing = current.mcpServers?.[request.name];
27
+ if (existing && existing.url !== request.serverUrl && !request.overwrite) {
28
+ throw new CliError("install.conflict", `Cursor already has an MCP entry named ${request.name} with a different URL.`, EXIT_CODES.installConflict, {
29
+ nextStep: "Retry with --overwrite or choose a different --name."
30
+ });
31
+ }
32
+ const changed = JSON.stringify(current) !== JSON.stringify(next);
33
+ if (!request.dryRun && changed) {
34
+ await writeTextFileAtomic(location, JSON.stringify(next, null, 2) + "\n");
35
+ }
36
+ if (request.openClient && !request.dryRun) {
37
+ const deeplink = new URL("cursor://anysphere.cursor-deeplink/mcp/install");
38
+ deeplink.searchParams.set("name", request.name);
39
+ deeplink.searchParams.set("config", JSON.stringify({
40
+ url: request.serverUrl
41
+ }));
42
+ await openExternalUrl(deeplink.toString());
43
+ }
44
+ return {
45
+ client: "cursor",
46
+ scope: request.scope,
47
+ name: request.name,
48
+ method: "file",
49
+ changed,
50
+ location,
51
+ managed: true,
52
+ nextStep: "Use the server in Cursor to trigger Cursor-owned OAuth."
53
+ };
54
+ }
55
+ export async function uninstallCursor(request, managedLocation) {
56
+ requireScope(request.scope, ["user", "project"]);
57
+ const location = managedLocation || configPath(request.scope, request.path);
58
+ const current = await readJsonFile(location, {});
59
+ const existing = current.mcpServers?.[request.name];
60
+ if (!existing) {
61
+ return {
62
+ client: "cursor",
63
+ scope: request.scope,
64
+ name: request.name,
65
+ method: "file",
66
+ changed: false,
67
+ location,
68
+ managed: true,
69
+ nextStep: "No managed Cursor entry was present."
70
+ };
71
+ }
72
+ const nextServers = { ...(current.mcpServers || {}) };
73
+ delete nextServers[request.name];
74
+ const next = {
75
+ ...current,
76
+ mcpServers: Object.keys(nextServers).length ? nextServers : undefined
77
+ };
78
+ if (!request.dryRun) {
79
+ await writeTextFileAtomic(location, JSON.stringify(next, null, 2) + "\n");
80
+ }
81
+ return {
82
+ client: "cursor",
83
+ scope: request.scope,
84
+ name: request.name,
85
+ method: "file",
86
+ changed: true,
87
+ location,
88
+ managed: true,
89
+ nextStep: "Cursor config was updated. Cursor-owned auth state is unchanged."
90
+ };
91
+ }
@@ -0,0 +1,138 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readJsonFile, requireScope, writeTextFileAtomic } from "./base.js";
3
+ import { vscodeWorkspaceConfigPath } from "../platform/paths.js";
4
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
5
+ import { openExternalUrl } from "../platform/browser.js";
6
+ import { commandExists } from "../platform/exec.js";
7
+ function runCli(command, args) {
8
+ return new Promise((resolve, reject) => {
9
+ const child = spawn(command, args, {
10
+ stdio: "ignore"
11
+ });
12
+ child.once("error", reject);
13
+ child.once("exit", (code) => {
14
+ if (code === 0)
15
+ resolve();
16
+ else
17
+ reject(new Error(`${command} exited with code ${code}`));
18
+ });
19
+ });
20
+ }
21
+ export async function installVsCode(request) {
22
+ requireScope(request.scope, ["user", "project"]);
23
+ if (request.scope === "user") {
24
+ const payload = JSON.stringify({
25
+ name: request.name,
26
+ type: "http",
27
+ url: request.serverUrl
28
+ });
29
+ let method = "cli";
30
+ if (!request.dryRun) {
31
+ await runCli("code", ["--add-mcp", payload]).catch((error) => {
32
+ if (request.openClient) {
33
+ const uri = `vscode:mcp/install?${encodeURIComponent(payload)}`;
34
+ method = "uri";
35
+ return openExternalUrl(uri);
36
+ }
37
+ throw new CliError("install.vscode_cli_failed", "VS Code CLI install failed.", EXIT_CODES.unsupportedClient, {
38
+ cause: error,
39
+ nextStep: "Ensure the `code` CLI is installed, or retry with --open-client."
40
+ });
41
+ });
42
+ }
43
+ else if (request.openClient && !commandExists("code")) {
44
+ method = "uri";
45
+ }
46
+ return {
47
+ client: "vscode",
48
+ scope: request.scope,
49
+ name: request.name,
50
+ method,
51
+ changed: true,
52
+ location: method === "cli" ? "code --add-mcp" : "vscode:mcp/install",
53
+ managed: true,
54
+ nextStep: "Use the server in VS Code to trigger VS Code-owned OAuth."
55
+ };
56
+ }
57
+ const location = vscodeWorkspaceConfigPath(request.path || process.cwd());
58
+ const current = await readJsonFile(location, {});
59
+ const next = {
60
+ ...current,
61
+ servers: {
62
+ ...(current.servers || {}),
63
+ [request.name]: {
64
+ type: "http",
65
+ url: request.serverUrl
66
+ }
67
+ }
68
+ };
69
+ const existing = current.servers?.[request.name];
70
+ if (existing && existing.url !== request.serverUrl && !request.overwrite) {
71
+ throw new CliError("install.conflict", `VS Code already has an MCP entry named ${request.name} with a different URL.`, EXIT_CODES.installConflict, {
72
+ nextStep: "Retry with --overwrite or choose a different --name."
73
+ });
74
+ }
75
+ const changed = JSON.stringify(current) !== JSON.stringify(next);
76
+ if (!request.dryRun && changed) {
77
+ await writeTextFileAtomic(location, JSON.stringify(next, null, 2) + "\n");
78
+ }
79
+ if (request.openClient && !request.dryRun) {
80
+ const payload = JSON.stringify({
81
+ name: request.name,
82
+ type: "http",
83
+ url: request.serverUrl
84
+ });
85
+ await openExternalUrl(`vscode:mcp/install?${encodeURIComponent(payload)}`);
86
+ }
87
+ return {
88
+ client: "vscode",
89
+ scope: request.scope,
90
+ name: request.name,
91
+ method: "file",
92
+ changed,
93
+ location,
94
+ managed: true,
95
+ nextStep: "Use the server in VS Code to trigger VS Code-owned OAuth."
96
+ };
97
+ }
98
+ export async function uninstallVsCode(request, managedLocation, managedMethod) {
99
+ requireScope(request.scope, ["user", "project"]);
100
+ if (request.scope === "user" || managedMethod === "cli") {
101
+ throw new CliError("install.conflict", "User-scope VS Code uninstall is not automated yet with documented surfaces.", EXIT_CODES.installConflict, {
102
+ nextStep: "Remove the server from VS Code MCP settings, then rerun uninstall when a documented removal surface exists."
103
+ });
104
+ }
105
+ const location = managedLocation || vscodeWorkspaceConfigPath(request.path || process.cwd());
106
+ const current = await readJsonFile(location, {});
107
+ if (!current.servers?.[request.name]) {
108
+ return {
109
+ client: "vscode",
110
+ scope: request.scope,
111
+ name: request.name,
112
+ method: "file",
113
+ changed: false,
114
+ location,
115
+ managed: true,
116
+ nextStep: "No managed VS Code workspace entry was present."
117
+ };
118
+ }
119
+ const nextServers = { ...(current.servers || {}) };
120
+ delete nextServers[request.name];
121
+ const next = {
122
+ ...current,
123
+ servers: Object.keys(nextServers).length ? nextServers : undefined
124
+ };
125
+ if (!request.dryRun) {
126
+ await writeTextFileAtomic(location, JSON.stringify(next, null, 2) + "\n");
127
+ }
128
+ return {
129
+ client: "vscode",
130
+ scope: request.scope,
131
+ name: request.name,
132
+ method: "file",
133
+ changed: true,
134
+ location,
135
+ managed: true,
136
+ nextStep: "VS Code workspace config was updated. VS Code-owned auth state is unchanged."
137
+ };
138
+ }
@@ -0,0 +1,81 @@
1
+ import { join } from "node:path";
2
+ import { readJsonFile, requireScope, writeTextFileAtomic } from "./base.js";
3
+ import { windsurfLegacyConfigPath, windsurfUserConfigPath } from "../platform/paths.js";
4
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
5
+ export async function installWindsurf(request) {
6
+ requireScope(request.scope, ["user"]);
7
+ const location = request.path ? join(request.path, "mcp_config.json") : windsurfUserConfigPath();
8
+ const current = await readJsonFile(location, {});
9
+ const next = {
10
+ ...current,
11
+ mcpServers: {
12
+ ...(current.mcpServers || {}),
13
+ [request.name]: {
14
+ serverUrl: request.serverUrl
15
+ }
16
+ }
17
+ };
18
+ const existing = current.mcpServers?.[request.name];
19
+ if (existing && existing.serverUrl !== request.serverUrl && !request.overwrite) {
20
+ throw new CliError("install.conflict", `Windsurf already has an MCP entry named ${request.name} with a different URL.`, EXIT_CODES.installConflict, {
21
+ nextStep: "Retry with --overwrite or choose a different --name."
22
+ });
23
+ }
24
+ const changed = JSON.stringify(current) !== JSON.stringify(next);
25
+ if (!request.dryRun && changed) {
26
+ await writeTextFileAtomic(location, JSON.stringify(next, null, 2) + "\n");
27
+ }
28
+ const notes = [];
29
+ const legacyPath = windsurfLegacyConfigPath();
30
+ const legacyExists = await readJsonFile(legacyPath, null).catch(() => null);
31
+ if (legacyExists && typeof legacyExists === "object") {
32
+ notes.push(`Legacy Windsurf plugin config detected at ${legacyPath}.`);
33
+ }
34
+ return {
35
+ client: "windsurf",
36
+ scope: request.scope,
37
+ name: request.name,
38
+ method: "file",
39
+ changed,
40
+ location,
41
+ managed: true,
42
+ nextStep: "Open Windsurf and refresh MCPs if needed.",
43
+ ...(notes.length ? { notes } : {})
44
+ };
45
+ }
46
+ export async function uninstallWindsurf(request, managedLocation) {
47
+ requireScope(request.scope, ["user"]);
48
+ const location = managedLocation || (request.path ? join(request.path, "mcp_config.json") : windsurfUserConfigPath());
49
+ const current = await readJsonFile(location, {});
50
+ if (!current.mcpServers?.[request.name]) {
51
+ return {
52
+ client: "windsurf",
53
+ scope: request.scope,
54
+ name: request.name,
55
+ method: "file",
56
+ changed: false,
57
+ location,
58
+ managed: true,
59
+ nextStep: "No managed Windsurf entry was present."
60
+ };
61
+ }
62
+ const nextServers = { ...(current.mcpServers || {}) };
63
+ delete nextServers[request.name];
64
+ const next = {
65
+ ...current,
66
+ mcpServers: Object.keys(nextServers).length ? nextServers : undefined
67
+ };
68
+ if (!request.dryRun) {
69
+ await writeTextFileAtomic(location, JSON.stringify(next, null, 2) + "\n");
70
+ }
71
+ return {
72
+ client: "windsurf",
73
+ scope: request.scope,
74
+ name: request.name,
75
+ method: "file",
76
+ changed: true,
77
+ location,
78
+ managed: true,
79
+ nextStep: "Windsurf config was updated. Windsurf-owned auth state is unchanged."
80
+ };
81
+ }
@@ -0,0 +1,123 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { promptText } from "../platform/prompt.js";
3
+ import { parseFlags } from "../cli/parse.js";
4
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
5
+ import { renderToolResult } from "../core/renderers.js";
6
+ import { promptObjectBySchema } from "../core/interactive-input.js";
7
+ function challengedScope(error) {
8
+ if (!error.debugDetails || typeof error.debugDetails !== "object")
9
+ return undefined;
10
+ const scope = error.debugDetails["scope"];
11
+ return typeof scope === "string" && scope.trim() ? scope : undefined;
12
+ }
13
+ async function requiredScopeForTool(context, toolName) {
14
+ const { serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
15
+ const tools = await context.runtimeClient.listTools(serverUrl);
16
+ const tool = tools.find((item) => item.name === toolName);
17
+ const directSchemes = Array.isArray(tool?.["securitySchemes"])
18
+ ? (tool.securitySchemes)
19
+ : [];
20
+ const metaSchemes = tool?._meta && typeof tool._meta === "object" && Array.isArray(tool._meta["securitySchemes"])
21
+ ? tool._meta["securitySchemes"]
22
+ : [];
23
+ const scopes = [...directSchemes, ...metaSchemes].find((scheme) => scheme.type === "oauth2")?.scopes;
24
+ return Array.isArray(scopes) && scopes.length ? scopes.join(" ") : undefined;
25
+ }
26
+ async function readStdin() {
27
+ const chunks = [];
28
+ for await (const chunk of process.stdin) {
29
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
30
+ }
31
+ return Buffer.concat(chunks).toString("utf8");
32
+ }
33
+ export async function callToolWithRetry(context, toolName, input, allowLogin) {
34
+ const { profileName, serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
35
+ const existingSession = await context.tokenManager.getSession(profileName);
36
+ try {
37
+ return {
38
+ result: await context.runtimeClient.callTool(serverUrl, existingSession?.accessToken, toolName, input),
39
+ ...(existingSession ? { session: existingSession } : {})
40
+ };
41
+ }
42
+ catch (error) {
43
+ if (!(error instanceof CliError) || !["auth.required", "auth.insufficient_scope"].includes(error.machineCode))
44
+ throw error;
45
+ if (error.machineCode === "auth.required" && existingSession?.refreshToken) {
46
+ const refreshed = await context.tokenManager.refresh(profileName, existingSession);
47
+ return {
48
+ result: await context.runtimeClient.callTool(serverUrl, refreshed.session.accessToken, toolName, input),
49
+ session: refreshed.session
50
+ };
51
+ }
52
+ if (allowLogin && !context.globalOptions.nonInteractive) {
53
+ const scope = challengedScope(error) || await requiredScopeForTool(context, toolName);
54
+ await context.tokenManager.login(context.globalOptions, {
55
+ scope
56
+ });
57
+ const nextSession = await context.tokenManager.getSession(profileName);
58
+ return {
59
+ result: await context.runtimeClient.callTool(serverUrl, nextSession?.accessToken, toolName, input),
60
+ ...(nextSession ? { session: nextSession } : {})
61
+ };
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+ async function listToolsWithRetry(context, allowLogin) {
67
+ const { profileName, serverUrl } = await context.tokenManager.resolveProfile(context.globalOptions);
68
+ const existingSession = await context.tokenManager.getSession(profileName);
69
+ try {
70
+ return await context.runtimeClient.listTools(serverUrl, existingSession?.accessToken);
71
+ }
72
+ catch (error) {
73
+ if (!(error instanceof CliError) || !["auth.required", "auth.insufficient_scope"].includes(error.machineCode))
74
+ throw error;
75
+ if (error.machineCode === "auth.required" && existingSession?.refreshToken) {
76
+ const refreshed = await context.tokenManager.refresh(profileName, existingSession);
77
+ return await context.runtimeClient.listTools(serverUrl, refreshed.session.accessToken);
78
+ }
79
+ if (allowLogin && !context.globalOptions.nonInteractive) {
80
+ await context.tokenManager.login(context.globalOptions, {
81
+ scope: challengedScope(error)
82
+ });
83
+ const nextSession = await context.tokenManager.getSession(profileName);
84
+ return await context.runtimeClient.listTools(serverUrl, nextSession?.accessToken);
85
+ }
86
+ throw error;
87
+ }
88
+ }
89
+ export async function runCallCommand(args, context) {
90
+ const { flags, positionals } = parseFlags(args, {
91
+ valueFlags: ["input-json", "input-file"],
92
+ booleanFlags: ["stdin", "interactive", "no-login"]
93
+ });
94
+ const toolName = positionals[0];
95
+ if (!toolName) {
96
+ throw new CliError("usage.tool_name_required", "A tool name is required.", EXIT_CODES.usage);
97
+ }
98
+ let input = {};
99
+ if (typeof flags["input-json"] === "string") {
100
+ input = JSON.parse(flags["input-json"]);
101
+ }
102
+ else if (typeof flags["input-file"] === "string") {
103
+ input = JSON.parse(await readFile(flags["input-file"], "utf8"));
104
+ }
105
+ else if (flags["stdin"]) {
106
+ input = JSON.parse(await readStdin());
107
+ }
108
+ else if (flags["interactive"]) {
109
+ const tools = await listToolsWithRetry(context, !flags["no-login"]);
110
+ const tool = tools.find((item) => item.name === toolName);
111
+ if (!tool) {
112
+ throw new CliError("tool.not_found", `Tool not found: ${toolName}`, EXIT_CODES.toolFailed);
113
+ }
114
+ input = await promptObjectBySchema(promptText, toolName, tool.inputSchema);
115
+ }
116
+ const { result } = await callToolWithRetry(context, toolName, input, !flags["no-login"]);
117
+ context.output.success({
118
+ schemaVersion: 1,
119
+ tool: toolName,
120
+ arguments: input,
121
+ result
122
+ }, [renderToolResult(result)]);
123
+ }