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.
@@ -4,316 +4,21 @@ import {
4
4
  updateStepRunStatus,
5
5
  updateWorkflowPhase,
6
6
  updateWorkflowRunStatus
7
- } from "./chunk-K4PGB2DU.js";
7
+ } from "./chunk-G43R5ASK.js";
8
8
  import {
9
- ConfigError,
10
- ExecutorError,
11
- PlannerError,
12
9
  cueclawHome,
13
10
  loadConfig
14
- } from "./chunk-JRHM3Z4C.js";
11
+ } from "./chunk-RSKXBXSJ.js";
12
+ import {
13
+ ConfigError,
14
+ ExecutorError
15
+ } from "./chunk-BVQG3WYO.js";
15
16
  import {
16
17
  logger
17
- } from "./chunk-E7BP6DMO.js";
18
-
19
- // src/planner.ts
20
- import Anthropic from "@anthropic-ai/sdk";
21
- import { z } from "zod/v4";
22
- import { nanoid } from "nanoid";
23
-
24
- // src/workflow.ts
25
- function validateDAG(steps) {
26
- const errors = [];
27
- const stepIds = new Set(steps.map((s) => s.id));
28
- for (const step of steps) {
29
- for (const dep of step.depends_on) {
30
- if (!stepIds.has(dep)) {
31
- errors.push(`Step "${step.id}" depends on unknown step "${dep}"`);
32
- }
33
- }
34
- }
35
- for (const step of steps) {
36
- const refs = extractStepRefs(step.inputs);
37
- for (const ref of refs) {
38
- if (!stepIds.has(ref)) {
39
- errors.push(`Step "${step.id}" references unknown step "${ref}" in inputs`);
40
- }
41
- if (stepIds.has(ref) && !step.depends_on.includes(ref)) {
42
- errors.push(`Step "${step.id}" references "$steps.${ref}.output" but does not list "${ref}" in depends_on`);
43
- }
44
- }
45
- }
46
- if (hasCycle(steps)) {
47
- errors.push("DAG contains a cycle");
48
- }
49
- return errors;
50
- }
51
- function extractStepRefs(obj) {
52
- const refs = [];
53
- const pattern = /\$steps\.([a-z0-9-]+)\.output/g;
54
- function walk(value) {
55
- if (typeof value === "string") {
56
- let match;
57
- while ((match = pattern.exec(value)) !== null) {
58
- refs.push(match[1]);
59
- }
60
- pattern.lastIndex = 0;
61
- } else if (Array.isArray(value)) {
62
- for (const item of value) walk(item);
63
- } else if (value !== null && typeof value === "object") {
64
- for (const v of Object.values(value)) walk(v);
65
- }
66
- }
67
- walk(obj);
68
- return refs;
69
- }
70
- function hasCycle(steps) {
71
- const inDegree = /* @__PURE__ */ new Map();
72
- const adjacency = /* @__PURE__ */ new Map();
73
- for (const step of steps) {
74
- inDegree.set(step.id, 0);
75
- adjacency.set(step.id, []);
76
- }
77
- for (const step of steps) {
78
- for (const dep of step.depends_on) {
79
- if (adjacency.has(dep)) {
80
- adjacency.get(dep).push(step.id);
81
- inDegree.set(step.id, (inDegree.get(step.id) ?? 0) + 1);
82
- }
83
- }
84
- }
85
- const queue = [];
86
- for (const [id, deg] of inDegree) {
87
- if (deg === 0) queue.push(id);
88
- }
89
- let processed = 0;
90
- while (queue.length > 0) {
91
- const current = queue.shift();
92
- processed++;
93
- for (const neighbor of adjacency.get(current) ?? []) {
94
- const newDeg = (inDegree.get(neighbor) ?? 1) - 1;
95
- inDegree.set(neighbor, newDeg);
96
- if (newDeg === 0) queue.push(neighbor);
97
- }
98
- }
99
- return processed !== steps.length;
100
- }
101
-
102
- // src/planner.ts
103
- var PlanStepSchema = z.object({
104
- id: z.string(),
105
- description: z.string(),
106
- expected_output: z.string().optional(),
107
- agent: z.literal("claude"),
108
- inputs: z.record(z.string(), z.any()),
109
- depends_on: z.array(z.string())
110
- });
111
- var TriggerConfigSchema = z.union([
112
- z.object({
113
- type: z.literal("poll"),
114
- interval_seconds: z.number(),
115
- check_script: z.string(),
116
- diff_mode: z.enum(["new_items", "any_change"])
117
- }),
118
- z.object({
119
- type: z.literal("cron"),
120
- expression: z.string(),
121
- timezone: z.string().optional()
122
- }),
123
- z.object({ type: z.literal("manual") })
124
- ]);
125
- var FailurePolicySchema = z.object({
126
- on_step_failure: z.enum(["stop", "skip_dependents", "ask_user"]),
127
- max_retries: z.number(),
128
- retry_delay_ms: z.number()
129
- });
130
- var PlannerOutputSchema = z.object({
131
- name: z.string(),
132
- description: z.string(),
133
- trigger: TriggerConfigSchema,
134
- steps: z.array(PlanStepSchema).min(1),
135
- failure_policy: FailurePolicySchema
136
- });
137
- var plannerOutputJsonSchema = {
138
- type: "object",
139
- required: ["name", "description", "trigger", "steps", "failure_policy"],
140
- properties: {
141
- name: { type: "string", description: "Short workflow name" },
142
- description: { type: "string", description: "User's original natural language description" },
143
- trigger: {
144
- type: "object",
145
- description: "Trigger configuration",
146
- properties: {
147
- type: { type: "string", enum: ["poll", "cron", "manual"] },
148
- interval_seconds: { type: "number" },
149
- check_script: { type: "string" },
150
- diff_mode: { type: "string", enum: ["new_items", "any_change"] },
151
- expression: { type: "string" },
152
- timezone: { type: "string" }
153
- },
154
- required: ["type"]
155
- },
156
- steps: {
157
- type: "array",
158
- items: {
159
- type: "object",
160
- required: ["id", "description", "agent", "inputs", "depends_on"],
161
- properties: {
162
- id: { type: "string", description: "kebab-case step ID" },
163
- description: { type: "string", description: "Detailed step description for the agent" },
164
- expected_output: { type: "string" },
165
- agent: { type: "string", enum: ["claude"] },
166
- inputs: { type: "object", description: "Step inputs, may use $steps.{id}.output or $trigger_data" },
167
- depends_on: { type: "array", items: { type: "string" }, description: "Step IDs this step depends on" }
168
- }
169
- },
170
- minItems: 1
171
- },
172
- failure_policy: {
173
- type: "object",
174
- required: ["on_step_failure", "max_retries", "retry_delay_ms"],
175
- properties: {
176
- on_step_failure: { type: "string", enum: ["stop", "skip_dependents", "ask_user"] },
177
- max_retries: { type: "number" },
178
- retry_delay_ms: { type: "number" }
179
- }
180
- }
181
- }
182
- };
183
- var plannerTool = {
184
- name: "create_workflow",
185
- description: "Create a workflow definition from the user description. Generate steps as a DAG with proper dependencies.",
186
- input_schema: plannerOutputJsonSchema
187
- };
188
- function buildPlannerSystemPrompt(config) {
189
- const identity = config.identity?.name ? `
190
- User identity: ${config.identity.name}` : "";
191
- return `You are CueClaw Planner. Convert user's natural language into a structured Workflow.
192
-
193
- ## Execution Environment
194
-
195
- All steps are executed by the Claude Agent SDK. The agent has Bash and can use
196
- any CLI tool installed locally. It auto-loads .claude/skills/.
197
- You don't need to verify tool availability \u2014 the agent detects at runtime.
198
-
199
- ## Rules
200
-
201
- 1. Every step's agent field is "claude" (Claude Agent SDK execution)
202
- 2. Step description uses natural language \u2014 the agent decides which tools to use
203
- 3. depends_on must reference defined step IDs, forming a valid DAG (no cycles)
204
- 4. Step IDs use kebab-case: "fetch-issues", "create-branch"
205
- 5. Use $steps.{id}.output in inputs to reference prior step results
206
- 6. Use $trigger_data in inputs to reference the trigger's output data
207
- 7. Do NOT generate position, phase, schema_version, status, or timestamp fields (framework auto-fills). You MUST generate step id fields.
208
- 8. Trigger check_script can use any shell commands
209
- 9. Step description should be detailed enough for the agent to execute independently \u2014 not just a one-line summary
210
- 10. Input keys use snake_case (e.g., issue_number, repo_path)
211
- 11. Steps must NOT include a status field \u2014 the framework sets all steps to pending automatically
212
- 12. Step output is a plain text string \u2014 downstream agents parse structure themselves
213
-
214
- ## User Identity
215
- ${identity}`;
216
- }
217
- async function generatePlan(userDescription, config) {
218
- const anthropic = new Anthropic({ apiKey: config.claude.api_key });
219
- const MAX_RETRIES = 2;
220
- let retryContext = "";
221
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
222
- const prompt = retryContext ? `${userDescription}
223
-
224
- ${retryContext}` : userDescription;
225
- logger.debug({ attempt, hasRetryContext: !!retryContext }, "Planner attempt");
226
- const response = await anthropic.messages.create({
227
- model: config.claude.planner.model,
228
- max_tokens: 4096,
229
- system: buildPlannerSystemPrompt(config),
230
- messages: [{ role: "user", content: prompt }],
231
- tools: [plannerTool],
232
- tool_choice: { type: "tool", name: "create_workflow" }
233
- });
234
- const toolBlock = response.content.find((b) => b.type === "tool_use");
235
- if (!toolBlock || toolBlock.type !== "tool_use") {
236
- throw new PlannerError("Unexpected planner response format: no tool_use block");
237
- }
238
- const parseResult = PlannerOutputSchema.safeParse(toolBlock.input);
239
- if (!parseResult.success) {
240
- const errMsg = parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
241
- if (attempt < MAX_RETRIES) {
242
- retryContext = `[System] Previous plan had validation issues:
243
- ${errMsg}
244
- Please fix and try again.`;
245
- continue;
246
- }
247
- throw new PlannerError(`Invalid plan after ${MAX_RETRIES + 1} attempts: ${errMsg}`);
248
- }
249
- const dagErrors = validateDAG(parseResult.data.steps);
250
- if (dagErrors.length > 0) {
251
- if (attempt < MAX_RETRIES) {
252
- retryContext = `[System] DAG dependency issues:
253
- ${dagErrors.join("\n")}
254
- Please fix the step dependencies.`;
255
- continue;
256
- }
257
- throw new PlannerError(`DAG validation failed after ${MAX_RETRIES + 1} attempts: ${dagErrors.join(", ")}`);
258
- }
259
- const now = (/* @__PURE__ */ new Date()).toISOString();
260
- return {
261
- ...parseResult.data,
262
- schema_version: "1.0",
263
- id: `wf_${nanoid()}`,
264
- phase: "awaiting_confirmation",
265
- created_at: now,
266
- updated_at: now
267
- };
268
- }
269
- throw new PlannerError("Failed to generate valid plan after retries");
270
- }
271
- async function modifyPlan(originalWorkflow, modificationDescription, config) {
272
- const plannerOutput = {
273
- name: originalWorkflow.name,
274
- description: originalWorkflow.description,
275
- trigger: originalWorkflow.trigger,
276
- steps: originalWorkflow.steps,
277
- failure_policy: originalWorkflow.failure_policy
278
- };
279
- const combinedPrompt = `Here is the current workflow plan:
280
- \`\`\`json
281
- ${JSON.stringify(plannerOutput, null, 2)}
282
- \`\`\`
283
-
284
- The user wants to modify it as follows:
285
- ${modificationDescription}
286
-
287
- Preserve unmodified steps' IDs, descriptions, and dependencies \u2014 only change what the user specified.
288
- Return the complete modified workflow using the create_workflow tool.`;
289
- const result = await generatePlan(combinedPrompt, config);
290
- return {
291
- ...result,
292
- id: originalWorkflow.id,
293
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
294
- };
295
- }
296
- function confirmPlan(workflow) {
297
- if (workflow.phase !== "awaiting_confirmation") {
298
- throw new PlannerError(`Cannot confirm workflow in phase "${workflow.phase}"`);
299
- }
300
- const nextPhase = workflow.trigger.type === "manual" ? "executing" : "active";
301
- return {
302
- ...workflow,
303
- phase: nextPhase,
304
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
305
- };
306
- }
307
- function rejectPlan(workflow) {
308
- return {
309
- ...workflow,
310
- phase: "planning",
311
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
312
- };
313
- }
18
+ } from "./chunk-QBOYMF4A.js";
314
19
 
