@kolisachint/hoocode-agent 0.4.33 → 0.4.35

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.
@@ -17,6 +17,7 @@
17
17
  import { spawn } from "node:child_process";
18
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
19
  import { readdir } from "node:fs/promises";
20
+ import { homedir } from "node:os";
20
21
  import { join, relative } from "node:path";
21
22
  import { createInterface } from "node:readline";
22
23
  import { Type } from "typebox";
@@ -382,8 +383,70 @@ function buildMcpSchema(tool) {
382
383
  }
383
384
  return Type.Object(shape);
384
385
  }
386
+ /**
387
+ * Parse standard MCP config format (used by Claude Desktop, VS Code, etc.)
388
+ * into hoocode's McpServerConfig format.
389
+ */
390
+ function parseStandardMcpConfig(config, _source) {
391
+ if (!config.mcpServers)
392
+ return [];
393
+ const servers = [];
394
+ for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
395
+ servers.push({
396
+ name,
397
+ command: serverConfig.command,
398
+ args: serverConfig.args,
399
+ env: serverConfig.env,
400
+ });
401
+ }
402
+ return servers;
403
+ }
404
+ /**
405
+ * Load MCP servers from a standard mcp.json file.
406
+ * Returns an array of McpServerConfig, or empty array if file doesn't exist or is invalid.
407
+ */
408
+ function loadStandardMcpFile(filePath) {
409
+ if (!existsSync(filePath))
410
+ return [];
411
+ try {
412
+ const content = readFileSync(filePath, "utf8");
413
+ const config = JSON.parse(content);
414
+ return parseStandardMcpConfig(config, filePath);
415
+ }
416
+ catch {
417
+ return [];
418
+ }
419
+ }
385
420
  export function setupMcpLoader(pi) {
386
421
  pi.on("session_start", async (_event, ctx) => {
422
+ const allServerConfigs = [];
423
+ const seenNames = new Set();
424
+ // 1. Load from standard mcp.json locations
425
+ // User-level: ~/.agents/mcp.json
426
+ const userAgentsConfig = loadStandardMcpFile(join(homedir(), ".agents", "mcp.json"));
427
+ for (const config of userAgentsConfig) {
428
+ if (!seenNames.has(config.name)) {
429
+ seenNames.add(config.name);
430
+ allServerConfigs.push(config);
431
+ }
432
+ }
433
+ // Project-level: ./.agents/mcp.json
434
+ const projectAgentsConfig = loadStandardMcpFile(join(ctx.cwd, ".agents", "mcp.json"));
435
+ for (const config of projectAgentsConfig) {
436
+ if (!seenNames.has(config.name)) {
437
+ seenNames.add(config.name);
438
+ allServerConfigs.push(config);
439
+ }
440
+ }
441
+ // Claude Desktop: ~/.config/claude/mcp.json
442
+ const claudeDesktopConfig = loadStandardMcpFile(join(homedir(), ".config", "claude", "mcp.json"));
443
+ for (const config of claudeDesktopConfig) {
444
+ if (!seenNames.has(config.name)) {
445
+ seenNames.add(config.name);
446
+ allServerConfigs.push(config);
447
+ }
448
+ }
449
+ // 2. Load from hoocode's per-server format (existing behavior)
387
450
  const searchDirs = [join(HOOCODE_DIR, "mcp-servers"), join(ctx.cwd, ".hoocode", "mcp-servers")];
388
451
  for (const dir of searchDirs) {
389
452
  if (!existsSync(dir))
@@ -409,53 +472,61 @@ export function setupMcpLoader(pi) {
409
472
  ctx.ui.notify(`MCP: failed to parse "${file}": ${String(err)}`, "error");
410
473
  continue;
411
474
  }
412
- try {
413
- const { tools } = await connectMcpServer(serverConfig);
414
- for (const tool of tools) {
415
- const toolName = `mcp_${serverConfig.name}_${tool.name}`;
416
- const schema = buildMcpSchema(tool);
417
- const capturedServer = serverConfig.name;
418
- const capturedTool = tool.name;
419
- pi.registerTool({
420
- name: toolName,
421
- label: `[MCP] ${serverConfig.name} › ${tool.name}`,
422
- description: tool.description,
423
- parameters: schema,
424
- async execute(_toolCallId, params, signal, _onUpdate) {
425
- const activeConn = mcpConnections.get(capturedServer);
426
- if (!activeConn) {
427
- return {
428
- content: [
429
- {
430
- type: "text",
431
- text: `MCP server "${capturedServer}" is not connected`,
432
- },
433
- ],
434
- details: undefined,
435
- };
436
- }
437
- const abortPromise = new Promise((_, reject) => {
438
- signal.addEventListener("abort", () => reject(new Error("Aborted")));
439
- });
440
- const result = await Promise.race([
441
- activeConn.rpc("tools/call", {
442
- name: capturedTool,
443
- arguments: params,
444
- }),
445
- abortPromise,
446
- ]);
475
+ // Skip if already loaded from standard config
476
+ if (seenNames.has(serverConfig.name))
477
+ continue;
478
+ seenNames.add(serverConfig.name);
479
+ allServerConfigs.push(serverConfig);
480
+ }
481
+ }
482
+ // 3. Connect to all servers and register tools
483
+ for (const serverConfig of allServerConfigs) {
484
+ try {
485
+ const { tools } = await connectMcpServer(serverConfig);
486
+ for (const tool of tools) {
487
+ const toolName = `mcp_${serverConfig.name}_${tool.name}`;
488
+ const schema = buildMcpSchema(tool);
489
+ const capturedServer = serverConfig.name;
490
+ const capturedTool = tool.name;
491
+ pi.registerTool({
492
+ name: toolName,
493
+ label: `[MCP] ${serverConfig.name} › ${tool.name}`,
494
+ description: tool.description,
495
+ parameters: schema,
496
+ async execute(_toolCallId, params, signal, _onUpdate) {
497
+ const activeConn = mcpConnections.get(capturedServer);
498
+ if (!activeConn) {
447
499
  return {
448
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
500
+ content: [
501
+ {
502
+ type: "text",
503
+ text: `MCP server "${capturedServer}" is not connected`,
504
+ },
505
+ ],
449
506
  details: undefined,
450
507
  };
451
- },
452
- });
453
- }
454
- ctx.ui.notify(`MCP: connected "${serverConfig.name}" (${tools.length} tool${tools.length === 1 ? "" : "s"})`, "info");
455
- }
456
- catch (err) {
457
- ctx.ui.notify(`MCP: failed to connect "${serverConfig.name}": ${String(err)}`, "error");
508
+ }
509
+ const abortPromise = new Promise((_, reject) => {
510
+ signal.addEventListener("abort", () => reject(new Error("Aborted")));
511
+ });
512
+ const result = await Promise.race([
513
+ activeConn.rpc("tools/call", {
514
+ name: capturedTool,
515
+ arguments: params,
516
+ }),
517
+ abortPromise,
518
+ ]);
519
+ return {
520
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
521
+ details: undefined,
522
+ };
523
+ },
524
+ });
458
525
  }
526
+ ctx.ui.notify(`MCP: connected "${serverConfig.name}" (${tools.length} tool${tools.length === 1 ? "" : "s"})`, "info");
527
+ }
528
+ catch (err) {
529
+ ctx.ui.notify(`MCP: failed to connect "${serverConfig.name}": ${String(err)}`, "error");
459
530
  }
460
531
  }
461
532
  });
@@ -762,7 +833,7 @@ export function setupMode(pi) {
762
833
  });
