@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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
- package/CHANGELOG.md +22 -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/dist/cli.js +121 -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-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 +136 -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/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 +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
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Task Tracker Hook (PreToolUse & PostToolUse)
|
|
4
|
+
*
|
|
5
|
+
* Tracks Claude Code's Task tool invocations to monitor
|
|
6
|
+
* Claude-subscription agents (Sisyphus, Claude-Reviewer, Claude-Scout).
|
|
7
|
+
*
|
|
8
|
+
* Since Task tool runs as subprocesses, we track:
|
|
9
|
+
* - PreToolUse: When a Task is about to launch
|
|
10
|
+
* - PostToolUse: When a Task completes
|
|
11
|
+
*
|
|
12
|
+
* Updates the shared status file for statusline display.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { join, dirname } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
|
|
19
|
+
interface ToolUseInput {
|
|
20
|
+
tool: string;
|
|
21
|
+
tool_input?: {
|
|
22
|
+
subagent_type?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
prompt?: string;
|
|
25
|
+
};
|
|
26
|
+
tool_output?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface HookResponse {
|
|
30
|
+
decision: "approve";
|
|
31
|
+
hookSpecificOutput?: {
|
|
32
|
+
hookEventName: "PreToolUse" | "PostToolUse";
|
|
33
|
+
additionalContext?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Status file path (shared with MCP server)
|
|
38
|
+
const STATUS_FILE_PATH = join(homedir(), ".claude", "oh-my-claude", "status.json");
|
|
39
|
+
|
|
40
|
+
// Task agents file (tracks active Task tool agents)
|
|
41
|
+
const TASK_AGENTS_FILE = join(homedir(), ".claude", "oh-my-claude", "task-agents.json");
|
|
42
|
+
|
|
43
|
+
interface TaskAgent {
|
|
44
|
+
id: string;
|
|
45
|
+
type: string;
|
|
46
|
+
description: string;
|
|
47
|
+
startedAt: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface TaskAgentsData {
|
|
51
|
+
agents: TaskAgent[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load current task agents
|
|
56
|
+
*/
|
|
57
|
+
function loadTaskAgents(): TaskAgentsData {
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(TASK_AGENTS_FILE)) {
|
|
60
|
+
return { agents: [] };
|
|
61
|
+
}
|
|
62
|
+
const content = readFileSync(TASK_AGENTS_FILE, "utf-8");
|
|
63
|
+
return JSON.parse(content);
|
|
64
|
+
} catch {
|
|
65
|
+
return { agents: [] };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Save task agents
|
|
71
|
+
*/
|
|
72
|
+
function saveTaskAgents(data: TaskAgentsData): void {
|
|
73
|
+
try {
|
|
74
|
+
const dir = dirname(TASK_AGENTS_FILE);
|
|
75
|
+
if (!existsSync(dir)) {
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
writeFileSync(TASK_AGENTS_FILE, JSON.stringify(data, null, 2));
|
|
79
|
+
} catch {
|
|
80
|
+
// Silently fail
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update the shared status file to include Task agents
|
|
86
|
+
*/
|
|
87
|
+
function updateStatusFile(): void {
|
|
88
|
+
try {
|
|
89
|
+
// Read current status
|
|
90
|
+
let status: any = {
|
|
91
|
+
activeTasks: [],
|
|
92
|
+
providers: {},
|
|
93
|
+
updatedAt: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (existsSync(STATUS_FILE_PATH)) {
|
|
97
|
+
try {
|
|
98
|
+
status = JSON.parse(readFileSync(STATUS_FILE_PATH, "utf-8"));
|
|
99
|
+
} catch {
|
|
100
|
+
// Use default
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Load task agents and merge with status
|
|
105
|
+
const taskAgents = loadTaskAgents();
|
|
106
|
+
|
|
107
|
+
// Add Task agents to activeTasks (mark with special prefix)
|
|
108
|
+
const taskAgentTasks = taskAgents.agents.map((agent) => ({
|
|
109
|
+
agent: `@${agent.type}`, // @ prefix indicates Claude-subscription agent
|
|
110
|
+
startedAt: agent.startedAt,
|
|
111
|
+
isTaskTool: true,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
// Filter out stale Task agents (older than 30 minutes)
|
|
115
|
+
const thirtyMinutesAgo = Date.now() - 30 * 60 * 1000;
|
|
116
|
+
const activeTaskAgents = taskAgentTasks.filter(
|
|
117
|
+
(t) => t.startedAt > thirtyMinutesAgo
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Merge: MCP tasks first, then Task tool agents
|
|
121
|
+
const mcpTasks = (status.activeTasks || []).filter((t: any) => !t.isTaskTool);
|
|
122
|
+
status.activeTasks = [...mcpTasks, ...activeTaskAgents];
|
|
123
|
+
status.updatedAt = new Date().toISOString();
|
|
124
|
+
|
|
125
|
+
// Write back
|
|
126
|
+
const dir = dirname(STATUS_FILE_PATH);
|
|
127
|
+
if (!existsSync(dir)) {
|
|
128
|
+
mkdirSync(dir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
writeFileSync(STATUS_FILE_PATH, JSON.stringify(status, null, 2));
|
|
131
|
+
} catch {
|
|
132
|
+
// Silently fail
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generate a simple ID for tracking
|
|
138
|
+
*/
|
|
139
|
+
function generateId(): string {
|
|
140
|
+
return `task_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Map subagent_type to display name
|
|
145
|
+
*/
|
|
146
|
+
function getAgentDisplayName(subagentType: string): string {
|
|
147
|
+
const mapping: Record<string, string> = {
|
|
148
|
+
Bash: "Bash",
|
|
149
|
+
Explore: "Scout",
|
|
150
|
+
Plan: "Planner",
|
|
151
|
+
"general-purpose": "General",
|
|
152
|
+
"claude-code-guide": "Guide",
|
|
153
|
+
};
|
|
154
|
+
return mapping[subagentType] || subagentType;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function main() {
|
|
158
|
+
// Read input from stdin
|
|
159
|
+
let inputData = "";
|
|
160
|
+
try {
|
|
161
|
+
inputData = readFileSync(0, "utf-8");
|
|
162
|
+
} catch {
|
|
163
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!inputData.trim()) {
|
|
168
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let toolInput: ToolUseInput;
|
|
173
|
+
try {
|
|
174
|
+
toolInput = JSON.parse(inputData);
|
|
175
|
+
} catch {
|
|
176
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Only process Task tool
|
|
181
|
+
if (toolInput.tool !== "Task") {
|
|
182
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const subagentType = toolInput.tool_input?.subagent_type || "unknown";
|
|
187
|
+
const description = toolInput.tool_input?.description || "";
|
|
188
|
+
|
|
189
|
+
// Check if this is PreToolUse (no tool_output) or PostToolUse (has tool_output)
|
|
190
|
+
const isPreToolUse = !toolInput.tool_output;
|
|
191
|
+
|
|
192
|
+
if (isPreToolUse) {
|
|
193
|
+
// Task is about to launch - add to tracking
|
|
194
|
+
const taskAgents = loadTaskAgents();
|
|
195
|
+
|
|
196
|
+
const newAgent: TaskAgent = {
|
|
197
|
+
id: generateId(),
|
|
198
|
+
type: getAgentDisplayName(subagentType),
|
|
199
|
+
description: description,
|
|
200
|
+
startedAt: Date.now(),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
taskAgents.agents.push(newAgent);
|
|
204
|
+
saveTaskAgents(taskAgents);
|
|
205
|
+
updateStatusFile();
|
|
206
|
+
|
|
207
|
+
// Provide context about the launch
|
|
208
|
+
const response: HookResponse = {
|
|
209
|
+
decision: "approve",
|
|
210
|
+
};
|
|
211
|
+
console.log(JSON.stringify(response));
|
|
212
|
+
} else {
|
|
213
|
+
// Task completed - remove from tracking
|
|
214
|
+
const taskAgents = loadTaskAgents();
|
|
215
|
+
|
|
216
|
+
// Remove the most recent agent of this type (LIFO)
|
|
217
|
+
const displayName = getAgentDisplayName(subagentType);
|
|
218
|
+
const index = taskAgents.agents.findIndex((a) => a.type === displayName);
|
|
219
|
+
|
|
220
|
+
if (index !== -1) {
|
|
221
|
+
const removedArr = taskAgents.agents.splice(index, 1);
|
|
222
|
+
const removed = removedArr[0];
|
|
223
|
+
if (!removed) {
|
|
224
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const duration = Math.floor((Date.now() - removed.startedAt) / 1000);
|
|
228
|
+
|
|
229
|
+
saveTaskAgents(taskAgents);
|
|
230
|
+
updateStatusFile();
|
|
231
|
+
|
|
232
|
+
// Provide completion notification
|
|
233
|
+
const durationStr =
|
|
234
|
+
duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m`;
|
|
235
|
+
const response: HookResponse = {
|
|
236
|
+
decision: "approve",
|
|
237
|
+
hookSpecificOutput: {
|
|
238
|
+
hookEventName: "PostToolUse",
|
|
239
|
+
additionalContext: `\n[@] ${displayName}: completed (${durationStr})`,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
console.log(JSON.stringify(response));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
main().catch(() => {
|
|
251
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
252
|
+
});
|
package/src/installer/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { join, dirname } from "node:path";
|
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
|
|
16
16
|
import { generateAllAgentFiles, removeAgentFiles } from "../generators/agent-generator";
|
|
17
|
-
import { installHooks, installMcpServer, uninstallFromSettings } from "./settings-merger";
|
|
17
|
+
import { installHooks, installMcpServer, installStatusLine, uninstallFromSettings, uninstallStatusLine } from "./settings-merger";
|
|
18
18
|
import { DEFAULT_CONFIG } from "../config/schema";
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -52,12 +52,20 @@ export function getConfigPath(): string {
|
|
|
52
52
|
return join(homedir(), ".claude", "oh-my-claude.json");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Get statusline script path
|
|
57
|
+
*/
|
|
58
|
+
export function getStatusLineScriptPath(): string {
|
|
59
|
+
return join(getInstallDir(), "dist", "statusline", "statusline.js");
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
export interface InstallResult {
|
|
56
63
|
success: boolean;
|
|
57
64
|
agents: { generated: string[]; skipped: string[] };
|
|
58
65
|
commands: { installed: string[]; skipped: string[] };
|
|
59
66
|
hooks: { installed: string[]; skipped: string[] };
|
|
60
67
|
mcp: { installed: boolean };
|
|
68
|
+
statusLine: { installed: boolean; wrapperCreated: boolean };
|
|
61
69
|
config: { created: boolean };
|
|
62
70
|
errors: string[];
|
|
63
71
|
}
|
|
@@ -74,6 +82,8 @@ export async function install(options?: {
|
|
|
74
82
|
skipHooks?: boolean;
|
|
75
83
|
/** Skip MCP server installation */
|
|
76
84
|
skipMcp?: boolean;
|
|
85
|
+
/** Skip statusline installation */
|
|
86
|
+
skipStatusLine?: boolean;
|
|
77
87
|
/** Force overwrite existing files */
|
|
78
88
|
force?: boolean;
|
|
79
89
|
/** Source directory (for built files) */
|
|
@@ -85,6 +95,7 @@ export async function install(options?: {
|
|
|
85
95
|
commands: { installed: [], skipped: [] },
|
|
86
96
|
hooks: { installed: [], skipped: [] },
|
|
87
97
|
mcp: { installed: false },
|
|
98
|
+
statusLine: { installed: false, wrapperCreated: false },
|
|
88
99
|
config: { created: false },
|
|
89
100
|
errors: [],
|
|
90
101
|
};
|
|
@@ -213,7 +224,30 @@ process.exit(1);
|
|
|
213
224
|
}
|
|
214
225
|
}
|
|
215
226
|
|
|
216
|
-
// 4.
|
|
227
|
+
// 4. Install statusline
|
|
228
|
+
if (!options?.skipStatusLine) {
|
|
229
|
+
try {
|
|
230
|
+
const statusLineDir = join(installDir, "dist", "statusline");
|
|
231
|
+
if (!existsSync(statusLineDir)) {
|
|
232
|
+
mkdirSync(statusLineDir, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Copy statusline script (assuming it's built to dist/statusline/)
|
|
236
|
+
const builtStatusLineDir = join(sourceDir, "dist", "statusline");
|
|
237
|
+
if (existsSync(builtStatusLineDir)) {
|
|
238
|
+
cpSync(builtStatusLineDir, statusLineDir, { recursive: true });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Install statusline into settings.json
|
|
242
|
+
const statusLineResult = installStatusLine(getStatusLineScriptPath());
|
|
243
|
+
result.statusLine.installed = statusLineResult.installed;
|
|
244
|
+
result.statusLine.wrapperCreated = statusLineResult.wrapperCreated;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
result.errors.push(`Failed to install statusline: ${error}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 5. Create default config if not exists
|
|
217
251
|
const configPath = getConfigPath();
|
|
218
252
|
if (!existsSync(configPath) || options?.force) {
|
|
219
253
|
try {
|
|
@@ -243,6 +277,7 @@ export interface UninstallResult {
|
|
|
243
277
|
commands: string[];
|
|
244
278
|
hooks: string[];
|
|
245
279
|
mcp: boolean;
|
|
280
|
+
statusLine: boolean;
|
|
246
281
|
errors: string[];
|
|
247
282
|
}
|
|
248
283
|
|
|
@@ -259,6 +294,7 @@ export async function uninstall(options?: {
|
|
|
259
294
|
commands: [],
|
|
260
295
|
hooks: [],
|
|
261
296
|
mcp: false,
|
|
297
|
+
statusLine: false,
|
|
262
298
|
errors: [],
|
|
263
299
|
};
|
|
264
300
|
|
|
@@ -284,11 +320,13 @@ export async function uninstall(options?: {
|
|
|
284
320
|
"omc-explore",
|
|
285
321
|
"omc-plan",
|
|
286
322
|
"omc-start-work",
|
|
323
|
+
"omc-status",
|
|
287
324
|
// Quick action commands (omcx-)
|
|
288
325
|
"omcx-commit",
|
|
289
326
|
"omcx-implement",
|
|
290
327
|
"omcx-refactor",
|
|
291
328
|
"omcx-docs",
|
|
329
|
+
"omcx-issue",
|
|
292
330
|
];
|
|
293
331
|
const { unlinkSync } = require("node:fs");
|
|
294
332
|
for (const cmd of ourCommands) {
|
|
@@ -312,7 +350,14 @@ export async function uninstall(options?: {
|
|
|
312
350
|
result.errors.push(`Failed to update settings: ${error}`);
|
|
313
351
|
}
|
|
314
352
|
|
|
315
|
-
//
|
|
353
|
+
// 4. Remove statusline
|
|
354
|
+
try {
|
|
355
|
+
result.statusLine = uninstallStatusLine();
|
|
356
|
+
} catch (error) {
|
|
357
|
+
result.errors.push(`Failed to remove statusline: ${error}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 5. Remove installation directory
|
|
316
361
|
const installDir = getInstallDir();
|
|
317
362
|
if (existsSync(installDir)) {
|
|
318
363
|
try {
|
|
@@ -323,7 +368,7 @@ export async function uninstall(options?: {
|
|
|
323
368
|
}
|
|
324
369
|
}
|
|
325
370
|
|
|
326
|
-
//
|
|
371
|
+
// 6. Remove config (unless keepConfig)
|
|
327
372
|
if (!options?.keepConfig) {
|
|
328
373
|
const configPath = getConfigPath();
|
|
329
374
|
if (existsSync(configPath)) {
|
|
@@ -354,14 +399,19 @@ export function checkInstallation(): {
|
|
|
354
399
|
agents: boolean;
|
|
355
400
|
hooks: boolean;
|
|
356
401
|
mcp: boolean;
|
|
402
|
+
statusLine: boolean;
|
|
357
403
|
config: boolean;
|
|
358
404
|
};
|
|
359
405
|
} {
|
|
360
406
|
const installDir = getInstallDir();
|
|
361
407
|
const hooksDir = getHooksDir();
|
|
362
408
|
const mcpServerPath = getMcpServerPath();
|
|
409
|
+
const statusLineScriptPath = getStatusLineScriptPath();
|
|
363
410
|
const configPath = getConfigPath();
|
|
364
411
|
|
|
412
|
+
// Check if statusline is configured in settings
|
|
413
|
+
const { isStatusLineConfigured } = require("./statusline-merger");
|
|
414
|
+
|
|
365
415
|
return {
|
|
366
416
|
installed:
|
|
367
417
|
existsSync(installDir) &&
|
|
@@ -371,6 +421,7 @@ export function checkInstallation(): {
|
|
|
371
421
|
agents: existsSync(join(homedir(), ".claude", "agents", "sisyphus.md")),
|
|
372
422
|
hooks: existsSync(join(hooksDir, "comment-checker.js")),
|
|
373
423
|
mcp: existsSync(mcpServerPath),
|
|
424
|
+
statusLine: existsSync(statusLineScriptPath) && isStatusLineConfigured(),
|
|
374
425
|
config: existsSync(configPath),
|
|
375
426
|
},
|
|
376
427
|
};
|
|
@@ -32,6 +32,11 @@ interface ClaudeSettings {
|
|
|
32
32
|
env?: Record<string, string>;
|
|
33
33
|
}
|
|
34
34
|
>;
|
|
35
|
+
statusLine?: {
|
|
36
|
+
type: "command";
|
|
37
|
+
command: string;
|
|
38
|
+
padding?: number;
|
|
39
|
+
};
|
|
35
40
|
[key: string]: unknown;
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -199,6 +204,34 @@ export function installHooks(hooksDir: string): { installed: string[]; skipped:
|
|
|
199
204
|
skipped.push("todo-continuation (already installed)");
|
|
200
205
|
}
|
|
201
206
|
|
|
207
|
+
// Task tracker hook (PreToolUse for Task tool)
|
|
208
|
+
if (
|
|
209
|
+
addHook(
|
|
210
|
+
settings,
|
|
211
|
+
"PreToolUse",
|
|
212
|
+
"Task",
|
|
213
|
+
`node ${hooksDir}/task-tracker.js`
|
|
214
|
+
)
|
|
215
|
+
) {
|
|
216
|
+
installed.push("task-tracker (PreToolUse:Task)");
|
|
217
|
+
} else {
|
|
218
|
+
skipped.push("task-tracker (already installed)");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Task tracker hook (PostToolUse for Task tool completion)
|
|
222
|
+
if (
|
|
223
|
+
addHook(
|
|
224
|
+
settings,
|
|
225
|
+
"PostToolUse",
|
|
226
|
+
"Task",
|
|
227
|
+
`node ${hooksDir}/task-tracker.js`
|
|
228
|
+
)
|
|
229
|
+
) {
|
|
230
|
+
installed.push("task-tracker (PostToolUse:Task)");
|
|
231
|
+
} else {
|
|
232
|
+
skipped.push("task-tracker (already installed)");
|
|
233
|
+
}
|
|
234
|
+
|
|
202
235
|
saveSettings(settings);
|
|
203
236
|
return { installed, skipped };
|
|
204
237
|
}
|
|
@@ -293,3 +326,56 @@ export function uninstallFromSettings(): {
|
|
|
293
326
|
|
|
294
327
|
return { removedHooks, removedMcp };
|
|
295
328
|
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Install oh-my-claude statusLine
|
|
332
|
+
* If user has existing statusLine, creates a wrapper that calls both
|
|
333
|
+
*/
|
|
334
|
+
export function installStatusLine(statusLineScriptPath: string): {
|
|
335
|
+
installed: boolean;
|
|
336
|
+
wrapperCreated: boolean;
|
|
337
|
+
existingBackedUp: boolean;
|
|
338
|
+
} {
|
|
339
|
+
const { mergeStatusLine } = require("./statusline-merger");
|
|
340
|
+
|
|
341
|
+
const settings = loadSettings();
|
|
342
|
+
const existing = settings.statusLine;
|
|
343
|
+
|
|
344
|
+
const result = mergeStatusLine(existing);
|
|
345
|
+
|
|
346
|
+
if (result.config.command !== existing?.command) {
|
|
347
|
+
settings.statusLine = result.config;
|
|
348
|
+
saveSettings(settings);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
installed: true,
|
|
353
|
+
wrapperCreated: result.wrapperCreated,
|
|
354
|
+
existingBackedUp: result.backupCreated,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Remove oh-my-claude statusLine and restore original if backed up
|
|
360
|
+
*/
|
|
361
|
+
export function uninstallStatusLine(): boolean {
|
|
362
|
+
const { restoreStatusLine, isStatusLineConfigured } = require("./statusline-merger");
|
|
363
|
+
|
|
364
|
+
if (!isStatusLineConfigured()) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const settings = loadSettings();
|
|
369
|
+
|
|
370
|
+
// Try to restore original statusLine
|
|
371
|
+
const backup = restoreStatusLine();
|
|
372
|
+
if (backup) {
|
|
373
|
+
settings.statusLine = backup;
|
|
374
|
+
} else {
|
|
375
|
+
// No backup - just remove our statusLine
|
|
376
|
+
delete settings.statusLine;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
saveSettings(settings);
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusLine Merger
|
|
3
|
+
*
|
|
4
|
+
* Handles merging oh-my-claude's statusLine with existing user statusLine configuration.
|
|
5
|
+
* If user already has a statusLine (like CCometixLine), we create a wrapper that calls both.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync, chmodSync, existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
|
|
12
|
+
export interface StatusLineConfig {
|
|
13
|
+
type: "command";
|
|
14
|
+
command: string;
|
|
15
|
+
padding?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Our statusline command
|
|
19
|
+
const OMC_STATUSLINE_COMMAND = "node ~/.claude/oh-my-claude/dist/statusline/statusline.js";
|
|
20
|
+
|
|
21
|
+
// Wrapper script path
|
|
22
|
+
const WRAPPER_SCRIPT_PATH = join(homedir(), ".claude", "oh-my-claude", "statusline-wrapper.sh");
|
|
23
|
+
|
|
24
|
+
// Backup file for original statusline config
|
|
25
|
+
const BACKUP_FILE_PATH = join(homedir(), ".claude", "oh-my-claude", "statusline-backup.json");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a statusline command is our own
|
|
29
|
+
*/
|
|
30
|
+
function isOurStatusLine(command: string): boolean {
|
|
31
|
+
return command.includes("oh-my-claude") && command.includes("statusline");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate a wrapper script that calls both statuslines
|
|
36
|
+
* Puts omc status on second line for better visibility
|
|
37
|
+
*/
|
|
38
|
+
function generateWrapperScript(existingCommand: string): string {
|
|
39
|
+
return `#!/bin/bash
|
|
40
|
+
# oh-my-claude StatusLine Wrapper
|
|
41
|
+
# Calls both the original statusLine and oh-my-claude's statusline
|
|
42
|
+
# Auto-generated - do not edit manually
|
|
43
|
+
|
|
44
|
+
input=$(cat)
|
|
45
|
+
|
|
46
|
+
# Call existing statusline
|
|
47
|
+
existing_output=$(echo "$input" | ${existingCommand} 2>/dev/null || echo "")
|
|
48
|
+
|
|
49
|
+
# Call oh-my-claude statusline
|
|
50
|
+
omc_output=$(echo "$input" | ${OMC_STATUSLINE_COMMAND} 2>/dev/null || echo "omc")
|
|
51
|
+
|
|
52
|
+
# Combine outputs - put omc on second line for better visibility
|
|
53
|
+
if [ -n "$existing_output" ] && [ -n "$omc_output" ]; then
|
|
54
|
+
printf "%s\\n%s\\n" "$existing_output" "$omc_output"
|
|
55
|
+
elif [ -n "$existing_output" ]; then
|
|
56
|
+
printf "%s\\n" "$existing_output"
|
|
57
|
+
elif [ -n "$omc_output" ]; then
|
|
58
|
+
printf "%s\\n" "$omc_output"
|
|
59
|
+
else
|
|
60
|
+
echo ""
|
|
61
|
+
fi
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Merge statusLine configuration
|
|
67
|
+
*
|
|
68
|
+
* Returns the new config and whether a wrapper was created
|
|
69
|
+
*/
|
|
70
|
+
export function mergeStatusLine(
|
|
71
|
+
existing: StatusLineConfig | undefined
|
|
72
|
+
): {
|
|
73
|
+
config: StatusLineConfig;
|
|
74
|
+
wrapperCreated: boolean;
|
|
75
|
+
backupCreated: boolean;
|
|
76
|
+
} {
|
|
77
|
+
// If no existing statusline, just use ours
|
|
78
|
+
if (!existing) {
|
|
79
|
+
return {
|
|
80
|
+
config: {
|
|
81
|
+
type: "command",
|
|
82
|
+
command: OMC_STATUSLINE_COMMAND,
|
|
83
|
+
},
|
|
84
|
+
wrapperCreated: false,
|
|
85
|
+
backupCreated: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If existing is already ours or wrapper, don't change anything
|
|
90
|
+
if (isOurStatusLine(existing.command)) {
|
|
91
|
+
return {
|
|
92
|
+
config: existing,
|
|
93
|
+
wrapperCreated: false,
|
|
94
|
+
backupCreated: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if existing is already our wrapper
|
|
99
|
+
if (existing.command.includes("statusline-wrapper.sh")) {
|
|
100
|
+
return {
|
|
101
|
+
config: existing,
|
|
102
|
+
wrapperCreated: false,
|
|
103
|
+
backupCreated: false,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create wrapper script
|
|
108
|
+
const wrapperContent = generateWrapperScript(existing.command);
|
|
109
|
+
writeFileSync(WRAPPER_SCRIPT_PATH, wrapperContent);
|
|
110
|
+
chmodSync(WRAPPER_SCRIPT_PATH, 0o755);
|
|
111
|
+
|
|
112
|
+
// Backup existing config
|
|
113
|
+
writeFileSync(BACKUP_FILE_PATH, JSON.stringify(existing, null, 2));
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
config: {
|
|
117
|
+
type: "command",
|
|
118
|
+
command: WRAPPER_SCRIPT_PATH,
|
|
119
|
+
padding: existing.padding,
|
|
120
|
+
},
|
|
121
|
+
wrapperCreated: true,
|
|
122
|
+
backupCreated: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Restore original statusLine configuration (for uninstall)
|
|
128
|
+
*/
|
|
129
|
+
export function restoreStatusLine(): StatusLineConfig | null {
|
|
130
|
+
if (!existsSync(BACKUP_FILE_PATH)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const backup = JSON.parse(readFileSync(BACKUP_FILE_PATH, "utf-8"));
|
|
136
|
+
return backup as StatusLineConfig;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if statusline is configured
|
|
144
|
+
*/
|
|
145
|
+
export function isStatusLineConfigured(): boolean {
|
|
146
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
147
|
+
if (!existsSync(settingsPath)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
153
|
+
if (!settings.statusLine) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const command = settings.statusLine.command || "";
|
|
158
|
+
return isOurStatusLine(command) || command.includes("statusline-wrapper.sh");
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get the path to our statusline script
|
|
166
|
+
*/
|
|
167
|
+
export function getOmcStatusLineCommand(): string {
|
|
168
|
+
return OMC_STATUSLINE_COMMAND;
|
|
169
|
+
}
|
|
@@ -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
|
|