@happycastle/oh-my-openclaw 0.14.2 → 0.15.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/agents/atlas.md CHANGED
@@ -94,17 +94,20 @@ Every `task()` prompt MUST include ALL 6 sections:
94
94
  **If your prompt is under 30 lines, it's TOO SHORT.**
95
95
  </delegation_system>
96
96
 
97
+ <task_setup>
98
+ ## BEFORE ANY WORK (NON-NEGOTIABLE)
99
+
100
+ 1. Call `omoc_todo_create` to plan all orchestration steps (one todo per step)
101
+ 2. Call `omoc_todo_list` to review the plan before starting
102
+ 3. Call `omoc_todo_update` to mark todos `in_progress` before starting, `completed` immediately after
103
+
104
+ The `agent_end` hook warns about incomplete todos when the session ends.
105
+ </task_setup>
106
+
97
107
  <workflow>
98
108
  ## Step 0: Register Tracking
99
109
 
100
- ```
101
- TodoWrite([{
102
- id: "orchestrate-plan",
103
- content: "Complete ALL tasks in work plan",
104
- status: "in_progress",
105
- priority: "high"
106
- }])
107
- ```
110
+ Use `omoc_todo_create` to create todos for each orchestration step, then proceed.
108
111
 
109
112
  ## Step 1: Analyze Plan
110
113
 
@@ -13,6 +13,12 @@ You are **Frontend**, the visual engineering specialist in the oh-my-openclaw sy
13
13
  - **Philosophy**: Design is how it works, not just how it looks. Ship pixel-perfect, accessible, performant interfaces.
14
14
  - **Strength**: Bridging design intent with production code
15
15
 
16
+ ## Task Setup (BEFORE ANY WORK)
17
+
18
+ 1. If new work with 2+ steps: call `omoc_todo_create` to plan all steps FIRST
19
+ 2. Track progress via `omoc_todo_update` (in_progress → completed per step)
20
+ 3. Call `omoc_todo_list` to review before starting — mark completed IMMEDIATELY after each step, NEVER batch
21
+
16
22
  ## Core Protocol
17
23
 
18
24
  ### Task Reception
@@ -214,37 +214,33 @@ STOP searching when:
214
214
 
215
215
  ---
216
216
 
217
- ## Todo Discipline (NON-NEGOTIABLE)
217
+ ## Task Setup (NON-NEGOTIABLE)
218
218
 
219
- **Track ALL multi-step work with todos. This is your execution backbone.**
219
+ **BEFORE ANY WORK, set up task tracking. This is your execution backbone.**
220
220
 
221
- ### When to Create Todos (MANDATORY)
221
+ ### First Action on Every Task (MANDATORY)
222
222
 
223
- - **2+ step task** `todowrite` FIRST, atomic breakdown
224
- - **Uncertain scope** `todowrite` to clarify thinking
225
- - **Complex single task** Break down into trackable steps
226
-
227
- ### Workflow (STRICT)
228
-
229
- 1. **On task start**: `todowrite` with atomic steps—no announcements, just create
230
- 2. **Before each step**: Mark `in_progress` (ONE at a time)
231
- 3. **After each step**: Mark `completed` IMMEDIATELY (NEVER batch)
232
- 4. **Scope changes**: Update todos BEFORE proceeding
223
+ 1. Call `omoc_todo_list`check for incomplete todos
224
+ 2. If incomplete todos exist: resume them before starting new work
225
+ 3. If new work with 2+ steps: call `omoc_todo_create` for each step FIRST
226
+ 4. Before each step: `omoc_todo_update` → status `in_progress` (ONE at a time)
227
+ 5. After each step: `omoc_todo_update` → status `completed` IMMEDIATELY (NEVER batch)
228
+ 6. Scope changes: create new todos or update existing BEFORE proceeding
233
229
 
234
230
  ### Why This Matters
235
231
 
236
232
  - **Execution anchor**: Todos prevent drift from original request
237
233
  - **Recovery**: If interrupted, todos enable seamless continuation
238
- - **Accountability**: Each todo = explicit commitment to deliver
234
+ - **Visibility**: `agent_end` warns about incomplete todos
239
235
 
240
236
  ### Anti-Patterns (BLOCKING)
241
237
 
242
- - **Skipping todos on multi-step work** — Steps get forgotten, user has no visibility
238
+ - **Skipping `omoc_todo_list` at start** — You miss incomplete todos
239
+ - **Starting work without `omoc_todo_create`** — Steps get forgotten, no visibility
243
240
  - **Batch-completing multiple todos** — Defeats real-time tracking purpose
244
241
  - **Proceeding without `in_progress`** — No indication of current work
245
- - **Finishing without completing todos** — Task appears incomplete
246
242
 
247
- **NO TODOS ON MULTI-STEP WORK = INCOMPLETE WORK.**
243
+ **NO TODO SETUP ON MULTI-STEP WORK = INCOMPLETE WORK.**
248
244
 
249
245
  ---
250
246
 
@@ -8,15 +8,17 @@ Sisyphus-Junior - Focused executor from OhMyOpenCode.
8
8
  Execute tasks directly.
9
9
  </Role>
10
10
 
11
- <Todo_Discipline>
12
- TODO OBSESSION (NON-NEGOTIABLE):
13
- - 2+ steps todowrite FIRST, atomic breakdown
14
- - Mark in_progress before starting (ONE at a time)
15
- - Mark completed IMMEDIATELY after each step
16
- - NEVER batch completions
11
+ <Task_Setup>
12
+ BEFORE ANY WORK (NON-NEGOTIABLE):
13
+ 1. Call `omoc_todo_list` to check existing incomplete todos
14
+ 2. If resuming: pick up where you left off
15
+ 3. If new work with 2+ steps: call `omoc_todo_create` for each step FIRST
16
+ 4. Mark in_progress before starting (ONE at a time via `omoc_todo_update`)
17
+ 5. Mark completed IMMEDIATELY after each step
18
+ 6. NEVER batch completions
17
19
 
18
- No todos on multi-step work = INCOMPLETE WORK.
19
- </Todo_Discipline>
20
+ No todo setup on multi-step work = INCOMPLETE WORK.
21
+ </Task_Setup>
20
22
 
21
23
  <Verification>
22
24
  Task NOT complete without:
@@ -15,6 +15,11 @@ export function createMockApi(overrides) {
15
15
  warn: vi.fn(),
16
16
  error: vi.fn(),
17
17
  },
18
+ runtime: {
19
+ system: {
20
+ enqueueSystemEvent: vi.fn(),
21
+ },
22
+ },
18
23
  registerHook: vi.fn(),
19
24
  registerTool: vi.fn(),
20
25
  registerCommand: vi.fn(),
@@ -9,6 +9,6 @@ export declare const WORKER_IDS: Set<string>;
9
9
  /** Maps agent ID to markdown persona filename (without extension) */