763
834
  }
764
835
  // ============================================================================
765
- // Scaffold commands — /new-skill and /new-agent
836
+ // Scaffold commands — /new-skill, /new-agent, and /new-command
766
837
  // ============================================================================
767
838
  /** Validates a resource name: lowercase a-z, 0-9, hyphens, no leading/trailing/double hyphens. */
768
839
  function validateResourceName(name) {
@@ -860,6 +931,46 @@ function setupScaffold(pi) {
860
931
  ctx.ui.notify(`Agent created: ${join(".hoocode", "agents", `${name}.md`)}\nEdit the file, then run /reload to activate it.`, "info");
861
932
  },
862
933
  });
934
+ // ── /new-command <name> ───────────────────────────────────────────────────
935
+ // Creates .hoocode/commands/<name>.md with a slash-command prompt-template
936
+ // frontmatter (name, description, argument-hint) so it is ready to edit and
937
+ // picked up on next reload. Body supports $1, $@, $ARGUMENTS placeholders.
938
+ pi.registerCommand("new-command", {
939
+ description: "Scaffold a new slash command. Usage: /new-command <name>",
940
+ getArgumentCompletions: () => [],
941
+ handler: async (args, ctx) => {
942
+ const name = args.trim();
943
+ const error = validateResourceName(name);
944
+ if (error) {
945
+ ctx.ui.notify(`/new-command: ${error}. Usage: /new-command <name>`, "warning");
946
+ return;
947
+ }
948
+ const commandsDir = join(ctx.cwd, ".hoocode", "commands");
949
+ const commandFile = join(commandsDir, `${name}.md`);
950
+ if (existsSync(commandFile)) {
951
+ ctx.ui.notify(`/new-command: ${commandFile} already exists`, "warning");
952
+ return;
953
+ }
954
+ mkdirSync(commandsDir, { recursive: true });
955
+ writeFileSync(commandFile, [
956
+ "---",
957
+ `name: ${name}`,
958
+ "description: |",
959
+ ` TODO: describe what /${name} does and when to use it.`,
960
+ ` Usage: /${name} <args>`,
961
+ "argument-hint: <args>",
962
+ "---",
963
+ `Run the /${name} command with arguments: **$ARGUMENTS**.`,
964
+ "",
965
+ "TODO: write the instructions here. Placeholders you can use:",
966
+ "- $1, $2, ... for positional arguments",
967
+ "- $@ or $ARGUMENTS for all arguments",
968
+ `- $${"{"}@:N} / $${"{"}@:N:L} for bash-style slices`,
969
+ "",
970
+ ].join("\n"), "utf8");
971
+ ctx.ui.notify(`Command created: ${join(".hoocode", "commands", `${name}.md`)}\nEdit the file, then run /reload to activate it.`, "info");
972
+ },
973
+ });
863
974
  }
864
975
  // ============================================================================
865
976
  // D. Options pane — ask_options tool