chainlesschain 0.37.9 → 0.37.10

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 (51) hide show
  1. package/README.md +309 -19
  2. package/bin/chainlesschain.js +4 -0
  3. package/package.json +1 -1
  4. package/src/commands/audit.js +286 -0
  5. package/src/commands/auth.js +387 -0
  6. package/src/commands/browse.js +184 -0
  7. package/src/commands/did.js +376 -0
  8. package/src/commands/encrypt.js +233 -0
  9. package/src/commands/export.js +125 -0
  10. package/src/commands/git.js +215 -0
  11. package/src/commands/import.js +259 -0
  12. package/src/commands/instinct.js +202 -0
  13. package/src/commands/llm.js +155 -4
  14. package/src/commands/mcp.js +302 -0
  15. package/src/commands/memory.js +282 -0
  16. package/src/commands/note.js +187 -0
  17. package/src/commands/org.js +505 -0
  18. package/src/commands/p2p.js +274 -0
  19. package/src/commands/plugin.js +398 -0
  20. package/src/commands/search.js +237 -0
  21. package/src/commands/session.js +238 -0
  22. package/src/commands/sync.js +249 -0
  23. package/src/commands/tokens.js +214 -0
  24. package/src/commands/wallet.js +416 -0
  25. package/src/index.js +49 -1
  26. package/src/lib/audit-logger.js +364 -0
  27. package/src/lib/bm25-search.js +322 -0
  28. package/src/lib/browser-automation.js +216 -0
  29. package/src/lib/crypto-manager.js +246 -0
  30. package/src/lib/did-manager.js +270 -0
  31. package/src/lib/ensure-utf8.js +59 -0
  32. package/src/lib/git-integration.js +220 -0
  33. package/src/lib/instinct-manager.js +190 -0
  34. package/src/lib/knowledge-exporter.js +302 -0
  35. package/src/lib/knowledge-importer.js +293 -0
  36. package/src/lib/llm-providers.js +325 -0
  37. package/src/lib/mcp-client.js +413 -0
  38. package/src/lib/memory-manager.js +211 -0
  39. package/src/lib/note-versioning.js +244 -0
  40. package/src/lib/org-manager.js +424 -0
  41. package/src/lib/p2p-manager.js +317 -0
  42. package/src/lib/pdf-parser.js +96 -0
  43. package/src/lib/permission-engine.js +374 -0
  44. package/src/lib/plan-mode.js +333 -0
  45. package/src/lib/plugin-manager.js +312 -0
  46. package/src/lib/response-cache.js +156 -0
  47. package/src/lib/session-manager.js +189 -0
  48. package/src/lib/sync-manager.js +347 -0
  49. package/src/lib/token-tracker.js +200 -0
  50. package/src/lib/wallet-manager.js +348 -0
  51. package/src/repl/agent-repl.js +142 -12
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Instinct learning commands
3
+ * chainlesschain instinct show|reset|categories
4
+ */
5
+
6
+ import chalk from "chalk";
7
+ import { logger } from "../lib/logger.js";
8
+ import { bootstrap, shutdown } from "../runtime/bootstrap.js";
9
+ import {
10
+ getInstincts,
11
+ getStrongInstincts,
12
+ resetInstincts,
13
+ deleteInstinct,
14
+ decayInstincts,
15
+ generateInstinctPrompt,
16
+ INSTINCT_CATEGORIES,
17
+ } from "../lib/instinct-manager.js";
18
+
19
+ export function registerInstinctCommand(program) {
20
+ const instinct = program
21
+ .command("instinct")
22
+ .description("Instinct learning — learned user preferences");
23
+
24
+ // instinct show
25
+ instinct
26
+ .command("show", { isDefault: true })
27
+ .description("Show learned instincts")
28
+ .option("--category <cat>", "Filter by category")
29
+ .option("-n, --limit <n>", "Max entries", "30")
30
+ .option("--strong", "Show only high-confidence instincts (>= 70%)")
31
+ .option("--json", "Output as JSON")
32
+ .action(async (options) => {
33
+ try {
34
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
35
+ if (!ctx.db) {
36
+ logger.error("Database not available");
37
+ process.exit(1);
38
+ }
39
+ const db = ctx.db.getDatabase();
40
+
41
+ const instincts = options.strong
42
+ ? getStrongInstincts(db)
43
+ : getInstincts(db, {
44
+ category: options.category,
45
+ limit: parseInt(options.limit) || 30,
46
+ });
47
+
48
+ if (options.json) {
49
+ console.log(JSON.stringify(instincts, null, 2));
50
+ } else if (instincts.length === 0) {
51
+ logger.info(
52
+ "No instincts learned yet. Use the agent to build up preferences.",
53
+ );
54
+ } else {
55
+ logger.log(chalk.bold(`Learned Instincts (${instincts.length}):\n`));
56
+ for (const inst of instincts) {
57
+ const pct = (inst.confidence * 100).toFixed(0);
58
+ const bar =
59
+ "█".repeat(Math.round(inst.confidence * 10)) +
60
+ "░".repeat(10 - Math.round(inst.confidence * 10));
61
+ logger.log(
62
+ ` ${chalk.gray(inst.id.slice(0, 8))} ${chalk.cyan(inst.category.padEnd(18))} ${bar} ${pct}%`,
63
+ );
64
+ logger.log(` ${chalk.white(inst.pattern)}`);
65
+ logger.log(
66
+ ` ${chalk.gray(`seen ${inst.occurrences}x | last: ${inst.last_seen || "unknown"}`)}`,
67
+ );
68
+ }
69
+ }
70
+
71
+ await shutdown();
72
+ } catch (err) {
73
+ logger.error(`Failed: ${err.message}`);
74
+ process.exit(1);
75
+ }
76
+ });
77
+
78
+ // instinct categories
79
+ instinct
80
+ .command("categories")
81
+ .description("List instinct categories")
82
+ .action(async () => {
83
+ logger.log(chalk.bold("Instinct Categories:\n"));
84
+ for (const [key, value] of Object.entries(INSTINCT_CATEGORIES)) {
85
+ logger.log(` ${chalk.cyan(value.padEnd(20))} ${chalk.gray(key)}`);
86
+ }
87
+ });
88
+
89
+ // instinct prompt
90
+ instinct
91
+ .command("prompt")
92
+ .description("Generate a system prompt from learned instincts")
93
+ .option("--json", "Output as JSON")
94
+ .action(async (options) => {
95
+ try {
96
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
97
+ if (!ctx.db) {
98
+ logger.error("Database not available");
99
+ process.exit(1);
100
+ }
101
+ const db = ctx.db.getDatabase();
102
+ const prompt = generateInstinctPrompt(db);
103
+
104
+ if (options.json) {
105
+ console.log(JSON.stringify({ prompt }, null, 2));
106
+ } else if (!prompt) {
107
+ logger.info("No strong instincts yet. Keep using the agent!");
108
+ } else {
109
+ logger.log(prompt);
110
+ }
111
+
112
+ await shutdown();
113
+ } catch (err) {
114
+ logger.error(`Failed: ${err.message}`);
115
+ process.exit(1);
116
+ }
117
+ });
118
+
119
+ // instinct delete
120
+ instinct
121
+ .command("delete")
122
+ .description("Delete an instinct by ID")
123
+ .argument("<id>", "Instinct ID (or prefix)")
124
+ .action(async (id) => {
125
+ try {
126
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
127
+ if (!ctx.db) {
128
+ logger.error("Database not available");
129
+ process.exit(1);
130
+ }
131
+ const db = ctx.db.getDatabase();
132
+ const ok = deleteInstinct(db, id);
133
+ if (ok) {
134
+ logger.success("Instinct deleted");
135
+ } else {
136
+ logger.error(`Instinct not found: ${id}`);
137
+ }
138
+
139
+ await shutdown();
140
+ } catch (err) {
141
+ logger.error(`Failed: ${err.message}`);
142
+ process.exit(1);
143
+ }
144
+ });
145
+
146
+ // instinct reset
147
+ instinct
148
+ .command("reset")
149
+ .description("Reset all learned instincts")
150
+ .option("--force", "Skip confirmation")
151
+ .action(async (options) => {
152
+ try {
153
+ if (!options.force) {
154
+ const { confirm } = await import("@inquirer/prompts");
155
+ const ok = await confirm({
156
+ message: "Reset all learned instincts? This cannot be undone.",
157
+ });
158
+ if (!ok) {
159
+ logger.info("Cancelled");
160
+ return;
161
+ }
162
+ }
163
+
164
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
165
+ if (!ctx.db) {
166
+ logger.error("Database not available");
167
+ process.exit(1);
168
+ }
169
+ const db = ctx.db.getDatabase();
170
+ const count = resetInstincts(db);
171
+ logger.success(`Reset ${count} instincts`);
172
+
173
+ await shutdown();
174
+ } catch (err) {
175
+ logger.error(`Failed: ${err.message}`);
176
+ process.exit(1);
177
+ }
178
+ });
179
+
180
+ // instinct decay
181
+ instinct
182
+ .command("decay")
183
+ .description("Decay old instincts (reduce confidence of unused patterns)")
184
+ .option("--days <n>", "Days threshold", "30")
185
+ .action(async (options) => {
186
+ try {
187
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
188
+ if (!ctx.db) {
189
+ logger.error("Database not available");
190
+ process.exit(1);
191
+ }
192
+ const db = ctx.db.getDatabase();
193
+ const decayed = decayInstincts(db, parseInt(options.days) || 30);
194
+ logger.success(`Decayed ${decayed} old instincts`);
195
+
196
+ await shutdown();
197
+ } catch (err) {
198
+ logger.error(`Failed: ${err.message}`);
199
+ process.exit(1);
200
+ }
201
+ });
202
+ }
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * LLM management commands
3
- * chainlesschain llm list|test|models
3
+ * chainlesschain llm models|test|providers|add-provider|switch
4
4
  */
