Skip to content

Commit 9a21972

Browse files
authored
Add toolcall cmd and completions (#1167)
Signed-off-by: Marcos Candeia <[email protected]>
1 parent 57e8ca9 commit 9a21972

File tree

6 files changed

+646
-11
lines changed

6 files changed

+646
-11
lines changed

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "deco-cli",
3-
"version": "0.17.7",
3+
"version": "0.18.0",
44
"description": "CLI for managing decocms.com apps & projects",
55
"license": "MIT",
66
"author": "Deco team",

packages/cli/src/cli.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ import { genEnv } from "./commands/gen/gen.js";
6565
import { upgradeCommand } from "./commands/update/upgrade.js";
6666
import { updateCommand } from "./commands/update/update.js";
6767
import { addCommand } from "./commands/add/add.js";
68+
import {
69+
callToolCommand,
70+
autocompleteIntegrations,
71+
} from "./commands/tools/call-tool.js";
72+
import { completionCommand } from "./commands/completion/completion.js";
73+
import { installCompletionCommand } from "./commands/completion/install.js";
6874
import { detectRuntime } from "./lib/runtime.js";
6975

7076
const __filename = fileURLToPath(import.meta.url);
@@ -370,6 +376,111 @@ const add = new Command("add")
370376
}
371377
});
372378

379+
// Call-tool command implementation
380+
const callTool = new Command("call-tool")
381+
.description("Call a tool on an integration using MCP protocol.")
382+
.argument("<tool>", "Name of the tool to call")
383+
.option(
384+
"-i, --integration <integration>",
385+
"Integration ID to call the tool on",
386+
)
387+
.option("-p, --payload <payload>", "JSON payload to send to the tool")
388+
.option(
389+
"--set <key=value>",
390+
"Set a key-value pair in the payload (can be used multiple times)",
391+
(value, previous: string[] | undefined) => {
392+
return previous ? [...previous, value] : [value];
393+
},
394+
)
395+
.option("-w, --workspace <workspace>", "Workspace name")
396+
.configureHelp({
397+
subcommandTerm: (cmd) => cmd.name(), // for auto-completion
398+
})
399+
.action(async (toolName, options) => {
400+
// Validate required integration parameter
401+
if (!options.integration) {
402+
console.error(
403+
"❌ Integration ID is required. Use -i or --integration flag.",
404+
);
405+
406+
// Show available integrations for user convenience
407+
try {
408+
console.log("🔍 Available integrations:");
409+
const integrations = await autocompleteIntegrations("");
410+
if (integrations.length > 0) {
411+
integrations.slice(0, 10).forEach((id) => console.log(` • ${id}`));
412+
if (integrations.length > 10) {
413+
console.log(` ... and ${integrations.length - 10} more`);
414+
}
415+
} else {
416+
console.log(
417+
" No integrations found. Run 'deco add' to add integrations.",
418+
);
419+
}
420+
} catch {
421+
console.log(" Run 'deco add' to add integrations.");
422+
}
423+
424+
process.exit(1);
425+
}
426+
427+
try {
428+
await callToolCommand(toolName, {
429+
integration: options.integration,
430+
payload: options.payload,
431+
set: options.set,
432+
workspace: options.workspace,
433+
});
434+
} catch (error) {
435+
console.error(
436+
"❌ Tool call failed:",
437+
error instanceof Error ? error.message : String(error),
438+
);
439+
process.exit(1);
440+
}
441+
});
442+
443+
// Completion command implementation (internal command)
444+
const completion = new Command("completion")
445+
.description("Generate shell completions (internal command)")
446+
.argument("<type>", "Type of completion to generate")
447+
.option("--current <current>", "Current word being completed")
448+
.option("--previous <previous>", "Previous word in command line")
449+
.option("--line <line>", "Full command line")
450+
.action(async (type, options) => {
451+
try {
452+
await completionCommand(type, {
453+
current: options.current,
454+
previous: options.previous,
455+
line: options.line,
456+
});
457+
} catch {
458+
// Silently fail for completions
459+
}
460+
});
461+
462+
// Install completion command
463+
const installCompletion = new Command("install-completion")
464+
.description("Install shell completion scripts")
465+
.argument(
466+
"[shell]",
467+
"Target shell (bash, zsh). Auto-detected if not specified",
468+
)
469+
.option("-o, --output <path>", "Output path for completion script")
470+
.action(async (shell, options) => {
471+
try {
472+
await installCompletionCommand(shell, {
473+
output: options.output,
474+
});
475+
} catch (error) {
476+
console.error(
477+
"❌ Failed to install completion:",
478+
error instanceof Error ? error.message : String(error),
479+
);
480+
process.exit(1);
481+
}
482+
});
483+
373484
// Hosting parent command
374485
const hosting = new Command("hosting")
375486
.description("Manage hosting apps in a workspace.")
@@ -463,10 +574,13 @@ const program = new Command()
463574
.addCommand(dev)
464575
.addCommand(configure)
465576
.addCommand(add)
577+
.addCommand(callTool)
466578
.addCommand(upgrade)
467579
.addCommand(update)
468580
.addCommand(linkCmd)
469581
.addCommand(gen)
470-
.addCommand(create);
582+
.addCommand(create)
583+
.addCommand(completion)
584+
.addCommand(installCompletion);
471585

