@oh-my-tool/cli 0.2.0 → 0.3.1

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