5
5
 
6
6
  import ora from "ora";
7
7
  import chalk from "chalk";
8
8
  import { logger } from "../lib/logger.js";
9
+ import { bootstrap, shutdown } from "../runtime/bootstrap.js";
10
+ import {
11
+ BUILT_IN_PROVIDERS,
12
+ LLMProviderRegistry,
13
+ } from "../lib/llm-providers.js";
9
14
 
10
15
  export function registerLlmCommand(program) {
11
16
  const llm = program.command("llm").description("LLM provider management");
@@ -44,9 +49,21 @@ export function registerLlmCommand(program) {
44
49
  }
45
50
  }
46
51
  } else {
47
- spinner.fail(
48
- `Model listing not supported for provider: ${options.provider}`,
49
- );
52
+ // Show known models for non-Ollama providers
53
+ spinner.stop();
54
+ const provider = BUILT_IN_PROVIDERS[options.provider];
55
+ if (provider) {
56
+ if (options.json) {
57
+ console.log(JSON.stringify(provider.models, null, 2));
58
+ } else {
59
+ logger.log(chalk.bold(`${provider.displayName} Models:\n`));
60
+ for (const m of provider.models) {
61
+ logger.log(` ${chalk.cyan(m)}`);
62
+ }
63
+ }
64
+ } else {
65
+ logger.error(`Unknown provider: ${options.provider}`);
66
+ }
50
67
  }
