@digitalpresence/cliclaw 0.2.1 → 0.3.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.
@@ -3,6 +3,7 @@ import { createInterface } from "readline";
3
3
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
4
4
  import { join } from "path";
5
5
  import type { AgentStore, CronJobConfig } from "@digitalpresence/cliclaw-auth";
6
+ import { generateUniversalClaudeMd, generateContextMd } from "@digitalpresence/cliclaw-auth";
6
7
  import { getProgressFilePath, ensureCronDirs, writeRunningMarker, clearRunningMarker } from "./progress.js";
7
8
  import { cronLog } from "./logger.js";
8
9
 
@@ -18,9 +19,18 @@ export interface RalphWiggumResult {
18
19
  completed: boolean;
19
20
  iterations: number;
20
21
  totalCostUsd: number;
22
+ report?: string;
21
23
  transcript: TranscriptBlock[];
22
24
  }
23
25
 
26
+ function readReport(instancePath: string): string | undefined {
27
+ const reportPath = join(instancePath, "workspace", "REPORT.md");
28
+ if (existsSync(reportPath)) {
29
+ return readFileSync(reportPath, "utf-8").trim() || undefined;
30
+ }
31
+ return undefined;
32
+ }
33
+
24
34
  function loadTaskContent(store: AgentStore, agentName: string, job: CronJobConfig): string {
25
35
  const taskFilePath = join(store.workspacePath(agentName), "cron-tasks", job.taskFile);
26
36
  if (existsSync(taskFilePath)) {
@@ -45,18 +55,39 @@ async function runContainerIteration(
45
55
  "utf-8",
46
56
  );
47
57
 
58
+ // Ensure persistent Claude state directory for session resumption
59
+ const claudeStatePath = join(instancePath, ".claude-state");
60
+ if (!existsSync(claudeStatePath)) {
61
+ mkdirSync(claudeStatePath, { recursive: true });
62
+ }
63
+
64
+ // Mount cliclaw config if available (portal injects this for Google OAuth)
65
+ const cliclawConfigPath = join(instancePath, ".cliclaw-config");
66
+
48
67
  const args = [
49
68
  "run", "--rm", "-i",
50
69
  "-v", `${instancePath}:/instance`,
70
+ "-v", `${claudeStatePath}:/home/agent/.claude`,
71
+ ...(existsSync(cliclawConfigPath)
72
+ ? ["-v", `${cliclawConfigPath}:/home/agent/.cliclaw`]
73
+ : []),
51
74
  "--network=host",
52
75
  "--cpus=2", "--memory=2g",
53
76
  ];
54
77
 
55
- // Pass API key
56
- if (process.env.ANTHROPIC_API_KEY) {
78
+ // Pass auth env vars
79
+ if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
80
+ args.push("-e", `CLAUDE_CODE_OAUTH_TOKEN=${process.env.CLAUDE_CODE_OAUTH_TOKEN}`);
81
+ } else if (process.env.ANTHROPIC_API_KEY) {
57
82
  args.push("-e", `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`);
58
83
  }
59
84
 
85
+ // Pass tokens path if set (portal injects this for client integrations)
86
+ if (process.env.CLICLAW_TOKENS_PATH) {
87
+ const containerTokensPath = process.env.CLICLAW_TOKENS_PATH.replace(instancePath, "/instance");
88
+ args.push("-e", `CLICLAW_TOKENS_PATH=${containerTokensPath}`);
89
+ }
90
+
60
91
  args.push(CONTAINER_IMAGE);
61
92
 
62
93
  return new Promise((resolve) => {
@@ -109,20 +140,21 @@ export async function executeRalphWiggumLoop(
109
140
  const instancesDir = join(store.workspacePath(agentName), "..", "..", "instances");
110
141
  const cronInstancePath = join(instancesDir, agentName, "_cron");
111
142
 
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
- }
124
- }
125
- }
143
+ // Ensure cron instance exists with unified CLAUDE.md
144
+ mkdirSync(join(cronInstancePath, "workspace"), { recursive: true });
145
+ mkdirSync(join(cronInstancePath, "memory"), { recursive: true });
146
+
147
+ // Generate unified CLAUDE.md with soul/role/context inlined
148
+ const config = store.get(agentName)!;
149
+ const agentDir = store.workspacePath(agentName);
150
+ const soul = existsSync(join(agentDir, "SOUL.md")) ? readFileSync(join(agentDir, "SOUL.md"), "utf-8") : "";
151
+ const role = existsSync(join(agentDir, "ROLE.md")) ? readFileSync(join(agentDir, "ROLE.md"), "utf-8") : "";
152
+ const context = generateContextMd(config, []);
153
+ writeFileSync(
154
+ join(cronInstancePath, "CLAUDE.md"),
155
+ generateUniversalClaudeMd({ soul, role, context }),
156
+ "utf-8",
157
+ );
126
158
 
127
159
  const progressFile = getProgressFilePath(agentName, job.id);
128
160
  const taskContent = loadTaskContent(store, agentName, job);
@@ -150,7 +182,9 @@ export async function executeRalphWiggumLoop(
150
182
  ``,
151
183
  taskContent,
152
184
  ``,
153
- `Write your output/progress to: /instance/workspace/progress.md`,
185
+ `You MUST write two files:`,
186
+ `1. /instance/workspace/progress.md — working notes and intermediate state`,
187
+ `2. /instance/workspace/REPORT.md — a clean, final summary of what you did and found (this is shown to the user)`,
154
188
  ``,
155
189
  `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.`,
156
190
  ].join("\n");
@@ -162,6 +196,10 @@ export async function executeRalphWiggumLoop(
162
196
  `Original task:`,
163
197
  taskContent,
164
198
  ``,
199
+ `You MUST write two files:`,
200
+ `1. /instance/workspace/progress.md — working notes and intermediate state`,
201
+ `2. /instance/workspace/REPORT.md — a clean, final summary of what you did and found (this is shown to the user)`,
202
+ ``,
165
203
  `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.`,
166
204
  ].join("\n");
167
205
  }
@@ -199,7 +237,19 @@ export async function executeRalphWiggumLoop(
199
237
  }
200
238
  currentToolInput = "";
201
239
  }
