@lgcyaxi/oh-my-claude 1.0.1 → 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.
@@ -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();