@oh-my-tool/cli 0.2.0 → 0.3.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.
Files changed (46) hide show
  1. package/README.md +7 -2
  2. package/assets/skills/oh-my-tool/SKILL.md +11 -3
  3. package/bin/ohmytool.cjs +0 -0
  4. package/package.json +12 -10
  5. package/src/cli/commands/describe.ts +23 -22
  6. package/src/cli/commands/extension.ts +24 -24
  7. package/src/cli/commands/index.ts +7 -6
  8. package/src/cli/commands/integrate.ts +64 -64
  9. package/src/cli/commands/mcp.ts +86 -0
  10. package/src/cli/commands/run.ts +8 -7
  11. package/src/cli/commands/search.ts +14 -13
  12. package/src/cli/commands/secret.ts +68 -68
  13. package/src/cli/context.ts +25 -2
  14. package/src/cli/index.ts +296 -272
  15. package/src/cli/parseArgs.ts +62 -44
  16. package/src/config/config.ts +155 -63
  17. package/src/core/executor.ts +89 -89
  18. package/src/core/registry.ts +31 -31
  19. package/src/core/result.ts +14 -14
  20. package/src/extension/discovery.ts +61 -61
  21. package/src/extension/install.ts +23 -23
  22. package/src/extension/loader.ts +32 -32
  23. package/src/extension/manifest.ts +114 -114
  24. package/src/integration/adapters.ts +98 -98
  25. package/src/integration/index.ts +4 -4
  26. package/src/integration/manager.ts +375 -375
  27. package/src/integration/skill.ts +84 -84
  28. package/src/integration/types.ts +55 -55
  29. package/src/policy/policy.ts +136 -136
  30. package/src/runtime/errors.ts +7 -2
  31. package/src/runtime/executor.ts +6 -1
  32. package/src/runtime/provider.ts +1 -0
  33. package/src/runtime/providers/mcp/normalize.ts +36 -0
  34. package/src/runtime/providers/mcp/oauth-callback.ts +91 -0
  35. package/src/runtime/providers/mcp/oauth-provider.ts +348 -0
  36. package/src/runtime/providers/mcp/oauth-store.ts +106 -0
  37. package/src/runtime/providers/mcp/provider.ts +99 -0
  38. package/src/runtime/providers/mcp/safe-errors.ts +63 -0
  39. package/src/runtime/providers/mcp/session.ts +117 -0
  40. package/src/runtime/providers/mcp/transport.ts +140 -0
  41. package/src/runtime/result.ts +1 -1
  42. package/src/runtime/runtime.ts +38 -12
  43. package/src/runtime/schema.ts +14 -4
  44. package/src/search/search.ts +78 -78
  45. package/src/secrets/secrets.ts +45 -45
  46. package/src/version.ts +1 -1
@@ -1,9 +1,10 @@
1
1
  import { createPaths } from "../paths";
2
2
  import { prepareHome } from "../migration";
3
- import { loadConfig, getConnectionConfig } from "../config/config";
3
+ import { loadConfig, getConnectionConfig, type McpEnabledServerConfig } from "../config/config";
4
4
  import { SecretsManager } from "../secrets/secrets";
5
5
  import { applyLimits, validateConnectionInput } from "../policy/policy";
6
6
  import { NativeExtensionProvider } from "../runtime/providers/native/provider";
7
+ import { McpProvider } from "../runtime/providers/mcp/provider";
7
8
  import { createToolRuntime } from "../runtime/runtime";
8
9
  import type { ToolDescriptor } from "../runtime/provider";
9
10
 
