cueclaw 0.0.3 → 0.1.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.
@@ -0,0 +1,409 @@
1
+ import {
2
+ createAnthropicClient
3
+ } from "./chunk-DVQFSFIZ.js";
4
+ import {
5
+ getConfiguredSecretKeys
6
+ } from "./chunk-ZCK3IFLC.js";
7
+ import {
8
+ PlannerError
9
+ } from "./chunk-BVQG3WYO.js";
10
+ import {
11
+ logger
12
+ } from "./chunk-QBOYMF4A.js";
13
+
14
+ // src/planner.ts
15
+ import { z } from "zod/v4";
16
+ import { nanoid } from "nanoid";
17
+
18
+ // src/workflow.ts
19
+ function validateDAG(steps) {
20
+ const errors = [];
21
+ const stepIds = new Set(steps.map((s) => s.id));
22
+ for (const step of steps) {
23
+ for (const dep of step.depends_on) {
24
+ if (!stepIds.has(dep)) {
25
+ errors.push(`Step "${step.id}" depends on unknown step "${dep}"`);
26
+ }
27
+ }
28
+ }
29
+ for (const step of steps) {
30
+ const refs = extractStepRefs(step.inputs);
31
+ for (const ref of refs) {
32
+ if (!stepIds.has(ref)) {
33
+ errors.push(`Step "${step.id}" references unknown step "${ref}" in inputs`);
34
+ }
35
+ if (stepIds.has(ref) && !step.depends_on.includes(ref)) {
36
+ errors.push(`Step "${step.id}" references "$steps.${ref}.output" but does not list "${ref}" in depends_on`);
37
+ }
38
+ }
39
+ }
40
+ if (hasCycle(steps)) {
41
+ errors.push("DAG contains a cycle");
42
+ }
43
+ return errors;
44
+ }
45
+ function extractStepRefs(obj) {
46
+ const refs = [];
47
+ const pattern = /\$steps\.([a-z0-9-]+)\.output/g;
48
+ function walk(value) {
49
+ if (typeof value === "string") {
50
+ let match;
51
+ while ((match = pattern.exec(value)) !== null) {
52
+ refs.push(match[1]);
53
+ }
54
+ pattern.lastIndex = 0;
55
+ } else if (Array.isArray(value)) {
56
+ for (const item of value) walk(item);
57
+ } else if (value !== null && typeof value === "object") {
58
+ for (const v of Object.values(value)) walk(v);
59
+ }
60
+ }
61
+ walk(obj);
62
+ return refs;
63
+ }
64
+ function hasCycle(steps) {
65
+ const inDegree = /* @__PURE__ */ new Map();
66
+ const adjacency = /* @__PURE__ */ new Map();
67
+ for (const step of steps) {
68
+ inDegree.set(step.id, 0);
69
+ adjacency.set(step.id, []);
70
+ }
71
+ for (const step of steps) {
72
+ for (const dep of step.depends_on) {
73
+ if (adjacency.has(dep)) {
74
+ adjacency.get(dep).push(step.id);
75
+ inDegree.set(step.id, (inDegree.get(step.id) ?? 0) + 1);
76
+ }
77
+ }
78
+ }
79
+ const queue = [];
80
+ for (const [id, deg] of inDegree) {
81
+ if (deg === 0) queue.push(id);
82
+ }
83
+ let processed = 0;
84
+ while (queue.length > 0) {
85
+ const current = queue.shift();
86
+ processed++;
87
+ for (const neighbor of adjacency.get(current) ?? []) {
88
+ const newDeg = (inDegree.get(neighbor) ?? 1) - 1;
89
+ inDegree.set(neighbor, newDeg);
90
+ if (newDeg === 0) queue.push(neighbor);
91
+ }
92
+ }
93
+ return processed !== steps.length;
94
+ }
95
+
96
+ // src/planner.ts
97
+ var PlanStepSchema = z.object({
98
+ id: z.string(),
99
+ description: z.string(),
100
+ expected_output: z.string().optional(),
101
+ agent: z.literal("claude"),
102
+ inputs: z.record(z.string(), z.any()),
103
+ depends_on: z.array(z.string())
104
+ });
105
+ var TriggerConfigSchema = z.union([
106
+ z.object({
107
+ type: z.literal("poll"),
108
+ interval_seconds: z.number(),
109
+ check_script: z.string(),
110
+ diff_mode: z.enum(["new_items", "any_change"])
111
+ }),
112
+ z.object({
113
+ type: z.literal("cron"),
114
+ expression: z.string(),
115
+ timezone: z.string().optional()
116
+ }),
117
+ z.object({ type: z.literal("manual") })
118
+ ]);
119
+ var FailurePolicySchema = z.object({
120
+ on_step_failure: z.enum(["stop", "skip_dependents", "ask_user"]),
121
+ max_retries: z.number(),
122
+ retry_delay_ms: z.number()
123
+ });
124
+ var PlannerOutputSchema = z.object({
125
+ name: z.string(),
126
+ description: z.string(),
127
+ trigger: TriggerConfigSchema,
128
+ steps: z.array(PlanStepSchema).min(1),
129
+ failure_policy: FailurePolicySchema
130
+ });
131
+ var plannerOutputJsonSchema = {
132
+ type: "object",
133
+ required: ["name", "description", "trigger", "steps", "failure_policy"],
134
+ properties: {
135
+ name: { type: "string", description: "Short workflow name" },
136
+ description: { type: "string", description: "User's original natural language description" },
137
+ trigger: {
138
+ type: "object",
139
+ description: "Trigger configuration",
140
+ properties: {
141
+ type: { type: "string", enum: ["poll", "cron", "manual"] },
142
+ interval_seconds: { type: "number" },
143
+ check_script: { type: "string" },
144
+ diff_mode: { type: "string", enum: ["new_items", "any_change"] },
145
+ expression: { type: "string" },
146
+ timezone: { type: "string" }
147
+ },
148
+ required: ["type"]
149
+ },
150
+ steps: {
151
+ type: "array",
152
+ items: {
153
+ type: "object",
154
+ required: ["id", "description", "agent", "inputs", "depends_on"],
155
+ properties: {
156
+ id: { type: "string", description: "kebab-case step ID" },
157
+ description: { type: "string", description: "Detailed step description for the agent" },
158
+ expected_output: { type: "string" },
159
+ agent: { type: "string", enum: ["claude"] },
160
+ inputs: { type: "object", description: "Step inputs, may use $steps.{id}.output or $trigger_data" },
161
+ depends_on: { type: "array", items: { type: "string" }, description: "Step IDs this step depends on" }
162
+ }
163
+ },
164
+ minItems: 1
165
+ },
166
+ failure_policy: {
167
+ type: "object",
168
+ required: ["on_step_failure", "max_retries", "retry_delay_ms"],
169
+ properties: {
170
+ on_step_failure: { type: "string", enum: ["stop", "skip_dependents", "ask_user"] },
171
+ max_retries: { type: "number" },
172
+ retry_delay_ms: { type: "number" }
173
+ }
174
+ }
175
+ }
176
+ };
177
+ var plannerTool = {
178
+ name: "create_workflow",
179
+ description: "Create a workflow definition from the user description. Generate steps as a DAG with proper dependencies.",
180
+ input_schema: plannerOutputJsonSchema
181
+ };
182
+ var askQuestionTool = {
183
+ name: "ask_question",
184
+ description: "Ask the user a clarifying question to gather more information before creating a workflow. Use this when the user description is ambiguous or missing key details such as trigger frequency, specific repositories, filters, output format, etc.",
185
+ input_schema: {
186
+ type: "object",
187
+ required: ["question"],
188
+ properties: {
189
+ question: { type: "string", description: "The question to ask the user" }
190
+ }
191
+ }
192
+ };
193
+ var setSecretTool = {
194
+ name: "set_secret",
195
+ description: "Store a secret/credential provided by the user (e.g., API token, webhook URL). The value will be saved to the environment for use by workflow steps. Only call this after the user explicitly provides the secret value.",
196
+ input_schema: {
197
+ type: "object",
198
+ required: ["key", "value"],
199
+ properties: {
200
+ key: { type: "string", description: "Environment variable name in UPPER_SNAKE_CASE (e.g., GITHUB_TOKEN)" },
201
+ value: { type: "string", description: "The secret value provided by the user" }
202
+ }
203
+ }
204
+ };
205
+ function parsePlannerToolResponse(response) {
206
+ if (!response.content || !Array.isArray(response.content)) {
207
+ return { type: "error", error: "Unexpected API response: no content array" };
208
+ }
209
+ const askBlock = response.content.find((b) => b.type === "tool_use" && b.name === "ask_question");
210
+ if (askBlock && askBlock.type === "tool_use") {
211
+ const question = askBlock.input?.question;
212
+ return { type: "question", question: question ?? "Could you provide more details?" };
213
+ }
214
+ const secretBlock = response.content.find((b) => b.type === "tool_use" && b.name === "set_secret");
215
+ if (secretBlock && secretBlock.type === "tool_use") {
216
+ const input = secretBlock.input;
217
+ return { type: "set_secret", key: input.key, value: input.value };
218
+ }
219
+ const toolBlock = response.content.find((b) => b.type === "tool_use" && b.name === "create_workflow");
220
+ if (toolBlock && toolBlock.type === "tool_use") {
221
+ const parseResult = PlannerOutputSchema.safeParse(toolBlock.input);
222
+ if (!parseResult.success) {
223
+ const errMsg = parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
224
+ return { type: "error", error: `Invalid plan: ${errMsg}` };
225
+ }
226
+ const dagErrors = validateDAG(parseResult.data.steps);
227
+ if (dagErrors.length > 0) {
228
+ return { type: "error", error: `DAG validation failed: ${dagErrors.join(", ")}` };
229
+ }
230
+ return { type: "plan", plannerOutput: parseResult.data };
231
+ }
232
+ const textBlocks = response.content.filter((b) => b.type === "text");
233
+ if (textBlocks.length > 0) {
234
+ const text = textBlocks.map((b) => b.type === "text" ? b.text : "").join("\n");
235
+ return { type: "text", text };
236
+ }
237
+ return { type: "error", error: "Unexpected planner response format" };
238
+ }
239
+ function buildPlannerSystemPrompt(config) {
240
+ const identity = config.identity?.name ? `
241
+ User identity: ${config.identity.name}` : "";
242
+ return `You are CueClaw Planner. Convert user's natural language into a structured Workflow.
243
+
244
+ ## Execution Environment
245
+
246
+ All steps are executed by the Claude Agent SDK. The agent has Bash and can use
247
+ any CLI tool installed locally. It auto-loads .claude/skills/.
248
+ You don't need to verify tool availability \u2014 the agent detects at runtime.
249
+
250
+ ## Rules
251
+
252
+ 1. Every step's agent field is "claude" (Claude Agent SDK execution)
253
+ 2. Step description uses natural language \u2014 the agent decides which tools to use
254
+ 3. depends_on must reference defined step IDs, forming a valid DAG (no cycles)
255
+ 4. Step IDs use kebab-case: "fetch-issues", "create-branch"
256
+ 5. Use $steps.{id}.output in inputs to reference prior step results
257
+ 6. Use $trigger_data in inputs to reference the trigger's output data
258
+ 7. Do NOT generate position, phase, schema_version, status, or timestamp fields (framework auto-fills). You MUST generate step id fields.
259
+ 8. Trigger check_script can use any shell commands
260
+ 9. Step description should be detailed enough for the agent to execute independently \u2014 not just a one-line summary
261
+ 10. Input keys use snake_case (e.g., issue_number, repo_path)
262
+ 11. Steps must NOT include a status field \u2014 the framework sets all steps to pending automatically
263
+ 12. Step output is a plain text string \u2014 downstream agents parse structure themselves
264
+
265
+ ## Available Credentials
266
+
267
+ ${(() => {
268
+ const keys = getConfiguredSecretKeys();
269
+ if (keys.length > 0) {
270
+ return `The following credentials are configured: ${keys.join(", ")}
271
+ You can reference these in workflow steps \u2014 they are available as environment variables.`;
272
+ }
273
+ return "No credentials are currently configured.";
274
+ })()}
275
+
276
+ 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
+
278
+ ## User Identity
279
+ ${identity}`;
280
+ }
281
+ async function generatePlan(userDescription, config) {
282
+ const anthropic = createAnthropicClient(config);
283
+ const MAX_RETRIES = 2;
284
+ let retryContext = "";
285
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
286
+ const prompt = retryContext ? `${userDescription}
287
+
288
+ ${retryContext}` : userDescription;
289
+ logger.debug({ attempt, hasRetryContext: !!retryContext }, "Planner attempt");
290
+ let response;
291
+ try {
292
+ response = await anthropic.messages.create({
293
+ model: config.claude.planner.model,
294
+ max_tokens: 4096,
295
+ system: buildPlannerSystemPrompt(config),
296
+ messages: [{ role: "user", content: prompt }],
297
+ tools: [plannerTool],
298
+ tool_choice: { type: "tool", name: "create_workflow" }
299
+ });
300
+ } catch (err) {
301
+ const detail = err instanceof Error ? err.message : String(err);
302
+ throw new PlannerError(
303
+ `API request failed: ${detail}. Check your API key and base_url in ~/.cueclaw/config.yaml`
304
+ );
305
+ }
306
+ const rawResponse = response;
307
+ if (rawResponse.type === "error" || rawResponse.error) {
308
+ const errMsg = rawResponse.error?.message ?? JSON.stringify(rawResponse.error ?? rawResponse);
309
+ throw new PlannerError(`API error: ${errMsg}`);
310
+ }
311
+ if (!response.content || !Array.isArray(response.content)) {
312
+ logger.debug({ response: JSON.stringify(response).slice(0, 500) }, "Unexpected API response shape");
313
+ throw new PlannerError(
314
+ `Unexpected API response (no content array). This may indicate an issue with your API provider. Response: ${JSON.stringify(response).slice(0, 200)}`
315
+ );
316
+ }
317
+ const toolBlock = response.content.find((b) => b.type === "tool_use");
318
+ if (!toolBlock || toolBlock.type !== "tool_use") {
319
+ throw new PlannerError("Unexpected planner response format: no tool_use block");
320
+ }
321
+ const parseResult = PlannerOutputSchema.safeParse(toolBlock.input);
322
+ if (!parseResult.success) {
323
+ const errMsg = parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
324
+ if (attempt < MAX_RETRIES) {
325
+ retryContext = `[System] Previous plan had validation issues:
326
+ ${errMsg}
327
+ Please fix and try again.`;
328
+ continue;
329
+ }
330
+ throw new PlannerError(`Invalid plan after ${MAX_RETRIES + 1} attempts: ${errMsg}`);
331
+ }
332
+ const dagErrors = validateDAG(parseResult.data.steps);
333
+ if (dagErrors.length > 0) {
334
+ if (attempt < MAX_RETRIES) {
335
+ retryContext = `[System] DAG dependency issues:
336
+ ${dagErrors.join("\n")}
337
+ Please fix the step dependencies.`;
338
+ continue;
339
+ }
340
+ throw new PlannerError(`DAG validation failed after ${MAX_RETRIES + 1} attempts: ${dagErrors.join(", ")}`);
341
+ }
342
+ const now = (/* @__PURE__ */ new Date()).toISOString();
343
+ return {
344
+ ...parseResult.data,
345
+ schema_version: "1.0",
346
+ id: `wf_${nanoid()}`,
347
+ phase: "awaiting_confirmation",
348
+ created_at: now,
349
+ updated_at: now
350
+ };
351
+ }
352
+ throw new PlannerError("Failed to generate valid plan after retries");
353
+ }
354
+ async function modifyPlan(originalWorkflow, modificationDescription, config) {
355
+ const plannerOutput = {
356
+ name: originalWorkflow.name,
357
+ description: originalWorkflow.description,
358
+ trigger: originalWorkflow.trigger,
359
+ steps: originalWorkflow.steps,
360
+ failure_policy: originalWorkflow.failure_policy
361
+ };
362
+ const combinedPrompt = `Here is the current workflow plan:
363
+ \`\`\`json
364
+ ${JSON.stringify(plannerOutput, null, 2)}
365
+ \`\`\`
366
+
367
+ The user wants to modify it as follows:
368
+ ${modificationDescription}
369
+
370
+ Preserve unmodified steps' IDs, descriptions, and dependencies \u2014 only change what the user specified.
371
+ Return the complete modified workflow using the create_workflow tool.`;
372
+ const result = await generatePlan(combinedPrompt, config);
373
+ return {
374
+ ...result,
375
+ id: originalWorkflow.id,
376
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
377
+ };
378
+ }
379
+ function confirmPlan(workflow) {
380
+ if (workflow.phase !== "awaiting_confirmation") {
381
+ throw new PlannerError(`Cannot confirm workflow in phase "${workflow.phase}"`);
382
+ }
383
+ const nextPhase = workflow.trigger.type === "manual" ? "executing" : "active";
384
+ return {
385
+ ...workflow,
386
+ phase: nextPhase,
387
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
388
+ };
389
+ }
390
+ function rejectPlan(workflow) {
391
+ return {
392
+ ...workflow,
393
+ phase: "planning",
394
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
395
+ };
396
+ }
397
+
398
+ export {
399
+ PlannerOutputSchema,
400
+ plannerTool,
401
+ askQuestionTool,
402
+ setSecretTool,
403
+ parsePlannerToolResponse,
404
+ buildPlannerSystemPrompt,
405
+ generatePlan,
406
+ modifyPlan,
407
+ confirmPlan,
408
+ rejectPlan
409
+ };
@@ -0,0 +1,59 @@
1
+ // src/env.ts
2
+ import { readFileSync, writeFileSync, existsSync } from "fs";
3
+ import { resolve, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { parse } from "dotenv";
6
+ var secrets = {};
7
+ var isDev = !import.meta.url.includes("/dist/");
8
+ function loadSecrets() {
9
+ if (!isDev) return;
10
+ const thisDir = dirname(fileURLToPath(import.meta.url));
11
+ const envPath = resolve(thisDir, "..", ".env");
12
+ if (!existsSync(envPath)) return;
13
+ const content = readFileSync(envPath, "utf-8");
14
+ secrets = parse(content);
15
+ for (const [key, value] of Object.entries(secrets)) {
16
+ if (process.env[key] === void 0) {
17
+ process.env[key] = value;
18
+ }
19
+ }
20
+ }
21
+ function getSecret(key) {
22
+ return secrets[key] ?? process.env[key];
23
+ }
24
+ function hasSecret(key) {
25
+ return getSecret(key) !== void 0;
26
+ }
27
+ function getConfiguredSecretKeys() {
28
+ const patterns = [/_TOKEN$/, /_API_KEY$/, /_SECRET$/, /_WEBHOOK/, /_PASSWORD$/];
29
+ return Object.keys(process.env).filter((k) => patterns.some((p) => p.test(k)) && process.env[k]).sort();
30
+ }
31
+ function writeEnvVar(key, value) {
32
+ if (!isDev) throw new Error("writeEnvVar should only be called in dev mode");
33
+ const thisDir = dirname(fileURLToPath(import.meta.url));
34
+ const envPath = resolve(thisDir, "..", ".env");
35
+ let lines = [];
36
+ if (existsSync(envPath)) {
37
+ lines = readFileSync(envPath, "utf-8").split("\n");
38
+ }
39
+ const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
40
+ const entry = `${key}=${value}`;
41
+ if (idx >= 0) {
42
+ lines[idx] = entry;
43
+ } else {
44
+ lines.push(entry);
45
+ }
46
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
47
+ writeFileSync(envPath, lines.join("\n") + "\n", "utf-8");
48
+ process.env[key] = value;
49
+ secrets[key] = value;
50
+ }
51
+
52
+ export {
53
+ isDev,
54
+ loadSecrets,
55
+ getSecret,
56
+ hasSecret,
57
+ getConfiguredSecretKeys,
58
+ writeEnvVar
59
+ };