@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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# /ulw - Ultrawork Mode
|
|
2
|
+
|
|
3
|
+
**ULTRAWORK MODE ACTIVATED!**
|
|
4
|
+
|
|
5
|
+
You are now operating in **MAXIMUM PERFORMANCE MODE**. All restrictions on effort, thoroughness, and completion are lifted. You will work relentlessly until the task is FULLY complete.
|
|
6
|
+
|
|
7
|
+
## Core Mandate
|
|
8
|
+
|
|
9
|
+
**ZERO PARTIAL COMPLETION. 100% DELIVERY OR NOTHING.**
|
|
10
|
+
|
|
11
|
+
This is not a suggestion. This is your operating contract:
|
|
12
|
+
- NO scope reduction
|
|
13
|
+
- NO mock versions or placeholders
|
|
14
|
+
- NO skipped requirements
|
|
15
|
+
- NO "good enough" stopping points
|
|
16
|
+
- NO deleting tests to make builds pass
|
|
17
|
+
- NO leaving code in broken state
|
|
18
|
+
|
|
19
|
+
**60-80% completion is FAILURE. Only 100% is acceptable.**
|
|
20
|
+
|
|
21
|
+
## Agent Utilization (MANDATORY)
|
|
22
|
+
|
|
23
|
+
You MUST leverage ALL available agents to their fullest potential:
|
|
24
|
+
|
|
25
|
+
### Sync Agents (Task tool)
|
|
26
|
+
| Agent | Use For | When |
|
|
27
|
+
|-------|---------|------|
|
|
28
|
+
| **Explore** | Codebase search, pattern finding | FIRST - before any implementation |
|
|
29
|
+
| **Claude-Reviewer** | Code review, verification | AFTER - every significant change |
|
|
30
|
+
| **Claude-Scout** | Fast exploration, quick checks | PARALLEL - multiple searches |
|
|
31
|
+
|
|
32
|
+
### Async Agents (MCP background)
|
|
33
|
+
| Agent | Use For | When |
|
|
34
|
+
|-------|---------|------|
|
|
35
|
+
| **Oracle** | Architecture decisions, deep reasoning | When stuck or uncertain |
|
|
36
|
+
| **Librarian** | External docs, library research | Before using unfamiliar APIs |
|
|
37
|
+
| **Frontend-UI-UX** | Visual/UI implementation | All frontend work |
|
|
38
|
+
| **Document-Writer** | Documentation, README | After implementation |
|
|
39
|
+
|
|
40
|
+
**FIRE PARALLEL AGENTS AGGRESSIVELY.** Launch 5-10+ background tasks if needed. Don't wait - collect results when ready.
|
|
41
|
+
|
|
42
|
+
## Execution Protocol
|
|
43
|
+
|
|
44
|
+
### 1. Immediate Planning
|
|
45
|
+
```
|
|
46
|
+
FIRST ACTION: Create comprehensive TodoWrite list
|
|
47
|
+
- Break down EVERY step
|
|
48
|
+
- Include verification steps
|
|
49
|
+
- Include documentation steps
|
|
50
|
+
- NO hidden work - everything tracked
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Aggressive Parallelization
|
|
54
|
+
```
|
|
55
|
+
LAUNCH PARALLEL:
|
|
56
|
+
- Explore agents for codebase context
|
|
57
|
+
- Librarian for external documentation
|
|
58
|
+
- Multiple search paths simultaneously
|
|
59
|
+
|
|
60
|
+
DO NOT wait for one search to complete before starting another.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Implementation with Verification
|
|
64
|
+
```
|
|
65
|
+
FOR EACH CHANGE:
|
|
66
|
+
1. Implement the change
|
|
67
|
+
2. Run diagnostics immediately
|
|
68
|
+
3. Run tests if applicable
|
|
69
|
+
4. Mark todo complete ONLY after verification passes
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 4. Zero-Tolerance Quality Gates
|
|
73
|
+
|
|
74
|
+
**Before marking ANY task complete:**
|
|
75
|
+
- [ ] Code compiles/transpiles without errors
|
|
76
|
+
- [ ] Linter passes (no suppressions added)
|
|
77
|
+
- [ ] Tests pass (no tests deleted or skipped)
|
|
78
|
+
- [ ] Build succeeds (if applicable)
|
|
79
|
+
- [ ] Functionality verified with actual execution
|
|
80
|
+
|
|
81
|
+
**If verification fails:**
|
|
82
|
+
1. Fix immediately
|
|
83
|
+
2. Re-verify
|
|
84
|
+
3. After 3 failures: STOP, consult Oracle, then ask user
|
|
85
|
+
|
|
86
|
+
### 5. Completion Criteria
|
|
87
|
+
|
|
88
|
+
The task is NOT complete until:
|
|
89
|
+
- [ ] ALL todo items marked done
|
|
90
|
+
- [ ] ALL verification gates passed
|
|
91
|
+
- [ ] ALL user requirements addressed
|
|
92
|
+
- [ ] Evidence collected for each completion claim
|
|
93
|
+
|
|
94
|
+
## Anti-Patterns (BLOCKED)
|
|
95
|
+
|
|
96
|
+
| Violation | Consequence |
|
|
97
|
+
|-----------|-------------|
|
|
98
|
+
| Claiming "done" without verification | REJECTED - redo with proof |
|
|
99
|
+
| Reducing scope without user approval | REJECTED - implement full scope |
|
|
100
|
+
| Skipping agent delegation | REJECTED - suboptimal execution |
|
|
101
|
+
| Batch-completing todos | REJECTED - defeats tracking |
|
|
102
|
+
| Leaving broken code | REJECTED - fix before proceeding |
|
|
103
|
+
|
|
104
|
+
## Work Until Done Protocol
|
|
105
|
+
|
|
106
|
+
**If session ends with incomplete todos:**
|
|
107
|
+
1. Boulder state persists your progress
|
|
108
|
+
2. Next session: `/omc-start-work` to resume
|
|
109
|
+
3. Continue from first unchecked item
|
|
110
|
+
4. NEVER restart from scratch
|
|
111
|
+
|
|
112
|
+
**If you hit a blocker:**
|
|
113
|
+
1. Document the blocker explicitly
|
|
114
|
+
2. Consult Oracle for architecture advice
|
|
115
|
+
3. If still blocked: Ask user for guidance
|
|
116
|
+
4. NEVER silently give up
|
|
117
|
+
|
|
118
|
+
## Response Format
|
|
119
|
+
|
|
120
|
+
Start your response with:
|
|
121
|
+
```
|
|
122
|
+
**ULTRAWORK MODE ENABLED!**
|
|
123
|
+
|
|
124
|
+
[Immediately begin planning/executing - no preamble]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Then execute with maximum intensity until COMPLETE.
|
|
128
|
+
|
|
129
|
+
## Arguments
|
|
130
|
+
|
|
131
|
+
`/ulw [task description]`
|
|
132
|
+
|
|
133
|
+
- Provide the task you want completed
|
|
134
|
+
- Or use after `/omc-plan` to execute an existing plan with ultrawork intensity
|
|
135
|
+
|
|
136
|
+
## Examples
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
/ulw implement the authentication system from the plan
|
|
140
|
+
/ulw fix all type errors in the codebase
|
|
141
|
+
/ulw add comprehensive test coverage for the API
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**NOW EXECUTE. NO HALF MEASURES. WORK UNTIL DONE.**
|
package/src/config/loader.ts
CHANGED
|
@@ -103,3 +103,76 @@ export function getProviderDetails(
|
|
|
103
103
|
}
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a provider is configured (has API key set)
|
|
109
|
+
*/
|
|
110
|
+
export function isProviderConfigured(
|
|
111
|
+
config: OhMyClaudeConfig,
|
|
112
|
+
providerName: string
|
|
113
|
+
): boolean {
|
|
114
|
+
const providerConfig = config.providers[providerName];
|
|
115
|
+
if (!providerConfig) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Claude subscription is always "configured"
|
|
120
|
+
if (providerConfig.type === "claude-subscription") {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check if API key environment variable is set
|
|
125
|
+
if (providerConfig.api_key_env) {
|
|
126
|
+
const apiKey = process.env[providerConfig.api_key_env];
|
|
127
|
+
return !!apiKey && apiKey.length > 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get fallback configuration for an agent
|
|
135
|
+
*/
|
|
136
|
+
export function getAgentFallback(
|
|
137
|
+
config: OhMyClaudeConfig,
|
|
138
|
+
agentName: string
|
|
139
|
+
): { provider: string; model: string; executionMode?: string } | null {
|
|
140
|
+
const agentConfig = config.agents[agentName];
|
|
141
|
+
if (agentConfig?.fallback) {
|
|
142
|
+
return {
|
|
143
|
+
provider: agentConfig.fallback.provider,
|
|
144
|
+
model: agentConfig.fallback.model,
|
|
145
|
+
executionMode: agentConfig.fallback.executionMode,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if an agent should use fallback (primary provider not configured)
|
|
153
|
+
*/
|
|
154
|
+
export function shouldUseFallback(
|
|
155
|
+
config: OhMyClaudeConfig,
|
|
156
|
+
agentName: string
|
|
157
|
+
): { useFallback: boolean; reason?: string; fallback?: { provider: string; model: string; executionMode?: string } } {
|
|
158
|
+
const agentConfig = config.agents[agentName];
|
|
159
|
+
if (!agentConfig) {
|
|
160
|
+
return { useFallback: false };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if primary provider is configured
|
|
164
|
+
if (!isProviderConfigured(config, agentConfig.provider)) {
|
|
165
|
+
const fallback = getAgentFallback(config, agentName);
|
|
166
|
+
if (fallback) {
|
|
167
|
+
const providerConfig = config.providers[agentConfig.provider];
|
|
168
|
+
const envVar = providerConfig?.api_key_env ?? `${agentConfig.provider.toUpperCase()}_API_KEY`;
|
|
169
|
+
return {
|
|
170
|
+
useFallback: true,
|
|
171
|
+
reason: `${envVar} is not set`,
|
|
172
|
+
fallback,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { useFallback: false };
|
|
178
|
+
}
|
package/src/config/schema.ts
CHANGED
|
@@ -15,6 +15,13 @@ export const ProviderConfigSchema = z.object({
|
|
|
15
15
|
note: z.string().optional(),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
// Agent fallback configuration schema
|
|
19
|
+
export const AgentFallbackConfigSchema = z.object({
|
|
20
|
+
provider: z.string(),
|
|
21
|
+
model: z.string(),
|
|
22
|
+
executionMode: z.enum(["task", "mcp"]).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
18
25
|
// Agent configuration schema
|
|
19
26
|
export const AgentConfigSchema = z.object({
|
|
20
27
|
provider: z.string(),
|
|
@@ -27,6 +34,7 @@ export const AgentConfigSchema = z.object({
|
|
|
27
34
|
budget_tokens: z.number().optional(),
|
|
28
35
|
})
|
|
29
36
|
.optional(),
|
|
37
|
+
fallback: AgentFallbackConfigSchema.optional(),
|
|
30
38
|
});
|
|
31
39
|
|
|
32
40
|
// Category configuration schema
|
|
@@ -77,6 +85,7 @@ export const OhMyClaudeConfigSchema = z.object({
|
|
|
77
85
|
}),
|
|
78
86
|
|
|
79
87
|
agents: z.record(z.string(), AgentConfigSchema).default({
|
|
88
|
+
// Claude subscription agents (no fallback needed)
|
|
80
89
|
Sisyphus: { provider: "claude", model: "claude-opus-4-5" },
|
|
81
90
|
"claude-reviewer": {
|
|
82
91
|
provider: "claude",
|
|
@@ -88,22 +97,36 @@ export const OhMyClaudeConfigSchema = z.object({
|
|
|
88
97
|
model: "claude-haiku-4-5",
|
|
89
98
|
temperature: 0.3,
|
|
90
99
|
},
|
|
100
|
+
// External API agents (with Claude fallbacks)
|
|
91
101
|
oracle: {
|
|
92
102
|
provider: "deepseek",
|
|
93
103
|
model: "deepseek-reasoner",
|
|
94
104
|
temperature: 0.1,
|
|
105
|
+
fallback: { provider: "claude", model: "claude-opus-4-5", executionMode: "task" },
|
|
106
|
+
},
|
|
107
|
+
librarian: {
|
|
108
|
+
provider: "zhipu",
|
|
109
|
+
model: "glm-4.7",
|
|
110
|
+
temperature: 0.3,
|
|
111
|
+
fallback: { provider: "claude", model: "claude-sonnet-4-5", executionMode: "task" },
|
|
112
|
+
},
|
|
113
|
+
explore: {
|
|
114
|
+
provider: "deepseek",
|
|
115
|
+
model: "deepseek-chat",
|
|
116
|
+
temperature: 0.1,
|
|
117
|
+
fallback: { provider: "claude", model: "claude-haiku-4-5", executionMode: "task" },
|
|
95
118
|
},
|
|
96
|
-
librarian: { provider: "zhipu", model: "glm-4.7", temperature: 0.3 },
|
|
97
|
-
explore: { provider: "deepseek", model: "deepseek-chat", temperature: 0.1 },
|
|
98
119
|
"frontend-ui-ux": {
|
|
99
120
|
provider: "zhipu",
|
|
100
121
|
model: "glm-4v-flash",
|
|
101
122
|
temperature: 0.7,
|
|
123
|
+
fallback: { provider: "claude", model: "claude-sonnet-4-5", executionMode: "task" },
|
|
102
124
|
},
|
|
103
125
|
"document-writer": {
|
|
104
126
|
provider: "minimax",
|
|
105
127
|
model: "MiniMax-M2.1",
|
|
106
128
|
temperature: 0.5,
|
|
129
|
+
fallback: { provider: "claude", model: "claude-sonnet-4-5", executionMode: "task" },
|
|
107
130
|
},
|
|
108
131
|
}),
|
|
109
132
|
|
|
@@ -35,7 +35,7 @@ export function generateAgentMarkdown(agent: AgentDefinition): string {
|
|
|
35
35
|
lines.push(`> ${agent.description}`);
|
|
36
36
|
lines.push("");
|
|
37
37
|
|
|
38
|
-
// Execution mode note
|
|
38
|
+
// Execution mode note with fallback info
|
|
39
39
|
if (agent.executionMode === "task") {
|
|
40
40
|
lines.push(
|
|
41
41
|
`<!-- Execution: Claude Code Task tool (sync) - Uses Claude subscription -->`
|
|
@@ -44,12 +44,28 @@ export function generateAgentMarkdown(agent: AgentDefinition): string {
|
|
|
44
44
|
lines.push(
|
|
45
45
|
`<!-- Execution: oh-my-claude MCP server (async) - Uses ${agent.defaultProvider} API -->`
|
|
46
46
|
);
|
|
47
|
+
// Add fallback info for MCP agents
|
|
48
|
+
if (agent.fallback) {
|
|
49
|
+
lines.push(
|
|
50
|
+
`<!-- Fallback: ${agent.fallback.provider}/${agent.fallback.model} via Task tool (when ${agent.defaultProvider.toUpperCase()}_API_KEY is not set) -->`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
47
53
|
}
|
|
48
54
|
lines.push("");
|
|
49
55
|
|
|
50
56
|
// The actual prompt
|
|
51
57
|
lines.push(agent.prompt);
|
|
52
58
|
|
|
59
|
+
// Add fallback usage note for MCP agents
|
|
60
|
+
if (agent.executionMode === "mcp" && agent.fallback) {
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push("## Fallback Mode");
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(`If \`${agent.defaultProvider.toUpperCase()}_API_KEY\` is not configured, this agent will automatically fall back to using **${agent.fallback.model}** via Claude's Task tool.`);
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push("The fallback provides similar capabilities using your Claude subscription, though the primary provider may offer specialized features.");
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
return lines.join("\n");
|
|
54
70
|
}
|
|
55
71
|
|
|
@@ -31,7 +31,7 @@ interface ToolInput {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
interface HookResponse {
|
|
34
|
-
decision: "approve" | "block"
|
|
34
|
+
decision: "approve" | "block";
|
|
35
35
|
reason?: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -125,7 +125,7 @@ async function main() {
|
|
|
125
125
|
|
|
126
126
|
// Only check Edit and Write tools
|
|
127
127
|
if (toolInput.tool !== "Edit" && toolInput.tool !== "Write") {
|
|
128
|
-
const response: HookResponse = { decision: "
|
|
128
|
+
const response: HookResponse = { decision: "approve" };
|
|
129
129
|
console.log(JSON.stringify(response));
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Task Notification Hook (PostToolUse)
|
|
4
|
+
*
|
|
5
|
+
* Monitors for MCP background task completions and provides
|
|
6
|
+
* notifications in the Claude Code output.
|
|
7
|
+
*
|
|
8
|
+
* Usage in settings.json:
|
|
9
|
+
* {
|
|
10
|
+
* "hooks": {
|
|
11
|
+
* "PostToolUse": [{
|
|
12
|
+
* "matcher": "mcp__oh-my-claude-background__.*",
|
|
13
|
+
* "hooks": [{
|
|
14
|
+
* "type": "command",
|
|
15
|
+
* "command": "node ~/.claude/oh-my-claude/hooks/task-notification.js"
|
|
16
|
+
* }]
|
|
17
|
+
* }]
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
23
|
+
import { join, dirname } from "node:path";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
|
|
26
|
+
interface PostToolUseInput {
|
|
27
|
+
tool: string;
|
|
28
|
+
tool_input?: Record<string, unknown>;
|
|
29
|
+
tool_output?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface HookResponse {
|
|
33
|
+
decision: "approve";
|
|
34
|
+
hookSpecificOutput?: {
|
|
35
|
+
hookEventName: "PostToolUse";
|
|
36
|
+
additionalContext?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// File to track notified task IDs (prevent duplicate notifications)
|
|
41
|
+
const NOTIFIED_FILE_PATH = join(homedir(), ".claude", "oh-my-claude", "notified-tasks.json");
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load list of already-notified task IDs
|
|
45
|
+
*/
|
|
46
|
+
function loadNotifiedTasks(): Set<string> {
|
|
47
|
+
try {
|
|
48
|
+
if (!existsSync(NOTIFIED_FILE_PATH)) {
|
|
49
|
+
return new Set();
|
|
50
|
+
}
|
|
51
|
+
const content = readFileSync(NOTIFIED_FILE_PATH, "utf-8");
|
|
52
|
+
const data = JSON.parse(content);
|
|
53
|
+
// Clean up old entries (older than 1 hour)
|
|
54
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
55
|
+
const filtered = Object.entries(data)
|
|
56
|
+
.filter(([_, timestamp]) => (timestamp as number) > oneHourAgo)
|
|
57
|
+
.map(([id]) => id);
|
|
58
|
+
return new Set(filtered);
|
|
59
|
+
} catch {
|
|
60
|
+
return new Set();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Save notified task ID
|
|
66
|
+
*/
|
|
67
|
+
function saveNotifiedTask(taskId: string): void {
|
|
68
|
+
try {
|
|
69
|
+
const dir = dirname(NOTIFIED_FILE_PATH);
|
|
70
|
+
if (!existsSync(dir)) {
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let data: Record<string, number> = {};
|
|
75
|
+
if (existsSync(NOTIFIED_FILE_PATH)) {
|
|
76
|
+
try {
|
|
77
|
+
data = JSON.parse(readFileSync(NOTIFIED_FILE_PATH, "utf-8"));
|
|
78
|
+
} catch {
|
|
79
|
+
data = {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Clean up old entries
|
|
84
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
85
|
+
for (const [id, timestamp] of Object.entries(data)) {
|
|
86
|
+
if (timestamp < oneHourAgo) {
|
|
87
|
+
delete data[id];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
data[taskId] = Date.now();
|
|
92
|
+
writeFileSync(NOTIFIED_FILE_PATH, JSON.stringify(data));
|
|
93
|
+
} catch {
|
|
94
|
+
// Silently fail
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format duration for display
|
|
100
|
+
*/
|
|
101
|
+
function formatDuration(ms: number): string {
|
|
102
|
+
const seconds = Math.floor(ms / 1000);
|
|
103
|
+
if (seconds < 60) {
|
|
104
|
+
return `${seconds}s`;
|
|
105
|
+
}
|
|
106
|
+
const minutes = Math.floor(seconds / 60);
|
|
107
|
+
const remainingSeconds = seconds % 60;
|
|
108
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function main() {
|
|
112
|
+
// Read input from stdin
|
|
113
|
+
let inputData = "";
|
|
114
|
+
try {
|
|
115
|
+
inputData = readFileSync(0, "utf-8");
|
|
116
|
+
} catch {
|
|
117
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!inputData.trim()) {
|
|
122
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let toolInput: PostToolUseInput;
|
|
127
|
+
try {
|
|
128
|
+
toolInput = JSON.parse(inputData);
|
|
129
|
+
} catch {
|
|
130
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Only process MCP tool outputs
|
|
135
|
+
if (!toolInput.tool?.includes("oh-my-claude-background")) {
|
|
136
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check for poll_task or list_tasks responses that contain completed tasks
|
|
141
|
+
const toolOutput = toolInput.tool_output;
|
|
142
|
+
if (!toolOutput) {
|
|
143
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const output = JSON.parse(toolOutput);
|
|
149
|
+
const notifiedTasks = loadNotifiedTasks();
|
|
150
|
+
const notifications: string[] = [];
|
|
151
|
+
|
|
152
|
+
// Check for single task completion (poll_task)
|
|
153
|
+
if (output.status === "completed" && toolInput.tool?.includes("poll_task")) {
|
|
154
|
+
// This is handled by the poll itself, no need for additional notification
|
|
155
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check for task list with completed tasks
|
|
160
|
+
if (output.tasks && Array.isArray(output.tasks)) {
|
|
161
|
+
for (const task of output.tasks) {
|
|
162
|
+
if (
|
|
163
|
+
(task.status === "completed" || task.status === "failed") &&
|
|
164
|
+
task.id &&
|
|
165
|
+
!notifiedTasks.has(task.id)
|
|
166
|
+
) {
|
|
167
|
+
// Calculate duration if we have timestamps
|
|
168
|
+
let durationStr = "";
|
|
169
|
+
if (task.created && task.completed) {
|
|
170
|
+
const created = new Date(task.created).getTime();
|
|
171
|
+
const completed = new Date(task.completed).getTime();
|
|
172
|
+
durationStr = ` (${formatDuration(completed - created)})`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const statusIcon = task.status === "completed" ? "+" : "!";
|
|
176
|
+
const agentName = task.agent || "unknown";
|
|
177
|
+
notifications.push(
|
|
178
|
+
`[${statusIcon}] ${agentName}: ${task.status}${durationStr}`
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
saveNotifiedTask(task.id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (notifications.length > 0) {
|
|
187
|
+
const response: HookResponse = {
|
|
188
|
+
decision: "approve",
|
|
189
|
+
hookSpecificOutput: {
|
|
190
|
+
hookEventName: "PostToolUse",
|
|
191
|
+
additionalContext: `\n[omc] ${notifications.join(" | ")}`,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
console.log(JSON.stringify(response));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// Parsing failed, just approve
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
main().catch(() => {
|
|
205
|
+
console.log(JSON.stringify({ decision: "approve" }));
|
|
206
|
+
});
|