@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.
Files changed (40) hide show
  1. package/dist/agent/crud.d.ts.map +1 -1
  2. package/dist/agent/crud.js +3 -4
  3. package/dist/agent/crud.js.map +1 -1
  4. package/dist/agent/memory.js +4 -4
  5. package/dist/agent/memory.js.map +1 -1
  6. package/dist/agent/permissions.d.ts +2 -2
  7. package/dist/agent/permissions.d.ts.map +1 -1
  8. package/dist/agent/permissions.js +16 -13
  9. package/dist/agent/permissions.js.map +1 -1
  10. package/dist/cli.js +2 -0
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +4 -6
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/cron.js +4 -4
  16. package/dist/commands/cron.js.map +1 -1
  17. package/dist/commands/init.d.ts +3 -0
  18. package/dist/commands/init.d.ts.map +1 -0
  19. package/dist/commands/init.js +19 -0
  20. package/dist/commands/init.js.map +1 -0
  21. package/dist/cron/daemon.js +2 -2
  22. package/dist/cron/daemon.js.map +1 -1
  23. package/dist/cron/handlers.d.ts +1 -1
  24. package/dist/cron/handlers.d.ts.map +1 -1
  25. package/dist/cron/handlers.js +14 -5
  26. package/dist/cron/handlers.js.map +1 -1
  27. package/dist/cron/ralph-wiggum.d.ts.map +1 -1
  28. package/dist/cron/ralph-wiggum.js +109 -54
  29. package/dist/cron/ralph-wiggum.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/agent/crud.ts +3 -4
  32. package/src/agent/memory.ts +4 -4
  33. package/src/agent/permissions.ts +14 -17
  34. package/src/cli.ts +2 -0
  35. package/src/commands/agent.ts +4 -6
  36. package/src/commands/cron.ts +4 -4
  37. package/src/commands/init.ts +22 -0
  38. package/src/cron/daemon.ts +2 -2
  39. package/src/cron/handlers.ts +17 -6
  40. package/src/cron/ralph-wiggum.ts +131 -63
@@ -1,15 +1,14 @@
1
- import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2
- import { existsSync, mkdirSync, writeFileSync, readFileSync, realpathSync } from "fs";
3
- import { join, dirname } from "path";
4
- import { fileURLToPath } from "url";
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
- const workspacePath = store.workspacePath(agentName);
35
- const progressFile = getProgressFilePath(agentName, job.id);
36
-
37
- try {
38
- // Ensure cliclaw CLI is in PATH
39
- const cleanEnv = { ...process.env };
40
- delete cleanEnv.CLAUDECODE;
41
-
42
- const monorepoRoot = join(__dirname, "..", "..", "..");
43
- const binScript = join(monorepoRoot, "packages", "cliclaw", "dist", "cli.js");
44
- if (existsSync(binScript)) {
45
- const resolvedBin = realpathSync(binScript);
46
- const localBin = join(workspacePath, ".bin");
47
- if (!existsSync(localBin)) mkdirSync(localBin, { recursive: true });
48
- const wrapper = join(localBin, "cliclaw");
49
- writeFileSync(wrapper, `#!/bin/sh\nexec node "${resolvedBin}" "$@"\n`, { mode: 0o755 });
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
- let totalCostUsd = 0;
54
- let sessionId: string | undefined;
55
- const transcript: TranscriptBlock[] = [];
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
- `${job.task}`,
151
+ taskContent,
74
152
  ``,
75
- `Write your output/progress to: ${progressFile}`,
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 ${progressFile}. Otherwise, just complete the task normally — no special signal is needed.`,
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: ${progressFile}`,
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: ${job.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 ${progressFile}. Otherwise, just complete the task normally — no special signal is needed.`,
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 conversation = query({
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
- for await (const event of conversation) {
106
- const msg = event as SDKMessage;
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 ((msg as any).type === "tool_result") {
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 (msg.type === "result") {
146
- if ("total_cost_usd" in msg) {
147
- totalCostUsd += (msg.total_cost_usd as number) ?? 0;
207
+ } else if (event.type === "result") {
208
+ if (event.total_cost_usd) {
209
+ totalCostUsd += event.total_cost_usd;
148
210
  }
149
- if (msg.session_id) {
150
- sessionId = msg.session_id;
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
- const needsMore = existsSync(progressFile) &&
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
  }