315
20
  // src/executor.ts
316
- import { nanoid as nanoid4 } from "nanoid";
21
+ import { nanoid as nanoid3 } from "nanoid";
317
22
 
318
23
  // src/hooks.ts
319
24
  function checkBashSafety(command) {
@@ -402,7 +107,7 @@ import {
402
107
  mkdirSync
403
108
  } from "fs";
404
109
  import { join as join2 } from "path";
405
- import { nanoid as nanoid2 } from "nanoid";
110
+ import { nanoid } from "nanoid";
406
111
  var IpcWatcher = class {
407
112
  constructor(workflowId, _stepId, ipcDir, onMessage, pollInterval = 500) {
408
113
  this.workflowId = workflowId;
@@ -451,7 +156,7 @@ var IpcWatcher = class {
451
156
  sendToContainer(msg) {
452
157
  const inputDir = join2(this.ipcDir, "input");
453
158
  mkdirSync(inputDir, { recursive: true });
454
- const filename = `${Date.now()}-${nanoid2(6)}.json`;
159
+ const filename = `${Date.now()}-${nanoid(6)}.json`;
455
160
  const tempPath = join2(inputDir, `.${filename}.tmp`);
456
161
  const finalPath = join2(inputDir, filename);
457
162
  writeFileSync2(tempPath, JSON.stringify(msg));
@@ -744,8 +449,17 @@ function runAgent(opts) {
744
449
  }
745
450
  let aborted = false;
746
451
  const resultPromise = (async () => {
452
+ const authToken = config.claude.executor.api_key ?? config.claude.api_key;
453
+ const baseUrl = config.claude.executor.base_url ?? config.claude.base_url;
454
+ const prevAuthToken = process.env["ANTHROPIC_AUTH_TOKEN"];
455
+ const prevBaseUrl = process.env["ANTHROPIC_BASE_URL"];
456
+ process.env["ANTHROPIC_AUTH_TOKEN"] = authToken;
457
+ if (baseUrl !== "https://api.anthropic.com") {
458
+ process.env["ANTHROPIC_BASE_URL"] = baseUrl;
459
+ }
747
460
  try {
748
461
  const { query } = await import("@anthropic-ai/claude-agent-sdk");
462
+ const permMode = config.claude.executor.skip_permissions ? "dangerously-skip-permissions" : "default";
749
463
  const q = query({
750
464
  prompt: opts.prompt,
751
465
  options: {
@@ -763,7 +477,7 @@ function runAgent(opts) {
763
477
  "WebFetch"
764
478
  ],
765
479
  settingSources: ["project"],
766
- permissionMode: "default"
480
+ permissionMode: permMode
767
481
  }
768
482
  });
769
483
  let sessionId;
@@ -802,6 +516,11 @@ function runAgent(opts) {
802
516
  const errorMsg = err instanceof Error ? err.message : String(err);
803
517
  logger.error({ err, stepId: opts.stepId }, "Agent execution failed");
804
518
  return { status: "failed", error: errorMsg };
519
+ } finally {
520
+ if (prevAuthToken !== void 0) process.env["ANTHROPIC_AUTH_TOKEN"] = prevAuthToken;
521
+ else delete process.env["ANTHROPIC_AUTH_TOKEN"];
522
+ if (prevBaseUrl !== void 0) process.env["ANTHROPIC_BASE_URL"] = prevBaseUrl;
523
+ else delete process.env["ANTHROPIC_BASE_URL"];
805
524
  }
806
525
  })();
807
526
  return {
@@ -813,11 +532,11 @@ function runAgent(opts) {
813
532
  }
814
533
 
815
534
  // src/session.ts
816
- import { nanoid as nanoid3 } from "nanoid";
535
+ import { nanoid as nanoid2 } from "nanoid";
817
536
  function createSession(db, stepRunId, sdkSessionId) {
818
537
  const now = (/* @__PURE__ */ new Date()).toISOString();
819
538
  const session = {
820
- id: `sess_${nanoid3()}`,
539
+ id: `sess_${nanoid2()}`,
821
540
  step_run_id: stepRunId,
822
541
  sdk_session_id: sdkSessionId,
823
542
  created_at: now,
@@ -881,7 +600,7 @@ function hasSkipMarker(inputs) {
881
600
  return false;
882
601
  }
883
602
  async function executeStepOnce(step, resolvedInputs, runId, db, cwd, onProgress) {
884
- const stepRunId = `sr_${nanoid4()}`;
603
+ const stepRunId = `sr_${nanoid3()}`;
885
604
  const now = (/* @__PURE__ */ new Date()).toISOString();
886
605
  const stepRun = {
887
606
  id: stepRunId,
@@ -927,7 +646,7 @@ async function executeStepWithRetry(step, resolvedInputs, runId, db, cwd, policy
927
646
  }
928
647
  async function executeWorkflow(opts) {
929
648
  const { workflow, triggerData, db, cwd, onStepFailure, onProgress, signal } = opts;
930
- const runId = `run_${nanoid4()}`;
649
+ const runId = `run_${nanoid3()}`;
931
650
  const run = {
932
651
  id: runId,
933
652
  workflow_id: workflow.id,
@@ -946,6 +665,7 @@ async function executeWorkflow(opts) {
946
665
  if (signal?.aborted) {
947
666
  for (const id of remaining) {
948
667
  completed.set(id, { status: "skipped", error: "Aborted" });
668
+ onProgress?.(id, { status: "skipped" });
949
669
  }
950
670
  remaining.clear();
951
671
  runFailed = true;
@@ -967,7 +687,7 @@ async function executeWorkflow(opts) {
967
687
  if (depsFailed && workflow.failure_policy.on_step_failure !== "ask_user") {
968
688
  remaining.delete(id);
969
689
  completed.set(id, { status: "skipped" });
970
- const skipRunId = `sr_${nanoid4()}`;
690
+ const skipRunId = `sr_${nanoid3()}`;
971
691
  insertStepRun(db, { id: skipRunId, run_id: runId, step_id: id, status: "skipped" });
972
692
  continue;
973
693
  }
@@ -975,7 +695,7 @@ async function executeWorkflow(opts) {
975
695
  if (hasSkipMarker(resolvedInputs)) {
976
696
  remaining.delete(id);
977
697
  completed.set(id, { status: "skipped" });
978
- const skipRunId = `sr_${nanoid4()}`;
698
+ const skipRunId = `sr_${nanoid3()}`;
979
699
  insertStepRun(db, { id: skipRunId, run_id: runId, step_id: id, status: "skipped" });
980
700
  continue;
981
701
  }
@@ -1005,7 +725,7 @@ async function executeWorkflow(opts) {
1005
725
  if (policy === "stop") {
1006
726
  for (const remainingId of remaining) {
1007
727
  completed.set(remainingId, { status: "skipped" });
1008
- const skipRunId = `sr_${nanoid4()}`;
728
+ const skipRunId = `sr_${nanoid3()}`;
1009
729
  insertStepRun(db, { id: skipRunId, run_id: runId, step_id: remainingId, status: "skipped" });
1010
730
  }
1011
731
  remaining.clear();
@@ -1017,7 +737,7 @@ async function executeWorkflow(opts) {
1017
737
  if (decision === "stop") {
1018
738
  for (const remainingId of remaining) {
1019
739
  completed.set(remainingId, { status: "skipped" });
1020
- const skipRunId = `sr_${nanoid4()}`;
740
+ const skipRunId = `sr_${nanoid3()}`;
1021
741
  insertStepRun(db, { id: skipRunId, run_id: runId, step_id: remainingId, status: "skipped" });
1022
742
  }
1023
743
  remaining.clear();
@@ -1043,9 +763,7 @@ async function executeWorkflow(opts) {
1043
763
  }
1044
764
 
1045
765
  export {
1046
- generatePlan,
1047
- modifyPlan,
1048
- confirmPlan,
1049
- rejectPlan,
766
+ resolveValue,
767
+ resolveInputs,
1050
768
  executeWorkflow
1051
769
  };
@@ -1,18 +1,9 @@
1
- import {
2
- MessageRouter
3
- } from "./chunk-GMHDL4CG.js";
4
1
  import {
5
2
  executeWorkflow
6
- } from "./chunk-D77G7ABJ.js";
7
- import {
8
- initDb
9
- } from "./chunk-K4PGB2DU.js";
10
- import {
11
- loadConfig
12
- } from "./chunk-JRHM3Z4C.js";
3
+ } from "./chunk-SEYPA5M2.js";
13
4
  import {
14
5
  logger
15
- } from "./chunk-E7BP6DMO.js";
6
+ } from "./chunk-QBOYMF4A.js";
16
7
 
17
8
  // src/trigger-loop.ts
18
9
  import { CronExpressionParser } from "cron-parser";
@@ -233,76 +224,6 @@ var TriggerLoop = class {
233
224
  }
234
225
  };
235
226
 
236
- // src/daemon.ts
237
- async function startDaemon() {
238
- const config = loadConfig();
239
- const db = initDb();
240
- const cwd = process.cwd();
241
- const router = new MessageRouter(db, config, cwd);
242
- if (config.whatsapp?.enabled) {
243
- try {
244
- const { WhatsAppChannel } = await import("./whatsapp-36XWDSJ5.js");
245
- const wa = new WhatsAppChannel(
246
- config.whatsapp.auth_dir ?? `${process.env["HOME"]}/.cueclaw/auth/whatsapp`,
247
- config.whatsapp.allowed_jids ?? [],
248
- (jid, msg) => router.handleInbound("whatsapp", jid, msg)
249
- );
250
- router.registerChannel(wa);
251
- await wa.connect();
252
- logger.info("WhatsApp channel started");
253
- } catch (err) {
254
- logger.error({ err }, "Failed to start WhatsApp channel");
255
- }
256
- }
257
- if (config.telegram?.enabled) {
258
- try {
259
- const { TelegramChannel } = await import("./telegram-BTTWEETO.js");
260
- const tg = new TelegramChannel(
261
- config.telegram.token,
262
- config.telegram.allowed_users ?? [],
263
- (jid, msg) => router.handleInbound("telegram", jid, msg)
264
- );
265
- router.registerChannel(tg);
266
- await tg.connect();
267
- logger.info("Telegram channel started");
268
- } catch (err) {
269
- logger.error({ err }, "Failed to start Telegram channel");
270
- }
271
- }
272
- await recoverRunningWorkflows(db, router);
273
- const maxConcurrent = 5;
274
- const triggerLoop = new TriggerLoop(db, router, cwd, maxConcurrent);
275
- triggerLoop.start();
276
- router.start();
277
- logger.info("Daemon started");
278
- const shutdown = () => {
279
- logger.info("Shutting down daemon...");
280
- triggerLoop.stop();
281
- router.stop();
282
- db.close();
283
- process.exit(0);
284
- };
285
- process.on("SIGTERM", shutdown);
286
- process.on("SIGINT", shutdown);
287
- }
288
- async function recoverRunningWorkflows(db, router) {
289
- const interruptedRuns = db.prepare(
290
- "SELECT id, workflow_id FROM workflow_runs WHERE status = 'running'"
291
- ).all();
292
- if (interruptedRuns.length === 0) return;
293
- logger.warn({ count: interruptedRuns.length }, "Recovering interrupted workflow runs");
294
- for (const run of interruptedRuns) {
295
- db.prepare(
296
- "UPDATE workflow_runs SET status = 'failed', error = ?, completed_at = ? WHERE id = ?"
297
- ).run("Daemon restarted during execution", (/* @__PURE__ */ new Date()).toISOString(), run.id);
298
- db.prepare(
299
- "UPDATE step_runs SET status = 'failed', error = ? WHERE run_id = ? AND status = 'running'"
300
- ).run("Daemon restarted during execution", run.id);
301
- router.broadcastNotification(
302
- `Workflow ${run.workflow_id} was interrupted by daemon restart (run ${run.id})`
303
- );
304
- }
305
- }
306
227
  export {
307
- startDaemon
228
+ TriggerLoop
308
229
  };