51
68
  } catch (err) {
52
69
  spinner.fail(`Failed to list models: ${err.message}`);
@@ -134,4 +151,138 @@ export function registerLlmCommand(program) {
134
151
  process.exit(1);
135
152
  }
136
153
  });
154
+
155
+ // llm providers - list all available providers
156
+ llm
157
+ .command("providers")
158
+ .description("List all available LLM providers")
159
+ .option("--json", "Output as JSON")
160
+ .action(async (options) => {
161
+ try {
162
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
163
+ if (!ctx.db) {
164
+ // Fall back to built-in list without DB
165
+ const providers = Object.values(BUILT_IN_PROVIDERS);
166
+ if (options.json) {
167
+ console.log(JSON.stringify(providers, null, 2));
168
+ } else {
169
+ logger.log(chalk.bold(`LLM Providers (${providers.length}):\n`));
170
+ for (const p of providers) {
171
+ const hasKey = p.apiKeyEnv
172
+ ? process.env[p.apiKeyEnv]
173
+ ? chalk.green("✓")
174
+ : chalk.red("✗")
175
+ : chalk.green("✓");
176
+ logger.log(
177
+ ` ${hasKey} ${chalk.cyan(p.name.padEnd(15))} ${p.displayName}`,
178
+ );
179
+ logger.log(` ${chalk.gray("Models:")} ${p.models.join(", ")}`);
180
+ }
181
+ }
182
+ await shutdown();
183
+ return;
184
+ }
185
+
186
+ const db = ctx.db.getDatabase();
187
+ const registry = new LLMProviderRegistry(db);
188
+ const providers = registry.list();
189
+ const active = registry.getActive();
190
+
191
+ if (options.json) {
192
+ console.log(JSON.stringify({ active, providers }, null, 2));
193
+ } else {
194
+ logger.log(chalk.bold(`LLM Providers (${providers.length}):\n`));
195
+ for (const p of providers) {
196
+ const isActive = p.name === active ? chalk.green(" [active]") : "";
197
+ const keyStatus = p.hasApiKey
198
+ ? chalk.green("✓")
199
+ : chalk.red("✗ key missing");
200
+ const custom = p.custom ? chalk.yellow(" [custom]") : "";
201
+ logger.log(
202
+ ` ${keyStatus} ${chalk.cyan(p.name.padEnd(15))} ${p.displayName}${isActive}${custom}`,
203
+ );
204
+ logger.log(` ${chalk.gray("Models:")} ${p.models.join(", ")}`);
205
+ }
206
+ }
207
+
208
+ await shutdown();
209
+ } catch (err) {
210
+ logger.error(`Failed: ${err.message}`);
211
+ process.exit(1);
212
+ }
213
+ });
214
+
215
+ // llm add-provider - add a custom provider
216
+ llm
217
+ .command("add-provider")
218
+ .description("Add a custom LLM provider")
219
+ .argument("<name>", "Provider name")
220
+ .requiredOption("-u, --base-url <url>", "API base URL")
221
+ .option("-d, --display-name <name>", "Display name")
222
+ .option("-k, --api-key-env <var>", "Environment variable for API key")
223
+ .option("-m, --models <models>", "Comma-separated model names")
224
+ .option("--json", "Output as JSON")
225
+ .action(async (name, options) => {
226
+ try {
227
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
228
+ if (!ctx.db) {
229
+ logger.error("Database not available");
230
+ process.exit(1);
231
+ }
232
+
233
+ const db = ctx.db.getDatabase();
234
+ const registry = new LLMProviderRegistry(db);
235
+ const provider = registry.addProvider(name, {
236
+ displayName: options.displayName || name,
237
+ baseUrl: options.baseUrl,
238
+ apiKeyEnv: options.apiKeyEnv,
239
+ models: options.models
240
+ ? options.models.split(",").map((m) => m.trim())
241
+ : [],
242
+ });
243
+
244
+ if (options.json) {
245
+ console.log(JSON.stringify(provider, null, 2));
246
+ } else {
247
+ logger.success(`Added provider ${chalk.cyan(name)}`);
248
+ logger.log(` ${chalk.gray("URL:")} ${provider.baseUrl}`);
249
+ }
250
+
251
+ await shutdown();
252
+ } catch (err) {
253
+ logger.error(`Failed: ${err.message}`);
254
+ process.exit(1);
255
+ }
256
+ });
257
+
258
+ // llm switch - switch active provider
259
+ llm
260
+ .command("switch")
261
+ .description("Switch the active LLM provider")
262
+ .argument("<name>", "Provider name")
263
+ .option("--json", "Output as JSON")
264
+ .action(async (name, options) => {
265
+ try {
266
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
267
+ if (!ctx.db) {
268
+ logger.error("Database not available");
269
+ process.exit(1);
270
+ }
271
+
272
+ const db = ctx.db.getDatabase();
273
+ const registry = new LLMProviderRegistry(db);
274
+ const provider = registry.setActive(name);
275
+
276
+ if (options.json) {
277
+ console.log(JSON.stringify({ active: name, provider }, null, 2));
278
+ } else {
279
+ logger.success(`Switched to ${chalk.cyan(provider.displayName)}`);
280
+ }
281
+
282
+ await shutdown();
283
+ } catch (err) {
284
+ logger.error(`Failed: ${err.message}`);
285
+ process.exit(1);
286
+ }
287
+ });
137
288
  }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * MCP (Model Context Protocol) commands
