@happycastle/oh-my-openclaw 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +15 -4
- package/dist/commands/status-commands.d.ts +2 -0
- package/dist/commands/status-commands.js +45 -0
- package/dist/commands/workflow-commands.js +9 -9
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +11 -0
- package/dist/hooks/message-monitor.d.ts +1 -1
- package/dist/hooks/message-monitor.js +24 -7
- package/dist/hooks/startup.d.ts +2 -0
- package/dist/hooks/startup.js +11 -0
- package/dist/hooks/todo-enforcer.d.ts +6 -0
- package/dist/hooks/todo-enforcer.js +36 -5
- package/dist/index.js +41 -6
- package/dist/services/ralph-loop.js +5 -14
- package/dist/tools/checkpoint.js +24 -22
- package/dist/tools/look-at.js +22 -6
- package/dist/tools/task-delegation.js +54 -19
- package/dist/types.d.ts +50 -8
- package/dist/utils/config.js +15 -4
- package/dist/utils/state.d.ts +9 -1
- package/dist/utils/state.js +9 -3
- package/dist/utils/validation.d.ts +2 -1
- package/dist/utils/validation.js +2 -12
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/openclaw.plugin.json +80 -10
- package/package.json +5 -4
- package/skills/delegation-prompt.md +110 -31
- package/skills/gemini-look-at.md +2 -2
- package/skills/web-search.md +155 -0
- package/skills/workflow-auto-rescue.md +1 -0
- package/skills/workflow-tool-patterns.md +1 -0
package/dist/cli.js
CHANGED
|
@@ -56,6 +56,7 @@ function commandExists(cmd) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
function run(cmd, opts) {
|
|
59
|
+
const throwOnError = opts?.throwOnError ?? false;
|
|
59
60
|
try {
|
|
60
61
|
return execSync(cmd, {
|
|
61
62
|
cwd: opts?.cwd,
|
|
@@ -64,8 +65,11 @@ function run(cmd, opts) {
|
|
|
64
65
|
})?.toString().trim() ?? '';
|
|
65
66
|
}
|
|
66
67
|
catch (e) {
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
if (!throwOnError) {
|
|
69
|
+
const err = e;
|
|
70
|
+
return err.stdout?.toString().trim() ?? '';
|
|
71
|
+
}
|
|
72
|
+
throw e;
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
function isSymlink(p) {
|
|
@@ -145,10 +149,17 @@ function install(installDir) {
|
|
|
145
149
|
const pluginDir = join(installDir, 'plugin');
|
|
146
150
|
if (existsSync(join(pluginDir, 'package.json'))) {
|
|
147
151
|
info('Installing dependencies...');
|
|
148
|
-
run('npm install', { cwd: pluginDir });
|
|
152
|
+
run('npm install', { cwd: pluginDir, throwOnError: true });
|
|
149
153
|
ok('Dependencies installed');
|
|
150
154
|
info('Building TypeScript...');
|
|
151
|
-
|
|
155
|
+
try {
|
|
156
|
+
run('npm run build', { cwd: pluginDir, throwOnError: true });
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
160
|
+
fail(`Plugin build failed: ${message}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
152
163
|
ok('Plugin built');
|
|
153
164
|
// Run tests silently to verify
|
|
154
165
|
info('Running tests...');
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PLUGIN_ID } from '../types.js';
|
|
2
|
+
import { VERSION } from '../version.js';
|
|
3
|
+
import { getConfig } from '../utils/config.js';
|
|
4
|
+
import { getStatus as getRalphStatus } from '../services/ralph-loop.js';
|
|
5
|
+
import { getMessageCount } from '../hooks/message-monitor.js';
|
|
6
|
+
export function registerStatusCommands(api) {
|
|
7
|
+
api.registerCommand({
|
|
8
|
+
name: 'omoc-health',
|
|
9
|
+
description: 'Plugin health check (auto-reply, no AI invocation)',
|
|
10
|
+
handler: async () => {
|
|
11
|
+
const config = getConfig(api);
|
|
12
|
+
const ralphState = await getRalphStatus();
|
|
13
|
+
const messageCount = getMessageCount();
|
|
14
|
+
return {
|
|
15
|
+
text: [
|
|
16
|
+
`# ${PLUGIN_ID} Health`,
|
|
17
|
+
`- Version: ${VERSION}`,
|
|
18
|
+
`- Ralph Loop: ${ralphState.active ? 'ACTIVE' : 'INACTIVE'}`,
|
|
19
|
+
`- Todo Enforcer: ${config.todo_enforcer_enabled ? 'ON' : 'OFF'}`,
|
|
20
|
+
`- Comment Checker: ${config.comment_checker_enabled ? 'ON' : 'OFF'}`,
|
|
21
|
+
`- Messages Tracked: ${messageCount}`,
|
|
22
|
+
].join('\n'),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
api.registerCommand({
|
|
27
|
+
name: 'omoc-config',
|
|
28
|
+
description: 'Show current plugin configuration (auto-reply)',
|
|
29
|
+
handler: () => {
|
|
30
|
+
const config = getConfig(api);
|
|
31
|
+
const safeConfig = {};
|
|
32
|
+
for (const [key, value] of Object.entries(config)) {
|
|
33
|
+
if (typeof value === 'string' && (key.includes('token') || key.includes('secret') || key.includes('key'))) {
|
|
34
|
+
safeConfig[key] = '***';
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
safeConfig[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
text: `# ${PLUGIN_ID} Configuration\n\`\`\`json\n${JSON.stringify(safeConfig, null, 2)}\n\`\`\``,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
const __filename = fileURLToPath(import.meta.url);
|
|
5
5
|
const __dirname = dirname(__filename);
|
|
6
6
|
// From dist/commands/ → plugin root is ../../
|
|
7
7
|
const PLUGIN_ROOT = join(__dirname, '..', '..');
|
|
8
|
-
function readWorkflow(workflowName) {
|
|
8
|
+
async function readWorkflow(workflowName) {
|
|
9
9
|
try {
|
|
10
10
|
const workflowPath = join(PLUGIN_ROOT, 'workflows', `${workflowName}.md`);
|
|
11
|
-
return
|
|
11
|
+
return await fs.readFile(workflowPath, 'utf-8');
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
14
|
return `Error: Could not read workflow file 'workflows/${workflowName}.md'. Plugin root: ${PLUGIN_ROOT}`;
|
|
@@ -18,9 +18,9 @@ export function registerWorkflowCommands(api) {
|
|
|
18
18
|
api.registerCommand({
|
|
19
19
|
name: 'ultrawork',
|
|
20
20
|
description: 'Full planning \u2192 execution \u2192 verification workflow',
|
|
21
|
-
handler: (ctx) => {
|
|
21
|
+
handler: async (ctx) => {
|
|
22
22
|
const taskDescription = ctx.args || 'No task specified';
|
|
23
|
-
const workflow = readWorkflow('ultrawork');
|
|
23
|
+
const workflow = await readWorkflow('ultrawork');
|
|
24
24
|
return {
|
|
25
25
|
text: `# Ultrawork Mode\n\n**Task**: ${taskDescription}\n\n---\n\n${workflow}`,
|
|
26
26
|
};
|
|
@@ -29,9 +29,9 @@ export function registerWorkflowCommands(api) {
|
|
|
29
29
|
api.registerCommand({
|
|
30
30
|
name: 'plan',
|
|
31
31
|
description: 'Create a structured execution plan',
|
|
32
|
-
handler: (ctx) => {
|
|
32
|
+
handler: async (ctx) => {
|
|
33
33
|
const topic = ctx.args || 'No topic specified';
|
|
34
|
-
const workflow = readWorkflow('plan');
|
|
34
|
+
const workflow = await readWorkflow('plan');
|
|
35
35
|
return {
|
|
36
36
|
text: `# Planning Mode\n\n**Topic**: ${topic}\n\n---\n\n${workflow}`,
|
|
37
37
|
};
|
|
@@ -40,9 +40,9 @@ export function registerWorkflowCommands(api) {
|
|
|
40
40
|
api.registerCommand({
|
|
41
41
|
name: 'start-work',
|
|
42
42
|
description: 'Execute an approved plan',
|
|
43
|
-
handler: (ctx) => {
|
|
43
|
+
handler: async (ctx) => {
|
|
44
44
|
const planPath = ctx.args || 'most recent plan';
|
|
45
|
-
const workflow = readWorkflow('start-work');
|
|
45
|
+
const workflow = await readWorkflow('start-work');
|
|
46
46
|
return {
|
|
47
47
|
text: `# Start Work Mode\n\n**Plan**: ${planPath}\n\n---\n\n${workflow}`,
|
|
48
48
|
};
|
|
@@ -8,4 +8,4 @@ export declare function registerMessageMonitor(api: OmocPluginApi): void;
|
|
|
8
8
|
* Returns the current message count
|
|
9
9
|
* Useful for status reporting
|
|
10
10
|
*/
|
|
11
|
-
export declare function getMessageCount(): number;
|
|
11
|
+
export declare function getMessageCount(channelId?: string): number;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
let messageCount = 0;
|
|
1
|
+
const messageCounts = new Map();
|
|
3
2
|
/**
|
|
4
3
|
* Registers the message monitor hook
|
|
5
4
|
* Logs message events for audit purposes without modifying messages
|
|
@@ -11,26 +10,44 @@ export function registerMessageMonitor(api) {
|
|
|
11
10
|
const preview = content.substring(0, 100);
|
|
12
11
|
const channelId = context?.channelId || 'unknown';
|
|
13
12
|
const timestamp = new Date().toISOString();
|
|
13
|
+
const currentCount = messageCounts.get(channelId) ?? 0;
|
|
14
|
+
const nextCount = currentCount + 1;
|
|
15
|
+
messageCounts.set(channelId, nextCount);
|
|
14
16
|
// Log the message event
|
|
15
17
|
api.logger.info('[omoc] Message sent:', {
|
|
16
18
|
preview,
|
|
17
19
|
channelId,
|
|
18
20
|
timestamp,
|
|
19
|
-
messageCount:
|
|
21
|
+
messageCount: nextCount
|
|
20
22
|
});
|
|
21
|
-
// Increment message counter
|
|
22
|
-
messageCount++;
|
|
23
23
|
// Return undefined to not modify the message
|
|
24
24
|
return undefined;
|
|
25
25
|
}, {
|
|
26
26
|
name: 'oh-my-openclaw.message-monitor',
|
|
27
27
|
description: 'Monitors message events for audit logging'
|
|
28
28
|
});
|
|
29
|
+
api.registerHook('message:received', (context) => {
|
|
30
|
+
const content = context?.content || '';
|
|
31
|
+
const preview = content.substring(0, 100);
|
|
32
|
+
const channelId = context?.channelId || 'unknown';
|
|
33
|
+
api.logger.info('[omoc] Message received:', { preview, channelId });
|
|
34
|
+
return undefined;
|
|
35
|
+
}, {
|
|
36
|
+
name: 'oh-my-openclaw.message-received-monitor',
|
|
37
|
+
description: 'Monitors inbound message events for audit logging'
|
|
38
|
+
});
|
|
29
39
|
}
|
|
30
40
|
/**
|
|
31
41
|
* Returns the current message count
|
|
32
42
|
* Useful for status reporting
|
|
33
43
|
*/
|
|
34
|
-
export function getMessageCount() {
|
|
35
|
-
|
|
44
|
+
export function getMessageCount(channelId) {
|
|
45
|
+
if (channelId) {
|
|
46
|
+
return messageCounts.get(channelId) ?? 0;
|
|
47
|
+
}
|
|
48
|
+
let total = 0;
|
|
49
|
+
for (const count of messageCounts.values()) {
|
|
50
|
+
total += count;
|
|
51
|
+
}
|
|
52
|
+
return total;
|
|
36
53
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PLUGIN_ID } from '../types.js';
|
|
2
|
+
import { VERSION } from '../version.js';
|
|
3
|
+
export function registerStartupHook(api) {
|
|
4
|
+
api.registerHook('gateway:startup', () => {
|
|
5
|
+
api.logger.info(`[${PLUGIN_ID}] Gateway started — plugin v${VERSION} active`);
|
|
6
|
+
return undefined;
|
|
7
|
+
}, {
|
|
8
|
+
name: 'oh-my-openclaw.gateway-startup',
|
|
9
|
+
description: 'Logs plugin activation on gateway startup'
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import { OmocPluginApi } from '../types.js';
|
|
2
|
+
export declare function resetEnforcerState(): void;
|
|
3
|
+
export declare function getEnforcerState(): {
|
|
4
|
+
lastInjectionTime: number;
|
|
5
|
+
consecutiveFailures: number;
|
|
6
|
+
disabledByFailures: boolean;
|
|
7
|
+
};
|
|
2
8
|
export declare function registerTodoEnforcer(api: OmocPluginApi): void;
|
|
@@ -5,20 +5,51 @@ You MUST continue working on incomplete todos.
|
|
|
5
5
|
- Do NOT ask for permission to continue
|
|
6
6
|
- Mark each task complete immediately when finished
|
|
7
7
|
- If blocked, document the blocker and move to next task`;
|
|
8
|
+
let lastInjectionTime = 0;
|
|
9
|
+
let consecutiveFailures = 0;
|
|
10
|
+
let disabledByFailures = false;
|
|
11
|
+
export function resetEnforcerState() {
|
|
12
|
+
lastInjectionTime = 0;
|
|
13
|
+
consecutiveFailures = 0;
|
|
14
|
+
disabledByFailures = false;
|
|
15
|
+
}
|
|
16
|
+
export function getEnforcerState() {
|
|
17
|
+
return { lastInjectionTime, consecutiveFailures, disabledByFailures };
|
|
18
|
+
}
|
|
8
19
|
export function registerTodoEnforcer(api) {
|
|
9
20
|
api.registerHook('agent:bootstrap', (event) => {
|
|
10
21
|
const config = getConfig(api);
|
|
11
22
|
if (!config.todo_enforcer_enabled) {
|
|
12
23
|
return;
|
|
13
24
|
}
|
|
25
|
+
if (disabledByFailures) {
|
|
26
|
+
api.logger.warn('[omoc] Todo enforcer disabled due to consecutive failures');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
if (config.todo_enforcer_cooldown_ms > 0 && (now - lastInjectionTime) < config.todo_enforcer_cooldown_ms) {
|
|
31
|
+
api.logger.info('[omoc] Todo enforcer skipped (cooldown)');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
14
34
|
if (!event.context.bootstrapFiles) {
|
|
15
35
|
event.context.bootstrapFiles = [];
|
|
16
36
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
try {
|
|
38
|
+
event.context.bootstrapFiles.push({
|
|
39
|
+
path: 'omoc://todo-enforcer',
|
|
40
|
+
content: DIRECTIVE_TEXT,
|
|
41
|
+
});
|
|
42
|
+
lastInjectionTime = now;
|
|
43
|
+
consecutiveFailures = 0;
|
|
44
|
+
api.logger.info('[omoc] Todo enforcer directive injected');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
consecutiveFailures++;
|
|
48
|
+
if (config.todo_enforcer_max_failures > 0 && consecutiveFailures >= config.todo_enforcer_max_failures) {
|
|
49
|
+
disabledByFailures = true;
|
|
50
|
+
api.logger.error(`[omoc] Todo enforcer disabled after ${consecutiveFailures} consecutive failures`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
22
53
|
}, {
|
|
23
54
|
name: 'oh-my-openclaw.todo-enforcer',
|
|
24
55
|
description: 'Injects TODO continuation directive into agent bootstrap',
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import { PLUGIN_ID } from './types.js';
|
|
2
|
+
import { VERSION } from './version.js';
|
|
2
3
|
import { getConfig } from './utils/config.js';
|
|
3
4
|
import { registerTodoEnforcer } from './hooks/todo-enforcer.js';
|
|
4
5
|
import { registerCommentChecker } from './hooks/comment-checker.js';
|
|
5
6
|
import { registerMessageMonitor } from './hooks/message-monitor.js';
|
|
7
|
+
import { registerStartupHook } from './hooks/startup.js';
|
|
6
8
|
import { registerRalphLoop } from './services/ralph-loop.js';
|
|
7
9
|
import { registerDelegateTool } from './tools/task-delegation.js';
|
|
8
10
|
import { registerLookAtTool } from './tools/look-at.js';
|
|
9
11
|
import { registerCheckpointTool } from './tools/checkpoint.js';
|
|
10
12
|
import { registerWorkflowCommands } from './commands/workflow-commands.js';
|
|
11
13
|
import { registerRalphCommands } from './commands/ralph-commands.js';
|
|
14
|
+
import { registerStatusCommands } from './commands/status-commands.js';
|
|
15
|
+
/** Registry of successfully registered components */
|
|
16
|
+
const registry = {
|
|
17
|
+
hooks: [],
|
|
18
|
+
services: [],
|
|
19
|
+
tools: [],
|
|
20
|
+
commands: [],
|
|
21
|
+
};
|
|
12
22
|
export default function register(api) {
|
|
13
23
|
const config = getConfig(api);
|
|
14
|
-
api.logger.info(`[${PLUGIN_ID}] Initializing plugin
|
|
24
|
+
api.logger.info(`[${PLUGIN_ID}] Initializing plugin v${VERSION}`);
|
|
15
25
|
try {
|
|
16
26
|
registerTodoEnforcer(api);
|
|
27
|
+
registry.hooks.push('todo-enforcer');
|
|
17
28
|
api.logger.info(`[${PLUGIN_ID}] Todo Enforcer hook registered (enabled: ${config.todo_enforcer_enabled})`);
|
|
18
29
|
}
|
|
19
30
|
catch (err) {
|
|
@@ -21,6 +32,7 @@ export default function register(api) {
|
|
|
21
32
|
}
|
|
22
33
|
try {
|
|
23
34
|
registerCommentChecker(api);
|
|
35
|
+
registry.hooks.push('comment-checker');
|
|
24
36
|
api.logger.info(`[${PLUGIN_ID}] Comment Checker hook registered (enabled: ${config.comment_checker_enabled})`);
|
|
25
37
|
}
|
|
26
38
|
catch (err) {
|
|
@@ -28,13 +40,23 @@ export default function register(api) {
|
|
|
28
40
|
}
|
|
29
41
|
try {
|
|
30
42
|
registerMessageMonitor(api);
|
|
43
|
+
registry.hooks.push('message-monitor', 'message-received-monitor');
|
|
31
44
|
api.logger.info(`[${PLUGIN_ID}] Message Monitor hook registered`);
|
|
32
45
|
}
|
|
33
46
|
catch (err) {
|
|
34
47
|
api.logger.error(`[${PLUGIN_ID}] Failed to register Message Monitor:`, err);
|
|
35
48
|
}
|
|
49
|
+
try {
|
|
50
|
+
registerStartupHook(api);
|
|
51
|
+
registry.hooks.push('gateway-startup');
|
|
52
|
+
api.logger.info(`[${PLUGIN_ID}] Gateway startup hook registered`);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register startup hook:`, err);
|
|
56
|
+
}
|
|
36
57
|
try {
|
|
37
58
|
registerRalphLoop(api);
|
|
59
|
+
registry.services.push('ralph-loop');
|
|
38
60
|
api.logger.info(`[${PLUGIN_ID}] Ralph Loop service registered`);
|
|
39
61
|
}
|
|
40
62
|
catch (err) {
|
|
@@ -42,6 +64,7 @@ export default function register(api) {
|
|
|
42
64
|
}
|
|
43
65
|
try {
|
|
44
66
|
registerDelegateTool(api);
|
|
67
|
+
registry.tools.push('omoc_delegate');
|
|
45
68
|
api.logger.info(`[${PLUGIN_ID}] Delegate tool registered`);
|
|
46
69
|
}
|
|
47
70
|
catch (err) {
|
|
@@ -49,6 +72,7 @@ export default function register(api) {
|
|
|
49
72
|
}
|
|
50
73
|
try {
|
|
51
74
|
registerLookAtTool(api);
|
|
75
|
+
registry.tools.push('omoc_look_at');
|
|
52
76
|
api.logger.info(`[${PLUGIN_ID}] Look-At tool registered`);
|
|
53
77
|
}
|
|
54
78
|
catch (err) {
|
|
@@ -56,6 +80,7 @@ export default function register(api) {
|
|
|
56
80
|
}
|
|
57
81
|
try {
|
|
58
82
|
registerCheckpointTool(api);
|
|
83
|
+
registry.tools.push('omoc_checkpoint');
|
|
59
84
|
api.logger.info(`[${PLUGIN_ID}] Checkpoint tool registered`);
|
|
60
85
|
}
|
|
61
86
|
catch (err) {
|
|
@@ -63,6 +88,7 @@ export default function register(api) {
|
|
|
63
88
|
}
|
|
64
89
|
try {
|
|
65
90
|
registerWorkflowCommands(api);
|
|
91
|
+
registry.commands.push('ultrawork', 'plan', 'start-work');
|
|
66
92
|
api.logger.info(`[${PLUGIN_ID}] Workflow commands registered (ultrawork, plan, start-work)`);
|
|
67
93
|
}
|
|
68
94
|
catch (err) {
|
|
@@ -70,20 +96,29 @@ export default function register(api) {
|
|
|
70
96
|
}
|
|
71
97
|
try {
|
|
72
98
|
registerRalphCommands(api);
|
|
99
|
+
registry.commands.push('ralph-loop', 'ralph-stop', 'omoc-status');
|
|
73
100
|
api.logger.info(`[${PLUGIN_ID}] Ralph commands registered (ralph-loop, ralph-stop, omoc-status)`);
|
|
74
101
|
}
|
|
75
102
|
catch (err) {
|
|
76
103
|
api.logger.error(`[${PLUGIN_ID}] Failed to register Ralph commands:`, err);
|
|
77
104
|
}
|
|
105
|
+
try {
|
|
106
|
+
registerStatusCommands(api);
|
|
107
|
+
registry.commands.push('omoc-health', 'omoc-config');
|
|
108
|
+
api.logger.info(`[${PLUGIN_ID}] Status commands registered (omoc-health, omoc-config)`);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
api.logger.error(`[${PLUGIN_ID}] Failed to register Status commands:`, err);
|
|
112
|
+
}
|
|
78
113
|
api.registerGatewayMethod('oh-my-openclaw.status', () => {
|
|
79
114
|
return {
|
|
80
115
|
ok: true,
|
|
81
116
|
plugin: PLUGIN_ID,
|
|
82
|
-
version:
|
|
83
|
-
hooks: [
|
|
84
|
-
services: [
|
|
85
|
-
tools: [
|
|
86
|
-
commands: [
|
|
117
|
+
version: VERSION,
|
|
118
|
+
hooks: [...registry.hooks],
|
|
119
|
+
services: [...registry.services],
|
|
120
|
+
tools: [...registry.tools],
|
|
121
|
+
commands: [...registry.commands],
|
|
87
122
|
config: {
|
|
88
123
|
todo_enforcer_enabled: config.todo_enforcer_enabled,
|
|
89
124
|
comment_checker_enabled: config.comment_checker_enabled,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
1
|
import { getConfig } from '../utils/config.js';
|
|
3
2
|
import { join } from 'path';
|
|
4
3
|
import { ABSOLUTE_MAX_RALPH_ITERATIONS, PLUGIN_ID, } from '../types.js';
|
|
@@ -25,23 +24,15 @@ async function loadStateFromFile() {
|
|
|
25
24
|
currentState = { ...DEFAULT_STATE };
|
|
26
25
|
return;
|
|
27
26
|
}
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
currentState =
|
|
27
|
+
const result = await readState(stateFilePath);
|
|
28
|
+
if (result.ok) {
|
|
29
|
+
currentState = result.data;
|
|
31
30
|
return;
|
|
32
31
|
}
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
await fs.access(stateFilePath);
|
|
36
|
-
hasExistingFile = true;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
hasExistingFile = false;
|
|
40
|
-
}
|
|
41
|
-
currentState = { ...DEFAULT_STATE };
|
|
42
|
-
if (hasExistingFile) {
|
|
32
|
+
if (result.error === 'corrupted') {
|
|
43
33
|
getApi().logger.warn(`[${PLUGIN_ID}] Ralph Loop state was corrupted; recovering with default state`);
|
|
44
34
|
}
|
|
35
|
+
currentState = { ...DEFAULT_STATE };
|
|
45
36
|
}
|
|
46
37
|
async function saveStateToFile() {
|
|
47
38
|
if (!stateFilePath) {
|
package/dist/tools/checkpoint.js
CHANGED
|
@@ -3,37 +3,39 @@ import { promises as fs } from 'fs';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { readState, writeState, ensureDir } from '../utils/state.js';
|
|
5
5
|
import { getConfig } from '../utils/config.js';
|
|
6
|
+
const CheckpointParamsSchema = Type.Object({
|
|
7
|
+
action: Type.Union([
|
|
8
|
+
Type.Literal('save'),
|
|
9
|
+
Type.Literal('load'),
|
|
10
|
+
Type.Literal('list'),
|
|
11
|
+
], { description: 'Checkpoint operation' }),
|
|
12
|
+
task: Type.Optional(Type.String({ description: 'Current task name (for save)' })),
|
|
13
|
+
step: Type.Optional(Type.String({ description: 'Current step name (for save)' })),
|
|
14
|
+
changed_files: Type.Optional(Type.Array(Type.String(), { description: 'Files modified since last checkpoint' })),
|
|
15
|
+
next_action: Type.Optional(Type.String({ description: 'What to do after restore' })),
|
|
16
|
+
});
|
|
6
17
|
export function registerCheckpointTool(api) {
|
|
7
18
|
api.registerTool({
|
|
8
19
|
name: 'omoc_checkpoint',
|
|
9
20
|
description: 'Save, load, or list session checkpoints for crash recovery',
|
|
10
|
-
parameters:
|
|
11
|
-
action: Type.Unsafe({
|
|
12
|
-
type: 'string',
|
|
13
|
-
enum: ['save', 'load', 'list'],
|
|
14
|
-
description: 'Checkpoint operation',
|
|
15
|
-
}),
|
|
16
|
-
task: Type.Optional(Type.String({ description: 'Current task name (for save)' })),
|
|
17
|
-
step: Type.Optional(Type.String({ description: 'Current step name (for save)' })),
|
|
18
|
-
changed_files: Type.Optional(Type.Array(Type.String(), { description: 'Files modified since last checkpoint' })),
|
|
19
|
-
next_action: Type.Optional(Type.String({ description: 'What to do after restore' })),
|
|
20
|
-
}),
|
|
21
|
+
parameters: CheckpointParamsSchema,
|
|
21
22
|
execute: async (params) => {
|
|
22
23
|
const config = getConfig(api);
|
|
23
24
|
const checkpointDir = config.checkpoint_dir;
|
|
24
25
|
await ensureDir(checkpointDir);
|
|
25
26
|
if (params.action === 'save') {
|
|
27
|
+
const now = Date.now();
|
|
26
28
|
const checkpoint = {
|
|
27
29
|
type: 'session-checkpoint',
|
|
28
|
-
session_id: `checkpoint-${
|
|
30
|
+
session_id: `checkpoint-${now}`,
|
|
29
31
|
task: params.task || 'unknown',
|
|
30
32
|
step: params.step || 'unknown',
|
|
31
33
|
changed_files: params.changed_files || [],
|
|
32
34
|
verification: { diagnostics: 'not-run', tests: 'not-run', build: 'not-run' },
|
|
33
35
|
next_action: params.next_action || '',
|
|
34
|
-
timestamp: new Date().toISOString(),
|
|
36
|
+
timestamp: new Date(now).toISOString(),
|
|
35
37
|
};
|
|
36
|
-
const filename = `checkpoint-${
|
|
38
|
+
const filename = `checkpoint-${now}.json`;
|
|
37
39
|
await writeState(join(checkpointDir, filename), checkpoint);
|
|
38
40
|
return {
|
|
39
41
|
content: [
|
|
@@ -54,17 +56,17 @@ export function registerCheckpointTool(api) {
|
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
58
|
const mostRecent = jsonFiles[0];
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
59
|
+
const result = await readState(join(checkpointDir, mostRecent));
|
|
60
|
+
if (!result.ok) {
|
|
59
61
|
return {
|
|
60
|
-
content: [{ type: 'text', text: `Failed to load checkpoint
|
|
62
|
+
content: [{ type: 'text', text: `Failed to load checkpoint ${mostRecent}: ${result.message}` }],
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
return {
|
|
64
66
|
content: [
|
|
65
67
|
{
|
|
66
68
|
type: 'text',
|
|
67
|
-
text: JSON.stringify({ loaded: mostRecent, checkpoint }, null, 2),
|
|
69
|
+
text: JSON.stringify({ loaded: mostRecent, checkpoint: result.data }, null, 2),
|
|
68
70
|
},
|
|
69
71
|
],
|
|
70
72
|
};
|
|
@@ -90,12 +92,12 @@ export function registerCheckpointTool(api) {
|
|
|
90
92
|
};
|
|
91
93
|
}
|
|
92
94
|
const checkpoints = await Promise.all(jsonFiles.map(async (file) => {
|
|
93
|
-
const
|
|
95
|
+
const result = await readState(join(checkpointDir, file));
|
|
94
96
|
return {
|
|
95
97
|
file,
|
|
96
|
-
timestamp:
|
|
97
|
-
task:
|
|
98
|
-
step:
|
|
98
|
+
timestamp: result.ok ? result.data.timestamp : 'unknown',
|
|
99
|
+
task: result.ok ? result.data.task : 'unknown',
|
|
100
|
+
step: result.ok ? result.data.step : 'unknown',
|
|
99
101
|
};
|
|
100
102
|
}));
|
|
101
103
|
return {
|
package/dist/tools/look-at.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
3
4
|
import { promises as fs } from 'fs';
|
|
4
5
|
import { TOOL_PREFIX } from '../types.js';
|
|
5
|
-
|
|
6
|
+
import { getConfig } from '../utils/config.js';
|
|
6
7
|
const TMUX_SESSION_TARGET = 'gemini:0.0';
|
|
7
8
|
const GEMINI_TIMEOUT_MS = 60_000;
|
|
9
|
+
let isRunning = false;
|
|
8
10
|
function escapeShellArg(arg) {
|
|
9
11
|
return arg.replace(/'/g, "'\\''");
|
|
10
12
|
}
|
|
@@ -21,14 +23,19 @@ export function registerLookAtTool(api) {
|
|
|
21
23
|
})),
|
|
22
24
|
}),
|
|
23
25
|
execute: async (params) => {
|
|
24
|
-
|
|
26
|
+
if (isRunning) {
|
|
27
|
+
return { content: [{ type: 'text', text: 'Error: Another look_at operation is in progress' }] };
|
|
28
|
+
}
|
|
29
|
+
isRunning = true;
|
|
30
|
+
const tempFile = `/tmp/omoc-look-at-${randomUUID()}.md`;
|
|
31
|
+
const tmuxSocket = getConfig(api).tmux_socket;
|
|
25
32
|
try {
|
|
26
33
|
const model = params.model ?? 'gemini-2.5-flash';
|
|
27
34
|
const escapedFilePath = escapeShellArg(params.file_path);
|
|
28
35
|
const escapedGoal = escapeShellArg(params.goal);
|
|
29
36
|
const escapedModel = escapeShellArg(model);
|
|
30
37
|
const command = `gemini -m ${escapedModel} --prompt '${escapedGoal}' -f '${escapedFilePath}' -o text > ${tempFile} 2>&1`;
|
|
31
|
-
const tmuxCommand = `tmux -S ${
|
|
38
|
+
const tmuxCommand = `tmux -S ${tmuxSocket} send-keys -t ${TMUX_SESSION_TARGET} -l -- '${command}' && sleep 0.1 && tmux -S ${tmuxSocket} send-keys -t ${TMUX_SESSION_TARGET} Enter`;
|
|
32
39
|
execSync(tmuxCommand, { timeout: 5000 });
|
|
33
40
|
const startTime = Date.now();
|
|
34
41
|
while (Date.now() - startTime < GEMINI_TIMEOUT_MS) {
|
|
@@ -40,13 +47,17 @@ export function registerLookAtTool(api) {
|
|
|
40
47
|
return { content: [{ type: 'text', text: result }] };
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
|
-
catch {
|
|
50
|
+
catch {
|
|
51
|
+
/* file not ready yet, continue polling */
|
|
52
|
+
}
|
|
44
53
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
45
54
|
}
|
|
46
55
|
try {
|
|
47
56
|
await fs.unlink(tempFile);
|
|
48
57
|
}
|
|
49
|
-
catch {
|
|
58
|
+
catch (cleanupErr) {
|
|
59
|
+
api.logger.warn('[omoc] Failed to clean up temp file:', tempFile, cleanupErr);
|
|
60
|
+
}
|
|
50
61
|
return {
|
|
51
62
|
content: [
|
|
52
63
|
{ type: 'text', text: 'Error: Gemini CLI timed out after 60 seconds' },
|
|
@@ -57,10 +68,15 @@ export function registerLookAtTool(api) {
|
|
|
57
68
|
try {
|
|
58
69
|
await fs.unlink(tempFile);
|
|
59
70
|
}
|
|
60
|
-
catch {
|
|
71
|
+
catch (cleanupErr) {
|
|
72
|
+
api.logger.warn('[omoc] Failed to clean up temp file:', tempFile, cleanupErr);
|
|
73
|
+
}
|
|
61
74
|
const message = error instanceof Error ? error.message : String(error);
|
|
62
75
|
return { content: [{ type: 'text', text: `Error: ${message}` }] };
|
|
63
76
|
}
|
|
77
|
+
finally {
|
|
78
|
+
isRunning = false;
|
|
79
|
+
}
|
|
64
80
|
},
|
|
65
81
|
optional: true,
|
|
66
82
|
});
|