@@ -16,10 +17,15 @@ export async function createRuntime() {
16
17
  await prepareHome(paths);
17
18
  const config = loadConfig(paths.home);
18
19
  const secrets = new SecretsManager();
20
+ const providers = [new NativeExtensionProvider(paths), ...Object.entries(config.mcp.servers)
21
+ .sort(([a], [b]) => a.localeCompare(b))
22
+ .filter((entry): entry is [string, McpEnabledServerConfig] => entry[1].enabled)
23
+ .map(([serverId, server]) => new McpProvider({ serverId, config: server, secrets }))];
19
24
  return createToolRuntime({
20
- providers: [new NativeExtensionProvider(paths)],
25
+ providers,
21
26
  policy: {
22
27
  preflight(descriptor, input) {
28
+ if (descriptor.provider.kind !== "native") return;
23
29
  const limits = applyLimits(input);
24
30
  input.maxRows = limits.maxRows;
25
31
  input.timeoutMs = limits.timeoutMs;
@@ -42,3 +48,20 @@ export async function createRuntime() {
42
48
  },
43
49
  });
44
50
  }
51
+
52
+ export async function withRuntime<T>(operation: (runtime: Awaited<ReturnType<typeof createRuntime>>) => Promise<T>): Promise<T> {
53
+ const runtime = await createRuntime();
54
+ let operationFailed = false;
55
+ try {
56
+ return await operation(runtime);
57
+ } catch (error) {
58
+ operationFailed = true;
59
+ throw error;
60
+ } finally {
61
+ try {
62
+ await runtime.close();
63
+ } catch (closeError) {
64
+ if (!operationFailed) throw closeError;
65
+ }
66
+ }
67
+ }
package/src/cli/index.ts CHANGED
@@ -1,22 +1,24 @@
1
- import { parseArgs } from "./parseArgs";
2
- import { runSearch } from "./commands/search";
3
- import { runDescribe } from "./commands/describe";
1
+ import { parseArgs, parseMcpCommand } from "./parseArgs";
2
+ import { runSearch } from "./commands/search";
3
+ import { runDescribe } from "./commands/describe";
4
4
  import { runTool } from "./commands/run";
5
- import { runExtensionList, runExtensionInstall } from "./commands/extension";
6
- import { runSecretList, runSecretSet } from "./commands/secret";
7
- import {
8
- defaultIntegrationManager,
9
- runIntegrate,
10
- type IntegrateAction,
11
- } from "./commands/integrate";
12
- import type { AgentDetection, AgentId, IntegrationResult, IntegrationStatus } from "../integration";
13
- import { AGENT_IDS } from "../integration";
14
- import { multiselect, isCancel } from "@clack/prompts";
15
- import { VERSION } from "../version";
16
-
17
- const HELP = `Oh My Tool - local and enterprise tools for agents
18
-
19
- Usage:
5
+ import { runExtensionList, runExtensionInstall } from "./commands/extension";
6
+ import { runSecretList, runSecretSet } from "./commands/secret";
7
+ import {
8
+ defaultIntegrationManager,
9
+ runIntegrate,
10
+ type IntegrateAction,
11
+ } from "./commands/integrate";
12
+ import type { AgentDetection, AgentId, IntegrationResult, IntegrationStatus } from "../integration";
13
+ import { AGENT_IDS } from "../integration";
14
+ import { multiselect, isCancel } from "@clack/prompts";
15
+ import { VERSION } from "../version";
16
+ import { runMcpAuth, runMcpList, runMcpLogout } from "./commands/mcp";
17
+ import { RuntimeError } from "../runtime/errors";
18
+
19
+ const HELP = `Oh My Tool - local and enterprise tools for agents
20
+
21
+ Usage:
20
22
  ohmytool search "<task>" search tools by intent
21
23
  ohmytool describe <tool> inspect a tool and its input schema
22
24
  ohmytool run <tool> [key=value ...] execute a tool
@@ -25,272 +27,294 @@ Usage:
25
27
  ohmytool extension install <path> install an extension from a local dir
26
28
  ohmytool secret set <name> set a secret (interactive hidden prompt or stdin pipe)
27
29
  ohmytool secret list list secret names (Windows only, values never shown)
30
+ ohmytool mcp list list configured MCP servers
31
+ ohmytool mcp auth <server> authorize an OAuth MCP server
32
+ ohmytool mcp logout <server> remove locally stored OAuth credentials
28
33
  ohmytool setup detect agents and install the OMT skill
29
34
  ohmytool integrate [status|repair|uninstall]
30
35
  manage agent skill integrations
31
36
  ohmytool --version print version
32
-
33
- Examples:
34
- ohmytool search "查询 mysql 设备数据"
37
+
38
+ Examples:
39
+ ohmytool search "query mysql devices data"
35
40
  ohmytool describe mysql.query
36
41
  ohmytool run mysql.query connection=iot-test sql="SELECT id FROM device"
37
42
  echo '{"connection":"iot-test","sql":"SELECT 1"}' | ohmytool run mysql.query --stdin
38
- `;
39
-
40
- const AGENT_IDS_SET = new Set<AgentId>(AGENT_IDS);
41
-
42
- function readStdin(): Promise<string> {
43
- return new Promise((resolve, reject) => {
44
- let data = "";
45
- process.stdin.setEncoding("utf8");
46
- process.stdin.on("data", (chunk: string) => (data += chunk));
47
- process.stdin.on("end", () => resolve(data.trim()));
48
- process.stdin.on("error", reject);
49
- });
50
- }
51
-
52
- /** 交互式隐藏输入(不回显、不进历史、不落盘),仅 TTY 下调用。 */
53
- function readSecretHidden(prompt: string): Promise<string> {
54
- const { promise, resolve, reject } = Promise.withResolvers<string>();
55
- process.stdout.write(prompt);
56
- const stdin = process.stdin;
57
- const prevRaw = stdin.isRaw;
58
- stdin.setRawMode(true);
59
- stdin.resume();
60
- stdin.setEncoding("utf8");
61
- let value = "";
62
- const finish = () => {
63
- stdin.removeListener("data", onData);
64
- stdin.setRawMode(prevRaw);
65
- stdin.pause();
66
- };
67
- const onData = (chunk: string) => {
68
- for (const ch of chunk) {
69
- if (ch === "\r" || ch === "\n") {
70
- finish();
71
- process.stdout.write("\n");
72
- resolve(value);
73
- return;
74
- }
75
- if (ch === "\x03") {
76
- finish();
77
- process.stdout.write("\n");
78
- reject(new Error("aborted"));
79
- return;
80
- }
81
- if (ch === "\x7f" || ch === "\b") {
82
- value = value.slice(0, -1);
83
- continue;
84
- }
85
- value += ch;
86
- }
87
- };
88
- stdin.on("data", onData);
89
- return promise;
90
- }
91
-
92
- function print(v: unknown): void {
93
- console.log(JSON.stringify(v, null, 2));
94
- }
95
-
96
- function parseAgentIds(raw?: string): AgentId[] | undefined {
97
- if (!raw) return undefined;
98
- const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
99
- for (const value of values) {
100
- if (!AGENT_IDS_SET.has(value as AgentId)) throw new Error(`Unknown agent: ${value}`);
101
- }
102
- return [...new Set(values as AgentId[])];
103
- }
104
-
105
- function printDetected(agents: AgentDetection[]): void {
106
- console.log("Detected agents:\n");
107
- for (const agent of agents) console.log(`✓ ${agent.variant ?? agent.displayName}`);
108
- }
109
-
110
- const STATUS_ICON: Record<IntegrationStatus, string> = {
111
- current: "✓",
112
- installed: "✓",
113
- repaired: "✓",
114
- uninstalled: "✓",
115
- "update-available": "↻",
116
- "not-installed": "○",
117
- broken: "⚠",
118
- conflict: "✗",
119
- };
120
-
121
- const STATUS_SEVERITY: Record<IntegrationStatus, number> = {
122
- conflict: 0,
123
- broken: 1,
124
- "update-available": 2,
125
- "not-installed": 3,
126
- current: 4,
127
- installed: 4,
128
- repaired: 4,
129
- uninstalled: 4,
130
- };
131
-
132
- function printIntegrationResults(results: IntegrationResult[]): void {
133
- console.log("");
134
- for (const item of results) {
135
- const suffix = item.detail ? ` — ${item.detail}` : "";
136
- console.log(`${STATUS_ICON[item.status]} ${item.displayName.padEnd(14)} ${item.status}${suffix}`);
137
- }
138
- }
139
-
140
- function printStatus(results: IntegrationResult[]): void {
141
- console.log("");
142
- const ordered = [...results].sort((a, b) => STATUS_SEVERITY[a.status] - STATUS_SEVERITY[b.status]);
143
- for (const item of ordered) {
144
- console.log(
145
- `${STATUS_ICON[item.status]} ${item.displayName.padEnd(14)} ${item.status.padEnd(19)} ${item.detail ?? item.target}`,
146
- );
147
- }
148
- const counts: Record<string, number> = {};
149
- for (const item of results) counts[item.status] = (counts[item.status] ?? 0) + 1;
150
- const summary = Object.entries(counts).map(([status, n]) => `${n} ${status}`).join(" · ");
151
- console.log(`\nSummary: ${summary}`);
152
- if (results.some((item) => item.status === "broken")) {
153
- console.log("Tip: run `omt integrate repair` to recreate broken links");
154
- }
155
- if (results.some((item) => item.status === "conflict")) {
156
- console.log("Tip: conflict means OMT refuses to touch an unmanaged path; run `omt integrate --force` only if you accept replacing it");
157
- }
158
- }
159
-
160
- async function promptForAgents(agents: AgentDetection[]): Promise<AgentId[]> {
161
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
162
- throw new Error("Interactive confirmation requires a TTY; pass --yes for unattended setup");
163
- }
164
- const selected = await multiselect({
165
- message: "选择要集成的 Agent(空格切换,回车确认)",
166
- options: agents.map((agent) => ({
167
- value: agent.id,
168
- label: agent.variant ?? agent.displayName,
169
- hint: agent.target,
170
- })),
171
- initialValues: agents.map((agent) => agent.id),
172
- required: false,
173
- });
174
- if (isCancel(selected) || !selected?.length) return [];
175
- return selected as AgentId[];
176
- }
177
-
178
- export async function main(argv: string[]): Promise<number> {
179
- const parsed = parseArgs(argv);
180
- const cmd = parsed.positional[0];
181
- try {
182
- switch (cmd) {
183
- case "search": {
184
- const q = parsed.positional.slice(1).join(" ");
185
- print(await runSearch(q));
186
- return 0;
187
- }
188
- case "describe": {
189
- print(await runDescribe(parsed.positional[1]));
190
- return 0;
191
- }
43
+ `;
44
+
45
+ const AGENT_IDS_SET = new Set<AgentId>(AGENT_IDS);
46
+
47
+ function readStdin(): Promise<string> {
48
+ return new Promise((resolve, reject) => {
49
+ let data = "";
50
+ process.stdin.setEncoding("utf8");
51
+ process.stdin.on("data", (chunk: string) => (data += chunk));
52
+ process.stdin.on("end", () => resolve(data.trim()));
53
+ process.stdin.on("error", reject);
54
+ });
55
+ }
56
+
57
+ /** 交互式隐藏输入(不回显、不进历史、不落盘),仅 TTY 下调用。 */
58
+ function readSecretHidden(prompt: string): Promise<string> {
59
+ const { promise, resolve, reject } = Promise.withResolvers<string>();
60
+ process.stdout.write(prompt);
61
+ const stdin = process.stdin;
62
+ const prevRaw = stdin.isRaw;
63
+ stdin.setRawMode(true);
64
+ stdin.resume();
65
+ stdin.setEncoding("utf8");
66
+ let value = "";
67
+ const finish = () => {
68
+ stdin.removeListener("data", onData);
69
+ stdin.setRawMode(prevRaw);
70
+ stdin.pause();
71
+ };
72
+ const onData = (chunk: string) => {
73
+ for (const ch of chunk) {
74
+ if (ch === "\r" || ch === "\n") {
75
+ finish();
76
+ process.stdout.write("\n");
77
+ resolve(value);
78
+ return;
79
+ }
80
+ if (ch === "\x03") {
81
+ finish();
82
+ process.stdout.write("\n");
83
+ reject(new Error("aborted"));
84
+ return;
85
+ }
86
+ if (ch === "\x7f" || ch === "\b") {
87
+ value = value.slice(0, -1);
88
+ continue;
89
+ }
90
+ value += ch;
91
+ }
92
+ };
93
+ stdin.on("data", onData);
94
+ return promise;
95
+ }
96
+
97
+ function print(v: unknown): void {
98
+ console.log(JSON.stringify(v, null, 2));
99
+ }
100
+
101
+ function parseAgentIds(raw?: string): AgentId[] | undefined {
102
+ if (!raw) return undefined;
103
+ const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
104
+ for (const value of values) {
105
+ if (!AGENT_IDS_SET.has(value as AgentId)) throw new Error(`Unknown agent: ${value}`);
106
+ }
107
+ return [...new Set(values as AgentId[])];
108
+ }
109
+
110
+ function printDetected(agents: AgentDetection[]): void {
111
+ console.log("Detected agents:\n");
112
+ for (const agent of agents) console.log(`✓ ${agent.variant ?? agent.displayName}`);
113
+ }
114
+
115
+ const STATUS_ICON: Record<IntegrationStatus, string> = {
116
+ current: "✓",
117
+ installed: "✓",
118
+ repaired: "✓",
119
+ uninstalled: "✓",
120
+ "update-available": "↻",
121
+ "not-installed": "○",
122
+ broken: "⚠",
123
+ conflict: "✗",
124
+ };
125
+
126
+ const STATUS_SEVERITY: Record<IntegrationStatus, number> = {
127
+ conflict: 0,
128
+ broken: 1,
129
+ "update-available": 2,
130
+ "not-installed": 3,
131
+ current: 4,
132
+ installed: 4,
133
+ repaired: 4,
134
+ uninstalled: 4,
135
+ };
136
+
137
+ function printIntegrationResults(results: IntegrationResult[]): void {
138
+ console.log("");
139
+ for (const item of results) {
140
+ const suffix = item.detail ? ` — ${item.detail}` : "";
141
+ console.log(`${STATUS_ICON[item.status]} ${item.displayName.padEnd(14)} ${item.status}${suffix}`);
142
+ }
143
+ }
144
+
145
+ function printStatus(results: IntegrationResult[]): void {
146
+ console.log("");
147
+ const ordered = [...results].sort((a, b) => STATUS_SEVERITY[a.status] - STATUS_SEVERITY[b.status]);
148
+ for (const item of ordered) {
149
+ console.log(
150
+ `${STATUS_ICON[item.status]} ${item.displayName.padEnd(14)} ${item.status.padEnd(19)} ${item.detail ?? item.target}`,
151
+ );
152
+ }
153
+ const counts: Record<string, number> = {};
154
+ for (const item of results) counts[item.status] = (counts[item.status] ?? 0) + 1;
155
+ const summary = Object.entries(counts).map(([status, n]) => `${n} ${status}`).join(" · ");
156
+ console.log(`\nSummary: ${summary}`);
157
+ if (results.some((item) => item.status === "broken")) {
158
+ console.log("Tip: run `omt integrate repair` to recreate broken links");
159
+ }
160
+ if (results.some((item) => item.status === "conflict")) {
161
+ console.log("Tip: conflict means OMT refuses to touch an unmanaged path; run `omt integrate --force` only if you accept replacing it");
162
+ }
163
+ }
164
+
165
+ async function promptForAgents(agents: AgentDetection[]): Promise<AgentId[]> {
166
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
167
+ throw new Error("Interactive confirmation requires a TTY; pass --yes for unattended setup");
168
+ }
169
+ const selected = await multiselect({
170
+ message: "选择要集成的 Agent(空格切换,回车确认)",
171
+ options: agents.map((agent) => ({
172
+ value: agent.id,
173
+ label: agent.variant ?? agent.displayName,
174
+ hint: agent.target,
175
+ })),
176
+ initialValues: agents.map((agent) => agent.id),
177
+ required: false,
178
+ });
179
+ if (isCancel(selected) || !selected?.length) return [];
180
+ return selected as AgentId[];
181
+ }
182
+
183
+ export interface CliDependencies {
184
+ readonly runMcpAuth: typeof runMcpAuth;
185
+ readonly runMcpLogout: typeof runMcpLogout;
186
+ readonly runMcpList?: typeof runMcpList;
187
+ }
188
+
189
+ const defaultCliDependencies: CliDependencies = { runMcpAuth, runMcpLogout, runMcpList };
190
+
191
+ export async function main(argv: string[], dependencies: CliDependencies = defaultCliDependencies): Promise<number> {
192
+ const parsed = parseArgs(argv);
193
+ const cmd = parsed.positional[0];
194
+ try {
195
+ switch (cmd) {
196
+ case "search": {
197
+ const q = parsed.positional.slice(1).join(" ");
198
+ print(await runSearch(q));
199
+ return 0;
200
+ }
201
+ case "describe": {
202
+ print(await runDescribe(parsed.positional[1]));
203
+ return 0;
204
+ }
192
205
  case "run": {
193
206
  const tool = parsed.positional[1];
194
207
  const res = await runTool(tool, parsed.keyValues, parsed.flags.includes("stdin"));
195
- print(res);
196
- return res.ok ? 0 : 1;
197
- }
198
- case "secret": {
199
- const sub = parsed.positional[1];
200
- if (sub === "set") {
201
- const name = parsed.positional[2];
202
- if (!name) {
208
+ print(res);
209
+ return res.ok ? 0 : 1;
210
+ }
211
+ case "secret": {
212
+ const sub = parsed.positional[1];
213
+ if (sub === "set") {
214
+ const name = parsed.positional[2];
215
+ if (!name) {
203
216
  console.error("usage: ohmytool secret set <name> (交互输入或 stdin 管道)");
204
- return 1;
205
- }
206
- // TTY 下交互隐藏输入(不回显/不进历史),非 TTY 保持管道 stdin
207
- const value = process.stdin.isTTY ? await readSecretHidden("password: ") : await readStdin();
208
- print(await runSecretSet(name, value));
209
- return 0;
210
- }
211
- if (sub === "list") {
212
- print(await runSecretList());
213
- return 0;
214
- }
217
+ return 1;
218
+ }
219
+ // TTY 下交互隐藏输入(不回显/不进历史),非 TTY 保持管道 stdin
220
+ const value = process.stdin.isTTY ? await readSecretHidden("password: ") : await readStdin();
221
+ print(await runSecretSet(name, value));
222
+ return 0;
223
+ }
224
+ if (sub === "list") {
225
+ print(await runSecretList());
226
+ return 0;
227
+ }
215
228
  console.error("usage: ohmytool secret set <name> | secret list");
216
- return 1;
217
- }
218
- case "extension": {
219
- const sub = parsed.positional[1];
220
- if (sub === "list") {
221
- print(await runExtensionList());
222
- return 0;
223
- }
224
- if (sub === "install") {
225
- print(await runExtensionInstall(parsed.positional[2]));
226
- return 0;
227
- }
229
+ return 1;
230
+ }
231
+ case "extension": {
232
+ const sub = parsed.positional[1];
233
+ if (sub === "list") {
234
+ print(await runExtensionList());
235
+ return 0;
236
+ }
237
+ if (sub === "install") {
238
+ print(await runExtensionInstall(parsed.positional[2]));
239
+ return 0;
240
+ }
228
241
  console.error("usage: ohmytool extension list|install <path>");
229
- return 1;
230
- }
231
- case "setup":
232
- case "integrate": {
233
- const action = (cmd === "setup" ? "install" : parsed.positional[1] ?? "install") as IntegrateAction;
234
- if (!["install", "status", "repair", "uninstall"].includes(action)) {
235
- throw new Error(`Unknown integrate action: ${action}`);
236
- }
237
- const manager = defaultIntegrationManager();
238
- const detected = await manager.detect();
239
- if (!detected.length) throw new Error("No supported agents detected");
240
- printDetected(detected);
241
- let agents = parseAgentIds(parsed.options.agents);
242
- const mutating = action !== "status" && !parsed.flags.includes("dry-run");
243
- if (mutating && !parsed.flags.includes("yes") && !agents) {
244
- agents = await promptForAgents(detected);
245
- if (!agents.length) {
246
- console.log("\nCancelled.");
247
- return 0;
248
- }
249
- } else if (mutating && !parsed.flags.includes("yes")) {
250
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
251
- throw new Error("Confirmation required; pass --yes for unattended setup");
252
- }
253
- const selectedDetections = detected.filter((agent) => agents!.includes(agent.id));
254
- agents = await promptForAgents(selectedDetections);
255
- if (!agents.length) {
256
- console.log("\nCancelled.");
257
- return 0;
258
- }
259
- }
260
- const integration = await runIntegrate(
261
- {
262
- action,
263
- agents,
264
- force: parsed.flags.includes("force"),
265
- dryRun: parsed.flags.includes("dry-run"),
266
- },
267
- manager,
268
- );
269
- if (integration.dryRun) {
270
- console.log(`\nDry run: would ${action} ${integration.selected.join(", ")}`);
271
- } else if (action === "status") {
272
- printStatus(integration.results);
273
- } else {
274
- printIntegrationResults(integration.results);
275
- }
276
- return 0;
277
- }
278
- case "-v": {
242
+ return 1;
243
+ }
244
+ case "mcp": {
245
+ const mcp = parseMcpCommand(parsed);
246
+ if (mcp === undefined) {
247
+ console.error("usage: ohmytool mcp list | mcp auth <server> | mcp logout <server>");
248
+ return 1;
249
+ }
250
+ if (mcp.action === "list") print(await (dependencies.runMcpList ?? runMcpList)());
251
+ else if (mcp.action === "auth") print(await dependencies.runMcpAuth(mcp.serverId));
252
+ else print(await dependencies.runMcpLogout(mcp.serverId));
253
+ return 0;
254
+ }
255
+ case "setup":
256
+ case "integrate": {
257
+ const action = (cmd === "setup" ? "install" : parsed.positional[1] ?? "install") as IntegrateAction;
258
+ if (!["install", "status", "repair", "uninstall"].includes(action)) {
259
+ throw new Error(`Unknown integrate action: ${action}`);
260
+ }
261
+ const manager = defaultIntegrationManager();
262
+ const detected = await manager.detect();
263
+ if (!detected.length) throw new Error("No supported agents detected");
264
+ printDetected(detected);
265
+ let agents = parseAgentIds(parsed.options.agents);
266
+ const mutating = action !== "status" && !parsed.flags.includes("dry-run");
267
+ if (mutating && !parsed.flags.includes("yes") && !agents) {
268
+ agents = await promptForAgents(detected);
269
+ if (!agents.length) {
270
+ console.log("\nCancelled.");
271
+ return 0;
272
+ }
273
+ } else if (mutating && !parsed.flags.includes("yes")) {
274
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
275
+ throw new Error("Confirmation required; pass --yes for unattended setup");
276
+ }
277
+ const selectedDetections = detected.filter((agent) => agents!.includes(agent.id));
278
+ agents = await promptForAgents(selectedDetections);
279
+ if (!agents.length) {
280
+ console.log("\nCancelled.");
281
+ return 0;
282
+ }
283
+ }
284
+ const integration = await runIntegrate(
285
+ {
286
+ action,
287
+ agents,
288
+ force: parsed.flags.includes("force"),
289
+ dryRun: parsed.flags.includes("dry-run"),
290
+ },
291
+ manager,
292
+ );
293
+ if (integration.dryRun) {
294
+ console.log(`\nDry run: would ${action} ${integration.selected.join(", ")}`);
295
+ } else if (action === "status") {
296
+ printStatus(integration.results);
297
+ } else {
298
+ printIntegrationResults(integration.results);
299
+ }
300
+ return 0;
301
+ }
302
+ case "-v": {
279
303
  print({ name: "ohmytool", version: VERSION });
280
- return 0;
281
- }
282
- default:
283
- if (parsed.flags.includes("version")) {
304
+ return 0;
305
+ }
306
+ default:
307
+ if (parsed.flags.includes("version")) {
284
308
  print({ name: "ohmytool", version: VERSION });
285
- return 0;
286
- }
287
- console.log(HELP);
288
- return cmd ? 1 : 0;
289
- }
290
- } catch (e) {
291
- console.error(e instanceof Error ? e.message : String(e));
292
- return 1;
293
- }
294
- }
295
-
296
-
309
+ return 0;
310
+ }
311
+ console.log(HELP);
312
+ return cmd ? 1 : 0;
313
+ }
314
+ } catch (e) {
315
+ console.error(e instanceof RuntimeError ? `${e.code}: ${e.message}` : e instanceof Error ? e.message : String(e));
316
+ return 1;
317
+ }
318
+ }
319
+
320
+