@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 +11 -8
- package/agents/frontend.md +6 -0
- package/agents/hephaestus.md +13 -17
- package/agents/sisyphus-junior.md +10 -8
- package/dist/__tests__/helpers/mock-factory.js +5 -0
- package/dist/agents/agent-ids.d.ts +1 -1
- package/dist/agents/agent-ids.js +2 -2
- package/dist/cli/mcporter-setup.d.ts +47 -0
- package/dist/cli/mcporter-setup.js +118 -0
- package/dist/cli/model-presets.d.ts +1 -1
- package/dist/cli/model-presets.js +7 -4
- package/dist/cli/setup.d.ts +22 -2
- package/dist/cli/setup.js +102 -10
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/hooks/session-sync.js +1 -1
- package/dist/hooks/spawn-guard.js +1 -1
- package/dist/hooks/todo-reminder.d.ts +7 -0
- package/dist/hooks/todo-reminder.js +93 -0
- package/dist/index.js +22 -0
- package/dist/tools/todo/index.d.ts +2 -0
- package/dist/tools/todo/index.js +8 -0
- package/dist/tools/todo/session-key.d.ts +1 -0
- package/dist/tools/todo/session-key.js +10 -0
- package/dist/tools/todo/store.d.ts +23 -0
- package/dist/tools/todo/store.js +60 -0
- package/dist/tools/todo/todo-create.d.ts +2 -0
- package/dist/tools/todo/todo-create.js +35 -0
- package/dist/tools/todo/todo-list.d.ts +2 -0
- package/dist/tools/todo/todo-list.js +26 -0
- package/dist/tools/todo/todo-update.d.ts +2 -0
- package/dist/tools/todo/todo-update.js +45 -0
- package/dist/types.d.ts +8 -0
- package/dist/utils/persona-state.d.ts +1 -1
- package/dist/utils/persona-state.js +10 -2
- package/package.json +1 -1
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
|
|
package/agents/frontend.md
CHANGED
|
@@ -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
|
package/agents/hephaestus.md
CHANGED
|
@@ -214,37 +214,33 @@ STOP searching when:
|
|
|
214
214
|
|
|
215
215
|
---
|
|
216
216
|
|
|
217
|
-
##
|
|
217
|
+
## Task Setup (NON-NEGOTIABLE)
|
|
218
218
|
|
|
219
|
-
**
|
|
219
|
+
**BEFORE ANY WORK, set up task tracking. This is your execution backbone.**
|
|
220
220
|
|
|
221
|
-
###
|
|
221
|
+
### First Action on Every Task (MANDATORY)
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
- **
|
|
234
|
+
- **Visibility**: `agent_end` warns about incomplete todos
|
|
239
235
|
|
|
240
236
|
### Anti-Patterns (BLOCKING)
|
|
241
237
|
|
|
242
|
-
- **Skipping
|
|
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
|
|
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
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
19
|
-
</
|
|
20
|
+
No todo setup on multi-step work = INCOMPLETE WORK.
|
|
21
|
+
</Task_Setup>
|
|
20
22
|
|
|
21
23
|
<Verification>
|
|
22
24
|
Task NOT complete without:
|
|
@@ -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, '
|
|
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[];
|
package/dist/agents/agent-ids.js
CHANGED
|
@@ -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: '
|
|
33
|
-
omoc_atlas: '
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = ['
|
|
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) {
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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('
|
|
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('
|
|
183
|
-
logger.info('
|
|
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/
|
|
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(`
|
|
222
|
+
logger.info(` Selected: ${PROVIDER_LABELS[provider] ?? 'Custom'}`);
|
|
213
223
|
logger.info('');
|
|
214
|
-
|
|
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
|
|
232
|
+
return emptyResult;
|
|
222
233
|
}
|
|
234
|
+
// Step 3/4: MCP servers
|
|
223
235
|
logger.info('');
|
|
224
|
-
|
|
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('');
|
package/dist/constants.d.ts
CHANGED
|
@@ -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);
|
|
@@ -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,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,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,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,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
|
-
|
|
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