@lgcyaxi/oh-my-claude 1.0.0 → 1.1.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/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
  2. package/CHANGELOG.md +53 -0
  3. package/CLAUDE.md +60 -0
  4. package/README.md +97 -12
  5. package/README.zh-CN.md +58 -11
  6. package/bin/oh-my-claude.js +4 -2
  7. package/changelog/v1.0.0.md +28 -0
  8. package/changelog/v1.0.1.md +28 -0
  9. package/changelog/v1.1.0.md +20 -0
  10. package/changelog/v1.1.1.md +71 -0
  11. package/dist/cli.js +213 -4
  12. package/dist/hooks/comment-checker.js +1 -1
  13. package/dist/hooks/task-notification.js +124 -0
  14. package/dist/hooks/task-tracker.js +144 -0
  15. package/dist/index-1dv6t98k.js +7654 -0
  16. package/dist/index-5ars1tn4.js +7348 -0
  17. package/dist/index-d79fk9ah.js +7350 -0
  18. package/dist/index-hzm01rkh.js +7654 -0
  19. package/dist/index-qrbfj4cd.js +7664 -0
  20. package/dist/index-ypyx3ye0.js +7349 -0
  21. package/dist/index.js +24 -1
  22. package/dist/mcp/server.js +202 -45
  23. package/dist/statusline/statusline.js +146 -0
  24. package/package.json +6 -5
  25. package/src/agents/document-writer.ts +5 -0
  26. package/src/agents/explore.ts +5 -0
  27. package/src/agents/frontend-ui-ux.ts +5 -0
  28. package/src/agents/librarian.ts +5 -0
  29. package/src/agents/oracle.ts +5 -0
  30. package/src/agents/types.ts +11 -0
  31. package/src/cli.ts +261 -2
  32. package/src/commands/index.ts +8 -1
  33. package/src/commands/omc-status.md +71 -0
  34. package/src/commands/omcx-issue.md +175 -0
  35. package/src/commands/ulw.md +144 -0
  36. package/src/config/loader.ts +73 -0
  37. package/src/config/schema.ts +25 -2
  38. package/src/generators/agent-generator.ts +17 -1
  39. package/src/hooks/comment-checker.ts +2 -2
  40. package/src/hooks/task-notification.ts +206 -0
  41. package/src/hooks/task-tracker.ts +252 -0
  42. package/src/installer/index.ts +55 -4
  43. package/src/installer/settings-merger.ts +86 -0
  44. package/src/installer/statusline-merger.ts +169 -0
  45. package/src/mcp/background-agent-server/server.ts +11 -2
  46. package/src/mcp/background-agent-server/task-manager.ts +83 -4
  47. package/src/providers/router.ts +28 -0
  48. package/src/statusline/formatter.ts +164 -0
  49. package/src/statusline/statusline.ts +103 -0
package/dist/cli.js CHANGED
@@ -2,13 +2,18 @@
2
2
  import {
3
3
  __commonJS,
4
4
  __require,
5
+ __toCommonJS,
5
6
  __toESM,
6
7
  checkInstallation,
8
+ exports_installer,
9
+ exports_settings_merger,
7
10
  getProvidersStatus,
11
+ init_installer,
12
+ init_settings_merger,
8
13
  install,
9
14
  loadConfig,
10
15
  uninstall
11
- } from "./index-ejmtq3zc.js";
16
+ } from "./index-qrbfj4cd.js";
12
17
 
13
18
  // node_modules/commander/lib/error.js
