@happycastle/oh-my-openclaw 0.20.0 → 0.21.0
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/dist/commands/todo-commands.d.ts +2 -0
- package/dist/commands/todo-commands.js +57 -0
- package/dist/hooks/subagent-tracker.js +3 -2
- package/dist/hooks/todo-reminder.js +6 -1
- package/dist/index.js +6 -0
- package/dist/services/webhook-bridge.js +30 -11
- package/dist/tools/task-delegation.js +5 -5
- package/dist/tools/todo/todo-update.js +6 -1
- package/dist/utils/webhook-client.d.ts +5 -1
- package/dist/utils/webhook-client.js +6 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { _sessions } from '../tools/todo/store.js';
|
|
2
|
+
const STATUS_EMOJI = {
|
|
3
|
+
pending: '⏳',
|
|
4
|
+
in_progress: '🔄',
|
|
5
|
+
completed: '✅',
|
|
6
|
+
cancelled: '❌',
|
|
7
|
+
};
|
|
8
|
+
function formatTodoList(todos) {
|
|
9
|
+
if (todos.length === 0) {
|
|
10
|
+
return '📋 No todos found.';
|
|
11
|
+
}
|
|
12
|
+
const lines = ['# 📋 Todo List\n'];
|
|
13
|
+
// Status summary
|
|
14
|
+
const statusOrder = ['in_progress', 'pending', 'completed', 'cancelled'];
|
|
15
|
+
const counts = {};
|
|
16
|
+
for (const todo of todos) {
|
|
17
|
+
counts[todo.status] = (counts[todo.status] ?? 0) + 1;
|
|
18
|
+
}
|
|
19
|
+
const summaryParts = [];
|
|
20
|
+
for (const status of statusOrder) {
|
|
21
|
+
const count = counts[status];
|
|
22
|
+
if (count && count > 0) {
|
|
23
|
+
summaryParts.push(`${STATUS_EMOJI[status]} ${status}: ${count}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
lines.push(summaryParts.join(' | '));
|
|
27
|
+
lines.push('');
|
|
28
|
+
// List each todo
|
|
29
|
+
for (const todo of todos) {
|
|
30
|
+
const emoji = STATUS_EMOJI[todo.status] ?? '❓';
|
|
31
|
+
const priority = todo.priority !== 'medium' ? ` [${todo.priority}]` : '';
|
|
32
|
+
lines.push(`${emoji}${priority} **${todo.id}**: ${todo.content}`);
|
|
33
|
+
}
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Collect todos from ALL session stores.
|
|
38
|
+
* Command handlers don't receive session context, so we aggregate
|
|
39
|
+
* across every store to give the user a complete view.
|
|
40
|
+
*/
|
|
41
|
+
function collectAllTodos() {
|
|
42
|
+
const all = [];
|
|
43
|
+
for (const store of _sessions.values()) {
|
|
44
|
+
all.push(...store.todos);
|
|
45
|
+
}
|
|
46
|
+
return all;
|
|
47
|
+
}
|
|
48
|
+
export function registerTodoCommands(api) {
|
|
49
|
+
api.registerCommand({
|
|
50
|
+
name: 'todos',
|
|
51
|
+
description: 'Show current todo list (auto-reply, no AI invocation)',
|
|
52
|
+
handler: () => {
|
|
53
|
+
const todos = collectAllTodos();
|
|
54
|
+
return { text: formatTodoList(todos) };
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -105,7 +105,8 @@ export function registerSubagentTracker(api) {
|
|
|
105
105
|
const wakeMessage = requesterSessionKey
|
|
106
106
|
? `[System] Sub-agent completed (runId=${runId}, requester=${requesterSessionKey}). Process the result and continue pending work.`
|
|
107
107
|
: `[System] Sub-agent completed (runId=${runId}). Process the result and continue pending work.`;
|
|
108
|
-
const
|
|
108
|
+
const targetSession = requesterSessionKey ?? callerSession;
|
|
109
|
+
const result = await callHooksWake(wakeMessage, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger, targetSession ? { sessionKey: targetSession } : undefined);
|
|
109
110
|
if (result.ok) {
|
|
110
111
|
api.logger.info(`${LOG_PREFIX} Wake sent from subagent_ended: runId=${runId}`);
|
|
111
112
|
}
|
|
@@ -130,7 +131,7 @@ export function registerSubagentTracker(api) {
|
|
|
130
131
|
// Send wake to ensure the main agent processes the announce and continues work
|
|
131
132
|
const config = getConfig(api);
|
|
132
133
|
if (config.webhook_bridge_enabled && config.gateway_url && config.hooks_token) {
|
|
133
|
-
void callHooksWake(`[System] Sub-agent completed (runId=${matchedRunId}). Process the announce result and continue any pending work.`, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger).then((result) => {
|
|
134
|
+
void callHooksWake(`[System] Sub-agent completed (runId=${matchedRunId}). Process the announce result and continue any pending work.`, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger, callerSession ? { sessionKey: callerSession } : undefined).then((result) => {
|
|
134
135
|
if (result.ok) {
|
|
135
136
|
api.logger.info(`${LOG_PREFIX} Wake sent after sub-agent announce: runId=${matchedRunId}`);
|
|
136
137
|
}
|
|
@@ -63,7 +63,12 @@ export function registerAgentEndReminder(api) {
|
|
|
63
63
|
}
|
|
64
64
|
const config = getConfig(api);
|
|
65
65
|
if (config.webhook_bridge_enabled && config.hooks_token) {
|
|
66
|
-
|
|
66
|
+
if (!sessionKey) {
|
|
67
|
+
api.logger.warn(`${LOG_PREFIX} No sessionKey available for wake after agent_end — skipping to avoid new session creation`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
callHooksWake(`⚠️ Agent ended with ${incomplete.length} incomplete todo(s). Resume work.`, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger, { sessionKey }).catch(() => { });
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
73
|
api.logger.warn(`${LOG_PREFIX} Agent ended with ${incomplete.length} incomplete todo(s)`);
|
|
69
74
|
}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerWebSearchTool } from './tools/web-search.js';
|
|
|
17
17
|
import { registerRalphCommands } from './commands/ralph-commands.js';
|
|
18
18
|
import { registerStatusCommands } from './commands/status-commands.js';
|
|
19
19
|
import { registerPersonaCommands } from './commands/persona-commands.js';
|
|
20
|
+
import { registerTodoCommands } from './commands/todo-commands.js';
|
|
20
21
|
import { registerContextInjector } from './hooks/context-injector.js';
|
|
21
22
|
import { registerGuardrailInjector } from './hooks/guardrail-injector.js';
|
|
22
23
|
import { registerSessionSync } from './hooks/session-sync.js';
|
|
@@ -181,6 +182,11 @@ export default function register(api) {
|
|
|
181
182
|
registry.commands.push('omoc_health', 'omoc_config');
|
|
182
183
|
api.logger.info(`[${PLUGIN_ID}] Status commands registered (omoc_health, omoc_config)`);
|
|
183
184
|
});
|
|
185
|
+
safeRegister(api, 'todo-commands', 'command', () => {
|
|
186
|
+
registerTodoCommands(api);
|
|
187
|
+
registry.commands.push('todos');
|
|
188
|
+
api.logger.info(`[${PLUGIN_ID}] Todo commands registered (/todos)`);
|
|
189
|
+
});
|
|
184
190
|
initPersonaState(api);
|
|
185
191
|
safeRegister(api, 'persona-commands', 'command', () => {
|
|
186
192
|
registerPersonaCommands(api);
|
|
@@ -84,17 +84,36 @@ async function checkStaleSubagents(api) {
|
|
|
84
84
|
}
|
|
85
85
|
if (stale.length === 0)
|
|
86
86
|
return;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
87
|
+
// Group stale entries by callerSessionKey and send separate wakes per group
|
|
88
|
+
const grouped = new Map();
|
|
89
|
+
for (const s of stale) {
|
|
90
|
+
const key = s.callerSessionKey ?? '__default__';
|
|
91
|
+
const group = grouped.get(key) ?? [];
|
|
92
|
+
group.push(s);
|
|
93
|
+
grouped.set(key, group);
|
|
94
|
+
}
|
|
95
|
+
for (const [sessionKey, entries] of grouped) {
|
|
96
|
+
const details = entries
|
|
97
|
+
.map((s) => ` - runId=${s.runId} task="${s.task.substring(0, 80)}" (${Math.round((now - s.spawnedAt) / 60000)}m ago)`)
|
|
98
|
+
.join('\n');
|
|
99
|
+
const message = `[OmOC Sub-agent Alert] ${entries.length} sub-agent(s) may have completed without announce:\n${details}\n\n` +
|
|
100
|
+
`Check sub-agent status with \`/subagents list\` or \`/subagents info <id>\`. ` +
|
|
101
|
+
`If completed, collect results and proceed. If still running, wait.`;
|
|
102
|
+
const targetSession = sessionKey !== '__default__' ? sessionKey : undefined;
|
|
103
|
+
if (!targetSession) {
|
|
104
|
+
api.logger.warn(`${LOG_PREFIX} No sessionKey available for stale sub-agent alert (${entries.length} stale) — cleaning up without wake to avoid new session creation`);
|
|
105
|
+
// Still clean up tracking to prevent infinite reprocessing
|
|
106
|
+
for (const s of entries) {
|
|
107
|
+
trackedSubagents.delete(s.runId);
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const result = await callHooksWake(message, webhookConfig, api.logger, { sessionKey: targetSession });
|
|
112
|
+
if (result.ok) {
|
|
113
|
+
api.logger.info(`${LOG_PREFIX} Stale sub-agent alert sent via hooks/wake (${entries.length} stale, target=${targetSession})`);
|
|
114
|
+
for (const s of entries) {
|
|
115
|
+
trackedSubagents.delete(s.runId);
|
|
116
|
+
}
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
}
|
|
@@ -34,7 +34,7 @@ const DelegateParamsSchema = Type.Object({
|
|
|
34
34
|
skills: Type.Optional(Type.Array(Type.String(), { description: 'Skill names to load' })),
|
|
35
35
|
background: Type.Optional(Type.Boolean({ description: 'Run in background (default: false)', default: false })),
|
|
36
36
|
});
|
|
37
|
-
function
|
|
37
|
+
function getRecommendedModelForCategory(category, api) {
|
|
38
38
|
const config = getConfig(api);
|
|
39
39
|
const override = config.model_routing?.[category];
|
|
40
40
|
if (override?.model) {
|
|
@@ -59,7 +59,7 @@ export function registerDelegateTool(api) {
|
|
|
59
59
|
return toolError(`Invalid category: ${params.category}. Valid: ${validCategories.join(', ')}`);
|
|
60
60
|
}
|
|
61
61
|
const category = params.category;
|
|
62
|
-
const { model, alternatives } =
|
|
62
|
+
const { model, alternatives } = getRecommendedModelForCategory(category, api);
|
|
63
63
|
const agentId = params.agent_id || DEFAULT_CATEGORY_AGENTS[category];
|
|
64
64
|
api.logger.info(`${LOG_PREFIX} Delegating task:`, { category, model, agentId });
|
|
65
65
|
const instruction = [
|
|
@@ -68,12 +68,12 @@ export function registerDelegateTool(api) {
|
|
|
68
68
|
'⚡ NOW CALL sessions_spawn with these parameters:',
|
|
69
69
|
` task: "${params.task_description}"`,
|
|
70
70
|
` mode: "run"`,
|
|
71
|
-
` model: "${model}"`,
|
|
72
71
|
` agentId: "${agentId}"`,
|
|
73
|
-
|
|
74
|
-
alternatives?.length ? `
|
|
72
|
+
` # recommended model (do NOT pass to sessions_spawn): ${model}`,
|
|
73
|
+
alternatives?.length ? ` Recommended fallback models (informational only): ${alternatives.join(', ')}` : '',
|
|
75
74
|
params.background ? ' (background execution — results will arrive via push notification)' : '',
|
|
76
75
|
'',
|
|
76
|
+
'Do NOT set sessions_spawn model unless explicitly asked by user.',
|
|
77
77
|
'Do NOT just return this metadata. Actually call sessions_spawn NOW.',
|
|
78
78
|
'',
|
|
79
79
|
'⚠️ AFTER the subagent completes:',
|
|
@@ -39,7 +39,12 @@ export function registerTodoUpdateTool(api) {
|
|
|
39
39
|
}, sessionKey);
|
|
40
40
|
if (!updated)
|
|
41
41
|
return toolResponse(JSON.stringify({ error: 'todo_not_found', id }));
|
|
42
|
-
|
|
42
|
+
// Include completion notice when status changes to completed
|
|
43
|
+
const result = { todo: updated };
|
|
44
|
+
if (params.status === 'completed') {
|
|
45
|
+
result.notice = `✅ Todo ${updated.id} completed: ${updated.content}`;
|
|
46
|
+
}
|
|
47
|
+
return toolResponse(JSON.stringify(result, null, 2));
|
|
43
48
|
},
|
|
44
49
|
});
|
|
45
50
|
}
|
|
@@ -8,6 +8,10 @@ export interface HooksAgentOptions {
|
|
|
8
8
|
sessionKey?: string;
|
|
9
9
|
deliver?: boolean;
|
|
10
10
|
}
|
|
11
|
+
export interface HooksWakeOptions {
|
|
12
|
+
/** Target a specific session instead of creating a new one */
|
|
13
|
+
sessionKey?: string;
|
|
14
|
+
}
|
|
11
15
|
export interface WebhookResult {
|
|
12
16
|
ok: boolean;
|
|
13
17
|
status?: number;
|
|
@@ -15,7 +19,7 @@ export interface WebhookResult {
|
|
|
15
19
|
}
|
|
16
20
|
export declare function callHooksWake(text: string, config: WebhookConfig, logger?: {
|
|
17
21
|
warn: (...args: unknown[]) => void;
|
|
18
|
-
}): Promise<WebhookResult>;
|
|
22
|
+
}, options?: HooksWakeOptions): Promise<WebhookResult>;
|
|
19
23
|
export declare function callHooksAgent(message: string, config: WebhookConfig, options?: HooksAgentOptions, logger?: {
|
|
20
24
|
warn: (...args: unknown[]) => void;
|
|
21
25
|
}): Promise<WebhookResult>;
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { LOG_PREFIX } from '../constants.js';
|
|
2
|
-
export async function callHooksWake(text, config, logger) {
|
|
2
|
+
export async function callHooksWake(text, config, logger, options) {
|
|
3
3
|
if (!config.hooks_token) {
|
|
4
4
|
return { ok: false, error: 'hooks_token not configured' };
|
|
5
5
|
}
|
|
6
6
|
try {
|
|
7
|
+
const payload = { text, mode: 'now' };
|
|
8
|
+
if (options?.sessionKey) {
|
|
9
|
+
payload.sessionKey = options.sessionKey;
|
|
10
|
+
}
|
|
7
11
|
const res = await fetch(`${config.gateway_url}/hooks/wake`, {
|
|
8
12
|
method: 'POST',
|
|
9
13
|
headers: {
|
|
10
14
|
'Authorization': `Bearer ${config.hooks_token}`,
|
|
11
15
|
'Content-Type': 'application/json',
|
|
12
16
|
},
|
|
13
|
-
body: JSON.stringify(
|
|
17
|
+
body: JSON.stringify(payload),
|
|
14
18
|
});
|
|
15
19
|
return { ok: res.ok, status: res.status };
|
|
16
20
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "oh-my-openclaw",
|
|
3
3
|
"name": "Oh-My-OpenClaw",
|
|
4
4
|
"description": "Multi-agent orchestration plugin — 11 agents, category-based model routing, todo enforcer, ralph loop, agent setup CLI, and custom tools",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.21.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills"
|
|
8
8
|
],
|
package/package.json
CHANGED