@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.
@@ -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
+ });
@@ -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. Create default config if not exists
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
- // 3. Remove installation directory
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
- // 4. Remove config (unless keepConfig)
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
  }
@@ -213,7 +246,7 @@ export function installMcpServer(serverPath: string): boolean {
213
246
  // Check if already installed
214
247
  let alreadyInstalled = false;
215
248
  try {
216
- const list = execSync("claude mcp list 2>/dev/null", { encoding: "utf-8" });
249
+ const list = execSync("claude mcp list", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
217
250
  alreadyInstalled = list.includes("oh-my-claude-background");
218
251
  } catch {
219
252
  // Ignore if list fails
@@ -280,7 +313,7 @@ export function uninstallFromSettings(): {
280
313
  // Remove MCP server via CLI
281
314
  let removedMcp = false;
282
315
  try {
283
- execSync("claude mcp remove --scope user oh-my-claude-background 2>/dev/null");
316
+ execSync("claude mcp remove --scope user oh-my-claude-background", { stdio: ["pipe", "pipe", "pipe"] });
284
317
  removedMcp = true;
285
318
  } catch {
286
319
  // Try removing from settings.json as fallback
@@ -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
+ }