@lgcyaxi/oh-my-claude 1.0.1 → 1.1.2
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 +30 -0
- package/CLAUDE.md +60 -0
- package/README.md +69 -1
- package/README.zh-CN.md +30 -0
- package/changelog/v1.1.0.md +20 -0
- package/changelog/v1.1.1.md +71 -0
- package/changelog/v1.1.2.md +15 -0
- package/dist/cli.js +132 -11
- 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-814gp2s3.js +7664 -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 +14 -1
- package/dist/mcp/server.js +64 -28
- package/dist/statusline/statusline.js +146 -0
- package/package.json +4 -3
- package/src/cli.ts +150 -10
- 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/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 +88 -2
- package/src/installer/statusline-merger.ts +169 -0
- package/src/mcp/background-agent-server/server.ts +5 -0
- package/src/mcp/background-agent-server/task-manager.ts +53 -0
- package/src/statusline/formatter.ts +164 -0
- package/src/statusline/statusline.ts +103 -0
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
cancelAllTasks,
|
|
25
25
|
listTasks,
|
|
26
26
|
cleanupTasks,
|
|
27
|
+
updateStatusFile,
|
|
27
28
|
} from "./task-manager";
|
|
28
29
|
import { getConcurrencyStatus } from "./concurrency";
|
|
29
30
|
import { getProvidersStatus } from "../../providers/router";
|
|
@@ -373,6 +374,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
373
374
|
async function main() {
|
|
374
375
|
const transport = new StdioServerTransport();
|
|
375
376
|
await server.connect(transport);
|
|
377
|
+
|
|
378
|
+
// Write initial status file for statusline display
|
|
379
|
+
updateStatusFile();
|
|
380
|
+
|
|
376
381
|
console.error("oh-my-claude Background Agent MCP Server running");
|
|
377
382
|
}
|
|
378
383
|
|
|
@@ -5,11 +5,19 @@
|
|
|
5
5
|
* - Launch tasks with agent/category routing
|
|
6
6
|
* - Track task status and results
|
|
7
7
|
* - Handle concurrency limits per provider
|
|
8
|
+
* - Write status file for statusline display
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { routeByAgent, routeByCategory, FallbackRequiredError } from "../../providers/router";
|
|
11
12
|
import { getAgent } from "../../agents";
|
|
12
13
|
import type { ChatMessage } from "../../providers/types";
|
|
14
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { getConcurrencyStatus } from "./concurrency";
|
|
18
|
+
|
|
19
|
+
// Status file path for statusline integration
|
|
20
|
+
const STATUS_FILE_PATH = join(homedir(), ".claude", "oh-my-claude", "status.json");
|
|
13
21
|
|
|
14
22
|
export type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled" | "fallback_required";
|
|
15
23
|
|
|
@@ -41,6 +49,42 @@ function generateTaskId(): string {
|
|
|
41
49
|
return `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Write current status to file for statusline integration
|
|
54
|
+
* Called on every task state change and on server startup
|
|
55
|
+
*/
|
|
56
|
+
export function updateStatusFile(): void {
|
|
57
|
+
try {
|
|
58
|
+
// Ensure directory exists
|
|
59
|
+
const dir = dirname(STATUS_FILE_PATH);
|
|
60
|
+
if (!existsSync(dir)) {
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get active tasks
|
|
65
|
+
const activeTasks = Array.from(tasks.values())
|
|
66
|
+
.filter((t) => t.status === "running" || t.status === "pending")
|
|
67
|
+
.map((t) => ({
|
|
68
|
+
agent: t.agentName || t.categoryName || "unknown",
|
|
69
|
+
startedAt: t.startedAt || t.createdAt,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
// Get provider concurrency
|
|
73
|
+
const providers = getConcurrencyStatus();
|
|
74
|
+
|
|
75
|
+
const status = {
|
|
76
|
+
activeTasks,
|
|
77
|
+
providers,
|
|
78
|
+
updatedAt: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
writeFileSync(STATUS_FILE_PATH, JSON.stringify(status, null, 2));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Silently fail - statusline is non-critical
|
|
84
|
+
console.error("Failed to update status file:", error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
44
88
|
/**
|
|
45
89
|
* Launch a new background task
|
|
46
90
|
*/
|
|
@@ -77,6 +121,7 @@ export async function launchTask(options: {
|
|
|
77
121
|
};
|
|
78
122
|
|
|
79
123
|
tasks.set(taskId, task);
|
|
124
|
+
updateStatusFile();
|
|
80
125
|
|
|
81
126
|
// Start the task asynchronously
|
|
82
127
|
runTask(task, finalSystemPrompt).catch((error) => {
|
|
@@ -94,6 +139,7 @@ async function runTask(task: Task, systemPrompt?: string): Promise<void> {
|
|
|
94
139
|
task.status = "running";
|
|
95
140
|
task.startedAt = Date.now();
|
|
96
141
|
tasks.set(task.id, task);
|
|
142
|
+
updateStatusFile();
|
|
97
143
|
|
|
98
144
|
try {
|
|
99
145
|
const messages: ChatMessage[] = [];
|
|
@@ -129,6 +175,7 @@ async function runTask(task: Task, systemPrompt?: string): Promise<void> {
|
|
|
129
175
|
task.result = result;
|
|
130
176
|
task.completedAt = Date.now();
|
|
131
177
|
tasks.set(task.id, task);
|
|
178
|
+
updateStatusFile();
|
|
132
179
|
} catch (error) {
|
|
133
180
|
// Handle fallback required error specially
|
|
134
181
|
if (error instanceof FallbackRequiredError) {
|
|
@@ -146,6 +193,7 @@ async function runTask(task: Task, systemPrompt?: string): Promise<void> {
|
|
|
146
193
|
}
|
|
147
194
|
task.completedAt = Date.now();
|
|
148
195
|
tasks.set(task.id, task);
|
|
196
|
+
updateStatusFile();
|
|
149
197
|
}
|
|
150
198
|
}
|
|
151
199
|
|
|
@@ -199,6 +247,7 @@ export function cancelTask(taskId: string): boolean {
|
|
|
199
247
|
task.status = "cancelled";
|
|
200
248
|
task.completedAt = Date.now();
|
|
201
249
|
tasks.set(taskId, task);
|
|
250
|
+
updateStatusFile();
|
|
202
251
|
return true;
|
|
203
252
|
}
|
|
204
253
|
|
|
@@ -220,6 +269,10 @@ export function cancelAllTasks(): number {
|
|
|
220
269
|
}
|
|
221
270
|
}
|
|
222
271
|
|
|
272
|
+
if (cancelled > 0) {
|
|
273
|
+
updateStatusFile();
|
|
274
|
+
}
|
|
275
|
+
|
|
223
276
|
return cancelled;
|
|
224
277
|
}
|
|
225
278
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusLine Formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats task and provider data into a compact status line string
|
|
5
|
+
* for display in Claude Code's statusLine feature.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ActiveTask {
|
|
9
|
+
agent: string;
|
|
10
|
+
startedAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ProviderStatus {
|
|
14
|
+
active: number;
|
|
15
|
+
limit: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StatusLineData {
|
|
19
|
+
activeTasks: ActiveTask[];
|
|
20
|
+
providers: Record<string, ProviderStatus>;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Agent name abbreviations for compact display (MCP agents)
|
|
25
|
+
const AGENT_ABBREV: Record<string, string> = {
|
|
26
|
+
oracle: "Oracle",
|
|
27
|
+
librarian: "Lib",
|
|
28
|
+
explore: "Exp",
|
|
29
|
+
"frontend-ui-ux": "UI",
|
|
30
|
+
"document-writer": "Doc",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Task tool agent abbreviations (Claude-subscription agents)
|
|
34
|
+
const TASK_AGENT_ABBREV: Record<string, string> = {
|
|
35
|
+
Scout: "Scout",
|
|
36
|
+
Planner: "Plan",
|
|
37
|
+
General: "Gen",
|
|
38
|
+
Guide: "Guide",
|
|
39
|
+
Bash: "Bash",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Provider name abbreviations
|
|
43
|
+
const PROVIDER_ABBREV: Record<string, string> = {
|
|
44
|
+
deepseek: "DS",
|
|
45
|
+
zhipu: "ZP",
|
|
46
|
+
minimax: "MM",
|
|
47
|
+
openrouter: "OR",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format duration in seconds to compact string
|
|
52
|
+
* e.g., 45 -> "45s", 125 -> "2m"
|
|
53
|
+
*/
|
|
54
|
+
function formatDuration(seconds: number): string {
|
|
55
|
+
if (seconds < 60) {
|
|
56
|
+
return `${Math.floor(seconds)}s`;
|
|
57
|
+
}
|
|
58
|
+
const minutes = Math.floor(seconds / 60);
|
|
59
|
+
return `${minutes}m`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get abbreviated agent name
|
|
64
|
+
* Handles both MCP agents and Task tool agents (prefixed with @)
|
|
65
|
+
*/
|
|
66
|
+
function getAgentAbbrev(agent: string): string {
|
|
67
|
+
// Task tool agents are prefixed with @
|
|
68
|
+
if (agent.startsWith("@")) {
|
|
69
|
+
const taskAgent = agent.slice(1);
|
|
70
|
+
return `@${TASK_AGENT_ABBREV[taskAgent] || taskAgent.slice(0, 4)}`;
|
|
71
|
+
}
|
|
72
|
+
return AGENT_ABBREV[agent.toLowerCase()] || agent.slice(0, 4);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get abbreviated provider name
|
|
77
|
+
*/
|
|
78
|
+
function getProviderAbbrev(provider: string): string {
|
|
79
|
+
return PROVIDER_ABBREV[provider.toLowerCase()] || provider.slice(0, 2).toUpperCase();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Format the status line data into a compact string
|
|
84
|
+
*
|
|
85
|
+
* Output format examples:
|
|
86
|
+
* - No tasks: "omc"
|
|
87
|
+
* - With tasks: "omc [Oracle: 32s] [Lib: 12s] | DS: 2/10 ZP: 1/10"
|
|
88
|
+
* - Tasks only: "omc [Oracle: 32s]"
|
|
89
|
+
*/
|
|
90
|
+
export function formatStatusLine(data: StatusLineData): string {
|
|
91
|
+
const parts: string[] = ["omc"];
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
|
|
94
|
+
// Format active tasks
|
|
95
|
+
if (data.activeTasks.length > 0) {
|
|
96
|
+
// Limit to 3 tasks max for readability
|
|
97
|
+
const tasksToShow = data.activeTasks.slice(0, 3);
|
|
98
|
+
|
|
99
|
+
for (const task of tasksToShow) {
|
|
100
|
+
const durationSec = Math.floor((now - task.startedAt) / 1000);
|
|
101
|
+
const agentName = getAgentAbbrev(task.agent);
|
|
102
|
+
const duration = formatDuration(durationSec);
|
|
103
|
+
parts.push(`[${agentName}: ${duration}]`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (data.activeTasks.length > 3) {
|
|
107
|
+
parts.push(`+${data.activeTasks.length - 3}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Format provider concurrency (only show non-zero or if there are active tasks)
|
|
112
|
+
const providerParts: string[] = [];
|
|
113
|
+
const providersToShow = ["deepseek", "zhipu", "minimax"];
|
|
114
|
+
|
|
115
|
+
for (const provider of providersToShow) {
|
|
116
|
+
const status = data.providers[provider];
|
|
117
|
+
if (status && (status.active > 0 || data.activeTasks.length > 0)) {
|
|
118
|
+
const abbrev = getProviderAbbrev(provider);
|
|
119
|
+
providerParts.push(`${abbrev}: ${status.active}/${status.limit}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (providerParts.length > 0) {
|
|
124
|
+
parts.push("|");
|
|
125
|
+
parts.push(...providerParts);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return parts.join(" ");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Format a minimal status line when no data is available
|
|
133
|
+
* Shows "omc ready" to indicate the system is available
|
|
134
|
+
*/
|
|
135
|
+
export function formatEmptyStatusLine(): string {
|
|
136
|
+
return "omc ready";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Format idle status line with provider info
|
|
141
|
+
* Shows availability even when no tasks are running
|
|
142
|
+
*/
|
|
143
|
+
export function formatIdleStatusLine(providers: Record<string, ProviderStatus>): string {
|
|
144
|
+
const parts: string[] = ["omc ready"];
|
|
145
|
+
|
|
146
|
+
// Show provider availability
|
|
147
|
+
const providerParts: string[] = [];
|
|
148
|
+
const providersToShow = ["deepseek", "zhipu", "minimax"];
|
|
149
|
+
|
|
150
|
+
for (const provider of providersToShow) {
|
|
151
|
+
const status = providers[provider];
|
|
152
|
+
if (status && status.limit > 0) {
|
|
153
|
+
const abbrev = getProviderAbbrev(provider);
|
|
154
|
+
providerParts.push(`${abbrev}: ${status.limit}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (providerParts.length > 0) {
|
|
159
|
+
parts.push("|");
|
|
160
|
+
parts.push(...providerParts);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return parts.join(" ");
|
|
164
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* oh-my-claude StatusLine Script
|
|
4
|
+
*
|
|
5
|
+
* Reads MCP background task status from a status file and outputs
|
|
6
|
+
* a formatted status line for Claude Code's statusLine feature.
|
|
7
|
+
*
|
|
8
|
+
* Usage in settings.json:
|
|
9
|
+
* {
|
|
10
|
+
* "statusLine": {
|
|
11
|
+
* "type": "command",
|
|
12
|
+
* "command": "node ~/.claude/oh-my-claude/dist/statusline/statusline.js"
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
|
|
21
|
+
import { formatStatusLine, formatEmptyStatusLine, formatIdleStatusLine, type StatusLineData } from "./formatter";
|
|
22
|
+
|
|
23
|
+
// Status file path - MCP server writes to this
|
|
24
|
+
const STATUS_FILE_PATH = join(homedir(), ".claude", "oh-my-claude", "status.json");
|
|
25
|
+
|
|
26
|
+
// Timeout for the entire script (prevent blocking terminal)
|
|
27
|
+
const TIMEOUT_MS = 100;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read status from the shared status file
|
|
31
|
+
*/
|
|
32
|
+
function readStatusFile(): StatusLineData | null {
|
|
33
|
+
try {
|
|
34
|
+
if (!existsSync(STATUS_FILE_PATH)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const content = readFileSync(STATUS_FILE_PATH, "utf-8");
|
|
39
|
+
const data = JSON.parse(content) as StatusLineData;
|
|
40
|
+
|
|
41
|
+
// Validate the data structure
|
|
42
|
+
if (!data || typeof data !== "object") {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if data is stale (older than 5 minutes)
|
|
47
|
+
if (data.updatedAt) {
|
|
48
|
+
const updatedAt = new Date(data.updatedAt).getTime();
|
|
49
|
+
const age = Date.now() - updatedAt;
|
|
50
|
+
if (age > 5 * 60 * 1000) {
|
|
51
|
+
// Data is stale, return empty
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return data;
|
|
57
|
+
} catch {
|
|
58
|
+
// Silently fail - status file may not exist or be malformed
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Main function
|
|
65
|
+
*/
|
|
66
|
+
async function main() {
|
|
67
|
+
// Set up timeout to prevent blocking
|
|
68
|
+
const timeoutId = setTimeout(() => {
|
|
69
|
+
// Output empty status and exit on timeout
|
|
70
|
+
console.log(formatEmptyStatusLine());
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}, TIMEOUT_MS);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Read Claude Code's input from stdin (may be empty or JSON)
|
|
76
|
+
// We don't actually need it, but we consume it to avoid broken pipe
|
|
77
|
+
let _input = "";
|
|
78
|
+
try {
|
|
79
|
+
_input = readFileSync(0, "utf-8");
|
|
80
|
+
} catch {
|
|
81
|
+
// stdin may be empty or unavailable
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Read status from file
|
|
85
|
+
const statusData = readStatusFile();
|
|
86
|
+
|
|
87
|
+
if (!statusData) {
|
|
88
|
+
console.log(formatEmptyStatusLine());
|
|
89
|
+
} else if (statusData.activeTasks.length === 0) {
|
|
90
|
+
// No active tasks - show idle status with provider info
|
|
91
|
+
console.log(formatIdleStatusLine(statusData.providers));
|
|
92
|
+
} else {
|
|
93
|
+
console.log(formatStatusLine(statusData));
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// On any error, output minimal status
|
|
97
|
+
console.log(formatEmptyStatusLine());
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(timeoutId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main();
|