@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.
@@ -0,0 +1,2 @@
1
+ import { OmocPluginApi } from '../types.js';
2
+ export declare function registerTodoCommands(api: OmocPluginApi): void;
@@ -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 result = await callHooksWake(wakeMessage, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger);
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
- callHooksWake(`⚠️ Agent ended with ${incomplete.length} incomplete todo(s). Resume work.`, { gateway_url: config.gateway_url, hooks_token: config.hooks_token }, api.logger).catch(() => { });
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
- const details = stale
88
- .map((s) => ` - runId=${s.runId} task="${s.task.substring(0, 80)}" (${Math.round((now - s.spawnedAt) / 60000)}m ago)`)
89
- .join('\n');
90
- const message = `[OmOC Sub-agent Alert] ${stale.length} sub-agent(s) may have completed without announce:\n${details}\n\n` +
91
- `Check sub-agent status with \`/subagents list\` or \`/subagents info <id>\`. ` +
92
- `If completed, collect results and proceed. If still running, wait.`;
93
- const result = await callHooksWake(message, webhookConfig, api.logger);
94
- if (result.ok) {
95
- api.logger.info(`${LOG_PREFIX} Stale sub-agent alert sent via hooks/wake (${stale.length} stale)`);
96
- for (const s of stale) {
97
- trackedSubagents.delete(s.runId);
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 getModelForCategory(category, api) {
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 } = getModelForCategory(category, api);
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
- alternatives?.length ? ` fallback_models: ${JSON.stringify(alternatives)}` : '',
74
- alternatives?.length ? ` If "${model}" is unavailable, try: ${alternatives.join(', ')}` : '',
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
- return toolResponse(JSON.stringify({ todo: updated }, null, 2));
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({ text, mode: 'now' }),
17
+ body: JSON.stringify(payload),
14
18
  });
15
19
  return { ok: res.ok, status: res.status };
16
20
  }
@@ -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.20.0",
5
+ "version": "0.21.0",
6
6
  "skills": [
7
7
  "skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happycastle/oh-my-openclaw",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Oh-My-OpenClaw plugin — multi-agent orchestration, todo enforcer, ralph loop, and custom tools for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",