cueclaw 0.1.2 → 0.1.4

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.
@@ -1,13 +1,19 @@
1
1
  import {
2
2
  ConfigError
3
3
  } from "./chunk-BVQG3WYO.js";
4
+ import {
5
+ isDev
6
+ } from "./chunk-ZCK3IFLC.js";
4
7
 
5
8
  // src/config.ts
6
9
  import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
7
10
  import { join } from "path";
8
11
  import { homedir } from "os";
12
+ import { createRequire } from "module";
9
13
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
10
14
  import { z } from "zod/v4";
15
+ var require2 = createRequire(import.meta.url);
16
+ var { version: pkgVersion } = require2("../package.json");
11
17
  var ConfigSchema = z.object({
12
18
  claude: z.object({
13
19
  api_key: z.string(),
@@ -37,13 +43,18 @@ var ConfigSchema = z.object({
37
43
  }).optional(),
38
44
  container: z.object({
39
45
  enabled: z.boolean().default(false),
40
- image: z.string().default("cueclaw-agent:latest"),
46
+ image: z.string().default("ghcr.io/memodb-io/cueclaw-agent:latest"),
41
47
  timeout: z.number().default(18e5),
42
48
  max_output_size: z.number().default(10485760),
43
49
  idle_timeout: z.number().default(18e5),
44
50
  network: z.enum(["none", "host", "bridge"]).default("none")
45
51
  }).optional()
46
52
  });
53
+ var GHCR_IMAGE = "ghcr.io/memodb-io/cueclaw-agent";
54
+ var DEV_IMAGE = "cueclaw-agent:latest";
55
+ function getDefaultImage() {
56
+ return isDev ? DEV_IMAGE : `${GHCR_IMAGE}:${pkgVersion}`;
57
+ }
47
58
  function cueclawHome() {
48
59
  return join(homedir(), ".cueclaw");
49
60
  }
@@ -104,6 +115,12 @@ function loadConfig() {
104
115
  merged.claude = merged.claude ?? {};
105
116
  merged.claude.base_url = process.env["ANTHROPIC_BASE_URL"];
106
117
  }
118
+ if (process.env["CUECLAW_MODEL"]) {
119
+ merged.claude = merged.claude ?? {};
120
+ const model = process.env["CUECLAW_MODEL"];
121
+ merged.claude.planner = { ...merged.claude.planner, model };
122
+ merged.claude.executor = { ...merged.claude.executor, model };
123
+ }
107
124
  if (process.env["TELEGRAM_BOT_TOKEN"]) {
108
125
  merged.telegram = merged.telegram ?? {};
109
126
  merged.telegram.token = process.env["TELEGRAM_BOT_TOKEN"];
@@ -128,6 +145,9 @@ claude:
128
145
  executor:
129
146
  model: claude-sonnet-4-6
130
147
 
148
+ container:
149
+ enabled: true
150
+
131
151
  logging:
132
152
  level: info
133
153
  `;
@@ -236,6 +256,7 @@ function writeConfig(updates) {
236
256
  }
237
257
 
238
258
  export {
259
+ getDefaultImage,
239
260
  cueclawHome,
240
261
  ensureCueclawHome,
241
262
  loadConfig,
@@ -29,8 +29,9 @@ function checkEnvironment() {
29
29
  async function validateAuth(config) {
30
30
  try {
31
31
  const client = createAnthropicClient(config);
32
+ const model = config.claude.planner.model ?? "claude-haiku-4-5-20251001";
32
33
  await client.messages.create({
33
- model: "claude-haiku-4-5-20251001",
34
+ model,
34
35
  max_tokens: 10,
35
36
  messages: [{ role: "user", content: "ping" }]
36
37
  });
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cueclawHome
3
- } from "./chunk-RSKXBXSJ.js";
3
+ } from "./chunk-5TV4LNC3.js";
4
4
 
5
5
  // src/db.ts
6
6
  import Database from "better-sqlite3";
@@ -113,6 +113,24 @@ function insertWorkflow(db, workflow) {
113
113
  workflow.updated_at
114
114
  );
115
115
  }
116
+ function upsertWorkflow(db, workflow) {
117
+ db.prepare(`
118
+ INSERT OR REPLACE INTO workflows (id, name, description, trigger_json, steps_json, failure_policy_json, phase, schema_version, metadata_json, created_at, updated_at)
119
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
120
+ `).run(
121
+ workflow.id,
122
+ workflow.name,
123
+ workflow.description,
124
+ JSON.stringify(workflow.trigger),
125
+ JSON.stringify(workflow.steps),
126
+ JSON.stringify(workflow.failure_policy),
127
+ workflow.phase,
128
+ workflow.schema_version,
129
+ workflow.metadata ? JSON.stringify(workflow.metadata) : null,
130
+ workflow.created_at,
131
+ workflow.updated_at
132
+ );
133
+ }
116
134
  function getWorkflow(db, id) {
117
135
  const row = db.prepare("SELECT * FROM workflows WHERE id = ?").get(id);
118
136
  return row ? rowToWorkflow(row) : void 0;
@@ -148,8 +166,16 @@ function insertWorkflowRun(db, run) {
148
166
  `).run(run.id, run.workflow_id, run.trigger_data, run.status, run.started_at, run.completed_at ?? null, run.error ?? null, run.duration_ms ?? null);
149
167
  }
150
168
  function updateWorkflowRunStatus(db, id, status, error) {
151
- const completedAt = status !== "running" ? (/* @__PURE__ */ new Date()).toISOString() : null;
152
- db.prepare("UPDATE workflow_runs SET status = ?, completed_at = ?, error = ? WHERE id = ?").run(status, completedAt, error ?? null, id);
169
+ const now = /* @__PURE__ */ new Date();
170
+ const completedAt = status !== "running" ? now.toISOString() : null;
171
+ let durationMs = null;
172
+ if (completedAt) {
173
+ const row = db.prepare("SELECT started_at FROM workflow_runs WHERE id = ?").get(id);
174
+ if (row?.started_at) {
175
+ durationMs = now.getTime() - new Date(row.started_at).getTime();
176
+ }
177
+ }
178
+ db.prepare("UPDATE workflow_runs SET status = ?, completed_at = ?, error = ?, duration_ms = ? WHERE id = ?").run(status, completedAt, error ?? null, durationMs, id);
153
179
  }
154
180
  function insertStepRun(db, stepRun) {
155
181
  db.prepare(`
@@ -158,8 +184,16 @@ function insertStepRun(db, stepRun) {
158
184
  `).run(stepRun.id, stepRun.run_id, stepRun.step_id, stepRun.status, stepRun.output_json ?? null, stepRun.error ?? null, stepRun.started_at ?? null, stepRun.completed_at ?? null, stepRun.duration_ms ?? null);
159
185
  }
160
186
  function updateStepRunStatus(db, id, status, output, error) {
161
- const completedAt = status === "succeeded" || status === "failed" || status === "skipped" ? (/* @__PURE__ */ new Date()).toISOString() : null;
162
- db.prepare("UPDATE step_runs SET status = ?, output_json = ?, error = ?, completed_at = ? WHERE id = ?").run(status, output ?? null, error ?? null, completedAt, id);
187
+ const now = /* @__PURE__ */ new Date();
188
+ const completedAt = status === "succeeded" || status === "failed" || status === "skipped" ? now.toISOString() : null;
189
+ let durationMs = null;
190
+ if (completedAt) {
191
+ const row = db.prepare("SELECT started_at FROM step_runs WHERE id = ?").get(id);
192
+ if (row?.started_at) {
193
+ durationMs = now.getTime() - new Date(row.started_at).getTime();
194
+ }
195
+ }
196
+ db.prepare("UPDATE step_runs SET status = ?, output_json = ?, error = ?, completed_at = ?, duration_ms = ? WHERE id = ?").run(status, output ?? null, error ?? null, completedAt, durationMs, id);
163
197
  }
164
198
  function getStepRunsByRunId(db, runId) {
165
199
  return db.prepare("SELECT * FROM step_runs WHERE run_id = ?").all(runId);
@@ -171,6 +205,7 @@ function getWorkflowRunsByWorkflowId(db, workflowId) {
171
205
  export {
172
206
  initDb,
173
207
  insertWorkflow,
208
+ upsertWorkflow,
174
209
  getWorkflow,
175
210
  listWorkflows,
176
211
  updateWorkflowPhase,
@@ -0,0 +1,116 @@
1
+ // src/logger.ts
2
+ import pino from "pino";
3
+ import { PassThrough } from "stream";
4
+ import { createWriteStream, mkdirSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ var listeners = /* @__PURE__ */ new Set();
8
+ var tuiStream = new PassThrough();
9
+ tuiStream.on("data", (chunk) => {
10
+ for (const raw of chunk.toString().split("\n")) {
11
+ if (!raw.trim()) continue;
12
+ try {
13
+ const obj = JSON.parse(raw);
14
+ const lvl = (pino.levels.labels[obj.level] ?? "INFO").toUpperCase();
15
+ const mod = obj.module ? ` [${obj.module}]` : "";
16
+ const msg = obj.msg ?? "";
17
+ for (const fn of listeners) fn(`${lvl}${mod} ${msg}`);
18
+ } catch {
19
+ for (const fn of listeners) fn(raw);
20
+ }
21
+ }
22
+ });
23
+ var fileStream = null;
24
+ var logDir = null;
25
+ var configuredLevel = null;
26
+ function resolveDir(dir) {
27
+ return dir.startsWith("~") ? join(homedir(), dir.slice(1)) : dir;
28
+ }
29
+ function getLevel() {
30
+ return configuredLevel ?? process.env["LOG_LEVEL"] ?? "info";
31
+ }
32
+ var logger = pino({
33
+ level: process.env["LOG_LEVEL"] ?? "info",
34
+ transport: process.env["NODE_ENV"] === "production" ? void 0 : { target: "pino-pretty", options: { colorize: true } }
35
+ });
36
+ function initLogger(opts) {
37
+ const level = opts.level ?? process.env["LOG_LEVEL"] ?? "info";
38
+ configuredLevel = level;
39
+ if (opts.dir) {
40
+ const resolved = resolveDir(opts.dir);
41
+ logDir = resolved;
42
+ mkdirSync(join(resolved, "executions"), { recursive: true });
43
+ fileStream = createWriteStream(join(resolved, "daemon.log"), { flags: "a" });
44
+ logger = pino(
45
+ { level },
46
+ pino.multistream([
47
+ { stream: process.stdout },
48
+ { stream: fileStream }
49
+ ])
50
+ );
51
+ } else {
52
+ logger = pino({
53
+ level,
54
+ transport: process.env["NODE_ENV"] === "production" ? void 0 : { target: "pino-pretty", options: { colorize: true } }
55
+ });
56
+ }
57
+ }
58
+ function enableTuiLogging() {
59
+ if (fileStream) {
60
+ logger = pino(
61
+ { level: getLevel() },
62
+ pino.multistream([
63
+ { stream: tuiStream },
64
+ { stream: fileStream }
65
+ ])
66
+ );
67
+ } else {
68
+ logger = pino({ level: getLevel() }, tuiStream);
69
+ }
70
+ }
71
+ function onLogLine(fn) {
72
+ listeners.add(fn);
73
+ return () => {
74
+ listeners.delete(fn);
75
+ };
76
+ }
77
+ function createChildLogger(bindings) {
78
+ return logger.child(bindings);
79
+ }
80
+ function createExecutionLogger(workflowId, runId) {
81
+ if (!logDir) {
82
+ return logger.child({ workflowId, runId });
83
+ }
84
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
85
+ const filename = `${workflowId}_${date}.log`;
86
+ const execStream = createWriteStream(join(logDir, "executions", filename), { flags: "a" });
87
+ return pino(
88
+ { level: getLevel() },
89
+ pino.multistream([
90
+ { stream: execStream },
91
+ ...fileStream ? [{ stream: fileStream }] : []
92
+ ])
93
+ );
94
+ }
95
+ function resetLogger() {
96
+ if (fileStream) {
97
+ fileStream.end();
98
+ fileStream = null;
99
+ }
100
+ logDir = null;
101
+ configuredLevel = null;
102
+ logger = pino({
103
+ level: process.env["LOG_LEVEL"] ?? "info",
104
+ transport: process.env["NODE_ENV"] === "production" ? void 0 : { target: "pino-pretty", options: { colorize: true } }
105
+ });
106
+ }
107
+
108
+ export {
109
+ logger,
110
+ initLogger,
111
+ enableTuiLogging,
112
+ onLogLine,
113
+ createChildLogger,
114
+ createExecutionLogger,
115
+ resetLogger
116
+ };
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  cueclawHome
3
- } from "./chunk-RSKXBXSJ.js";
3
+ } from "./chunk-5TV4LNC3.js";
4
4
  import {
5
5
  logger
6
- } from "./chunk-QBOYMF4A.js";
6
+ } from "./chunk-KBLMQZ3P.js";
7
7
 
8
8
  // src/service.ts
9
9
  import { writeFileSync, existsSync, unlinkSync, mkdirSync } from "fs";
@@ -91,6 +91,7 @@ function installLaunchd() {
91
91
  <string>${cliPath}</string>
92
92
  <string>daemon</string>
93
93
  <string>start</string>
94
+ <string>--foreground</string>
94
95
  </array>
95
96
  <key>KeepAlive</key>
96
97
  <true/>
@@ -136,7 +137,7 @@ Description=CueClaw Daemon
136
137
  After=network.target
137
138
 
138
139
  [Service]
139
- ExecStart=${nodePath} ${cliPath} daemon start
140
+ ExecStart=${nodePath} ${cliPath} daemon start --foreground
140
141
  Restart=always
141
142
  RestartSec=5
142
143
 
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  createAnthropicClient
3
3
  } from "./chunk-DVQFSFIZ.js";
4
- import {
5
- getConfiguredSecretKeys
6
- } from "./chunk-ZCK3IFLC.js";
7
4
  import {
8
5
  PlannerError
9
6
  } from "./chunk-BVQG3WYO.js";
7
+ import {
8
+ getConfiguredSecretKeys
9
+ } from "./chunk-ZCK3IFLC.js";
10
10
  import {
11
11
  logger
12
- } from "./chunk-QBOYMF4A.js";
12
+ } from "./chunk-KBLMQZ3P.js";
13
13
 
14
14
  // src/planner.ts
15
15
  import { z } from "zod/v4";
@@ -236,9 +236,26 @@ function parsePlannerToolResponse(response) {
236
236
  }
237
237
  return { type: "error", error: "Unexpected planner response format" };
238
238
  }
