clixad 0.0.1-beta.14 → 0.0.1-beta.16
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.
- package/README.md +4 -0
- package/dist/clixad.mjs +620 -108
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,10 @@ npm install -g clixad
|
|
|
19
19
|
|
|
20
20
|
Requires Node.js >= 20.
|
|
21
21
|
|
|
22
|
+
## Community
|
|
23
|
+
|
|
24
|
+
Discord: <https://discord.gg/pwxPM3QyF> — bug reports and feature requests are read there.
|
|
25
|
+
|
|
22
26
|
## What it does
|
|
23
27
|
|
|
24
28
|
It reads, searches and edits files and runs shell commands in the directory you start it in, asking
|
package/dist/clixad.mjs
CHANGED
|
@@ -101,6 +101,52 @@ function grantRangeLabel(range) {
|
|
|
101
101
|
const n = (v) => v.toLocaleString("en-US");
|
|
102
102
|
return low === high ? `${n(low)} credits on completion` : `${n(low)}\u2013${n(high)} credits on completion`;
|
|
103
103
|
}
|
|
104
|
+
function streakLabel(streak) {
|
|
105
|
+
if (streak.length <= 0 || streak.day <= 0) return null;
|
|
106
|
+
const n = (x) => x.toLocaleString("en-US");
|
|
107
|
+
const where = `day ${streak.day} of ${streak.length}`;
|
|
108
|
+
if (!streak.claimed) return `daily streak: ${where} \xB7 today's already counted`;
|
|
109
|
+
const tomorrow = streak.schedule[Math.min(streak.day, streak.length - 1)];
|
|
110
|
+
const next = tomorrow === void 0 ? "" : ` \xB7 tomorrow ${n(tomorrow)}`;
|
|
111
|
+
return `daily streak: ${where} \xB7 +${n(streak.credits)} credits${next}`;
|
|
112
|
+
}
|
|
113
|
+
function referLines(info) {
|
|
114
|
+
const n = (x) => x.toLocaleString("en-US");
|
|
115
|
+
const lines = [
|
|
116
|
+
`your code: ${info.code}`,
|
|
117
|
+
`share it: clixad login --invite ${info.code}`,
|
|
118
|
+
`you both get ${n(info.inviterCredits)} credits once they complete their first offer`
|
|
119
|
+
];
|
|
120
|
+
if (info.invited === 0) {
|
|
121
|
+
lines.push("nobody has signed up with it yet");
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
const waiting = info.invited - info.credited;
|
|
125
|
+
lines.push(
|
|
126
|
+
`${n(info.invited)} signed up \xB7 ${n(info.credited)} completed an offer \xB7 ${n(info.creditsEarned)} credits earned`
|
|
127
|
+
);
|
|
128
|
+
if (waiting > 0) {
|
|
129
|
+
lines.push(`${n(waiting)} haven't completed an offer yet \u2014 that pays nothing until they do`);
|
|
130
|
+
}
|
|
131
|
+
if (info.remaining === 0 && info.maxCredited > 0) {
|
|
132
|
+
lines.push(`you've been paid for the maximum of ${n(info.maxCredited)} referrals`);
|
|
133
|
+
}
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
function redeemMessage(result) {
|
|
137
|
+
switch (result.status) {
|
|
138
|
+
case "redeemed":
|
|
139
|
+
return { ok: true, text: `+${result.credited.toLocaleString("en-US")} credits` };
|
|
140
|
+
case "used":
|
|
141
|
+
return { ok: false, text: "that code has already been used" };
|
|
142
|
+
case "expired":
|
|
143
|
+
return { ok: false, text: "that code has expired" };
|
|
144
|
+
case "wrong_account":
|
|
145
|
+
return { ok: false, text: "that code belongs to a different account \u2014 check who you're signed in as" };
|
|
146
|
+
default:
|
|
147
|
+
return { ok: false, text: "that isn't a code we recognise \u2014 check it for typos" };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
104
150
|
function ledgerReasonLabel(reason) {
|
|
105
151
|
return LEDGER_REASONS[reason] ?? reason.replace(/_/g, " ");
|
|
106
152
|
}
|
|
@@ -195,6 +241,10 @@ var init_client = __esm({
|
|
|
195
241
|
CHAT_ENCODED_CONTENT_TYPE = "application/vnd.clixad.chat+gzip";
|
|
196
242
|
LEDGER_REASONS = {
|
|
197
243
|
signup_bonus: "signup bonus",
|
|
244
|
+
login_streak: "daily streak",
|
|
245
|
+
feedback_bonus: "thanks for the feedback",
|
|
246
|
+
referral_bonus: "referral bonus",
|
|
247
|
+
redeem_code: "code redeemed",
|
|
198
248
|
ad_reward: "offer reward",
|
|
199
249
|
ad_screenout_bonus: "screenout bonus (didn't qualify)",
|
|
200
250
|
ad_sponsor_impression: "sponsored line shown",
|
|
@@ -323,6 +373,138 @@ var init_client = __esm({
|
|
|
323
373
|
return void 0;
|
|
324
374
|
}
|
|
325
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Claim today's login-streak grant, or find out that today is already claimed.
|
|
378
|
+
*
|
|
379
|
+
* Safe to call from anywhere and as often as you like: the gateway keys it on
|
|
380
|
+
* `(account, UTC day)`, so the second call of the day credits nothing and
|
|
381
|
+
* answers with the same day number. That is what lets every surface which
|
|
382
|
+
* might be somebody's first contact today — a login, the REPL opening,
|
|
383
|
+
* `wallet` — claim without coordinating.
|
|
384
|
+
*
|
|
385
|
+
* **Never throws.** Like `sponsorConfig`, every caller is decorating something
|
|
386
|
+
* that already works: a gateway too old to have the route, a cold start, a
|
|
387
|
+
* dropped connection and an expired token all mean the same thing here, which
|
|
388
|
+
* is "say nothing about a streak". A missed grant is claimed by the next
|
|
389
|
+
* command; a session that died over one would be a real loss.
|
|
390
|
+
*/
|
|
391
|
+
async claimStreak(signal) {
|
|
392
|
+
try {
|
|
393
|
+
const res = await fetch(`${this.config.gatewayUrl}/v1/streak/claim`, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
headers: this.headers(),
|
|
396
|
+
...signal ? { signal } : {}
|
|
397
|
+
});
|
|
398
|
+
if (!res.ok) return null;
|
|
399
|
+
const body = await res.json();
|
|
400
|
+
if (typeof body.day !== "number" || typeof body.credits !== "number") return null;
|
|
401
|
+
return {
|
|
402
|
+
day: body.day,
|
|
403
|
+
credits: body.credits,
|
|
404
|
+
claimed: body.claimed === true,
|
|
405
|
+
balance: typeof body.balance === "number" ? body.balance : 0,
|
|
406
|
+
schedule: Array.isArray(body.schedule) ? body.schedule : [],
|
|
407
|
+
length: typeof body.length === "number" ? body.length : 0
|
|
408
|
+
};
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Send one rating, and answer what it was paid.
|
|
415
|
+
*
|
|
416
|
+
* **Never throws**, for `claimStreak`'s reason and one of its own: this is
|
|
417
|
+
* called from a prompt that has already interrupted somebody, and the worst
|
|
418
|
+
* possible outcome is that answering it takes the session down. A gateway too
|
|
419
|
+
* old for the route, a cold start and an expired token all mean the same thing
|
|
420
|
+
* — the rating is lost, which costs the user nothing and us one data point.
|
|
421
|
+
*
|
|
422
|
+
* `null` for "nothing to say"; an outcome with `credited: 0` for "recorded,
|
|
423
|
+
* unpaid", which is what the daily ceiling looks like from here and is worth
|
|
424
|
+
* telling the user apart from a grant.
|
|
425
|
+
*/
|
|
426
|
+
async sendFeedback(report, signal) {
|
|
427
|
+
try {
|
|
428
|
+
const res = await fetch(`${this.config.gatewayUrl}/v1/feedback`, {
|
|
429
|
+
method: "POST",
|
|
430
|
+
headers: this.headers(),
|
|
431
|
+
...signal ? { signal } : {},
|
|
432
|
+
body: JSON.stringify({
|
|
433
|
+
session_id: report.session,
|
|
434
|
+
turn: report.turn,
|
|
435
|
+
rating: report.rating,
|
|
436
|
+
model: report.model,
|
|
437
|
+
prompt: report.prompt,
|
|
438
|
+
response: report.response,
|
|
439
|
+
...report.steps === void 0 ? {} : { steps: report.steps }
|
|
440
|
+
})
|
|
441
|
+
});
|
|
442
|
+
if (!res.ok) return null;
|
|
443
|
+
const body = await res.json();
|
|
444
|
+
return {
|
|
445
|
+
recorded: body.recorded === true,
|
|
446
|
+
credited: typeof body.credited === "number" ? body.credited : 0,
|
|
447
|
+
balance: typeof body.balance === "number" ? body.balance : 0
|
|
448
|
+
};
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* This account's referral code and standing, minting the code if it has none.
|
|
455
|
+
*
|
|
456
|
+
* **This one throws**, unlike `claimStreak` and `sendFeedback`. Those two
|
|
457
|
+
* decorate something that already works and a failure costs a line of chrome;
|
|
458
|
+
* this *is* the command — `clixad refer` with nothing to print is a command
|
|
459
|
+
* that did not run, and saying so beats printing an empty summary.
|
|
460
|
+
*/
|
|
461
|
+
async referral(signal) {
|
|
462
|
+
const res = await fetch(`${this.config.gatewayUrl}/v1/referrals`, {
|
|
463
|
+
headers: this.headers(),
|
|
464
|
+
...signal ? { signal } : {}
|
|
465
|
+
});
|
|
466
|
+
if (res.status === 401) throw await authFailure(res, "referrals");
|
|
467
|
+
if (!res.ok) throw new Error(await httpFailure(res, "referrals"));
|
|
468
|
+
const body = await res.json();
|
|
469
|
+
const num = (value) => typeof value === "number" ? value : 0;
|
|
470
|
+
if (typeof body.code !== "string" || !body.code) {
|
|
471
|
+
throw new Error("the gateway did not return a referral code");
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
code: body.code,
|
|
475
|
+
invited: num(body.invited),
|
|
476
|
+
credited: num(body.credited),
|
|
477
|
+
creditsEarned: num(body.credits_earned),
|
|
478
|
+
remaining: num(body.remaining),
|
|
479
|
+
maxCredited: num(body.max_credited),
|
|
480
|
+
inviterCredits: num(body.inviter_credits),
|
|
481
|
+
inviteeCredits: num(body.invitee_credits)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Spend a redeem code.
|
|
486
|
+
*
|
|
487
|
+
* **Throws**, like `referral` and unlike `claimStreak`: this is the command
|
|
488
|
+
* itself, and somebody who typed a code and got silence has been told nothing.
|
|
489
|
+
* A refusal is not a throw though — an expired or already-spent code answers
|
|
490
|
+
* 200 with a status, because that is an answer rather than a failure.
|
|
491
|
+
*/
|
|
492
|
+
async redeem(code, signal) {
|
|
493
|
+
const res = await fetch(`${this.config.gatewayUrl}/v1/redeem`, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers: this.headers(),
|
|
496
|
+
...signal ? { signal } : {},
|
|
497
|
+
body: JSON.stringify({ code })
|
|
498
|
+
});
|
|
499
|
+
if (res.status === 401) throw await authFailure(res, "redeem");
|
|
500
|
+
if (!res.ok) throw new Error(await httpFailure(res, "redeem"));
|
|
501
|
+
const body = await res.json();
|
|
502
|
+
return {
|
|
503
|
+
status: body.status ?? "unknown",
|
|
504
|
+
credited: typeof body.credited === "number" ? body.credited : 0,
|
|
505
|
+
balance: typeof body.balance === "number" ? body.balance : 0
|
|
506
|
+
};
|
|
507
|
+
}
|
|
326
508
|
async creditPacks() {
|
|
327
509
|
const res = await fetch(`${this.config.gatewayUrl}/v1/billing/packs`);
|
|
328
510
|
if (!res.ok) throw new Error(`packs failed: ${res.status}`);
|
|
@@ -604,7 +786,12 @@ var init_client = __esm({
|
|
|
604
786
|
const calls = /* @__PURE__ */ new Map();
|
|
605
787
|
for await (const evt of parseSSE(res.body)) {
|
|
606
788
|
if (evt === "[DONE]") break;
|
|
607
|
-
|
|
789
|
+
let json;
|
|
790
|
+
try {
|
|
791
|
+
json = JSON.parse(evt);
|
|
792
|
+
} catch {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
608
795
|
const choice = json.choices?.[0];
|
|
609
796
|
const delta = choice?.delta?.content;
|
|
610
797
|
if (delta) {
|
|
@@ -1239,12 +1426,13 @@ function installExitHook(registry) {
|
|
|
1239
1426
|
});
|
|
1240
1427
|
}
|
|
1241
1428
|
}
|
|
1242
|
-
var MAX_SHELL_BUFFER, MAX_SHELLS, ShellError, ShellRegistry, shells, hookInstalled;
|
|
1429
|
+
var MAX_SHELL_BUFFER, MAX_SHELLS, MAX_FINISHED_SHELLS, ShellError, ShellRegistry, shells, hookInstalled;
|
|
1243
1430
|
var init_shells = __esm({
|
|
1244
1431
|
"src/shells.ts"() {
|
|
1245
1432
|
"use strict";
|
|
1246
1433
|
MAX_SHELL_BUFFER = 2e5;
|
|
1247
1434
|
MAX_SHELLS = 8;
|
|
1435
|
+
MAX_FINISHED_SHELLS = 8;
|
|
1248
1436
|
ShellError = class extends Error {
|
|
1249
1437
|
};
|
|
1250
1438
|
ShellRegistry = class {
|
|
@@ -1252,21 +1440,27 @@ var init_shells = __esm({
|
|
|
1252
1440
|
seq = 0;
|
|
1253
1441
|
/** Start a command in the background and return it before it has finished. */
|
|
1254
1442
|
start(root, command, now = Date.now()) {
|
|
1443
|
+
this.pruneFinished();
|
|
1255
1444
|
const live = [...this.entries.values()].filter((e) => e.status === "running");
|
|
1256
1445
|
if (live.length >= MAX_SHELLS) {
|
|
1257
1446
|
throw new ShellError(
|
|
1258
1447
|
`${MAX_SHELLS} background commands are already running (${live.map((e) => e.id).join(", ")}) \u2014 stop one with kill_shell before starting another`
|
|
1259
1448
|
);
|
|
1260
1449
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1450
|
+
let child;
|
|
1451
|
+
try {
|
|
1452
|
+
child = spawn2(command, {
|
|
1453
|
+
cwd: root,
|
|
1454
|
+
shell: true,
|
|
1455
|
+
windowsHide: true,
|
|
1456
|
+
detached: process.platform !== "win32",
|
|
1457
|
+
// Nothing types at a background command, and leaving stdin inherited
|
|
1458
|
+
// lets it steal the keystrokes meant for the REPL.
|
|
1459
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1460
|
+
});
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
throw new ShellError(`could not start that command: ${err.message}`);
|
|
1463
|
+
}
|
|
1270
1464
|
const entry = {
|
|
1271
1465
|
id: `bash_${++this.seq}`,
|
|
1272
1466
|
command,
|
|
@@ -1339,10 +1533,23 @@ var init_shells = __esm({
|
|
|
1339
1533
|
list(now = Date.now()) {
|
|
1340
1534
|
return [...this.entries.values()].map((e) => snapshot(e, now));
|
|
1341
1535
|
}
|
|
1342
|
-
/**
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1536
|
+
/**
|
|
1537
|
+
* Forget all but the most recent `MAX_FINISHED_SHELLS` finished commands.
|
|
1538
|
+
*
|
|
1539
|
+
* Run when a new one starts, which is the only moment the registry grows.
|
|
1540
|
+
* Running commands are never touched — those are `killAll`'s and
|
|
1541
|
+
* `kill`'s — and the Map's insertion order is the start order, so "oldest"
|
|
1542
|
+
* needs no timestamp.
|
|
1543
|
+
*
|
|
1544
|
+
* This replaces a `forget(id)` that nothing ever called: an explicit
|
|
1545
|
+
* single-entry drop is only useful to a caller who knows a shell is finished
|
|
1546
|
+
* with, and neither the tools nor the REPL is in a position to know that.
|
|
1547
|
+
*/
|
|
1548
|
+
pruneFinished() {
|
|
1549
|
+
const finished = [...this.entries.values()].filter((e) => e.status !== "running");
|
|
1550
|
+
for (const entry of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_SHELLS))) {
|
|
1551
|
+
this.entries.delete(entry.id);
|
|
1552
|
+
}
|
|
1346
1553
|
}
|
|
1347
1554
|
/** Stop everything. Called on exit; safe to call more than once. */
|
|
1348
1555
|
killAll() {
|
|
@@ -2189,6 +2396,64 @@ function passesThrough(err) {
|
|
|
2189
2396
|
if (err instanceof PaywallError) return true;
|
|
2190
2397
|
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
2191
2398
|
}
|
|
2399
|
+
function fsCall(what, run) {
|
|
2400
|
+
try {
|
|
2401
|
+
return run();
|
|
2402
|
+
} catch (err) {
|
|
2403
|
+
if (passesThrough(err)) throw err;
|
|
2404
|
+
const code = err.code ?? "";
|
|
2405
|
+
throw new ToolError(`${what}: ${FS_REASON[code] ?? err.message}`);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
function countArg(raw, fallback) {
|
|
2409
|
+
const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : Number.NaN;
|
|
2410
|
+
return Number.isFinite(n) ? Math.max(1, Math.floor(n)) : fallback;
|
|
2411
|
+
}
|
|
2412
|
+
function requireString(value, tool, field) {
|
|
2413
|
+
const text = readString(value);
|
|
2414
|
+
if (text === void 0) {
|
|
2415
|
+
throw new ToolError(`${tool}: ${field} must be a string, got ${describeValue(value)}`);
|
|
2416
|
+
}
|
|
2417
|
+
return text;
|
|
2418
|
+
}
|
|
2419
|
+
function optionalString(value, tool, field) {
|
|
2420
|
+
if (value === void 0 || value === null) return void 0;
|
|
2421
|
+
return requireString(value, tool, field);
|
|
2422
|
+
}
|
|
2423
|
+
function readString(value) {
|
|
2424
|
+
if (typeof value === "string") return value;
|
|
2425
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : void 0;
|
|
2426
|
+
}
|
|
2427
|
+
function describeValue(value) {
|
|
2428
|
+
if (value === null) return "null";
|
|
2429
|
+
if (value === void 0) return "nothing";
|
|
2430
|
+
if (Array.isArray(value)) return "an array";
|
|
2431
|
+
if (typeof value === "number") return "an unusable number";
|
|
2432
|
+
return typeof value === "object" ? "an object" : `a ${typeof value}`;
|
|
2433
|
+
}
|
|
2434
|
+
function entryKind(entry, abs) {
|
|
2435
|
+
if (entry.isDirectory()) return "dir";
|
|
2436
|
+
if (entry.isFile()) return "file";
|
|
2437
|
+
if (!entry.isSymbolicLink()) return "other";
|
|
2438
|
+
try {
|
|
2439
|
+
const stat = statSync(abs);
|
|
2440
|
+
return stat.isDirectory() ? "dir" : stat.isFile() ? "file" : "other";
|
|
2441
|
+
} catch {
|
|
2442
|
+
return "other";
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
function isFilePath(abs) {
|
|
2446
|
+
try {
|
|
2447
|
+
return statSync(abs).isFile();
|
|
2448
|
+
} catch {
|
|
2449
|
+
return false;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
function searchBase(ctx, path) {
|
|
2453
|
+
const abs = safeResolve(ctx.root, path ?? ".");
|
|
2454
|
+
if (!existsSync2(abs)) throw new ToolError(`not found: ${path ?? "."}`);
|
|
2455
|
+
return abs;
|
|
2456
|
+
}
|
|
2192
2457
|
function commandSignature(command) {
|
|
2193
2458
|
return `run:${command.replace(/\s+/g, " ").trim()}`;
|
|
2194
2459
|
}
|
|
@@ -2299,13 +2564,19 @@ async function runMcpTool(ctx, name, args) {
|
|
|
2299
2564
|
}
|
|
2300
2565
|
function execute(ctx, command, timeout) {
|
|
2301
2566
|
return new Promise((resolvePromise) => {
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2567
|
+
let child;
|
|
2568
|
+
try {
|
|
2569
|
+
child = spawn4(command, {
|
|
2570
|
+
cwd: ctx.root,
|
|
2571
|
+
shell: true,
|
|
2572
|
+
windowsHide: true,
|
|
2573
|
+
// Own process group on POSIX so killTree can take the children with it.
|
|
2574
|
+
detached: process.platform !== "win32"
|
|
2575
|
+
});
|
|
2576
|
+
} catch (err) {
|
|
2577
|
+
return resolvePromise(`command failed to start: ${err.message}
|
|
2578
|
+
(no output)`);
|
|
2579
|
+
}
|
|
2309
2580
|
let out = "";
|
|
2310
2581
|
let total = 0;
|
|
2311
2582
|
let timedOut = false;
|
|
@@ -2352,6 +2623,10 @@ function listWorkspaceFiles(root, limit = 5e3) {
|
|
|
2352
2623
|
return files.sort();
|
|
2353
2624
|
}
|
|
2354
2625
|
function walk(dir, root, visit) {
|
|
2626
|
+
if (isFilePath(dir)) {
|
|
2627
|
+
visit(dir);
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2355
2630
|
let seen = 0;
|
|
2356
2631
|
const stack = [dir];
|
|
2357
2632
|
while (stack.length > 0) {
|
|
@@ -2364,12 +2639,13 @@ function walk(dir, root, visit) {
|
|
|
2364
2639
|
}
|
|
2365
2640
|
for (const entry of entries) {
|
|
2366
2641
|
const abs = join3(current, entry.name);
|
|
2367
|
-
|
|
2368
|
-
|
|
2642
|
+
const kind = entryKind(entry, abs);
|
|
2643
|
+
if (kind === "dir") {
|
|
2644
|
+
if (entry.isSymbolicLink() || SKIP_DIRS.has(entry.name)) continue;
|
|
2369
2645
|
stack.push(abs);
|
|
2370
2646
|
continue;
|
|
2371
2647
|
}
|
|
2372
|
-
if (
|
|
2648
|
+
if (kind !== "file") continue;
|
|
2373
2649
|
if (++seen > MAX_WALK_FILES) return;
|
|
2374
2650
|
if (!visit(abs)) return;
|
|
2375
2651
|
}
|
|
@@ -2377,8 +2653,9 @@ function walk(dir, root, visit) {
|
|
|
2377
2653
|
void root;
|
|
2378
2654
|
}
|
|
2379
2655
|
function globToRegExp(pattern) {
|
|
2380
|
-
const p = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2656
|
+
const p = requireString(pattern, "glob", "pattern").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2381
2657
|
let re = "";
|
|
2658
|
+
let depth = 0;
|
|
2382
2659
|
for (let i = 0; i < p.length; i++) {
|
|
2383
2660
|
const ch = p[i];
|
|
2384
2661
|
if (ch === "*") {
|
|
@@ -2394,14 +2671,23 @@ function globToRegExp(pattern) {
|
|
|
2394
2671
|
re += "[^/]*";
|
|
2395
2672
|
}
|
|
2396
2673
|
} else if (ch === "?") re += "[^/]";
|
|
2397
|
-
else if (ch === "{")
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
else if (
|
|
2674
|
+
else if (ch === "{") {
|
|
2675
|
+
re += "(?:";
|
|
2676
|
+
depth++;
|
|
2677
|
+
} else if (ch === "}" && depth > 0) {
|
|
2678
|
+
re += ")";
|
|
2679
|
+
depth--;
|
|
2680
|
+
} else if (ch === "," && depth > 0) re += "|";
|
|
2681
|
+
else if (".+^$()[]|\\{}".includes(ch)) re += "\\" + ch;
|
|
2401
2682
|
else re += ch;
|
|
2402
2683
|
}
|
|
2684
|
+
re += ")".repeat(depth);
|
|
2403
2685
|
const anchored = p.includes("/") ? `^${re}$` : `^(?:.*/)?${re}$`;
|
|
2404
|
-
|
|
2686
|
+
try {
|
|
2687
|
+
return new RegExp(anchored);
|
|
2688
|
+
} catch (err) {
|
|
2689
|
+
throw new ToolError(`not a usable glob pattern: ${pattern} \u2014 ${err.message}`);
|
|
2690
|
+
}
|
|
2405
2691
|
}
|
|
2406
2692
|
function toolSchemaFor(mode, mcpSchemas = [], available = {}) {
|
|
2407
2693
|
const usable = (t) => t.function.name !== "web_search" || available.webSearch === true;
|
|
@@ -2416,7 +2702,7 @@ function subagentToolSchema(available = {}) {
|
|
|
2416
2702
|
(t) => SUBAGENT_TOOLS.includes(t.function.name) && (t.function.name !== "web_search" || available.webSearch === true)
|
|
2417
2703
|
);
|
|
2418
2704
|
}
|
|
2419
|
-
var ToolError, PermissionDeniedError, MAX_FILE_BYTES, MAX_READ_LINES, MAX_READ_LINE_CHARS, MAX_OUTPUT_CHARS, MAX_GLOB_RESULTS, MAX_GREP_MATCHES, MAX_WALK_FILES, DEFAULT_COMMAND_TIMEOUT, MAX_IMAGE_BYTES, DEFAULT_SEARCH_RESULTS, MAX_SEARCH_RESULTS, MAX_ASK_OPTIONS, BACKGROUND_SETTLE_MS, SKIP_DIRS, TOOLS, READ_ONLY_TOOLS, EXIT_PLAN_MODE_SCHEMA, SUBAGENT_TOOLS, TOOL_SCHEMA;
|
|
2705
|
+
var ToolError, PermissionDeniedError, FS_REASON, MAX_FILE_BYTES, MAX_READ_LINES, MAX_READ_LINE_CHARS, MAX_OUTPUT_CHARS, MAX_GLOB_RESULTS, MAX_GREP_MATCHES, MAX_WALK_FILES, DEFAULT_COMMAND_TIMEOUT, MAX_IMAGE_BYTES, DEFAULT_SEARCH_RESULTS, MAX_SEARCH_RESULTS, MAX_ASK_OPTIONS, BACKGROUND_SETTLE_MS, SKIP_DIRS, TOOLS, READ_ONLY_TOOLS, EXIT_PLAN_MODE_SCHEMA, SUBAGENT_TOOLS, TOOL_SCHEMA;
|
|
2420
2706
|
var init_tools = __esm({
|
|
2421
2707
|
"src/tools.ts"() {
|
|
2422
2708
|
"use strict";
|
|
@@ -2436,6 +2722,19 @@ var init_tools = __esm({
|
|
|
2436
2722
|
this.notice = notice;
|
|
2437
2723
|
}
|
|
2438
2724
|
};
|
|
2725
|
+
FS_REASON = {
|
|
2726
|
+
ENOTDIR: "it is a file, not a directory \u2014 use read_file",
|
|
2727
|
+
EISDIR: "it is a directory, not a file \u2014 use list_dir, or edit a file inside it",
|
|
2728
|
+
ENOENT: "it does not exist (a symlink pointing at nothing looks like this too)",
|
|
2729
|
+
EACCES: "permission denied",
|
|
2730
|
+
EPERM: "permission denied",
|
|
2731
|
+
ELOOP: "the path goes through a symlink loop",
|
|
2732
|
+
ENAMETOOLONG: "the path is too long",
|
|
2733
|
+
EMFILE: "this process has too many files open \u2014 try again",
|
|
2734
|
+
ENFILE: "the system has too many files open \u2014 try again",
|
|
2735
|
+
ENOSPC: "the disk is full",
|
|
2736
|
+
EROFS: "the filesystem is read-only"
|
|
2737
|
+
};
|
|
2439
2738
|
MAX_FILE_BYTES = 4e5;
|
|
2440
2739
|
MAX_READ_LINES = 2e3;
|
|
2441
2740
|
MAX_READ_LINE_CHARS = 2e3;
|
|
@@ -2450,16 +2749,37 @@ var init_tools = __esm({
|
|
|
2450
2749
|
MAX_ASK_OPTIONS = 6;
|
|
2451
2750
|
BACKGROUND_SETTLE_MS = 500;
|
|
2452
2751
|
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2752
|
+
// Version control.
|
|
2453
2753
|
".git",
|
|
2754
|
+
".hg",
|
|
2755
|
+
".svn",
|
|
2756
|
+
// Dependencies and their caches.
|
|
2454
2757
|
"node_modules",
|
|
2758
|
+
".yarn",
|
|
2759
|
+
".pnpm-store",
|
|
2760
|
+
".venv",
|
|
2761
|
+
".tox",
|
|
2762
|
+
// Build output.
|
|
2455
2763
|
"dist",
|
|
2456
2764
|
"build",
|
|
2457
2765
|
"coverage",
|
|
2458
2766
|
".next",
|
|
2767
|
+
".nuxt",
|
|
2768
|
+
".svelte-kit",
|
|
2769
|
+
".output",
|
|
2770
|
+
".angular",
|
|
2771
|
+
".dart_tool",
|
|
2772
|
+
// Tool caches.
|
|
2459
2773
|
".turbo",
|
|
2774
|
+
".nx",
|
|
2460
2775
|
".cache",
|
|
2776
|
+
".parcel-cache",
|
|
2777
|
+
".gradle",
|
|
2461
2778
|
"__pycache__",
|
|
2462
|
-
".
|
|
2779
|
+
".mypy_cache",
|
|
2780
|
+
".pytest_cache",
|
|
2781
|
+
".ruff_cache",
|
|
2782
|
+
".terraform"
|
|
2463
2783
|
]);
|
|
2464
2784
|
TOOLS = {
|
|
2465
2785
|
/**
|
|
@@ -2478,11 +2798,14 @@ var init_tools = __esm({
|
|
|
2478
2798
|
* is told precisely what went wrong instead of guessing.
|
|
2479
2799
|
*/
|
|
2480
2800
|
read_file(ctx, args) {
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
if (
|
|
2484
|
-
const bytes = readFileSync3(abs);
|
|
2801
|
+
const path = requireString(args.path, "read_file", "path");
|
|
2802
|
+
const abs = safeResolve(ctx.root, path);
|
|
2803
|
+
if (!existsSync2(abs)) throw new ToolError(`not found: ${path}`);
|
|
2485
2804
|
const name = rel(ctx.root, abs);
|
|
2805
|
+
if (fsCall(`cannot read ${name}`, () => statSync(abs)).isDirectory()) {
|
|
2806
|
+
throw new ToolError(`${path} is a directory \u2014 use list_dir`);
|
|
2807
|
+
}
|
|
2808
|
+
const bytes = fsCall(`cannot read ${name}`, () => readFileSync3(abs));
|
|
2486
2809
|
const image = imageType(bytes);
|
|
2487
2810
|
if (image) return readImage(ctx, bytes, name, image);
|
|
2488
2811
|
if (looksLikePdf(bytes)) {
|
|
@@ -2503,11 +2826,11 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2503
2826
|
const raw = bytes.toString("utf8");
|
|
2504
2827
|
const all = raw.split("\n");
|
|
2505
2828
|
const total = all.length > 1 && all[all.length - 1] === "" ? all.length - 1 : all.length;
|
|
2506
|
-
const first =
|
|
2507
|
-
const want =
|
|
2829
|
+
const first = countArg(args.offset, 1);
|
|
2830
|
+
const want = countArg(args.limit, MAX_READ_LINES);
|
|
2508
2831
|
const last = Math.min(total, first + want - 1);
|
|
2509
2832
|
if (first > total) {
|
|
2510
|
-
return `${
|
|
2833
|
+
return `${path} has ${total} lines; offset ${first} is past the end`;
|
|
2511
2834
|
}
|
|
2512
2835
|
const body = all.slice(first - 1, last).map((line2, i) => {
|
|
2513
2836
|
const n = String(first + i).padStart(5, " ");
|
|
@@ -2523,27 +2846,35 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2523
2846
|
return body + note;
|
|
2524
2847
|
},
|
|
2525
2848
|
list_dir(ctx, args) {
|
|
2526
|
-
const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2849
|
+
const path = optionalString(args.path, "list_dir", "path") ?? ".";
|
|
2850
|
+
const abs = safeResolve(ctx.root, path);
|
|
2851
|
+
if (!existsSync2(abs)) throw new ToolError(`not found: ${path}`);
|
|
2852
|
+
const name = rel(ctx.root, abs);
|
|
2853
|
+
if (!fsCall(`cannot list ${name}`, () => statSync(abs)).isDirectory()) {
|
|
2854
|
+
throw new ToolError(`${path} is a file \u2014 use read_file`);
|
|
2855
|
+
}
|
|
2856
|
+
const entries = fsCall(`cannot list ${name}`, () => readdirSync(abs, { withFileTypes: true })).map(
|
|
2857
|
+
(entry) => {
|
|
2858
|
+
const isDir = entryKind(entry, resolve(abs, entry.name)) === "dir";
|
|
2859
|
+
return { name: isDir ? `${entry.name}/` : entry.name, isDir };
|
|
2860
|
+
}
|
|
2861
|
+
);
|
|
2532
2862
|
entries.sort((a, b) => Number(b.isDir) - Number(a.isDir) || a.name.localeCompare(b.name));
|
|
2533
2863
|
return entries.map((e) => e.name).join("\n") || "(empty)";
|
|
2534
2864
|
},
|
|
2535
2865
|
/** Find files by glob, e.g. `src/**\/*.ts`. Cheap orientation before reading. */
|
|
2536
2866
|
glob(ctx, args) {
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
const
|
|
2867
|
+
const pattern = optionalString(args.pattern, "glob", "pattern");
|
|
2868
|
+
if (!pattern) throw new ToolError("glob needs a pattern");
|
|
2869
|
+
const base = searchBase(ctx, optionalString(args.path, "glob", "path"));
|
|
2870
|
+
const re = globToRegExp(pattern);
|
|
2540
2871
|
const hits = [];
|
|
2541
2872
|
walk(base, ctx.root, (abs) => {
|
|
2542
2873
|
const r = rel(ctx.root, abs);
|
|
2543
2874
|
if (re.test(r)) hits.push(r);
|
|
2544
2875
|
return hits.length < MAX_GLOB_RESULTS;
|
|
2545
2876
|
});
|
|
2546
|
-
if (hits.length === 0) return `no files match ${
|
|
2877
|
+
if (hits.length === 0) return `no files match ${pattern}`;
|
|
2547
2878
|
hits.sort();
|
|
2548
2879
|
const more = hits.length >= MAX_GLOB_RESULTS ? `
|
|
2549
2880
|
\u2026 [capped at ${MAX_GLOB_RESULTS}]` : "";
|
|
@@ -2551,15 +2882,17 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2551
2882
|
},
|
|
2552
2883
|
/** Search file contents with a regular expression; returns path:line: text. */
|
|
2553
2884
|
grep(ctx, args) {
|
|
2554
|
-
|
|
2885
|
+
const pattern = optionalString(args.pattern, "grep", "pattern");
|
|
2886
|
+
if (!pattern) throw new ToolError("grep needs a pattern");
|
|
2555
2887
|
let re;
|
|
2556
2888
|
try {
|
|
2557
|
-
re = new RegExp(
|
|
2889
|
+
re = new RegExp(pattern);
|
|
2558
2890
|
} catch (err) {
|
|
2559
2891
|
throw new ToolError(`invalid regular expression: ${err.message}`);
|
|
2560
2892
|
}
|
|
2561
|
-
const base =
|
|
2562
|
-
const
|
|
2893
|
+
const base = searchBase(ctx, optionalString(args.path, "grep", "path"));
|
|
2894
|
+
const globArg = optionalString(args.glob, "grep", "glob");
|
|
2895
|
+
const filter = globArg ? globToRegExp(globArg) : void 0;
|
|
2563
2896
|
const out = [];
|
|
2564
2897
|
walk(base, ctx.root, (abs) => {
|
|
2565
2898
|
const r = rel(ctx.root, abs);
|
|
@@ -2580,24 +2913,28 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2580
2913
|
}
|
|
2581
2914
|
return true;
|
|
2582
2915
|
});
|
|
2583
|
-
if (out.length === 0) return `no matches for ${
|
|
2916
|
+
if (out.length === 0) return `no matches for ${pattern}`;
|
|
2584
2917
|
const more = out.length >= MAX_GREP_MATCHES ? `
|
|
2585
2918
|
\u2026 [capped at ${MAX_GREP_MATCHES} matches]` : "";
|
|
2586
2919
|
return out.join("\n") + more;
|
|
2587
2920
|
},
|
|
2588
2921
|
async write_file(ctx, args) {
|
|
2589
|
-
const abs = safeResolve(ctx.root, args.path);
|
|
2590
|
-
const
|
|
2591
|
-
const
|
|
2922
|
+
const abs = safeResolve(ctx.root, requireString(args.path, "write_file", "path"));
|
|
2923
|
+
const content = requireString(args.content, "write_file", "content");
|
|
2924
|
+
const name = rel(ctx.root, abs);
|
|
2925
|
+
const before = existsSync2(abs) ? fsCall(`cannot write ${name}`, () => readFileSync3(abs, "utf8")) : "";
|
|
2926
|
+
const preview = renderDiff(before, content) || "(no change)";
|
|
2592
2927
|
await gate(ctx, {
|
|
2593
2928
|
tool: "write_file",
|
|
2594
|
-
signature: `write:${
|
|
2595
|
-
summary: `${before ? "overwrite" : "create"} ${
|
|
2929
|
+
signature: `write:${name}`,
|
|
2930
|
+
summary: `${before ? "overwrite" : "create"} ${name}`,
|
|
2596
2931
|
preview
|
|
2597
2932
|
});
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2933
|
+
fsCall(`cannot write ${name}`, () => {
|
|
2934
|
+
mkdirSync2(dirname2(abs), { recursive: true });
|
|
2935
|
+
writeFileSync2(abs, content, "utf8");
|
|
2936
|
+
});
|
|
2937
|
+
return `wrote ${content.length} bytes to ${name}`;
|
|
2601
2938
|
},
|
|
2602
2939
|
/**
|
|
2603
2940
|
* Replace an exact snippet in a file. Preferred over write_file: it keeps the
|
|
@@ -2605,41 +2942,42 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2605
2942
|
* An ambiguous match is an error rather than a guess.
|
|
2606
2943
|
*/
|
|
2607
2944
|
async edit_file(ctx, args) {
|
|
2608
|
-
const
|
|
2609
|
-
|
|
2610
|
-
if (
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
if (
|
|
2614
|
-
const
|
|
2615
|
-
const
|
|
2945
|
+
const path = requireString(args.path, "edit_file", "path");
|
|
2946
|
+
const abs = safeResolve(ctx.root, path);
|
|
2947
|
+
if (!existsSync2(abs)) throw new ToolError(`not found: ${path}`);
|
|
2948
|
+
const oldString = requireString(args.old_string, "edit_file", "old_string");
|
|
2949
|
+
const newString = requireString(args.new_string, "edit_file", "new_string");
|
|
2950
|
+
if (oldString === newString) throw new ToolError("old_string and new_string are identical");
|
|
2951
|
+
const name = rel(ctx.root, abs);
|
|
2952
|
+
const before = fsCall(`cannot edit ${name}`, () => readFileSync3(abs, "utf8"));
|
|
2953
|
+
const count = occurrences(before, oldString);
|
|
2616
2954
|
if (count === 0) {
|
|
2617
2955
|
throw new ToolError(
|
|
2618
|
-
`old_string not found in ${
|
|
2956
|
+
`old_string not found in ${path} \u2014 read the file and copy the exact text, without the line numbers read_file puts in front of each line`
|
|
2619
2957
|
);
|
|
2620
2958
|
}
|
|
2621
2959
|
if (count > 1 && !args.replace_all) {
|
|
2622
2960
|
throw new ToolError(
|
|
2623
|
-
`old_string appears ${count} times in ${
|
|
2961
|
+
`old_string appears ${count} times in ${path} \u2014 add more surrounding context, or pass replace_all: true`
|
|
2624
2962
|
);
|
|
2625
2963
|
}
|
|
2626
|
-
const after = before.split(
|
|
2964
|
+
const after = before.split(oldString).join(newString);
|
|
2627
2965
|
const preview = renderDiff(before, after) || "(no change)";
|
|
2628
2966
|
await gate(ctx, {
|
|
2629
2967
|
tool: "edit_file",
|
|
2630
|
-
signature: `write:${
|
|
2631
|
-
summary: `edit ${
|
|
2968
|
+
signature: `write:${name}`,
|
|
2969
|
+
summary: `edit ${name}`,
|
|
2632
2970
|
preview
|
|
2633
2971
|
});
|
|
2634
|
-
writeFileSync2(abs, after, "utf8");
|
|
2635
|
-
return `edited ${
|
|
2972
|
+
fsCall(`cannot edit ${name}`, () => writeFileSync2(abs, after, "utf8"));
|
|
2973
|
+
return `edited ${name} (${count > 1 ? `${count} occurrences` : "1 occurrence"})`;
|
|
2636
2974
|
},
|
|
2637
2975
|
/**
|
|
2638
2976
|
* Run a shell command in the workspace. Streams output to `ctx.onOutput` as it
|
|
2639
2977
|
* arrives and dies with `ctx.signal`, so a runaway build can be interrupted.
|
|
2640
2978
|
*/
|
|
2641
2979
|
async run_command(ctx, args) {
|
|
2642
|
-
const command = (args.command ?? "").trim();
|
|
2980
|
+
const command = (optionalString(args.command, "run_command", "command") ?? "").trim();
|
|
2643
2981
|
if (!command) throw new ToolError("run_command needs a command");
|
|
2644
2982
|
const background = args.background === true;
|
|
2645
2983
|
await gate(ctx, {
|
|
@@ -2651,7 +2989,7 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
2651
2989
|
summary: `${background ? "run in the background" : "run"} ${command.length > 60 ? command.slice(0, 59) + "\u2026" : command}`,
|
|
2652
2990
|
preview: command
|
|
2653
2991
|
});
|
|
2654
|
-
if (!background) return execute(ctx, command, args.timeout
|
|
2992
|
+
if (!background) return execute(ctx, command, countArg(args.timeout, DEFAULT_COMMAND_TIMEOUT));
|
|
2655
2993
|
const registry = ctx.shells ?? shells;
|
|
2656
2994
|
let started;
|
|
2657
2995
|
try {
|
|
@@ -2671,7 +3009,7 @@ ${first.output.trim().slice(0, 4e3)}` : "");
|
|
|
2671
3009
|
/** Whatever a background command has printed since it was last read. */
|
|
2672
3010
|
read_output(ctx, args) {
|
|
2673
3011
|
const registry = ctx.shells ?? shells;
|
|
2674
|
-
const id = (args.shell_id ?? "").trim();
|
|
3012
|
+
const id = (optionalString(args.shell_id, "read_output", "shell_id") ?? "").trim();
|
|
2675
3013
|
if (!id) {
|
|
2676
3014
|
const running = registry.list();
|
|
2677
3015
|
throw new ToolError(
|
|
@@ -2680,7 +3018,8 @@ ${first.output.trim().slice(0, 4e3)}` : "");
|
|
|
2680
3018
|
}
|
|
2681
3019
|
let read;
|
|
2682
3020
|
try {
|
|
2683
|
-
|
|
3021
|
+
const filter = optionalString(args.filter, "read_output", "filter");
|
|
3022
|
+
read = registry.read(id, filter ? { filter } : {});
|
|
2684
3023
|
} catch (err) {
|
|
2685
3024
|
asToolError(err);
|
|
2686
3025
|
}
|
|
@@ -2696,7 +3035,7 @@ ${body || "(nothing new since the last read)"}`;
|
|
|
2696
3035
|
},
|
|
2697
3036
|
async kill_shell(ctx, args) {
|
|
2698
3037
|
const registry = ctx.shells ?? shells;
|
|
2699
|
-
const id = (args.shell_id ?? "").trim();
|
|
3038
|
+
const id = (optionalString(args.shell_id, "kill_shell", "shell_id") ?? "").trim();
|
|
2700
3039
|
if (!id) throw new ToolError("kill_shell needs a shell_id");
|
|
2701
3040
|
await gate(ctx, {
|
|
2702
3041
|
tool: "kill_shell",
|
|
@@ -2719,14 +3058,14 @@ ${body || "(nothing new since the last read)"}`;
|
|
|
2719
3058
|
* a context that is billed by the token.
|
|
2720
3059
|
*/
|
|
2721
3060
|
async web_search(ctx, args) {
|
|
2722
|
-
const query = (args.query ?? "").trim();
|
|
3061
|
+
const query = (optionalString(args.query, "web_search", "query") ?? "").trim();
|
|
2723
3062
|
if (!query) throw new ToolError("web_search needs a query");
|
|
2724
3063
|
if (!ctx.webSearch) {
|
|
2725
3064
|
throw new ToolError(
|
|
2726
3065
|
"web search is not available in this run \u2014 use web_fetch on a URL you already know instead"
|
|
2727
3066
|
);
|
|
2728
3067
|
}
|
|
2729
|
-
const count = Math.
|
|
3068
|
+
const count = Math.min(MAX_SEARCH_RESULTS, countArg(args.count, DEFAULT_SEARCH_RESULTS));
|
|
2730
3069
|
let results;
|
|
2731
3070
|
try {
|
|
2732
3071
|
results = await ctx.webSearch(query, { count, ...ctx.signal ? { signal: ctx.signal } : {} });
|
|
@@ -2754,9 +3093,9 @@ ${body || "(nothing new since the last read)"}`;
|
|
|
2754
3093
|
async web_fetch(ctx, args) {
|
|
2755
3094
|
let page;
|
|
2756
3095
|
try {
|
|
2757
|
-
page = await fetchPage(args.url ?? "", {
|
|
3096
|
+
page = await fetchPage(optionalString(args.url, "web_fetch", "url") ?? "", {
|
|
2758
3097
|
...ctx.signal ? { signal: ctx.signal } : {},
|
|
2759
|
-
...
|
|
3098
|
+
...args.max_chars === void 0 ? {} : { maxChars: Math.max(1e3, countArg(args.max_chars, MAX_FILE_BYTES)) }
|
|
2760
3099
|
});
|
|
2761
3100
|
} catch (err) {
|
|
2762
3101
|
networkToolError(err);
|
|
@@ -2783,8 +3122,8 @@ ${page.text}`;
|
|
|
2783
3122
|
* the credits it saves do not pay for that.
|
|
2784
3123
|
*/
|
|
2785
3124
|
async task(ctx, args) {
|
|
2786
|
-
const prompt = (args.prompt ?? "").trim();
|
|
2787
|
-
const description = (args.description ?? "").trim() || "subagent";
|
|
3125
|
+
const prompt = (optionalString(args.prompt, "task", "prompt") ?? "").trim();
|
|
3126
|
+
const description = (optionalString(args.description, "task", "description") ?? "").trim() || "subagent";
|
|
2788
3127
|
if (!prompt) throw new ToolError("task needs a prompt describing what to find out");
|
|
2789
3128
|
if (!ctx.spawn) {
|
|
2790
3129
|
throw new ToolError(
|
|
@@ -2821,7 +3160,7 @@ ${page.text}`;
|
|
|
2821
3160
|
* be told something that was not on the list.
|
|
2822
3161
|
*/
|
|
2823
3162
|
async ask_user(ctx, args) {
|
|
2824
|
-
const question = (args.question ?? "").trim();
|
|
3163
|
+
const question = (optionalString(args.question, "ask_user", "question") ?? "").trim();
|
|
2825
3164
|
if (!question) throw new ToolError("ask_user needs a question");
|
|
2826
3165
|
if (!ctx.askUser) {
|
|
2827
3166
|
throw new ToolError(
|
|
@@ -2846,7 +3185,7 @@ ${page.text}`;
|
|
|
2846
3185
|
* able to grant itself write access.
|
|
2847
3186
|
*/
|
|
2848
3187
|
async exit_plan_mode(ctx, args) {
|
|
2849
|
-
const plan = (args.plan ?? "").trim();
|
|
3188
|
+
const plan = (optionalString(args.plan, "exit_plan_mode", "plan") ?? "").trim();
|
|
2850
3189
|
if (!plan) throw new ToolError("exit_plan_mode needs a plan");
|
|
2851
3190
|
if (!ctx.approvePlan) {
|
|
2852
3191
|
throw new ToolError(
|
|
@@ -3337,8 +3676,6 @@ var init_agent = __esm({
|
|
|
3337
3676
|
"use strict";
|
|
3338
3677
|
init_tools();
|
|
3339
3678
|
init_mcp();
|
|
3340
|
-
init_shells();
|
|
3341
|
-
init_web();
|
|
3342
3679
|
init_client();
|
|
3343
3680
|
SUBAGENT_MAX_STEPS = 12;
|
|
3344
3681
|
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 did and why.\n\nNot every message is a task. If the user asks a question, or asks you to say something, answer in plain text and call no tools at all. Reach for a file or a command only when the request is actually about one, and when a message is ambiguous, prefer the reading of it that changes nothing.\n\nA tool call that was refused did not happen. Never describe it as done, and do not repeat it on a later message unless the user asks for it again.\n\nReach outside the workspace when the answer is outside it: web_search for current documentation, releases and error messages, then web_fetch to read the page. Do not guess at an API you could look up.\n\nStart anything that does not exit on its own \u2014 dev servers, watchers, tailed logs \u2014 with run_command background: true, and read it with read_output. A foreground command is killed after its timeout.\n\nWhen finding something out means opening many files, give it to task instead: the sub-agent reads them in its own context and reports back, which keeps this conversation short. Use todo_write for work with several distinct steps, and keep it current as you go.";
|
|
@@ -4297,6 +4634,15 @@ var init_commands = __esm({
|
|
|
4297
4634
|
// forms, surveys and app trials, and describing the top-up as a video costs
|
|
4298
4635
|
// the user the one expectation that makes a screenout make sense.
|
|
4299
4636
|
{ name: "earn", desc: "open the offer wall to earn credits" },
|
|
4637
|
+
// Deliberately *no* `args`, for /login's reason: an arg hint makes enter
|
|
4638
|
+
// complete the command instead of running it, and printing the code is the
|
|
4639
|
+
// whole command.
|
|
4640
|
+
{ name: "refer", desc: "your invite code, and what it has earned" },
|
|
4641
|
+
// This one *does* carry an `args` hint, unlike /refer and /login: the code is
|
|
4642
|
+
// the whole command, and `/redeem` on its own has nothing to do — so enter
|
|
4643
|
+
// completing to "/redeem " is the helpful outcome rather than the surprising
|
|
4644
|
+
// one.
|
|
4645
|
+
{ name: "redeem", args: "<code>", desc: "spend a code you were given" },
|
|
4300
4646
|
// Reachable from inside the REPL because that is where the 401 is printed.
|
|
4301
4647
|
// "Run `clixad login` first" is not a runnable instruction at this prompt —
|
|
4302
4648
|
// anything without a leading slash is a prompt, so it went to the model and
|
|
@@ -4771,7 +5117,7 @@ function useTerminalSize() {
|
|
|
4771
5117
|
}, [stdout]);
|
|
4772
5118
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
4773
5119
|
}
|
|
4774
|
-
function App({ client, config, wallet, session, initialTask, sponsorServe }) {
|
|
5120
|
+
function App({ client, config, wallet, streak, session, initialTask, sponsorServe }) {
|
|
4775
5121
|
const { exit } = useApp();
|
|
4776
5122
|
const { stdout, write: writeToStdout } = useStdout();
|
|
4777
5123
|
const idRef = useRef(1);
|
|
@@ -4831,7 +5177,9 @@ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
|
|
|
4831
5177
|
const busyAbortRef = useRef(null);
|
|
4832
5178
|
const filesRef = useRef(null);
|
|
4833
5179
|
const ctrlCRef = useRef(0);
|
|
4834
|
-
const sessionRef = useRef(
|
|
5180
|
+
const sessionRef = useRef(
|
|
5181
|
+
session ?? { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() }
|
|
5182
|
+
);
|
|
4835
5183
|
const runStartedAtRef = useRef(Date.now());
|
|
4836
5184
|
const contextRef = useRef(collectContext(root));
|
|
4837
5185
|
const catalogRef = useRef([]);
|
|
@@ -4844,6 +5192,8 @@ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
|
|
|
4844
5192
|
const pendingTaskRef = useRef(null);
|
|
4845
5193
|
const mcpRef = useRef(McpHub.empty());
|
|
4846
5194
|
const todosRef = useRef(new TodoList());
|
|
5195
|
+
const lastTurnRef = useRef(null);
|
|
5196
|
+
const turnCountRef = useRef(config.turns ?? 0);
|
|
4847
5197
|
const deltaBufRef = useRef("");
|
|
4848
5198
|
const deltaTimerRef = useRef(null);
|
|
4849
5199
|
const sponsorRef = useRef(sponsorSource());
|
|
@@ -4923,6 +5273,8 @@ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
|
|
|
4923
5273
|
client.onNotice = (text2) => push({ kind: "notice", tone: "warn", text: text2 });
|
|
4924
5274
|
const files = contextRef.current.files;
|
|
4925
5275
|
if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
|
|
5276
|
+
const streakLine = streak && streak.claimed ? streakLabel(streak) : null;
|
|
5277
|
+
if (streakLine) push({ kind: "notice", tone: "good", text: ` ${streakLine}` });
|
|
4926
5278
|
if (session?.messages.length) {
|
|
4927
5279
|
push({ kind: "notice", text: ` resumed session ${session.id} (${session.messages.length} messages)` });
|
|
4928
5280
|
}
|
|
@@ -5185,16 +5537,14 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5185
5537
|
});
|
|
5186
5538
|
const next = result.messages.slice(1);
|
|
5187
5539
|
setMessages(next);
|
|
5540
|
+
sessionRef.current.title ??= (task.split("\n")[0] || task).slice(0, 80);
|
|
5188
5541
|
saveSession({
|
|
5189
5542
|
id: sessionRef.current.id,
|
|
5190
5543
|
started: sessionRef.current.started,
|
|
5191
5544
|
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5192
5545
|
cwd: root,
|
|
5193
5546
|
model,
|
|
5194
|
-
|
|
5195
|
-
// a blank line, and a session titled with a file's contents is not a
|
|
5196
|
-
// title. What the user typed is always the first line.
|
|
5197
|
-
title: (session?.title || task.split("\n")[0] || task).slice(0, 80),
|
|
5547
|
+
title: sessionRef.current.title,
|
|
5198
5548
|
messages: next
|
|
5199
5549
|
});
|
|
5200
5550
|
if (result.stopped === "aborted") {
|
|
@@ -5209,6 +5559,16 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5209
5559
|
text: result.content,
|
|
5210
5560
|
meta: `[${result.creditsCharged} credits \xB7 balance ${result.balance.toLocaleString("en-US")}]`
|
|
5211
5561
|
});
|
|
5562
|
+
lastTurnRef.current = {
|
|
5563
|
+
turn: turnCountRef.current,
|
|
5564
|
+
model,
|
|
5565
|
+
prompt: task,
|
|
5566
|
+
response: result.content,
|
|
5567
|
+
steps: result.steps
|
|
5568
|
+
};
|
|
5569
|
+
turnCountRef.current += 1;
|
|
5570
|
+
config.turns = turnCountRef.current;
|
|
5571
|
+
saveConfig(config);
|
|
5212
5572
|
}
|
|
5213
5573
|
} catch (err) {
|
|
5214
5574
|
if (err instanceof PaywallError) {
|
|
@@ -5248,8 +5608,7 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5248
5608
|
nextSponsor,
|
|
5249
5609
|
permit,
|
|
5250
5610
|
push,
|
|
5251
|
-
root
|
|
5252
|
-
session?.title
|
|
5611
|
+
root
|
|
5253
5612
|
]
|
|
5254
5613
|
);
|
|
5255
5614
|
const stopCurrent = useCallback(() => {
|
|
@@ -5304,6 +5663,10 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5304
5663
|
tone: "good",
|
|
5305
5664
|
text: ` signed in as ${account.login ?? account.email ?? "you"} \xB7 balance ${account.balance.toLocaleString("en-US")} credits` + (account.created ? " (signup bonus)" : "")
|
|
5306
5665
|
});
|
|
5666
|
+
const claimed = await client.claimStreak(signal);
|
|
5667
|
+
const line2 = claimed?.claimed ? streakLabel(claimed) : null;
|
|
5668
|
+
if (claimed?.claimed) setBalance(claimed.balance);
|
|
5669
|
+
if (line2) push({ kind: "notice", tone: "good", text: ` ${line2}` });
|
|
5307
5670
|
return;
|
|
5308
5671
|
}
|
|
5309
5672
|
case "closed":
|
|
@@ -5395,6 +5758,7 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5395
5758
|
expandedRef.current = /* @__PURE__ */ new Set();
|
|
5396
5759
|
heightCacheRef.current = /* @__PURE__ */ new Map();
|
|
5397
5760
|
todosRef.current = new TodoList();
|
|
5761
|
+
lastTurnRef.current = null;
|
|
5398
5762
|
sessionRef.current = { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5399
5763
|
writeToStdout("\x1B[2J\x1B[3J\x1B[H");
|
|
5400
5764
|
push({ kind: "notice", text: " cleared \u2014 new session, empty context" });
|
|
@@ -5481,16 +5845,19 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5481
5845
|
return;
|
|
5482
5846
|
case "wallet":
|
|
5483
5847
|
await runBusy(LOADING, async (signal) => {
|
|
5848
|
+
const streakNow = await client.claimStreak(signal);
|
|
5484
5849
|
const w = await client.wallet(signal);
|
|
5485
5850
|
setBalance(w.balance);
|
|
5486
5851
|
const reversals = w.ledger.filter((e) => e.reason === "ad_reversal");
|
|
5487
5852
|
const reversed = reversals.reduce((sum, e) => sum + Math.abs(e.delta), 0);
|
|
5488
5853
|
const screenouts = w.screenouts_today ?? 0;
|
|
5489
5854
|
const screenoutCredits = w.screenout_credits_today ?? 0;
|
|
5855
|
+
const streakLine = streakNow && streakLabel(streakNow);
|
|
5490
5856
|
push({
|
|
5491
5857
|
kind: "notice",
|
|
5492
5858
|
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` + (screenouts ? `
|
|
5493
|
-
plus ${screenoutCredits.toLocaleString("en-US")} credits from ${screenouts} screenout bonus${screenouts === 1 ? "" : "es"} \u2014 attempts that didn't qualify, which CPX pays a little for anyway.` : "") + (
|
|
5859
|
+
plus ${screenoutCredits.toLocaleString("en-US")} credits from ${screenouts} screenout bonus${screenouts === 1 ? "" : "es"} \u2014 attempts that didn't qualify, which CPX pays a little for anyway.` : "") + (streakLine ? `
|
|
5860
|
+
${streakLine}` : "") + (reversals.length ? `
|
|
5494
5861
|
recently: ${reversed.toLocaleString("en-US")} credits from ${reversals.length} offer${reversals.length === 1 ? "" : "s"} were reversed by the provider. Run \`clixad wallet\` for the full ledger.` : "")
|
|
5495
5862
|
});
|
|
5496
5863
|
});
|
|
@@ -5515,7 +5882,7 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5515
5882
|
return;
|
|
5516
5883
|
}
|
|
5517
5884
|
setMessages(found.messages);
|
|
5518
|
-
sessionRef.current = { id: found.id, started: found.started };
|
|
5885
|
+
sessionRef.current = { id: found.id, started: found.started, title: found.title };
|
|
5519
5886
|
push({
|
|
5520
5887
|
kind: "notice",
|
|
5521
5888
|
tone: "good",
|
|
@@ -5598,6 +5965,32 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5598
5965
|
case "earn":
|
|
5599
5966
|
await runAdWall();
|
|
5600
5967
|
return;
|
|
5968
|
+
// Through `runBusy` like every other gateway call: the free instance can
|
|
5969
|
+
// cold-start for the better part of a minute, and the box says "esc to
|
|
5970
|
+
// stop" whenever it is locked.
|
|
5971
|
+
case "refer":
|
|
5972
|
+
await runBusy(LOADING, async (signal) => {
|
|
5973
|
+
const info = await client.referral(signal);
|
|
5974
|
+
push({ kind: "notice", text: referLines(info).map((line3) => ` ${line3}`).join("\n") });
|
|
5975
|
+
});
|
|
5976
|
+
return;
|
|
5977
|
+
case "redeem": {
|
|
5978
|
+
if (!arg) {
|
|
5979
|
+
push({ kind: "notice", tone: "warn", text: " usage: /redeem <code>" });
|
|
5980
|
+
return;
|
|
5981
|
+
}
|
|
5982
|
+
await runBusy(LOADING, async (signal) => {
|
|
5983
|
+
const result = await client.redeem(arg, signal);
|
|
5984
|
+
const { ok, text: text2 } = redeemMessage(result);
|
|
5985
|
+
if (ok) setBalance(result.balance);
|
|
5986
|
+
push({
|
|
5987
|
+
kind: "notice",
|
|
5988
|
+
tone: ok ? "good" : "warn",
|
|
5989
|
+
text: ok ? ` ${text2} \xB7 balance ${result.balance.toLocaleString("en-US")}` : ` ${text2}`
|
|
5990
|
+
});
|
|
5991
|
+
});
|
|
5992
|
+
return;
|
|
5993
|
+
}
|
|
5601
5994
|
case "login":
|
|
5602
5995
|
await runLogin(arg);
|
|
5603
5996
|
return;
|
|
@@ -5665,14 +6058,58 @@ ${output}` }
|
|
|
5665
6058
|
},
|
|
5666
6059
|
[push, root, runCommand, runShell, runTurn2]
|
|
5667
6060
|
);
|
|
6061
|
+
const askFeedback = useCallback(async () => {
|
|
6062
|
+
const turn = lastTurnRef.current;
|
|
6063
|
+
if (!turn) return;
|
|
6064
|
+
lastTurnRef.current = null;
|
|
6065
|
+
const amount = wallet?.rewards?.feedback_credits;
|
|
6066
|
+
const pay = amount ? `Both answers pay ${amount.toLocaleString("en-US")} credits` : "Both answers pay the same";
|
|
6067
|
+
const choice = await new Promise((resolve2) => {
|
|
6068
|
+
setPickerSel(0);
|
|
6069
|
+
setPicker({
|
|
6070
|
+
title: "Was that last answer any good?",
|
|
6071
|
+
subtitle: `${pay} \xB7 we keep that exchange to test against \xB7 esc skips`,
|
|
6072
|
+
items: [
|
|
6073
|
+
{ value: "up", label: "yes \u2014 that worked", hint: "" },
|
|
6074
|
+
{ value: "down", label: "no \u2014 that missed", hint: "" },
|
|
6075
|
+
{ value: "skip", label: "skip", hint: "nothing is sent, nothing is stored" }
|
|
6076
|
+
],
|
|
6077
|
+
onPick: (item) => resolve2(item.value),
|
|
6078
|
+
// Esc and "skip" are the same outcome and must stay that way: a skip
|
|
6079
|
+
// that cost something is a prompt people stop reading.
|
|
6080
|
+
onCancel: () => resolve2(null)
|
|
6081
|
+
});
|
|
6082
|
+
});
|
|
6083
|
+
if (choice !== "up" && choice !== "down") return;
|
|
6084
|
+
await runBusy(SENDING_FEEDBACK, async (signal) => {
|
|
6085
|
+
const outcome = await client.sendFeedback(
|
|
6086
|
+
{ ...turn, session: sessionRef.current.id, rating: choice },
|
|
6087
|
+
signal
|
|
6088
|
+
);
|
|
6089
|
+
if (!outcome) return;
|
|
6090
|
+
if (outcome.credited > 0) setBalance(outcome.balance);
|
|
6091
|
+
push({
|
|
6092
|
+
kind: "notice",
|
|
6093
|
+
tone: "good",
|
|
6094
|
+
text: outcome.credited > 0 ? ` thanks \u2014 +${outcome.credited.toLocaleString("en-US")} credits` : (
|
|
6095
|
+
// Recorded and unpaid: the daily ceiling, or a turn already rated.
|
|
6096
|
+
// Worth saying, because the offer of credits was made a moment ago.
|
|
6097
|
+
" thanks \u2014 noted (today's feedback credits are already in)"
|
|
6098
|
+
)
|
|
6099
|
+
});
|
|
6100
|
+
});
|
|
6101
|
+
}, [client, push, runBusy, wallet]);
|
|
5668
6102
|
const runSerially = useCallback(
|
|
5669
6103
|
async (line2) => {
|
|
5670
6104
|
await dispatchLine(line2);
|
|
5671
6105
|
for (let next = dequeue(); next !== void 0; next = dequeue()) {
|
|
5672
6106
|
await dispatchLine(next);
|
|
5673
6107
|
}
|
|
6108
|
+
if (lastTurnRef.current && turnCountRef.current % FEEDBACK_TURN_INTERVAL === 0) {
|
|
6109
|
+
await askFeedback();
|
|
6110
|
+
}
|
|
5674
6111
|
},
|
|
5675
|
-
[dequeue, dispatchLine]
|
|
6112
|
+
[askFeedback, dequeue, dispatchLine]
|
|
5676
6113
|
);
|
|
5677
6114
|
const submit = useCallback(
|
|
5678
6115
|
async (raw) => {
|
|
@@ -5722,9 +6159,27 @@ ${output}` }
|
|
|
5722
6159
|
setEditor((state) => insert(state, text2));
|
|
5723
6160
|
setHistIdx(-1);
|
|
5724
6161
|
});
|
|
6162
|
+
const dismissModals = useCallback(() => {
|
|
6163
|
+
if (plan) {
|
|
6164
|
+
setPlan(null);
|
|
6165
|
+
plan.resolve("keepPlanning");
|
|
6166
|
+
}
|
|
6167
|
+
if (ask2) {
|
|
6168
|
+
setAsk(null);
|
|
6169
|
+
ask2.resolve("deny");
|
|
6170
|
+
}
|
|
6171
|
+
if (picker) {
|
|
6172
|
+
const cancel = picker.onCancel;
|
|
6173
|
+
setPicker(null);
|
|
6174
|
+
cancel?.();
|
|
6175
|
+
}
|
|
6176
|
+
}, [ask2, picker, plan]);
|
|
5725
6177
|
useInput((ch, key) => {
|
|
5726
6178
|
if (key.ctrl && ch === "c") {
|
|
5727
|
-
if (busy)
|
|
6179
|
+
if (busy) {
|
|
6180
|
+
dismissModals();
|
|
6181
|
+
return stopCurrent();
|
|
6182
|
+
}
|
|
5728
6183
|
if (!isEmpty(editor)) return setEditor(EMPTY);
|
|
5729
6184
|
if (Date.now() - ctrlCRef.current < 2e3) return exit();
|
|
5730
6185
|
ctrlCRef.current = Date.now();
|
|
@@ -6154,7 +6609,7 @@ function tailLines(text, max) {
|
|
|
6154
6609
|
const lines = text.split("\n");
|
|
6155
6610
|
return lines.length <= max ? text : lines.slice(-max).join("\n");
|
|
6156
6611
|
}
|
|
6157
|
-
var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, ASK_HINT, PLAN_HINT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
|
|
6612
|
+
var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, SENDING_FEEDBACK, FEEDBACK_TURN_INTERVAL, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, ASK_HINT, PLAN_HINT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
|
|
6158
6613
|
var init_app = __esm({
|
|
6159
6614
|
"src/tui/app.tsx"() {
|
|
6160
6615
|
"use strict";
|
|
@@ -6191,6 +6646,8 @@ var init_app = __esm({
|
|
|
6191
6646
|
COMPACTING = "compacting\u2026";
|
|
6192
6647
|
SIGNING_IN = "signing in\u2026";
|
|
6193
6648
|
RUNNING = "running\u2026";
|
|
6649
|
+
SENDING_FEEDBACK = "sending\u2026";
|
|
6650
|
+
FEEDBACK_TURN_INTERVAL = 50;
|
|
6194
6651
|
NOT_SIGNED_IN = " Not signed in, or the stored token isn't valid for this gateway.\n Run /login to sign in.";
|
|
6195
6652
|
LIVE_OUTPUT_LINES = 5;
|
|
6196
6653
|
COMMITTED_OUTPUT_LINES = 4;
|
|
@@ -6313,6 +6770,10 @@ async function main() {
|
|
|
6313
6770
|
return showWallet(client);
|
|
6314
6771
|
case "earn":
|
|
6315
6772
|
return earn(client, config);
|
|
6773
|
+
case "refer":
|
|
6774
|
+
return refer(client, config);
|
|
6775
|
+
case "redeem":
|
|
6776
|
+
return redeem(client, config, rest[0]);
|
|
6316
6777
|
case "buy":
|
|
6317
6778
|
return buyCmd(client, config, rest[0]);
|
|
6318
6779
|
case "ask":
|
|
@@ -6374,6 +6835,7 @@ async function login(client, config, args) {
|
|
|
6374
6835
|
\u2713 logged in as ${who}`) + suffix);
|
|
6375
6836
|
const bonus = account.created ? c.dim(account.signupBonus === void 0 ? " (signup bonus)" : ` (signup bonus ${credits(account.signupBonus)})`) : "";
|
|
6376
6837
|
console.log(` balance: ${c.bold(credits(account.balance))} credits${bonus}`);
|
|
6838
|
+
await showStreak(client);
|
|
6377
6839
|
console.log(c.dim(` token stored in ${configPath()}`));
|
|
6378
6840
|
return;
|
|
6379
6841
|
}
|
|
@@ -6419,6 +6881,7 @@ function logout(config) {
|
|
|
6419
6881
|
delete config.token;
|
|
6420
6882
|
delete config.userId;
|
|
6421
6883
|
delete config.email;
|
|
6884
|
+
delete config.login;
|
|
6422
6885
|
saveConfig(config);
|
|
6423
6886
|
console.log(c.green("\u2713 logged out"));
|
|
6424
6887
|
}
|
|
@@ -6457,9 +6920,12 @@ async function setModel(client, config, model) {
|
|
|
6457
6920
|
console.log(c.green(`\u2713 model set to ${model}`));
|
|
6458
6921
|
}
|
|
6459
6922
|
async function showWallet(client) {
|
|
6923
|
+
const streak = await client.claimStreak();
|
|
6460
6924
|
const w = await client.wallet();
|
|
6461
6925
|
console.log(`balance: ${c.bold(w.balance.toLocaleString("en-US"))} credits`);
|
|
6462
6926
|
console.log(`${earnedToday(w)} \xB7 offers pay ${grantRangeLabel(w.grant_per_ad_range)}`);
|
|
6927
|
+
const line2 = streak && streakLabel(streak);
|
|
6928
|
+
if (line2) console.log(streak.claimed ? c.green(line2) : c.dim(line2));
|
|
6463
6929
|
if (w.ledger.length) {
|
|
6464
6930
|
console.log(c.dim("recent:"));
|
|
6465
6931
|
const label = (e) => ledgerReasonLabel(e.reason);
|
|
@@ -6506,6 +6972,40 @@ async function earn(client, config) {
|
|
|
6506
6972
|
);
|
|
6507
6973
|
console.log(c.yellow(" pays nothing and is normal. Start another, or check `clixad wallet` later."));
|
|
6508
6974
|
}
|
|
6975
|
+
async function refer(client, config) {
|
|
6976
|
+
if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
|
|
6977
|
+
let info;
|
|
6978
|
+
try {
|
|
6979
|
+
info = await client.referral();
|
|
6980
|
+
} catch (err) {
|
|
6981
|
+
console.error(c.red(err.message));
|
|
6982
|
+
process.exitCode = 1;
|
|
6983
|
+
return;
|
|
6984
|
+
}
|
|
6985
|
+
const [code, share, deal, ...rest] = referLines(info);
|
|
6986
|
+
console.log(`
|
|
6987
|
+
${c.bold(code)}`);
|
|
6988
|
+
console.log(` ${c.cyan(share)}`);
|
|
6989
|
+
console.log(c.dim(` ${deal}`));
|
|
6990
|
+
for (const line2 of rest) console.log(c.dim(` ${line2}`));
|
|
6991
|
+
console.log("");
|
|
6992
|
+
}
|
|
6993
|
+
async function redeem(client, config, code) {
|
|
6994
|
+
if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
|
|
6995
|
+
if (!code) return console.error(c.red("usage: clixad redeem <code>"));
|
|
6996
|
+
let result;
|
|
6997
|
+
try {
|
|
6998
|
+
result = await client.redeem(code);
|
|
6999
|
+
} catch (err) {
|
|
7000
|
+
console.error(c.red(err.message));
|
|
7001
|
+
process.exitCode = 1;
|
|
7002
|
+
return;
|
|
7003
|
+
}
|
|
7004
|
+
const { ok, text } = redeemMessage(result);
|
|
7005
|
+
if (!ok) return console.log(c.yellow(` ${text}`));
|
|
7006
|
+
console.log(c.green(`
|
|
7007
|
+
\u2713 ${text}`) + ` \xB7 balance ${c.bold(credits(result.balance))}`);
|
|
7008
|
+
}
|
|
6509
7009
|
async function buyCmd(client, config, pack) {
|
|
6510
7010
|
if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
|
|
6511
7011
|
const { enabled, packs } = await client.creditPacks();
|
|
@@ -6672,10 +7172,11 @@ async function repl(client, config, opts = {}) {
|
|
|
6672
7172
|
session = loadSession(opts.resume);
|
|
6673
7173
|
if (!session) return console.log(c.red(`no such session: ${opts.resume}`));
|
|
6674
7174
|
}
|
|
7175
|
+
const streak = await client.claimStreak();
|
|
6675
7176
|
const wallet = await safeWallet(client);
|
|
6676
7177
|
clearScreen();
|
|
6677
7178
|
const { startTui: startTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
|
|
6678
|
-
await startTui2({ client, config, wallet, session, initialTask: opts.task });
|
|
7179
|
+
await startTui2({ client, config, wallet, streak, session, initialTask: opts.task });
|
|
6679
7180
|
}
|
|
6680
7181
|
async function runTurn(client, config, messages) {
|
|
6681
7182
|
try {
|
|
@@ -6702,6 +7203,13 @@ async function runTurn(client, config, messages) {
|
|
|
6702
7203
|
return void 0;
|
|
6703
7204
|
}
|
|
6704
7205
|
}
|
|
7206
|
+
async function showStreak(client) {
|
|
7207
|
+
const streak = await client.claimStreak();
|
|
7208
|
+
const line2 = streak && streakLabel(streak);
|
|
7209
|
+
if (!line2) return streak;
|
|
7210
|
+
console.log(streak.claimed ? c.green(` ${line2}`) : c.dim(` ${line2}`));
|
|
7211
|
+
return streak;
|
|
7212
|
+
}
|
|
6705
7213
|
async function safeWallet(client) {
|
|
6706
7214
|
try {
|
|
6707
7215
|
return await client.wallet();
|
|
@@ -6719,6 +7227,8 @@ function printHelp() {
|
|
|
6719
7227
|
${c.cyan("model")} <id> set default model
|
|
6720
7228
|
${c.cyan("wallet")} balance, ads today, ledger
|
|
6721
7229
|
${c.cyan("earn")} open the ad wall to earn credits
|
|
7230
|
+
${c.cyan("refer")} your invite code, and what it has earned
|
|
7231
|
+
${c.cyan("redeem")} <code> spend a code you were given
|
|
6722
7232
|
${c.cyan("buy")} [pack] list credit packs / buy one via Stripe
|
|
6723
7233
|
${c.cyan("ask")} "<prompt>" one-shot completion (no tools)
|
|
6724
7234
|
${c.cyan("agent")} "<task>" open the REPL with a task already running
|
|
@@ -6728,7 +7238,9 @@ function printHelp() {
|
|
|
6728
7238
|
${c.cyan("--resume")} [id] list saved sessions, or resume one
|
|
6729
7239
|
${c.cyan("--version")} print the version and exit
|
|
6730
7240
|
(no command) interactive coding REPL ${c.dim("(signs you in if needed)")}
|
|
6731
|
-
"<task>" same as ${c.cyan("agent")} \u2014 anything that is not a command is a task
|
|
7241
|
+
"<task>" same as ${c.cyan("agent")} \u2014 anything that is not a command is a task
|
|
7242
|
+
|
|
7243
|
+
${c.dim("Discord: https://discord.gg/pwxPM3QyF \u2014 bugs and feature requests are read there")}`);
|
|
6732
7244
|
}
|
|
6733
7245
|
main().catch((err) => {
|
|
6734
7246
|
console.error(c.red(err.message));
|