14
19
  var require_error = __commonJS((exports) => {
@@ -2120,7 +2125,8 @@ var {
2120
2125
  } = import__.default;
2121
2126
 
2122
2127
  // src/cli.ts
2123
- program.name("oh-my-claude").description("Multi-agent orchestration plugin for Claude Code").version("0.1.0");
2128
+ init_installer();
2129
+ program.name("oh-my-claude").description("Multi-agent orchestration plugin for Claude Code").version("1.0.3");
2124
2130
  program.command("install").description("Install oh-my-claude into Claude Code").option("--skip-agents", "Skip agent file generation").option("--skip-hooks", "Skip hooks installation").option("--skip-mcp", "Skip MCP server installation").option("--force", "Force overwrite existing files").action(async (options) => {
2125
2131
  console.log(`Installing oh-my-claude...
2126
2132
  `);
@@ -2263,6 +2269,7 @@ program.command("doctor").description("Diagnose oh-my-claude configuration").opt
2263
2269
  console.log(` ${status.components.agents ? ok("Agent files generated") : fail("Agent files generated")}`);
2264
2270
  console.log(` ${status.components.hooks ? ok("Hooks configured") : fail("Hooks configured")}`);
2265
2271
  console.log(` ${status.components.mcp ? ok("MCP server configured") : fail("MCP server configured")}`);
2272
+ console.log(` ${status.components.statusLine ? ok("StatusLine configured") : warn("StatusLine not configured")}`);
2266
2273
  console.log(` ${status.components.config ? ok("Configuration file exists") : fail("Configuration file exists")}`);
2267
2274
  if (detail) {
2268
2275
  console.log(`
@@ -2296,10 +2303,12 @@ ${header("Commands (detailed):")}`);
2296
2303
  "omc-explore",
2297
2304
  "omc-plan",
2298
2305
  "omc-start-work",
2306
+ "omc-status",
2299
2307
  "omcx-commit",
2300
2308
  "omcx-implement",
2301
2309
  "omcx-refactor",
2302
- "omcx-docs"
2310
+ "omcx-docs",
2311
+ "omcx-issue"
2303
2312
  ];
2304
2313
  console.log(` ${subheader("Agent commands (omc-):")}`);
2305
2314
  for (const cmd of expectedCommands.filter((c2) => c2.startsWith("omc-"))) {
@@ -2341,12 +2350,41 @@ ${header("MCP Server (detailed):")}`);
2341
2350
  console.log(`
2342
2351
  ${header("Hooks (detailed):")}`);
2343
2352
  const hooksDir = join(homedir(), ".claude", "oh-my-claude", "hooks");
2344
- const expectedHooks = ["comment-checker.js", "todo-continuation.js"];
2353
+ const expectedHooks = ["comment-checker.js", "todo-continuation.js", "task-notification.js"];
2345
2354
  for (const hook of expectedHooks) {
2346
2355
  const hookPath = join(hooksDir, hook);
2347
2356
  const exists = existsSync(hookPath);
2348
2357
  console.log(` ${exists ? ok(hook) : fail(hook)}`);
2349
2358
  }
2359
+ console.log(`
2360
+ ${header("StatusLine (detailed):")}`);
2361
+ const statusLineDir = join(homedir(), ".claude", "oh-my-claude", "dist", "statusline");
2362
+ const statusLineScript = join(statusLineDir, "statusline.js");
2363
+ const statusFileExists = existsSync(statusLineScript);
2364
+ console.log(` ${statusFileExists ? ok("statusline.js installed") : fail("statusline.js not installed")}`);
2365
+ try {
2366
+ const settingsPath = join(homedir(), ".claude", "settings.json");
2367
+ if (existsSync(settingsPath)) {
2368
+ const settings = JSON.parse(__require("node:fs").readFileSync(settingsPath, "utf-8"));
2369
+ if (settings.statusLine) {
2370
+ const cmd = settings.statusLine.command || "";
2371
+ const isOurs = cmd.includes("oh-my-claude");
2372
+ const isWrapper = cmd.includes("statusline-wrapper");
2373
+ console.log(` ${ok("StatusLine configured in settings.json")}`);
2374
+ if (isWrapper) {
2375
+ console.log(` Mode: ${c.yellow}Merged (wrapper)${c.reset}`);
2376
+ } else if (isOurs) {
2377
+ console.log(` Mode: ${c.green}Direct${c.reset}`);
2378
+ } else {
2379
+ console.log(` Mode: ${c.cyan}External${c.reset}`);
2380
+ }
2381
+ } else {
2382
+ console.log(` ${warn("StatusLine not configured in settings.json")}`);
2383
+ }
2384
+ }
2385
+ } catch {
2386
+ console.log(` ${fail("Failed to read settings.json")}`);
2387
+ }
2350
2388
  }
2351
2389
  console.log(`
2352
2390
  ${header("Providers:")}`);
@@ -2404,6 +2442,177 @@ ${header("Recommendations:")}`);
2404
2442
  ${dimText("Tip: Run 'oh-my-claude doctor --detail' for detailed component status")}`);
2405
2443
  }
2406
2444
  });
2445
+ program.command("update").description("Update oh-my-claude to the latest version").option("--check", "Only check for updates without installing").option("--force", "Force update even if already on latest version").action(async (options) => {
2446
+ const { execSync } = __require("node:child_process");
2447
+ const { readFileSync, existsSync } = __require("node:fs");
2448
+ const { join } = __require("node:path");
2449
+ const { homedir } = __require("node:os");
2450
+ const useColor = process.stdout.isTTY;
2451
+ const c = {
2452
+ reset: useColor ? "\x1B[0m" : "",
2453
+ bold: useColor ? "\x1B[1m" : "",
2454
+ dim: useColor ? "\x1B[2m" : "",
2455
+ green: useColor ? "\x1B[32m" : "",
2456
+ red: useColor ? "\x1B[31m" : "",
2457
+ yellow: useColor ? "\x1B[33m" : "",
2458
+ cyan: useColor ? "\x1B[36m" : "",
2459
+ magenta: useColor ? "\x1B[35m" : ""
2460
+ };
2461
+ const ok = (text) => `${c.green}✓${c.reset} ${text}`;
2462
+ const fail = (text) => `${c.red}✗${c.reset} ${text}`;
2463
+ const warn = (text) => `${c.yellow}⚠${c.reset} ${text}`;
2464
+ const header = (text) => `${c.cyan}${c.bold}${text}${c.reset}`;
2465
+ const dimText = (text) => `${c.dim}${text}${c.reset}`;
2466
+ const PACKAGE_NAME = "@lgcyaxi/oh-my-claude";
2467
+ console.log(`${c.bold}${c.magenta}oh-my-claude Update${c.reset}
2468
+ `);
2469
+ let currentVersion = "unknown";
2470
+ try {
2471
+ const installDir = join(homedir(), ".claude", "oh-my-claude");
2472
+ const localPkgPath = join(installDir, "package.json");
2473
+ if (existsSync(localPkgPath)) {
2474
+ const pkg = JSON.parse(readFileSync(localPkgPath, "utf-8"));
2475
+ currentVersion = pkg.version;
2476
+ } else {
2477
+ currentVersion = program.version() || "unknown";
2478
+ }
2479
+ } catch (error) {
2480
+ currentVersion = program.version() || "unknown";
2481
+ }
2482
+ console.log(`Current version: ${c.cyan}${currentVersion}${c.reset}`);
2483
+ let latestVersion = "unknown";
2484
+ try {
2485
+ console.log(`${dimText("Checking npm registry for latest version...")}`);
2486
+ const npmInfo = execSync(`npm view ${PACKAGE_NAME} version 2>/dev/null`, {
2487
+ encoding: "utf-8"
2488
+ }).trim();
2489
+ latestVersion = npmInfo;
2490
+ console.log(`Latest version: ${c.cyan}${latestVersion}${c.reset}
2491
+ `);
2492
+ } catch (error) {
2493
+ console.log(`${fail("Failed to fetch latest version from npm")}`);
2494
+ console.log(`${dimText("Check your internet connection or try again later")}
2495
+ `);
2496
+ process.exit(1);
2497
+ }
2498
+ const isUpToDate = currentVersion === latestVersion;
2499
+ const needsUpdate = !isUpToDate || options.force;
2500
+ if (isUpToDate && !options.force) {
2501
+ console.log(ok("You are already on the latest version!"));
2502
+ process.exit(0);
2503
+ }
2504
+ if (options.check) {
2505
+ if (needsUpdate) {
2506
+ console.log(warn(`Update available: ${currentVersion} → ${latestVersion}`));
2507
+ console.log(`
2508
+ Run ${c.cyan}npx ${PACKAGE_NAME} update${c.reset} to update.`);
2509
+ }
2510
+ process.exit(0);
2511
+ }
2512
+ console.log(header(`Updating oh-my-claude...
2513
+ `));
2514
+ try {
2515
+ console.log(`${dimText("Clearing npx cache...")}`);
2516
+ try {
2517
+ execSync(`npx --yes clear-npx-cache 2>/dev/null || true`, { stdio: "pipe" });
2518
+ } catch {}
2519
+ console.log(`${dimText("Downloading latest version...")}`);
2520
+ const updateCmd = `npx --yes ${PACKAGE_NAME}@latest install --force`;
2521
+ console.log(`${dimText(`Running: ${updateCmd}`)}
2522
+ `);
2523
+ execSync(updateCmd, { stdio: "inherit" });
2524
+ console.log(`
2525
+ ${ok("Update complete!")}`);
2526
+ console.log(`Updated from ${c.yellow}${currentVersion}${c.reset} to ${c.green}${latestVersion}${c.reset}`);
2527
+ console.log(`
2528
+ ${dimText("View changelog at: https://github.com/lgcyaxi/oh-my-claude/blob/main/CHANGELOG.md")}`);
2529
+ } catch (error) {
2530
+ console.log(`
2531
+ ${fail("Update failed")}`);
2532
+ console.log(`${dimText("Try running manually:")}`);
2533
+ console.log(` ${c.cyan}npx ${PACKAGE_NAME}@latest install --force${c.reset}`);
2534
+ process.exit(1);
2535
+ }
2536
+ });
2537
+ program.command("statusline").description("Manage statusline integration").option("--enable", "Enable statusline").option("--disable", "Disable statusline").option("--status", "Show current statusline configuration").action((options) => {
2538
+ const { readFileSync, existsSync } = __require("node:fs");
2539
+ const { join } = __require("node:path");
2540
+ const { homedir } = __require("node:os");
2541
+ const useColor = process.stdout.isTTY;
2542
+ const c = {
2543
+ reset: useColor ? "\x1B[0m" : "",
2544
+ bold: useColor ? "\x1B[1m" : "",
2545
+ dim: useColor ? "\x1B[2m" : "",
2546
+ green: useColor ? "\x1B[32m" : "",
2547
+ red: useColor ? "\x1B[31m" : "",
2548
+ yellow: useColor ? "\x1B[33m" : "",
2549
+ cyan: useColor ? "\x1B[36m" : ""
2550
+ };
2551
+ const ok = (text) => `${c.green}+${c.reset} ${text}`;
2552
+ const fail = (text) => `${c.red}x${c.reset} ${text}`;
2553
+ const warn = (text) => `${c.yellow}!${c.reset} ${text}`;
2554
+ const settingsPath = join(homedir(), ".claude", "settings.json");
2555
+ if (options.status || !options.enable && !options.disable) {
2556
+ console.log(`${c.bold}StatusLine Status${c.reset}
2557
+ `);
2558
+ if (!existsSync(settingsPath)) {
2559
+ console.log(fail("settings.json not found"));
2560
+ process.exit(1);
2561
+ }
2562
+ try {
2563
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
2564
+ if (!settings.statusLine) {
2565
+ console.log(fail("StatusLine not configured"));
2566
+ console.log(`
2567
+ Run ${c.cyan}oh-my-claude statusline --enable${c.reset} to enable.`);
2568
+ } else {
2569
+ const cmd = settings.statusLine.command || "";
2570
+ const isOurs = cmd.includes("oh-my-claude");
2571
+ const isWrapper = cmd.includes("statusline-wrapper");
2572
+ console.log(ok("StatusLine configured"));
2573
+ console.log(` Command: ${c.dim}${cmd}${c.reset}`);
2574
+ if (isWrapper) {
2575
+ console.log(` Mode: ${c.yellow}Merged (wrapper)${c.reset}`);
2576
+ } else if (isOurs) {
2577
+ console.log(` Mode: ${c.green}Direct${c.reset}`);
2578
+ } else {
2579
+ console.log(` Mode: ${c.cyan}External${c.reset}`);
2580
+ }
2581
+ }
2582
+ } catch (error) {
2583
+ console.log(fail(`Failed to read settings: ${error}`));
2584
+ process.exit(1);
2585
+ }
2586
+ } else if (options.enable) {
2587
+ const { installStatusLine } = (init_settings_merger(), __toCommonJS(exports_settings_merger));
2588
+ const { getStatusLineScriptPath } = (init_installer(), __toCommonJS(exports_installer));
2589
+ try {
2590
+ const result = installStatusLine(getStatusLineScriptPath());
2591
+ if (result.installed) {
2592
+ console.log(ok("StatusLine enabled"));
2593
+ if (result.wrapperCreated) {
2594
+ console.log(warn("Wrapper created to merge with existing statusLine"));
2595
+ }
2596
+ }
2597
+ } catch (error) {
2598
+ console.log(fail(`Failed to enable statusline: ${error}`));
2599
+ process.exit(1);
2600
+ }
2601
+ } else if (options.disable) {
2602
+ const { uninstallStatusLine } = (init_settings_merger(), __toCommonJS(exports_settings_merger));
2603
+ try {
2604
+ const result = uninstallStatusLine();
2605
+ if (result) {
2606
+ console.log(ok("StatusLine disabled"));
2607
+ } else {
2608
+ console.log(warn("StatusLine was not configured"));
2609
+ }
2610
+ } catch (error) {
2611
+ console.log(fail(`Failed to disable statusline: ${error}`));
2612
+ process.exit(1);
2613
+ }
2614
+ }
2615
+ });
2407
2616
  program.command("setup-mcp").description("Install official MCP servers (MiniMax, GLM/ZhiPu)").option("--minimax", "Install MiniMax MCP only").option("--glm", "Install GLM/ZhiPu MCPs only").option("--thinking", "Install Sequential Thinking MCP only").option("--list", "List available MCP servers").action(async (options) => {
2408
2617
  const { execSync, spawnSync } = __require("node:child_process");
2409
2618
  const useColor = process.stdout.isTTY;
@@ -70,7 +70,7 @@ async function main() {
70
70
  return;
71
71
  }
72
72
  if (toolInput.tool !== "Edit" && toolInput.tool !== "Write") {
73
- const response2 = { decision: "skip" };
73
+ const response2 = { decision: "approve" };
74
74
  console.log(JSON.stringify(response2));
75
75
  return;
76
76
  }
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/task-notification.ts
4
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
5
+ import { join, dirname } from "node:path";
6
+ import { homedir } from "node:os";
7
+ var NOTIFIED_FILE_PATH = join(homedir(), ".claude", "oh-my-claude", "notified-tasks.json");
8
+ function loadNotifiedTasks() {
9
+ try {
10
+ if (!existsSync(NOTIFIED_FILE_PATH)) {
11
+ return new Set;
12
+ }
13
+ const content = readFileSync(NOTIFIED_FILE_PATH, "utf-8");
14
+ const data = JSON.parse(content);
15
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
16
+ const filtered = Object.entries(data).filter(([_, timestamp]) => timestamp > oneHourAgo).map(([id]) => id);
17
+ return new Set(filtered);
18
+ } catch {
19
+ return new Set;
20
+ }
21
+ }
22
+ function saveNotifiedTask(taskId) {
23
+ try {
24
+ const dir = dirname(NOTIFIED_FILE_PATH);
25
+ if (!existsSync(dir)) {
26
+ mkdirSync(dir, { recursive: true });
27
+ }
28
+ let data = {};
29
+ if (existsSync(NOTIFIED_FILE_PATH)) {
30
+ try {
31
+ data = JSON.parse(readFileSync(NOTIFIED_FILE_PATH, "utf-8"));
32
+ } catch {
33
+ data = {};
34
+ }
35
+ }
36
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
37
+ for (const [id, timestamp] of Object.entries(data)) {
38
+ if (timestamp < oneHourAgo) {
39
+ delete data[id];
40
+ }
41
+ }
42
+ data[taskId] = Date.now();
43
+ writeFileSync(NOTIFIED_FILE_PATH, JSON.stringify(data));
44
+ } catch {}
45
+ }
46
+ function formatDuration(ms) {
47
+ const seconds = Math.floor(ms / 1000);
48
+ if (seconds < 60) {
49
+ return `${seconds}s`;
50
+ }
51
+ const minutes = Math.floor(seconds / 60);
52
+ const remainingSeconds = seconds % 60;
53
+ return `${minutes}m ${remainingSeconds}s`;
54
+ }
55
+ async function main() {
56
+ let inputData = "";
57
+ try {
58
+ inputData = readFileSync(0, "utf-8");
59
+ } catch {
60
+ console.log(JSON.stringify({ decision: "approve" }));
61
+ return;
62
+ }
63
+ if (!inputData.trim()) {
64
+ console.log(JSON.stringify({ decision: "approve" }));
65
+ return;
66
+ }
67
+ let toolInput;
68
+ try {
69
+ toolInput = JSON.parse(inputData);
70
+ } catch {
71
+ console.log(JSON.stringify({ decision: "approve" }));
72
+ return;
73
+ }
74
+ if (!toolInput.tool?.includes("oh-my-claude-background")) {
75
+ console.log(JSON.stringify({ decision: "approve" }));
76
+ return;
77
+ }
78
+ const toolOutput = toolInput.tool_output;
79
+ if (!toolOutput) {
80
+ console.log(JSON.stringify({ decision: "approve" }));
81
+ return;
82
+ }
83
+ try {
84
+ const output = JSON.parse(toolOutput);
85
+ const notifiedTasks = loadNotifiedTasks();
86
+ const notifications = [];
87
+ if (output.status === "completed" && toolInput.tool?.includes("poll_task")) {
88
+ console.log(JSON.stringify({ decision: "approve" }));
89
+ return;
90
+ }
91
+ if (output.tasks && Array.isArray(output.tasks)) {
92
+ for (const task of output.tasks) {
93
+ if ((task.status === "completed" || task.status === "failed") && task.id && !notifiedTasks.has(task.id)) {
94
+ let durationStr = "";
95
+ if (task.created && task.completed) {
96
+ const created = new Date(task.created).getTime();
97
+ const completed = new Date(task.completed).getTime();
98
+ durationStr = ` (${formatDuration(completed - created)})`;
99
+ }
100
+ const statusIcon = task.status === "completed" ? "+" : "!";
101
+ const agentName = task.agent || "unknown";
102
+ notifications.push(`[${statusIcon}] ${agentName}: ${task.status}${durationStr}`);
103
+ saveNotifiedTask(task.id);
104
+ }
105
+ }
106
+ }
107
+ if (notifications.length > 0) {
108
+ const response = {
109
+ decision: "approve",
110
+ hookSpecificOutput: {
111
+ hookEventName: "PostToolUse",
112
+ additionalContext: `
113
+ [omc] ${notifications.join(" | ")}`
114
+ }
115
+ };
116
+ console.log(JSON.stringify(response));
117
+ return;
118
+ }
119
+ } catch {}
120
+ console.log(JSON.stringify({ decision: "approve" }));
121
+ }
122
+ main().catch(() => {
123
+ console.log(JSON.stringify({ decision: "approve" }));
124
+ });
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/task-tracker.ts
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
5
+ import { join, dirname } from "node:path";
6
+ import { homedir } from "node:os";
7
+ var STATUS_FILE_PATH = join(homedir(), ".claude", "oh-my-claude", "status.json");
8
+ var TASK_AGENTS_FILE = join(homedir(), ".claude", "oh-my-claude", "task-agents.json");
9
+ function loadTaskAgents() {
10
+ try {
11
+ if (!existsSync(TASK_AGENTS_FILE)) {
12
+ return { agents: [] };
13
+ }
14
+ const content = readFileSync(TASK_AGENTS_FILE, "utf-8");
15
+ return JSON.parse(content);
16
+ } catch {
17
+ return { agents: [] };
18
+ }
19
+ }
20
+ function saveTaskAgents(data) {
21
+ try {
22
+ const dir = dirname(TASK_AGENTS_FILE);
23
+ if (!existsSync(dir)) {
24
+ mkdirSync(dir, { recursive: true });
25
+ }
26
+ writeFileSync(TASK_AGENTS_FILE, JSON.stringify(data, null, 2));
27
+ } catch {}
28
+ }
29
+ function updateStatusFile() {
30
+ try {
31
+ let status = {
32
+ activeTasks: [],
33
+ providers: {},
34
+ updatedAt: new Date().toISOString()
35
+ };
36
+ if (existsSync(STATUS_FILE_PATH)) {
37
+ try {
38
+ status = JSON.parse(readFileSync(STATUS_FILE_PATH, "utf-8"));
39
+ } catch {}
40
+ }
41
+ const taskAgents = loadTaskAgents();
42
+ const taskAgentTasks = taskAgents.agents.map((agent) => ({
43
+ agent: `@${agent.type}`,
44
+ startedAt: agent.startedAt,
45
+ isTaskTool: true
46
+ }));
47
+ const thirtyMinutesAgo = Date.now() - 30 * 60 * 1000;
48
+ const activeTaskAgents = taskAgentTasks.filter((t) => t.startedAt > thirtyMinutesAgo);
49
+ const mcpTasks = (status.activeTasks || []).filter((t) => !t.isTaskTool);
50
+ status.activeTasks = [...mcpTasks, ...activeTaskAgents];
51
+ status.updatedAt = new Date().toISOString();
52
+ const dir = dirname(STATUS_FILE_PATH);
53
+ if (!existsSync(dir)) {
54
+ mkdirSync(dir, { recursive: true });
55
+ }
56
+ writeFileSync(STATUS_FILE_PATH, JSON.stringify(status, null, 2));
57
+ } catch {}
58
+ }
59
+ function generateId() {
60
+ return `task_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`;
61
+ }
62
+ function getAgentDisplayName(subagentType) {
63
+ const mapping = {
64
+ Bash: "Bash",
65
+ Explore: "Scout",
66
+ Plan: "Planner",
67
+ "general-purpose": "General",
68
+ "claude-code-guide": "Guide"
69
+ };
70
+ return mapping[subagentType] || subagentType;
71
+ }
72
+ async function main() {
73
+ let inputData = "";
74
+ try {
75
+ inputData = readFileSync(0, "utf-8");
76
+ } catch {
77
+ console.log(JSON.stringify({ decision: "approve" }));
78
+ return;
79
+ }
80
+ if (!inputData.trim()) {
81
+ console.log(JSON.stringify({ decision: "approve" }));
82
+ return;
83
+ }
84
+ let toolInput;
85
+ try {
86
+ toolInput = JSON.parse(inputData);
87
+ } catch {
88
+ console.log(JSON.stringify({ decision: "approve" }));
89
+ return;
90
+ }
91
+ if (toolInput.tool !== "Task") {
92
+ console.log(JSON.stringify({ decision: "approve" }));
93
+ return;
94
+ }
95
+ const subagentType = toolInput.tool_input?.subagent_type || "unknown";
96
+ const description = toolInput.tool_input?.description || "";
97
+ const isPreToolUse = !toolInput.tool_output;
98
+ if (isPreToolUse) {
99
+ const taskAgents = loadTaskAgents();
100
+ const newAgent = {
101
+ id: generateId(),
102
+ type: getAgentDisplayName(subagentType),
103
+ description,
104
+ startedAt: Date.now()
105
+ };
106
+ taskAgents.agents.push(newAgent);
107
+ saveTaskAgents(taskAgents);
108
+ updateStatusFile();
109
+ const response = {
110
+ decision: "approve"
111
+ };
112
+ console.log(JSON.stringify(response));
113
+ } else {
114
+ const taskAgents = loadTaskAgents();
115
+ const displayName = getAgentDisplayName(subagentType);
116
+ const index = taskAgents.agents.findIndex((a) => a.type === displayName);
117
+ if (index !== -1) {
118
+ const removedArr = taskAgents.agents.splice(index, 1);
119
+ const removed = removedArr[0];
120
+ if (!removed) {
121
+ console.log(JSON.stringify({ decision: "approve" }));
122
+ return;
123
+ }
124
+ const duration = Math.floor((Date.now() - removed.startedAt) / 1000);
125
+ saveTaskAgents(taskAgents);
126
+ updateStatusFile();
127
+ const durationStr = duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m`;
128
+ const response = {
129
+ decision: "approve",
130
+ hookSpecificOutput: {
131
+ hookEventName: "PostToolUse",
132
+ additionalContext: `
133
+ [@] ${displayName}: completed (${durationStr})`
134
+ }
135
+ };
136
+ console.log(JSON.stringify(response));
137
+ return;
138
+ }
139
+ console.log(JSON.stringify({ decision: "approve" }));
140
+ }
141
+ }
142
+ main().catch(() => {
143
+ console.log(JSON.stringify({ decision: "approve" }));
144
+ });