3
+ * chainlesschain mcp servers|connect|disconnect|tools|call
4
+ */
5
+
6
+ import chalk from "chalk";
7
+ import ora from "ora";
8
+ import { logger } from "../lib/logger.js";
9
+ import { bootstrap, shutdown } from "../runtime/bootstrap.js";
10
+ import { MCPClient, MCPServerConfig } from "../lib/mcp-client.js";
11
+
12
+ // Singleton MCP client for session reuse
13
+ let mcpClient = null;
14
+
15
+ function getClient() {
16
+ if (!mcpClient) mcpClient = new MCPClient();
17
+ return mcpClient;
18
+ }
19
+
20
+ export function registerMcpCommand(program) {
21
+ const mcp = program
22
+ .command("mcp")
23
+ .description("MCP server management and tool execution");
24
+
25
+ // mcp servers — list configured servers
26
+ mcp
27
+ .command("servers")
28
+ .description("List configured MCP servers")
29
+ .option("--json", "Output as JSON")
30
+ .action(async (options) => {
31
+ try {
32
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
33
+ if (!ctx.db) {
34
+ logger.error("Database not available");
35
+ process.exit(1);
36
+ }
37
+
38
+ const db = ctx.db.getDatabase();
39
+ const config = new MCPServerConfig(db);
40
+ const servers = config.list();
41
+
42
+ if (options.json) {
43
+ console.log(JSON.stringify(servers, null, 2));
44
+ } else if (servers.length === 0) {
45
+ logger.info("No MCP servers configured. Use 'mcp add' to add one.");
46
+ } else {
47
+ logger.log(chalk.bold(`MCP Servers (${servers.length}):\n`));
48
+ for (const s of servers) {
49
+ const auto = s.autoConnect ? chalk.green(" [auto]") : "";
50
+ logger.log(` ${chalk.cyan(s.name)}${auto}`);
51
+ logger.log(
52
+ ` ${chalk.gray("Command:")} ${s.command} ${s.args.join(" ")}`,
53
+ );
54
+ }
55
+ }
56
+
57
+ await shutdown();
58
+ } catch (err) {
59
+ logger.error(`Failed: ${err.message}`);
60
+ process.exit(1);
61
+ }
62
+ });
63
+
64
+ // mcp add — add/update a server config
65
+ mcp
66
+ .command("add")
67
+ .description("Add or update an MCP server configuration")
68
+ .argument("<name>", "Server name")
69
+ .requiredOption("-c, --command <cmd>", "Server command to run")
70
+ .option("-a, --args <args>", "Command arguments (comma-separated)")
71
+ .option("--auto-connect", "Auto-connect on startup")
72
+ .option("--json", "Output as JSON")
73
+ .action(async (name, options) => {
74
+ try {
75
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
76
+ if (!ctx.db) {
77
+ logger.error("Database not available");
78
+ process.exit(1);
79
+ }
80
+
81
+ const db = ctx.db.getDatabase();
82
+ const config = new MCPServerConfig(db);
83
+ const args = options.args
84
+ ? options.args.split(",").map((a) => a.trim())
85
+ : [];
86
+
87
+ config.add(name, {
88
+ command: options.command,
89
+ args,
90
+ autoConnect: !!options.autoConnect,
91
+ });
92
+
93
+ if (options.json) {
94
+ console.log(
95
+ JSON.stringify({
96
+ name,
97
+ command: options.command,
98
+ args,
99
+ autoConnect: !!options.autoConnect,
100
+ }),
101
+ );
102
+ } else {
103
+ logger.success(`MCP server "${chalk.cyan(name)}" configured`);
104
+ logger.log(
105
+ ` ${chalk.gray("Command:")} ${options.command} ${args.join(" ")}`,
106
+ );
107
+ }
108
+
109
+ await shutdown();
110
+ } catch (err) {
111
+ logger.error(`Failed: ${err.message}`);
112
+ process.exit(1);
113
+ }
114
+ });
115
+
116
+ // mcp remove — remove a server config
117
+ mcp
118
+ .command("remove")
119
+ .description("Remove an MCP server configuration")
120
+ .argument("<name>", "Server name")
121
+ .action(async (name) => {
122
+ try {
123
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
124
+ if (!ctx.db) {
125
+ logger.error("Database not available");
126
+ process.exit(1);
127
+ }
128
+
129
+ const db = ctx.db.getDatabase();
130
+ const config = new MCPServerConfig(db);
131
+ const removed = config.remove(name);
132
+
133
+ if (removed) {
134
+ logger.success(`MCP server "${name}" removed`);
135
+ } else {
136
+ logger.error(`Server "${name}" not found`);
137
+ }
138
+
139
+ await shutdown();
140
+ } catch (err) {
141
+ logger.error(`Failed: ${err.message}`);
142
+ process.exit(1);
143
+ }
144
+ });
145
+
146
+ // mcp connect — connect to a server
147
+ mcp
148
+ .command("connect")
149
+ .description("Connect to an MCP server")
150
+ .argument("<name>", "Server name (configured or command)")
151
+ .option("--json", "Output as JSON")
152
+ .action(async (name, options) => {
153
+ try {
154
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
155
+ if (!ctx.db) {
156
+ logger.error("Database not available");
157
+ process.exit(1);
158
+ }
159
+
160
+ const db = ctx.db.getDatabase();
161
+ const config = new MCPServerConfig(db);
162
+ const serverConfig = config.get(name);
163
+
164
+ if (!serverConfig) {
165
+ logger.error(`Server "${name}" not configured. Use 'mcp add' first.`);
166
+ process.exit(1);
167
+ }
168
+
169
+ const spinner = ora(`Connecting to ${name}...`).start();
170
+ const client = getClient();
171
+
172
+ const result = await client.connect(name, serverConfig);
173
+ spinner.stop();
174
+
175
+ if (options.json) {
176
+ console.log(JSON.stringify(result, null, 2));
177
+ } else {
178
+ logger.success(`Connected to ${chalk.cyan(name)}`);
179
+ logger.log(` ${chalk.gray("Tools:")} ${result.tools.length}`);
180
+ logger.log(
181
+ ` ${chalk.gray("Resources:")} ${result.resources.length}`,
182
+ );
183
+ if (result.serverInfo?.name) {
184
+ logger.log(` ${chalk.gray("Server:")} ${result.serverInfo.name}`);
185
+ }
186
+ }
187
+
188
+ // Don't shutdown — keep connection alive for subsequent calls
189
+ } catch (err) {
190
+ logger.error(`Connection failed: ${err.message}`);
191
+ process.exit(1);
192
+ }
193
+ });
194
+
195
+ // mcp disconnect — disconnect from a server
196
+ mcp
197
+ .command("disconnect")
198
+ .description("Disconnect from an MCP server")
199
+ .argument("<name>", "Server name")
200
+ .action(async (name) => {
201
+ try {
202
+ const client = getClient();
203
+ const ok = await client.disconnect(name);
204
+ if (ok) {
205
+ logger.success(`Disconnected from ${name}`);
206
+ } else {
207
+ logger.error(`Server "${name}" not connected`);
208
+ }
209
+ } catch (err) {
210
+ logger.error(`Disconnect failed: ${err.message}`);
211
+ process.exit(1);
212
+ }
213
+ });
214
+
215
+ // mcp tools — list available tools
216
+ mcp
217
+ .command("tools")
218
+ .description("List available MCP tools")
219
+ .option("-s, --server <name>", "Filter by server name")
220
+ .option("--json", "Output as JSON")
221
+ .action(async (options) => {
222
+ try {
223
+ const client = getClient();
224
+ const tools = client.listTools(options.server);
225
+
226
+ if (options.json) {
227
+ console.log(JSON.stringify(tools, null, 2));
228
+ } else if (tools.length === 0) {
229
+ logger.info("No tools available. Connect to a server first.");
230
+ } else {
231
+ logger.log(chalk.bold(`MCP Tools (${tools.length}):\n`));
232
+ for (const t of tools) {
233
+ logger.log(
234
+ ` ${chalk.cyan(t.name)} ${chalk.gray(`[${t.server}]`)}`,
235
+ );
236
+ if (t.description) {
237
+ logger.log(` ${chalk.gray(t.description)}`);
238
+ }
239
+ }
240
+ }
241
+ } catch (err) {
242
+ logger.error(`Failed: ${err.message}`);
243
+ process.exit(1);
244
+ }
245
+ });
246
+
247
+ // mcp call — call a tool
248
+ mcp
249
+ .command("call")
250
+ .description("Call an MCP tool")
251
+ .argument("<tool>", "Tool name")
252
+ .option("-s, --server <name>", "Server name")
253
+ .option("-a, --args <json>", "Tool arguments as JSON")
254
+ .option("--json", "Output as JSON")
255
+ .action(async (tool, options) => {
256
+ try {
257
+ const client = getClient();
258
+
259
+ // Find which server has this tool
260
+ let serverName = options.server;
261
+ if (!serverName) {
262
+ const allTools = client.listTools();
263
+ const match = allTools.find((t) => t.name === tool);
264
+ if (!match) {
265
+ logger.error(
266
+ `Tool "${tool}" not found. Run 'mcp tools' to see available tools.`,
267
+ );
268
+ process.exit(1);
269
+ }
270
+ serverName = match.server;
271
+ }
272
+
273
+ const args = options.args ? JSON.parse(options.args) : {};
274
+ const spinner = ora(`Calling ${tool}...`).start();
275
+
276
+ const result = await client.callTool(serverName, tool, args);
277
+ spinner.stop();
278
+
279
+ if (options.json) {
280
+ console.log(JSON.stringify(result, null, 2));
281
+ } else {
282
+ // Display content blocks
283
+ if (result?.content) {
284
+ for (const block of result.content) {
285
+ if (block.type === "text") {
286
+ logger.log(block.text);
287
+ } else if (block.type === "image") {
288
+ logger.log(chalk.gray(`[Image: ${block.mimeType || "image"}]`));
289
+ } else {
290
+ logger.log(JSON.stringify(block, null, 2));
291
+ }
292
+ }
293
+ } else {
294
+ logger.log(JSON.stringify(result, null, 2));
295
+ }
296
+ }
297
+ } catch (err) {
298
+ logger.error(`Tool call failed: ${err.message}`);
299
+ process.exit(1);
300
+ }
301
+ });
302
+ }