202
- } else if (event.type === "tool_result") {
240
+ } else if (event.type === "assistant" && event.message?.content) {
241
+ // Full assistant message — extract text blocks
242
+ for (const block of event.message.content) {
243
+ if (block.type === "text" && block.text) {
244
+ const last = transcript[transcript.length - 1];
245
+ if (last?.type === "assistant") {
246
+ last.content = block.text; // Replace with full text
247
+ } else {
248
+ transcript.push({ type: "assistant", content: block.text });
249
+ }
250
+ }
251
+ }
252
+ } else if (event.type === "tool_result" || event.type === "tool_use_summary") {
203
253
  const lastTool = [...transcript].reverse().find((b) => b.type === "tool" && !b.done);
204
254
  if (lastTool && lastTool.type === "tool") {
205
255
  lastTool.done = true;
@@ -226,14 +276,16 @@ export async function executeRalphWiggumLoop(
226
276
 
227
277
  if (!needsMore && !needsMoreOriginal) {
228
278
  cronLog("info", `Task completed at iteration ${iteration}`, agentName, job.id);
229
- return { completed: true, iterations: iteration, totalCostUsd, transcript };
279
+ const report = readReport(cronInstancePath);
280
+ return { completed: true, iterations: iteration, totalCostUsd, report, transcript };
230
281
  }
231
282
 
232
283
  cronLog("info", `Agent requested continuation (${CONTINUE_MARKER} found in progress file)`, agentName, job.id);
233
284
  }
234
285
 
235
286
  cronLog("warn", `Max iterations (${job.maxIterations}) reached`, agentName, job.id);
236
- return { completed: false, iterations: job.maxIterations, totalCostUsd, transcript };
287
+ const report = readReport(cronInstancePath);
288
+ return { completed: false, iterations: job.maxIterations, totalCostUsd, report, transcript };
237
289
  } finally {
238
290
  clearRunningMarker(agentName, job.id);
239
291
  }
@@ -0,0 +1,73 @@
1
+ import { TokenStore } from "@digitalpresence/cliclaw-auth";
2
+ import { outputAuthRequired, outputError } from "../lib/output.js";
3
+
4
+ const GITHUB_API = "https://api.github.com";
5
+
6
+ export function getToken(tokenStore: TokenStore, account: string): string {
7
+ const tokenKey = `github:${account}`;
8
+ const creds = tokenStore.get(tokenKey);
9
+ if (!creds?.access_token) {
10
+ outputAuthRequired("github");
11
+ }
12
+ return creds.access_token as string;
13
+ }
14
+
15
+ export async function githubFetch(
16
+ token: string,
17
+ path: string,
18
+ options: RequestInit = {},
19
+ ): Promise<unknown> {
20
+ const url = path.startsWith("http") ? path : `${GITHUB_API}${path}`;
21
+ const res = await fetch(url, {
22
+ ...options,
23
+ headers: {
24
+ Authorization: `Bearer ${token}`,
25
+ Accept: "application/vnd.github+json",
26
+ "X-GitHub-Api-Version": "2022-11-28",
27
+ ...options.headers,
28
+ },
29
+ });
30
+
31
+ if (!res.ok) {
32
+ const body = await res.text().catch(() => "");
33
+ outputError("github_api_error", `${res.status} ${res.statusText}: ${body}`);
34
+ }
35
+
36
+ return res.json();
37
+ }
38
+
39
+ export async function githubFetchPaginated(
40
+ token: string,
41
+ path: string,
42
+ maxResults: number,
43
+ ): Promise<unknown[]> {
44
+ const results: unknown[] = [];
45
+ const separator = path.includes("?") ? "&" : "?";
46
+ let url: string | null = `${path}${separator}per_page=${Math.min(maxResults, 100)}`;
47
+
48
+ while (url && results.length < maxResults) {
49
+ const fullUrl: string = url.startsWith("http") ? url : `${GITHUB_API}${url}`;
50
+ const res: Response = await fetch(fullUrl, {
51
+ headers: {
52
+ Authorization: `Bearer ${token}`,
53
+ Accept: "application/vnd.github+json",
54
+ "X-GitHub-Api-Version": "2022-11-28",
55
+ },
56
+ });
57
+
58
+ if (!res.ok) {
59
+ const body = await res.text().catch(() => "");
60
+ outputError("github_api_error", `${res.status} ${res.statusText}: ${body}`);
61
+ }
62
+
63
+ const data = await res.json();
64
+ results.push(...(Array.isArray(data) ? data : [data]));
65
+
66
+ // Parse Link header for next page
67
+ const link: string | null = res.headers.get("link");
68
+ const next: RegExpMatchArray | null | undefined = link?.match(/<([^>]+)>;\s*rel="next"/);
69
+ url = next ? next[1] : null;
70
+ }
71
+
72
+ return results.slice(0, maxResults);
73
+ }
@@ -0,0 +1,325 @@
1
+ import type { TokenStore } from "@digitalpresence/cliclaw-auth";
2
+ import { getToken, githubFetch, githubFetchPaginated } from "./api.js";
3
+ import { outputJson, outputError } from "../lib/output.js";
4
+
5
+ // --- User ---
6
+
7
+ export async function handleWhoami(tokenStore: TokenStore, account: string): Promise<void> {
8
+ const token = getToken(tokenStore, account);
9
+ try {
10
+ const user = await githubFetch(token, "/user");
11
+ outputJson(user);
12
+ } catch (err) {
13
+ outputError("whoami_failed", err instanceof Error ? err.message : String(err));
14
+ }
15
+ }
16
+
17
+ // --- Repos ---
18
+
19
+ export async function handleRepos(
20
+ tokenStore: TokenStore,
21
+ account: string,
22
+ maxResults: number,
23
+ type: string,
24
+ sort: string,
25
+ ): Promise<void> {
26
+ const token = getToken(tokenStore, account);
27
+ try {
28
+ const repos = await githubFetchPaginated(
29
+ token,
30
+ `/user/repos?type=${type}&sort=${sort}`,
31
+ maxResults,
32
+ );
33
+ outputJson(repos);
34
+ } catch (err) {
35
+ outputError("repos_failed", err instanceof Error ? err.message : String(err));
36
+ }
37
+ }
38
+
39
+ export async function handleRepoGet(
40
+ tokenStore: TokenStore,
41
+ account: string,
42
+ owner: string,
43
+ repo: string,
44
+ ): Promise<void> {
45
+ const token = getToken(tokenStore, account);
46
+ try {
47
+ const data = await githubFetch(token, `/repos/${owner}/${repo}`);
48
+ outputJson(data);
49
+ } catch (err) {
50
+ outputError("repo_get_failed", err instanceof Error ? err.message : String(err));
51
+ }
52
+ }
53
+
54
+ // --- Issues ---
55
+
56
+ export async function handleIssues(
57
+ tokenStore: TokenStore,
58
+ account: string,
59
+ owner: string,
60
+ repo: string,
61
+ state: string,
62
+ maxResults: number,
63
+ ): Promise<void> {
64
+ const token = getToken(tokenStore, account);
65
+ try {
66
+ const issues = await githubFetchPaginated(
67
+ token,
68
+ `/repos/${owner}/${repo}/issues?state=${state}`,
69
+ maxResults,
70
+ );
71
+ outputJson(issues);
72
+ } catch (err) {
73
+ outputError("issues_failed", err instanceof Error ? err.message : String(err));
74
+ }
75
+ }
76
+
77
+ export async function handleIssueGet(
78
+ tokenStore: TokenStore,
79
+ account: string,
80
+ owner: string,
81
+ repo: string,
82
+ number: number,
83
+ ): Promise<void> {
84
+ const token = getToken(tokenStore, account);
85
+ try {
86
+ const issue = await githubFetch(token, `/repos/${owner}/${repo}/issues/${number}`);
87
+ outputJson(issue);
88
+ } catch (err) {
89
+ outputError("issue_get_failed", err instanceof Error ? err.message : String(err));
90
+ }
91
+ }
92
+
93
+ export async function handleIssueCreate(
94
+ tokenStore: TokenStore,
95
+ account: string,
96
+ owner: string,
97
+ repo: string,
98
+ title: string,
99
+ body?: string,
100
+ labels?: string[],
101
+ ): Promise<void> {
102
+ const token = getToken(tokenStore, account);
103
+ try {
104
+ const payload: Record<string, unknown> = { title };
105
+ if (body) payload.body = body;
106
+ if (labels?.length) payload.labels = labels;
107
+ const issue = await githubFetch(token, `/repos/${owner}/${repo}/issues`, {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify(payload),
111
+ });
112
+ outputJson(issue);
113
+ } catch (err) {
114
+ outputError("issue_create_failed", err instanceof Error ? err.message : String(err));
115
+ }
116
+ }
117
+
118
+ export async function handleIssueComment(
119
+ tokenStore: TokenStore,
120
+ account: string,
121
+ owner: string,
122
+ repo: string,
123
+ number: number,
124
+ body: string,
125
+ ): Promise<void> {
126
+ const token = getToken(tokenStore, account);
127
+ try {
128
+ const comment = await githubFetch(
129
+ token,
130
+ `/repos/${owner}/${repo}/issues/${number}/comments`,
131
+ {
132
+ method: "POST",
133
+ headers: { "Content-Type": "application/json" },
134
+ body: JSON.stringify({ body }),
135
+ },
136
+ );
137
+ outputJson(comment);
138
+ } catch (err) {
139
+ outputError("issue_comment_failed", err instanceof Error ? err.message : String(err));
140
+ }
141
+ }
142
+
143
+ export async function handleIssueClose(
144
+ tokenStore: TokenStore,
145
+ account: string,
146
+ owner: string,
147
+ repo: string,
148
+ number: number,
149
+ ): Promise<void> {
150
+ const token = getToken(tokenStore, account);
151
+ try {
152
+ const issue = await githubFetch(token, `/repos/${owner}/${repo}/issues/${number}`, {
153
+ method: "PATCH",
154
+ headers: { "Content-Type": "application/json" },
155
+ body: JSON.stringify({ state: "closed" }),
156
+ });
157
+ outputJson(issue);
158
+ } catch (err) {
159
+ outputError("issue_close_failed", err instanceof Error ? err.message : String(err));
160
+ }
161
+ }
162
+
163
+ // --- Pull Requests ---
164
+
165
+ export async function handlePulls(
166
+ tokenStore: TokenStore,
167
+ account: string,
168
+ owner: string,
169
+ repo: string,
170
+ state: string,
171
+ maxResults: number,
172
+ ): Promise<void> {
173
+ const token = getToken(tokenStore, account);
174
+ try {
175
+ const prs = await githubFetchPaginated(
176
+ token,
177
+ `/repos/${owner}/${repo}/pulls?state=${state}`,
178
+ maxResults,
179
+ );
180
+ outputJson(prs);
181
+ } catch (err) {
182
+ outputError("pulls_failed", err instanceof Error ? err.message : String(err));
183
+ }
184
+ }
185
+
186
+ export async function handlePullGet(
187
+ tokenStore: TokenStore,
188
+ account: string,
189
+ owner: string,
190
+ repo: string,
191
+ number: number,
192
+ ): Promise<void> {
193
+ const token = getToken(tokenStore, account);
194
+ try {
195
+ const pr = await githubFetch(token, `/repos/${owner}/${repo}/pulls/${number}`);
196
+ outputJson(pr);
197
+ } catch (err) {
198
+ outputError("pull_get_failed", err instanceof Error ? err.message : String(err));
199
+ }
200
+ }
201
+
202
+ export async function handlePullReviews(
203
+ tokenStore: TokenStore,
204
+ account: string,
205
+ owner: string,
206
+ repo: string,
207
+ number: number,
208
+ ): Promise<void> {
209
+ const token = getToken(tokenStore, account);
210
+ try {
211
+ const reviews = await githubFetch(
212
+ token,
213
+ `/repos/${owner}/${repo}/pulls/${number}/reviews`,
214
+ );
215
+ outputJson(reviews);
216
+ } catch (err) {
217
+ outputError("pull_reviews_failed", err instanceof Error ? err.message : String(err));
218
+ }
219
+ }
220
+
221
+ export async function handlePullMerge(
222
+ tokenStore: TokenStore,
223
+ account: string,
224
+ owner: string,
225
+ repo: string,
226
+ number: number,
227
+ method: string,
228
+ ): Promise<void> {
229
+ const token = getToken(tokenStore, account);
230
+ try {
231
+ const result = await githubFetch(
232
+ token,
233
+ `/repos/${owner}/${repo}/pulls/${number}/merge`,
234
+ {
235
+ method: "PUT",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify({ merge_method: method }),
238
+ },
239
+ );
240
+ outputJson(result);
241
+ } catch (err) {
242
+ outputError("pull_merge_failed", err instanceof Error ? err.message : String(err));
243
+ }
244
+ }
245
+
246
+ // --- Notifications ---
247
+
248
+ export async function handleNotifications(
249
+ tokenStore: TokenStore,
250
+ account: string,
251
+ maxResults: number,
252
+ ): Promise<void> {
253
+ const token = getToken(tokenStore, account);
254
+ try {
255
+ const notifications = await githubFetchPaginated(
256
+ token,
257
+ "/notifications",
258
+ maxResults,
259
+ );
260
+ outputJson(notifications);
261
+ } catch (err) {
262
+ outputError("notifications_failed", err instanceof Error ? err.message : String(err));
263
+ }
264
+ }
265
+
266
+ // --- Search ---
267
+
268
+ export async function handleSearch(
269
+ tokenStore: TokenStore,
270
+ account: string,
271
+ query: string,
272
+ type: string,
273
+ maxResults: number,
274
+ ): Promise<void> {
275
+ const token = getToken(tokenStore, account);
276
+ try {
277
+ const data = await githubFetch(
278
+ token,
279
+ `/search/${type}?q=${encodeURIComponent(query)}&per_page=${Math.min(maxResults, 100)}`,
280
+ );
281
+ outputJson(data);
282
+ } catch (err) {
283
+ outputError("search_failed", err instanceof Error ? err.message : String(err));
284
+ }
285
+ }
286
+
287
+ // --- Actions ---
288
+
289
+ export async function handleWorkflowRuns(
290
+ tokenStore: TokenStore,
291
+ account: string,
292
+ owner: string,
293
+ repo: string,
294
+ maxResults: number,
295
+ ): Promise<void> {
296
+ const token = getToken(tokenStore, account);
297
+ try {
298
+ const data = await githubFetch(
299
+ token,
300
+ `/repos/${owner}/${repo}/actions/runs?per_page=${Math.min(maxResults, 100)}`,
301
+ );
302
+ outputJson(data);
303
+ } catch (err) {
304
+ outputError("workflow_runs_failed", err instanceof Error ? err.message : String(err));
305
+ }
306
+ }
307
+
308
+ export async function handleWorkflowRunLogs(
309
+ tokenStore: TokenStore,
310
+ account: string,
311
+ owner: string,
312
+ repo: string,
313
+ runId: number,
314
+ ): Promise<void> {
315
+ const token = getToken(tokenStore, account);
316
+ try {
317
+ const jobs = await githubFetch(
318
+ token,
319
+ `/repos/${owner}/${repo}/actions/runs/${runId}/jobs`,
320
+ );
321
+ outputJson(jobs);
322
+ } catch (err) {
323
+ outputError("workflow_logs_failed", err instanceof Error ? err.message : String(err));
324
+ }
325
+ }
@@ -1,7 +0,0 @@
1
-
2
- > @digitalpresence/cliclaw@0.1.0 dev /Users/markshteyn/emdash-projects/worktrees/dark-doodles-rest-8y8/packages/cliclaw
3
- > tsx src/cli.ts
4
-
5
- sh: tsx: command not found
6
-  ELIFECYCLE  Command failed.
7
-  WARN  Local package.json exists, but node_modules missing, did you mean to install?