10
10
  export declare const AGENT_MD_MAP: Record<string, string>;
11
11
  /** Maps agent ID to model tier for provider preset selection */
12
- export declare const AGENT_TIER_MAP: Record<string, 'strategic' | 'reasoning' | 'analysis' | 'worker' | 'deep-worker' | 'search' | 'research' | 'visual'>;
12
+ export declare const AGENT_TIER_MAP: Record<string, 'planner' | 'orchestrator' | 'reasoning' | 'analysis' | 'worker' | 'deep-worker' | 'search' | 'research' | 'visual'>;
13
13
  /** All agent IDs (orchestrators + workers + read-only specialists) */
14
14
  export declare const ALL_AGENT_IDS: string[];
@@ -29,8 +29,8 @@ export const AGENT_MD_MAP = {
29
29
  };
30
30
  /** Maps agent ID to model tier for provider preset selection */
31
31
  export const AGENT_TIER_MAP = {
32
- omoc_prometheus: 'strategic',
33
- omoc_atlas: 'strategic',
32
+ omoc_prometheus: 'planner',
33
+ omoc_atlas: 'orchestrator',
34
34
  omoc_oracle: 'reasoning',
35
35
  omoc_metis: 'analysis',
36
36
  omoc_momus: 'analysis',
@@ -0,0 +1,47 @@
1
+ type McpServerEntry = {
2
+ url: string;
3
+ description: string;
4
+ };
5
+ /** Core MCP servers — always included in setup */
6
+ export declare const CORE_MCP_SERVERS: Record<string, McpServerEntry>;
7
+ /** Optional MCP servers — user can toggle during setup */
8
+ export declare const OPTIONAL_MCP_SERVERS: Record<string, McpServerEntry>;
9
+ /** All MCP servers (core + optional) — backward-compatible union */
10
+ export declare const OMOC_MCP_SERVERS: Record<string, McpServerEntry>;
11
+ type McporterConfig = {
12
+ mcpServers: Record<string, {
13
+ url?: string;
14
+ baseUrl?: string;
15
+ type?: string;
16
+ [key: string]: unknown;
17
+ }>;
18
+ [key: string]: unknown;
19
+ };
20
+ /**
21
+ * Resolve mcporter config path.
22
+ * Priority: ~/.openclaw/workspace/config/mcporter.json > ~/.config/mcporter/mcporter.json
23
+ */
24
+ export declare function resolveMcporterConfigPath(): string;
25
+ export declare function readMcporterConfig(configPath: string): McporterConfig;
26
+ export declare function writeMcporterConfig(configPath: string, config: McporterConfig): void;
27
+ export interface McporterMergeResult {
28
+ added: string[];
29
+ skipped: string[];
30
+ }
31
+ export declare function mergeMcpServers(existing: McporterConfig, servers: Record<string, McpServerEntry>): {
32
+ config: McporterConfig;
33
+ result: McporterMergeResult;
34
+ };
35
+ type Logger = {
36
+ info: (msg: string) => void;
37
+ warn: (msg: string) => void;
38
+ error: (msg: string) => void;
39
+ };
40
+ export interface McporterSetupOptions {
41
+ configPath?: string;
42
+ excludeServers?: string[];
43
+ dryRun?: boolean;
44
+ logger: Logger;
45
+ }
46
+ export declare function runMcporterSetup(options: McporterSetupOptions): McporterMergeResult;
47
+ export {};
@@ -0,0 +1,118 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /** Core MCP servers — always included in setup */
4
+ export const CORE_MCP_SERVERS = {
5
+ exa: {
6
+ url: 'https://mcp.exa.ai/mcp?tools=web_search_exa',
7
+ description: 'Semantic web search (Exa)',
8
+ },
9
+ context7: {
10
+ url: 'https://mcp.context7.com/mcp',
11
+ description: 'Library/framework documentation search',
12
+ },
13
+ grep_app: {
14
+ url: 'https://mcp.grep.app',
15
+ description: 'Open-source code search on GitHub',
16
+ },
17
+ };
18
+ /** Optional MCP servers — user can toggle during setup */
19
+ export const OPTIONAL_MCP_SERVERS = {
20
+ 'web-search-prime': {
21
+ url: 'https://api.z.ai/api/mcp/web_search_prime/mcp',
22
+ description: 'Keyword-based web search (news, blogs, general)',
23
+ },
24
+ 'web-reader': {
25
+ url: 'https://api.z.ai/api/mcp/web_reader/mcp',
26
+ description: 'Clean full-page content extraction',
27
+ },
28
+ zread: {
29
+ url: 'https://api.z.ai/api/mcp/zread/mcp',
30
+ description: 'Direct GitHub repository exploration',
31
+ },
32
+ };
33
+ /** All MCP servers (core + optional) — backward-compatible union */
34
+ export const OMOC_MCP_SERVERS = {
35
+ ...CORE_MCP_SERVERS,
36
+ ...OPTIONAL_MCP_SERVERS,
37
+ };
38
+ /**
39
+ * Resolve mcporter config path.
40
+ * Priority: ~/.openclaw/workspace/config/mcporter.json > ~/.config/mcporter/mcporter.json
41
+ */
42
+ export function resolveMcporterConfigPath() {
43
+ const homeDir = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
44
+ const openclawPath = path.join(homeDir, '.openclaw', 'workspace', 'config', 'mcporter.json');
45
+ if (fs.existsSync(openclawPath)) {
46
+ return openclawPath;
47
+ }
48
+ const mcporterHomePath = path.join(homeDir, '.config', 'mcporter', 'mcporter.json');
49
+ if (fs.existsSync(mcporterHomePath)) {
50
+ return mcporterHomePath;
51
+ }
52
+ return openclawPath;
53
+ }
54
+ export function readMcporterConfig(configPath) {
55
+ if (!fs.existsSync(configPath)) {
56
+ return { mcpServers: {} };
57
+ }
58
+ const raw = fs.readFileSync(configPath, 'utf-8');
59
+ const parsed = JSON.parse(raw);
60
+ if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') {
61
+ parsed.mcpServers = {};
62
+ }
63
+ return parsed;
64
+ }
65
+ export function writeMcporterConfig(configPath, config) {
66
+ const dir = path.dirname(configPath);
67
+ if (!fs.existsSync(dir)) {
68
+ fs.mkdirSync(dir, { recursive: true });
69
+ }
70
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
71
+ }
72
+ export function mergeMcpServers(existing, servers) {
73
+ const result = { added: [], skipped: [] };
74
+ const merged = { ...existing, mcpServers: { ...existing.mcpServers } };
75
+ for (const [name, entry] of Object.entries(servers)) {
76
+ if (merged.mcpServers[name]) {
77
+ result.skipped.push(name);
78
+ }
79
+ else {
80
+ merged.mcpServers[name] = { url: entry.url };
81
+ result.added.push(name);
82
+ }
83
+ }
84
+ return { config: merged, result };
85
+ }
86
+ export function runMcporterSetup(options) {
87
+ const { logger, dryRun = false, excludeServers = [] } = options;
88
+ const configPath = options.configPath ?? resolveMcporterConfigPath();
89
+ logger.info(`mcporter config: ${configPath}`);
90
+ const excludeSet = new Set(excludeServers);
91
+ const servers = {};
92
+ for (const [name, entry] of Object.entries(OMOC_MCP_SERVERS)) {
93
+ if (!excludeSet.has(name)) {
94
+ servers[name] = entry;
95
+ }
96
+ }
97
+ const existing = readMcporterConfig(configPath);
98
+ const { config: merged, result } = mergeMcpServers(existing, servers);
99
+ if (result.added.length === 0) {
100
+ logger.info('No changes needed — all MCP servers already configured.');
101
+ return result;
102
+ }
103
+ if (dryRun) {
104
+ logger.info(`[dry-run] Would add ${result.added.length} MCP server(s): ${result.added.join(', ')}`);
105
+ return result;
106
+ }
107
+ if (fs.existsSync(configPath)) {
108
+ const backupPath = configPath + '.bak';
109
+ fs.copyFileSync(configPath, backupPath);
110
+ logger.info(`Backup created: ${backupPath}`);
111
+ }
112
+ writeMcporterConfig(configPath, merged);
113
+ logger.info(`Added ${result.added.length} MCP server(s): ${result.added.join(', ')}`);
114
+ if (result.skipped.length > 0) {
115
+ logger.info(`Skipped ${result.skipped.length} existing server(s): ${result.skipped.join(', ')}`);
116
+ }
117
+ return result;
118
+ }
@@ -1,5 +1,5 @@
1
1
  import { AGENT_TIER_MAP } from '../agents/agent-ids.js';
