@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
|
@@ -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";
|
|
@@ -50,6 +51,9 @@ Use this for async operations that should run in parallel without blocking.
|
|
|
50
51
|
- visual-engineering: UI/UX work
|
|
51
52
|
- writing: Documentation and text
|
|
52
53
|
|
|
54
|
+
**Fallback Behavior**:
|
|
55
|
+
If the provider's API key is not configured, the task will return with status "fallback_required" and include fallback info. When this happens, use Claude Code's Task tool with the specified fallback model instead.
|
|
56
|
+
|
|
53
57
|
Note: Agents using Claude subscription (sisyphus, claude-reviewer, claude-scout) should use Claude Code's Task tool instead.`,
|
|
54
58
|
inputSchema: {
|
|
55
59
|
type: "object",
|
|
@@ -117,8 +121,8 @@ Note: Agents using Claude subscription (sisyphus, claude-reviewer, claude-scout)
|
|
|
117
121
|
properties: {
|
|
118
122
|
status: {
|
|
119
123
|
type: "string",
|
|
120
|
-
enum: ["pending", "running", "completed", "failed", "cancelled"],
|
|
121
|
-
description: "Filter by status",
|
|
124
|
+
enum: ["pending", "running", "completed", "failed", "cancelled", "fallback_required"],
|
|
125
|
+
description: "Filter by status. 'fallback_required' means the provider API key is not configured and Claude Task tool should be used instead.",
|
|
122
126
|
},
|
|
123
127
|
limit: {
|
|
124
128
|
type: "number",
|
|
@@ -320,6 +324,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
320
324
|
completed: new Date(t.completedAt).toISOString(),
|
|
321
325
|
}),
|
|
322
326
|
...(t.error && { error: t.error }),
|
|
327
|
+
...(t.fallback && { fallback: t.fallback }),
|
|
323
328
|
})),
|
|
324
329
|
}),
|
|
325
330
|
},
|
|
@@ -369,6 +374,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
369
374
|
async function main() {
|
|
370
375
|
const transport = new StdioServerTransport();
|
|
371
376
|
await server.connect(transport);
|
|
377
|
+
|
|
378
|
+
// Write initial status file for statusline display
|
|
379
|
+
updateStatusFile();
|
|
380
|
+
|
|
372
381
|
console.error("oh-my-claude Background Agent MCP Server running");
|
|
373
382
|
}
|
|
374
383
|
|
|
@@ -5,13 +5,21 @@
|
|
|
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
|
-
import { routeByAgent, routeByCategory } from "../../providers/router";
|
|
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";
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
// Status file path for statusline integration
|
|
20
|
+
const STATUS_FILE_PATH = join(homedir(), ".claude", "oh-my-claude", "status.json");
|
|
21
|
+
|
|
22
|
+
export type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled" | "fallback_required";
|
|
15
23
|
|
|
16
24
|
export interface Task {
|
|
17
25
|
id: string;
|
|
@@ -21,6 +29,13 @@ export interface Task {
|
|
|
21
29
|
status: TaskStatus;
|
|
22
30
|
result?: string;
|
|
23
31
|
error?: string;
|
|
32
|
+
/** Fallback info when primary provider is not configured */
|
|
33
|
+
fallback?: {
|
|
34
|
+
provider: string;
|
|
35
|
+
model: string;
|
|
36
|
+
executionMode?: string;
|
|
37
|
+
reason: string;
|
|
38
|
+
};
|
|
24
39
|
createdAt: number;
|
|
25
40
|
startedAt?: number;
|
|
26
41
|
completedAt?: number;
|
|
@@ -34,6 +49,42 @@ function generateTaskId(): string {
|
|
|
34
49
|
return `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
35
50
|
}
|
|
36
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
|
+
|
|
37
88
|
/**
|
|
38
89
|
* Launch a new background task
|
|
39
90
|
*/
|
|
@@ -70,6 +121,7 @@ export async function launchTask(options: {
|
|
|
70
121
|
};
|
|
71
122
|
|
|
72
123
|
tasks.set(taskId, task);
|
|
124
|
+
updateStatusFile();
|
|
73
125
|
|
|
74
126
|
// Start the task asynchronously
|
|
75
127
|
runTask(task, finalSystemPrompt).catch((error) => {
|
|
@@ -87,6 +139,7 @@ async function runTask(task: Task, systemPrompt?: string): Promise<void> {
|
|
|
87
139
|
task.status = "running";
|
|
88
140
|
task.startedAt = Date.now();
|
|
89
141
|
tasks.set(task.id, task);
|
|
142
|
+
updateStatusFile();
|
|
90
143
|
|
|
91
144
|
try {
|
|
92
145
|
const messages: ChatMessage[] = [];
|
|
@@ -122,11 +175,25 @@ async function runTask(task: Task, systemPrompt?: string): Promise<void> {
|
|
|
122
175
|
task.result = result;
|
|
123
176
|
task.completedAt = Date.now();
|
|
124
177
|
tasks.set(task.id, task);
|
|
178
|
+
updateStatusFile();
|
|
125
179
|
} catch (error) {
|
|
126
|
-
|
|
127
|
-
|
|
180
|
+
// Handle fallback required error specially
|
|
181
|
+
if (error instanceof FallbackRequiredError) {
|
|
182
|
+
task.status = "fallback_required";
|
|
183
|
+
task.error = error.message;
|
|
184
|
+
task.fallback = {
|
|
185
|
+
provider: error.fallback.provider,
|
|
186
|
+
model: error.fallback.model,
|
|
187
|
+
executionMode: error.fallback.executionMode,
|
|
188
|
+
reason: error.reason,
|
|
189
|
+
};
|
|
190
|
+
} else {
|
|
191
|
+
task.status = "failed";
|
|
192
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
193
|
+
}
|
|
128
194
|
task.completedAt = Date.now();
|
|
129
195
|
tasks.set(task.id, task);
|
|
196
|
+
updateStatusFile();
|
|
130
197
|
}
|
|
131
198
|
}
|
|
132
199
|
|
|
@@ -144,6 +211,12 @@ export function pollTask(taskId: string): {
|
|
|
144
211
|
status: TaskStatus;
|
|
145
212
|
result?: string;
|
|
146
213
|
error?: string;
|
|
214
|
+
fallback?: {
|
|
215
|
+
provider: string;
|
|
216
|
+
model: string;
|
|
217
|
+
executionMode?: string;
|
|
218
|
+
reason: string;
|
|
219
|
+
};
|
|
147
220
|
} {
|
|
148
221
|
const task = tasks.get(taskId);
|
|
149
222
|
|
|
@@ -155,6 +228,7 @@ export function pollTask(taskId: string): {
|
|
|
155
228
|
status: task.status,
|
|
156
229
|
result: task.result,
|
|
157
230
|
error: task.error,
|
|
231
|
+
fallback: task.fallback,
|
|
158
232
|
};
|
|
159
233
|
}
|
|
160
234
|
|
|
@@ -173,6 +247,7 @@ export function cancelTask(taskId: string): boolean {
|
|
|
173
247
|
task.status = "cancelled";
|
|
174
248
|
task.completedAt = Date.now();
|
|
175
249
|
tasks.set(taskId, task);
|
|
250
|
+
updateStatusFile();
|
|
176
251
|
return true;
|
|
177
252
|
}
|
|
178
253
|
|
|
@@ -194,6 +269,10 @@ export function cancelAllTasks(): number {
|
|
|
194
269
|
}
|
|
195
270
|
}
|
|
196
271
|
|
|
272
|
+
if (cancelled > 0) {
|
|
273
|
+
updateStatusFile();
|
|
274
|
+
}
|
|
275
|
+
|
|
197
276
|
return cancelled;
|
|
198
277
|
}
|
|
199
278
|
|
package/src/providers/router.ts
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
resolveProviderForAgent,
|
|
17
17
|
resolveProviderForCategory,
|
|
18
18
|
getProviderDetails,
|
|
19
|
+
shouldUseFallback,
|
|
20
|
+
isProviderConfigured,
|
|
19
21
|
type OhMyClaudeConfig,
|
|
20
22
|
} from "../config";
|
|
21
23
|
|
|
@@ -80,6 +82,21 @@ function getProviderClient(
|
|
|
80
82
|
return client;
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Custom error class for fallback scenarios
|
|
87
|
+
*/
|
|
88
|
+
export class FallbackRequiredError extends Error {
|
|
89
|
+
constructor(
|
|
90
|
+
message: string,
|
|
91
|
+
public readonly agentName: string,
|
|
92
|
+
public readonly fallback: { provider: string; model: string; executionMode?: string },
|
|
93
|
+
public readonly reason: string
|
|
94
|
+
) {
|
|
95
|
+
super(message);
|
|
96
|
+
this.name = "FallbackRequiredError";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
/**
|
|
84
101
|
* Route a request to the appropriate provider based on agent name
|
|
85
102
|
*/
|
|
@@ -106,6 +123,17 @@ export async function routeByAgent(
|
|
|
106
123
|
);
|
|
107
124
|
}
|
|
108
125
|
|
|
126
|
+
// Check if fallback should be used (primary provider not configured)
|
|
127
|
+
const fallbackCheck = shouldUseFallback(config, agentName);
|
|
128
|
+
if (fallbackCheck.useFallback && fallbackCheck.fallback) {
|
|
129
|
+
throw new FallbackRequiredError(
|
|
130
|
+
`Agent "${agentName}" requires fallback: ${fallbackCheck.reason}. Use Task tool with ${fallbackCheck.fallback.model} instead.`,
|
|
131
|
+
agentName,
|
|
132
|
+
fallbackCheck.fallback,
|
|
133
|
+
fallbackCheck.reason ?? "Provider not configured"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
109
137
|
const client = getProviderClient(agentConfig.provider, config);
|
|
110
138
|
|
|
111
139
|
const request: ChatCompletionRequest = {
|
|
@@ -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();
|