clixad 0.0.1-beta.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 (4) hide show
  1. package/NOTICE +24 -0
  2. package/README.md +23 -0
  3. package/dist/clixad.mjs +3018 -0
  4. package/package.json +45 -0
@@ -0,0 +1,3018 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // src/client.ts
18
+ function adsPerTaskLabel(estAdsPerTask) {
19
+ const effort = estAdsPerTask >= 1 ? `~${estAdsPerTask} ads/task` : "<1 ad/task";
20
+ return `${effort} on completion`;
21
+ }
22
+ async function authFailure(res, what) {
23
+ const body = await res.text().catch(() => "");
24
+ const isJson = (res.headers.get("content-type") ?? "").includes("json");
25
+ if (isJson) {
26
+ try {
27
+ const parsed = JSON.parse(body);
28
+ if (typeof parsed.error === "string") return new AuthError();
29
+ } catch {
30
+ }
31
+ }
32
+ return new Error(
33
+ `${what}: the gateway returned 401 without a Clixad error body. The gateway URL may be pointing at something else \u2014 a login/SSO page, a proxy, or a stale deployment. Check it, or set CLIXAD_GATEWAY_URL.`
34
+ );
35
+ }
36
+ function accumulateToolCall(calls, part, fallbackIndex) {
37
+ const index = part.index ?? fallbackIndex;
38
+ const current = calls.get(index) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
39
+ if (part.id) current.id = part.id;
40
+ if (part.function?.name) current.function.name = part.function.name;
41
+ if (part.function?.arguments) current.function.arguments += part.function.arguments;
42
+ calls.set(index, current);
43
+ }
44
+ async function* parseSSE(body) {
45
+ const reader = body.getReader();
46
+ const decoder = new TextDecoder();
47
+ let buffer = "";
48
+ for (; ; ) {
49
+ const { done, value } = await reader.read();
50
+ if (done) break;
51
+ buffer += decoder.decode(value, { stream: true });
52
+ let idx;
53
+ while ((idx = buffer.indexOf("\n\n")) !== -1) {
54
+ const frame = buffer.slice(0, idx);
55
+ buffer = buffer.slice(idx + 2);
56
+ for (const line2 of frame.split("\n")) {
57
+ if (line2.startsWith("data:")) yield line2.slice(5).trim();
58
+ }
59
+ }
60
+ }
61
+ }
62
+ var PaywallError, AuthError, GatewayClient;
63
+ var init_client = __esm({
64
+ "src/client.ts"() {
65
+ "use strict";
66
+ PaywallError = class extends Error {
67
+ constructor(balance, estimatedCost, grantPerAd) {
68
+ super("Out of credits");
69
+ this.balance = balance;
70
+ this.estimatedCost = estimatedCost;
71
+ this.grantPerAd = grantPerAd;
72
+ this.name = "PaywallError";
73
+ }
74
+ balance;
75
+ estimatedCost;
76
+ grantPerAd;
77
+ };
78
+ AuthError = class extends Error {
79
+ constructor() {
80
+ super("Not logged in. Run `clixad login` first.");
81
+ this.name = "AuthError";
82
+ }
83
+ };
84
+ GatewayClient = class {
85
+ constructor(config) {
86
+ this.config = config;
87
+ }
88
+ config;
89
+ headers(auth = true) {
90
+ const h = { "Content-Type": "application/json" };
91
+ if (auth) {
92
+ if (!this.config.token) throw new AuthError();
93
+ h.Authorization = `Bearer ${this.config.token}`;
94
+ }
95
+ return h;
96
+ }
97
+ /** Dev-only signup shortcut (production replaces this with GitHub OAuth). */
98
+ async signupDev(email) {
99
+ const res = await fetch(`${this.config.gatewayUrl}/dev/users`, {
100
+ method: "POST",
101
+ headers: this.headers(false),
102
+ body: JSON.stringify(email ? { email } : {})
103
+ });
104
+ if (!res.ok) throw new Error(`signup failed: ${res.status} ${await res.text()}`);
105
+ return res.json();
106
+ }
107
+ /** Begin GitHub device-flow login. Returns null if the gateway has no GitHub
108
+ * configured (501), so the caller can fall back to the dev shortcut. */
109
+ async deviceStart() {
110
+ const res = await fetch(`${this.config.gatewayUrl}/v1/auth/device/start`, {
111
+ method: "POST",
112
+ headers: this.headers(false)
113
+ });
114
+ if (res.status === 501) return null;
115
+ if (!res.ok) throw new Error(`device start failed: ${res.status} ${await res.text()}`);
116
+ return res.json();
117
+ }
118
+ /** Poll once for device-flow completion. */
119
+ async devicePoll(session) {
120
+ const res = await fetch(`${this.config.gatewayUrl}/v1/auth/device/poll`, {
121
+ method: "POST",
122
+ headers: this.headers(false),
123
+ body: JSON.stringify({ session })
124
+ });
125
+ const data = await res.json().catch(() => ({}));
126
+ if (!res.ok && !("status" in data && data.status)) {
127
+ return { status: "error", error: `http ${res.status}` };
128
+ }
129
+ return data;
130
+ }
131
+ /**
132
+ * Mint a single-use code the ad wall can trade for a session, so opening the
133
+ * dashboard doesn't ask the user to paste a token by hand. Returns undefined if
134
+ * the gateway can't issue one — the caller still opens the wall, just unsigned.
135
+ */
136
+ async handoffCode() {
137
+ try {
138
+ const res = await fetch(`${this.config.gatewayUrl}/v1/auth/handoff`, {
139
+ method: "POST",
140
+ headers: this.headers()
141
+ });
142
+ if (!res.ok) return void 0;
143
+ return (await res.json()).code;
144
+ } catch {
145
+ return void 0;
146
+ }
147
+ }
148
+ async creditPacks() {
149
+ const res = await fetch(`${this.config.gatewayUrl}/v1/billing/packs`);
150
+ if (!res.ok) throw new Error(`packs failed: ${res.status}`);
151
+ return res.json();
152
+ }
153
+ async checkout(pack) {
154
+ const res = await fetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
155
+ method: "POST",
156
+ headers: this.headers(),
157
+ body: JSON.stringify({ pack })
158
+ });
159
+ if (res.status === 401) throw await authFailure(res, "checkout");
160
+ if (res.status === 501) throw new Error("Buying credits isn't enabled (Stripe not configured on the gateway).");
161
+ if (!res.ok) throw new Error(`checkout failed: ${res.status} ${await res.text()}`);
162
+ return res.json();
163
+ }
164
+ async models() {
165
+ const res = await fetch(`${this.config.gatewayUrl}/v1/models`);
166
+ if (!res.ok) throw new Error(`models failed: ${res.status}`);
167
+ const data = await res.json();
168
+ return data.data;
169
+ }
170
+ async wallet() {
171
+ const res = await fetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers() });
172
+ if (res.status === 401) throw await authFailure(res, "wallet");
173
+ if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
174
+ return res.json();
175
+ }
176
+ // There is deliberately no `earnAd` here any more. It posted to
177
+ // `/v1/ads/reward`, the gateway's CLIXAD_DEV-only reward simulator, which is
178
+ // 404 in any real deployment — so both `clixad earn` and the REPL's `/earn`
179
+ // were broken in production while working perfectly against a dev gateway.
180
+ // Earning now goes through the browser handoff (`handoffCode` above) to the ad
181
+ // wall, and the reward arrives as a signed postback from the network to the
182
+ // gateway. Nothing comes back to the CLI; a rising balance is the only signal,
183
+ // which is why both callers poll `wallet()`.
184
+ /** Non-streaming chat. Kept for scripted/one-shot paths; the REPL streams. */
185
+ async chat(messages, model, tools, signal) {
186
+ const res = await fetch(`${this.config.gatewayUrl}/v1/chat/completions`, {
187
+ method: "POST",
188
+ headers: this.headers(),
189
+ body: JSON.stringify({ model, messages, stream: false, ...tools ? { tools } : {} }),
190
+ signal
191
+ });
192
+ if (res.status === 401) throw await authFailure(res, "chat");
193
+ if (res.status === 402) {
194
+ const body = await res.json();
195
+ throw new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad);
196
+ }
197
+ if (!res.ok) throw new Error(`chat failed: ${res.status} ${await res.text()}`);
198
+ const data = await res.json();
199
+ const choice = data.choices[0];
200
+ if (!choice) throw new Error("chat: no choices in response");
201
+ return {
202
+ message: choice.message,
203
+ finishReason: choice.finish_reason,
204
+ creditsCharged: data.clixad.credits_charged,
205
+ balance: data.clixad.balance
206
+ };
207
+ }
208
+ /**
209
+ * Streaming chat. Calls `opts.onDelta` for each content chunk and assembles
210
+ * the whole assistant turn — including `tool_calls`, which the gateway streams
211
+ * with a per-call `index` (OpenAI style, see server.ts `streamCompletion`), so
212
+ * the agent loop can stream *and* use tools in the same request.
213
+ */
214
+ async chatStream(messages, model, opts = {}) {
215
+ const res = await fetch(`${this.config.gatewayUrl}/v1/chat/completions`, {
216
+ method: "POST",
217
+ headers: this.headers(),
218
+ body: JSON.stringify({ model, messages, stream: true, ...opts.tools ? { tools: opts.tools } : {} }),
219
+ signal: opts.signal
220
+ });
221
+ if (res.status === 401) throw await authFailure(res, "chat");
222
+ if (res.status === 402) {
223
+ const body = await res.json();
224
+ throw new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad);
225
+ }
226
+ if (!res.ok || !res.body) throw new Error(`chat failed: ${res.status} ${await res.text()}`);
227
+ let content = "";
228
+ let finishReason = "stop";
229
+ let creditsCharged = 0;
230
+ let balance = 0;
231
+ let streamError;
232
+ const calls = /* @__PURE__ */ new Map();
233
+ for await (const evt of parseSSE(res.body)) {
234
+ if (evt === "[DONE]") break;
235
+ const json = JSON.parse(evt);
236
+ const choice = json.choices?.[0];
237
+ const delta = choice?.delta?.content;
238
+ if (delta) {
239
+ content += delta;
240
+ opts.onDelta?.(delta);
241
+ }
242
+ for (const [index, part] of (choice?.delta?.tool_calls ?? []).entries()) {
243
+ accumulateToolCall(calls, part, index);
244
+ }
245
+ if (choice?.finish_reason) finishReason = choice.finish_reason;
246
+ if (json.error) streamError = json.error.message || "the model stream failed";
247
+ if (json.clixad) {
248
+ creditsCharged = json.clixad.credits_charged;
249
+ balance = json.clixad.balance;
250
+ }
251
+ }
252
+ if (streamError) throw new Error(streamError);
253
+ const message = { role: "assistant", content: content || null };
254
+ if (calls.size > 0) {
255
+ message.tool_calls = [...calls.entries()].sort(([a], [b]) => a - b).map(([, c2]) => c2);
256
+ }
257
+ return { message, finishReason, creditsCharged, balance };
258
+ }
259
+ };
260
+ }
261
+ });
262
+
263
+ // src/config.ts
264
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
265
+ import { homedir } from "node:os";
266
+ import { dirname, join } from "node:path";
267
+ function loadConfig() {
268
+ let stored = {};
269
+ try {
270
+ stored = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
271
+ } catch {
272
+ }
273
+ for (const [key, stale] of Object.entries(SUPERSEDED_DEFAULTS)) {
274
+ const value = stored[key];
275
+ if (typeof value === "string" && stale.includes(value) && value !== DEFAULTS[key]) {
276
+ delete stored[key];
277
+ }
278
+ }
279
+ const config = { ...DEFAULTS, ...stored };
280
+ for (const [key, envVar] of ENV_OVERRIDES) {
281
+ const value = process.env[envVar];
282
+ if (value) config[key] = value;
283
+ }
284
+ return config;
285
+ }
286
+ function saveConfig(config) {
287
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
288
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
289
+ try {
290
+ chmodSync(CONFIG_PATH, 384);
291
+ } catch {
292
+ }
293
+ }
294
+ function configPath() {
295
+ return CONFIG_PATH;
296
+ }
297
+ var CONFIG_PATH, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS;
298
+ var init_config = __esm({
299
+ "src/config.ts"() {
300
+ "use strict";
301
+ CONFIG_PATH = join(homedir(), ".clixad", "config.json");
302
+ DEFAULTS = {
303
+ gatewayUrl: "https://clixad.onrender.com",
304
+ // Same origin as the gateway: the ad wall is served by the gateway process
305
+ // itself now (apps/gateway/src/dashboard.ts), not by a second service. The two
306
+ // fields stay separate so either can be pointed elsewhere, but they no longer
307
+ // diverge by default.
308
+ dashboardUrl: "https://clixad.onrender.com",
309
+ model: "gemini-2.5-flash-lite"
310
+ };
311
+ ENV_OVERRIDES = [
312
+ ["gatewayUrl", "CLIXAD_GATEWAY_URL"],
313
+ ["dashboardUrl", "CLIXAD_DASHBOARD_URL"]
314
+ ];
315
+ SUPERSEDED_DEFAULTS = {
316
+ gatewayUrl: ["http://127.0.0.1:8787", "http://localhost:8787"],
317
+ dashboardUrl: ["http://127.0.0.1:8788", "http://localhost:8788"]
318
+ };
319
+ }
320
+ });
321
+
322
+ // src/diff.ts
323
+ function splitLines(text) {
324
+ const lines = text.split("\n");
325
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
326
+ return lines;
327
+ }
328
+ function diffLines(a, b) {
329
+ let head = 0;
330
+ while (head < a.length && head < b.length && a[head] === b[head]) head++;
331
+ let tail = 0;
332
+ while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) {
333
+ tail++;
334
+ }
335
+ const midA = a.slice(head, a.length - tail);
336
+ const midB = b.slice(head, b.length - tail);
337
+ const out = [];
338
+ for (let i = 0; i < head; i++) out.push({ kind: "ctx", text: a[i], line: i + 1 });
339
+ if (midA.length * midB.length > LCS_LIMIT * LCS_LIMIT || midA.length * midB.length > 4e6) {
340
+ midA.forEach((text, i) => out.push({ kind: "del", text, line: head + i + 1 }));
341
+ midB.forEach((text, i) => out.push({ kind: "add", text, line: head + i + 1 }));
342
+ } else {
343
+ out.push(...lcsDiff(midA, midB, head));
344
+ }
345
+ for (let i = 0; i < tail; i++) {
346
+ const idx = b.length - tail + i;
347
+ out.push({ kind: "ctx", text: b[idx], line: idx + 1 });
348
+ }
349
+ return out;
350
+ }
351
+ function lcsDiff(a, b, offset2) {
352
+ const n = a.length;
353
+ const m = b.length;
354
+ const width = m + 1;
355
+ const dp = new Uint32Array((n + 1) * width);
356
+ for (let i2 = n - 1; i2 >= 0; i2--) {
357
+ for (let j2 = m - 1; j2 >= 0; j2--) {
358
+ dp[i2 * width + j2] = a[i2] === b[j2] ? dp[(i2 + 1) * width + j2 + 1] + 1 : Math.max(dp[(i2 + 1) * width + j2], dp[i2 * width + j2 + 1]);
359
+ }
360
+ }
361
+ const out = [];
362
+ let i = 0;
363
+ let j = 0;
364
+ while (i < n && j < m) {
365
+ if (a[i] === b[j]) {
366
+ out.push({ kind: "ctx", text: b[j], line: offset2 + j + 1 });
367
+ i++;
368
+ j++;
369
+ } else if (dp[(i + 1) * width + j] >= dp[i * width + j + 1]) {
370
+ out.push({ kind: "del", text: a[i], line: offset2 + i + 1 });
371
+ i++;
372
+ } else {
373
+ out.push({ kind: "add", text: b[j], line: offset2 + j + 1 });
374
+ j++;
375
+ }
376
+ }
377
+ while (i < n) out.push({ kind: "del", text: a[i], line: offset2 + i++ + 1 });
378
+ while (j < m) out.push({ kind: "add", text: b[j], line: offset2 + j++ + 1 });
379
+ return out;
380
+ }
381
+ function diffStat(lines) {
382
+ let added = 0;
383
+ let removed = 0;
384
+ for (const l of lines) {
385
+ if (l.kind === "add") added++;
386
+ else if (l.kind === "del") removed++;
387
+ }
388
+ return { added, removed };
389
+ }
390
+ function renderDiff(oldText, newText, opts = {}) {
391
+ const context = opts.context ?? 3;
392
+ const maxLines = opts.maxLines ?? 40;
393
+ const maxWidth = opts.maxWidth ?? 200;
394
+ const lines = diffLines(splitLines(oldText), splitLines(newText));
395
+ const { added, removed } = diffStat(lines);
396
+ if (added === 0 && removed === 0) return "";
397
+ const keep = new Array(lines.length).fill(false);
398
+ lines.forEach((l, i) => {
399
+ if (l.kind === "ctx") return;
400
+ for (let k = Math.max(0, i - context); k <= Math.min(lines.length - 1, i + context); k++) {
401
+ keep[k] = true;
402
+ }
403
+ });
404
+ const out = [];
405
+ let skipped = 0;
406
+ let truncated = 0;
407
+ for (let i = 0; i < lines.length; i++) {
408
+ if (!keep[i]) {
409
+ skipped++;
410
+ continue;
411
+ }
412
+ if (skipped > 0) {
413
+ out.push(`${DIM} \u22EF ${skipped} unchanged line${skipped === 1 ? "" : "s"}${R}`);
414
+ skipped = 0;
415
+ }
416
+ if (out.length >= maxLines) {
417
+ truncated++;
418
+ continue;
419
+ }
420
+ const l = lines[i];
421
+ const text = l.text.length > maxWidth ? l.text.slice(0, maxWidth - 1) + "\u2026" : l.text;
422
+ const no = String(l.line).padStart(4);
423
+ if (l.kind === "add") out.push(`${GREEN}${no} + ${text}${R}`);
424
+ else if (l.kind === "del") out.push(`${RED}${no} - ${text}${R}`);
425
+ else out.push(`${DIM}${no} ${text}${R}`);
426
+ }
427
+ if (truncated > 0) out.push(`${DIM} \u22EF ${truncated} more changed line${truncated === 1 ? "" : "s"}${R}`);
428
+ const stat = `${GREEN}+${added}${R} ${RED}-${removed}${R}`;
429
+ return `${stat}
430
+ ${out.join("\n")}`;
431
+ }
432
+ var R, GREEN, RED, DIM, LCS_LIMIT;
433
+ var init_diff = __esm({
434
+ "src/diff.ts"() {
435
+ "use strict";
436
+ R = "\x1B[0m";
437
+ GREEN = "\x1B[32m";
438
+ RED = "\x1B[31m";
439
+ DIM = "\x1B[2m";
440
+ LCS_LIMIT = 3e3;
441
+ }
442
+ });
443
+
444
+ // src/tools.ts
445
+ import { spawn } from "node:child_process";
446
+ import { existsSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
447
+ import { isAbsolute, relative, resolve, dirname as dirname2, join as join2, sep } from "node:path";
448
+ function safeResolve(root, p) {
449
+ const abs = isAbsolute(p) ? p : resolve(root, p);
450
+ const rel2 = relative(root, abs);
451
+ if (rel2 === ".." || rel2.startsWith(".." + sep) || isAbsolute(rel2)) {
452
+ throw new ToolError(`path escapes workspace: ${p}`);
453
+ }
454
+ return abs;
455
+ }
456
+ function rel(root, abs) {
457
+ return relative(root, abs).replace(/\\/g, "/") || ".";
458
+ }
459
+ async function gate(ctx, req) {
460
+ if (!ctx.permit) return;
461
+ const res = await ctx.permit(req);
462
+ if (!res.allowed) throw new ToolError(res.reason);
463
+ }
464
+ function commandSignature(command) {
465
+ return `run:${command.replace(/\s+/g, " ").trim()}`;
466
+ }
467
+ function occurrences(haystack, needle) {
468
+ if (!needle) return 0;
469
+ let n = 0;
470
+ let i = haystack.indexOf(needle);
471
+ while (i !== -1) {
472
+ n++;
473
+ i = haystack.indexOf(needle, i + needle.length);
474
+ }
475
+ return n;
476
+ }
477
+ function killTree(child) {
478
+ if (child.pid === void 0) {
479
+ child.kill("SIGKILL");
480
+ return;
481
+ }
482
+ if (process.platform === "win32") {
483
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
484
+ } else {
485
+ try {
486
+ process.kill(-child.pid, "SIGTERM");
487
+ } catch {
488
+ child.kill("SIGTERM");
489
+ }
490
+ }
491
+ }
492
+ function execute(ctx, command, timeout) {
493
+ return new Promise((resolvePromise) => {
494
+ const child = spawn(command, {
495
+ cwd: ctx.root,
496
+ shell: true,
497
+ windowsHide: true,
498
+ // Own process group on POSIX so killTree can take the children with it.
499
+ detached: process.platform !== "win32"
500
+ });
501
+ let out = "";
502
+ let total = 0;
503
+ let timedOut = false;
504
+ const collect = (buf) => {
505
+ const chunk = buf.toString("utf8");
506
+ ctx.onOutput?.(chunk);
507
+ total += chunk.length;
508
+ if (out.length < MAX_OUTPUT_CHARS) out += chunk;
509
+ };
510
+ child.stdout?.on("data", collect);
511
+ child.stderr?.on("data", collect);
512
+ const onAbort = () => killTree(child);
513
+ ctx.signal?.addEventListener("abort", onAbort, { once: true });
514
+ const timer = setTimeout(() => {
515
+ timedOut = true;
516
+ killTree(child);
517
+ }, timeout);
518
+ let settled = false;
519
+ const finish = (summary) => {
520
+ if (settled) return;
521
+ settled = true;
522
+ clearTimeout(timer);
523
+ ctx.signal?.removeEventListener("abort", onAbort);
524
+ const body = out.slice(0, MAX_OUTPUT_CHARS).trimEnd();
525
+ const note = total > MAX_OUTPUT_CHARS ? `
526
+ \u2026 [output capped: showing ${MAX_OUTPUT_CHARS} of ${total} chars]` : "";
527
+ resolvePromise(`${summary}
528
+ ${body || "(no output)"}${note}`);
529
+ };
530
+ child.on("error", (err) => finish(`command failed to start: ${err.message}`));
531
+ child.on("close", (code) => {
532
+ if (ctx.signal?.aborted) return finish("command cancelled by the user");
533
+ if (timedOut) return finish(`command killed after ${timeout} ms`);
534
+ finish(`exit ${code ?? 0}`);
535
+ });
536
+ });
537
+ }
538
+ function listWorkspaceFiles(root, limit = 5e3) {
539
+ const files = [];
540
+ walk(root, root, (abs) => {
541
+ files.push(rel(root, abs));
542
+ return files.length < limit;
543
+ });
544
+ return files.sort();
545
+ }
546
+ function walk(dir, root, visit) {
547
+ let seen = 0;
548
+ const stack = [dir];
549
+ while (stack.length > 0) {
550
+ const current = stack.pop();
551
+ let entries;
552
+ try {
553
+ entries = readdirSync(current, { withFileTypes: true });
554
+ } catch {
555
+ continue;
556
+ }
557
+ for (const entry of entries) {
558
+ const abs = join2(current, entry.name);
559
+ if (entry.isDirectory()) {
560
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
561
+ stack.push(abs);
562
+ continue;
563
+ }
564
+ if (!entry.isFile()) continue;
565
+ if (++seen > MAX_WALK_FILES) return;
566
+ if (!visit(abs)) return;
567
+ }
568
+ }
569
+ void root;
570
+ }
571
+ function globToRegExp(pattern) {
572
+ const p = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
573
+ let re = "";
574
+ for (let i = 0; i < p.length; i++) {
575
+ const ch = p[i];
576
+ if (ch === "*") {
577
+ if (p[i + 1] === "*") {
578
+ if (p[i + 2] === "/") {
579
+ re += "(?:.*/)?";
580
+ i += 2;
581
+ } else {
582
+ re += ".*";
583
+ i += 1;
584
+ }
585
+ } else {
586
+ re += "[^/]*";
587
+ }
588
+ } else if (ch === "?") re += "[^/]";
589
+ else if (ch === "{") re += "(?:";
590
+ else if (ch === "}") re += ")";
591
+ else if (ch === ",") re += "|";
592
+ else if (".+^$()[]|\\".includes(ch)) re += "\\" + ch;
593
+ else re += ch;
594
+ }
595
+ const anchored = p.includes("/") ? `^${re}$` : `^(?:.*/)?${re}$`;
596
+ return new RegExp(anchored);
597
+ }
598
+ var ToolError, MAX_FILE_BYTES, MAX_OUTPUT_CHARS, MAX_GLOB_RESULTS, MAX_GREP_MATCHES, MAX_WALK_FILES, DEFAULT_COMMAND_TIMEOUT, SKIP_DIRS, TOOLS, TOOL_SCHEMA;
599
+ var init_tools = __esm({
600
+ "src/tools.ts"() {
601
+ "use strict";
602
+ init_diff();
603
+ ToolError = class extends Error {
604
+ };
605
+ MAX_FILE_BYTES = 4e5;
606
+ MAX_OUTPUT_CHARS = 3e4;
607
+ MAX_GLOB_RESULTS = 200;
608
+ MAX_GREP_MATCHES = 100;
609
+ MAX_WALK_FILES = 2e4;
610
+ DEFAULT_COMMAND_TIMEOUT = 12e4;
611
+ SKIP_DIRS = /* @__PURE__ */ new Set([
612
+ ".git",
613
+ "node_modules",
614
+ "dist",
615
+ "build",
616
+ "coverage",
617
+ ".next",
618
+ ".turbo",
619
+ ".cache",
620
+ "__pycache__",
621
+ ".venv"
622
+ ]);
623
+ TOOLS = {
624
+ read_file(ctx, args) {
625
+ const abs = safeResolve(ctx.root, args.path);
626
+ if (!existsSync(abs)) throw new ToolError(`not found: ${args.path}`);
627
+ if (statSync(abs).isDirectory()) throw new ToolError(`${args.path} is a directory \u2014 use list_dir`);
628
+ const content = readFileSync2(abs, "utf8");
629
+ if (content.length > MAX_FILE_BYTES) {
630
+ return content.slice(0, MAX_FILE_BYTES) + `
631
+ \u2026 [truncated at ${MAX_FILE_BYTES} chars]`;
632
+ }
633
+ return content;
634
+ },
635
+ list_dir(ctx, args) {
636
+ const abs = safeResolve(ctx.root, args.path ?? ".");
637
+ if (!existsSync(abs)) throw new ToolError(`not found: ${args.path ?? "."}`);
638
+ const entries = readdirSync(abs).map((name) => {
639
+ const isDir = statSync(resolve(abs, name)).isDirectory();
640
+ return { name: isDir ? `${name}/` : name, isDir };
641
+ });
642
+ entries.sort((a, b) => Number(b.isDir) - Number(a.isDir) || a.name.localeCompare(b.name));
643
+ return entries.map((e) => e.name).join("\n") || "(empty)";
644
+ },
645
+ /** Find files by glob, e.g. `src/**\/*.ts`. Cheap orientation before reading. */
646
+ glob(ctx, args) {
647
+ if (!args.pattern) throw new ToolError("glob needs a pattern");
648
+ const base = safeResolve(ctx.root, args.path ?? ".");
649
+ const re = globToRegExp(args.pattern);
650
+ const hits = [];
651
+ walk(base, ctx.root, (abs) => {
652
+ const r = rel(ctx.root, abs);
653
+ if (re.test(r)) hits.push(r);
654
+ return hits.length < MAX_GLOB_RESULTS;
655
+ });
656
+ if (hits.length === 0) return `no files match ${args.pattern}`;
657
+ hits.sort();
658
+ const more = hits.length >= MAX_GLOB_RESULTS ? `
659
+ \u2026 [capped at ${MAX_GLOB_RESULTS}]` : "";
660
+ return hits.join("\n") + more;
661
+ },
662
+ /** Search file contents with a regular expression; returns path:line: text. */
663
+ grep(ctx, args) {
664
+ if (!args.pattern) throw new ToolError("grep needs a pattern");
665
+ let re;
666
+ try {
667
+ re = new RegExp(args.pattern);
668
+ } catch (err) {
669
+ throw new ToolError(`invalid regular expression: ${err.message}`);
670
+ }
671
+ const base = safeResolve(ctx.root, args.path ?? ".");
672
+ const filter = args.glob ? globToRegExp(args.glob) : void 0;
673
+ const out = [];
674
+ walk(base, ctx.root, (abs) => {
675
+ const r = rel(ctx.root, abs);
676
+ if (filter && !filter.test(r)) return true;
677
+ let content;
678
+ try {
679
+ content = readFileSync2(abs, "utf8");
680
+ } catch {
681
+ return true;
682
+ }
683
+ if (content.includes("\0")) return true;
684
+ const lines = content.split("\n");
685
+ for (let i = 0; i < lines.length; i++) {
686
+ const line2 = lines[i];
687
+ if (!re.test(line2)) continue;
688
+ out.push(`${r}:${i + 1}: ${line2.trim().slice(0, 200)}`);
689
+ if (out.length >= MAX_GREP_MATCHES) return false;
690
+ }
691
+ return true;
692
+ });
693
+ if (out.length === 0) return `no matches for ${args.pattern}`;
694
+ const more = out.length >= MAX_GREP_MATCHES ? `
695
+ \u2026 [capped at ${MAX_GREP_MATCHES} matches]` : "";
696
+ return out.join("\n") + more;
697
+ },
698
+ async write_file(ctx, args) {
699
+ const abs = safeResolve(ctx.root, args.path);
700
+ const before = existsSync(abs) ? readFileSync2(abs, "utf8") : "";
701
+ const preview = renderDiff(before, args.content) || "(no change)";
702
+ await gate(ctx, {
703
+ tool: "write_file",
704
+ signature: `write:${rel(ctx.root, abs)}`,
705
+ summary: `${before ? "overwrite" : "create"} ${rel(ctx.root, abs)}`,
706
+ preview
707
+ });
708
+ mkdirSync2(dirname2(abs), { recursive: true });
709
+ writeFileSync2(abs, args.content, "utf8");
710
+ return `wrote ${args.content.length} bytes to ${rel(ctx.root, abs)}`;
711
+ },
712
+ /**
713
+ * Replace an exact snippet in a file. Preferred over write_file: it keeps the
714
+ * rest of the file untouched and costs a fraction of the tokens (= credits).
715
+ * An ambiguous match is an error rather than a guess.
716
+ */
717
+ async edit_file(ctx, args) {
718
+ const abs = safeResolve(ctx.root, args.path);
719
+ if (!existsSync(abs)) throw new ToolError(`not found: ${args.path}`);
720
+ if (typeof args.old_string !== "string" || typeof args.new_string !== "string") {
721
+ throw new ToolError("edit_file needs old_string and new_string");
722
+ }
723
+ if (args.old_string === args.new_string) throw new ToolError("old_string and new_string are identical");
724
+ const before = readFileSync2(abs, "utf8");
725
+ const count = occurrences(before, args.old_string);
726
+ if (count === 0) throw new ToolError(`old_string not found in ${args.path} \u2014 read the file and copy the exact text`);
727
+ if (count > 1 && !args.replace_all) {
728
+ throw new ToolError(
729
+ `old_string appears ${count} times in ${args.path} \u2014 add more surrounding context, or pass replace_all: true`
730
+ );
731
+ }
732
+ const after = before.split(args.old_string).join(args.new_string);
733
+ const preview = renderDiff(before, after) || "(no change)";
734
+ await gate(ctx, {
735
+ tool: "edit_file",
736
+ signature: `write:${rel(ctx.root, abs)}`,
737
+ summary: `edit ${rel(ctx.root, abs)}`,
738
+ preview
739
+ });
740
+ writeFileSync2(abs, after, "utf8");
741
+ return `edited ${rel(ctx.root, abs)} (${count > 1 ? `${count} occurrences` : "1 occurrence"})`;
742
+ },
743
+ /**
744
+ * Run a shell command in the workspace. Streams output to `ctx.onOutput` as it
745
+ * arrives and dies with `ctx.signal`, so a runaway build can be interrupted.
746
+ */
747
+ async run_command(ctx, args) {
748
+ const command = (args.command ?? "").trim();
749
+ if (!command) throw new ToolError("run_command needs a command");
750
+ await gate(ctx, {
751
+ tool: "run_command",
752
+ signature: commandSignature(command),
753
+ summary: `run ${command.length > 60 ? command.slice(0, 59) + "\u2026" : command}`,
754
+ preview: command
755
+ });
756
+ return execute(ctx, command, args.timeout ?? DEFAULT_COMMAND_TIMEOUT);
757
+ }
758
+ };
759
+ TOOL_SCHEMA = [
760
+ {
761
+ type: "function",
762
+ function: {
763
+ name: "read_file",
764
+ description: "Read a UTF-8 text file from the workspace.",
765
+ parameters: {
766
+ type: "object",
767
+ properties: { path: { type: "string" } },
768
+ required: ["path"]
769
+ }
770
+ }
771
+ },
772
+ {
773
+ type: "function",
774
+ function: {
775
+ name: "list_dir",
776
+ description: "List files and directories at a workspace path.",
777
+ parameters: { type: "object", properties: { path: { type: "string" } } }
778
+ }
779
+ },
780
+ {
781
+ type: "function",
782
+ function: {
783
+ name: "glob",
784
+ description: "Find files by glob pattern (e.g. 'src/**/*.ts', '*.json'). Use this to locate files instead of shell commands.",
785
+ parameters: {
786
+ type: "object",
787
+ properties: {
788
+ pattern: { type: "string" },
789
+ path: { type: "string", description: "Directory to search in; defaults to the workspace root." }
790
+ },
791
+ required: ["pattern"]
792
+ }
793
+ }
794
+ },
795
+ {
796
+ type: "function",
797
+ function: {
798
+ name: "grep",
799
+ description: "Search file contents with a regular expression. Returns 'path:line: text'. Use this to find code instead of shell commands.",
800
+ parameters: {
801
+ type: "object",
802
+ properties: {
803
+ pattern: { type: "string" },
804
+ path: { type: "string", description: "Directory to search in; defaults to the workspace root." },
805
+ glob: { type: "string", description: "Only search files matching this glob." }
806
+ },
807
+ required: ["pattern"]
808
+ }
809
+ }
810
+ },
811
+ {
812
+ type: "function",
813
+ function: {
814
+ name: "edit_file",
815
+ description: "Replace an exact snippet in an existing file. Prefer this over write_file for changes to existing files. old_string must match exactly once unless replace_all is true.",
816
+ parameters: {
817
+ type: "object",
818
+ properties: {
819
+ path: { type: "string" },
820
+ old_string: { type: "string" },
821
+ new_string: { type: "string" },
822
+ replace_all: { type: "boolean" }
823
+ },
824
+ required: ["path", "old_string", "new_string"]
825
+ }
826
+ }
827
+ },
828
+ {
829
+ type: "function",
830
+ function: {
831
+ name: "write_file",
832
+ description: "Create a new file, or overwrite one completely. Use edit_file for partial changes.",
833
+ parameters: {
834
+ type: "object",
835
+ properties: { path: { type: "string" }, content: { type: "string" } },
836
+ required: ["path", "content"]
837
+ }
838
+ }
839
+ },
840
+ {
841
+ type: "function",
842
+ function: {
843
+ name: "run_command",
844
+ description: "Run a shell command in the workspace (build, test, git, etc.).",
845
+ parameters: {
846
+ type: "object",
847
+ properties: {
848
+ command: { type: "string" },
849
+ timeout: { type: "number", description: "Milliseconds before the command is killed." }
850
+ },
851
+ required: ["command"]
852
+ }
853
+ }
854
+ }
855
+ ];
856
+ }
857
+ });
858
+
859
+ // src/agent.ts
860
+ async function runAgent(client, task, opts) {
861
+ const maxSteps = opts.maxSteps ?? 16;
862
+ const emit = (e) => opts.onEvent?.(e);
863
+ const ctx = {
864
+ root: opts.root,
865
+ permit: opts.permit,
866
+ signal: opts.signal
867
+ };
868
+ const messages = [
869
+ { role: "system", content: opts.systemPrompt ?? SYSTEM_PROMPT },
870
+ ...opts.history ?? [],
871
+ { role: "user", content: task }
872
+ ];
873
+ let creditsCharged = 0;
874
+ let balance = 0;
875
+ let lastText = "";
876
+ for (let step = 0; step < maxSteps; step++) {
877
+ if (opts.signal?.aborted) return stop("aborted");
878
+ let turn;
879
+ try {
880
+ turn = await client.chatStream(messages, opts.model, {
881
+ tools: TOOL_SCHEMA,
882
+ signal: opts.signal,
883
+ onDelta: (text) => emit({ type: "delta", text })
884
+ });
885
+ } catch (err) {
886
+ if (isAbort(err) || opts.signal?.aborted) return stop("aborted");
887
+ throw err;
888
+ }
889
+ creditsCharged += turn.creditsCharged;
890
+ balance = turn.balance;
891
+ emit({ type: "usage", creditsCharged: turn.creditsCharged, balance: turn.balance });
892
+ messages.push(turn.message);
893
+ const content = turn.message.content ?? "";
894
+ if (content) lastText = content;
895
+ const calls = turn.message.tool_calls ?? [];
896
+ if (calls.length === 0) {
897
+ emit({ type: "final", content, creditsCharged, balance });
898
+ return { content, messages, creditsCharged, balance, steps: step + 1 };
899
+ }
900
+ if (content) emit({ type: "message", content });
901
+ for (const [i, call] of calls.entries()) {
902
+ if (opts.signal?.aborted) {
903
+ for (const rest of calls.slice(i)) messages.push(toolMessage(rest, "cancelled by the user"));
904
+ return stop("aborted");
905
+ }
906
+ const result = await executeToolCall(ctx, call, emit);
907
+ messages.push(toolMessage(call, result));
908
+ }
909
+ }
910
+ emit({ type: "max_steps" });
911
+ return {
912
+ content: lastText || "(stopped: reached max steps)",
913
+ messages,
914
+ creditsCharged,
915
+ balance,
916
+ steps: maxSteps,
917
+ stopped: "max_steps"
918
+ };
919
+ function stop(reason) {
920
+ emit({ type: reason });
921
+ return { content: lastText, messages, creditsCharged, balance, steps: 0, stopped: reason };
922
+ }
923
+ }
924
+ function toolMessage(call, content) {
925
+ return { role: "tool", tool_call_id: call.id, name: call.function.name, content };
926
+ }
927
+ function isAbort(err) {
928
+ return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
929
+ }
930
+ async function executeToolCall(ctx, call, emit) {
931
+ const name = call.function.name;
932
+ const id = call.id;
933
+ let args;
934
+ try {
935
+ args = call.function.arguments ? JSON.parse(call.function.arguments) : {};
936
+ } catch {
937
+ return `error: invalid JSON arguments for ${name}`;
938
+ }
939
+ emit({ type: "tool_call", id, name, args });
940
+ const scoped = { ...ctx, onOutput: (chunk) => emit({ type: "tool_output", id, chunk }) };
941
+ try {
942
+ const result = await dispatch(scoped, name, args);
943
+ emit({ type: "tool_result", id, name, result, ok: true });
944
+ return result;
945
+ } catch (err) {
946
+ if (err instanceof ToolError) {
947
+ const msg = `error: ${err.message}`;
948
+ emit({ type: "tool_result", id, name, result: msg, ok: false });
949
+ return msg;
950
+ }
951
+ throw err;
952
+ }
953
+ }
954
+ async function dispatch(ctx, name, args) {
955
+ switch (name) {
956
+ case "read_file":
957
+ return TOOLS.read_file(ctx, args);
958
+ case "list_dir":
959
+ return TOOLS.list_dir(ctx, args);
960
+ case "glob":
961
+ return TOOLS.glob(ctx, args);
962
+ case "grep":
963
+ return TOOLS.grep(ctx, args);
964
+ case "edit_file":
965
+ return TOOLS.edit_file(ctx, args);
966
+ case "write_file":
967
+ return TOOLS.write_file(ctx, args);
968
+ case "run_command":
969
+ return TOOLS.run_command(ctx, args);
970
+ default:
971
+ return `error: unknown tool ${name}`;
972
+ }
973
+ }
974
+ var SYSTEM_PROMPT;
975
+ var init_agent = __esm({
976
+ "src/agent.ts"() {
977
+ "use strict";
978
+ init_tools();
979
+ SYSTEM_PROMPT = "You are Clixad, a terminal coding agent. You can read, search, write and edit files and run shell commands, all confined to the user's workspace. Find your way around with glob and grep before reading whole files. Prefer edit_file over write_file for existing files. Make minimal correct edits, verify them when a test or build command is available, and stop when the task is complete. Keep your final message short: say what you changed and why.";
980
+ }
981
+ });
982
+
983
+ // src/context.ts
984
+ import { spawnSync } from "node:child_process";
985
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
986
+ import { homedir as homedir2, platform } from "node:os";
987
+ import { join as join3 } from "node:path";
988
+ function contextFileCandidates(root) {
989
+ return [join3(homedir2(), ".clixad", "CLIXAD.md"), join3(root, "AGENTS.md"), join3(root, "CLIXAD.md")];
990
+ }
991
+ function collectContext(root, base = SYSTEM_PROMPT) {
992
+ const parts = [base, environmentBlock(root)];
993
+ const files = [];
994
+ for (const path of contextFileCandidates(root)) {
995
+ if (!existsSync2(path)) continue;
996
+ let text;
997
+ try {
998
+ text = readFileSync3(path, "utf8").trim();
999
+ } catch {
1000
+ continue;
1001
+ }
1002
+ if (!text) continue;
1003
+ files.push(path);
1004
+ parts.push(
1005
+ `# Project instructions (${path.replace(homedir2(), "~")})
1006
+ These come from the user and take precedence over your defaults.
1007
+
1008
+ ` + text.slice(0, MAX_CONTEXT_FILE_CHARS)
1009
+ );
1010
+ }
1011
+ return { systemPrompt: parts.join("\n\n"), files };
1012
+ }
1013
+ function environmentBlock(root) {
1014
+ const lines = [
1015
+ "# Environment",
1016
+ `working directory: ${root}`,
1017
+ `platform: ${platform()}`,
1018
+ `shell: ${platform() === "win32" ? "cmd/powershell (Windows)" : "sh"}`
1019
+ ];
1020
+ const branch = gitBranch(root);
1021
+ if (branch) lines.push(`git branch: ${branch}`);
1022
+ const top = topLevel(root);
1023
+ if (top) lines.push(`top level: ${top}`);
1024
+ return lines.join("\n");
1025
+ }
1026
+ function gitBranch(root) {
1027
+ try {
1028
+ const res = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1029
+ cwd: root,
1030
+ encoding: "utf8",
1031
+ timeout: 2e3,
1032
+ windowsHide: true
1033
+ });
1034
+ if (res.status !== 0) return void 0;
1035
+ return res.stdout.trim() || void 0;
1036
+ } catch {
1037
+ return void 0;
1038
+ }
1039
+ }
1040
+ function topLevel(root) {
1041
+ try {
1042
+ const entries = readdirSync2(root, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && e.name !== "node_modules").slice(0, 40).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
1043
+ return entries.length ? entries.join(" ") : void 0;
1044
+ } catch {
1045
+ return void 0;
1046
+ }
1047
+ }
1048
+ var MAX_CONTEXT_FILE_CHARS, INIT_PROMPT;
1049
+ var init_context = __esm({
1050
+ "src/context.ts"() {
1051
+ "use strict";
1052
+ init_agent();
1053
+ MAX_CONTEXT_FILE_CHARS = 2e4;
1054
+ INIT_PROMPT = "Create a CLIXAD.md file in the workspace root that tells a future coding agent how to work in this project. Explore the repository first (glob, grep, read the README and package manifests). Keep it under 60 lines and cover: what the project is, the layout, how to build/test/run it, and the conventions a newcomer would otherwise get wrong. If CLIXAD.md already exists, improve it instead of replacing it wholesale.";
1055
+ }
1056
+ });
1057
+
1058
+ // src/permissions.ts
1059
+ function isEdit(tool) {
1060
+ return tool === "write_file" || tool === "edit_file";
1061
+ }
1062
+ function createState(mode = "normal") {
1063
+ return { mode, allowed: /* @__PURE__ */ new Set() };
1064
+ }
1065
+ function nextMode(mode) {
1066
+ const i = MODES.indexOf(mode);
1067
+ return MODES[(i + 1) % MODES.length];
1068
+ }
1069
+ function decide(state, req) {
1070
+ if (state.mode === "plan") {
1071
+ return {
1072
+ kind: "deny",
1073
+ reason: `plan mode is on, so ${req.summary} was not executed. Do not attempt further writes or commands \u2014 describe the plan instead and let the user approve it.`
1074
+ };
1075
+ }
1076
+ if (state.allowed.has(req.signature)) return { kind: "allow" };
1077
+ if (state.mode === "acceptEdits" && isEdit(req.tool)) return { kind: "allow" };
1078
+ return { kind: "ask" };
1079
+ }
1080
+ function createPermit(getState, ask2) {
1081
+ return async (req) => {
1082
+ const state = getState();
1083
+ const verdict = decide(state, req);
1084
+ if (verdict.kind === "allow") return { allowed: true };
1085
+ if (verdict.kind === "deny") return { allowed: false, reason: verdict.reason };
1086
+ const answer = await ask2(req);
1087
+ if (answer === "always") {
1088
+ state.allowed.add(req.signature);
1089
+ return { allowed: true };
1090
+ }
1091
+ if (answer === "once") return { allowed: true };
1092
+ return { allowed: false, reason: `the user denied ${req.summary}` };
1093
+ };
1094
+ }
1095
+ var MODES, MODE_LABEL, denyAll;
1096
+ var init_permissions = __esm({
1097
+ "src/permissions.ts"() {
1098
+ "use strict";
1099
+ MODES = ["normal", "acceptEdits", "plan"];
1100
+ MODE_LABEL = {
1101
+ plan: "plan mode (read-only)",
1102
+ normal: "ask before edits",
1103
+ acceptEdits: "auto-accept edits"
1104
+ };
1105
+ denyAll = async (req) => ({
1106
+ allowed: false,
1107
+ reason: `${req.summary} needs approval, but this run is non-interactive (use the REPL to approve it)`
1108
+ });
1109
+ }
1110
+ });
1111
+
1112
+ // src/session.ts
1113
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
1114
+ import { homedir as homedir3 } from "node:os";
1115
+ import { join as join4 } from "node:path";
1116
+ function newSessionId() {
1117
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "");
1118
+ return `${now}-${Math.random().toString(16).slice(2, 6)}`;
1119
+ }
1120
+ function saveSession(session) {
1121
+ try {
1122
+ mkdirSync3(SESSION_DIR, { recursive: true });
1123
+ writeFileSync3(join4(SESSION_DIR, `${session.id}.json`), JSON.stringify(session), "utf8");
1124
+ prune();
1125
+ } catch {
1126
+ }
1127
+ }
1128
+ function loadSession(id) {
1129
+ try {
1130
+ return JSON.parse(readFileSync4(join4(SESSION_DIR, `${id}.json`), "utf8"));
1131
+ } catch {
1132
+ return void 0;
1133
+ }
1134
+ }
1135
+ function listSessions(cwd, limit = 20) {
1136
+ if (!existsSync3(SESSION_DIR)) return [];
1137
+ const metas = [];
1138
+ for (const name of readdirSync3(SESSION_DIR)) {
1139
+ if (!name.endsWith(".json")) continue;
1140
+ const s = loadSession(name.slice(0, -5));
1141
+ if (!s) continue;
1142
+ if (cwd && s.cwd !== cwd) continue;
1143
+ const { messages: _messages, ...meta } = s;
1144
+ metas.push(meta);
1145
+ }
1146
+ metas.sort((a, b) => b.updated.localeCompare(a.updated));
1147
+ return metas.slice(0, limit);
1148
+ }
1149
+ function latestSession(cwd) {
1150
+ const [meta] = listSessions(cwd, 1);
1151
+ return meta ? loadSession(meta.id) : void 0;
1152
+ }
1153
+ function prune() {
1154
+ const files = readdirSync3(SESSION_DIR).filter((n) => n.endsWith(".json")).map((n) => ({ n, t: statSync2(join4(SESSION_DIR, n)).mtimeMs })).sort((a, b) => b.t - a.t);
1155
+ for (const { n } of files.slice(MAX_SESSIONS)) {
1156
+ try {
1157
+ rmSync(join4(SESSION_DIR, n));
1158
+ } catch {
1159
+ }
1160
+ }
1161
+ }
1162
+ function loadHistory() {
1163
+ try {
1164
+ return readFileSync4(HISTORY_FILE, "utf8").split("\n").filter(Boolean).slice(-MAX_HISTORY).reverse();
1165
+ } catch {
1166
+ return [];
1167
+ }
1168
+ }
1169
+ function saveHistory(entries) {
1170
+ try {
1171
+ mkdirSync3(ROOT, { recursive: true });
1172
+ writeFileSync3(HISTORY_FILE, entries.slice(0, MAX_HISTORY).reverse().join("\n") + "\n", "utf8");
1173
+ } catch {
1174
+ }
1175
+ }
1176
+ var ROOT, SESSION_DIR, HISTORY_FILE, MAX_HISTORY, MAX_SESSIONS;
1177
+ var init_session = __esm({
1178
+ "src/session.ts"() {
1179
+ "use strict";
1180
+ ROOT = join4(homedir3(), ".clixad");
1181
+ SESSION_DIR = join4(ROOT, "sessions");
1182
+ HISTORY_FILE = join4(ROOT, "history");
1183
+ MAX_HISTORY = 200;
1184
+ MAX_SESSIONS = 50;
1185
+ }
1186
+ });
1187
+
1188
+ // src/kimi.ts
1189
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
1190
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
1191
+ import { homedir as homedir4 } from "node:os";
1192
+ import { dirname as dirname3, join as join5 } from "node:path";
1193
+ function contextTokensFor(modelId) {
1194
+ return CONTEXT_TOKENS[modelId] ?? DEFAULT_CONTEXT_TOKENS;
1195
+ }
1196
+ function toml(value) {
1197
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1198
+ }
1199
+ function buildKimiConfig({ gatewayUrl, token, modelId }) {
1200
+ const baseUrl = `${gatewayUrl.replace(/\/+$/, "")}/v1`;
1201
+ const context = contextTokensFor(modelId);
1202
+ return [
1203
+ "# Clixad Code \u2014 auto-generated. Points Kimi CLI at the Clixad gateway",
1204
+ "# (OpenAI-compatible). Regenerated on every `clixad code` run.",
1205
+ `default_model = ${toml(modelId)}`,
1206
+ "default_yolo = false",
1207
+ "telemetry = false",
1208
+ "",
1209
+ "[providers.clixad]",
1210
+ 'type = "openai_legacy"',
1211
+ `base_url = ${toml(baseUrl)}`,
1212
+ `api_key = ${toml(token)}`,
1213
+ "",
1214
+ `[models.${toml(modelId)}]`,
1215
+ 'provider = "clixad"',
1216
+ `model = ${toml(modelId)}`,
1217
+ `max_context_size = ${context}`,
1218
+ ""
1219
+ ].join("\n");
1220
+ }
1221
+ function kimiConfigPath() {
1222
+ return join5(homedir4(), ".clixad", "kimi.config.toml");
1223
+ }
1224
+ function runKimiCode(config, task, opts = {}) {
1225
+ if (!config.token) {
1226
+ console.log("Not logged in. Run `clixad login` first.");
1227
+ return Promise.resolve(1);
1228
+ }
1229
+ const path = kimiConfigPath();
1230
+ mkdirSync4(dirname3(path), { recursive: true });
1231
+ writeFileSync4(path, buildKimiConfig({ gatewayUrl: config.gatewayUrl, token: config.token, modelId: config.model }), "utf8");
1232
+ const bin = process.env.CLIXAD_KIMI_BIN || "kimi";
1233
+ if (!binExists(bin)) {
1234
+ console.log("\n" + INSTALL_HELP);
1235
+ return Promise.resolve(127);
1236
+ }
1237
+ const args = ["--config-file", path];
1238
+ if (task) args.push("-p", task);
1239
+ if (opts.passthrough?.length) args.push(...opts.passthrough);
1240
+ const env = {
1241
+ ...process.env,
1242
+ OPENAI_BASE_URL: `${config.gatewayUrl.replace(/\/+$/, "")}/v1`,
1243
+ OPENAI_API_KEY: config.token
1244
+ };
1245
+ return new Promise((resolve2) => {
1246
+ const child = process.platform === "win32" ? spawn2([bin, ...args].map(quoteWin).join(" "), { stdio: "inherit", env, shell: true }) : spawn2(bin, args, { stdio: "inherit", env });
1247
+ child.on("error", (err) => {
1248
+ if (err.code === "ENOENT") console.log("\n" + INSTALL_HELP);
1249
+ else console.error(`failed to launch Kimi CLI: ${err.message}`);
1250
+ resolve2(127);
1251
+ });
1252
+ child.on("exit", (code) => resolve2(code ?? 0));
1253
+ });
1254
+ }
1255
+ function quoteWin(s) {
1256
+ return /[\s"]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
1257
+ }
1258
+ function binExists(bin) {
1259
+ if (bin.includes("/") || bin.includes("\\")) {
1260
+ return existsSync4(bin) || existsSync4(`${bin}.exe`) || existsSync4(`${bin}.cmd`);
1261
+ }
1262
+ const probe = process.platform === "win32" ? spawnSync2("where", [bin], { stdio: "ignore" }) : spawnSync2("sh", ["-c", `command -v "${bin}"`], { stdio: "ignore" });
1263
+ return probe.status === 0;
1264
+ }
1265
+ var CONTEXT_TOKENS, DEFAULT_CONTEXT_TOKENS, INSTALL_HELP;
1266
+ var init_kimi = __esm({
1267
+ "src/kimi.ts"() {
1268
+ "use strict";
1269
+ CONTEXT_TOKENS = {
1270
+ "gpt-5-nano": 4e5,
1271
+ "gemini-2.5-flash-lite": 1e6,
1272
+ "deepseek-v4-flash": 1048576,
1273
+ "gemini-3.1-flash-lite": 1048576,
1274
+ "kimi-k2": 131072,
1275
+ "kimi-k2.7-code": 262144,
1276
+ "claude-haiku-4.5": 2e5,
1277
+ "gemini-3.6-flash": 1048576,
1278
+ "claude-sonnet-5": 1e6,
1279
+ "gpt-5.2": 4e5,
1280
+ "kimi-k3": 1048576,
1281
+ "claude-opus-5": 1e6,
1282
+ "gpt-5.5": 105e4,
1283
+ "gpt-5.2-pro": 4e5
1284
+ };
1285
+ DEFAULT_CONTEXT_TOKENS = 256e3;
1286
+ INSTALL_HELP = `Kimi CLI is not installed (or not on PATH).
1287
+
1288
+ Clixad Code runs the open-source Kimi CLI (MoonshotAI, Apache-2.0) against your
1289
+ Clixad gateway. Install it once, then re-run \`clixad code\`:
1290
+
1291
+ uv tool install kimi-cli # recommended (https://docs.astral.sh/uv/)
1292
+ # or:
1293
+ pipx install kimi-cli
1294
+
1295
+ If it's installed under a different name/path, set CLIXAD_KIMI_BIN.`;
1296
+ }
1297
+ });
1298
+
1299
+ // src/banner.ts
1300
+ import { homedir as homedir5 } from "node:os";
1301
+ function duckLines() {
1302
+ const lines = [];
1303
+ for (let row = 0; row < DUCK.length; row += 2) {
1304
+ const top = DUCK[row];
1305
+ const bot = DUCK[row + 1] ?? "..............";
1306
+ let out = "";
1307
+ for (let col = 0; col < DUCK_W; col++) {
1308
+ const t = PAL[top[col] ?? "."];
1309
+ const b = PAL[bot[col] ?? "."];
1310
+ if (t && b) out += fg(...t) + bg(...b) + "\u2580" + R2;
1311
+ else if (t) out += fg(...t) + "\u2580" + R2;
1312
+ else if (b) out += fg(...b) + "\u2584" + R2;
1313
+ else out += " ";
1314
+ }
1315
+ lines.push(out);
1316
+ }
1317
+ return lines;
1318
+ }
1319
+ function centered(rendered, visibleW, colW) {
1320
+ const lead = Math.max(0, Math.floor((colW - visibleW) / 2));
1321
+ return cell(" ".repeat(lead) + rendered, lead + visibleW);
1322
+ }
1323
+ function earnedLabel(opts) {
1324
+ return `$${opts.earnedUsdToday.toFixed(2)}/$${opts.maxRewardUsd.toFixed(2)} today`;
1325
+ }
1326
+ function welcomeBanner(opts) {
1327
+ const width = Math.max(MIN_BANNER_WIDTH, opts.width ?? MIN_BANNER_WIDTH);
1328
+ const inner = width - 7;
1329
+ const rightW = Math.min(RIGHT_MAX, Math.max(26, inner - 30));
1330
+ const leftW = inner - rightW;
1331
+ const cwd = opts.cwd.replace(homedir5(), "~").replace(/\\/g, "/");
1332
+ const welcome = opts.name ? `Welcome back, ${truncate(opts.name, 18)}!` : "Welcome to Clixad!";
1333
+ const duck = duckLines();
1334
+ const model = truncate(opts.model, leftW - 4);
1335
+ const home = truncate(cwd, leftW);
1336
+ const left = [
1337
+ centered(BOLD + TEXT + welcome + R2, welcome.length, leftW),
1338
+ cell("", 0),
1339
+ ...duck.map((d) => centered(d, DUCK_W, leftW)),
1340
+ cell("", 0),
1341
+ centered(MANGO + "\u25C6 " + R2 + TEXT + model + R2, 2 + model.length, leftW),
1342
+ centered(FAINT + home + R2, home.length, leftW)
1343
+ ];
1344
+ const tip = (k, d) => cell(MANGO + k + R2 + FAINT + " " + d + R2, k.length + 1 + d.length);
1345
+ const right = [
1346
+ cell(BOLD + MANGO + "Getting started" + R2, 15),
1347
+ tip("/help", "commands"),
1348
+ tip("/model", "switch model"),
1349
+ tip("/earn", "earn credits"),
1350
+ tip("/wallet", "balance & ads"),
1351
+ cell("", 0),
1352
+ cell(BOLD + MANGO + "Wallet" + R2, 6),
1353
+ cell(TEXT + `${opts.balance.toLocaleString("en-US")} credits` + R2, `${opts.balance.toLocaleString("en-US")} credits`.length),
1354
+ cell(MUTED + earnedLabel(opts) + R2, earnedLabel(opts).length)
1355
+ ];
1356
+ const rows = Math.max(left.length, right.length);
1357
+ const out = [];
1358
+ const label = `Clixad ${opts.version}`;
1359
+ const topPrefixLen = 3 + label.length + 1;
1360
+ const topDashes = width - topPrefixLen - 1;
1361
+ out.push(MANGO + `\u256D\u2500 ${BOLD}${label}${R2}${MANGO} ` + "\u2500".repeat(Math.max(0, topDashes)) + "\u256E" + R2);
1362
+ for (let i = 0; i < rows; i++) {
1363
+ const l = padTo(left[i], leftW);
1364
+ const r = padTo(right[i], rightW);
1365
+ out.push(`${MANGO}\u2502${R2} ${l} ${MANGO}\u2502${R2} ${r} ${MANGO}\u2502${R2}`);
1366
+ }
1367
+ out.push(MANGO + "\u2570" + "\u2500".repeat(width - 2) + "\u256F" + R2);
1368
+ return out.join("\n");
1369
+ }
1370
+ function compactBanner(opts) {
1371
+ const welcome = opts.name ? `Welcome back, ${opts.name}!` : "Welcome to Clixad!";
1372
+ return duckLines().map((d) => " " + d).join("\n") + `
1373
+
1374
+ ${BOLD}${TEXT}${welcome}${R2}
1375
+ ${MANGO}\u25C6${R2} ${opts.model} ${FAINT}\xB7${R2} ${opts.balance.toLocaleString("en-US")} credits
1376
+ `;
1377
+ }
1378
+ function clearScreen() {
1379
+ const out = process.stdout;
1380
+ if (!out.isTTY && !process.stdin.isTTY) return;
1381
+ out.write("\n".repeat(out.rows ?? 40));
1382
+ out.write("\x1B[2J\x1B[3J\x1B[H\x1B[0f");
1383
+ }
1384
+ function visibleLength(s) {
1385
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
1386
+ }
1387
+ var R2, BOLD, fg, bg, MANGO, TEXT, MUTED, FAINT, PAL, DUCK, DUCK_W, cell, padTo, truncate, MIN_BANNER_WIDTH, RIGHT_MAX;
1388
+ var init_banner = __esm({
1389
+ "src/banner.ts"() {
1390
+ "use strict";
1391
+ R2 = "\x1B[0m";
1392
+ BOLD = "\x1B[1m";
1393
+ fg = (r, g, b) => `\x1B[38;2;${r};${g};${b}m`;
1394
+ bg = (r, g, b) => `\x1B[48;2;${r};${g};${b}m`;
1395
+ MANGO = fg(245, 184, 65);
1396
+ TEXT = fg(232, 240, 251);
1397
+ MUTED = fg(150, 165, 195);
1398
+ FAINT = fg(95, 112, 148);
1399
+ PAL = {
1400
+ Y: [245, 184, 65],
1401
+ // mango body
1402
+ y: [255, 210, 115],
1403
+ // highlight
1404
+ O: [226, 126, 42],
1405
+ // beak / feet
1406
+ k: [30, 26, 40]
1407
+ // eye
1408
+ };
1409
+ DUCK = [
1410
+ "....yYYYY.....",
1411
+ "...yYYYYYY....",
1412
+ "..YYYYYYYYY...",
1413
+ "..YYYYYYkYY...",
1414
+ "..YYYYYYYYYOO.",
1415
+ "..YYYYYYYYYOO.",
1416
+ ".YYYYYYYYYYY..",
1417
+ "YYYYYYYYYYYYY.",
1418
+ "YYYYYYYYYYYYYY",
1419
+ "YYYYYYYYYYYYYY",
1420
+ ".YYYYYYYYYYYY.",
1421
+ "..YYYYYYYYYY..",
1422
+ "...OO....OO...",
1423
+ ".............."
1424
+ ];
1425
+ DUCK_W = 14;
1426
+ cell = (text, w) => ({ text, w });
1427
+ padTo = (c2, width) => (c2?.text ?? "") + " ".repeat(Math.max(0, width - (c2?.w ?? 0)));
1428
+ truncate = (s, max) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1429
+ MIN_BANNER_WIDTH = 66;
1430
+ RIGHT_MAX = 34;
1431
+ }
1432
+ });
1433
+
1434
+ // src/browser.ts
1435
+ import { spawn as spawn3 } from "node:child_process";
1436
+ function openBrowser(url) {
1437
+ const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
1438
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
1439
+ spawn3(cmd, args, { detached: true, stdio: "ignore", windowsHide: true }).unref();
1440
+ }
1441
+ var init_browser = __esm({
1442
+ "src/browser.ts"() {
1443
+ "use strict";
1444
+ }
1445
+ });
1446
+
1447
+ // src/compact.ts
1448
+ function estimateTokens(text) {
1449
+ return Math.ceil(text.length / 4);
1450
+ }
1451
+ function conversationTokens(messages) {
1452
+ let total = 0;
1453
+ for (const m of messages) {
1454
+ total += estimateTokens(m.content ?? "");
1455
+ for (const call of m.tool_calls ?? []) total += estimateTokens(call.function.arguments) + 8;
1456
+ total += 4;
1457
+ }
1458
+ return total;
1459
+ }
1460
+ function shouldCompact(messages, contextTokens, threshold = COMPACT_THRESHOLD) {
1461
+ return conversationTokens(messages) > contextTokens * threshold;
1462
+ }
1463
+ function cutPoint(messages, keepTurns = 2) {
1464
+ let seen = 0;
1465
+ for (let i = messages.length - 1; i > 0; i--) {
1466
+ if (messages[i].role !== "user") continue;
1467
+ seen++;
1468
+ if (seen >= keepTurns) return i;
1469
+ }
1470
+ return 0;
1471
+ }
1472
+ async function compact(client, model, messages, opts = {}) {
1473
+ const cut = cutPoint(messages, opts.keepTurns ?? 2);
1474
+ if (cut <= 1) return void 0;
1475
+ const before = conversationTokens(messages);
1476
+ const turn = await client.chatStream(
1477
+ [...messages.slice(0, cut), { role: "user", content: SUMMARY_PROMPT }],
1478
+ model,
1479
+ { signal: opts.signal }
1480
+ );
1481
+ const summary = turn.message.content?.trim() || "(no summary returned)";
1482
+ const compacted = [
1483
+ messages[0],
1484
+ { role: "user", content: `Summary of the earlier conversation:
1485
+
1486
+ ${summary}` },
1487
+ { role: "assistant", content: "Understood \u2014 continuing from that summary." },
1488
+ ...messages.slice(cut)
1489
+ ];
1490
+ return {
1491
+ messages: compacted,
1492
+ summary,
1493
+ creditsCharged: turn.creditsCharged,
1494
+ balance: turn.balance,
1495
+ before,
1496
+ after: conversationTokens(compacted)
1497
+ };
1498
+ }
1499
+ var COMPACT_THRESHOLD, SUMMARY_PROMPT;
1500
+ var init_compact = __esm({
1501
+ "src/compact.ts"() {
1502
+ "use strict";
1503
+ COMPACT_THRESHOLD = 0.75;
1504
+ SUMMARY_PROMPT = "Summarise the conversation above for your future self, which will continue the work without seeing it. Cover: what the user asked for, what was already changed (files and the reasoning), what was learned about the codebase, and what is still open. Be specific about file paths and names. No preamble.";
1505
+ }
1506
+ });
1507
+
1508
+ // src/tui/commands.ts
1509
+ function commandLabel(c2) {
1510
+ return `/${c2.name}${c2.args ? ` ${c2.args}` : ""}`;
1511
+ }
1512
+ function helpText() {
1513
+ const w = Math.max(...COMMANDS.map((c2) => commandLabel(c2).length));
1514
+ return COMMANDS.map((c2) => ` ${commandLabel(c2).padEnd(w + 2)}${c2.desc}`).join("\n") + "\n\n" + [
1515
+ " @path reference a file (tab completes)",
1516
+ " shift+tab cycle permission mode \xB7 esc stop the current turn",
1517
+ " ctrl+o expand the last tool output \xB7 ctrl+c twice quit",
1518
+ " \\ + enter continue on a new line"
1519
+ ].join("\n");
1520
+ }
1521
+ var COMMANDS;
1522
+ var init_commands = __esm({
1523
+ "src/tui/commands.ts"() {
1524
+ "use strict";
1525
+ COMMANDS = [
1526
+ { name: "help", desc: "list the commands" },
1527
+ { name: "model", desc: "switch model" },
1528
+ { name: "models", desc: "list models + prices" },
1529
+ { name: "mode", args: "[name]", desc: "permission mode (or press shift+tab)" },
1530
+ { name: "wallet", desc: "balance & ads today" },
1531
+ { name: "earn", desc: "earn credits (watch an ad)" },
1532
+ { name: "compact", desc: "summarise the conversation to free context" },
1533
+ { name: "clear", desc: "clear the conversation context" },
1534
+ { name: "init", desc: "write a CLIXAD.md for this project" },
1535
+ { name: "exit", desc: "quit clixad" }
1536
+ ];
1537
+ }
1538
+ });
1539
+
1540
+ // src/tui/editor.ts
1541
+ function fromText(text, cursorAtEnd = true) {
1542
+ const lines = text.split("\n");
1543
+ const row = cursorAtEnd ? lines.length - 1 : 0;
1544
+ return { lines, row, col: cursorAtEnd ? lines[row].length : 0 };
1545
+ }
1546
+ function fromTextAt(text, cursor) {
1547
+ const before = text.slice(0, cursor).split("\n");
1548
+ return { lines: text.split("\n"), row: before.length - 1, col: before[before.length - 1].length };
1549
+ }
1550
+ function toText(state) {
1551
+ return state.lines.join("\n");
1552
+ }
1553
+ function isEmpty(state) {
1554
+ return state.lines.length === 1 && state.lines[0] === "";
1555
+ }
1556
+ function offset(state) {
1557
+ let n = 0;
1558
+ for (let i = 0; i < state.row; i++) n += state.lines[i].length + 1;
1559
+ return n + state.col;
1560
+ }
1561
+ function endsWithContinuation(state) {
1562
+ const line2 = state.lines[state.row] ?? "";
1563
+ return state.col === line2.length && /(^|[^\\])(\\\\)*\\$/.test(line2);
1564
+ }
1565
+ function apply(state, key) {
1566
+ if (key.ctrl) {
1567
+ switch (key.input) {
1568
+ case "a":
1569
+ return { ...state, col: 0 };
1570
+ case "e":
1571
+ return { ...state, col: line(state).length };
1572
+ case "k":
1573
+ return replaceLine(state, line(state).slice(0, state.col));
1574
+ case "u":
1575
+ return { ...replaceLine(state, line(state).slice(state.col)), col: 0 };
1576
+ case "w":
1577
+ return deleteWordLeft(state);
1578
+ default:
1579
+ break;
1580
+ }
1581
+ }
1582
+ if (key.meta && (key.name === "left" || key.name === "right")) {
1583
+ const col = key.name === "left" ? wordLeft(line(state), state.col) : wordRight(line(state), state.col);
1584
+ return { ...state, col };
1585
+ }
1586
+ switch (key.name) {
1587
+ case "left":
1588
+ if (state.col > 0) return { ...state, col: state.col - 1 };
1589
+ if (state.row > 0) return { ...state, row: state.row - 1, col: state.lines[state.row - 1].length };
1590
+ return state;
1591
+ case "right":
1592
+ if (state.col < line(state).length) return { ...state, col: state.col + 1 };
1593
+ if (state.row < state.lines.length - 1) return { ...state, row: state.row + 1, col: 0 };
1594
+ return state;
1595
+ case "up": {
1596
+ if (state.row === 0) return state;
1597
+ const row = state.row - 1;
1598
+ return { ...state, row, col: Math.min(state.col, state.lines[row].length) };
1599
+ }
1600
+ case "down": {
1601
+ if (state.row >= state.lines.length - 1) return state;
1602
+ const row = state.row + 1;
1603
+ return { ...state, row, col: Math.min(state.col, state.lines[row].length) };
1604
+ }
1605
+ case "home":
1606
+ return { ...state, col: 0 };
1607
+ case "end":
1608
+ return { ...state, col: line(state).length };
1609
+ case "backspace":
1610
+ return backspace(state);
1611
+ case "delete":
1612
+ return del(state);
1613
+ case "newline":
1614
+ return newline(state);
1615
+ default:
1616
+ break;
1617
+ }
1618
+ if (key.input && !key.ctrl && !key.meta) return insert(state, key.input);
1619
+ return state;
1620
+ }
1621
+ function insert(state, text) {
1622
+ const clean = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
1623
+ const safe = [...clean].filter((ch) => ch === "\n" || isPrintable(ch)).join("");
1624
+ if (!safe) return state;
1625
+ const current = line(state);
1626
+ const head = current.slice(0, state.col) + safe;
1627
+ const tail = current.slice(state.col);
1628
+ const inserted = (head + tail).split("\n");
1629
+ const lines = [...state.lines.slice(0, state.row), ...inserted, ...state.lines.slice(state.row + 1)];
1630
+ const row = state.row + inserted.length - 1;
1631
+ const col = inserted[inserted.length - 1].length - tail.length;
1632
+ return { lines, row, col };
1633
+ }
1634
+ function newline(state) {
1635
+ const current = line(state);
1636
+ const lines = [
1637
+ ...state.lines.slice(0, state.row),
1638
+ current.slice(0, state.col),
1639
+ current.slice(state.col),
1640
+ ...state.lines.slice(state.row + 1)
1641
+ ];
1642
+ return { lines, row: state.row + 1, col: 0 };
1643
+ }
1644
+ function continueLine(state) {
1645
+ const current = line(state);
1646
+ const trimmed = { ...replaceLine(state, current.slice(0, -1)), col: current.length - 1 };
1647
+ return newline(trimmed);
1648
+ }
1649
+ function backspace(state) {
1650
+ if (state.col > 0) {
1651
+ const current = line(state);
1652
+ return { ...replaceLine(state, current.slice(0, state.col - 1) + current.slice(state.col)), col: state.col - 1 };
1653
+ }
1654
+ if (state.row === 0) return state;
1655
+ const prev = state.lines[state.row - 1];
1656
+ const lines = [
1657
+ ...state.lines.slice(0, state.row - 1),
1658
+ prev + line(state),
1659
+ ...state.lines.slice(state.row + 1)
1660
+ ];
1661
+ return { lines, row: state.row - 1, col: prev.length };
1662
+ }
1663
+ function del(state) {
1664
+ const current = line(state);
1665
+ if (state.col < current.length) {
1666
+ return replaceLine(state, current.slice(0, state.col) + current.slice(state.col + 1));
1667
+ }
1668
+ if (state.row >= state.lines.length - 1) return state;
1669
+ const lines = [
1670
+ ...state.lines.slice(0, state.row),
1671
+ current + state.lines[state.row + 1],
1672
+ ...state.lines.slice(state.row + 2)
1673
+ ];
1674
+ return { ...state, lines };
1675
+ }
1676
+ function deleteWordLeft(state) {
1677
+ if (state.col === 0) return backspace(state);
1678
+ const current = line(state);
1679
+ const start = wordLeft(current, state.col);
1680
+ return { ...replaceLine(state, current.slice(0, start) + current.slice(state.col)), col: start };
1681
+ }
1682
+ function wordLeft(text, col) {
1683
+ let i = col;
1684
+ while (i > 0 && isSep(text[i - 1])) i--;
1685
+ while (i > 0 && !isSep(text[i - 1])) i--;
1686
+ return i;
1687
+ }
1688
+ function wordRight(text, col) {
1689
+ let i = col;
1690
+ while (i < text.length && isSep(text[i])) i++;
1691
+ while (i < text.length && !isSep(text[i])) i++;
1692
+ return i;
1693
+ }
1694
+ function isPrintable(ch) {
1695
+ const code = ch.codePointAt(0) ?? 0;
1696
+ return code >= 32 && code !== 127;
1697
+ }
1698
+ function isSep(ch) {
1699
+ return /[\s/\\.,:;()[\]{}'"`]/.test(ch);
1700
+ }
1701
+ function line(state) {
1702
+ return state.lines[state.row] ?? "";
1703
+ }
1704
+ function replaceLine(state, text) {
1705
+ const lines = [...state.lines];
1706
+ lines[state.row] = text;
1707
+ return { ...state, lines, col: Math.min(state.col, text.length) };
1708
+ }
1709
+ var EMPTY;
1710
+ var init_editor = __esm({
1711
+ "src/tui/editor.ts"() {
1712
+ "use strict";
1713
+ EMPTY = { lines: [""], row: 0, col: 0 };
1714
+ }
1715
+ });
1716
+
1717
+ // src/tui/suggest.ts
1718
+ function findToken(text, cursor) {
1719
+ const upto = text.slice(0, cursor);
1720
+ const start = Math.max(upto.lastIndexOf(" "), upto.lastIndexOf("\n")) + 1;
1721
+ const token = upto.slice(start);
1722
+ if (token.startsWith("/")) {
1723
+ return start === 0 ? { kind: "command", start, term: token.slice(1) } : void 0;
1724
+ }
1725
+ if (token.startsWith("@")) return { kind: "file", start, term: token.slice(1) };
1726
+ return void 0;
1727
+ }
1728
+ function suggestCommands(term) {
1729
+ const lower = term.toLowerCase();
1730
+ return COMMANDS.filter((c2) => c2.name.startsWith(lower)).map(toSuggestion);
1731
+ }
1732
+ function toSuggestion(c2) {
1733
+ return {
1734
+ kind: "command",
1735
+ value: `/${c2.name}`,
1736
+ label: commandLabel(c2),
1737
+ hint: c2.desc,
1738
+ takesArgs: Boolean(c2.args)
1739
+ };
1740
+ }
1741
+ function suggestFiles(term, files) {
1742
+ const lower = term.toLowerCase();
1743
+ const scored = [];
1744
+ for (const path of files) {
1745
+ const lowerPath = path.toLowerCase();
1746
+ const name = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
1747
+ let score;
1748
+ if (!lower) score = 2;
1749
+ else if (name.startsWith(lower)) score = 0;
1750
+ else if (lowerPath.startsWith(lower)) score = 1;
1751
+ else if (lowerPath.includes(lower)) score = 2;
1752
+ else continue;
1753
+ scored.push({ path, score });
1754
+ }
1755
+ scored.sort((a, b) => a.score - b.score || a.path.length - b.path.length || a.path.localeCompare(b.path));
1756
+ return scored.slice(0, MAX_SUGGESTIONS).map(({ path }) => ({
1757
+ kind: "file",
1758
+ value: `@${path}`,
1759
+ label: path.slice(path.lastIndexOf("/") + 1),
1760
+ hint: path
1761
+ }));
1762
+ }
1763
+ function suggest(query, sources) {
1764
+ return query.kind === "command" ? suggestCommands(query.term).slice(0, MAX_SUGGESTIONS) : suggestFiles(query.term, sources.files());
1765
+ }
1766
+ function applySuggestion(text, cursor, query, choice) {
1767
+ const head = text.slice(0, query.start) + choice.value + " ";
1768
+ return { text: head + text.slice(cursor), cursor: head.length };
1769
+ }
1770
+ var MAX_SUGGESTIONS;
1771
+ var init_suggest = __esm({
1772
+ "src/tui/suggest.ts"() {
1773
+ "use strict";
1774
+ init_commands();
1775
+ MAX_SUGGESTIONS = 10;
1776
+ }
1777
+ });
1778
+
1779
+ // src/tui/markdown.ts
1780
+ function renderMarkdown(text) {
1781
+ const out = [];
1782
+ let inFence = false;
1783
+ let fenceLang = "";
1784
+ for (const raw of text.split("\n")) {
1785
+ const fence = /^\s*```(.*)$/.exec(raw);
1786
+ if (fence) {
1787
+ if (!inFence) {
1788
+ inFence = true;
1789
+ fenceLang = fence[1].trim();
1790
+ if (fenceLang) out.push(`${DIM2} \u250C\u2500 ${fenceLang}${R3}`);
1791
+ else out.push(`${DIM2} \u250C\u2500${R3}`);
1792
+ } else {
1793
+ inFence = false;
1794
+ out.push(`${DIM2} \u2514\u2500${R3}`);
1795
+ }
1796
+ continue;
1797
+ }
1798
+ if (inFence) {
1799
+ out.push(`${DIM2} \u2502 ${R3}${CODE}${raw}${R3}`);
1800
+ continue;
1801
+ }
1802
+ const heading = /^(#{1,6})\s+(.*)$/.exec(raw);
1803
+ if (heading) {
1804
+ out.push(`${BOLD2}${MANGO2}${heading[2]}${R3}`);
1805
+ continue;
1806
+ }
1807
+ const rule = /^\s*(-{3,}|\*{3,}|_{3,})\s*$/.exec(raw);
1808
+ if (rule) {
1809
+ out.push(`${DIM2}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${R3}`);
1810
+ continue;
1811
+ }
1812
+ const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(raw);
1813
+ if (bullet) {
1814
+ out.push(`${bullet[1]}${MANGO2}\u2022${R3} ${inline(bullet[2])}`);
1815
+ continue;
1816
+ }
1817
+ if (/^\s*>\s?/.test(raw)) {
1818
+ out.push(`${DIM2}${raw.replace(/^\s*>\s?/, "\u258F ")}${R3}`);
1819
+ continue;
1820
+ }
1821
+ out.push(inline(raw));
1822
+ }
1823
+ return out.join("\n");
1824
+ }
1825
+ function inline(text) {
1826
+ return text.replace(/`([^`]+)`/g, (_m, code) => `${CODE}${code}${R3}`).replace(/\*\*([^*]+)\*\*/g, (_m, bold) => `${BOLD2}${bold}${R3}`).replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, (_m, pre, it) => `${pre}${ITALIC}${it}${R3}`);
1827
+ }
1828
+ var R3, BOLD2, DIM2, ITALIC, MANGO2, CODE;
1829
+ var init_markdown = __esm({
1830
+ "src/tui/markdown.ts"() {
1831
+ "use strict";
1832
+ R3 = "\x1B[0m";
1833
+ BOLD2 = "\x1B[1m";
1834
+ DIM2 = "\x1B[2m";
1835
+ ITALIC = "\x1B[3m";
1836
+ MANGO2 = "\x1B[38;2;245;184;65m";
1837
+ CODE = "\x1B[38;2;150;200;255m";
1838
+ }
1839
+ });
1840
+
1841
+ // src/tui/views.tsx
1842
+ import "react";
1843
+ import { Box, Text } from "ink";
1844
+ import { jsx, jsxs } from "react/jsx-runtime";
1845
+ function lineCount(text, cols) {
1846
+ return text.split("\n").reduce((n, line2) => n + Math.max(1, Math.ceil(visibleLength(line2) / Math.max(1, cols))), 0);
1847
+ }
1848
+ function entryHeight(entry, cols) {
1849
+ const margin = entry.kind === "banner" ? 0 : 1;
1850
+ switch (entry.kind) {
1851
+ case "assistant":
1852
+ return margin + lineCount(renderMarkdown(entry.text), cols) + (entry.meta ? 1 : 0);
1853
+ case "tool":
1854
+ return margin + 1 + (entry.output ? lineCount(entry.output, cols - 4) : 0) + (entry.outputMore ? 1 : 0);
1855
+ default:
1856
+ return margin + lineCount(entry.text, cols);
1857
+ }
1858
+ }
1859
+ function EntryView({ entry }) {
1860
+ switch (entry.kind) {
1861
+ case "banner":
1862
+ return /* @__PURE__ */ jsx(Text, { children: entry.text });
1863
+ case "user":
1864
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1865
+ /* @__PURE__ */ jsx(Text, { color: MANGO_BRIGHT, children: "\u276F " }),
1866
+ /* @__PURE__ */ jsx(Text, { bold: true, children: entry.text })
1867
+ ] });
1868
+ case "assistant":
1869
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1870
+ /* @__PURE__ */ jsx(Text, { children: renderMarkdown(entry.text) }),
1871
+ entry.meta ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${entry.meta}` }) : null
1872
+ ] });
1873
+ case "tool":
1874
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1875
+ /* @__PURE__ */ jsxs(Box, { children: [
1876
+ /* @__PURE__ */ jsx(Text, { color: entry.ok === false ? "red" : MANGO3, children: "\u23FA " }),
1877
+ /* @__PURE__ */ jsx(Text, { bold: true, children: entry.name }),
1878
+ entry.summary ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${entry.summary}` }) : null
1879
+ ] }),
1880
+ entry.output ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.output.split("\n").map((l) => ` ${l}`).join("\n") }) : null,
1881
+ entry.outputMore ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2026 +${entry.outputMore} lines (ctrl+o)` }) : null
1882
+ ] });
1883
+ default:
1884
+ return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(
1885
+ Text,
1886
+ {
1887
+ color: entry.tone === "error" ? "red" : entry.tone === "warn" ? "yellow" : entry.tone === "good" ? "green" : void 0,
1888
+ dimColor: !entry.tone,
1889
+ children: entry.text
1890
+ }
1891
+ ) });
1892
+ }
1893
+ }
1894
+ var MANGO3, MANGO_BRIGHT;
1895
+ var init_views = __esm({
1896
+ "src/tui/views.tsx"() {
1897
+ "use strict";
1898
+ init_banner();
1899
+ init_markdown();
1900
+ MANGO3 = "#f5b841";
1901
+ MANGO_BRIGHT = "#ffcf6b";
1902
+ }
1903
+ });
1904
+
1905
+ // src/tui/app.tsx
1906
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1907
+ import { Box as Box2, Static, Text as Text2, useApp, useInput } from "ink";
1908
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1909
+ function useTerminalSize() {
1910
+ const [size, setSize] = useState({
1911
+ rows: process.stdout.rows ?? 24,
1912
+ cols: process.stdout.columns ?? 80
1913
+ });
1914
+ useEffect(() => {
1915
+ const onResize = () => setSize({ rows: process.stdout.rows ?? 24, cols: process.stdout.columns ?? 80 });
1916
+ process.stdout.on("resize", onResize);
1917
+ return () => {
1918
+ process.stdout.off("resize", onResize);
1919
+ };
1920
+ }, []);
1921
+ return size;
1922
+ }
1923
+ function App({ client, config, wallet, session, initialTask }) {
1924
+ const { exit } = useApp();
1925
+ const idRef = useRef(1);
1926
+ const { rows, cols } = useTerminalSize();
1927
+ const root = process.cwd();
1928
+ const [entries, setEntries] = useState(() => [
1929
+ {
1930
+ id: 0,
1931
+ kind: "banner",
1932
+ text: ((process.stdout.columns ?? 80) >= MIN_BANNER_WIDTH ? welcomeBanner : compactBanner)({
1933
+ width: process.stdout.columns ?? 80,
1934
+ name: config.login ?? config.email?.split("@")[0],
1935
+ model: config.model,
1936
+ balance: wallet?.balance ?? 0,
1937
+ adsToday: wallet?.ads_today ?? 0,
1938
+ earnedUsdToday: wallet?.earned_usd_today ?? 0,
1939
+ maxRewardUsd: wallet?.max_reward_usd_per_day ?? 0,
1940
+ cwd: process.cwd(),
1941
+ version: "v0.0.1"
1942
+ })
1943
+ }
1944
+ ]);
1945
+ const [messages, setMessages] = useState(session?.messages ?? []);
1946
+ const [editor, setEditor] = useState(EMPTY);
1947
+ const [busy, setBusy] = useState(false);
1948
+ const [live, setLive] = useState(null);
1949
+ const [balance, setBalance] = useState(wallet?.balance ?? 0);
1950
+ const [spent, setSpent] = useState(0);
1951
+ const [model, setModel2] = useState(config.model);
1952
+ const [mode, setMode] = useState("normal");
1953
+ const [hist, setHist] = useState(() => loadHistory());
1954
+ const [histIdx, setHistIdx] = useState(-1);
1955
+ const [sel, setSel] = useState(0);
1956
+ const [menuOff, setMenuOff] = useState(false);
1957
+ const [ask2, setAsk] = useState(null);
1958
+ const [picker, setPicker] = useState(null);
1959
+ const [pickerSel, setPickerSel] = useState(0);
1960
+ const [tick, setTick] = useState(0);
1961
+ const [startedAt, setStartedAt] = useState(0);
1962
+ const [quitHint, setQuitHint] = useState(false);
1963
+ const messagesRef = useRef(messages);
1964
+ messagesRef.current = messages;
1965
+ const permRef = useRef(createState("normal"));
1966
+ const abortRef = useRef(null);
1967
+ const filesRef = useRef(null);
1968
+ const ctrlCRef = useRef(0);
1969
+ const sessionRef = useRef(session ?? { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() });
1970
+ const runStartedAtRef = useRef(Date.now());
1971
+ const contextRef = useRef(collectContext(root));
1972
+ const catalogRef = useRef([]);
1973
+ const lastOutputRef = useRef("");
1974
+ const runningToolRef = useRef(null);
1975
+ const pendingTaskRef = useRef(null);
1976
+ const push = useCallback((e) => {
1977
+ setEntries((prev) => [...prev, { ...e, id: idRef.current++ }]);
1978
+ }, []);
1979
+ useEffect(() => {
1980
+ const files = contextRef.current.files;
1981
+ if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
1982
+ if (session?.messages.length) {
1983
+ push({ kind: "notice", text: ` resumed session ${session.id} (${session.messages.length} messages)` });
1984
+ }
1985
+ client.models().then((m) => catalogRef.current = m).catch(() => void 0);
1986
+ }, []);
1987
+ useEffect(() => {
1988
+ if (!busy) return;
1989
+ const t = setInterval(() => setTick((n) => n + 1), 120);
1990
+ return () => clearInterval(t);
1991
+ }, [busy]);
1992
+ const contextWindow = useCallback(
1993
+ (id) => catalogRef.current.find((m) => m.id === id)?.context_window ?? contextTokensFor(id),
1994
+ []
1995
+ );
1996
+ const askUser = useCallback(
1997
+ (req) => new Promise((resolve2) => setAsk({ req, resolve: resolve2 })),
1998
+ []
1999
+ );
2000
+ const permit = useMemo(() => createPermit(() => permRef.current, askUser), [askUser]);
2001
+ const cycleMode = useCallback(() => {
2002
+ const next = nextMode(permRef.current.mode);
2003
+ permRef.current.mode = next;
2004
+ setMode(next);
2005
+ }, []);
2006
+ const handleEvent = useCallback(
2007
+ (event) => {
2008
+ switch (event.type) {
2009
+ case "delta":
2010
+ setLive((l) => ({ ...l ?? { text: "" }, text: (l?.text ?? "") + event.text }));
2011
+ return;
2012
+ case "message":
2013
+ push({ kind: "assistant", text: event.content });
2014
+ setLive((l) => ({ ...l ?? { text: "" }, text: "" }));
2015
+ return;
2016
+ case "tool_call": {
2017
+ const summary = toolSummary(event.name, event.args);
2018
+ runningToolRef.current = { name: event.name, summary };
2019
+ setLive((l) => ({ text: l?.text ?? "", tool: { name: event.name, summary, output: "" } }));
2020
+ return;
2021
+ }
2022
+ case "tool_output":
2023
+ setLive(
2024
+ (l) => l?.tool ? { ...l, tool: { ...l.tool, output: tailLines(l.tool.output + event.chunk, LIVE_OUTPUT_LINES) } } : l
2025
+ );
2026
+ return;
2027
+ case "tool_result": {
2028
+ lastOutputRef.current = event.result;
2029
+ const lines = event.result.split("\n").filter((l) => l.trim() !== "");
2030
+ const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => l.slice(0, Math.max(20, cols - 8)));
2031
+ setLive((l) => l ? { text: l.text, tool: void 0 } : l);
2032
+ push({
2033
+ kind: "tool",
2034
+ name: event.name,
2035
+ summary: runningToolRef.current?.summary ?? "",
2036
+ output: shown.join("\n"),
2037
+ outputMore: Math.max(0, lines.length - shown.length),
2038
+ ok: event.ok
2039
+ });
2040
+ runningToolRef.current = null;
2041
+ return;
2042
+ }
2043
+ case "usage":
2044
+ setBalance(event.balance);
2045
+ setSpent((s) => s + event.creditsCharged);
2046
+ return;
2047
+ default:
2048
+ return;
2049
+ }
2050
+ },
2051
+ [cols, push]
2052
+ );
2053
+ const runTurn2 = useCallback(
2054
+ async (task) => {
2055
+ const ac = new AbortController();
2056
+ abortRef.current = ac;
2057
+ setBusy(true);
2058
+ setStartedAt(Date.now());
2059
+ setLive({ text: "" });
2060
+ let history = messagesRef.current;
2061
+ try {
2062
+ const window = contextWindow(model);
2063
+ if (shouldCompact([{ role: "system", content: contextRef.current.systemPrompt }, ...history], window)) {
2064
+ const res = await compact(client, model, [
2065
+ { role: "system", content: contextRef.current.systemPrompt },
2066
+ ...history
2067
+ ], { signal: ac.signal });
2068
+ if (res) {
2069
+ history = res.messages.slice(1);
2070
+ setMessages(history);
2071
+ setBalance(res.balance);
2072
+ setSpent((s) => s + res.creditsCharged);
2073
+ push({
2074
+ kind: "notice",
2075
+ text: ` compacted context: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
2076
+ });
2077
+ }
2078
+ }
2079
+ const result = await runAgent(client, task, {
2080
+ model,
2081
+ root,
2082
+ history,
2083
+ systemPrompt: contextRef.current.systemPrompt,
2084
+ permit,
2085
+ signal: ac.signal,
2086
+ onEvent: handleEvent
2087
+ });
2088
+ const next = result.messages.slice(1);
2089
+ setMessages(next);
2090
+ saveSession({
2091
+ id: sessionRef.current.id,
2092
+ started: sessionRef.current.started,
2093
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
2094
+ cwd: root,
2095
+ model,
2096
+ title: (session?.title || task).slice(0, 80),
2097
+ messages: next
2098
+ });
2099
+ if (result.stopped === "aborted") {
2100
+ push({ kind: "notice", tone: "warn", text: " (stopped)" });
2101
+ const w = await client.wallet().catch(() => void 0);
2102
+ if (w) setBalance(w.balance);
2103
+ } else if (result.stopped === "max_steps") {
2104
+ push({ kind: "notice", tone: "warn", text: " (stopped: reached the step limit \u2014 ask me to continue)" });
2105
+ } else if (result.content) {
2106
+ push({
2107
+ kind: "assistant",
2108
+ text: result.content,
2109
+ meta: `[${result.creditsCharged} credits \xB7 balance ${result.balance.toLocaleString("en-US")}]`
2110
+ });
2111
+ }
2112
+ } catch (err) {
2113
+ if (err instanceof PaywallError) {
2114
+ pendingTaskRef.current = task;
2115
+ push({
2116
+ kind: "notice",
2117
+ tone: "warn",
2118
+ text: ` Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).
2119
+ Press enter to open the ad wall (+${err.grantPerAd} credits on completion) \u2014 I'll continue automatically.
2120
+ Offers you are screened out of pay nothing; that is normal, just start another.`
2121
+ });
2122
+ } else if (err instanceof AuthError) {
2123
+ push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2124
+ } else {
2125
+ push({ kind: "notice", tone: "error", text: ` error: ${err.message}` });
2126
+ }
2127
+ } finally {
2128
+ abortRef.current = null;
2129
+ setLive(null);
2130
+ setBusy(false);
2131
+ }
2132
+ },
2133
+ [client, contextWindow, handleEvent, model, permit, push, root, session?.title]
2134
+ );
2135
+ const runAdWall = useCallback(async () => {
2136
+ const task = pendingTaskRef.current;
2137
+ pendingTaskRef.current = null;
2138
+ const before = balance;
2139
+ const code = await client.handoffCode();
2140
+ const base = config.dashboardUrl.replace(/\/+$/, "");
2141
+ openBrowser(code ? `${base}/?c=${encodeURIComponent(code)}` : base);
2142
+ push({
2143
+ kind: "notice",
2144
+ text: (code ? ` opened ${base} (signed in) \u2014 waiting for the reward\u2026
2145
+ ` : ` opened ${base} \u2014 paste your token there; waiting for the reward\u2026
2146
+ `) + ` Credits land on completion; being screened out of an offer pays nothing and is normal.`
2147
+ });
2148
+ setBusy(true);
2149
+ const deadline = Date.now() + 5 * 6e4;
2150
+ let credited = false;
2151
+ while (Date.now() < deadline && !credited) {
2152
+ await new Promise((r) => setTimeout(r, 3e3));
2153
+ const w = await client.wallet().catch(() => void 0);
2154
+ if (w && w.balance > before) {
2155
+ setBalance(w.balance);
2156
+ credited = true;
2157
+ }
2158
+ }
2159
+ setBusy(false);
2160
+ if (!credited)
2161
+ return push({
2162
+ kind: "notice",
2163
+ tone: "warn",
2164
+ text: " no credit yet \u2014 offers take a while to confirm, and a screenout pays nothing.\n That is normal: start another with /earn, or check /wallet later."
2165
+ });
2166
+ push({ kind: "notice", tone: "good", text: " credits added \u2014 continuing" });
2167
+ if (task) await runTurn2(task);
2168
+ }, [balance, client, config.dashboardUrl, push, runTurn2]);
2169
+ const runCommand = useCallback(
2170
+ async (line2) => {
2171
+ const [cmd, ...rest] = line2.slice(1).split(" ");
2172
+ const arg = rest.join(" ").trim();
2173
+ switch (cmd) {
2174
+ case "exit":
2175
+ case "quit":
2176
+ exit();
2177
+ return;
2178
+ case "help":
2179
+ push({ kind: "notice", text: helpText() });
2180
+ return;
2181
+ case "clear":
2182
+ setMessages([]);
2183
+ sessionRef.current = { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() };
2184
+ push({ kind: "notice", text: " (context cleared)" });
2185
+ return;
2186
+ case "mode": {
2187
+ const wanted = MODES.find((m) => m.toLowerCase() === arg.toLowerCase().replace(/[-\s]/g, ""));
2188
+ if (arg && !wanted) {
2189
+ push({ kind: "notice", tone: "warn", text: ` unknown mode: ${arg} \u2014 try ${MODES.join(", ")}` });
2190
+ return;
2191
+ }
2192
+ if (wanted) {
2193
+ permRef.current.mode = wanted;
2194
+ setMode(wanted);
2195
+ } else {
2196
+ cycleMode();
2197
+ }
2198
+ push({ kind: "notice", text: ` mode \u2192 ${MODE_LABEL[permRef.current.mode]}` });
2199
+ return;
2200
+ }
2201
+ case "compact": {
2202
+ setBusy(true);
2203
+ try {
2204
+ const res = await compact(client, model, [
2205
+ { role: "system", content: contextRef.current.systemPrompt },
2206
+ ...messagesRef.current
2207
+ ]);
2208
+ if (!res) push({ kind: "notice", text: " nothing to compact yet" });
2209
+ else {
2210
+ setMessages(res.messages.slice(1));
2211
+ setBalance(res.balance);
2212
+ setSpent((s) => s + res.creditsCharged);
2213
+ push({
2214
+ kind: "notice",
2215
+ text: ` compacted: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
2216
+ });
2217
+ }
2218
+ } catch (err) {
2219
+ push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2220
+ }
2221
+ setBusy(false);
2222
+ return;
2223
+ }
2224
+ case "init":
2225
+ await runTurn2(INIT_PROMPT);
2226
+ return;
2227
+ // Always the picker — an id you have to remember and type is exactly
2228
+ // what a picker is for. A typed id only preselects a row.
2229
+ case "model": {
2230
+ setBusy(true);
2231
+ const models = catalogRef.current.length ? catalogRef.current : await client.models().catch(() => []);
2232
+ catalogRef.current = models;
2233
+ setBusy(false);
2234
+ if (!models.length) return push({ kind: "notice", tone: "error", text: " could not load the model list" });
2235
+ if (arg && !models.some((m) => m.id === arg)) {
2236
+ push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
2237
+ }
2238
+ const items = models.map((m) => ({
2239
+ value: m.id,
2240
+ label: m.id,
2241
+ hint: `${m.tier.padEnd(8)}${adsPerTaskLabel(m.est_ads_per_task)}`,
2242
+ current: m.id === model
2243
+ }));
2244
+ const preselect = items.findIndex((i) => i.value === arg);
2245
+ setPickerSel(preselect >= 0 ? preselect : Math.max(0, items.findIndex((i) => i.current)));
2246
+ setPicker({
2247
+ title: "Select model",
2248
+ subtitle: "Applies to this session and is saved as your default.",
2249
+ items,
2250
+ onPick: (choice) => {
2251
+ config.model = choice.value;
2252
+ saveConfig(config);
2253
+ setModel2(choice.value);
2254
+ push({ kind: "notice", text: ` model \u2192 ${choice.value}` });
2255
+ }
2256
+ });
2257
+ return;
2258
+ }
2259
+ case "models": {
2260
+ setBusy(true);
2261
+ try {
2262
+ const ms = await client.models();
2263
+ catalogRef.current = ms;
2264
+ push({
2265
+ kind: "notice",
2266
+ text: ms.map((m) => ` ${m.id.padEnd(24)} ${adsPerTaskLabel(m.est_ads_per_task)}`).join("\n")
2267
+ });
2268
+ } catch (err) {
2269
+ push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2270
+ }
2271
+ setBusy(false);
2272
+ return;
2273
+ }
2274
+ case "wallet": {
2275
+ setBusy(true);
2276
+ try {
2277
+ const w = await client.wallet();
2278
+ setBalance(w.balance);
2279
+ push({
2280
+ kind: "notice",
2281
+ text: ` balance ${w.balance.toLocaleString("en-US")} credits \xB7 ${w.ads_today} offers today \xB7 $${w.earned_usd_today.toFixed(2)}/$${w.max_reward_usd_per_day.toFixed(2)} earned`
2282
+ });
2283
+ } catch (err) {
2284
+ push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2285
+ }
2286
+ setBusy(false);
2287
+ return;
2288
+ }
2289
+ // The same browser handoff the paywall takes, rather than a second way of
2290
+ // doing it: /earn used to call the dev-only /v1/ads/reward simulator, so
2291
+ // in production the slash command the paywall itself recommends was a 404.
2292
+ case "earn":
2293
+ await runAdWall();
2294
+ return;
2295
+ default:
2296
+ push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
2297
+ }
2298
+ },
2299
+ [client, config, cycleMode, exit, model, push, runTurn2]
2300
+ );
2301
+ const submit = useCallback(
2302
+ async (raw) => {
2303
+ const line2 = raw.trim();
2304
+ setEditor(EMPTY);
2305
+ setHistIdx(-1);
2306
+ setMenuOff(false);
2307
+ if (!line2) return;
2308
+ setHist((h) => {
2309
+ const next = [line2, ...h.filter((x) => x !== line2)].slice(0, 200);
2310
+ saveHistory(next);
2311
+ return next;
2312
+ });
2313
+ push({ kind: "user", text: line2 });
2314
+ if (line2.startsWith("/")) return runCommand(line2);
2315
+ return runTurn2(line2);
2316
+ },
2317
+ [push, runCommand, runTurn2]
2318
+ );
2319
+ useEffect(() => {
2320
+ if (!initialTask) return;
2321
+ push({ kind: "user", text: initialTask });
2322
+ void runTurn2(initialTask);
2323
+ }, []);
2324
+ const text = toText(editor);
2325
+ const cursor = offset(editor);
2326
+ const query = useMemo(() => findToken(text, cursor), [text, cursor]);
2327
+ const matches = useMemo(
2328
+ () => query ? suggest(query, {
2329
+ files: () => filesRef.current ??= listWorkspaceFiles(root)
2330
+ }) : [],
2331
+ [query, root]
2332
+ );
2333
+ const menu = menuOff || busy || ask2 || picker ? [] : matches;
2334
+ useEffect(() => {
2335
+ setSel(0);
2336
+ setMenuOff(false);
2337
+ }, [text]);
2338
+ const accept = useCallback(
2339
+ (choice) => {
2340
+ if (!query) return;
2341
+ const { text: nextText, cursor: nextCursor } = applySuggestion(text, cursor, query, choice);
2342
+ setEditor(fromTextAt(nextText, nextCursor));
2343
+ },
2344
+ [cursor, query, text]
2345
+ );
2346
+ useInput((ch, key) => {
2347
+ if (key.ctrl && ch === "c") {
2348
+ if (busy) return abortRef.current?.abort();
2349
+ if (!isEmpty(editor)) return setEditor(EMPTY);
2350
+ if (Date.now() - ctrlCRef.current < 2e3) return exit();
2351
+ ctrlCRef.current = Date.now();
2352
+ setQuitHint(true);
2353
+ return;
2354
+ }
2355
+ if (quitHint) setQuitHint(false);
2356
+ if (ask2) {
2357
+ const answer = ch === "y" || ch === "j" || key.return ? "once" : ch === "a" ? "always" : ch === "n" || key.escape ? "deny" : void 0;
2358
+ if (!answer) return;
2359
+ setAsk(null);
2360
+ ask2.resolve(answer);
2361
+ push({
2362
+ kind: "notice",
2363
+ tone: answer === "deny" ? "warn" : void 0,
2364
+ text: ` ${answer === "deny" ? "denied" : answer === "always" ? "allowed (always)" : "allowed"}: ${ask2.req.summary}`
2365
+ });
2366
+ return;
2367
+ }
2368
+ if (picker) {
2369
+ const n = picker.items.length;
2370
+ if (key.escape) return setPicker(null);
2371
+ if (key.upArrow) return setPickerSel((s) => (s - 1 + n) % n);
2372
+ if (key.downArrow) return setPickerSel((s) => (s + 1) % n);
2373
+ if (/^[1-9]$/.test(ch)) {
2374
+ const choice = picker.items[Number(ch) - 1];
2375
+ if (!choice) return;
2376
+ setPicker(null);
2377
+ return picker.onPick(choice);
2378
+ }
2379
+ if (key.return) {
2380
+ const choice = picker.items[Math.min(pickerSel, n - 1)];
2381
+ setPicker(null);
2382
+ if (choice) picker.onPick(choice);
2383
+ }
2384
+ return;
2385
+ }
2386
+ if (busy) {
2387
+ if (key.escape) abortRef.current?.abort();
2388
+ return;
2389
+ }
2390
+ if (key.tab && key.shift) return cycleMode();
2391
+ if (key.ctrl && ch === "o") {
2392
+ const out = lastOutputRef.current;
2393
+ if (out) push({ kind: "notice", text: out.split("\n").map((l) => ` ${l}`).join("\n") });
2394
+ return;
2395
+ }
2396
+ if (menu.length > 0) {
2397
+ if (key.upArrow) return setSel((s) => (s - 1 + menu.length) % menu.length);
2398
+ if (key.downArrow) return setSel((s) => (s + 1) % menu.length);
2399
+ if (key.escape) return setMenuOff(true);
2400
+ if (key.return || key.tab) {
2401
+ const choice = menu[Math.min(sel, menu.length - 1)];
2402
+ if (key.tab || choice.takesArgs || choice.kind === "file") return accept(choice);
2403
+ return void submit(choice.value);
2404
+ }
2405
+ }
2406
+ if (key.return) {
2407
+ if (key.meta || key.shift) return setEditor(newline(editor));
2408
+ if (endsWithContinuation(editor)) return setEditor(continueLine(editor));
2409
+ if (pendingTaskRef.current && isEmpty(editor)) return void runAdWall();
2410
+ return void submit(toText(editor));
2411
+ }
2412
+ if (key.ctrl && ch === "j") return setEditor(newline(editor));
2413
+ if (key.upArrow && editor.row === 0) {
2414
+ const n = Math.min(histIdx + 1, hist.length - 1);
2415
+ const v = hist[n];
2416
+ if (v !== void 0) {
2417
+ setHistIdx(n);
2418
+ setEditor(fromText(v));
2419
+ }
2420
+ return;
2421
+ }
2422
+ if (key.downArrow && editor.row === editor.lines.length - 1 && histIdx >= 0) {
2423
+ const n = histIdx - 1;
2424
+ setHistIdx(n < 0 ? -1 : n);
2425
+ setEditor(n < 0 ? EMPTY : fromText(hist[n] ?? ""));
2426
+ return;
2427
+ }
2428
+ setEditor(apply(editor, toEditorKey(ch, key)));
2429
+ });
2430
+ const elapsed = busy && startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
2431
+ const spinner = SPINNER[tick % SPINNER.length];
2432
+ const liveText = live?.text ? tailLines(live.text, Math.max(4, rows - 12)) : "";
2433
+ const liveBlock = [
2434
+ liveText,
2435
+ live?.tool ? `\u23FA ${live.tool.name} ${live.tool.summary}` : "",
2436
+ live?.tool?.output ?? ""
2437
+ ].filter(Boolean).join("\n");
2438
+ const askBlock = ask2 ? [ask2.req.summary, ask2.req.preview ?? ""].filter(Boolean).join("\n") : "";
2439
+ const pickerHeight = picker ? picker.items.length + 8 : 0;
2440
+ const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
2441
+ const inputBoxHeight = 2 + editor.lines.length;
2442
+ const chromeHeight = inputBoxHeight + 2;
2443
+ const liveHeight = liveBlock ? lineCount(liveBlock, cols) + 1 : 0;
2444
+ const askHeight = ask2 ? lineCount(askBlock, cols) + 2 : 0;
2445
+ const printed = useMemo(() => entries.reduce((n, e) => n + entryHeight(e, cols), 0), [entries, cols]);
2446
+ const spacer = Math.max(
2447
+ 0,
2448
+ rows - 1 - printed - chromeHeight - menu.length - pickerHeight - liveHeight - askHeight
2449
+ );
2450
+ const labelW = menu.reduce((w, c2) => Math.max(w, c2.label.length), 0);
2451
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2452
+ /* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }),
2453
+ spacer > 0 ? /* @__PURE__ */ jsx2(Box2, { height: spacer }) : null,
2454
+ liveBlock ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: liveBlock }) }) : null,
2455
+ ask2 ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2456
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: ask2.req.summary }),
2457
+ ask2.req.preview ? /* @__PURE__ */ jsx2(Text2, { children: ask2.req.preview }) : null,
2458
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc cancels" })
2459
+ ] }) : null,
2460
+ picker ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MANGO3, paddingX: 1, children: [
2461
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, children: picker.title }),
2462
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: picker.subtitle }),
2463
+ /* @__PURE__ */ jsx2(Box2, { height: 1 }),
2464
+ picker.items.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
2465
+ /* @__PURE__ */ jsxs2(Text2, { color: i === pickerSel ? MANGO_BRIGHT : void 0, bold: i === pickerSel, children: [
2466
+ i === pickerSel ? "\u276F " : " ",
2467
+ `${i + 1}. `,
2468
+ item.label.padEnd(pickerLabelW + 2)
2469
+ ] }),
2470
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
2471
+ item.current ? /* @__PURE__ */ jsx2(Text2, { color: "green", children: " \u2190 current" }) : null
2472
+ ] }, item.value)),
2473
+ /* @__PURE__ */ jsx2(Box2, { height: 1 }),
2474
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2191\u2193 choose \xB7 1-9 jump straight to a row \xB7 \u23CE confirm \xB7 esc cancel" })
2475
+ ] }) : null,
2476
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
2477
+ /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO_BRIGHT : MANGO3, paddingX: 1, children: [
2478
+ /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
2479
+ busy ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2480
+ spinner,
2481
+ " working\u2026 ",
2482
+ elapsed,
2483
+ "s \xB7 esc to stop"
2484
+ ] }) : /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor) })
2485
+ ] }),
2486
+ menu.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
2487
+ /* @__PURE__ */ jsxs2(Text2, { color: i === sel ? MANGO_BRIGHT : MANGO3, bold: i === sel, children: [
2488
+ i === sel ? " \u276F " : " ",
2489
+ item.label.padEnd(labelW + 2)
2490
+ ] }),
2491
+ /* @__PURE__ */ jsx2(Text2, { dimColor: i !== sel, children: item.hint })
2492
+ ] }, item.value)),
2493
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2494
+ " ",
2495
+ statusLine({
2496
+ menu: menu.length > 0,
2497
+ quitHint,
2498
+ model,
2499
+ balance,
2500
+ spent,
2501
+ mode,
2502
+ minutes: (Date.now() - runStartedAtRef.current) / 6e4
2503
+ })
2504
+ ] })
2505
+ ] })
2506
+ ] });
2507
+ }
2508
+ function renderInput(state) {
2509
+ return state.lines.map((line2, row) => {
2510
+ const prefix = row === 0 ? "" : "\n";
2511
+ if (row !== state.row) return /* @__PURE__ */ jsx2(Text2, { children: prefix + line2 }, row);
2512
+ const before = line2.slice(0, state.col);
2513
+ const at = line2.slice(state.col, state.col + 1) || " ";
2514
+ const after = line2.slice(state.col + 1);
2515
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
2516
+ prefix + before,
2517
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, children: at }),
2518
+ after
2519
+ ] }, row);
2520
+ });
2521
+ }
2522
+ function statusLine(o) {
2523
+ if (o.quitHint) return "press ctrl+c again to quit";
2524
+ if (o.menu) return "\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close";
2525
+ const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
2526
+ const parts = [
2527
+ o.model,
2528
+ `${o.balance.toLocaleString("en-US")} cr`,
2529
+ o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")} this session` : void 0,
2530
+ burn,
2531
+ MODE_LABEL[o.mode],
2532
+ "/help"
2533
+ ].filter(Boolean);
2534
+ return parts.join(" \xB7 ");
2535
+ }
2536
+ function toEditorKey(ch, key) {
2537
+ const name = key.leftArrow ? "left" : key.rightArrow ? "right" : key.upArrow ? "up" : key.downArrow ? "down" : key.backspace ? "backspace" : key.delete ? "delete" : void 0;
2538
+ return {
2539
+ input: name || key.escape || key.tab ? void 0 : ch,
2540
+ name,
2541
+ ctrl: key.ctrl,
2542
+ meta: key.meta
2543
+ };
2544
+ }
2545
+ function toolSummary(name, args) {
2546
+ const str = (v) => typeof v === "string" ? v : "";
2547
+ switch (name) {
2548
+ case "read_file":
2549
+ case "write_file":
2550
+ case "edit_file":
2551
+ return str(args.path);
2552
+ case "list_dir":
2553
+ return str(args.path) || ".";
2554
+ case "glob":
2555
+ return str(args.pattern);
2556
+ case "grep":
2557
+ return str(args.pattern) + (args.path ? ` in ${str(args.path)}` : "");
2558
+ case "run_command":
2559
+ return str(args.command);
2560
+ default:
2561
+ return "";
2562
+ }
2563
+ }
2564
+ function tailLines(text, max) {
2565
+ const lines = text.split("\n");
2566
+ return lines.length <= max ? text : lines.slice(-max).join("\n");
2567
+ }
2568
+ var SPINNER, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES;
2569
+ var init_app = __esm({
2570
+ "src/tui/app.tsx"() {
2571
+ "use strict";
2572
+ init_client();
2573
+ init_config();
2574
+ init_banner();
2575
+ init_agent();
2576
+ init_context();
2577
+ init_compact();
2578
+ init_kimi();
2579
+ init_browser();
2580
+ init_tools();
2581
+ init_permissions();
2582
+ init_session();
2583
+ init_commands();
2584
+ init_editor();
2585
+ init_suggest();
2586
+ init_views();
2587
+ SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2588
+ LIVE_OUTPUT_LINES = 5;
2589
+ COMMITTED_OUTPUT_LINES = 4;
2590
+ }
2591
+ });
2592
+
2593
+ // src/tui/index.tsx
2594
+ var tui_exports = {};
2595
+ __export(tui_exports, {
2596
+ startTui: () => startTui
2597
+ });
2598
+ import "react";
2599
+ import { render } from "ink";
2600
+ import { jsx as jsx3 } from "react/jsx-runtime";
2601
+ async function startTui(props) {
2602
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx3(App, { ...props }), { patchConsole: false });
2603
+ await waitUntilExit();
2604
+ }
2605
+ var init_tui = __esm({
2606
+ "src/tui/index.tsx"() {
2607
+ "use strict";
2608
+ init_app();
2609
+ }
2610
+ });
2611
+
2612
+ // src/main.ts
2613
+ init_client();
2614
+ init_config();
2615
+ init_agent();
2616
+ init_context();
2617
+ init_permissions();
2618
+ init_session();
2619
+ init_kimi();
2620
+ init_banner();
2621
+ init_browser();
2622
+ var c = {
2623
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
2624
+ green: (s) => `\x1B[32m${s}\x1B[0m`,
2625
+ yellow: (s) => `\x1B[33m${s}\x1B[0m`,
2626
+ red: (s) => `\x1B[31m${s}\x1B[0m`,
2627
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
2628
+ bold: (s) => `\x1B[1m${s}\x1B[0m`
2629
+ };
2630
+ async function main() {
2631
+ const [cmd, ...rest] = process.argv.slice(2);
2632
+ const config = loadConfig();
2633
+ const client = new GatewayClient(config);
2634
+ switch (cmd) {
2635
+ case "login":
2636
+ return login(client, config, rest[0]);
2637
+ case "logout":
2638
+ return logout(config);
2639
+ case "whoami":
2640
+ return whoami(client, config);
2641
+ case "models":
2642
+ return listModels(client);
2643
+ case "model":
2644
+ return setModel(client, config, rest[0]);
2645
+ case "wallet":
2646
+ return showWallet(client);
2647
+ case "earn":
2648
+ return earn(client, config);
2649
+ case "buy":
2650
+ return buyCmd(client, config, rest[0]);
2651
+ case "ask":
2652
+ return ask(client, config, rest.join(" "));
2653
+ case "agent":
2654
+ return repl(client, config, { task: rest.join(" ").trim() || void 0 });
2655
+ case "-p":
2656
+ case "--print":
2657
+ return printCmd(client, config, rest.join(" ").trim());
2658
+ case "-c":
2659
+ case "--continue":
2660
+ return repl(client, config, { resume: "latest" });
2661
+ case "--resume":
2662
+ return repl(client, config, { resume: rest[0] ?? "pick" });
2663
+ case "code":
2664
+ return codeCmd(client, config, rest);
2665
+ case "help":
2666
+ case "--help":
2667
+ case "-h":
2668
+ return printHelp();
2669
+ case void 0:
2670
+ return repl(client, config);
2671
+ default:
2672
+ console.error(c.red(`unknown command: ${cmd}`));
2673
+ printHelp();
2674
+ process.exitCode = 1;
2675
+ }
2676
+ }
2677
+ async function login(client, config, email) {
2678
+ if (!email) {
2679
+ const start = await client.deviceStart().catch(() => null);
2680
+ if (start) return githubLogin(client, config, start);
2681
+ }
2682
+ return devLogin(client, config, email);
2683
+ }
2684
+ async function githubLogin(client, config, start) {
2685
+ console.log(`
2686
+ Open ${c.cyan(start.verification_uri)} and enter code: ${c.bold(start.user_code)}
2687
+ `);
2688
+ openBrowser(start.verification_uri);
2689
+ process.stdout.write(c.dim(" waiting for GitHub authorization\u2026 (Ctrl+C to cancel)"));
2690
+ const deadline = Date.now() + start.expires_in * 1e3;
2691
+ let interval = Math.max(start.interval, 1);
2692
+ while (Date.now() < deadline) {
2693
+ await sleep(interval * 1e3);
2694
+ process.stdout.write(c.dim("."));
2695
+ const poll = await client.devicePoll(start.session);
2696
+ if (poll.status === "pending") {
2697
+ if (poll.interval) interval = poll.interval;
2698
+ continue;
2699
+ }
2700
+ if (poll.status === "complete") {
2701
+ config.token = poll.token;
2702
+ config.userId = poll.userId;
2703
+ config.email = poll.email;
2704
+ config.login = poll.login;
2705
+ saveConfig(config);
2706
+ console.log(c.green(`
2707
+ \u2713 logged in as ${poll.login}`) + c.dim(poll.email ? ` (${poll.email})` : ""));
2708
+ const bonus = poll.created ? c.dim(` (signup bonus ${poll.signupBonus})`) : "";
2709
+ console.log(` balance: ${c.bold(String(poll.balance))} credits${bonus}`);
2710
+ console.log(c.dim(` token stored in ${configPath()}`));
2711
+ return;
2712
+ }
2713
+ console.log(c.red(`
2714
+ login failed: ${poll.error ?? poll.status}`));
2715
+ return;
2716
+ }
2717
+ console.log(c.red("\n login timed out \u2014 run `clixad login` again."));
2718
+ }
2719
+ async function devLogin(client, config, email) {
2720
+ const res = await client.signupDev(email);
2721
+ config.token = res.token;
2722
+ config.userId = res.userId;
2723
+ config.email = res.email;
2724
+ saveConfig(config);
2725
+ console.log(c.green(`\u2713 logged in as ${res.email}`));
2726
+ console.log(` balance: ${c.bold(String(res.balance))} credits (signup bonus)`);
2727
+ console.log(c.dim(` token stored in ${configPath()}`));
2728
+ }
2729
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2730
+ function logout(config) {
2731
+ delete config.token;
2732
+ delete config.userId;
2733
+ delete config.email;
2734
+ saveConfig(config);
2735
+ console.log(c.green("\u2713 logged out"));
2736
+ }
2737
+ async function whoami(client, config) {
2738
+ if (!config.token) return console.log(c.yellow("not logged in \u2014 run `clixad login`"));
2739
+ const w = await client.wallet();
2740
+ console.log(`${c.bold(config.email ?? "unknown")} \xB7 model ${c.cyan(config.model)}`);
2741
+ console.log(`balance: ${c.bold(String(w.balance))} credits \xB7 ${earnedToday(w)}`);
2742
+ }
2743
+ function earnedToday(w) {
2744
+ const offers = `${w.ads_today} offer${w.ads_today === 1 ? "" : "s"}`;
2745
+ return `earned today: ${offers} \xB7 $${w.earned_usd_today.toFixed(2)}/$${w.max_reward_usd_per_day.toFixed(2)}`;
2746
+ }
2747
+ async function listModels(client) {
2748
+ const models = await client.models();
2749
+ console.log(c.bold("Models (credit prices per 1M tokens)"));
2750
+ for (const m of models) {
2751
+ const tag = m.ad_fundable ? "" : c.dim(" [paid/BYOK]");
2752
+ console.log(
2753
+ ` ${c.cyan(m.id.padEnd(22))} ${m.tier.padEnd(8)} in ${String(m.credits_per_mtoken_input).padStart(9)} out ${String(m.credits_per_mtoken_output).padStart(9)} ${adsPerTaskLabel(m.est_ads_per_task)}${tag}`
2754
+ );
2755
+ }
2756
+ }
2757
+ async function setModel(client, config, model) {
2758
+ if (!model) return console.log(`current model: ${c.cyan(config.model)}`);
2759
+ const known = await client.models().catch(() => void 0);
2760
+ if (known && !known.some((m) => m.id === model)) {
2761
+ console.error(c.red(`unknown model: ${model}`));
2762
+ console.error(c.dim(` available: ${known.map((m) => m.id).join(", ")}`));
2763
+ process.exitCode = 1;
2764
+ return;
2765
+ }
2766
+ if (!known) console.log(c.yellow(" (couldn't reach the gateway to verify the id)"));
2767
+ config.model = model;
2768
+ saveConfig(config);
2769
+ console.log(c.green(`\u2713 model set to ${model}`));
2770
+ }
2771
+ async function showWallet(client) {
2772
+ const w = await client.wallet();
2773
+ console.log(`balance: ${c.bold(String(w.balance))} credits`);
2774
+ console.log(`${earnedToday(w)} \xB7 typical grant: ${w.grant_per_ad} on completion`);
2775
+ if (w.ledger.length) {
2776
+ console.log(c.dim("recent:"));
2777
+ for (const e of w.ledger.slice(0, 8)) {
2778
+ const sign = e.delta >= 0 ? c.green(`+${e.delta}`) : c.red(String(e.delta));
2779
+ console.log(c.dim(` ${sign.padEnd(18)} ${e.reason.padEnd(14)} -> ${e.balanceAfter}`));
2780
+ }
2781
+ }
2782
+ }
2783
+ async function earn(client, config) {
2784
+ if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
2785
+ const before = (await safeWallet(client))?.balance ?? 0;
2786
+ const code = await client.handoffCode();
2787
+ const base = config.dashboardUrl.replace(/\/+$/, "");
2788
+ const url = code ? `${base}/?c=${encodeURIComponent(code)}` : base;
2789
+ openBrowser(url);
2790
+ console.log(`
2791
+ Ad wall: ${c.dim(base)}`);
2792
+ if (!code) console.log(c.yellow(" (couldn't sign you in automatically \u2014 paste your token there)"));
2793
+ console.log(
2794
+ c.dim(" Credits are granted on completion. Surveys screen people out part way through \u2014\n") + c.dim(" that pays nothing and is the normal case, so just start another one.")
2795
+ );
2796
+ process.stdout.write(c.dim(" waiting for an offer to clear"));
2797
+ const deadline = Date.now() + 5 * 60 * 1e3;
2798
+ while (Date.now() < deadline) {
2799
+ await sleep(3e3);
2800
+ process.stdout.write(c.dim("."));
2801
+ const balance = (await safeWallet(client))?.balance ?? before;
2802
+ if (balance > before) {
2803
+ console.log(
2804
+ c.green(`
2805
+ \u2713 +${(balance - before).toLocaleString("en-US")} credits`) + ` \xB7 balance ${c.bold(balance.toLocaleString("en-US"))}`
2806
+ );
2807
+ return;
2808
+ }
2809
+ }
2810
+ console.log(
2811
+ c.yellow("\n Nothing yet \u2014 offers can take a while to confirm, and being screened out of one")
2812
+ );
2813
+ console.log(c.yellow(" pays nothing and is normal. Start another, or check `clixad wallet` later."));
2814
+ }
2815
+ async function buyCmd(client, config, pack) {
2816
+ if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
2817
+ const { enabled, packs } = await client.creditPacks();
2818
+ if (!enabled) return console.log(c.yellow("Buying credits isn't available (Stripe not configured on the gateway)."));
2819
+ if (!pack) {
2820
+ console.log(c.bold("Credit packs:"));
2821
+ for (const p of packs) {
2822
+ console.log(
2823
+ ` ${c.cyan(p.id.padEnd(9))} ${p.credits.toLocaleString("en-US").padStart(11)} credits ${c.bold("$" + p.price_usd.toFixed(2))}`
2824
+ );
2825
+ }
2826
+ console.log(c.dim("\n buy one with: clixad buy <id>"));
2827
+ return;
2828
+ }
2829
+ let checkout;
2830
+ try {
2831
+ checkout = await client.checkout(pack);
2832
+ } catch (err) {
2833
+ return console.log(c.red(err.message));
2834
+ }
2835
+ const before = (await safeWallet(client))?.balance ?? 0;
2836
+ console.log(`
2837
+ Stripe Checkout: ${c.bold(checkout.pack)} \u2014 ${checkout.credits.toLocaleString("en-US")} credits for ${c.bold("$" + checkout.price_usd.toFixed(2))}`);
2838
+ console.log(c.dim(` ${checkout.url}`));
2839
+ console.log(c.dim(" Test card: 4242 4242 4242 4242 \xB7 any future expiry \xB7 any CVC \xB7 any ZIP\n"));
2840
+ openBrowser(checkout.url);
2841
+ process.stdout.write(c.dim(" waiting for payment to clear"));
2842
+ const deadline = Date.now() + 5 * 60 * 1e3;
2843
+ while (Date.now() < deadline) {
2844
+ await sleep(3e3);
2845
+ process.stdout.write(c.dim("."));
2846
+ const balance = (await safeWallet(client))?.balance ?? before;
2847
+ if (balance > before) {
2848
+ console.log(c.green(`
2849
+ \u2713 +${(balance - before).toLocaleString("en-US")} credits`) + ` \xB7 balance ${c.bold(String(balance))}`);
2850
+ return;
2851
+ }
2852
+ }
2853
+ console.log(c.yellow("\n No credits yet \u2014 the webhook may be delayed. Check `clixad wallet`."));
2854
+ }
2855
+ async function ask(client, config, prompt) {
2856
+ if (!prompt) return console.error(c.red('usage: clixad ask "your prompt"'));
2857
+ const messages = [{ role: "user", content: prompt }];
2858
+ await runTurn(client, config, messages);
2859
+ }
2860
+ async function printCmd(client, config, task) {
2861
+ if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
2862
+ if (!task) return console.error(c.red('usage: clixad -p "describe a task"'));
2863
+ try {
2864
+ const res = await runAgent(client, task, {
2865
+ model: config.model,
2866
+ root: process.cwd(),
2867
+ systemPrompt: collectContext(process.cwd()).systemPrompt,
2868
+ permit: denyAll,
2869
+ onEvent: printAgentEvent
2870
+ });
2871
+ console.log(
2872
+ c.dim(`
2873
+ [${res.creditsCharged} credits \xB7 balance ${res.balance} \xB7 ${res.steps} step(s)]`)
2874
+ );
2875
+ } catch (err) {
2876
+ if (err instanceof PaywallError) {
2877
+ console.log(c.yellow(`
2878
+ Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
2879
+ console.log(
2880
+ c.yellow(
2881
+ `Run \`clixad earn\` to open the offerwall (+${err.grantPerAd} credits on completion) or buy credits.`
2882
+ )
2883
+ );
2884
+ console.log(c.dim(" Offers you are screened out of pay nothing \u2014 that is normal, just start another."));
2885
+ } else if (err instanceof AuthError) {
2886
+ console.log(c.red(err.message));
2887
+ } else {
2888
+ console.log(c.red(`
2889
+ agent error: ${err.message}`));
2890
+ }
2891
+ process.exitCode = 1;
2892
+ }
2893
+ }
2894
+ function printAgentEvent(event) {
2895
+ switch (event.type) {
2896
+ case "delta":
2897
+ process.stdout.write(event.text);
2898
+ break;
2899
+ case "tool_call":
2900
+ console.log(c.cyan(`
2901
+ \u2192 ${event.name}`) + c.dim(` ${JSON.stringify(event.args).slice(0, 200)}`));
2902
+ break;
2903
+ case "tool_result": {
2904
+ const preview = event.result.split("\n").slice(0, 4).join("\n ");
2905
+ console.log(c.dim(` ${preview.slice(0, 400)}`));
2906
+ break;
2907
+ }
2908
+ case "final":
2909
+ process.stdout.write("\n");
2910
+ break;
2911
+ case "max_steps":
2912
+ console.log(c.yellow(" (stopped: reached max steps)"));
2913
+ break;
2914
+ }
2915
+ }
2916
+ async function codeCmd(client, config, rest) {
2917
+ if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
2918
+ const w = await safeWallet(client);
2919
+ if (w) {
2920
+ console.log(
2921
+ c.dim(` wallet: ${w.balance} credits \xB7 model ${config.model} \xB7 gateway ${config.gatewayUrl}`)
2922
+ );
2923
+ if (w.balance <= 0) {
2924
+ console.log(c.yellow(" You're out of credits \u2014 run `clixad earn` first, or the agent will hit a paywall."));
2925
+ }
2926
+ }
2927
+ console.log(c.dim(" launching Kimi CLI (Clixad-configured)\u2026\n"));
2928
+ const task = rest.join(" ").trim();
2929
+ const code = await runKimiCode(config, task || void 0);
2930
+ if (code !== 0) process.exitCode = code;
2931
+ }
2932
+ async function repl(client, config, opts = {}) {
2933
+ if (!config.token) {
2934
+ console.log(c.yellow("Not logged in. Run `clixad login` first.\n"));
2935
+ return;
2936
+ }
2937
+ if (opts.resume === "pick") {
2938
+ const sessions = listSessions(void 0, 10);
2939
+ if (!sessions.length) return console.log(c.yellow("no saved sessions yet"));
2940
+ console.log(c.bold("Sessions:"));
2941
+ for (const s of sessions) {
2942
+ console.log(` ${c.cyan(s.id)} ${s.updated.slice(0, 16).replace("T", " ")} ${s.title}`);
2943
+ }
2944
+ console.log(c.dim("\n resume one with: clixad --resume <id>"));
2945
+ return;
2946
+ }
2947
+ if (!process.stdin.isTTY) {
2948
+ console.log(c.yellow('Interactive mode needs a terminal. Use `clixad -p "..."` for scripted runs.'));
2949
+ return;
2950
+ }
2951
+ let session;
2952
+ if (opts.resume === "latest") {
2953
+ session = latestSession(process.cwd());
2954
+ if (!session) console.log(c.dim(" no previous session here \u2014 starting a new one"));
2955
+ } else if (opts.resume) {
2956
+ session = loadSession(opts.resume);
2957
+ if (!session) return console.log(c.red(`no such session: ${opts.resume}`));
2958
+ }
2959
+ const wallet = await safeWallet(client);
2960
+ clearScreen();
2961
+ const { startTui: startTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
2962
+ await startTui2({ client, config, wallet, session, initialTask: opts.task });
2963
+ }
2964
+ async function runTurn(client, config, messages) {
2965
+ try {
2966
+ process.stdout.write(c.green("assistant ") + "");
2967
+ const result = await client.chatStream(messages, config.model, {
2968
+ onDelta: (chunk) => process.stdout.write(chunk)
2969
+ });
2970
+ process.stdout.write("\n");
2971
+ console.log(c.dim(` [${result.creditsCharged} credits \xB7 balance ${result.balance}]`));
2972
+ return result;
2973
+ } catch (err) {
2974
+ process.stdout.write("\n");
2975
+ if (err instanceof PaywallError) {
2976
+ console.log(c.yellow(`Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
2977
+ console.log(
2978
+ c.yellow(`Run /earn to open the offerwall (+${err.grantPerAd} credits on completion) or buy credits.`)
2979
+ );
2980
+ console.log(c.dim(" Offers you are screened out of pay nothing \u2014 that is normal, just start another."));
2981
+ } else if (err instanceof AuthError) {
2982
+ console.log(c.red(err.message));
2983
+ } else {
2984
+ console.log(c.red(`error: ${err.message}`));
2985
+ }
2986
+ return void 0;
2987
+ }
2988
+ }
2989
+ async function safeWallet(client) {
2990
+ try {
2991
+ return await client.wallet();
2992
+ } catch {
2993
+ return void 0;
2994
+ }
2995
+ }
2996
+ function printHelp() {
2997
+ console.log(`${c.bold("clixad")} \u2014 free AI coding in your terminal, funded by ads
2998
+
2999
+ ${c.cyan("login")} [email] create/attach an account
3000
+ ${c.cyan("logout")} forget the stored token
3001
+ ${c.cyan("whoami")} show account + balance
3002
+ ${c.cyan("models")} list models with credit prices
3003
+ ${c.cyan("model")} <id> set default model
3004
+ ${c.cyan("wallet")} balance, ads today, ledger
3005
+ ${c.cyan("earn")} open the ad wall to earn credits
3006
+ ${c.cyan("buy")} [pack] list credit packs / buy one via Stripe
3007
+ ${c.cyan("ask")} "<prompt>" one-shot completion (no tools)
3008
+ ${c.cyan("agent")} "<task>" open the REPL with a task already running
3009
+ ${c.cyan("-p")} "<task>" headless agent run (read-only tools)
3010
+ ${c.cyan("code")} ["<task>"] launch the full Kimi CLI coding agent on the current dir
3011
+ ${c.cyan("--continue")} resume the last session in this directory
3012
+ ${c.cyan("--resume")} [id] list saved sessions, or resume one
3013
+ (no command) interactive coding REPL`);
3014
+ }
3015
+ main().catch((err) => {
3016
+ console.error(c.red(err.message));
3017
+ process.exitCode = 1;
3018
+ });