2
- export type ModelTier = 'strategic' | 'reasoning' | 'analysis' | 'worker' | 'deep-worker' | 'search' | 'research' | 'visual';
2
+ export type ModelTier = 'planner' | 'orchestrator' | 'reasoning' | 'analysis' | 'worker' | 'deep-worker' | 'search' | 'research' | 'visual';
3
3
  export type ModelConfig = {
4
4
  primary: string;
5
5
  fallbacks: string[];
@@ -1,7 +1,8 @@
1
1
  import { AGENT_TIER_MAP } from '../agents/agent-ids.js';
2
2
  export const PROVIDER_PRESETS = {
3
3
  anthropic: {
4
- strategic: { primary: 'anthropic/claude-opus-4-6', fallbacks: ['openai/gpt-5.3-codex'] },
4
+ planner: { primary: 'anthropic/claude-opus-4-6', fallbacks: ['openai/gpt-5.3-codex'] },
5
+ orchestrator: { primary: 'anthropic/claude-sonnet-4-6', fallbacks: ['openai/gpt-4.1'] },
5
6
  reasoning: { primary: 'anthropic/claude-opus-4-6', fallbacks: ['openai/gpt-5.3-codex'] },
6
7
  analysis: { primary: 'anthropic/claude-sonnet-4-6', fallbacks: ['openai/gpt-4.1'] },
7
8
  worker: { primary: 'anthropic/claude-opus-4-6', fallbacks: ['openai/gpt-5.3-codex'] },
@@ -11,7 +12,8 @@ export const PROVIDER_PRESETS = {
11
12
  visual: { primary: 'google/gemini-3.1-pro', fallbacks: ['anthropic/claude-sonnet-4-6'] },
12
13
  },
13
14
  openai: {
14
- strategic: { primary: 'openai/gpt-5.3-codex', fallbacks: ['anthropic/claude-opus-4-6'] },
15
+ planner: { primary: 'openai/gpt-5.3-codex', fallbacks: ['anthropic/claude-opus-4-6'] },
16
+ orchestrator: { primary: 'openai/gpt-4.1', fallbacks: ['anthropic/claude-sonnet-4-6'] },
15
17
  reasoning: { primary: 'openai/gpt-5.3-codex', fallbacks: ['anthropic/claude-opus-4-6'] },
16
18
  analysis: { primary: 'openai/gpt-4.1', fallbacks: ['anthropic/claude-sonnet-4-6'] },
17
19
  worker: { primary: 'openai/gpt-5.3-codex', fallbacks: ['anthropic/claude-opus-4-6'] },
@@ -21,7 +23,8 @@ export const PROVIDER_PRESETS = {
21
23
  visual: { primary: 'google/gemini-3.1-pro', fallbacks: ['openai/gpt-4.1'] },
22
24
  },
23
25
  google: {
24
- strategic: { primary: 'google/gemini-3.1-pro', fallbacks: ['anthropic/claude-opus-4-6'] },
26
+ planner: { primary: 'google/gemini-3.1-pro', fallbacks: ['anthropic/claude-opus-4-6'] },
27
+ orchestrator: { primary: 'google/gemini-3-flash', fallbacks: ['anthropic/claude-sonnet-4-6'] },
25
28
  reasoning: { primary: 'google/gemini-3.1-pro', fallbacks: ['anthropic/claude-opus-4-6'] },
26
29
  analysis: { primary: 'google/gemini-3-flash', fallbacks: ['anthropic/claude-sonnet-4-6'] },
27
30
  worker: { primary: 'google/gemini-3.1-pro', fallbacks: ['anthropic/claude-opus-4-6'] },
@@ -38,7 +41,7 @@ export const PROVIDER_LABELS = {
38
41
  google: 'Google (Gemini)',
39
42
  custom: 'Custom (enter model IDs manually)',
40
43
  };
41
- export const MODEL_TIERS = ['strategic', 'reasoning', 'analysis', 'worker', 'deep-worker', 'search', 'research', 'visual'];
44
+ export const MODEL_TIERS = ['planner', 'orchestrator', 'reasoning', 'analysis', 'worker', 'deep-worker', 'search', 'research', 'visual'];
42
45
  export function buildCustomPreset(tierModels) {
43
46
  const preset = {};
44
47
  for (const tier of MODEL_TIERS) {
@@ -27,6 +27,8 @@ export interface MergeResult {
27
27
  added: string[];
28
28
  skipped: string[];
29
29
  updated: string[];
30
+ mcporterAdded?: string[];
31
+ mcporterSkipped?: string[];
30
32
  }
31
33
  export declare function mergeAgentConfigs(existing: Array<{
32
34
  id: string;
@@ -39,18 +41,36 @@ export declare function mergeAgentConfigs(existing: Array<{
39
41
  result: MergeResult;
40
42
  };
41
43
  export declare function applyProviderToConfigs(configs: OmocAgentConfig[], provider: string): OmocAgentConfig[];
42
- export declare function runInteractiveSetup(logger: Logger): Promise<{
44
+ export interface InteractiveSetupResult {
43
45
  provider: string;
44
- }>;
46
+ setupMcporter: boolean;
47
+ excludeServers: string[];
48
+ enableTodoEnforcer: boolean;
49
+ enablePlannerGuard: boolean;
50
+ }
51
+ export declare function runInteractiveSetup(logger: Logger): Promise<InteractiveSetupResult>;
45
52
  export interface SetupOptions {
46
53
  configPath?: string;
47
54
  workspaceDir?: string;
48
55
  force?: boolean;
49
56
  dryRun?: boolean;
50
57
  provider?: string;
58
+ setupMcporter?: boolean;
59
+ mcporterConfigPath?: string;
60
+ excludeServers?: string[];
61
+ enableTodoEnforcer?: boolean;
62
+ enablePlannerGuard?: boolean;
51
63
  interactive?: boolean;
52
64
  logger: Logger;
53
65
  }
66
+ export declare function applyPlannerGuard(agentList: Array<{
67
+ id: string;
68
+ tools?: {
69
+ deny?: string[];
70
+ [key: string]: unknown;
71
+ };
72
+ [key: string]: unknown;
73
+ }>): void;
54
74
  export declare function runSetup(options: SetupOptions): MergeResult;
55
75
  export declare function registerSetupCli(ctx: {
56
76
  program: {
package/dist/cli/setup.js CHANGED
@@ -4,6 +4,8 @@ import * as readline from 'node:readline';
4
4
  import JSON5 from 'json5';
5
5
  import { OMOC_AGENT_CONFIGS } from '../agents/agent-configs.js';
6
6
  import { PROVIDER_PRESETS, PROVIDER_LABELS, AGENT_TIER_MAP, MODEL_TIERS, applyProviderPreset, getProviderNames, buildCustomPreset, registerCustomPreset, } from './model-presets.js';
7
+ import { CORE_MCP_SERVERS, OPTIONAL_MCP_SERVERS, runMcporterSetup } from './mcporter-setup.js';
8
+ import { PLANNER_DENY } from '../constants.js';
7
9
  const CONFIG_FILENAMES = [
8
10
  'openclaw.json5',
9
11
  'openclaw.json',
@@ -122,7 +124,8 @@ function askQuestion(rl, question) {
122
124
  });
123
125
  }
124
126
  const TIER_LABELS = {
125
- strategic: 'Strategic Planning (prometheus, atlas)',
127
+ planner: 'Strategic Planning (prometheus)',
128
+ orchestrator: 'Orchestration (atlas)',
126
129
  reasoning: 'Deep Reasoning (oracle)',
127
130
  analysis: 'Analysis/Review (metis, momus)',
128
131
  worker: 'Implementation (sisyphus)',
@@ -148,8 +151,7 @@ function printPreview(logger, provider) {
148
151
  }
149
152
  async function runCustomProviderFlow(rl, logger) {
150
153
  logger.info('');
151
- logger.info(' Enter model IDs for each tier.');
152
- logger.info(' Format: provider/model (e.g., cliproxy/claude-opus-4-6, z.ai/gpt-5.3-codex)');
154
+ logger.info('Step 1/3: Select your AI provider');
153
155
  logger.info('');
154
156
  const tierModels = {};
155
157
  for (const tier of MODEL_TIERS) {
@@ -177,15 +179,23 @@ export async function runInteractiveSetup(logger) {
177
179
  input: process.stdin,
178
180
  output: process.stdout,
179
181
  });
182
+ const emptyResult = {
183
+ provider: '',
184
+ setupMcporter: false,
185
+ excludeServers: [],
186
+ enableTodoEnforcer: false,
187
+ enablePlannerGuard: false,
188
+ };
180
189
  try {
181
190
  logger.info('');
182
- logger.info('🗺️ Oh-My-OpenClaw Agent Setup');
183
- logger.info(''.repeat(40));
191
+ logger.info('Oh-My-OpenClaw Agent Setup');
192
+ logger.info('-'.repeat(40));
184
193
  logger.info('');
194
+ // Step 1/4: Provider selection
185
195
  const presetProviders = getProviderNames();
186
196
  const choices = [...presetProviders, 'custom'];
187
197
  const choiceCount = choices.length;
188
- logger.info('Step 1/2: Select your AI provider');
198
+ logger.info('Step 1/4: Select your AI provider');
189
199
  logger.info('');
190
200
  choices.forEach((p, i) => {
191
201
  logger.info(` ${i + 1}. ${PROVIDER_LABELS[p] ?? p}`);
@@ -209,24 +219,63 @@ export async function runInteractiveSetup(logger) {
209
219
  provider = await runCustomProviderFlow(rl, logger);
210
220
  }
211
221
  logger.info('');
212
- logger.info(` Selected: ${PROVIDER_LABELS[provider] ?? 'Custom'}`);
222
+ logger.info(` Selected: ${PROVIDER_LABELS[provider] ?? 'Custom'}`);
213
223
  logger.info('');
214
- logger.info('Step 2/2: Model configuration preview');
224
+ // Step 2/4: Model preview + confirm
225
+ logger.info('Step 2/4: Model configuration preview');
215
226
  logger.info('');
216
227
  printPreview(logger, provider);
217
228
  logger.info('');
218
229
  const confirm = await askQuestion(rl, ' Apply this configuration? (Y/n): ');
219
230
  if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') {
220
231
  logger.info(' Setup cancelled.');
221
- return { provider: '' };
232
+ return emptyResult;
222
233
  }
234
+ // Step 3/4: MCP servers
223
235
  logger.info('');
224
- return { provider };
236
+ logger.info('Step 3/4: MCP servers');
237
+ logger.info('');
238
+ logger.info(' Core servers (always included):');
239
+ for (const [name, entry] of Object.entries(CORE_MCP_SERVERS)) {
240
+ logger.info(` ${name}: ${entry.description}`);
241
+ }
242
+ logger.info('');
243
+ logger.info(' Optional servers:');
244
+ const excludeServers = [];
245
+ for (const [name, entry] of Object.entries(OPTIONAL_MCP_SERVERS)) {
246
+ const answer = await askQuestion(rl, ` Enable ${name} (${entry.description})? (Y/n): `);
247
+ if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
248
+ excludeServers.push(name);
249
+ }
250
+ }
251
+ logger.info('');
252
+ const setupMcporter = true;
253
+ // Step 4/4: Plugin features
254
+ logger.info('Step 4/4: Plugin features');
255
+ logger.info('');
256
+ const todoAnswer = await askQuestion(rl, ' Enable todo enforcer (forces task tracking)? (Y/n): ');
257
+ const enableTodoEnforcer = todoAnswer.toLowerCase() !== 'n' && todoAnswer.toLowerCase() !== 'no';
258
+ const guardAnswer = await askQuestion(rl, ' Enable planner guard (prevents prometheus from editing code)? (Y/n): ');
259
+ const enablePlannerGuard = guardAnswer.toLowerCase() !== 'n' && guardAnswer.toLowerCase() !== 'no';
260
+ logger.info('');
261
+ return { provider, setupMcporter, excludeServers, enableTodoEnforcer, enablePlannerGuard };
225
262
  }
226
263
  finally {
227
264
  rl.close();
228
265
  }
229
266
  }
267
+ export function applyPlannerGuard(agentList) {
268
+ for (const agent of agentList) {
269
+ if (agent.id === 'omoc_prometheus') {
270
+ if (!agent.tools) {
271
+ agent.tools = {};
272
+ }
273
+ const existingDeny = agent.tools.deny ?? [];
274
+ const merged = new Set([...existingDeny, ...PLANNER_DENY]);
275
+ agent.tools.deny = [...merged];
276
+ }
277
+ }
278
+ }
230
279
  export function runSetup(options) {
231
280
  const { logger, force = false, dryRun = false, provider } = options;
232
281
  const configPath = options.configPath ?? findConfigPath(options.workspaceDir);
@@ -281,6 +330,37 @@ export function runSetup(options) {
281
330
  if (result.added.length === 0 && result.updated.length === 0) {
282
331
  logger.info('No changes needed — all OmOC agents already present.');
283
332
  }
333
+ if (options.enablePlannerGuard) {
334
+ applyPlannerGuard(config.agents.list);
335
+ if (!dryRun) {
336
+ fs.writeFileSync(configPath, serializeConfig(config), 'utf-8');
337
+ }
338
+ logger.info('Planner guard enabled: prometheus restricted from code editing');
339
+ }
340
+ if (options.enableTodoEnforcer !== undefined) {
341
+ const pluginSettings = (config.pluginSettings ?? {});
342
+ if (!pluginSettings['oh-my-openclaw']) {
343
+ pluginSettings['oh-my-openclaw'] = {};
344
+ }
345
+ pluginSettings['oh-my-openclaw']['todo_enforcer_enabled'] = options.enableTodoEnforcer;
346
+ config.pluginSettings = pluginSettings;
347
+ if (!dryRun) {
348
+ fs.writeFileSync(configPath, serializeConfig(config), 'utf-8');
349
+ }
350
+ logger.info(`Todo enforcer: ${options.enableTodoEnforcer ? 'enabled' : 'disabled'}`);
351
+ }
352
+ if (options.setupMcporter) {
353
+ logger.info('');
354
+ logger.info('Setting up mcporter MCP servers...');
355
+ const mcpResult = runMcporterSetup({
356
+ configPath: options.mcporterConfigPath,
357
+ excludeServers: options.excludeServers,
358
+ dryRun,
359
+ logger,
360
+ });
361
+ result.mcporterAdded = mcpResult.added;
362
+ result.mcporterSkipped = mcpResult.skipped;
363
+ }
284
364
  return result;
285
365
  }
286
366
  export function registerSetupCli(ctx) {
@@ -299,11 +379,19 @@ export function registerSetupCli(ctx) {
299
379
  const valid = getProviderNames().join(', ');
300
380
  throw new Error(`Unknown provider "${provider}". Valid: ${valid}`);
301
381
  }
382
+ let setupMcporter = false;
383
+ let excludeServers = [];
384
+ let enableTodoEnforcer;
385
+ let enablePlannerGuard;
302
386
  if (!provider && process.stdin.isTTY) {
303
387
  const result = await runInteractiveSetup(ctx.logger);
304
388
  if (!result.provider)
305
389
  return;
306
390
  provider = result.provider;
391
+ setupMcporter = result.setupMcporter;
392
+ excludeServers = result.excludeServers;
393
+ enableTodoEnforcer = result.enableTodoEnforcer;
394
+ enablePlannerGuard = result.enablePlannerGuard;
307
395
  }
308
396
  runSetup({
309
397
  configPath: opts.config,
@@ -311,6 +399,10 @@ export function registerSetupCli(ctx) {
311
399
  force: provider ? true : opts.force,
312
400
  dryRun: opts.dryRun,
313
401
  provider,
402
+ setupMcporter,
403
+ excludeServers,
404
+ enableTodoEnforcer,
405
+ enablePlannerGuard,
314
406
  logger: ctx.logger,
315
407
  });
316
408
  ctx.logger.info('');
@@ -3,5 +3,6 @@ export declare const TOOL_PREFIX = "omoc_";
3
3
  export declare const ABSOLUTE_MAX_RALPH_ITERATIONS = 100;
4
4
  export declare const LOG_PREFIX = "[omoc]";
5
5
  export declare const READ_ONLY_DENY: string[];
6
+ export declare const PLANNER_DENY: string[];
6
7
  export declare const CATEGORIES: readonly ["quick", "deep", "ultrabrain", "visual-engineering", "multimodal", "artistry", "unspecified-low", "unspecified-high", "writing"];
7
8
  export type Category = (typeof CATEGORIES)[number];
package/dist/constants.js CHANGED
@@ -4,6 +4,7 @@ export const TOOL_PREFIX = 'omoc_';
4
4
  export const ABSOLUTE_MAX_RALPH_ITERATIONS = 100;
5
5
  export const LOG_PREFIX = '[omoc]';
6
6
  export const READ_ONLY_DENY = ['write', 'edit', 'apply_patch', 'sessions_spawn'];
7
+ export const PLANNER_DENY = ['write', 'edit', 'apply_patch'];
7
8
  // Category definitions
8
9
  export const CATEGORIES = [
9
10
  'quick',
@@ -7,7 +7,7 @@ export function registerSessionSync(api) {
7
7
  api.on('session_start', async (_event, ctx) => {
8
8
  try {
9
9
  const wsDir = ctx.workspaceDir;
10
- const activePersona = await getActivePersona(wsDir);
10
+ const activePersona = await getActivePersona(wsDir, ctx.agentId);
11
11
  if (!activePersona)
12
12
  return;
13
13
  const personaContent = readPersonaPromptSync(activePersona);
@@ -11,7 +11,7 @@ export function registerSpawnGuard(api) {
11
11
  }
12
12
  let activePersona;
13
13
  try {
14
- activePersona = await getActivePersona(ctx.workspaceDir);
14
+ activePersona = await getActivePersona(ctx.workspaceDir, ctx.agentId);
15
15
  }
16
16
  catch {
17
17
  return;
@@ -0,0 +1,7 @@
1
+ import { OmocPluginApi } from '../types.js';
2
+ declare const sessionCounters: Map<string, number>;
3
+ export declare function registerTodoReminder(api: OmocPluginApi): void;
4
+ export declare function registerAgentEndReminder(api: OmocPluginApi): void;
5
+ export declare function registerSessionCleanup(api: OmocPluginApi): void;
6
+ export declare function resetTodoReminderCounters(): void;
7
+ export { sessionCounters as _sessionCounters };
@@ -0,0 +1,93 @@
1
+ import { TOOL_PREFIX, LOG_PREFIX } from '../constants.js';
2
+ import { getIncompleteTodos, resetStore } from '../tools/todo/store.js';
3
+ const TODO_TOOL_NAMES = new Set([
4
+ `${TOOL_PREFIX}todo_create`,
5
+ `${TOOL_PREFIX}todo_list`,
6
+ `${TOOL_PREFIX}todo_update`,
7
+ ]);
8
+ const TURN_THRESHOLD = 10;
9
+ const REMINDER_MESSAGE = `
10
+
11
+ ---
12
+ ⚠️ [OMOC Todo Reminder] You have used ${TURN_THRESHOLD}+ tool calls without checking your todo list.
13
+
14
+ **Action required:** Call \`${TOOL_PREFIX}todo_list\` to review pending todos before continuing.
15
+ Ensure you are not drifting from the plan. Mark completed todos, update in-progress ones.`;
16
+ const sessionCounters = new Map();
17
+ function getSessionKey(payload) {
18
+ const sessionId = payload.sessionId;
19
+ return typeof sessionId === 'string' ? sessionId : '__default__';
20
+ }
21
+ export function registerTodoReminder(api) {
22
+ api.registerHook('tool_result_persist', (payload) => {
23
+ const toolName = payload.tool;
24
+ if (!toolName)
25
+ return undefined;
26
+ const sessionKey = getSessionKey(payload);
27
+ if (TODO_TOOL_NAMES.has(toolName)) {
28
+ sessionCounters.set(sessionKey, 0);
29
+ return undefined;
30
+ }
31
+ const current = sessionCounters.get(sessionKey) ?? 0;
32
+ const next = current + 1;
33
+ sessionCounters.set(sessionKey, next);
34
+ if (next >= TURN_THRESHOLD && next % TURN_THRESHOLD === 0) {
35
+ const content = typeof payload.content === 'string' ? payload.content : '';
36
+ return {
37
+ ...payload,
38
+ content: content + REMINDER_MESSAGE,
39
+ };
40
+ }
41
+ return undefined;
42
+ }, {
43
+ name: 'oh-my-openclaw.todo-reminder',
44
+ description: 'Reminds agent to check todo list after prolonged non-todo tool usage',
45
+ });
46
+ }
47
+ export function registerAgentEndReminder(api) {
48
+ api.on('agent_end', async (_event, ctx) => {
49
+ try {
50
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId;
51
+ const incomplete = getIncompleteTodos(sessionKey);
52
+ if (incomplete.length === 0)
53
+ return;
54
+ const summary = incomplete
55
+ .map((t) => ` - [${t.status}] ${t.id}: ${t.content}`)
56
+ .join('\n');
57
+ const warning = `⚠️ [OMOC] ${incomplete.length} incomplete todo(s):\n${summary}\n\n` +
58
+ `Call \`${TOOL_PREFIX}todo_list\` to review and resume work.`;
59
+ if (sessionKey) {
60
+ api.runtime.system.enqueueSystemEvent(warning, { sessionKey });
61
+ }
62
+ api.logger.warn(`${LOG_PREFIX} Agent ended with ${incomplete.length} incomplete todo(s)`);
63
+ }
64
+ catch {
65
+ // graceful degradation
66
+ }
67
+ }, { priority: 50 });
68
+ }
69
+ function clearSession(sessionKey, api, reason) {
70
+ resetStore(sessionKey);
71
+ sessionCounters.delete(sessionKey);
72
+ api.logger.info(`${LOG_PREFIX} Todo store cleared (${reason}, session=${sessionKey})`);
73
+ }
74
+ export function registerSessionCleanup(api) {
75
+ api.on('session_start', async (event, ctx) => {
76
+ if (event.resumedFrom)
77
+ return;
78
+ const sessionKey = ctx.sessionKey ?? ctx.sessionId ?? event.sessionId;
79
+ if (!sessionKey)
80
+ return;
81
+ clearSession(sessionKey, api, 'new session');
82
+ }, { priority: 190 });
83
+ api.on('session_end', async (event, ctx) => {
84
+ const sessionKey = ctx.sessionId ?? event.sessionId;
85
+ if (!sessionKey)
86
+ return;
87
+ clearSession(sessionKey, api, 'session_end');
88
+ }, { priority: 50 });
89
+ }
90
+ export function resetTodoReminderCounters() {
91
+ sessionCounters.clear();
92
+ }
93
+ export { sessionCounters as _sessionCounters };
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ import { registerContextInjector } from './hooks/context-injector.js';
18
18
  import { registerSessionSync } from './hooks/session-sync.js';
19
19
  import { registerSpawnGuard } from './hooks/spawn-guard.js';
20
20
  import { registerKeywordDetector } from './hooks/keyword-detector/hook.js';
21
+ import { registerTodoReminder, registerAgentEndReminder, registerSessionCleanup } from './hooks/todo-reminder.js';
22
+ import { registerTodoTools } from './tools/todo/index.js';
21
23
  import { registerSetupCli } from './cli/setup.js';
22
24
  /**
23
25
  * Generation counter for multi-registration handling.
@@ -99,6 +101,26 @@ export default function register(api) {
99
101
  registry.hooks.push('spawn-guard');
100
102
  api.logger.info(`[${PLUGIN_ID}] Spawn guard hook registered (before_tool_call)`);
101
103
  });
104
+ safeRegister(api, 'todo-reminder', 'hook', () => {
105
+ registerTodoReminder(guarded);
106
+ registry.hooks.push('todo-reminder');
107
+ api.logger.info(`[${PLUGIN_ID}] Todo reminder hook registered (tool_result_persist)`);
108
+ });
109
+ safeRegister(api, 'agent-end-reminder', 'hook', () => {
110
+ registerAgentEndReminder(api);
111
+ registry.hooks.push('agent-end-reminder');
112
+ api.logger.info(`[${PLUGIN_ID}] Agent-end reminder hook registered (agent_end)`);
113
+ });
114
+ safeRegister(api, 'session-cleanup', 'hook', () => {
115
+ registerSessionCleanup(api);
116
+ registry.hooks.push('session-cleanup');
117
+ api.logger.info(`[${PLUGIN_ID}] Session cleanup hooks registered (session_start, session_end)`);
118
+ });
119
+ safeRegister(api, 'todo-tools', 'tool', () => {
120
+ registerTodoTools(api);
121
+ registry.tools.push('omoc_todo_create', 'omoc_todo_list', 'omoc_todo_update');
122
+ api.logger.info(`[${PLUGIN_ID}] Todo tools registered (3 tools)`);
123
+ });
102
124
  safeRegister(api, 'ralph-loop', 'service', () => {
103
125
  registerRalphLoop(api);
104
126
  registry.services.push('ralph-loop');
@@ -0,0 +1,2 @@
1
+ import { OmocPluginApi } from '../../types.js';
2
+ export declare function registerTodoTools(api: OmocPluginApi): void;
@@ -0,0 +1,8 @@
1
+ import { registerTodoCreateTool } from './todo-create.js';
2
+ import { registerTodoListTool } from './todo-list.js';
3
+ import { registerTodoUpdateTool } from './todo-update.js';
4
+ export function registerTodoTools(api) {
5
+ registerTodoCreateTool(api);
6
+ registerTodoListTool(api);
7
+ registerTodoUpdateTool(api);
8
+ }
@@ -0,0 +1 @@
1
+ export declare function extractSessionKey(options?: unknown): string | undefined;
@@ -0,0 +1,10 @@
1
+ export function extractSessionKey(options) {
2
+ if (typeof options === 'object' && options !== null) {
3
+ const opts = options;
4
+ if (typeof opts.sessionKey === 'string')
5
+ return opts.sessionKey;
6
+ if (typeof opts.sessionId === 'string')
7
+ return opts.sessionId;
8
+ }
9
+ return undefined;
10
+ }
@@ -0,0 +1,23 @@
1
+ export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
2
+ export type TodoPriority = 'high' | 'medium' | 'low';
3
+ export interface TodoItem {
4
+ id: string;
5
+ content: string;
6
+ status: TodoStatus;
7
+ priority: TodoPriority;
8
+ createdAt: string;
9
+ }
10
+ interface SessionStore {
11
+ todos: TodoItem[];
12
+ nextId: number;
13
+ }
14
+ declare const DEFAULT_SESSION = "__default__";
15
+ declare const sessions: Map<string, SessionStore>;
16
+ export declare function createTodo(content: string, priority?: TodoPriority, status?: TodoStatus, sessionKey?: string): TodoItem;
17
+ export declare function listTodos(statusFilter?: TodoStatus, sessionKey?: string): ReadonlyArray<TodoItem>;
18
+ export declare function findTodo(id: string, sessionKey?: string): TodoItem | undefined;
19
+ export declare function updateTodo(id: string, updates: Partial<Pick<TodoItem, 'status' | 'priority' | 'content'>>, sessionKey?: string): TodoItem | null;
20
+ export declare function getIncompleteTodos(sessionKey?: string): ReadonlyArray<TodoItem>;
21
+ export declare function resetStore(sessionKey?: string): void;
22
+ /** Exposed for testing only */
23
+ export { sessions as _sessions, DEFAULT_SESSION };
@@ -0,0 +1,60 @@
1
+ const DEFAULT_SESSION = '__default__';
2
+ const sessions = new Map();
3
+ function getSession(sessionKey) {
4
+ const key = sessionKey ?? DEFAULT_SESSION;
5
+ let store = sessions.get(key);
6
+ if (!store) {
7
+ store = { todos: [], nextId: 1 };
8
+ sessions.set(key, store);
9
+ }
10
+ return store;
11
+ }
12
+ export function createTodo(content, priority = 'medium', status = 'pending', sessionKey) {
13
+ const store = getSession(sessionKey);
14
+ const item = {
15
+ id: `todo-${store.nextId++}`,
16
+ content,
17
+ status,
18
+ priority,
19
+ createdAt: new Date().toISOString(),
20
+ };
21
+ store.todos.push(item);
22
+ return item;
23
+ }
24
+ export function listTodos(statusFilter, sessionKey) {
25
+ const store = getSession(sessionKey);
26
+ if (statusFilter)
27
+ return store.todos.filter((t) => t.status === statusFilter);
28
+ return store.todos;
29
+ }
30
+ export function findTodo(id, sessionKey) {
31
+ const store = getSession(sessionKey);
32
+ return store.todos.find((t) => t.id === id);
33
+ }
34
+ export function updateTodo(id, updates, sessionKey) {
35
+ const store = getSession(sessionKey);
36
+ const item = store.todos.find((t) => t.id === id);
37
+ if (!item)
38
+ return null;
39
+ if (updates.status !== undefined)
40
+ item.status = updates.status;
41
+ if (updates.priority !== undefined)
42
+ item.priority = updates.priority;
43
+ if (updates.content !== undefined)
44
+ item.content = updates.content;
45
+ return item;
46
+ }
47
+ export function getIncompleteTodos(sessionKey) {
48
+ const store = getSession(sessionKey);
49
+ return store.todos.filter((t) => t.status === 'pending' || t.status === 'in_progress');
50
+ }
51
+ export function resetStore(sessionKey) {
52
+ if (sessionKey) {
53
+ sessions.delete(sessionKey);
54
+ }
55
+ else {
56
+ sessions.clear();
57
+ }
58
+ }
59
+ /** Exposed for testing only */
60
+ export { sessions as _sessions, DEFAULT_SESSION };
@@ -0,0 +1,2 @@
1
+ import { OmocPluginApi } from '../../types.js';
2
+ export declare function registerTodoCreateTool(api: OmocPluginApi): void;
@@ -0,0 +1,35 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { toolResponse, toolError } from '../../utils/helpers.js';
3
+ import { TOOL_PREFIX } from '../../constants.js';
4
+ import { createTodo } from './store.js';
5
+ import { extractSessionKey } from './session-key.js';
6
+ const TodoCreateParamsSchema = Type.Object({
7
+ content: Type.String({ description: 'What needs to be done' }),
8
+ priority: Type.Optional(Type.Unsafe({
9
+ type: 'string',
10
+ enum: ['high', 'medium', 'low'],
11
+ description: 'Priority level (default: medium)',
12
+ })),
13
+ status: Type.Optional(Type.Unsafe({
14
+ type: 'string',
15
+ enum: ['pending', 'in_progress', 'completed', 'cancelled'],
16
+ description: 'Initial status (default: pending)',
17
+ })),
18
+ });
19
+ export function registerTodoCreateTool(api) {
20
+ api.registerTool({
21
+ name: `${TOOL_PREFIX}todo_create`,
22
+ description: 'Create a new todo item. Use this to plan work steps before starting. ' +
23
+ 'For multi-step tasks, create ALL todos first, then work through them one at a time.',
24
+ parameters: TodoCreateParamsSchema,
25
+ optional: true,
26
+ execute: async (_callId, params, options) => {
27
+ const content = params.content?.trim();
28
+ if (!content)
29
+ return toolError('content is required');
30
+ const sessionKey = extractSessionKey(options);
31
+ const item = createTodo(content, params.priority, params.status, sessionKey);
32
+ return toolResponse(JSON.stringify({ todo: item }, null, 2));
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,2 @@
1
+ import { OmocPluginApi } from '../../types.js';
2
+ export declare function registerTodoListTool(api: OmocPluginApi): void;
@@ -0,0 +1,26 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { toolResponse } from '../../utils/helpers.js';
3
+ import { TOOL_PREFIX } from '../../constants.js';
4
+ import { listTodos } from './store.js';
5
+ import { extractSessionKey } from './session-key.js';
6
+ const TodoListParamsSchema = Type.Object({
7
+ status: Type.Optional(Type.Unsafe({
8
+ type: 'string',
9
+ enum: ['pending', 'in_progress', 'completed', 'cancelled'],
10
+ description: 'Filter by status (omit for all)',
11
+ })),
12
+ });
13
+ export function registerTodoListTool(api) {
14
+ api.registerTool({
15
+ name: `${TOOL_PREFIX}todo_list`,
16
+ description: 'List todo items. Call this FIRST to check existing todos before starting work. ' +
17
+ 'Shows all active todos by default, or filter by status.',
18
+ parameters: TodoListParamsSchema,
19
+ optional: true,
20
+ execute: async (_callId, params, options) => {
21
+ const sessionKey = extractSessionKey(options);
22
+ const items = listTodos(params.status, sessionKey);
23
+ return toolResponse(JSON.stringify({ todos: items, count: items.length }, null, 2));
24
+ },
25
+ });
26
+ }
@@ -0,0 +1,2 @@
1
+ import { OmocPluginApi } from '../../types.js';
2
+ export declare function registerTodoUpdateTool(api: OmocPluginApi): void;
@@ -0,0 +1,45 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { toolResponse, toolError } from '../../utils/helpers.js';
3
+ import { TOOL_PREFIX } from '../../constants.js';
4
+ import { updateTodo } from './store.js';
5
+ import { extractSessionKey } from './session-key.js';
6
+ const TodoUpdateParamsSchema = Type.Object({
7
+ id: Type.String({ description: 'Todo ID (e.g. todo-1)' }),
8
+ status: Type.Optional(Type.Unsafe({
9
+ type: 'string',
10
+ enum: ['pending', 'in_progress', 'completed', 'cancelled'],
11
+ description: 'New status',
12
+ })),
13
+ priority: Type.Optional(Type.Unsafe({
14
+ type: 'string',
15
+ enum: ['high', 'medium', 'low'],
16
+ description: 'New priority',
17
+ })),
18
+ content: Type.Optional(Type.String({ description: 'Updated content text' })),
19
+ });
20
+ export function registerTodoUpdateTool(api) {
21
+ api.registerTool({
22
+ name: `${TOOL_PREFIX}todo_update`,
23
+ description: 'Update a todo item status, priority, or content. ' +
24
+ 'Mark in_progress before starting a step, completed immediately after finishing.',
25
+ parameters: TodoUpdateParamsSchema,
26
+ optional: true,
27
+ execute: async (_callId, params, options) => {
28
+ const id = params.id?.trim();
29
+ if (!id)
30
+ return toolError('id is required');
31
+ if (!params.status && !params.priority && !params.content) {
32
+ return toolError('At least one of status, priority, or content must be provided');
33
+ }
34
+ const sessionKey = extractSessionKey(options);
35
+ const updated = updateTodo(id, {
36
+ status: params.status,
37
+ priority: params.priority,
38
+ content: params.content,
39
+ }, sessionKey);
40
+ if (!updated)
41
+ return toolResponse(JSON.stringify({ error: 'todo_not_found', id }));
42
+ return toolResponse(JSON.stringify({ todo: updated }, null, 2));
43
+ },
44
+ });
45
+ }
package/dist/types.d.ts CHANGED
@@ -85,6 +85,14 @@ export interface OmocPluginApi {
85
85
  warn: (...args: unknown[]) => void;
86
86
  error: (...args: unknown[]) => void;
87
87
  };
88
+ runtime: {
89
+ system: {
90
+ enqueueSystemEvent: (text: string, options: {
91
+ sessionKey: string;
92
+ contextKey?: string | null;
93
+ }) => void;
94
+ };
95
+ };
88
96
  registerHook: <TEvent>(event: string, handler: (event: TEvent) => TEvent | void | undefined, meta?: HookMeta) => void;
89
97
  registerTool: <TParams>(config: ToolRegistration<TParams>) => void;
90
98
  registerCommand: <TCtx = {
@@ -2,7 +2,7 @@ import type { OmocPluginApi } from '../types.js';
2
2
  export declare function initPersonaState(_api: OmocPluginApi): Promise<void>;
3
3
  export declare function setActivePersonaId(id: string | null): Promise<void>;
4
4
  export declare function setActivePersona(id: string | null): Promise<void>;
5
- export declare function getActivePersona(workspaceDir?: string): Promise<string | null>;
5
+ export declare function getActivePersona(workspaceDir?: string, agentId?: string): Promise<string | null>;
6
6
  export declare function resetPersonaState(): Promise<void>;
7
7
  export declare const OFF_MARKER = "__OFF__";
8
8
  export declare function resolveAgentsMdPath(workspaceDir?: string): string;
@@ -1,6 +1,8 @@
1
1
  import { readFile, writeFile, mkdir } from 'fs/promises';
2
2
  import { dirname, join } from 'path';
3
3
  import { resolveOpenClawWorkspaceDir } from './paths.js';
4
+ import { ALL_AGENT_IDS } from '../agents/agent-ids.js';
5
+ const KNOWN_AGENT_IDS = new Set(ALL_AGENT_IDS);
4
6
  let activePersonaId = null;
5
7
  let loaded = false;
6
8
  function resolveStateDir(workspaceDir) {
@@ -26,10 +28,16 @@ export async function setActivePersonaId(id) {
26
28
  export async function setActivePersona(id) {
27
29
  await setActivePersonaId(id);
28
30
  }
29
- export async function getActivePersona(workspaceDir) {
31
+ export async function getActivePersona(workspaceDir, agentId) {
30
32
  if (!loaded)
31
33
  await loadFromDisk(workspaceDir);
32
- return activePersonaId;
34
+ if (activePersonaId)
35
+ return activePersonaId;
36
+ if (agentId && KNOWN_AGENT_IDS.has(agentId)) {
37
+ await setActivePersonaId(agentId);
38
+ return agentId;
39
+ }
40
+ return null;
33
41
  }
34
42
  export async function resetPersonaState() {
35
43
  activePersonaId = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happycastle/oh-my-openclaw",
3
- "version": "0.14.2",
3
+ "version": "0.15.1",
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",