@digitalpresence/cliclaw 0.1.2 → 0.2.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/dist/agent/crud.d.ts.map +1 -1
- package/dist/agent/crud.js +3 -4
- package/dist/agent/crud.js.map +1 -1
- package/dist/agent/memory.js +4 -4
- package/dist/agent/memory.js.map +1 -1
- package/dist/agent/permissions.d.ts +2 -2
- package/dist/agent/permissions.d.ts.map +1 -1
- package/dist/agent/permissions.js +16 -13
- package/dist/agent/permissions.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +4 -6
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/cron.js +4 -4
- package/dist/commands/cron.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +19 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/cron/daemon.js +2 -2
- package/dist/cron/daemon.js.map +1 -1
- package/dist/cron/handlers.d.ts +1 -1
- package/dist/cron/handlers.d.ts.map +1 -1
- package/dist/cron/handlers.js +14 -5
- package/dist/cron/handlers.js.map +1 -1
- package/dist/cron/ralph-wiggum.d.ts.map +1 -1
- package/dist/cron/ralph-wiggum.js +109 -54
- package/dist/cron/ralph-wiggum.js.map +1 -1
- package/package.json +2 -2
- package/src/agent/crud.ts +3 -4
- package/src/agent/memory.ts +4 -4
- package/src/agent/permissions.ts +14 -17
- package/src/cli.ts +2 -0
- package/src/commands/agent.ts +4 -6
- package/src/commands/cron.ts +4 -4
- package/src/commands/init.ts +22 -0
- package/src/cron/daemon.ts +2 -2
- package/src/cron/handlers.ts +17 -6
- package/src/cron/ralph-wiggum.ts +131 -63
package/src/cron/ralph-wiggum.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { createInterface } from "readline";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
5
|
import type { AgentStore, CronJobConfig } from "@digitalpresence/cliclaw-auth";
|
|
6
6
|
import { getProgressFilePath, ensureCronDirs, writeRunningMarker, clearRunningMarker } from "./progress.js";
|
|
7
7
|
import { cronLog } from "./logger.js";
|
|
8
8
|
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = dirname(__filename);
|
|
11
|
-
|
|
12
9
|
const CONTINUE_MARKER = "NEEDS_MORE_ITERATIONS";
|
|
10
|
+
const CONTAINER_IMAGE = "cliclaw-agent";
|
|
11
|
+
const CONTAINER_TIMEOUT_MS = 300_000; // 5 minutes per iteration
|
|
13
12
|
|
|
14
13
|
export type TranscriptBlock =
|
|
15
14
|
| { type: "assistant"; content: string }
|
|
@@ -22,6 +21,81 @@ export interface RalphWiggumResult {
|
|
|
22
21
|
transcript: TranscriptBlock[];
|
|
23
22
|
}
|
|
24
23
|
|
|
24
|
+
function loadTaskContent(store: AgentStore, agentName: string, job: CronJobConfig): string {
|
|
25
|
+
const taskFilePath = join(store.workspacePath(agentName), "cron-tasks", job.taskFile);
|
|
26
|
+
if (existsSync(taskFilePath)) {
|
|
27
|
+
return readFileSync(taskFilePath, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
return `Task file not found: ${job.taskFile}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run a single iteration inside a Docker container.
|
|
34
|
+
* Returns parsed NDJSON events.
|
|
35
|
+
*/
|
|
36
|
+
async function runContainerIteration(
|
|
37
|
+
instancePath: string,
|
|
38
|
+
prompt: string,
|
|
39
|
+
sessionId?: string,
|
|
40
|
+
): Promise<{ events: any[]; exitCode: number }> {
|
|
41
|
+
// Write session.json
|
|
42
|
+
writeFileSync(
|
|
43
|
+
join(instancePath, "session.json"),
|
|
44
|
+
JSON.stringify({ prompt, ...(sessionId ? { sessionId } : {}) }),
|
|
45
|
+
"utf-8",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const args = [
|
|
49
|
+
"run", "--rm", "-i",
|
|
50
|
+
"-v", `${instancePath}:/instance`,
|
|
51
|
+
"--network=host",
|
|
52
|
+
"--cpus=2", "--memory=2g",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Pass API key
|
|
56
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
57
|
+
args.push("-e", `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
args.push(CONTAINER_IMAGE);
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const child = spawn("docker", args, {
|
|
64
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const timeout = setTimeout(() => {
|
|
68
|
+
child.kill("SIGTERM");
|
|
69
|
+
}, CONTAINER_TIMEOUT_MS);
|
|
70
|
+
|
|
71
|
+
const events: any[] = [];
|
|
72
|
+
const rl = createInterface({ input: child.stdout! });
|
|
73
|
+
|
|
74
|
+
rl.on("line", (line) => {
|
|
75
|
+
if (!line.trim()) return;
|
|
76
|
+
try {
|
|
77
|
+
events.push(JSON.parse(line));
|
|
78
|
+
} catch {
|
|
79
|
+
// skip malformed lines
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let stderr = "";
|
|
84
|
+
child.stderr!.on("data", (chunk: Buffer) => {
|
|
85
|
+
stderr += chunk.toString();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
child.on("close", (code) => {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
rl.close();
|
|
91
|
+
if (stderr.trim()) {
|
|
92
|
+
console.error(`[ralph-wiggum] container stderr: ${stderr.trim()}`);
|
|
93
|
+
}
|
|
94
|
+
resolve({ events, exitCode: code ?? 1 });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
25
99
|
export async function executeRalphWiggumLoop(
|
|
26
100
|
store: AgentStore,
|
|
27
101
|
agentName: string,
|
|
@@ -31,29 +105,33 @@ export async function executeRalphWiggumLoop(
|
|
|
31
105
|
ensureCronDirs(agentName, job.id);
|
|
32
106
|
writeRunningMarker(agentName, job.id, startedAt ?? new Date().toISOString());
|
|
33
107
|
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
cleanEnv.PATH = `${localBin}:${cleanEnv.PATH ?? ""}`;
|
|
108
|
+
// Use the cron instance path
|
|
109
|
+
const instancesDir = join(store.workspacePath(agentName), "..", "..", "instances");
|
|
110
|
+
const cronInstancePath = join(instancesDir, agentName, "_cron");
|
|
111
|
+
|
|
112
|
+
// Ensure cron instance exists with necessary files
|
|
113
|
+
if (!existsSync(cronInstancePath)) {
|
|
114
|
+
mkdirSync(join(cronInstancePath, "workspace"), { recursive: true });
|
|
115
|
+
mkdirSync(join(cronInstancePath, "memory"), { recursive: true });
|
|
116
|
+
|
|
117
|
+
// Copy template files
|
|
118
|
+
const agentDir = store.workspacePath(agentName);
|
|
119
|
+
for (const file of ["SOUL.md", "ROLE.md"]) {
|
|
120
|
+
const src = join(agentDir, file);
|
|
121
|
+
if (existsSync(src)) {
|
|
122
|
+
writeFileSync(join(cronInstancePath, file), readFileSync(src, "utf-8"));
|
|
123
|
+
}
|
|
51
124
|
}
|
|
125
|
+
}
|
|
52
126
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
127
|
+
const progressFile = getProgressFilePath(agentName, job.id);
|
|
128
|
+
const taskContent = loadTaskContent(store, agentName, job);
|
|
129
|
+
|
|
130
|
+
let totalCostUsd = 0;
|
|
131
|
+
let sessionId: string | undefined;
|
|
132
|
+
const transcript: TranscriptBlock[] = [];
|
|
56
133
|
|
|
134
|
+
try {
|
|
57
135
|
for (let iteration = 1; iteration <= job.maxIterations; iteration++) {
|
|
58
136
|
cronLog("info", `Iteration ${iteration}/${job.maxIterations}`, agentName, job.id);
|
|
59
137
|
|
|
@@ -70,47 +148,31 @@ export async function executeRalphWiggumLoop(
|
|
|
70
148
|
prompt = [
|
|
71
149
|
`You are executing a scheduled task. Here are your instructions:`,
|
|
72
150
|
``,
|
|
73
|
-
|
|
151
|
+
taskContent,
|
|
74
152
|
``,
|
|
75
|
-
`Write your output/progress to:
|
|
153
|
+
`Write your output/progress to: /instance/workspace/progress.md`,
|
|
76
154
|
``,
|
|
77
|
-
`If you CANNOT finish the task in this iteration and need to be re-invoked, write "${CONTINUE_MARKER}" anywhere in
|
|
155
|
+
`If you CANNOT finish the task in this iteration and need to be re-invoked, write "${CONTINUE_MARKER}" anywhere in the progress file. Otherwise, just complete the task normally — no special signal is needed.`,
|
|
78
156
|
].join("\n");
|
|
79
157
|
} else {
|
|
80
158
|
prompt = [
|
|
81
|
-
`You are continuing a scheduled task. Read your progress file at:
|
|
159
|
+
`You are continuing a scheduled task. Read your progress file at: /instance/workspace/progress.md`,
|
|
82
160
|
`Continue from where you left off.`,
|
|
83
161
|
``,
|
|
84
|
-
`Original task
|
|
162
|
+
`Original task:`,
|
|
163
|
+
taskContent,
|
|
85
164
|
``,
|
|
86
|
-
`If you CANNOT finish the task in this iteration and need to be re-invoked, write "${CONTINUE_MARKER}" anywhere in
|
|
165
|
+
`If you CANNOT finish the task in this iteration and need to be re-invoked, write "${CONTINUE_MARKER}" anywhere in the progress file. Otherwise, just complete the task normally — no special signal is needed.`,
|
|
87
166
|
].join("\n");
|
|
88
167
|
}
|
|
89
168
|
|
|
90
|
-
const
|
|
91
|
-
prompt,
|
|
92
|
-
options: {
|
|
93
|
-
cwd: workspacePath,
|
|
94
|
-
env: cleanEnv,
|
|
95
|
-
systemPrompt: { type: "preset" as const, preset: "claude_code" as const },
|
|
96
|
-
settingSources: ["project"],
|
|
97
|
-
includePartialMessages: true,
|
|
98
|
-
allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
|
|
99
|
-
...(sessionId ? { resume: sessionId } : {}),
|
|
100
|
-
},
|
|
101
|
-
});
|
|
169
|
+
const { events } = await runContainerIteration(cronInstancePath, prompt, sessionId);
|
|
102
170
|
|
|
171
|
+
// Process events into transcript
|
|
103
172
|
let currentToolInput = "";
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (msg.type === "stream_event" && (msg as any).event) {
|
|
109
|
-
const streamEvent = (msg as any).event as {
|
|
110
|
-
type: string;
|
|
111
|
-
content_block?: { type: string; name?: string; id?: string };
|
|
112
|
-
delta?: { type: string; text?: string; partial_json?: string };
|
|
113
|
-
};
|
|
173
|
+
for (const event of events) {
|
|
174
|
+
if (event.type === "stream_event" && event.event) {
|
|
175
|
+
const streamEvent = event.event;
|
|
114
176
|
|
|
115
177
|
if (streamEvent.type === "content_block_start" && streamEvent.content_block?.type === "tool_use") {
|
|
116
178
|
transcript.push({ type: "tool", name: streamEvent.content_block.name ?? "unknown", done: false });
|
|
@@ -137,26 +199,32 @@ export async function executeRalphWiggumLoop(
|
|
|
137
199
|
}
|
|
138
200
|
currentToolInput = "";
|
|
139
201
|
}
|
|
140
|
-
} else if (
|
|
202
|
+
} else if (event.type === "tool_result") {
|
|
141
203
|
const lastTool = [...transcript].reverse().find((b) => b.type === "tool" && !b.done);
|
|
142
204
|
if (lastTool && lastTool.type === "tool") {
|
|
143
205
|
lastTool.done = true;
|
|
144
206
|
}
|
|
145
|
-
} else if (
|
|
146
|
-
if (
|
|
147
|
-
totalCostUsd +=
|
|
207
|
+
} else if (event.type === "result") {
|
|
208
|
+
if (event.total_cost_usd) {
|
|
209
|
+
totalCostUsd += event.total_cost_usd;
|
|
148
210
|
}
|
|
149
|
-
if (
|
|
150
|
-
sessionId =
|
|
211
|
+
if (event.session_id) {
|
|
212
|
+
sessionId = event.session_id;
|
|
151
213
|
}
|
|
152
214
|
}
|
|
153
215
|
}
|
|
154
216
|
|
|
155
217
|
// Check if the agent needs more iterations by reading the progress file
|
|
156
|
-
|
|
218
|
+
// In container mode, progress is written to workspace/progress.md
|
|
219
|
+
const containerProgressFile = join(cronInstancePath, "workspace", "progress.md");
|
|
220
|
+
const needsMore = existsSync(containerProgressFile) &&
|
|
221
|
+
readFileSync(containerProgressFile, "utf-8").includes(CONTINUE_MARKER);
|
|
222
|
+
|
|
223
|
+
// Also check the original progress file location
|
|
224
|
+
const needsMoreOriginal = existsSync(progressFile) &&
|
|
157
225
|
readFileSync(progressFile, "utf-8").includes(CONTINUE_MARKER);
|
|
158
226
|
|
|
159
|
-
if (!needsMore) {
|
|
227
|
+
if (!needsMore && !needsMoreOriginal) {
|
|
160
228
|
cronLog("info", `Task completed at iteration ${iteration}`, agentName, job.id);
|
|
161
229
|
return { completed: true, iterations: iteration, totalCostUsd, transcript };
|
|
162
230
|
}
|