472586
program.parse();
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
autocompleteIntegrations,
3+
autocompleteTools,
4+
} from "../tools/call-tool.js";
5+
6+
interface CompletionOptions {
7+
current: string;
8+
previous: string;
9+
line: string;
10+
}
11+
12+
/**
13+
* Generate completions for the call-tool command
14+
*/
15+
export async function generateCompletions(
16+
options: CompletionOptions,
17+
): Promise<void> {
18+
const { current, previous, line } = options;
19+
20+
try {
21+
// Parse the command line to understand context
22+
const words = line.split(/\s+/);
23+
const commandIndex = words.findIndex((word) => word === "call-tool");
24+
25+
if (commandIndex === -1) {
26+
return;
27+
}
28+
29+
// Extract arguments and options
30+
const args = words.slice(commandIndex + 1);
31+
const integrationIndex = args.findIndex(
32+
(arg) => arg === "-i" || arg === "--integration",
33+
);
34+
const integration =
35+
integrationIndex !== -1 && integrationIndex + 1 < args.length
36+
? args[integrationIndex + 1]
37+
: undefined;
38+
39+
let completions: string[] = [];
40+
41+
// Determine what to complete based on context
42+
if (previous === "-i" || previous === "--integration") {
43+
// Complete integration IDs
44+
completions = await autocompleteIntegrations(current);
45+
} else if (integration && !current.startsWith("-")) {
46+
// Complete tool names if we have an integration and current is not an option
47+
const toolIndex = args.findIndex(
48+
(arg) => !arg.startsWith("-") && arg !== integration,
49+
);
50+
if (toolIndex === -1 || args[toolIndex] === current) {
51+
completions = await autocompleteTools(current, { integration });
52+
}
53+
} else if (!current.startsWith("-") && !integration) {
54+
// If no integration specified yet, suggest common options
55+
completions = [
56+
"-i",
57+
"--integration",
58+
"-p",
59+
"--payload",
60+
"--set",
61+
"-w",
62+
"--workspace",
63+
];
64+
}
65+
66+
// Output completions (one per line for shell completion)
67+
completions.forEach((completion) => console.log(completion));
68+
} catch (error) {
69+
// Silently fail for completions to avoid breaking shell completion
70+
console.error("Completion error:", error);
71+
}
72+
}
73+
74+
/**
75+
* Main completion command handler
76+
*/
77+
export async function completionCommand(
78+
type: string,
79+
options: { current?: string; previous?: string; line?: string },
80+
): Promise<void> {
81+
if (type === "call-tool") {
82+
await generateCompletions({
83+
current: options.current || "",
84+
previous: options.previous || "",
85+
line: options.line || "",
86+
});
87+
}
88+
}

0 commit comments

Comments
 (0)