chainlesschain 0.45.81 → 0.47.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.
Files changed (71) hide show
  1. package/README.md +10 -0
  2. package/bin/chainlesschain.js +0 -0
  3. package/package.json +1 -1
  4. package/src/assets/web-panel/.build-hash +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-C1AnPdMx.js → Analytics-DgypYeUB.js} +2 -2
  6. package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +1 -0
  7. package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +1 -0
  8. package/src/assets/web-panel/assets/{Backup-D31iZX3l.js → Backup-Ba9UybpT.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-DiXJ3TuK.js → Chat-BwXskT21.js} +1 -1
  10. package/src/assets/web-panel/assets/Cowork-CXuhlHew.css +1 -0
  11. package/src/assets/web-panel/assets/Cowork-UmOe7qvE.js +7 -0
  12. package/src/assets/web-panel/assets/{Cron-DBt1ueXh.js → Cron-JHS-rc-4.js} +2 -2
  13. package/src/assets/web-panel/assets/{Dashboard-HPh9FcPt.js → Dashboard-B95cMCO7.js} +2 -2
  14. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
  15. package/src/assets/web-panel/assets/{Git-hwQ1oZHj.js → Git-CSYO0_zk.js} +2 -2
  16. package/src/assets/web-panel/assets/{Logs-4D9p6PRM.js → Logs-Hxw_K0km.js} +2 -2
  17. package/src/assets/web-panel/assets/{McpTools-CyAUjbbs.js → McpTools-DIE75TrB.js} +2 -2
  18. package/src/assets/web-panel/assets/{Memory-BMqOR7S-.js → Memory-C4KVnLlp.js} +2 -2
  19. package/src/assets/web-panel/assets/{Notes-Cmas8i4E.js → Notes-DuzrHMAk.js} +2 -2
  20. package/src/assets/web-panel/assets/{Organization-DnSa58Tl.js → Organization-DTq6uF82.js} +4 -4
  21. package/src/assets/web-panel/assets/{P2P-BxksIBWs.js → P2P-C0hjlhsR.js} +2 -2
  22. package/src/assets/web-panel/assets/{Permissions-Bq5Qn2s3.js → Permissions-Ec0NH-xC.js} +4 -4
  23. package/src/assets/web-panel/assets/{Projects-B7EM0uPg.js → Projects-U8D0asCS.js} +2 -2
  24. package/src/assets/web-panel/assets/{Providers-DAwgG5KV.js → Providers-BngtTLvJ.js} +2 -2
  25. package/src/assets/web-panel/assets/{RssFeed-HSZoRXvS.js → RssFeed-B9NbwCKM.js} +3 -3
  26. package/src/assets/web-panel/assets/{Security-Cz17qBny.js → Security-BL5Rkr1T.js} +3 -3
  27. package/src/assets/web-panel/assets/{Services-D2EsLq-v.js → Services-D4MJzLld.js} +2 -2
  28. package/src/assets/web-panel/assets/{Skills-C9v-f3vZ.js → Skills-CQTOMDwF.js} +1 -1
  29. package/src/assets/web-panel/assets/{Tasks-yMEcU0n7.js → Tasks-DepbJMnL.js} +1 -1
  30. package/src/assets/web-panel/assets/{Templates-l7SvlKuB.js → Templates-C24PVZPu.js} +1 -1
  31. package/src/assets/web-panel/assets/{Wallet-BHWhLWn9.js → Wallet-PQoSpN_P.js} +3 -3
  32. package/src/assets/web-panel/assets/{WebAuthn-kWhFYaUK.js → WebAuthn-BcuyQ4Lr.js} +4 -4
  33. package/src/assets/web-panel/assets/WorkflowEditor-C-SvXbHW.js +1 -0
  34. package/src/assets/web-panel/assets/WorkflowEditor-D5bX6woe.css +1 -0
  35. package/src/assets/web-panel/assets/{antd-D6h4fDFf.js → antd-DEjZPGMj.js} +82 -82
  36. package/src/assets/web-panel/assets/index-CwvzTTw_.js +2 -0
  37. package/src/assets/web-panel/assets/{markdown-BZsB-Dsv.js → markdown-CusdXFxb.js} +1 -1
  38. package/src/assets/web-panel/index.html +2 -2
  39. package/src/commands/cowork.js +867 -0
  40. package/src/gateways/ws/action-protocol.js +182 -2
  41. package/src/gateways/ws/message-dispatcher.js +5 -0
  42. package/src/gateways/ws/ws-server.js +21 -0
  43. package/src/lib/cowork-cron.js +474 -0
  44. package/src/lib/cowork-evomap-adapter.js +121 -0
  45. package/src/lib/cowork-learning.js +438 -0
  46. package/src/lib/cowork-mcp-tools.js +182 -0
  47. package/src/lib/cowork-observe-html.js +108 -0
  48. package/src/lib/cowork-observe.js +160 -0
  49. package/src/lib/cowork-share.js +322 -0
  50. package/src/lib/cowork-task-runner.js +317 -3
  51. package/src/lib/cowork-task-templates.js +101 -13
  52. package/src/lib/cowork-template-marketplace.js +205 -0
  53. package/src/lib/cowork-workflow.js +571 -0
  54. package/src/lib/provider-options.js +133 -0
  55. package/src/lib/skill-loader.js +65 -0
  56. package/src/lib/sub-agent-context.js +54 -2
  57. package/src/lib/sub-agent-profiles.js +164 -0
  58. package/src/lib/todo-manager.js +108 -0
  59. package/src/lib/turn-context.js +95 -0
  60. package/src/lib/web-fetch.js +224 -0
  61. package/src/lib/workflow-expr.js +318 -0
  62. package/src/repl/agent-repl.js +4 -0
  63. package/src/runtime/agent-core.js +135 -3
  64. package/src/runtime/coding-agent-contract-shared.cjs +131 -0
  65. package/src/runtime/coding-agent-policy.cjs +30 -0
  66. package/src/assets/web-panel/assets/AppLayout-YdvJBMHH.js +0 -1
  67. package/src/assets/web-panel/assets/AppLayout-cxfKLu-m.css +0 -1
  68. package/src/assets/web-panel/assets/Cowork-BnrHWwZw.js +0 -7
  69. package/src/assets/web-panel/assets/Cowork-CcSoS3eX.css +0 -1
  70. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
  71. package/src/assets/web-panel/assets/index-ByUk2Wmr.js +0 -2
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Web Fetch — agent-safe HTTP(S) GET with allowlist + size/timeout limits.
3
+ *
4
+ * Inspired by open-agents web_fetch tool. Guards against SSRF by rejecting
5
+ * private/loopback hosts unless explicitly allowlisted.
6
+ */
7
+
8
+ import http from "http";
9
+ import https from "https";
10
+ import { URL } from "url";
11
+
12
+ const DEFAULT_MAX_BYTES = 2_000_000;
13
+ const DEFAULT_TIMEOUT_MS = 15_000;
14
+
15
+ // RFC 1918 + loopback + link-local. Blocked by default unless config.allowPrivateHosts.
16
+ const PRIVATE_HOST_PATTERNS = [
17
+ /^127\./,
18
+ /^10\./,
19
+ /^192\.168\./,
20
+ /^172\.(1[6-9]|2\d|3[01])\./,
21
+ /^169\.254\./,
22
+ /^0\.0\.0\.0$/,
23
+ /^::1$/,
24
+ /^localhost$/i,
25
+ ];
26
+
27
+ export function isPrivateHost(host) {
28
+ if (!host) return true;
29
+ return PRIVATE_HOST_PATTERNS.some((re) => re.test(host));
30
+ }
31
+
32
+ export function checkAllowed(urlStr, config = {}) {
33
+ let parsed;
34
+ try {
35
+ parsed = new URL(urlStr);
36
+ } catch {
37
+ return { allowed: false, reason: "invalid URL" };
38
+ }
39
+ if (!/^https?:$/.test(parsed.protocol)) {
40
+ return {
41
+ allowed: false,
42
+ reason: `unsupported protocol: ${parsed.protocol}`,
43
+ };
44
+ }
45
+ if (isPrivateHost(parsed.hostname) && !config.allowPrivateHosts) {
46
+ return {
47
+ allowed: false,
48
+ reason: `private/loopback host blocked: ${parsed.hostname}`,
49
+ };
50
+ }
51
+ const allowed = Array.isArray(config.allowedDomains)
52
+ ? config.allowedDomains
53
+ : ["*"];
54
+ if (!allowed.includes("*")) {
55
+ const match = allowed.some((pattern) => {
56
+ if (pattern.startsWith("*.")) {
57
+ return parsed.hostname.endsWith(pattern.slice(1));
58
+ }
59
+ return parsed.hostname === pattern;
60
+ });
61
+ if (!match) {
62
+ return {
63
+ allowed: false,
64
+ reason: `host not in allowedDomains: ${parsed.hostname}`,
65
+ };
66
+ }
67
+ }
68
+ return { allowed: true, url: parsed };
69
+ }
70
+
71
+ export function htmlToMarkdown(html) {
72
+ if (!html) return "";
73
+ let text = String(html);
74
+ // Strip scripts/styles entirely
75
+ text = text.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
76
+ text = text.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "");
77
+ text = text.replace(/<!--[\s\S]*?-->/g, "");
78
+ // Convert headings
79
+ text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, n, inner) => {
80
+ return `\n\n${"#".repeat(Number(n))} ${inner.trim()}\n\n`;
81
+ });
82
+ // Paragraphs / breaks
83
+ text = text.replace(/<br\s*\/?>/gi, "\n");
84
+ text = text.replace(/<\/p>/gi, "\n\n");
85
+ // Links: [text](href)
86
+ text = text.replace(
87
+ /<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi,
88
+ (_, href, inner) => `[${stripTags(inner).trim()}](${href})`,
89
+ );
90
+ // List items
91
+ text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, inner) => {
92
+ return `- ${stripTags(inner).trim()}\n`;
93
+ });
94
+ // Strip remaining tags
95
+ text = stripTags(text);
96
+ // Decode minimal HTML entities
97
+ text = text
98
+ .replace(/&nbsp;/g, " ")
99
+ .replace(/&amp;/g, "&")
100
+ .replace(/&lt;/g, "<")
101
+ .replace(/&gt;/g, ">")
102
+ .replace(/&quot;/g, '"')
103
+ .replace(/&#39;/g, "'");
104
+ // Collapse whitespace
105
+ text = text.replace(/\n{3,}/g, "\n\n").replace(/[ \t]+/g, " ");
106
+ return text.trim();
107
+ }
108
+
109
+ function stripTags(html) {
110
+ return String(html).replace(/<[^>]+>/g, "");
111
+ }
112
+
113
+ async function _doRequest(parsed, { maxBytes, timeout, headers }) {
114
+ const lib = parsed.protocol === "https:" ? https : http;
115
+ return new Promise((resolve, reject) => {
116
+ const req = lib.request(
117
+ {
118
+ method: "GET",
119
+ protocol: parsed.protocol,
120
+ hostname: parsed.hostname,
121
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
122
+ path: parsed.pathname + parsed.search,
123
+ headers: {
124
+ "User-Agent": "ChainlessChain-Agent/1.0",
125
+ Accept: "*/*",
126
+ ...headers,
127
+ },
128
+ timeout,
129
+ },
130
+ (res) => {
131
+ if (
132
+ res.statusCode >= 300 &&
133
+ res.statusCode < 400 &&
134
+ res.headers.location
135
+ ) {
136
+ res.resume();
137
+ return resolve({ redirect: res.headers.location });
138
+ }
139
+ const chunks = [];
140
+ let size = 0;
141
+ res.on("data", (chunk) => {
142
+ size += chunk.length;
143
+ if (size > maxBytes) {
144
+ req.destroy(new Error(`response exceeds maxBytes (${maxBytes})`));
145
+ return;
146
+ }
147
+ chunks.push(chunk);
148
+ });
149
+ res.on("end", () => {
150
+ resolve({
151
+ statusCode: res.statusCode,
152
+ headers: res.headers,
153
+ body: Buffer.concat(chunks).toString("utf8"),
154
+ });
155
+ });
156
+ res.on("error", reject);
157
+ },
158
+ );
159
+ req.on("error", reject);
160
+ req.on("timeout", () => req.destroy(new Error("request timeout")));
161
+ req.end();
162
+ });
163
+ }
164
+
165
+ export async function webFetch(url, options = {}) {
166
+ const {
167
+ format = "markdown",
168
+ maxBytes = DEFAULT_MAX_BYTES,
169
+ timeout = DEFAULT_TIMEOUT_MS,
170
+ config = {},
171
+ headers = {},
172
+ maxRedirects = 3,
173
+ } = options;
174
+
175
+ const check = checkAllowed(url, config);
176
+ if (!check.allowed) {
177
+ return { error: `web_fetch blocked: ${check.reason}` };
178
+ }
179
+
180
+ let parsed = check.url;
181
+ let redirects = 0;
182
+ let response;
183
+ while (true) {
184
+ response = await _doRequest(parsed, { maxBytes, timeout, headers });
185
+ if (!response.redirect) break;
186
+ if (++redirects > maxRedirects) {
187
+ return { error: "too many redirects" };
188
+ }
189
+ const next = new URL(response.redirect, parsed);
190
+ const nextCheck = checkAllowed(next.toString(), config);
191
+ if (!nextCheck.allowed) {
192
+ return { error: `redirect blocked: ${nextCheck.reason}` };
193
+ }
194
+ parsed = nextCheck.url;
195
+ }
196
+
197
+ const { statusCode, headers: respHeaders, body } = response;
198
+ const contentType = String(respHeaders["content-type"] || "");
199
+
200
+ let output = body;
201
+ let outputFormat = format;
202
+ if (format === "markdown") {
203
+ output = /html/i.test(contentType) ? htmlToMarkdown(body) : body;
204
+ } else if (format === "text") {
205
+ output = /html/i.test(contentType) ? stripTags(body) : body;
206
+ } else if (format === "json") {
207
+ try {
208
+ output = JSON.parse(body);
209
+ } catch {
210
+ return { error: "response is not valid JSON", statusCode, body };
211
+ }
212
+ }
213
+
214
+ return {
215
+ url: parsed.toString(),
216
+ statusCode,
217
+ contentType,
218
+ format: outputFormat,
219
+ bytes: Buffer.byteLength(body, "utf8"),
220
+ content: output,
221
+ };
222
+ }
223
+
224
+ export const _deps = { http, https };
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Workflow expression sandbox — evaluate `when` conditions and resolve
3
+ * typed step-result references for `forEach` expansion.
4
+ *
5
+ * SAFETY: hand-written tokenizer + recursive-descent parser, no
6
+ * `Function` / `eval`. Only the operators below are supported.
7
+ *
8
+ * Grammar (informal):
9
+ *
10
+ * expr := or
11
+ * or := and ( "||" and )*
12
+ * and := not ( "&&" not )*
13
+ * not := "!" not | cmp
14
+ * cmp := primary ( OP primary )? OP ∈ { == != < <= > >= contains }
15
+ * primary := "(" expr ")" | ref | string | number | bool | null
16
+ * ref := "${" "step" "." ID "." ID "}" OR "${item}"
17
+ *
18
+ * String literals use single or double quotes; `\'` / `\"` / `\\` escapes.
19
+ *
20
+ * `evaluate(expr, ctx)` returns a boolean. `ctx` shape:
21
+ * {
22
+ * step: Map<stepId, { status, taskId, result: { summary, tokenCount, ... } }>,
23
+ * item: any, // for forEach expansion
24
+ * }
25
+ *
26
+ * `resolveReference(ref, ctx)` returns the raw value of a `${...}` token
27
+ * (used by forEach to materialize array sources).
28
+ *
29
+ * @module workflow-expr
30
+ */
31
+
32
+ const TOKEN_OP = new Set([
33
+ "==",
34
+ "!=",
35
+ "<",
36
+ "<=",
37
+ ">",
38
+ ">=",
39
+ "contains",
40
+ "&&",
41
+ "||",
42
+ "!",
43
+ "(",
44
+ ")",
45
+ ]);
46
+
47
+ /**
48
+ * Tokenize an expression string. Returns an array of `{ type, value }`.
49
+ * Types: `op`, `ref`, `string`, `number`, `bool`, `null`.
50
+ */
51
+ export function tokenize(src) {
52
+ if (typeof src !== "string") throw new Error("expr must be a string");
53
+ const tokens = [];
54
+ let i = 0;
55
+ const n = src.length;
56
+ while (i < n) {
57
+ const c = src[i];
58
+ if (c === " " || c === "\t" || c === "\n") {
59
+ i++;
60
+ continue;
61
+ }
62
+ if (c === "(" || c === ")") {
63
+ tokens.push({ type: "op", value: c });
64
+ i++;
65
+ continue;
66
+ }
67
+ if (c === "$" && src[i + 1] === "{") {
68
+ const end = src.indexOf("}", i + 2);
69
+ if (end === -1) throw new Error("unterminated reference");
70
+ tokens.push({ type: "ref", value: src.slice(i + 2, end).trim() });
71
+ i = end + 1;
72
+ continue;
73
+ }
74
+ if (c === "'" || c === '"') {
75
+ let j = i + 1;
76
+ let out = "";
77
+ while (j < n && src[j] !== c) {
78
+ if (src[j] === "\\" && j + 1 < n) {
79
+ out += src[j + 1];
80
+ j += 2;
81
+ } else {
82
+ out += src[j];
83
+ j++;
84
+ }
85
+ }
86
+ if (j >= n) throw new Error("unterminated string literal");
87
+ tokens.push({ type: "string", value: out });
88
+ i = j + 1;
89
+ continue;
90
+ }
91
+ if (/[0-9-]/.test(c)) {
92
+ let j = i;
93
+ if (src[j] === "-") j++;
94
+ while (j < n && /[0-9.]/.test(src[j])) j++;
95
+ const lit = src.slice(i, j);
96
+ const num = Number(lit);
97
+ if (!Number.isFinite(num)) throw new Error(`bad number: ${lit}`);
98
+ tokens.push({ type: "number", value: num });
99
+ i = j;
100
+ continue;
101
+ }
102
+ // Multi-char ops
103
+ const two = src.slice(i, i + 2);
104
+ if (
105
+ two === "==" ||
106
+ two === "!=" ||
107
+ two === "<=" ||
108
+ two === ">=" ||
109
+ two === "&&" ||
110
+ two === "||"
111
+ ) {
112
+ tokens.push({ type: "op", value: two });
113
+ i += 2;
114
+ continue;
115
+ }
116
+ if (c === "<" || c === ">" || c === "!") {
117
+ tokens.push({ type: "op", value: c });
118
+ i++;
119
+ continue;
120
+ }
121
+ // Identifiers: contains / true / false / null
122
+ if (/[a-zA-Z_]/.test(c)) {
123
+ let j = i;
124
+ while (j < n && /[a-zA-Z0-9_]/.test(src[j])) j++;
125
+ const id = src.slice(i, j);
126
+ if (id === "true" || id === "false") {
127
+ tokens.push({ type: "bool", value: id === "true" });
128
+ } else if (id === "null") {
129
+ tokens.push({ type: "null", value: null });
130
+ } else if (id === "contains") {
131
+ tokens.push({ type: "op", value: "contains" });
132
+ } else {
133
+ throw new Error(`unknown identifier: ${id}`);
134
+ }
135
+ i = j;
136
+ continue;
137
+ }
138
+ throw new Error(`unexpected char at ${i}: ${c}`);
139
+ }
140
+ return tokens;
141
+ }
142
+
143
+ /** Resolve a ref token like `step.fetch.summary` or `item` against ctx. */
144
+ export function resolveReference(ref, ctx) {
145
+ if (ref === "item") return ctx?.item;
146
+ const parts = ref.split(".");
147
+ if (parts[0] !== "step") {
148
+ throw new Error(`unknown reference root: ${parts[0]}`);
149
+ }
150
+ if (parts.length !== 3) {
151
+ throw new Error(`ref must be step.<id>.<field>: ${ref}`);
152
+ }
153
+ const [, stepId, field] = parts;
154
+ const entry = ctx?.step?.get?.(stepId);
155
+ if (!entry) return undefined;
156
+ if (field === "status") return entry.status;
157
+ if (field === "taskId") return entry.taskId;
158
+ if (field === "summary") return entry.result?.summary;
159
+ if (field === "tokenCount") return entry.result?.tokenCount;
160
+ if (field === "iterationCount") return entry.result?.iterationCount;
161
+ if (field === "toolsUsed") return entry.result?.toolsUsed;
162
+ if (field === "items") return entry.result?.items;
163
+ // Generic fallback: look up directly on result
164
+ return entry.result?.[field];
165
+ }
166
+
167
+ /** Recursive-descent parser returning an AST. */
168
+ function parse(tokens) {
169
+ let pos = 0;
170
+ function peek() {
171
+ return tokens[pos];
172
+ }
173
+ function consume(expected) {
174
+ const t = tokens[pos];
175
+ if (!t) throw new Error("unexpected end of expression");
176
+ if (expected && (t.type !== "op" || t.value !== expected)) {
177
+ throw new Error(`expected ${expected}, got ${t.value ?? t.type}`);
178
+ }
179
+ pos++;
180
+ return t;
181
+ }
182
+ function parseExpr() {
183
+ return parseOr();
184
+ }
185
+ function parseOr() {
186
+ let left = parseAnd();
187
+ while (peek() && peek().type === "op" && peek().value === "||") {
188
+ pos++;
189
+ const right = parseAnd();
190
+ left = { kind: "or", left, right };
191
+ }
192
+ return left;
193
+ }
194
+ function parseAnd() {
195
+ let left = parseNot();
196
+ while (peek() && peek().type === "op" && peek().value === "&&") {
197
+ pos++;
198
+ const right = parseNot();
199
+ left = { kind: "and", left, right };
200
+ }
201
+ return left;
202
+ }
203
+ function parseNot() {
204
+ if (peek() && peek().type === "op" && peek().value === "!") {
205
+ pos++;
206
+ return { kind: "not", expr: parseNot() };
207
+ }
208
+ return parseCmp();
209
+ }
210
+ function parseCmp() {
211
+ const left = parsePrimary();
212
+ const t = peek();
213
+ const cmpOps = new Set(["==", "!=", "<", "<=", ">", ">=", "contains"]);
214
+ if (t && t.type === "op" && cmpOps.has(t.value)) {
215
+ pos++;
216
+ const right = parsePrimary();
217
+ return { kind: "cmp", op: t.value, left, right };
218
+ }
219
+ // bare value → truthiness check
220
+ return { kind: "truthy", expr: left };
221
+ }
222
+ function parsePrimary() {
223
+ const t = peek();
224
+ if (!t) throw new Error("unexpected end of expression");
225
+ if (t.type === "op" && t.value === "(") {
226
+ pos++;
227
+ const e = parseExpr();
228
+ consume(")");
229
+ return e;
230
+ }
231
+ if (t.type === "ref") {
232
+ pos++;
233
+ return { kind: "ref", name: t.value };
234
+ }
235
+ if (
236
+ t.type === "string" ||
237
+ t.type === "number" ||
238
+ t.type === "bool" ||
239
+ t.type === "null"
240
+ ) {
241
+ pos++;
242
+ return { kind: "lit", value: t.value };
243
+ }
244
+ throw new Error(`unexpected token: ${t.value ?? t.type}`);
245
+ }
246
+
247
+ const ast = parseExpr();
248
+ if (pos !== tokens.length) {
249
+ throw new Error(`trailing tokens at ${pos}`);
250
+ }
251
+ return ast;
252
+ }
253
+
254
+ function evalAst(ast, ctx) {
255
+ switch (ast.kind) {
256
+ case "lit":
257
+ return ast.value;
258
+ case "ref":
259
+ return resolveReference(ast.name, ctx);
260
+ case "not":
261
+ return !evalAst(ast.expr, ctx);
262
+ case "and":
263
+ return evalAst(ast.left, ctx) && evalAst(ast.right, ctx);
264
+ case "or":
265
+ return evalAst(ast.left, ctx) || evalAst(ast.right, ctx);
266
+ case "truthy": {
267
+ const v = evalAst(ast.expr, ctx);
268
+ return !!v;
269
+ }
270
+ case "cmp": {
271
+ const l = evalAst(ast.left, ctx);
272
+ const r = evalAst(ast.right, ctx);
273
+ switch (ast.op) {
274
+ case "==":
275
+ // Loose equality across string/number for ergonomic use
276
+ // eslint-disable-next-line eqeqeq
277
+ return l == r;
278
+ case "!=":
279
+ // eslint-disable-next-line eqeqeq
280
+ return l != r;
281
+ case "<":
282
+ return l < r;
283
+ case "<=":
284
+ return l <= r;
285
+ case ">":
286
+ return l > r;
287
+ case ">=":
288
+ return l >= r;
289
+ case "contains": {
290
+ if (l == null) return false;
291
+ if (Array.isArray(l)) return l.includes(r);
292
+ return String(l).includes(String(r));
293
+ }
294
+ default:
295
+ throw new Error(`unknown op: ${ast.op}`);
296
+ }
297
+ }
298
+ default:
299
+ throw new Error(`unknown node: ${ast.kind}`);
300
+ }
301
+ }
302
+
303
+ /** Evaluate an expression string against a context. Returns a boolean. */
304
+ export function evaluate(src, ctx) {
305
+ const tokens = tokenize(src);
306
+ const ast = parse(tokens);
307
+ const v = evalAst(ast, ctx);
308
+ return !!v;
309
+ }
310
+
311
+ /** Evaluate an expression and return the raw (non-coerced) value. */
312
+ export function evaluateRaw(src, ctx) {
313
+ const tokens = tokenize(src);
314
+ let ast = parse(tokens);
315
+ // Strip the implicit truthy-wrapper so bare refs return raw values.
316
+ if (ast.kind === "truthy") ast = ast.expr;
317
+ return evalAst(ast, ctx);
318
+ }
@@ -39,6 +39,7 @@ import {
39
39
  } from "../harness/jsonl-session-store.js";
40
40
  import { storeMemory, consolidateMemory } from "../lib/hierarchical-memory.js";
41
41
  import { CLIContextEngineering } from "../lib/cli-context-engineering.js";
42
+ import { defaultPrepareCall } from "../lib/turn-context.js";
42
43
  import { createChatFn } from "../lib/cowork-adapter.js";
43
44
  import {
44
45
  detectTaskType,
@@ -1223,6 +1224,9 @@ export async function startAgentRepl(options = {}) {
1223
1224
  apiKey,
1224
1225
  contextEngine,
1225
1226
  iterationBudget,
1227
+ sessionId,
1228
+ cwd: process.cwd(),
1229
+ prepareCall: defaultPrepareCall,
1226
1230
  });
1227
1231
 
1228
1232
  if (response) {