@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
@@ -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
- export type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
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
- task.status = "failed";
127
- task.error = error instanceof Error ? error.message : String(error);
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
 
@@ -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();