@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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
- package/CHANGELOG.md +53 -0
- package/CLAUDE.md +60 -0
- package/README.md +97 -12
- package/README.zh-CN.md +58 -11
- package/bin/oh-my-claude.js +4 -2
- package/changelog/v1.0.0.md +28 -0
- package/changelog/v1.0.1.md +28 -0
- package/changelog/v1.1.0.md +20 -0
- package/changelog/v1.1.1.md +71 -0
- package/dist/cli.js +213 -4
- package/dist/hooks/comment-checker.js +1 -1
- package/dist/hooks/task-notification.js +124 -0
- package/dist/hooks/task-tracker.js +144 -0
- package/dist/index-1dv6t98k.js +7654 -0
- package/dist/index-5ars1tn4.js +7348 -0
- package/dist/index-d79fk9ah.js +7350 -0
- package/dist/index-hzm01rkh.js +7654 -0
- package/dist/index-qrbfj4cd.js +7664 -0
- package/dist/index-ypyx3ye0.js +7349 -0
- package/dist/index.js +24 -1
- package/dist/mcp/server.js +202 -45
- package/dist/statusline/statusline.js +146 -0
- package/package.json +6 -5
- package/src/agents/document-writer.ts +5 -0
- package/src/agents/explore.ts +5 -0
- package/src/agents/frontend-ui-ux.ts +5 -0
- package/src/agents/librarian.ts +5 -0
- package/src/agents/oracle.ts +5 -0
- package/src/agents/types.ts +11 -0
- package/src/cli.ts +261 -2
- package/src/commands/index.ts +8 -1
- package/src/commands/omc-status.md +71 -0
- package/src/commands/omcx-issue.md +175 -0
- package/src/commands/ulw.md +144 -0
- package/src/config/loader.ts +73 -0
- package/src/config/schema.ts +25 -2
- package/src/generators/agent-generator.ts +17 -1
- package/src/hooks/comment-checker.ts +2 -2
- package/src/hooks/task-notification.ts +206 -0
- package/src/hooks/task-tracker.ts +252 -0
- package/src/installer/index.ts +55 -4
- package/src/installer/settings-merger.ts +86 -0
- package/src/installer/statusline-merger.ts +169 -0
- package/src/mcp/background-agent-server/server.ts +11 -2
- package/src/mcp/background-agent-server/task-manager.ts +83 -4
- package/src/providers/router.ts +28 -0
- package/src/statusline/formatter.ts +164 -0
- 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-
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
+
});
|