239
- function buildPlannerSystemPrompt(config) {
239
+ function buildPlannerSystemPrompt(config, channelContext) {
240
240
  const identity = config.identity?.name ? `
241
241
  User identity: ${config.identity.name}` : "";
242
+ let channelSection;
243
+ if (channelContext && channelContext.channel !== "tui") {
244
+ channelSection = `
245
+ ## Channel Context
246
+
247
+ The user is requesting this workflow via **${channelContext.channel}**.
248
+ - Chat ID: ${channelContext.chatJid}
249
+ - Sender: ${channelContext.sender}
250
+
251
+ When the workflow needs to send notifications or messages back to the user, use the chat ID and channel above as the recipient. Do not ask the user for recipient information \u2014 you already have it.`;
252
+ } else {
253
+ channelSection = `
254
+ ## Channel Context
255
+
256
+ The user is using the TUI (terminal interface). No chat recipient is available.
257
+ If the workflow needs to send notifications or messages to someone, the step description must require explicit recipient input (e.g., email address, phone number, chat ID) \u2014 do not assume any default recipient.`;
258
+ }
242
259
  return `You are CueClaw Planner. Convert user's natural language into a structured Workflow.
243
260
 
244
261
  ## Execution Environment
@@ -276,9 +293,9 @@ You can reference these in workflow steps \u2014 they are available as environme
276
293
  If a workflow needs credentials not listed above, use the set_secret tool to store them after the user provides the value. Never invent or guess secret values.
277
294
 
278
295
  ## User Identity
279
- ${identity}`;
296
+ ${identity}${channelSection}`;
280
297
  }
281
- async function generatePlan(userDescription, config) {
298
+ async function generatePlan(userDescription, config, channelContext) {
282
299
  const anthropic = createAnthropicClient(config);
283
300
  const MAX_RETRIES = 2;
284
301
  let retryContext = "";
@@ -292,13 +309,14 @@ ${retryContext}` : userDescription;
292
309
  response = await anthropic.messages.create({
293
310
  model: config.claude.planner.model,
294
311
  max_tokens: 4096,
295
- system: buildPlannerSystemPrompt(config),
312
+ system: buildPlannerSystemPrompt(config, channelContext),
296
313
  messages: [{ role: "user", content: prompt }],
297
314
  tools: [plannerTool],
298
315
  tool_choice: { type: "tool", name: "create_workflow" }
299
316
  });
300
317
  } catch (err) {
301
318
  const detail = err instanceof Error ? err.message : String(err);
319
+ logger.error({ err, attempt }, "Planner API request failed");
302
320
  throw new PlannerError(
303
321
  `API request failed: ${detail}. Check your API key and base_url in ~/.cueclaw/config.yaml`
304
322
  );
@@ -322,6 +340,7 @@ ${retryContext}` : userDescription;
322
340
  if (!parseResult.success) {
323
341
  const errMsg = parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
324
342
  if (attempt < MAX_RETRIES) {
343
+ logger.warn({ attempt, errors: errMsg }, "Planner output validation failed, retrying");
325
344
  retryContext = `[System] Previous plan had validation issues:
326
345
  ${errMsg}
327
346
  Please fix and try again.`;
@@ -332,6 +351,7 @@ Please fix and try again.`;
332
351
  const dagErrors = validateDAG(parseResult.data.steps);
333
352
  if (dagErrors.length > 0) {
334
353
  if (attempt < MAX_RETRIES) {
354
+ logger.warn({ attempt, dagErrors }, "DAG validation failed, retrying");
335
355
  retryContext = `[System] DAG dependency issues:
336
356
  ${dagErrors.join("\n")}
337
357
  Please fix the step dependencies.`;
@@ -340,7 +360,7 @@ Please fix the step dependencies.`;
340
360
  throw new PlannerError(`DAG validation failed after ${MAX_RETRIES + 1} attempts: ${dagErrors.join(", ")}`);
341
361
  }
342
362
  const now = (/* @__PURE__ */ new Date()).toISOString();
343
- return {
363
+ const workflow = {
344
364
  ...parseResult.data,
345
365
  schema_version: "1.0",
346
366
  id: `wf_${nanoid()}`,
@@ -348,10 +368,12 @@ Please fix the step dependencies.`;
348
368
  created_at: now,
349
369
  updated_at: now
350
370
  };
371
+ logger.info({ name: workflow.name, stepCount: workflow.steps.length, workflowId: workflow.id }, "Plan generated successfully");
372
+ return workflow;
351
373
  }
352
374
  throw new PlannerError("Failed to generate valid plan after retries");
353
375
  }
354
- async function modifyPlan(originalWorkflow, modificationDescription, config) {
376
+ async function modifyPlan(originalWorkflow, modificationDescription, config, channelContext) {
355
377
  const plannerOutput = {
356
378
  name: originalWorkflow.name,
357
379
  description: originalWorkflow.description,
@@ -369,7 +391,8 @@ ${modificationDescription}
369
391
 
370
392
  Preserve unmodified steps' IDs, descriptions, and dependencies \u2014 only change what the user specified.
371
393
  Return the complete modified workflow using the create_workflow tool.`;
372
- const result = await generatePlan(combinedPrompt, config);
394
+ const result = await generatePlan(combinedPrompt, config, channelContext);
395
+ logger.info({ workflowId: originalWorkflow.id }, "Plan modified");
373
396
  return {
374
397
  ...result,
375
398
  id: originalWorkflow.id,
@@ -381,6 +404,7 @@ function confirmPlan(workflow) {
381
404
  throw new PlannerError(`Cannot confirm workflow in phase "${workflow.phase}"`);
382
405
  }
383
406
  const nextPhase = workflow.trigger.type === "manual" ? "executing" : "active";
407
+ logger.info({ workflowId: workflow.id, nextPhase }, "Plan confirmed");
384
408
  return {
385
409
  ...workflow,
386
410
  phase: nextPhase,
@@ -388,6 +412,7 @@ function confirmPlan(workflow) {
388
412
  };
389
413
  }
390
414
  function rejectPlan(workflow) {
415
+ logger.info({ workflowId: workflow.id }, "Plan rejected");
391
416
  return {
392
417
  ...workflow,
393
418
  phase: "planning",