free-pi-cli 0.6.19 → 0.6.21
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 +27 -1
- package/dist/index.js +1 -1
- package/dist/{main-qn3gsj3n.js → main-7sxf1v5b.js} +1089 -162
- package/package.json +2 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
4
|
import { spawn as spawn4 } from "node:child_process";
|
|
5
|
-
import { realpathSync as realpathSync8, writeFileSync as
|
|
5
|
+
import { realpathSync as realpathSync8, writeFileSync as writeFileSync7 } from "node:fs";
|
|
6
6
|
import { homedir as homedir10 } from "node:os";
|
|
7
|
-
import { dirname as
|
|
7
|
+
import { dirname as dirname15, join as join16 } from "node:path";
|
|
8
8
|
|
|
9
9
|
// src/browser.ts
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
@@ -515,7 +515,7 @@ function folderMatches(cwd, sessionCwd) {
|
|
|
515
515
|
// package.json
|
|
516
516
|
var package_default = {
|
|
517
517
|
name: "free-pi-cli",
|
|
518
|
-
version: "0.6.
|
|
518
|
+
version: "0.6.21",
|
|
519
519
|
description: "Free AI coding agent for your terminal. Ads and a daily free allowance pay for the models; sign in with GitHub and run npx free-pi-cli.",
|
|
520
520
|
keywords: [
|
|
521
521
|
"coding agent",
|
|
@@ -554,6 +554,7 @@ var package_default = {
|
|
|
554
554
|
prepublishOnly: "bun run build"
|
|
555
555
|
},
|
|
556
556
|
dependencies: {
|
|
557
|
+
"@earendil-works/pi-ai": "1.2.0",
|
|
557
558
|
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
558
559
|
"@earendil-works/pi-tui": "1.2.0",
|
|
559
560
|
"@gravity-ai/api": "1.2.1"
|
|
@@ -569,7 +570,7 @@ var package_default = {
|
|
|
569
570
|
var CLI_VERSION = package_default.version;
|
|
570
571
|
|
|
571
572
|
// src/output-limit.ts
|
|
572
|
-
var MAX_OUTPUT_TOKENS =
|
|
573
|
+
var MAX_OUTPUT_TOKENS = 32768;
|
|
573
574
|
var OUTPUT_LIMIT_MESSAGE = "This tool call was cut off by the output limit before it finished. It either did not run or ran with incomplete content, " + "so a file may be partly written. Check what was written, then redo the work in smaller parts " + "(for example write the first part, then add the rest with edit).";
|
|
574
575
|
function createOutputLimitExtension() {
|
|
575
576
|
let truncated = new Set;
|
|
@@ -6020,6 +6021,8 @@ var AdNextResponseSchema = object({
|
|
|
6020
6021
|
});
|
|
6021
6022
|
var GRAVITY_CONTEXT_HEADER = "x-gravity-context";
|
|
6022
6023
|
var GRAVITY_CONTEXT_MAX_BYTES = 8192;
|
|
6024
|
+
var AD_CONTEXT_HEADER = "x-ad-context";
|
|
6025
|
+
var AD_CONTEXT_MAX_CHARS = 1000;
|
|
6023
6026
|
var AdImpressionRequestSchema = object({
|
|
6024
6027
|
ad_id: string2(),
|
|
6025
6028
|
click_token: string2(),
|
|
@@ -6583,14 +6586,15 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
6583
6586
|
function piMcpCliPath() {
|
|
6584
6587
|
return join4(dirname5(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "extensions", "mcp", "cli.js");
|
|
6585
6588
|
}
|
|
6589
|
+
var MCP_UNAVAILABLE = "free-pi-cli mcp isn't available in this install. Reinstall with npx free-pi-cli@latest.";
|
|
6586
6590
|
async function loadPiRunner() {
|
|
6587
6591
|
const path = piMcpCliPath();
|
|
6588
6592
|
if (!existsSync2(path)) {
|
|
6589
|
-
throw new Error(
|
|
6593
|
+
throw new Error(MCP_UNAVAILABLE);
|
|
6590
6594
|
}
|
|
6591
6595
|
const mod = await import(pathToFileURL(path).href);
|
|
6592
6596
|
if (typeof mod.runMcpCommand !== "function") {
|
|
6593
|
-
throw new Error(
|
|
6597
|
+
throw new Error(MCP_UNAVAILABLE);
|
|
6594
6598
|
}
|
|
6595
6599
|
return mod.runMcpCommand;
|
|
6596
6600
|
}
|
|
@@ -6608,7 +6612,9 @@ async function runFreePiMcp(args, deps) {
|
|
|
6608
6612
|
error: (line) => deps.error(brandMcpLine(line))
|
|
6609
6613
|
});
|
|
6610
6614
|
} catch (err) {
|
|
6611
|
-
|
|
6615
|
+
const msg = (err instanceof Error ? err.message : String(err)).split(`
|
|
6616
|
+
at `)[0];
|
|
6617
|
+
deps.error(msg === MCP_UNAVAILABLE ? msg : `free-pi-cli mcp failed: ${msg}`);
|
|
6612
6618
|
return 1;
|
|
6613
6619
|
}
|
|
6614
6620
|
}
|
|
@@ -6763,6 +6769,52 @@ function gravityHeader(deps) {
|
|
|
6763
6769
|
return {};
|
|
6764
6770
|
}
|
|
6765
6771
|
}
|
|
6772
|
+
function textParts(content) {
|
|
6773
|
+
if (typeof content === "string")
|
|
6774
|
+
return content;
|
|
6775
|
+
if (!Array.isArray(content))
|
|
6776
|
+
return "";
|
|
6777
|
+
return content.map((p) => p && p.type === "text" && typeof p.text === "string" ? p.text : "").filter(Boolean).join(`
|
|
6778
|
+
`);
|
|
6779
|
+
}
|
|
6780
|
+
function cut(redact, text) {
|
|
6781
|
+
const full = redact(text.slice(0, 20000));
|
|
6782
|
+
const s = full.slice(0, AD_CONTEXT_MAX_CHARS);
|
|
6783
|
+
return full.length > s.length && /\S/.test(full[s.length]) ? s.replace(/\S+$/, "").trimEnd() : s;
|
|
6784
|
+
}
|
|
6785
|
+
function adContextHeader(deps, ctx, prompt) {
|
|
6786
|
+
if (!deps.adContext || !ctx)
|
|
6787
|
+
return;
|
|
6788
|
+
try {
|
|
6789
|
+
const out = {};
|
|
6790
|
+
const entries = ctx.sessionManager.getBranch();
|
|
6791
|
+
if (prompt?.trim())
|
|
6792
|
+
out.user = cut(deps.adContext.redact, prompt.trim());
|
|
6793
|
+
for (let i = entries.length - 1;i >= 0 && !(out.user && out.assistant); i--) {
|
|
6794
|
+
const m = entries[i].type === "message" ? entries[i].message : undefined;
|
|
6795
|
+
if (m?.role !== "user" && m?.role !== "assistant" || out[m.role])
|
|
6796
|
+
continue;
|
|
6797
|
+
const text = textParts(m.content).trim();
|
|
6798
|
+
if (text)
|
|
6799
|
+
out[m.role] = cut(deps.adContext.redact, text);
|
|
6800
|
+
}
|
|
6801
|
+
if (!out.user && !out.assistant)
|
|
6802
|
+
return;
|
|
6803
|
+
const limit = Math.min(4096, 16 * 1024 - 2500 - (deps.getGravityContext?.()?.length ?? 0));
|
|
6804
|
+
if (limit < 600)
|
|
6805
|
+
return;
|
|
6806
|
+
let encoded = Buffer.from(JSON.stringify(out)).toString("base64url");
|
|
6807
|
+
while (encoded.length > limit && (out.user || out.assistant)) {
|
|
6808
|
+
for (const k of ["user", "assistant"])
|
|
6809
|
+
if (out[k])
|
|
6810
|
+
out[k] = out[k].slice(0, Math.floor(out[k].length / 2));
|
|
6811
|
+
encoded = Buffer.from(JSON.stringify(out)).toString("base64url");
|
|
6812
|
+
}
|
|
6813
|
+
return encoded;
|
|
6814
|
+
} catch {
|
|
6815
|
+
return;
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6766
6818
|
var IMP_TIMEOUT_MS = 3000;
|
|
6767
6819
|
function isGravityImpUrl(raw) {
|
|
6768
6820
|
if (!raw)
|
|
@@ -6782,9 +6834,11 @@ function fireGravityImpression(deps, ad) {
|
|
|
6782
6834
|
} catch {}
|
|
6783
6835
|
return true;
|
|
6784
6836
|
}
|
|
6785
|
-
async function fetchAdNext(deps, slot, trigger) {
|
|
6837
|
+
async function fetchAdNext(deps, slot, trigger, ctx, prompt) {
|
|
6786
6838
|
try {
|
|
6787
|
-
const
|
|
6839
|
+
const pair = adContextHeader(deps, ctx, prompt);
|
|
6840
|
+
const headers = { ...trigger ? { "x-ad-trigger": trigger } : {}, ...pair ? { [AD_CONTEXT_HEADER]: pair } : {} };
|
|
6841
|
+
const res = await authedFetch(deps, `/ads/next?slot=${slot}`, { headers });
|
|
6788
6842
|
if (res.status === 204 || !res.ok)
|
|
6789
6843
|
return;
|
|
6790
6844
|
return await res.json();
|
|
@@ -6884,8 +6938,8 @@ function createBannerRenderer(opts = {}) {
|
|
|
6884
6938
|
let lastPostedToken;
|
|
6885
6939
|
const screen = opts.screen ?? {};
|
|
6886
6940
|
return {
|
|
6887
|
-
async render(deps, ctx, config, trigger) {
|
|
6888
|
-
const ad = await fetchAdNext(deps, "banner", trigger);
|
|
6941
|
+
async render(deps, ctx, config, trigger, prompt) {
|
|
6942
|
+
const ad = await fetchAdNext(deps, "banner", trigger, ctx, prompt);
|
|
6889
6943
|
if (opts.isActive && !opts.isActive())
|
|
6890
6944
|
return;
|
|
6891
6945
|
if (!ad) {
|
|
@@ -6943,7 +6997,7 @@ function createInlineRenderer(screen = {}) {
|
|
|
6943
6997
|
return;
|
|
6944
6998
|
if (screen.banner === "gravity")
|
|
6945
6999
|
return;
|
|
6946
|
-
const ad = await fetchAdNext(deps, "inline");
|
|
7000
|
+
const ad = await fetchAdNext(deps, "inline", undefined, ctx);
|
|
6947
7001
|
if (!ad)
|
|
6948
7002
|
return;
|
|
6949
7003
|
const isGravity = ad.source === "gravity";
|
|
@@ -7161,6 +7215,7 @@ function createAdsExtension(opts) {
|
|
|
7161
7215
|
clientVersion: opts.clientVersion,
|
|
7162
7216
|
fetchImpl: opts.fetchImpl,
|
|
7163
7217
|
getGravityContext: opts.getGravityContext,
|
|
7218
|
+
adContext: opts.adContext,
|
|
7164
7219
|
onMe: opts.onMe
|
|
7165
7220
|
};
|
|
7166
7221
|
const config = loadConfig();
|
|
@@ -7176,14 +7231,14 @@ function createAdsExtension(opts) {
|
|
|
7176
7231
|
let launchAt = 0;
|
|
7177
7232
|
let lastBannerAt = 0;
|
|
7178
7233
|
let bannerInFlight = false;
|
|
7179
|
-
const renderBanner = async (ctx, trigger, force = false) => {
|
|
7234
|
+
const renderBanner = async (ctx, trigger, force = false, prompt) => {
|
|
7180
7235
|
if (stopped)
|
|
7181
7236
|
return;
|
|
7182
7237
|
if (!force && (bannerInFlight || Date.now() - lastBannerAt < BANNER_MIN_REFRESH_MS))
|
|
7183
7238
|
return;
|
|
7184
7239
|
bannerInFlight = true;
|
|
7185
7240
|
try {
|
|
7186
|
-
await banner.render(deps, ctx, config, trigger);
|
|
7241
|
+
await banner.render(deps, ctx, config, trigger, prompt);
|
|
7187
7242
|
} finally {
|
|
7188
7243
|
bannerInFlight = false;
|
|
7189
7244
|
lastBannerAt = Date.now();
|
|
@@ -7207,7 +7262,7 @@ function createAdsExtension(opts) {
|
|
|
7207
7262
|
await Promise.all([renderBanner(ctx, "launch", true), renderMeter(deps, ctx)]);
|
|
7208
7263
|
});
|
|
7209
7264
|
const LAUNCH_AD_MAX_AGE_MS = 60 * 60000;
|
|
7210
|
-
const newMessage = async (ctx) => {
|
|
7265
|
+
const newMessage = async (ctx, prompt) => {
|
|
7211
7266
|
if (launchTurnUnused) {
|
|
7212
7267
|
launchTurnUnused = false;
|
|
7213
7268
|
const token = await Promise.resolve(deps.getToken()).catch(() => {
|
|
@@ -7217,17 +7272,17 @@ function createAdsExtension(opts) {
|
|
|
7217
7272
|
return;
|
|
7218
7273
|
}
|
|
7219
7274
|
startTurn();
|
|
7220
|
-
renderBanner(ctx, "user_message", true);
|
|
7275
|
+
renderBanner(ctx, "user_message", true, prompt);
|
|
7221
7276
|
};
|
|
7222
7277
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
7223
7278
|
if (event.messageType !== "user")
|
|
7224
7279
|
return;
|
|
7225
|
-
await newMessage(ctx);
|
|
7280
|
+
await newMessage(ctx, event.prompt);
|
|
7226
7281
|
});
|
|
7227
7282
|
pi.on("input", (event, ctx) => {
|
|
7228
7283
|
const e = event;
|
|
7229
7284
|
if (e.streamingBehavior && e.source !== "extension")
|
|
7230
|
-
return newMessage(ctx).then(() => ({ action: "continue" }));
|
|
7285
|
+
return newMessage(ctx, e.text).then(() => ({ action: "continue" }));
|
|
7231
7286
|
return { action: "continue" };
|
|
7232
7287
|
});
|
|
7233
7288
|
pi.on("turn_end", async (_event, ctx) => {
|
|
@@ -7243,7 +7298,7 @@ function createAdsExtension(opts) {
|
|
|
7243
7298
|
}
|
|
7244
7299
|
|
|
7245
7300
|
// src/pi-launch.ts
|
|
7246
|
-
import { dirname as
|
|
7301
|
+
import { dirname as dirname12, join as join12, resolve as resolve4 } from "node:path";
|
|
7247
7302
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
7248
7303
|
|
|
7249
7304
|
// ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/metrics.mjs
|
|
@@ -7573,6 +7628,28 @@ var integerKeyPattern = new RegExp("^(?:0|[1-9][0-9]*)$");
|
|
|
7573
7628
|
|
|
7574
7629
|
// ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/engine/indexed/from_object.mjs
|
|
7575
7630
|
var NumericKeyPattern = new RegExp(IntegerKey);
|
|
7631
|
+
// src/friendly-error.ts
|
|
7632
|
+
function friendlyThrown(e) {
|
|
7633
|
+
const name = e instanceof Error ? e.name : "";
|
|
7634
|
+
if (name === "TimeoutError")
|
|
7635
|
+
return "free-pi took too long to answer. Try again.";
|
|
7636
|
+
if (name === "AbortError")
|
|
7637
|
+
return "Cancelled.";
|
|
7638
|
+
if (name === "SyntaxError")
|
|
7639
|
+
return "free-pi sent an unexpected reply. Try again.";
|
|
7640
|
+
return "Couldn't reach free-pi — check your connection.";
|
|
7641
|
+
}
|
|
7642
|
+
function friendlyStatus(status, audience = "user", message) {
|
|
7643
|
+
if (status === 401) {
|
|
7644
|
+
return audience === "model" ? "The user's free-pi sign-in has expired; tell them to run /login." : "Your free-pi sign-in has expired. Run /login.";
|
|
7645
|
+
}
|
|
7646
|
+
if (message?.trim())
|
|
7647
|
+
return message.trim();
|
|
7648
|
+
if (status >= 500)
|
|
7649
|
+
return `free-pi is having trouble right now (HTTP ${status}). Try again in a minute.`;
|
|
7650
|
+
return `free-pi could not complete the request (HTTP ${status}).`;
|
|
7651
|
+
}
|
|
7652
|
+
|
|
7576
7653
|
// src/usage-tool.ts
|
|
7577
7654
|
var USAGE_TOOL_NAME = "free_pi_usage";
|
|
7578
7655
|
var PARAMS = _Object_({});
|
|
@@ -7630,11 +7707,11 @@ async function fetchUsageText(opts) {
|
|
|
7630
7707
|
signal: AbortSignal.timeout(5000)
|
|
7631
7708
|
});
|
|
7632
7709
|
if (!res.ok)
|
|
7633
|
-
return { ok: false, text:
|
|
7710
|
+
return { ok: false, text: friendlyStatus(res.status) };
|
|
7634
7711
|
const stats = await res.json();
|
|
7635
7712
|
return { ok: true, text: formatStats(stats), stats };
|
|
7636
7713
|
} catch (err) {
|
|
7637
|
-
return { ok: false, text:
|
|
7714
|
+
return { ok: false, text: friendlyThrown(err) };
|
|
7638
7715
|
}
|
|
7639
7716
|
}
|
|
7640
7717
|
function createUsageToolExtension(opts) {
|
|
@@ -7702,11 +7779,9 @@ async function post(opts, body, signal) {
|
|
|
7702
7779
|
const mapped = ERROR_TEXT[code];
|
|
7703
7780
|
if (mapped)
|
|
7704
7781
|
return { ok: false, text: mapped };
|
|
7705
|
-
return { ok: false, text:
|
|
7782
|
+
return { ok: false, text: friendlyStatus(res.status, "model", j.error?.message ?? j.message) };
|
|
7706
7783
|
} catch (e) {
|
|
7707
|
-
|
|
7708
|
-
return { ok: false, text: "Image generation timed out after 120 seconds." };
|
|
7709
|
-
return { ok: false, text: `Request failed: ${String(e)}` };
|
|
7784
|
+
return { ok: false, text: friendlyThrown(e) };
|
|
7710
7785
|
}
|
|
7711
7786
|
}
|
|
7712
7787
|
function slug(s) {
|
|
@@ -7783,7 +7858,8 @@ function createImageToolExtension(opts) {
|
|
|
7783
7858
|
await fh.close();
|
|
7784
7859
|
}
|
|
7785
7860
|
} catch (e) {
|
|
7786
|
-
|
|
7861
|
+
const why = { EACCES: "permission denied", EPERM: "permission denied", ENOSPC: "the disk is full", ENOENT: "the folder does not exist" }[e.code ?? ""] ?? "it could not be written";
|
|
7862
|
+
return fail(`Couldn't save the image to images: ${why}.`);
|
|
7787
7863
|
}
|
|
7788
7864
|
return done(`Saved image to ${rel} (${ext}, ${buf.length} bytes). You cannot see the image; reference this path in your answer or code, and tell the user you could not check how it looks.`, { ok: true, path: rel, cost_usd: d.cost_usd, model: d.model });
|
|
7789
7865
|
}
|
|
@@ -8392,11 +8468,9 @@ async function post2(opts, path, body, label, signal) {
|
|
|
8392
8468
|
const mapped = ERROR_TEXT3[err.code ?? ""];
|
|
8393
8469
|
if (mapped)
|
|
8394
8470
|
return { ok: false, text: typeof mapped === "function" ? mapped(label) : mapped };
|
|
8395
|
-
return { ok: false, text:
|
|
8471
|
+
return { ok: false, text: friendlyStatus(res.status, "model", err.message) };
|
|
8396
8472
|
} catch (e) {
|
|
8397
|
-
|
|
8398
|
-
return { ok: false, text: "Request timed out after 30 seconds." };
|
|
8399
|
-
return { ok: false, text: `Request failed: ${String(e)}` };
|
|
8473
|
+
return { ok: false, text: friendlyThrown(e) };
|
|
8400
8474
|
}
|
|
8401
8475
|
}
|
|
8402
8476
|
var done3 = (o) => ({ content: [{ type: "text", text: o.text }], details: o.details });
|
|
@@ -8465,11 +8539,11 @@ async function resolveBuyPage(opts) {
|
|
|
8465
8539
|
signal: AbortSignal.timeout(5000)
|
|
8466
8540
|
});
|
|
8467
8541
|
if (!res.ok)
|
|
8468
|
-
return { kind: "error", text:
|
|
8542
|
+
return { kind: "error", text: friendlyStatus(res.status) };
|
|
8469
8543
|
const me = await res.json();
|
|
8470
8544
|
return me.buy_url ? { kind: "url", url: me.buy_url } : { kind: "unavailable" };
|
|
8471
8545
|
} catch (err) {
|
|
8472
|
-
return { kind: "error", text:
|
|
8546
|
+
return { kind: "error", text: friendlyThrown(err) };
|
|
8473
8547
|
}
|
|
8474
8548
|
}
|
|
8475
8549
|
var BUY_NOT_AVAILABLE_TEXT = "Buying credits is not available on this server yet.";
|
|
@@ -8549,7 +8623,6 @@ session.
|
|
|
8549
8623
|
|
|
8550
8624
|
## Commands
|
|
8551
8625
|
- \`/usage\` — spend and remaining budget today
|
|
8552
|
-
- \`/support\` — visit today's advertiser
|
|
8553
8626
|
- \`/feedback <text>\` — send feedback to the free-pi team
|
|
8554
8627
|
- \`/tos\` — open the Terms of Service
|
|
8555
8628
|
- \`/privacy-policy\` — open the Privacy Policy
|
|
@@ -8585,9 +8658,6 @@ in to a server that uses OAuth, and \`free-pi-cli mcp logout <server>\`. Add
|
|
|
8585
8658
|
Inside a session, \`/mcp\` shows each server and its tools, and lets you sign
|
|
8586
8659
|
in, reconnect, or turn a server on or off.
|
|
8587
8660
|
|
|
8588
|
-
## Support
|
|
8589
|
-
Today's sponsor keeps free-pi free. When the user asks how to help the
|
|
8590
|
-
project, or seems to like free-pi, suggest \`/support\` to visit the sponsor.
|
|
8591
8661
|
More at https://freepi.ai
|
|
8592
8662
|
`;
|
|
8593
8663
|
|
|
@@ -8980,6 +9050,142 @@ function createToolAliasesExtension(opts = {}) {
|
|
|
8980
9050
|
};
|
|
8981
9051
|
}
|
|
8982
9052
|
|
|
9053
|
+
// ../../node_modules/.bun/@earendil-works+pi-ai@1.2.0+a410056ef8f430f0/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js
|
|
9054
|
+
var OVERFLOW_PATTERNS = [
|
|
9055
|
+
/prompt (?:is )?too long/i,
|
|
9056
|
+
/prompt exceeds max length/i,
|
|
9057
|
+
/request_too_large/i,
|
|
9058
|
+
/input is too long for requested model/i,
|
|
9059
|
+
/exceeds the context window/i,
|
|
9060
|
+
/exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
|
|
9061
|
+
/input token count.*exceeds the maximum/i,
|
|
9062
|
+
/maximum prompt length is \d+/i,
|
|
9063
|
+
/reduce the length of the messages/i,
|
|
9064
|
+
/maximum context length is \d+ tokens/i,
|
|
9065
|
+
/exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i,
|
|
9066
|
+
/input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i,
|
|
9067
|
+
/exceeds the limit of \d+/i,
|
|
9068
|
+
/exceeds the available context size/i,
|
|
9069
|
+
/greater than the context length/i,
|
|
9070
|
+
/context window exceeds limit/i,
|
|
9071
|
+
/exceeded model token limit/i,
|
|
9072
|
+
/too large for model with \d+ maximum context length/i,
|
|
9073
|
+
/prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i,
|
|
9074
|
+
/model_context_window_exceeded/i,
|
|
9075
|
+
/prompt too long; exceeded (?:max )?context length/i,
|
|
9076
|
+
/range of input length should be/i,
|
|
9077
|
+
/context[_ ]length[_ ]exceeded/i,
|
|
9078
|
+
/too many tokens/i,
|
|
9079
|
+
/token limit exceeded/i
|
|
9080
|
+
];
|
|
9081
|
+
var CEREBRAS_BODYLESS_OVERFLOW_PATTERN = /^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i;
|
|
9082
|
+
var NON_OVERFLOW_PATTERNS = [
|
|
9083
|
+
/^(Throttling error|Service unavailable):/i,
|
|
9084
|
+
/rate limit/i,
|
|
9085
|
+
/too many requests/i
|
|
9086
|
+
];
|
|
9087
|
+
function isContextOverflow(message, contextWindow) {
|
|
9088
|
+
if (message.stopReason === "error" && message.errorMessage) {
|
|
9089
|
+
const isNonOverflow = NON_OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage));
|
|
9090
|
+
if (!isNonOverflow) {
|
|
9091
|
+
if (OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage))) {
|
|
9092
|
+
return true;
|
|
9093
|
+
}
|
|
9094
|
+
if (message.provider === "cerebras" && CEREBRAS_BODYLESS_OVERFLOW_PATTERN.test(message.errorMessage)) {
|
|
9095
|
+
return true;
|
|
9096
|
+
}
|
|
9097
|
+
}
|
|
9098
|
+
}
|
|
9099
|
+
if (contextWindow && message.stopReason === "stop") {
|
|
9100
|
+
const inputTokens = message.usage.input + message.usage.cacheRead;
|
|
9101
|
+
if (inputTokens > contextWindow) {
|
|
9102
|
+
return true;
|
|
9103
|
+
}
|
|
9104
|
+
}
|
|
9105
|
+
if (contextWindow && message.stopReason === "length" && message.usage.output === 0) {
|
|
9106
|
+
const inputTokens = message.usage.input + message.usage.cacheRead;
|
|
9107
|
+
if (inputTokens >= contextWindow * 0.99) {
|
|
9108
|
+
return true;
|
|
9109
|
+
}
|
|
9110
|
+
}
|
|
9111
|
+
return false;
|
|
9112
|
+
}
|
|
9113
|
+
|
|
9114
|
+
// ../../node_modules/.bun/@earendil-works+pi-ai@1.2.0+a410056ef8f430f0/node_modules/@earendil-works/pi-ai/dist/utils/retry.js
|
|
9115
|
+
function buildProviderErrorPattern(patterns) {
|
|
9116
|
+
return new RegExp(patterns.join("|"), "i");
|
|
9117
|
+
}
|
|
9118
|
+
var NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([
|
|
9119
|
+
"GoUsageLimitError",
|
|
9120
|
+
"FreeUsageLimitError",
|
|
9121
|
+
"Monthly usage limit reached",
|
|
9122
|
+
"available balance",
|
|
9123
|
+
"insufficient_quota",
|
|
9124
|
+
"out of budget",
|
|
9125
|
+
"quota exceeded",
|
|
9126
|
+
"billing",
|
|
9127
|
+
"subscription_sharing_usage_limit_exceeded"
|
|
9128
|
+
]);
|
|
9129
|
+
var RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
|
|
9130
|
+
"overloaded",
|
|
9131
|
+
"server_busy",
|
|
9132
|
+
"servers are currently busy",
|
|
9133
|
+
"currently experiencing high demand",
|
|
9134
|
+
"model is at capacity",
|
|
9135
|
+
"rate.?limit",
|
|
9136
|
+
"too many requests",
|
|
9137
|
+
"429",
|
|
9138
|
+
"500",
|
|
9139
|
+
"502",
|
|
9140
|
+
"503",
|
|
9141
|
+
"504",
|
|
9142
|
+
"520",
|
|
9143
|
+
"524",
|
|
9144
|
+
"service.?unavailable",
|
|
9145
|
+
"server.?error",
|
|
9146
|
+
"internal.?error",
|
|
9147
|
+
"provider.?returned.?(?:an.?)?error",
|
|
9148
|
+
"exceeded request buffer limit while retrying upstream",
|
|
9149
|
+
"network.?error",
|
|
9150
|
+
"connection.?error",
|
|
9151
|
+
"connection.?refused",
|
|
9152
|
+
"connection.?lost",
|
|
9153
|
+
"other side closed",
|
|
9154
|
+
"fetch failed",
|
|
9155
|
+
"getaddrinfo",
|
|
9156
|
+
"ENOTFOUND",
|
|
9157
|
+
"EAI_AGAIN",
|
|
9158
|
+
"upstream.?connect",
|
|
9159
|
+
"reset before headers",
|
|
9160
|
+
"socket hang up",
|
|
9161
|
+
"socket connection was closed",
|
|
9162
|
+
"timed? out",
|
|
9163
|
+
"timeout",
|
|
9164
|
+
"terminated",
|
|
9165
|
+
"websocket.?closed",
|
|
9166
|
+
"websocket.?error",
|
|
9167
|
+
"ended without",
|
|
9168
|
+
"stream ended before message_stop",
|
|
9169
|
+
"stream ended before a terminal response event",
|
|
9170
|
+
"http2 request did not get a response",
|
|
9171
|
+
"pending stream has been canceled",
|
|
9172
|
+
"retry delay",
|
|
9173
|
+
"you can retry your request",
|
|
9174
|
+
"try your request again",
|
|
9175
|
+
"please retry your request",
|
|
9176
|
+
"ResourceExhausted",
|
|
9177
|
+
"subscription_sharing_usage_unavailable",
|
|
9178
|
+
"subscription_sharing_user_unavailable"
|
|
9179
|
+
]);
|
|
9180
|
+
function isRetryableAssistantError(message) {
|
|
9181
|
+
if (message.stopReason !== "error" || !message.errorMessage)
|
|
9182
|
+
return false;
|
|
9183
|
+
const errorMessage = message.errorMessage;
|
|
9184
|
+
if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage))
|
|
9185
|
+
return false;
|
|
9186
|
+
return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);
|
|
9187
|
+
}
|
|
9188
|
+
|
|
8983
9189
|
// src/fusion.ts
|
|
8984
9190
|
var FUSION_ID = "fusion";
|
|
8985
9191
|
var FUSION_PICKER_ID = "\uD83E\uDDE8 fusion";
|
|
@@ -9175,7 +9381,11 @@ function createFusionExtension(defaultModelId, ids = resolveFusionIds([])) {
|
|
|
9175
9381
|
|
|
9176
9382
|
// src/error-notice.ts
|
|
9177
9383
|
var UPSTREAM_ERROR_FRAME_MESSAGE = "upstream returned an error frame";
|
|
9384
|
+
var PREMIUM_MODEL_NOTICE = "This model needs premium credits. Get some with /buy-credits, or switch to another model with /model.";
|
|
9178
9385
|
var PROVIDER_FAILING_NOTICE = "This model's provider is failing right now. Try again in a moment, or switch models with /model.";
|
|
9386
|
+
var SIGNIN_EXPIRED_NOTICE = "Your free-pi sign-in has expired. Run /login, or restart free-pi-cli.";
|
|
9387
|
+
var CONNECTION_NOTICE = "Can't reach free-pi right now (connection error). Check your internet connection.";
|
|
9388
|
+
var CONTENT_FILTER_NOTICE = "The model's reply was blocked by a content filter. Try rephrasing your request.";
|
|
9179
9389
|
var FREE_OFFER_TITLE = "Switch to the free model and resend this prompt?";
|
|
9180
9390
|
var FREE_OFFER_ACCEPT = "Switch and resend";
|
|
9181
9391
|
var FREE_OFFER_DECLINE = "Stay on this model";
|
|
@@ -9235,6 +9445,8 @@ function parseErrorEnvelope(errorMessage) {
|
|
|
9235
9445
|
}
|
|
9236
9446
|
}
|
|
9237
9447
|
function formatLimitEnvelope(env, opts) {
|
|
9448
|
+
if (env.code === "model_requires_credits" && !/premium credit/i.test(env.message))
|
|
9449
|
+
return PREMIUM_MODEL_NOTICE;
|
|
9238
9450
|
if (env.code === "concurrent_session" || SERVER_MESSAGE_CODES.has(env.code) || env.prompt === "github_signin" || env.copy_variant === "treatment" || typeof env.remaining_credits_today !== "number") {
|
|
9239
9451
|
return env.message;
|
|
9240
9452
|
}
|
|
@@ -9280,6 +9492,36 @@ function lastUserEntryId(ctx) {
|
|
|
9280
9492
|
}
|
|
9281
9493
|
return "";
|
|
9282
9494
|
}
|
|
9495
|
+
var RAW_ERROR_FIELD = "freePiRawError";
|
|
9496
|
+
function rawErrorMessage(msg) {
|
|
9497
|
+
if (!msg)
|
|
9498
|
+
return;
|
|
9499
|
+
const m = msg;
|
|
9500
|
+
return typeof m[RAW_ERROR_FIELD] === "string" ? m[RAW_ERROR_FIELD] : m.errorMessage;
|
|
9501
|
+
}
|
|
9502
|
+
function envelopeOf(raw) {
|
|
9503
|
+
return raw === UPSTREAM_ERROR_FRAME_MESSAGE ? { status: 502, code: "upstream_error", message: UPSTREAM_ERROR_FRAME_MESSAGE } : parseErrorEnvelope(raw);
|
|
9504
|
+
}
|
|
9505
|
+
var friendlyText = (env, timeZone) => env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : env.code === "unauthorized" ? SIGNIN_EXPIRED_NOTICE : formatLimitEnvelope(env, { timeZone });
|
|
9506
|
+
var isFreePiProvider = (provider) => provider === PROVIDER_NAME || provider === DEFENSE_PROVIDER_NAME;
|
|
9507
|
+
function friendlyRawText(raw) {
|
|
9508
|
+
const text = raw.trim();
|
|
9509
|
+
if (/^(?:connection error\.?|request timed out\.?|terminated|fetch failed)$/i.test(text))
|
|
9510
|
+
return CONNECTION_NOTICE;
|
|
9511
|
+
const http = /^(5\d\d)(?: status code \(no body\)$|\s*<(?:!doctype html|html)\b)/i.exec(text);
|
|
9512
|
+
if (http)
|
|
9513
|
+
return `${PROVIDER_FAILING_NOTICE} (HTTP ${http[1]})`;
|
|
9514
|
+
const finish = /^Provider finish_reason: (\w+)$/.exec(text);
|
|
9515
|
+
if (finish)
|
|
9516
|
+
return finish[1] === "content_filter" ? CONTENT_FILTER_NOTICE : `${PROVIDER_FAILING_NOTICE} (${finish[1].replace(/_/g, " ")})`;
|
|
9517
|
+
return;
|
|
9518
|
+
}
|
|
9519
|
+
function friendlyError(raw, timeZone) {
|
|
9520
|
+
if (typeof raw !== "string")
|
|
9521
|
+
return;
|
|
9522
|
+
const env = envelopeOf(raw);
|
|
9523
|
+
return env ? friendlyText(env, timeZone) : friendlyRawText(raw);
|
|
9524
|
+
}
|
|
9283
9525
|
function createErrorNoticeExtension(opts = {}) {
|
|
9284
9526
|
const lastAt = new Map;
|
|
9285
9527
|
const now = opts.now ?? Date.now;
|
|
@@ -9312,21 +9554,44 @@ function createErrorNoticeExtension(opts = {}) {
|
|
|
9312
9554
|
return {
|
|
9313
9555
|
name: "free-pi-error-notice",
|
|
9314
9556
|
factory(pi) {
|
|
9557
|
+
pi.on("message_end", (event) => {
|
|
9558
|
+
const msg = event.message;
|
|
9559
|
+
if (msg.role !== "assistant" || !isFreePiProvider(msg.provider) || msg.stopReason !== "error" || typeof msg.errorMessage !== "string")
|
|
9560
|
+
return;
|
|
9561
|
+
const env = envelopeOf(msg.errorMessage);
|
|
9562
|
+
const friendly = env ? friendlyText(env, timeZone) : friendlyRawText(msg.errorMessage);
|
|
9563
|
+
if (friendly === undefined)
|
|
9564
|
+
return;
|
|
9565
|
+
const same = (text) => {
|
|
9566
|
+
const next = { ...msg, errorMessage: text };
|
|
9567
|
+
return isRetryableAssistantError(next) === retryable && isContextOverflow(next, 0) === overflow;
|
|
9568
|
+
};
|
|
9569
|
+
const retryable = isRetryableAssistantError(msg);
|
|
9570
|
+
const overflow = isContextOverflow(msg, 0);
|
|
9571
|
+
let text = friendly;
|
|
9572
|
+
if (env && retryable && !isRetryableAssistantError({ ...msg, errorMessage: text }))
|
|
9573
|
+
text += ` (HTTP ${env.status})`;
|
|
9574
|
+
if (!same(text))
|
|
9575
|
+
return;
|
|
9576
|
+
return { message: { ...msg, errorMessage: text, [RAW_ERROR_FIELD]: msg.errorMessage } };
|
|
9577
|
+
});
|
|
9315
9578
|
pi.on("turn_end", async (event, ctx) => {
|
|
9316
9579
|
lastTurnEntryId = event.messageEntryId;
|
|
9317
9580
|
const msg = event.message;
|
|
9318
9581
|
if (!msg || msg.stopReason !== "error")
|
|
9319
9582
|
return;
|
|
9320
|
-
const env =
|
|
9583
|
+
const env = envelopeOf(rawErrorMessage(msg));
|
|
9321
9584
|
if (!env)
|
|
9322
9585
|
return;
|
|
9323
9586
|
const t = now();
|
|
9324
9587
|
if (t - (lastAt.get(env.code) ?? -Infinity) >= NOTICE_DEDUPE_MS) {
|
|
9325
9588
|
lastAt.set(env.code, t);
|
|
9326
9589
|
const extra = opts.extraNotice?.(env);
|
|
9327
|
-
const
|
|
9590
|
+
const shown = msg[RAW_ERROR_FIELD] !== undefined;
|
|
9591
|
+
const text = shown ? extra : friendlyText(env, timeZone) + (extra ? `
|
|
9328
9592
|
${extra}` : "");
|
|
9329
|
-
|
|
9593
|
+
if (text)
|
|
9594
|
+
ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
|
|
9330
9595
|
}
|
|
9331
9596
|
await offerFreeModel(pi, env, event.messageEntryId, ctx);
|
|
9332
9597
|
});
|
|
@@ -9464,9 +9729,16 @@ function createUpgradeNoticeExtension(opts) {
|
|
|
9464
9729
|
|
|
9465
9730
|
// src/tool-outcomes.ts
|
|
9466
9731
|
var MAX_IDS = 50;
|
|
9732
|
+
var TOOL_META_HEADER = "x-freepi-tool-meta";
|
|
9733
|
+
var COMPACTED_HEADER = "x-freepi-compacted";
|
|
9734
|
+
var META_BUDGET = 1500;
|
|
9467
9735
|
function createToolOutcomesExtension() {
|
|
9468
9736
|
const failed = [];
|
|
9469
9737
|
let evicted = false;
|
|
9738
|
+
const meta = [];
|
|
9739
|
+
let metaBytes = 3;
|
|
9740
|
+
let droppedMessages = 0;
|
|
9741
|
+
let pendingDropped = 0;
|
|
9470
9742
|
return {
|
|
9471
9743
|
name: "free-pi-tool-outcomes",
|
|
9472
9744
|
factory(pi) {
|
|
@@ -9480,7 +9752,37 @@ function createToolOutcomesExtension() {
|
|
|
9480
9752
|
evicted = true;
|
|
9481
9753
|
}
|
|
9482
9754
|
});
|
|
9755
|
+
pi.on("tool_execution_end", (event) => {
|
|
9756
|
+
const e = event;
|
|
9757
|
+
if (!e.toolCallId || !/^[\w.:-]{1,100}$/.test(e.toolCallId) || typeof e.durationMs !== "number")
|
|
9758
|
+
return;
|
|
9759
|
+
let exit = "-";
|
|
9760
|
+
if (e.toolName === "bash" || e.toolName === "powershell") {
|
|
9761
|
+
const m = e.isError ? /Command exited with code (\d{1,3})\s*$/.exec(e.result?.content?.map((b) => b.text ?? "").join("") ?? "") : null;
|
|
9762
|
+
exit = m ? m[1] : e.isError ? "-" : "0";
|
|
9763
|
+
}
|
|
9764
|
+
const entry = `${e.toolCallId}~${Math.max(0, Math.round(e.durationMs))}~${exit}~${e.result?.details?.truncation?.truncated ? 1 : 0}`;
|
|
9765
|
+
if (entry.length > 150)
|
|
9766
|
+
return;
|
|
9767
|
+
meta.push(entry);
|
|
9768
|
+
metaBytes += entry.length + 1;
|
|
9769
|
+
while (metaBytes > META_BUDGET && meta.length > 1)
|
|
9770
|
+
metaBytes -= meta.shift().length + 1;
|
|
9771
|
+
});
|
|
9772
|
+
pi.on("session_before_compact", (event) => {
|
|
9773
|
+
const p = event.preparation;
|
|
9774
|
+
pendingDropped = (p?.messagesToSummarize?.length ?? 0) + (p?.turnPrefixMessages?.length ?? 0);
|
|
9775
|
+
});
|
|
9776
|
+
pi.on("session_compact", () => {
|
|
9777
|
+
droppedMessages = pendingDropped;
|
|
9778
|
+
});
|
|
9483
9779
|
pi.on("before_provider_headers", (event) => {
|
|
9780
|
+
if (meta.length)
|
|
9781
|
+
event.headers[TOOL_META_HEADER] = `v1:${meta.join(",")}`;
|
|
9782
|
+
if (droppedMessages) {
|
|
9783
|
+
event.headers[COMPACTED_HEADER] = `v1:${droppedMessages}`;
|
|
9784
|
+
droppedMessages = 0;
|
|
9785
|
+
}
|
|
9484
9786
|
event.headers["x-freepi-tool-errors"] = `${evicted ? "v1x" : "v1"}:${failed.join(",")}`;
|
|
9485
9787
|
});
|
|
9486
9788
|
}
|
|
@@ -9820,6 +10122,20 @@ function installBottomPin(mode) {
|
|
|
9820
10122
|
return [...lines, ...Array(bottomPadding(lines.length, other, rows)).fill("")];
|
|
9821
10123
|
};
|
|
9822
10124
|
}
|
|
10125
|
+
var IMAGE_MARKS = ["\x1B_G", "\x1B]1337;File="];
|
|
10126
|
+
function clampLines(lines, width) {
|
|
10127
|
+
return lines.map((l) => IMAGE_MARKS.some((m) => l.includes(m)) || visibleWidth3(l) <= width ? l : truncateToWidth6(l, Math.max(0, width), ""));
|
|
10128
|
+
}
|
|
10129
|
+
function installWidthGuard(mode) {
|
|
10130
|
+
const m = mode;
|
|
10131
|
+
for (const k of ["headerContainer", "loadedResourcesContainer", "chatContainer", "pendingMessagesContainer", "statusContainer", "widgetContainerAbove", "editorContainer", "widgetContainerBelow", "footerContainer"]) {
|
|
10132
|
+
const c = m[k];
|
|
10133
|
+
if (!c)
|
|
10134
|
+
continue;
|
|
10135
|
+
const render = c.render.bind(c);
|
|
10136
|
+
c.render = (w) => clampLines(render(w), w);
|
|
10137
|
+
}
|
|
10138
|
+
}
|
|
9823
10139
|
|
|
9824
10140
|
// src/guest-handoff.ts
|
|
9825
10141
|
var LOGIN_VALUE_LINE = "sign in free with GitHub: full daily allowance, FREE abliterated DeepSeek, history saved";
|
|
@@ -9909,6 +10225,14 @@ function mergeAttribution(a, b) {
|
|
|
9909
10225
|
|
|
9910
10226
|
// src/changelog.ts
|
|
9911
10227
|
var CHANGELOG_HIGHLIGHTS = [
|
|
10228
|
+
{
|
|
10229
|
+
version: "0.6.21",
|
|
10230
|
+
highlights: ["The /support command is removed."]
|
|
10231
|
+
},
|
|
10232
|
+
{
|
|
10233
|
+
version: "0.6.20",
|
|
10234
|
+
highlights: ["Error messages are now plain sentences instead of raw server text."]
|
|
10235
|
+
},
|
|
9912
10236
|
{
|
|
9913
10237
|
version: "0.6.19",
|
|
9914
10238
|
highlights: ["The ad banner now refreshes when you send a message, not on a timer."]
|
|
@@ -10273,6 +10597,7 @@ function highlightsFor(version) {
|
|
|
10273
10597
|
|
|
10274
10598
|
// src/commands.ts
|
|
10275
10599
|
import { access } from "node:fs/promises";
|
|
10600
|
+
import { basename as basename3 } from "node:path";
|
|
10276
10601
|
|
|
10277
10602
|
// src/auth-flow.ts
|
|
10278
10603
|
class UnauthorizedError extends Error {
|
|
@@ -10287,13 +10612,23 @@ async function postJson(baseUrl, path, body, extraHeaders = {}) {
|
|
|
10287
10612
|
body: JSON.stringify(body)
|
|
10288
10613
|
});
|
|
10289
10614
|
}
|
|
10615
|
+
var NETWORK_TEXT = "Can't reach free-pi. Check your internet connection and try again.";
|
|
10616
|
+
var RATE_LIMIT_TEXT = "Too many sign-in attempts from your network. Wait a minute and try again.";
|
|
10617
|
+
var UNEXPECTED_REPLY_TEXT = "free-pi sent an unexpected reply. If you're on public wifi, sign in to the network first, then try again.";
|
|
10618
|
+
async function parseReply(res, schema) {
|
|
10619
|
+
try {
|
|
10620
|
+
return schema.parse(await res.json());
|
|
10621
|
+
} catch (err) {
|
|
10622
|
+
throw new Error(UNEXPECTED_REPLY_TEXT, { cause: err });
|
|
10623
|
+
}
|
|
10624
|
+
}
|
|
10290
10625
|
async function postJsonRetry(baseUrl, path, body, attempts = 4, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), extraHeaders = {}) {
|
|
10291
10626
|
for (let attempt = 1;; attempt++) {
|
|
10292
10627
|
try {
|
|
10293
10628
|
return await postJson(baseUrl, path, body, extraHeaders);
|
|
10294
10629
|
} catch (err) {
|
|
10295
10630
|
if (attempt >= attempts) {
|
|
10296
|
-
throw new Error(
|
|
10631
|
+
throw new Error(NETWORK_TEXT, { cause: err });
|
|
10297
10632
|
}
|
|
10298
10633
|
await sleep(1500 * attempt);
|
|
10299
10634
|
}
|
|
@@ -10310,6 +10645,16 @@ function refusalMessage(status, body) {
|
|
|
10310
10645
|
}
|
|
10311
10646
|
return;
|
|
10312
10647
|
}
|
|
10648
|
+
function failureText(status, body) {
|
|
10649
|
+
if (status === 429 || body?.code === "rate_limited" && status < 500)
|
|
10650
|
+
return RATE_LIMIT_TEXT;
|
|
10651
|
+
const refusal = refusalMessage(status, body);
|
|
10652
|
+
if (refusal)
|
|
10653
|
+
return refusal;
|
|
10654
|
+
if (status >= 500)
|
|
10655
|
+
return `free-pi is having trouble right now (HTTP ${status}). Try again in a minute.`;
|
|
10656
|
+
return `free-pi could not complete sign-in (HTTP ${status}). Try again in a minute.`;
|
|
10657
|
+
}
|
|
10313
10658
|
async function readRefusal(res) {
|
|
10314
10659
|
return await res.json().catch(() => {
|
|
10315
10660
|
return;
|
|
@@ -10318,9 +10663,9 @@ async function readRefusal(res) {
|
|
|
10318
10663
|
async function startDeviceFlow(baseUrl) {
|
|
10319
10664
|
const res = await postJsonRetry(baseUrl, "/auth/github/device", {});
|
|
10320
10665
|
if (!res.ok) {
|
|
10321
|
-
throw new Error(
|
|
10666
|
+
throw new Error(failureText(res.status, await readRefusal(res)));
|
|
10322
10667
|
}
|
|
10323
|
-
return
|
|
10668
|
+
return parseReply(res, GithubDeviceResponseSchema);
|
|
10324
10669
|
}
|
|
10325
10670
|
async function startConnect(baseUrl, consentVersion, attribution = {}, client = {}) {
|
|
10326
10671
|
const res = await postJsonRetry(baseUrl, "/auth/connect/start", {
|
|
@@ -10330,8 +10675,8 @@ async function startConnect(baseUrl, consentVersion, attribution = {}, client =
|
|
|
10330
10675
|
...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
|
|
10331
10676
|
}, undefined, undefined, { [SIGNIN_EXP_HEADER]: "1" });
|
|
10332
10677
|
if (!res.ok)
|
|
10333
|
-
throw new Error(
|
|
10334
|
-
return
|
|
10678
|
+
throw new Error(failureText(res.status, await readRefusal(res)));
|
|
10679
|
+
return parseReply(res, ConnectStartResponseSchema);
|
|
10335
10680
|
}
|
|
10336
10681
|
async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {}) {
|
|
10337
10682
|
const res = await postJsonRetry(baseUrl, "/auth/consent", {
|
|
@@ -10341,7 +10686,7 @@ async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {
|
|
|
10341
10686
|
...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
|
|
10342
10687
|
});
|
|
10343
10688
|
if (!res.ok) {
|
|
10344
|
-
throw new Error(
|
|
10689
|
+
throw new Error(failureText(res.status, await readRefusal(res)));
|
|
10345
10690
|
}
|
|
10346
10691
|
}
|
|
10347
10692
|
function deviceReport() {
|
|
@@ -10374,8 +10719,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
|
|
|
10374
10719
|
throw new GuestUnavailableError(body?.reason ?? "disabled");
|
|
10375
10720
|
}
|
|
10376
10721
|
if (!res.ok)
|
|
10377
|
-
throw new Error(
|
|
10378
|
-
const parsed =
|
|
10722
|
+
throw new Error(failureText(res.status, await readRefusal(res)));
|
|
10723
|
+
const parsed = await parseReply(res, GuestStartResponseSchema);
|
|
10379
10724
|
return {
|
|
10380
10725
|
token: parsed.token,
|
|
10381
10726
|
allowance_usd: parsed.allowance_usd,
|
|
@@ -10455,7 +10800,7 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
|
|
|
10455
10800
|
throw new PollAbortedError;
|
|
10456
10801
|
consecutiveNetworkFailures++;
|
|
10457
10802
|
if (consecutiveNetworkFailures >= 6) {
|
|
10458
|
-
throw new Error(
|
|
10803
|
+
throw new Error(NETWORK_TEXT, { cause: err });
|
|
10459
10804
|
}
|
|
10460
10805
|
await sleep(Math.max(waitSeconds, 5) * 1000);
|
|
10461
10806
|
continue;
|
|
@@ -10473,9 +10818,9 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
|
|
|
10473
10818
|
throw new LoginTimeoutError;
|
|
10474
10819
|
if (isExpiredTokenPoll(body))
|
|
10475
10820
|
throw new DeviceCodeExpiredError;
|
|
10476
|
-
throw new Error(
|
|
10821
|
+
throw new Error(failureText(res.status, body));
|
|
10477
10822
|
}
|
|
10478
|
-
const parsed =
|
|
10823
|
+
const parsed = await parseReply(res, AuthTokenResponseSchema);
|
|
10479
10824
|
if (opts.signal?.aborted)
|
|
10480
10825
|
throw new PollAbortedError;
|
|
10481
10826
|
if ("token" in parsed)
|
|
@@ -11377,15 +11722,6 @@ async function buyCredits(opts, ctx) {
|
|
|
11377
11722
|
(opts.openBrowserImpl ?? openBrowser)(page.url);
|
|
11378
11723
|
ctx.ui.notify(buyPageText(page.url), "info");
|
|
11379
11724
|
}
|
|
11380
|
-
async function openAdvertiser(opts, ctx) {
|
|
11381
|
-
const url = opts.getAdvertiserUrl?.();
|
|
11382
|
-
if (!url) {
|
|
11383
|
-
ctx.ui.notify("No advertiser loaded yet.", "info");
|
|
11384
|
-
return;
|
|
11385
|
-
}
|
|
11386
|
-
(opts.openBrowserImpl ?? openBrowser)(url);
|
|
11387
|
-
ctx.ui.notify("Opening today's advertiser in your browser. Thank you for supporting free-pi.", "info");
|
|
11388
|
-
}
|
|
11389
11725
|
var TERMS_URL = "https://freepi.ai/terms";
|
|
11390
11726
|
var PRIVACY_URL = "https://freepi.ai/privacy";
|
|
11391
11727
|
async function openTos(opts, ctx) {
|
|
@@ -11449,7 +11785,7 @@ async function runLoginUnguarded(opts, ctx, lead) {
|
|
|
11449
11785
|
ctx.ui.notify(LOGIN_DENIED_LINE, "info");
|
|
11450
11786
|
return false;
|
|
11451
11787
|
}
|
|
11452
|
-
const reason = err instanceof Error && err.message ?
|
|
11788
|
+
const reason = err instanceof Error && err.message ? `: ${err.message.replace(/\.$/, "")}` : "";
|
|
11453
11789
|
ctx.ui.notify(`Couldn't finish GitHub sign-in${reason}. Run /login to try again.`, "error");
|
|
11454
11790
|
return false;
|
|
11455
11791
|
}
|
|
@@ -11467,6 +11803,26 @@ function installLoginHandler(mode, opts) {
|
|
|
11467
11803
|
return runLogin(opts, { ui: { notify } });
|
|
11468
11804
|
};
|
|
11469
11805
|
}
|
|
11806
|
+
var IMPORT_NEXT_STEP = "Run /import again, or start a fresh session.";
|
|
11807
|
+
var FS_REASONS = {
|
|
11808
|
+
EACCES: "permission denied",
|
|
11809
|
+
EPERM: "permission denied",
|
|
11810
|
+
ENOENT: "not found",
|
|
11811
|
+
EISDIR: "that is a folder, not a file",
|
|
11812
|
+
EMFILE: "too many files are open",
|
|
11813
|
+
ENOSPC: "the disk is full"
|
|
11814
|
+
};
|
|
11815
|
+
function friendlyImportError(err, action = "import") {
|
|
11816
|
+
const e = err;
|
|
11817
|
+
const code = typeof e?.code === "string" && /^E[A-Z0-9]+$/.test(e.code) ? e.code : undefined;
|
|
11818
|
+
if (code) {
|
|
11819
|
+
const file = typeof e.path === "string" && e.path ? basename3(e.path) : undefined;
|
|
11820
|
+
const reason = FS_REASONS[code] ?? `error ${code}`;
|
|
11821
|
+
return `Couldn't ${action}${file ? ` ${file}` : ""}: ${reason}. ${IMPORT_NEXT_STEP}`;
|
|
11822
|
+
}
|
|
11823
|
+
const msg = (err instanceof Error ? err.message : String(err)).trim().replace(/[.\s]+$/, "");
|
|
11824
|
+
return `Couldn't ${action}: ${msg || "unknown error"}. ${IMPORT_NEXT_STEP}`;
|
|
11825
|
+
}
|
|
11470
11826
|
function installImportHandler(mode, opts) {
|
|
11471
11827
|
const m = mode;
|
|
11472
11828
|
mode.handleImportCommand = async (text) => {
|
|
@@ -11489,11 +11845,11 @@ function installImportHandler(mode, opts) {
|
|
|
11489
11845
|
for (let waited = 0;m.session.isCompacting && waited < 60000; waited += 100)
|
|
11490
11846
|
await new Promise((r) => setTimeout(r, 100));
|
|
11491
11847
|
const options = m.session.isStreaming ? { streamingBehavior: "followUp" } : undefined;
|
|
11492
|
-
m.session.prompt(message, options).catch((err) => m.showError(err
|
|
11848
|
+
m.session.prompt(message, options).catch((err) => m.showError(friendlyImportError(err, "send the import")));
|
|
11493
11849
|
}
|
|
11494
11850
|
});
|
|
11495
11851
|
} catch (err) {
|
|
11496
|
-
m.showError(
|
|
11852
|
+
m.showError(friendlyImportError(err));
|
|
11497
11853
|
}
|
|
11498
11854
|
};
|
|
11499
11855
|
}
|
|
@@ -11641,7 +11997,7 @@ function createGuestLoginExtension(opts) {
|
|
|
11641
11997
|
if (!opts.isGuest?.())
|
|
11642
11998
|
return;
|
|
11643
11999
|
const msg = event.message;
|
|
11644
|
-
const envelope = msg?.stopReason === "error" ? parseErrorEnvelope(msg
|
|
12000
|
+
const envelope = msg?.stopReason === "error" ? parseErrorEnvelope(rawErrorMessage(msg)) : undefined;
|
|
11645
12001
|
if (envelope && (envelope.code === "guest_allowance" || envelope.code === "guest_unavailable" || envelope.prompt === "github_signin")) {
|
|
11646
12002
|
if (!inFlight && !signedIn) {
|
|
11647
12003
|
const busy = envelope.code === "guest_allowance" && (envelope.remaining_credits_today ?? 0) > 0;
|
|
@@ -11711,10 +12067,6 @@ function createFreePiCommandsExtension(opts) {
|
|
|
11711
12067
|
description: FEEDBACK_DESCRIPTION,
|
|
11712
12068
|
handler: (args, ctx) => sendFeedback(opts, args, ctx)
|
|
11713
12069
|
});
|
|
11714
|
-
pi.registerCommand("support", {
|
|
11715
|
-
description: "Open today's advertiser in your browser to support free-pi.",
|
|
11716
|
-
handler: (_args, ctx) => openAdvertiser(opts, ctx)
|
|
11717
|
-
});
|
|
11718
12070
|
pi.registerCommand("invite", {
|
|
11719
12071
|
description: "Get your invite code and link: you and a friend both get +5k credits/day.",
|
|
11720
12072
|
handler: (_args, ctx) => showInvite(opts, ctx)
|
|
@@ -11751,7 +12103,7 @@ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync a
|
|
|
11751
12103
|
import { homedir as homedir4 } from "node:os";
|
|
11752
12104
|
import { dirname as dirname8 } from "node:path";
|
|
11753
12105
|
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
11754
|
-
import { Box, Text as Text2, truncateToWidth as truncateToWidth7 } from "@earendil-works/pi-tui";
|
|
12106
|
+
import { Box, Text as Text2, truncateToWidth as truncateToWidth7, visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
|
|
11755
12107
|
var TOOL_MODES = ["compact", "full", "hidden"];
|
|
11756
12108
|
var TOOLS_USAGE = "Usage: /tools compact|full|hidden (compact: one line per tool call; full: every call in full; hidden: just a count)";
|
|
11757
12109
|
function readToolMode(path) {
|
|
@@ -11851,12 +12203,30 @@ function toolLines(i) {
|
|
|
11851
12203
|
}
|
|
11852
12204
|
return done;
|
|
11853
12205
|
}
|
|
12206
|
+
var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
12207
|
+
function wrapByWidth(text, room) {
|
|
12208
|
+
const clean = text.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)|\x1b\[[0-9;?]*[ -\/]*[@-~]/g, "").replace(/\t/g, " ").replace(/[\x00-\x08\x0a-\x1f\x7f-\x9f]/g, "");
|
|
12209
|
+
const pieces = [];
|
|
12210
|
+
let cur = "";
|
|
12211
|
+
let curW = 0;
|
|
12212
|
+
for (const { segment } of graphemes.segment(clean)) {
|
|
12213
|
+
const w = visibleWidth4(segment);
|
|
12214
|
+
if (curW + w > room && cur) {
|
|
12215
|
+
pieces.push(cur);
|
|
12216
|
+
cur = "";
|
|
12217
|
+
curW = 0;
|
|
12218
|
+
}
|
|
12219
|
+
cur += segment;
|
|
12220
|
+
curW += w;
|
|
12221
|
+
}
|
|
12222
|
+
return [...pieces, cur];
|
|
12223
|
+
}
|
|
11854
12224
|
function component(rows, theme, pad) {
|
|
11855
12225
|
return {
|
|
11856
12226
|
render: (w) => rows.flatMap((r) => {
|
|
11857
12227
|
const room = Math.max(1, w - pad);
|
|
11858
|
-
const pieces = r.raw ?
|
|
11859
|
-
return pieces.map((t) => " ".repeat(pad) + theme.fg(r.error ? "error" : "dim", t));
|
|
12228
|
+
const pieces = r.raw ? wrapByWidth(r.text, room) : [truncateToWidth7(r.text, room, "…")];
|
|
12229
|
+
return pieces.map((t) => truncateToWidth7(" ".repeat(pad) + theme.fg(r.error ? "error" : "dim", t), Math.max(0, w), ""));
|
|
11860
12230
|
}),
|
|
11861
12231
|
invalidate() {}
|
|
11862
12232
|
};
|
|
@@ -11959,7 +12329,6 @@ var ALL_COMMANDS = [
|
|
|
11959
12329
|
{ name: "/usage", description: "credits left today and refill time" },
|
|
11960
12330
|
{ name: "/import", description: "continue a Claude Code or Codex session" },
|
|
11961
12331
|
{ name: "/hive", description: "share this session with friends" },
|
|
11962
|
-
{ name: "/support", description: "visit today's advertiser" },
|
|
11963
12332
|
{ name: "/tos", description: "terms of service" },
|
|
11964
12333
|
{ name: "/privacy-policy", description: "privacy policy" },
|
|
11965
12334
|
{ name: "/buy-credits", description: "get more usage" },
|
|
@@ -12128,16 +12497,63 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
|
|
|
12128
12497
|
return scoped;
|
|
12129
12498
|
}
|
|
12130
12499
|
|
|
12500
|
+
// src/default-model.ts
|
|
12501
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
12502
|
+
import { dirname as dirname9, join as join8 } from "node:path";
|
|
12503
|
+
var getDefaultModelPath = (agentDir) => join8(agentDir, "default-model.json");
|
|
12504
|
+
function readDefaultModel(agentDir) {
|
|
12505
|
+
if (!agentDir)
|
|
12506
|
+
return;
|
|
12507
|
+
try {
|
|
12508
|
+
const v = JSON.parse(readFileSync5(getDefaultModelPath(agentDir), "utf8"));
|
|
12509
|
+
return typeof v.provider === "string" && typeof v.model === "string" && v.provider && v.model ? { provider: v.provider, model: v.model } : undefined;
|
|
12510
|
+
} catch {
|
|
12511
|
+
return;
|
|
12512
|
+
}
|
|
12513
|
+
}
|
|
12514
|
+
|
|
12515
|
+
class DefaultModelStorage {
|
|
12516
|
+
agentDir;
|
|
12517
|
+
global;
|
|
12518
|
+
project;
|
|
12519
|
+
constructor(agentDir) {
|
|
12520
|
+
this.agentDir = agentDir;
|
|
12521
|
+
}
|
|
12522
|
+
withLock(scope, fn) {
|
|
12523
|
+
const next = fn(scope === "global" ? this.global : this.project);
|
|
12524
|
+
if (next === undefined)
|
|
12525
|
+
return;
|
|
12526
|
+
if (scope === "project") {
|
|
12527
|
+
this.project = next;
|
|
12528
|
+
return;
|
|
12529
|
+
}
|
|
12530
|
+
const before = this.global;
|
|
12531
|
+
this.global = next;
|
|
12532
|
+
if (before === undefined || !this.agentDir)
|
|
12533
|
+
return;
|
|
12534
|
+
try {
|
|
12535
|
+
const a = JSON.parse(before);
|
|
12536
|
+
const b = JSON.parse(next);
|
|
12537
|
+
if (a.defaultProvider === b.defaultProvider && a.defaultModel === b.defaultModel)
|
|
12538
|
+
return;
|
|
12539
|
+
if (!b.defaultProvider || !b.defaultModel)
|
|
12540
|
+
return;
|
|
12541
|
+
mkdirSync3(dirname9(getDefaultModelPath(this.agentDir)), { recursive: true });
|
|
12542
|
+
writeFileSync3(getDefaultModelPath(this.agentDir), JSON.stringify({ provider: b.defaultProvider, model: b.defaultModel }));
|
|
12543
|
+
} catch {}
|
|
12544
|
+
}
|
|
12545
|
+
}
|
|
12546
|
+
|
|
12131
12547
|
// src/hive.ts
|
|
12132
12548
|
import { createHash as createHash3 } from "node:crypto";
|
|
12133
12549
|
import { execFileSync } from "node:child_process";
|
|
12134
|
-
import { lstatSync as lstatSync2, readFileSync as
|
|
12135
|
-
import { join as
|
|
12550
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync6, readlinkSync as readlinkSync2 } from "node:fs";
|
|
12551
|
+
import { join as join10 } from "node:path";
|
|
12136
12552
|
|
|
12137
12553
|
// src/hive-guard.ts
|
|
12138
12554
|
import { existsSync as existsSync4, lstatSync, readdirSync, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
|
|
12139
12555
|
import { homedir as homedir5 } from "node:os";
|
|
12140
|
-
import { basename as
|
|
12556
|
+
import { basename as basename4, dirname as dirname10, isAbsolute as isAbsolute2, join as join9, relative as relative2, resolve as resolve2, sep as sep3 } from "node:path";
|
|
12141
12557
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
12142
12558
|
|
|
12143
12559
|
// src/subagents.ts
|
|
@@ -12254,8 +12670,10 @@ function lastAssistant(session) {
|
|
|
12254
12670
|
continue;
|
|
12255
12671
|
const text = Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(`
|
|
12256
12672
|
`).trim() : "";
|
|
12257
|
-
if (m.stopReason === "error")
|
|
12258
|
-
|
|
12673
|
+
if (m.stopReason === "error") {
|
|
12674
|
+
const raw = rawErrorMessage(m);
|
|
12675
|
+
return { text, error: (raw && isFreePiProvider(m.provider) && !raw.includes("subagents_unavailable") ? friendlyError(raw) : undefined) ?? (raw || "the model request failed") };
|
|
12676
|
+
}
|
|
12259
12677
|
if (m.stopReason === "aborted")
|
|
12260
12678
|
return { text, error: "aborted" };
|
|
12261
12679
|
return { text };
|
|
@@ -12646,7 +13064,7 @@ var SAFE_READ_TOOLS = ["read", "grep", "find", "ls"];
|
|
|
12646
13064
|
var ALWAYS_ASK = ["codemode", "tool_search", FUSION_PLAN_TOOL, WEB_SEARCH_TOOL_NAME, DOCS_LOOKUP_TOOL_NAME];
|
|
12647
13065
|
var CLASSIFIED = ["bash", "write", "edit"];
|
|
12648
13066
|
var SECRET_BASENAME = [/^\.env(\..*)?$/, /\.pem$/, /^id_(rsa|dsa|ecdsa|ed25519)/];
|
|
12649
|
-
var SECRET_HOME_DIRS = [".ssh", ".aws",
|
|
13067
|
+
var SECRET_HOME_DIRS = [".ssh", ".aws", join9(".config", "freepi"), ".free-pi"];
|
|
12650
13068
|
var SECRET_IN_TEXT = /(?:^|[\s"'=:/])\.env(?:\.[\w.-]+)?(?=$|[\s"';|&)])|\.pem\b|\bid_(?:rsa|dsa|ecdsa|ed25519)|\.ssh\b|\.aws\b|\.config\/freepi|\.free-pi\b/;
|
|
12651
13069
|
var BASH_DENY = [
|
|
12652
13070
|
[/\brm\s+(?:-\S+\s+)*(?:\/|~|\$HOME)\*?(?:\s|$)/, "destructive command"],
|
|
@@ -12669,7 +13087,7 @@ function protectedLinkTargets(root, limit) {
|
|
|
12669
13087
|
const note = (link) => {
|
|
12670
13088
|
let target;
|
|
12671
13089
|
try {
|
|
12672
|
-
target = resolve2(
|
|
13090
|
+
target = resolve2(dirname10(link), readlinkSync(link));
|
|
12673
13091
|
} catch {
|
|
12674
13092
|
return;
|
|
12675
13093
|
}
|
|
@@ -12679,7 +13097,7 @@ function protectedLinkTargets(root, limit) {
|
|
|
12679
13097
|
out.push(real);
|
|
12680
13098
|
};
|
|
12681
13099
|
for (const entry of [".git/hooks", ".git/config"]) {
|
|
12682
|
-
const link =
|
|
13100
|
+
const link = join9(root, entry);
|
|
12683
13101
|
try {
|
|
12684
13102
|
if (lstatSync(link).isSymbolicLink())
|
|
12685
13103
|
note(link);
|
|
@@ -12698,7 +13116,7 @@ function protectedLinkTargets(root, limit) {
|
|
|
12698
13116
|
for (const entry of entries) {
|
|
12699
13117
|
if (++seen > limit)
|
|
12700
13118
|
return null;
|
|
12701
|
-
const path =
|
|
13119
|
+
const path = join9(dir, entry.name);
|
|
12702
13120
|
if (entry.isSymbolicLink()) {
|
|
12703
13121
|
if (protectedByName(relative2(root, path)))
|
|
12704
13122
|
note(path);
|
|
@@ -12748,7 +13166,7 @@ function resolveLikePi(raw, cwd, home, platform) {
|
|
|
12748
13166
|
if (p === "~")
|
|
12749
13167
|
p = home;
|
|
12750
13168
|
else if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
|
|
12751
|
-
p =
|
|
13169
|
+
p = join9(home, p.slice(2));
|
|
12752
13170
|
if (/^file:\/\//.test(p))
|
|
12753
13171
|
p = fileURLToPath3(p);
|
|
12754
13172
|
return resolve2(cwd, p);
|
|
@@ -12758,17 +13176,17 @@ function realpathDeepest(p) {
|
|
|
12758
13176
|
const rest = [];
|
|
12759
13177
|
for (;; ) {
|
|
12760
13178
|
try {
|
|
12761
|
-
return
|
|
13179
|
+
return join9(realpathSync3(current), ...rest.reverse());
|
|
12762
13180
|
} catch {
|
|
12763
13181
|
try {
|
|
12764
13182
|
lstatSync(current);
|
|
12765
13183
|
return null;
|
|
12766
13184
|
} catch {}
|
|
12767
13185
|
}
|
|
12768
|
-
const parent =
|
|
13186
|
+
const parent = dirname10(current);
|
|
12769
13187
|
if (parent === current)
|
|
12770
13188
|
return null;
|
|
12771
|
-
rest.push(
|
|
13189
|
+
rest.push(basename4(current));
|
|
12772
13190
|
current = parent;
|
|
12773
13191
|
}
|
|
12774
13192
|
}
|
|
@@ -12777,10 +13195,10 @@ function isInside(root, target) {
|
|
|
12777
13195
|
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
|
|
12778
13196
|
}
|
|
12779
13197
|
function isSecret(target, home) {
|
|
12780
|
-
if (SECRET_BASENAME.some((re) => re.test(
|
|
13198
|
+
if (SECRET_BASENAME.some((re) => re.test(basename4(target))))
|
|
12781
13199
|
return true;
|
|
12782
13200
|
return SECRET_HOME_DIRS.some((dir) => {
|
|
12783
|
-
const lexical =
|
|
13201
|
+
const lexical = join9(home, dir);
|
|
12784
13202
|
return isInside(lexical, target) || isInside(realpathDeepest(lexical) ?? lexical, target);
|
|
12785
13203
|
});
|
|
12786
13204
|
}
|
|
@@ -12801,7 +13219,7 @@ function scanForSecret(dir) {
|
|
|
12801
13219
|
if (SECRET_BASENAME.some((re) => re.test(entry.name)))
|
|
12802
13220
|
return "yes";
|
|
12803
13221
|
if (entry.isDirectory())
|
|
12804
|
-
queue.push(
|
|
13222
|
+
queue.push(join9(current, entry.name));
|
|
12805
13223
|
}
|
|
12806
13224
|
}
|
|
12807
13225
|
return "no";
|
|
@@ -12925,6 +13343,33 @@ async function decideToolCall(event, opts) {
|
|
|
12925
13343
|
}
|
|
12926
13344
|
|
|
12927
13345
|
// src/hive.ts
|
|
13346
|
+
var HIVE_ERRORS = new Map(Object.entries({
|
|
13347
|
+
rate_limited: "Hive: slow down, too many messages in a short time.",
|
|
13348
|
+
pending: "Hive: another request is still waiting for an answer.",
|
|
13349
|
+
not_driver: "Hive: only the person driving the room can do that.",
|
|
13350
|
+
already_driver: "Hive: you are already driving the room.",
|
|
13351
|
+
unknown_conn: "Hive: that person has left the room.",
|
|
13352
|
+
unknown_prompt: "Hive: that prompt is no longer waiting.",
|
|
13353
|
+
unknown_request: "Hive: that request is no longer waiting.",
|
|
13354
|
+
bad_frame: "Hive: the room got a message it could not read. Try again.",
|
|
13355
|
+
internal: "Hive: something went wrong on the server. Try again."
|
|
13356
|
+
}));
|
|
13357
|
+
var HIVE_CLOSED = new Map(Object.entries({
|
|
13358
|
+
replaced: "Hive room closed: you opened a newer room.",
|
|
13359
|
+
host_left: "Hive room closed: the host left.",
|
|
13360
|
+
ended: "Hive room closed: the host ended it.",
|
|
13361
|
+
expired: "Hive room closed: it expired.",
|
|
13362
|
+
kicked: "Hive room closed: you were removed from it.",
|
|
13363
|
+
shutdown: "Hive room closed: the server is restarting.",
|
|
13364
|
+
no_room: "Hive room closed: the room no longer exists.",
|
|
13365
|
+
unauthorized: "Hive room closed: you are not signed in.",
|
|
13366
|
+
auth_timeout: "Hive room closed: sign-in took too long.",
|
|
13367
|
+
busy: "Hive room closed: the server is busy. Try again soon.",
|
|
13368
|
+
overloaded: "Hive room closed: the server is busy. Try again soon.",
|
|
13369
|
+
slow: "Hive room closed: the connection was too slow."
|
|
13370
|
+
}));
|
|
13371
|
+
var hiveErrorText = (code) => HIVE_ERRORS.get(code) ?? `Hive: ${code}`;
|
|
13372
|
+
var hiveClosedText = (reason) => HIVE_CLOSED.get(reason) ?? `Hive room closed (${reason}).`;
|
|
12928
13373
|
var RUN_IN_SANDBOX = "Run in sandbox";
|
|
12929
13374
|
var RUN_OUTSIDE = "Run outside the sandbox";
|
|
12930
13375
|
var DENY = "Deny";
|
|
@@ -12948,7 +13393,7 @@ function fingerprint(path) {
|
|
|
12948
13393
|
return "";
|
|
12949
13394
|
if (st.size > HASH_MAX_BYTES)
|
|
12950
13395
|
return `${st.size}:${st.mtimeMs}`;
|
|
12951
|
-
return createHash3("sha256").update(
|
|
13396
|
+
return createHash3("sha256").update(readFileSync6(path)).digest("hex");
|
|
12952
13397
|
} catch {
|
|
12953
13398
|
return "";
|
|
12954
13399
|
}
|
|
@@ -12978,7 +13423,7 @@ function worktreeSnapshot(g) {
|
|
|
12978
13423
|
const status = entry.slice(0, 2);
|
|
12979
13424
|
if (status[0] === "R" || status[0] === "C")
|
|
12980
13425
|
i++;
|
|
12981
|
-
files.set(entry.slice(3), `${status}:${fingerprint(
|
|
13426
|
+
files.set(entry.slice(3), `${status}:${fingerprint(join10(root, entry.slice(3)))}`);
|
|
12982
13427
|
}
|
|
12983
13428
|
return { files, head: runGit(g, "rev-parse", "HEAD")?.trim() };
|
|
12984
13429
|
} catch (e) {
|
|
@@ -13106,6 +13551,10 @@ function createHiveExtension(opts) {
|
|
|
13106
13551
|
roomWaiter = undefined;
|
|
13107
13552
|
}
|
|
13108
13553
|
async function start(ctx) {
|
|
13554
|
+
if (opts.isRemoteAttached?.()) {
|
|
13555
|
+
notify("Hive: turn the Telegram remote off first (/remote off).");
|
|
13556
|
+
return;
|
|
13557
|
+
}
|
|
13109
13558
|
if (ws || starting) {
|
|
13110
13559
|
notify(roomUrl ? `Hive room: ${roomUrl}` : "Hive: still connecting...");
|
|
13111
13560
|
return;
|
|
@@ -13140,10 +13589,13 @@ function createHiveExtension(opts) {
|
|
|
13140
13589
|
};
|
|
13141
13590
|
socket.onopen = () => socket.send(JSON.stringify({ type: "auth", token }));
|
|
13142
13591
|
socket.onerror = failed;
|
|
13143
|
-
socket.onclose = () => {
|
|
13592
|
+
socket.onclose = (event) => {
|
|
13144
13593
|
if (ws !== socket)
|
|
13145
13594
|
return;
|
|
13146
|
-
|
|
13595
|
+
const known = event?.reason ? HIVE_CLOSED.get(event.reason) : undefined;
|
|
13596
|
+
if (known)
|
|
13597
|
+
endRoom(known);
|
|
13598
|
+
else if (!roomUrl)
|
|
13147
13599
|
failed();
|
|
13148
13600
|
else
|
|
13149
13601
|
endRoom("Hive room closed.");
|
|
@@ -13217,10 +13669,10 @@ Guests sign in with GitHub to watch and chat. They can ask to drive; you approve
|
|
|
13217
13669
|
break;
|
|
13218
13670
|
case "error":
|
|
13219
13671
|
if (frame.code !== "stale")
|
|
13220
|
-
notify(
|
|
13672
|
+
notify(hiveErrorText(frame.code), "warning");
|
|
13221
13673
|
break;
|
|
13222
13674
|
case "closed":
|
|
13223
|
-
endRoom(
|
|
13675
|
+
endRoom(hiveClosedText(frame.reason));
|
|
13224
13676
|
break;
|
|
13225
13677
|
}
|
|
13226
13678
|
}
|
|
@@ -13431,22 +13883,440 @@ ${decision.reason}`, ACTION_DIALOG_MS);
|
|
|
13431
13883
|
};
|
|
13432
13884
|
}
|
|
13433
13885
|
|
|
13886
|
+
// src/remote.ts
|
|
13887
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
13888
|
+
import { basename as basename5 } from "node:path";
|
|
13889
|
+
|
|
13890
|
+
// ../shared/src/remote.ts
|
|
13891
|
+
var REMOTE_MAX_TITLE_CHARS = 80;
|
|
13892
|
+
var REMOTE_MAX_COMMAND_CHARS = 2000;
|
|
13893
|
+
var IdSchema = string2().min(1).max(64);
|
|
13894
|
+
var RemoteChoiceSchema = _enum(["once", "turn", "deny"]);
|
|
13895
|
+
var RemoteSessionFrameSchema = discriminatedUnion("type", [
|
|
13896
|
+
HiveAuthFrameSchema,
|
|
13897
|
+
object({ type: literal("hello"), session_id: IdSchema, title: string2().max(REMOTE_MAX_TITLE_CHARS) }),
|
|
13898
|
+
object({ type: literal("entry"), entry: HiveEntrySchema }),
|
|
13899
|
+
object({ type: literal("busy"), settled: boolean2() }),
|
|
13900
|
+
object({ type: literal("prompt_result"), prompt_id: IdSchema, accepted: boolean2(), reason: string2().max(100).optional() }),
|
|
13901
|
+
object({ type: literal("question"), id: IdSchema, text: string2().max(500), command: string2().max(REMOTE_MAX_COMMAND_CHARS) }),
|
|
13902
|
+
object({ type: literal("question_closed"), id: IdSchema })
|
|
13903
|
+
]);
|
|
13904
|
+
var RemoteServerFrameSchema = discriminatedUnion("type", [
|
|
13905
|
+
object({ type: literal("ready"), linked: boolean2(), link_url: string2().optional() }),
|
|
13906
|
+
object({ type: literal("prompt"), prompt_id: IdSchema, text: string2().min(1).max(HIVE_MAX_PROMPT_CHARS) }),
|
|
13907
|
+
object({ type: literal("answer"), question_id: IdSchema, choice: RemoteChoiceSchema }),
|
|
13908
|
+
object({ type: literal("stop") }),
|
|
13909
|
+
object({ type: literal("error"), code: string2() }),
|
|
13910
|
+
object({ type: literal("closed"), reason: string2() })
|
|
13911
|
+
]);
|
|
13912
|
+
var RemoteLinkCodeResponseSchema = object({ url: string2(), expires_at: string2() });
|
|
13913
|
+
var RemoteLinkStatusSchema = object({ linked: boolean2() });
|
|
13914
|
+
|
|
13915
|
+
// src/remote.ts
|
|
13916
|
+
var OPEN2 = 1;
|
|
13917
|
+
var MCP_RESOURCE_TOOLS3 = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
|
|
13918
|
+
var HARD_BLOCKED = new Set([SUBAGENT_TOOL, SEND_MESSAGE_TOOL, BUY_TOOL_NAME]);
|
|
13919
|
+
function wsUrl2(baseUrl) {
|
|
13920
|
+
const url = new URL("/remote/ws", baseUrl);
|
|
13921
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
13922
|
+
return url.toString();
|
|
13923
|
+
}
|
|
13924
|
+
function createRemoteExtension(opts) {
|
|
13925
|
+
return {
|
|
13926
|
+
name: "free-pi-remote",
|
|
13927
|
+
factory(pi) {
|
|
13928
|
+
let ws;
|
|
13929
|
+
let ctxRef;
|
|
13930
|
+
let attachedCwd = "";
|
|
13931
|
+
let title = "";
|
|
13932
|
+
let settled = true;
|
|
13933
|
+
let compacting = false;
|
|
13934
|
+
let allowTurn = false;
|
|
13935
|
+
let localDetach = false;
|
|
13936
|
+
let reconnectAttempt = 0;
|
|
13937
|
+
let reconnectTimer;
|
|
13938
|
+
let injectTimer;
|
|
13939
|
+
let questionChain = Promise.resolve();
|
|
13940
|
+
let openQuestion;
|
|
13941
|
+
const notify = (message, type = "info") => ctxRef?.ui.notify(message, type);
|
|
13942
|
+
const send = (frame) => {
|
|
13943
|
+
if (ws?.readyState === OPEN2)
|
|
13944
|
+
ws.send(JSON.stringify(frame));
|
|
13945
|
+
};
|
|
13946
|
+
const sendEntry = (entry) => send({ type: "entry", entry });
|
|
13947
|
+
function setSettled(next) {
|
|
13948
|
+
if (settled === next)
|
|
13949
|
+
return;
|
|
13950
|
+
settled = next;
|
|
13951
|
+
send({ type: "busy", settled });
|
|
13952
|
+
}
|
|
13953
|
+
function finishQuestion(choice, reason) {
|
|
13954
|
+
const question = openQuestion;
|
|
13955
|
+
if (!question)
|
|
13956
|
+
return;
|
|
13957
|
+
openQuestion = undefined;
|
|
13958
|
+
question.finish(choice, reason);
|
|
13959
|
+
if (choice === undefined)
|
|
13960
|
+
send({ type: "question_closed", id: question.id });
|
|
13961
|
+
}
|
|
13962
|
+
function cancelQuestion(reason) {
|
|
13963
|
+
finishQuestion(undefined, reason);
|
|
13964
|
+
}
|
|
13965
|
+
function clearRemoteState(reason = "turn ended") {
|
|
13966
|
+
opts.state.remoteTurn = false;
|
|
13967
|
+
allowTurn = false;
|
|
13968
|
+
cancelQuestion(reason);
|
|
13969
|
+
}
|
|
13970
|
+
function endApprovals(reason) {
|
|
13971
|
+
allowTurn = false;
|
|
13972
|
+
cancelQuestion(reason);
|
|
13973
|
+
}
|
|
13974
|
+
function recoverRejectedPrompt() {
|
|
13975
|
+
setSettled(true);
|
|
13976
|
+
clearRemoteState();
|
|
13977
|
+
}
|
|
13978
|
+
function clearSocket() {
|
|
13979
|
+
const old = ws;
|
|
13980
|
+
ws = undefined;
|
|
13981
|
+
clearTimeout(reconnectTimer);
|
|
13982
|
+
reconnectTimer = undefined;
|
|
13983
|
+
ctxRef?.ui.setStatus("remote", undefined);
|
|
13984
|
+
cancelQuestion("disconnected");
|
|
13985
|
+
try {
|
|
13986
|
+
old?.close();
|
|
13987
|
+
} catch {}
|
|
13988
|
+
}
|
|
13989
|
+
function detach(notice) {
|
|
13990
|
+
localDetach = true;
|
|
13991
|
+
opts.state.attached = false;
|
|
13992
|
+
endApprovals("detached");
|
|
13993
|
+
clearSocket();
|
|
13994
|
+
attachedCwd = "";
|
|
13995
|
+
reconnectAttempt = 0;
|
|
13996
|
+
if (notice)
|
|
13997
|
+
notify(notice);
|
|
13998
|
+
}
|
|
13999
|
+
function questionFor(event, ctx) {
|
|
14000
|
+
const toolName = String(event.toolName);
|
|
14001
|
+
const input = event.input ?? {};
|
|
14002
|
+
const summary = toolName === "bash" ? String(input.command ?? "") : toolName + " " + JSON.stringify(input);
|
|
14003
|
+
const command = summary.slice(0, 2000);
|
|
14004
|
+
const run = async () => {
|
|
14005
|
+
if (!opts.state.remoteTurn)
|
|
14006
|
+
return { block: true, reason: "Remote: turn ended before approval" };
|
|
14007
|
+
if (allowTurn)
|
|
14008
|
+
return;
|
|
14009
|
+
if (ws?.readyState !== OPEN2)
|
|
14010
|
+
return { block: true, reason: "Remote: disconnected before approval" };
|
|
14011
|
+
const id = randomUUID2();
|
|
14012
|
+
let timer;
|
|
14013
|
+
let abortListener;
|
|
14014
|
+
const answer = await new Promise((resolve) => {
|
|
14015
|
+
openQuestion = {
|
|
14016
|
+
id,
|
|
14017
|
+
finish: (choice, reason) => {
|
|
14018
|
+
clearTimeout(timer);
|
|
14019
|
+
if (abortListener && ctx.signal)
|
|
14020
|
+
ctx.signal.removeEventListener("abort", abortListener);
|
|
14021
|
+
resolve({ choice, reason });
|
|
14022
|
+
}
|
|
14023
|
+
};
|
|
14024
|
+
const timeoutMs = opts.questionTimeoutMs ?? 5 * 60000;
|
|
14025
|
+
timer = setTimeout(() => finishQuestion(undefined, "timed out"), timeoutMs);
|
|
14026
|
+
timer.unref?.();
|
|
14027
|
+
abortListener = () => finishQuestion(undefined, "turn aborted");
|
|
14028
|
+
if (ctx.signal?.aborted)
|
|
14029
|
+
abortListener();
|
|
14030
|
+
else
|
|
14031
|
+
ctx.signal?.addEventListener("abort", abortListener, { once: true });
|
|
14032
|
+
send({ type: "question", id, text: "The agent wants to run this:", command });
|
|
14033
|
+
});
|
|
14034
|
+
if (answer.choice === "deny")
|
|
14035
|
+
return { block: true, reason: "Remote: denied from Telegram" };
|
|
14036
|
+
if (answer.choice === "turn")
|
|
14037
|
+
allowTurn = true;
|
|
14038
|
+
if (answer.choice === "once" || answer.choice === "turn")
|
|
14039
|
+
return;
|
|
14040
|
+
const why = answer.reason ?? "approval ended";
|
|
14041
|
+
return { block: true, reason: "Remote: " + why };
|
|
14042
|
+
};
|
|
14043
|
+
const current = questionChain.then(run, run);
|
|
14044
|
+
questionChain = current.catch(() => {
|
|
14045
|
+
return;
|
|
14046
|
+
});
|
|
14047
|
+
return current;
|
|
14048
|
+
}
|
|
14049
|
+
function handle(frame) {
|
|
14050
|
+
switch (frame.type) {
|
|
14051
|
+
case "ready":
|
|
14052
|
+
if (!frame.linked && frame.link_url) {
|
|
14053
|
+
notify("Remote: open this link in Telegram to connect your account: " + frame.link_url);
|
|
14054
|
+
} else if (frame.linked) {
|
|
14055
|
+
notify("Remote is on. Messages you send to the free-pi bot in Telegram go to this session.");
|
|
14056
|
+
}
|
|
14057
|
+
ctxRef?.ui.setStatus("remote", "remote: on");
|
|
14058
|
+
reconnectAttempt = 0;
|
|
14059
|
+
if (!settled)
|
|
14060
|
+
send({ type: "busy", settled: false });
|
|
14061
|
+
break;
|
|
14062
|
+
case "prompt": {
|
|
14063
|
+
if (!settled || compacting) {
|
|
14064
|
+
send({ type: "prompt_result", prompt_id: frame.prompt_id, accepted: false, reason: "busy" });
|
|
14065
|
+
break;
|
|
14066
|
+
}
|
|
14067
|
+
opts.state.remoteTurn = true;
|
|
14068
|
+
allowTurn = false;
|
|
14069
|
+
setSettled(false);
|
|
14070
|
+
clearTimeout(injectTimer);
|
|
14071
|
+
injectTimer = setTimeout(() => {
|
|
14072
|
+
injectTimer = undefined;
|
|
14073
|
+
if (ctxRef?.isIdle() !== false)
|
|
14074
|
+
recoverRejectedPrompt();
|
|
14075
|
+
}, opts.injectGraceMs ?? 2000);
|
|
14076
|
+
injectTimer.unref?.();
|
|
14077
|
+
try {
|
|
14078
|
+
Promise.resolve(pi.sendUserMessage(frame.text, { expandPromptTemplates: false })).catch(recoverRejectedPrompt);
|
|
14079
|
+
send({ type: "prompt_result", prompt_id: frame.prompt_id, accepted: true });
|
|
14080
|
+
} catch {
|
|
14081
|
+
recoverRejectedPrompt();
|
|
14082
|
+
send({ type: "prompt_result", prompt_id: frame.prompt_id, accepted: false, reason: "error" });
|
|
14083
|
+
}
|
|
14084
|
+
break;
|
|
14085
|
+
}
|
|
14086
|
+
case "answer":
|
|
14087
|
+
if (openQuestion?.id === frame.question_id)
|
|
14088
|
+
finishQuestion(frame.choice);
|
|
14089
|
+
break;
|
|
14090
|
+
case "stop":
|
|
14091
|
+
cancelQuestion("stopped");
|
|
14092
|
+
if (!settled)
|
|
14093
|
+
ctxRef?.abort();
|
|
14094
|
+
break;
|
|
14095
|
+
case "closed":
|
|
14096
|
+
if (frame.reason === "shutdown")
|
|
14097
|
+
break;
|
|
14098
|
+
detach(frame.reason === "replaced" ? "Remote moved to another session." : "Remote closed.");
|
|
14099
|
+
break;
|
|
14100
|
+
case "error":
|
|
14101
|
+
notify("Remote: the server could not process the connection.", "warning");
|
|
14102
|
+
break;
|
|
14103
|
+
}
|
|
14104
|
+
}
|
|
14105
|
+
function connect() {
|
|
14106
|
+
let socket;
|
|
14107
|
+
try {
|
|
14108
|
+
const Impl = opts.WebSocketImpl ?? WebSocket;
|
|
14109
|
+
socket = new Impl(wsUrl2(opts.baseUrl));
|
|
14110
|
+
} catch {
|
|
14111
|
+
retryConnect();
|
|
14112
|
+
return;
|
|
14113
|
+
}
|
|
14114
|
+
ws = socket;
|
|
14115
|
+
localDetach = false;
|
|
14116
|
+
socket.onopen = async () => {
|
|
14117
|
+
if (ws !== socket)
|
|
14118
|
+
return;
|
|
14119
|
+
try {
|
|
14120
|
+
const token = await opts.getToken();
|
|
14121
|
+
if (ws !== socket)
|
|
14122
|
+
return;
|
|
14123
|
+
socket.send(JSON.stringify({ type: "auth", token }));
|
|
14124
|
+
socket.send(JSON.stringify({ type: "hello", session_id: opts.sessionId, title }));
|
|
14125
|
+
} catch {
|
|
14126
|
+
socket.close();
|
|
14127
|
+
}
|
|
14128
|
+
};
|
|
14129
|
+
socket.onerror = () => {
|
|
14130
|
+
if (ws === socket)
|
|
14131
|
+
socket.close();
|
|
14132
|
+
};
|
|
14133
|
+
socket.onclose = () => {
|
|
14134
|
+
if (ws !== socket)
|
|
14135
|
+
return;
|
|
14136
|
+
ws = undefined;
|
|
14137
|
+
ctxRef?.ui.setStatus("remote", undefined);
|
|
14138
|
+
cancelQuestion("disconnected");
|
|
14139
|
+
if (!localDetach)
|
|
14140
|
+
retryConnect();
|
|
14141
|
+
};
|
|
14142
|
+
socket.onmessage = (event) => {
|
|
14143
|
+
if (ws !== socket || typeof event.data !== "string")
|
|
14144
|
+
return;
|
|
14145
|
+
let json;
|
|
14146
|
+
try {
|
|
14147
|
+
json = JSON.parse(event.data);
|
|
14148
|
+
} catch {
|
|
14149
|
+
return;
|
|
14150
|
+
}
|
|
14151
|
+
const parsed = RemoteServerFrameSchema.safeParse(json);
|
|
14152
|
+
if (parsed.success)
|
|
14153
|
+
handle(parsed.data);
|
|
14154
|
+
};
|
|
14155
|
+
}
|
|
14156
|
+
function retryConnect() {
|
|
14157
|
+
if (localDetach)
|
|
14158
|
+
return;
|
|
14159
|
+
const delays = opts.reconnectDelaysMs ?? [1000, 2000, 5000, 15000, 30000];
|
|
14160
|
+
if (reconnectAttempt >= delays.length) {
|
|
14161
|
+
localDetach = true;
|
|
14162
|
+
opts.state.attached = false;
|
|
14163
|
+
endApprovals("disconnected");
|
|
14164
|
+
notify("Remote: lost the connection. Run /remote to attach again.", "warning");
|
|
14165
|
+
return;
|
|
14166
|
+
}
|
|
14167
|
+
const delay = delays[reconnectAttempt++];
|
|
14168
|
+
reconnectTimer = setTimeout(() => {
|
|
14169
|
+
reconnectTimer = undefined;
|
|
14170
|
+
connect();
|
|
14171
|
+
}, delay);
|
|
14172
|
+
reconnectTimer.unref?.();
|
|
14173
|
+
}
|
|
14174
|
+
function attach(ctx) {
|
|
14175
|
+
if (ws || reconnectTimer) {
|
|
14176
|
+
notify("Remote is already attached.");
|
|
14177
|
+
return;
|
|
14178
|
+
}
|
|
14179
|
+
ctxRef = ctx;
|
|
14180
|
+
if (opts.isHiveOpen?.()) {
|
|
14181
|
+
notify("Remote: end the Hive room first (/hive end).");
|
|
14182
|
+
return;
|
|
14183
|
+
}
|
|
14184
|
+
opts.state.attached = true;
|
|
14185
|
+
attachedCwd = ctx.cwd;
|
|
14186
|
+
title = basename5(ctx.cwd).slice(0, 80);
|
|
14187
|
+
settled = ctx.isIdle();
|
|
14188
|
+
compacting = false;
|
|
14189
|
+
localDetach = false;
|
|
14190
|
+
reconnectAttempt = 0;
|
|
14191
|
+
connect();
|
|
14192
|
+
}
|
|
14193
|
+
pi.on("tool_call", (event, ctx) => {
|
|
14194
|
+
if (!opts.state.remoteTurn)
|
|
14195
|
+
return;
|
|
14196
|
+
ctxRef = ctx;
|
|
14197
|
+
if (HARD_BLOCKED.has(event.toolName)) {
|
|
14198
|
+
return { block: true, reason: "Remote: this tool is not run for a prompt sent from Telegram" };
|
|
14199
|
+
}
|
|
14200
|
+
const needsApproval = event.toolName === "bash" || event.toolName.startsWith("mcp__") || MCP_RESOURCE_TOOLS3.includes(event.toolName) || Boolean(event.parentToolCallId);
|
|
14201
|
+
return needsApproval ? questionFor(event, ctx) : undefined;
|
|
14202
|
+
});
|
|
14203
|
+
pi.on("agent_start", () => {
|
|
14204
|
+
clearTimeout(injectTimer);
|
|
14205
|
+
injectTimer = undefined;
|
|
14206
|
+
setSettled(false);
|
|
14207
|
+
});
|
|
14208
|
+
pi.on("agent_settled", () => {
|
|
14209
|
+
setSettled(true);
|
|
14210
|
+
clearRemoteState();
|
|
14211
|
+
sendEntry({ kind: "settled" });
|
|
14212
|
+
});
|
|
14213
|
+
pi.on("session_before_compact", (_event, ctx) => {
|
|
14214
|
+
ctxRef = ctx;
|
|
14215
|
+
compacting = true;
|
|
14216
|
+
setSettled(false);
|
|
14217
|
+
});
|
|
14218
|
+
const compactionEnded = (_event, ctx) => {
|
|
14219
|
+
ctxRef = ctx;
|
|
14220
|
+
compacting = false;
|
|
14221
|
+
if (ctx.isIdle())
|
|
14222
|
+
setSettled(true);
|
|
14223
|
+
};
|
|
14224
|
+
pi.on("session_compact", compactionEnded);
|
|
14225
|
+
pi.on("session_compact_failed", compactionEnded);
|
|
14226
|
+
pi.on("session_start", (_event, ctx) => {
|
|
14227
|
+
ctxRef = ctx;
|
|
14228
|
+
if (attachedCwd && ctx.cwd !== attachedCwd)
|
|
14229
|
+
detach();
|
|
14230
|
+
});
|
|
14231
|
+
pi.on("session_before_switch", () => detach());
|
|
14232
|
+
pi.on("session_shutdown", () => detach());
|
|
14233
|
+
pi.on("input", (event) => {
|
|
14234
|
+
if (event.source !== "extension")
|
|
14235
|
+
sendEntry({ kind: "prompt", text: event.text.slice(0, HIVE_MAX_PROMPT_CHARS) });
|
|
14236
|
+
return { action: "continue" };
|
|
14237
|
+
});
|
|
14238
|
+
pi.on("message_end", (event) => {
|
|
14239
|
+
const message = event.message;
|
|
14240
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
14241
|
+
return;
|
|
14242
|
+
const text = message.content.map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").join("");
|
|
14243
|
+
for (let i = 0;i < text.length; i += HIVE_MAX_ASSISTANT_CHARS) {
|
|
14244
|
+
sendEntry({ kind: "assistant", text: text.slice(i, i + HIVE_MAX_ASSISTANT_CHARS) });
|
|
14245
|
+
}
|
|
14246
|
+
});
|
|
14247
|
+
pi.on("tool_execution_end", (event) => {
|
|
14248
|
+
sendEntry({ kind: "tool", name: event.toolName, is_error: event.isError });
|
|
14249
|
+
});
|
|
14250
|
+
if (process.env.FREEPI_REMOTE === "1") {
|
|
14251
|
+
pi.registerCommand("remote", {
|
|
14252
|
+
description: "Attach this session to Telegram: /remote, /remote off, /remote unlink",
|
|
14253
|
+
handler: async (args, ctx) => {
|
|
14254
|
+
ctxRef = ctx;
|
|
14255
|
+
const sub = args.trim().toLowerCase();
|
|
14256
|
+
if (sub === "")
|
|
14257
|
+
return attach(ctx);
|
|
14258
|
+
if (sub === "off") {
|
|
14259
|
+
detach();
|
|
14260
|
+
notify("Remote is off.");
|
|
14261
|
+
return;
|
|
14262
|
+
}
|
|
14263
|
+
if (sub === "unlink") {
|
|
14264
|
+
allowTurn = false;
|
|
14265
|
+
cancelQuestion("unlinked");
|
|
14266
|
+
const token = await opts.getToken().catch(() => {
|
|
14267
|
+
return;
|
|
14268
|
+
});
|
|
14269
|
+
if (!token) {
|
|
14270
|
+
notify("Remote: couldn't read your free-pi login.", "error");
|
|
14271
|
+
return;
|
|
14272
|
+
}
|
|
14273
|
+
try {
|
|
14274
|
+
const response = await (opts.fetchImpl ?? fetch)(opts.baseUrl + "/remote/link", {
|
|
14275
|
+
method: "DELETE",
|
|
14276
|
+
headers: { authorization: "Bearer " + token }
|
|
14277
|
+
});
|
|
14278
|
+
if (response.ok)
|
|
14279
|
+
detach();
|
|
14280
|
+
notify(response.ok ? "Remote account unlinked." : "Remote: couldn't unlink the account.", response.ok ? "info" : "warning");
|
|
14281
|
+
} catch {
|
|
14282
|
+
notify("Remote: couldn't unlink the account.", "warning");
|
|
14283
|
+
}
|
|
14284
|
+
return;
|
|
14285
|
+
}
|
|
14286
|
+
notify("Usage: /remote, /remote off, or /remote unlink.");
|
|
14287
|
+
}
|
|
14288
|
+
});
|
|
14289
|
+
}
|
|
14290
|
+
}
|
|
14291
|
+
};
|
|
14292
|
+
}
|
|
14293
|
+
|
|
13434
14294
|
// src/shell-sandbox.ts
|
|
13435
14295
|
import { spawnSync } from "node:child_process";
|
|
13436
14296
|
import {
|
|
13437
14297
|
chmodSync,
|
|
13438
14298
|
existsSync as existsSync5,
|
|
13439
14299
|
lstatSync as lstatSync3,
|
|
13440
|
-
mkdirSync as
|
|
14300
|
+
mkdirSync as mkdirSync4,
|
|
13441
14301
|
mkdtempSync,
|
|
13442
14302
|
readdirSync as readdirSync2,
|
|
13443
14303
|
realpathSync as realpathSync4,
|
|
13444
14304
|
rmSync,
|
|
13445
14305
|
statSync as statSync3,
|
|
13446
|
-
writeFileSync as
|
|
14306
|
+
writeFileSync as writeFileSync4
|
|
13447
14307
|
} from "node:fs";
|
|
13448
14308
|
import { homedir as homedir6, tmpdir } from "node:os";
|
|
13449
|
-
import { dirname as
|
|
14309
|
+
import { dirname as dirname11, join as join11 } from "node:path";
|
|
14310
|
+
var ERRNO_WORDS = { EACCES: "permission denied", EPERM: "operation not permitted", ENOENT: "not found", ENOSPC: "disk full", EROFS: "read-only file system" };
|
|
14311
|
+
function sandboxCause(err) {
|
|
14312
|
+
const e = err;
|
|
14313
|
+
if (typeof e?.code === "string") {
|
|
14314
|
+
const where = [e.syscall, e.path].filter((x) => typeof x === "string").join(" ");
|
|
14315
|
+
return `${Object.hasOwn(ERRNO_WORDS, e.code) ? ERRNO_WORDS[e.code] : e.code}${where ? ` (${where})` : ""}`;
|
|
14316
|
+
}
|
|
14317
|
+
return (err instanceof Error ? err.message : String(err)).split(`
|
|
14318
|
+
`)[0].replace(/\.$/, "");
|
|
14319
|
+
}
|
|
13450
14320
|
var SECRET_NAME_PATTERNS = [
|
|
13451
14321
|
"\\.env(\\..*)?",
|
|
13452
14322
|
"[^/]*\\.pem",
|
|
@@ -13482,7 +14352,7 @@ var TOOLCHAIN_ROOTS = [
|
|
|
13482
14352
|
"Library/pnpm"
|
|
13483
14353
|
];
|
|
13484
14354
|
function toolchainRoots(home, exists = existsSync5) {
|
|
13485
|
-
return TOOLCHAIN_ROOTS.map((r) =>
|
|
14355
|
+
return TOOLCHAIN_ROOTS.map((r) => join11(home, r)).filter((p) => exists(p));
|
|
13486
14356
|
}
|
|
13487
14357
|
var ENV_ALLOW = ["PATH", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "COLORTERM", "TZ", "USER", "LOGNAME", "SHELL", "PI_PROVIDER", "PI_MODEL"];
|
|
13488
14358
|
function under(path, dir) {
|
|
@@ -13500,10 +14370,10 @@ function sandboxEnv(base, o) {
|
|
|
13500
14370
|
for (const k of ENV_ALLOW)
|
|
13501
14371
|
if (base[k] !== undefined)
|
|
13502
14372
|
env[k] = base[k];
|
|
13503
|
-
env.PATH = [...o.developerDir ? [
|
|
14373
|
+
env.PATH = [...o.developerDir ? [join11(o.developerDir, "usr/bin")] : [], ...o.pathPrefix ?? [], ...(base.PATH ?? "/usr/bin:/bin").split(":")].filter((p) => p.startsWith("/") && !under(p, o.project) && !under(realOrSelf(p), o.project)).join(":");
|
|
13504
14374
|
if (o.developerDir)
|
|
13505
14375
|
env.DEVELOPER_DIR = o.developerDir;
|
|
13506
|
-
env.HOME =
|
|
14376
|
+
env.HOME = join11(o.scratch, "home");
|
|
13507
14377
|
env.TMP = env.TEMP = o.scratch;
|
|
13508
14378
|
env.TMPDIR = `${o.scratch}/`;
|
|
13509
14379
|
env.BUN_OPTIONS = "--no-env-file";
|
|
@@ -13513,7 +14383,7 @@ function sandboxEnv(base, o) {
|
|
|
13513
14383
|
env.GIT_CONFIG_COUNT = "1";
|
|
13514
14384
|
env.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
13515
14385
|
env.GIT_CONFIG_VALUE_0 = "";
|
|
13516
|
-
env.npm_config_cache =
|
|
14386
|
+
env.npm_config_cache = join11(o.scratch, "npm-cache");
|
|
13517
14387
|
env.FREEPI_HIVE_CALL = o.callId;
|
|
13518
14388
|
return env;
|
|
13519
14389
|
}
|
|
@@ -13559,7 +14429,7 @@ function scanProject(project, limit = SCAN_LIMIT2) {
|
|
|
13559
14429
|
scan.overflow = true;
|
|
13560
14430
|
break;
|
|
13561
14431
|
}
|
|
13562
|
-
const path =
|
|
14432
|
+
const path = join11(dir, e.name);
|
|
13563
14433
|
if (e.isSymbolicLink())
|
|
13564
14434
|
continue;
|
|
13565
14435
|
if (e.isDirectory()) {
|
|
@@ -13659,7 +14529,7 @@ function developerDir(o = {}) {
|
|
|
13659
14529
|
continue;
|
|
13660
14530
|
try {
|
|
13661
14531
|
const real = realpath(c);
|
|
13662
|
-
if (exists(
|
|
14532
|
+
if (exists(join11(real, "usr/bin/git")))
|
|
13663
14533
|
return real;
|
|
13664
14534
|
} catch {}
|
|
13665
14535
|
}
|
|
@@ -13677,18 +14547,18 @@ function opensslFiles(o = {}) {
|
|
|
13677
14547
|
const files = [];
|
|
13678
14548
|
const dirs = [];
|
|
13679
14549
|
for (const prefix of o.prefixes ?? ["/opt/homebrew", "/usr/local"]) {
|
|
13680
|
-
const etc =
|
|
14550
|
+
const etc = join11(prefix, "etc");
|
|
13681
14551
|
for (const name of list(etc)) {
|
|
13682
14552
|
if (!/^openssl(@[0-9.]+)?$/.test(name))
|
|
13683
14553
|
continue;
|
|
13684
14554
|
for (const f of ["openssl.cnf", "ct_log_list.cnf", "cert.pem"])
|
|
13685
|
-
if (exists(
|
|
13686
|
-
files.push(
|
|
13687
|
-
if (exists(
|
|
13688
|
-
dirs.push(
|
|
14555
|
+
if (exists(join11(etc, name, f)))
|
|
14556
|
+
files.push(join11(etc, name, f));
|
|
14557
|
+
if (exists(join11(etc, name, "certs")))
|
|
14558
|
+
dirs.push(join11(etc, name, "certs"));
|
|
13689
14559
|
}
|
|
13690
|
-
if (exists(
|
|
13691
|
-
files.push(
|
|
14560
|
+
if (exists(join11(etc, "ca-certificates/cert.pem")))
|
|
14561
|
+
files.push(join11(etc, "ca-certificates/cert.pem"));
|
|
13692
14562
|
}
|
|
13693
14563
|
return { files, dirs };
|
|
13694
14564
|
}
|
|
@@ -13696,7 +14566,7 @@ var MAC_DEV_READ = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/d
|
|
|
13696
14566
|
var MAC_DEV_WRITE = ["/dev/null", "/dev/tty", "/dev/zero", "/dev/dtracehelper"];
|
|
13697
14567
|
function ancestors(p) {
|
|
13698
14568
|
const out = [];
|
|
13699
|
-
for (let d =
|
|
14569
|
+
for (let d = dirname11(p);; d = dirname11(d)) {
|
|
13700
14570
|
out.push(d);
|
|
13701
14571
|
if (d === "/")
|
|
13702
14572
|
break;
|
|
@@ -13732,9 +14602,9 @@ function buildMacProfile(i) {
|
|
|
13732
14602
|
L.push(`(deny file-read* file-write* (literal ${sbString(s)}))`);
|
|
13733
14603
|
for (const f of i.links)
|
|
13734
14604
|
L.push(`(deny file-write* (literal ${sbString(f)}))`);
|
|
13735
|
-
const guarded = new Set([i.project,
|
|
13736
|
-
for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [],
|
|
13737
|
-
for (let d =
|
|
14605
|
+
const guarded = new Set([i.project, join11(i.project, ".git"), ...i.nodeModules ?? []]);
|
|
14606
|
+
for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join11(i.project, ".git")]) {
|
|
14607
|
+
for (let d = dirname11(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname11(d))
|
|
13738
14608
|
guarded.add(d);
|
|
13739
14609
|
}
|
|
13740
14610
|
for (const d of [...guarded].sort())
|
|
@@ -13803,10 +14673,10 @@ function buildBwrapArgs(i) {
|
|
|
13803
14673
|
a.push("--bind", i.scratch, i.scratch);
|
|
13804
14674
|
for (const d of i.nodeModules)
|
|
13805
14675
|
ro(d);
|
|
13806
|
-
if (exists(
|
|
13807
|
-
ro(
|
|
13808
|
-
if (exists(
|
|
13809
|
-
ro(
|
|
14676
|
+
if (exists(join11(i.project, ".git/hooks")))
|
|
14677
|
+
ro(join11(i.project, ".git/hooks"));
|
|
14678
|
+
if (exists(join11(i.project, ".git/config")))
|
|
14679
|
+
ro(join11(i.project, ".git/config"));
|
|
13810
14680
|
for (const f of i.links)
|
|
13811
14681
|
ro(f);
|
|
13812
14682
|
for (const s of i.sockets)
|
|
@@ -13836,7 +14706,7 @@ function removeTree(path) {
|
|
|
13836
14706
|
return;
|
|
13837
14707
|
chmodSync(p, 448);
|
|
13838
14708
|
for (const e of readdirSync2(p))
|
|
13839
|
-
walk(
|
|
14709
|
+
walk(join11(p, e));
|
|
13840
14710
|
} catch {}
|
|
13841
14711
|
};
|
|
13842
14712
|
walk(path);
|
|
@@ -13874,7 +14744,7 @@ function createShellSandbox(deps = {}) {
|
|
|
13874
14744
|
if (deps.python !== undefined)
|
|
13875
14745
|
return deps.python;
|
|
13876
14746
|
const dev = platform === "darwin" ? macHost().developerDir : null;
|
|
13877
|
-
return dev && existsSync5(
|
|
14747
|
+
return dev && existsSync5(join11(dev, "usr/bin/python3")) ? join11(dev, "usr/bin/python3") : null;
|
|
13878
14748
|
};
|
|
13879
14749
|
let cached;
|
|
13880
14750
|
function probe() {
|
|
@@ -13992,13 +14862,13 @@ function createShellSandbox(deps = {}) {
|
|
|
13992
14862
|
return { ready: false, reason: "a folder in the project cannot be read, so it cannot be checked for sockets and hard links" };
|
|
13993
14863
|
let scratch;
|
|
13994
14864
|
try {
|
|
13995
|
-
scratch = mkdtempSync(
|
|
14865
|
+
scratch = mkdtempSync(join11(realpathSync4(tmpdir()), "freepi-hive-"));
|
|
13996
14866
|
const dir = scratch;
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
const emptyFile =
|
|
14000
|
-
const marker =
|
|
14001
|
-
|
|
14867
|
+
mkdirSync4(join11(dir, "home"));
|
|
14868
|
+
mkdirSync4(join11(dir, "npm-cache"));
|
|
14869
|
+
const emptyFile = join11(dir, "empty");
|
|
14870
|
+
const marker = join11(dir, "call.fd");
|
|
14871
|
+
writeFileSync4(emptyFile, "", { mode: 292 });
|
|
14002
14872
|
const roots = toolchainRoots(home);
|
|
14003
14873
|
const mac = platform === "darwin";
|
|
14004
14874
|
const host = mac ? macHost() : undefined;
|
|
@@ -14050,7 +14920,7 @@ function createShellSandbox(deps = {}) {
|
|
|
14050
14920
|
} catch (err) {
|
|
14051
14921
|
if (scratch)
|
|
14052
14922
|
removeTree(scratch);
|
|
14053
|
-
return { ready: false, reason: `
|
|
14923
|
+
return { ready: false, reason: `Couldn't set up the command sandbox: ${sandboxCause(err)}.` };
|
|
14054
14924
|
}
|
|
14055
14925
|
},
|
|
14056
14926
|
mark(toolCallId, mode) {
|
|
@@ -14147,6 +15017,44 @@ function installSafeUrlOpen(mode, open = openLink) {
|
|
|
14147
15017
|
});
|
|
14148
15018
|
}
|
|
14149
15019
|
|
|
15020
|
+
// src/retry-failed-line.ts
|
|
15021
|
+
var RETRY_FAILED = /^Retry failed after (\d+) attempts?: ([\s\S]*)$/;
|
|
15022
|
+
var CRASH_WARNING = /^\S+ crashed on /;
|
|
15023
|
+
var PRODUCT = "free-pi-cli";
|
|
15024
|
+
function installRetryFailedLine(mode) {
|
|
15025
|
+
const m = mode;
|
|
15026
|
+
const original = m.showError.bind(m);
|
|
15027
|
+
m.showError = (msg) => {
|
|
15028
|
+
const hit = typeof msg === "string" ? RETRY_FAILED.exec(msg) : null;
|
|
15029
|
+
if (!hit)
|
|
15030
|
+
return original(msg);
|
|
15031
|
+
const [, n, cause = ""] = hit;
|
|
15032
|
+
if (cause === "Retry cancelled")
|
|
15033
|
+
return m.showWarning("Retry cancelled.");
|
|
15034
|
+
const tries = `${n} ${n === "1" ? "try" : "tries"}`;
|
|
15035
|
+
m.showWarning(`Gave up after ${tries}: ${cause}`);
|
|
15036
|
+
};
|
|
15037
|
+
const warn = m.showWarning.bind(m);
|
|
15038
|
+
m.showWarning = (msg) => warn(typeof msg === "string" ? msg.replace(CRASH_WARNING, `${PRODUCT} crashed on `) : msg);
|
|
15039
|
+
for (const name of ["renderProjectTrustWarningIfNeeded", "suggestBugReport"]) {
|
|
15040
|
+
const orig = m[name]?.bind(m);
|
|
15041
|
+
if (!orig)
|
|
15042
|
+
continue;
|
|
15043
|
+
m[name] = () => {
|
|
15044
|
+
const before = m.chatContainer?.children?.length ?? 0;
|
|
15045
|
+
orig();
|
|
15046
|
+
for (const child of m.chatContainer?.children?.slice(before) ?? []) {
|
|
15047
|
+
const build = child.build;
|
|
15048
|
+
if (typeof build === "function")
|
|
15049
|
+
child.build = () => reword(build.call(child));
|
|
15050
|
+
}
|
|
15051
|
+
};
|
|
15052
|
+
}
|
|
15053
|
+
}
|
|
15054
|
+
function reword(text) {
|
|
15055
|
+
return text.replace(/restart pi\./, `restart ${PRODUCT}.`).replace(/looks like a pi bug/, "looks like a free-pi bug");
|
|
15056
|
+
}
|
|
15057
|
+
|
|
14150
15058
|
// src/project-trust.ts
|
|
14151
15059
|
import { createInterface } from "node:readline/promises";
|
|
14152
15060
|
import { resolve as resolve3 } from "node:path";
|
|
@@ -14293,14 +15201,20 @@ var SDK_TOOLS = [
|
|
|
14293
15201
|
"list_mcp_resource_templates",
|
|
14294
15202
|
"read_mcp_resource"
|
|
14295
15203
|
];
|
|
14296
|
-
var SKILLS_DIR = resolve4(
|
|
15204
|
+
var SKILLS_DIR = resolve4(dirname12(fileURLToPath4(import.meta.url)), "..", "skills");
|
|
14297
15205
|
function resolveModelName(opts) {
|
|
14298
15206
|
return opts.models?.[0]?.name || opts.model || MODEL_ID;
|
|
14299
15207
|
}
|
|
14300
15208
|
function buildSettingsManager(opts, models, projectTrusted) {
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
15209
|
+
const saved = readDefaultModel(opts.agentDir);
|
|
15210
|
+
const known = new Map([
|
|
15211
|
+
[PROVIDER_NAME, new Set([...models.map((m) => m.id), FUSION_PICKER_ID, ...models.some((m) => m.id === ULTRAFAST_CATALOG_ID) ? [ULTRAFAST_PICKER_ID] : []])],
|
|
15212
|
+
[DEFENSE_PROVIDER_NAME, new Set(defenseModelsFor(opts).map((m) => m.id))]
|
|
15213
|
+
]);
|
|
15214
|
+
const start = saved && known.get(saved.provider)?.has(saved.model) ? saved : undefined;
|
|
15215
|
+
const settings = {
|
|
15216
|
+
defaultProvider: start?.provider ?? PROVIDER_NAME,
|
|
15217
|
+
defaultModel: start?.model ?? models[0].id,
|
|
14304
15218
|
defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
|
|
14305
15219
|
packages: [],
|
|
14306
15220
|
quietStartup: true,
|
|
@@ -14310,7 +15224,10 @@ function buildSettingsManager(opts, models, projectTrusted) {
|
|
|
14310
15224
|
theme: "light/dark",
|
|
14311
15225
|
enableInstallTelemetry: false,
|
|
14312
15226
|
...compactionSettingsFor(opts.costArm) ? { compaction: compactionSettingsFor(opts.costArm) } : {}
|
|
14313
|
-
}
|
|
15227
|
+
};
|
|
15228
|
+
const storage = new DefaultModelStorage(opts.agentDir);
|
|
15229
|
+
storage.withLock("global", () => JSON.stringify(settings, null, 2));
|
|
15230
|
+
return SettingsManager2.fromStorage(storage, { projectTrusted });
|
|
14314
15231
|
}
|
|
14315
15232
|
function settingsForRuntime(opts, rtCwd, launch) {
|
|
14316
15233
|
return buildSettingsManager(opts, catalogModelsFor(opts), projectTrustForCwd(rtCwd, launch, opts.agentDir));
|
|
@@ -14334,7 +15251,6 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
14334
15251
|
name: "free-pi-provider",
|
|
14335
15252
|
factory: (pi) => registerProviders(pi, sessionId)
|
|
14336
15253
|
};
|
|
14337
|
-
let advertiserUrl;
|
|
14338
15254
|
const adsExtension = createAdsExtension({
|
|
14339
15255
|
registerMeterRefresh: (refresh) => auth.setOnSignedIn(() => void refresh().catch(() => {})),
|
|
14340
15256
|
baseUrl: opts.baseUrl,
|
|
@@ -14342,9 +15258,7 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
14342
15258
|
sessionId,
|
|
14343
15259
|
clientVersion: CLI_VERSION,
|
|
14344
15260
|
getGravityContext: opts.getGravityContext,
|
|
14345
|
-
|
|
14346
|
-
advertiserUrl = u;
|
|
14347
|
-
},
|
|
15261
|
+
adContext: opts.adContext,
|
|
14348
15262
|
onMe: (me) => {
|
|
14349
15263
|
updateAllowanceCache(me);
|
|
14350
15264
|
}
|
|
@@ -14398,7 +15312,6 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
14398
15312
|
const commandsExtension = createFreePiCommandsExtension({
|
|
14399
15313
|
baseUrl: opts.baseUrl,
|
|
14400
15314
|
getToken: () => auth.getJwt(),
|
|
14401
|
-
getAdvertiserUrl: () => advertiserUrl,
|
|
14402
15315
|
credentialsPath: opts.credentialsPath,
|
|
14403
15316
|
agentDir: opts.agentDir,
|
|
14404
15317
|
isGuest: () => auth.isGuest(),
|
|
@@ -14429,12 +15342,21 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
14429
15342
|
const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
|
|
14430
15343
|
const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
|
|
14431
15344
|
const hiveRoom = { open: false };
|
|
15345
|
+
const remoteState = { remoteTurn: false, attached: false };
|
|
14432
15346
|
const hiveExtension = createHiveExtension({
|
|
14433
15347
|
baseUrl: opts.baseUrl,
|
|
14434
15348
|
getToken: () => auth.getJwt(),
|
|
14435
15349
|
knownTools: allowedTools,
|
|
14436
15350
|
sandbox: shellSandbox,
|
|
14437
|
-
roomState: hiveRoom
|
|
15351
|
+
roomState: hiveRoom,
|
|
15352
|
+
isRemoteAttached: () => remoteState.attached
|
|
15353
|
+
});
|
|
15354
|
+
const remoteExtension = createRemoteExtension({
|
|
15355
|
+
baseUrl: opts.baseUrl,
|
|
15356
|
+
getToken: async () => auth.getJwt(),
|
|
15357
|
+
sessionId,
|
|
15358
|
+
state: remoteState,
|
|
15359
|
+
isHiveOpen: () => hiveRoom.open
|
|
14438
15360
|
});
|
|
14439
15361
|
const choiceExtension = choiceOn ? createChoiceToolExtension({
|
|
14440
15362
|
baseUrl: opts.baseUrl,
|
|
@@ -14442,7 +15364,7 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
14442
15364
|
sessionId,
|
|
14443
15365
|
arm: () => opts.choiceArm ?? null,
|
|
14444
15366
|
headless: !(tty.stdin && tty.stdout),
|
|
14445
|
-
isRoomOpen: () => hiveRoom.open
|
|
15367
|
+
isRoomOpen: () => hiveRoom.open || remoteState.remoteTurn
|
|
14446
15368
|
}) : undefined;
|
|
14447
15369
|
const subagentsExtension = createSubagentsExtension({
|
|
14448
15370
|
sessionId,
|
|
@@ -14487,6 +15409,7 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
14487
15409
|
toolSearchExtension,
|
|
14488
15410
|
subagentsExtension,
|
|
14489
15411
|
hiveExtension,
|
|
15412
|
+
remoteExtension,
|
|
14490
15413
|
...guestLoginExtension ? [guestLoginExtension] : [],
|
|
14491
15414
|
...opts.guest ? [createGuestNotesExtension({ isGuest: () => auth.isGuest() })] : []
|
|
14492
15415
|
];
|
|
@@ -14506,7 +15429,7 @@ ${CHOICE_RULES}` : agentRulesFor(opts.costArm), ...shellPromptLine(shell) ? [she
|
|
|
14506
15429
|
function getSessionDir(cwd, agentDir) {
|
|
14507
15430
|
const resolvedCwd = resolve4(cwd);
|
|
14508
15431
|
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
14509
|
-
return
|
|
15432
|
+
return join12(resolve4(agentDir), "sessions", safePath);
|
|
14510
15433
|
}
|
|
14511
15434
|
async function findLatestSession(cwd, sessionDir, api = SessionManager2) {
|
|
14512
15435
|
const infos = await api.list(cwd, sessionDir);
|
|
@@ -14608,11 +15531,13 @@ async function launchPi(opts) {
|
|
|
14608
15531
|
installLogoutHandler(mode, { credentialsPath: opts.credentialsPath, isGuest: () => auth.isGuest() });
|
|
14609
15532
|
installModelAllowanceHandler(mode);
|
|
14610
15533
|
installSafeUrlOpen(mode);
|
|
15534
|
+
installRetryFailedLine(mode);
|
|
14611
15535
|
installFooterRightAlign(mode, () => {
|
|
14612
15536
|
const id = runtime.session?.model?.id;
|
|
14613
15537
|
return freeModelBase(pickerModelName(opts, id));
|
|
14614
15538
|
});
|
|
14615
15539
|
installEditorPlaceholder(mode);
|
|
15540
|
+
installWidthGuard(mode);
|
|
14616
15541
|
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
14617
15542
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
14618
15543
|
installBottomPin(mode);
|
|
@@ -14665,7 +15590,7 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
|
|
|
14665
15590
|
// src/relaunch.ts
|
|
14666
15591
|
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, realpathSync as realpathSync5, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
|
|
14667
15592
|
import { constants as osConstants, tmpdir as tmpdir2 } from "node:os";
|
|
14668
|
-
import { delimiter, dirname as
|
|
15593
|
+
import { delimiter, dirname as dirname13, isAbsolute as isAbsolute3, join as join13, resolve as resolve5, sep as sep4, win32 } from "node:path";
|
|
14669
15594
|
function applyRelaunchCwd(env, chdir = (d) => process.chdir(d), isDir = (p) => {
|
|
14670
15595
|
try {
|
|
14671
15596
|
return statSync4(p).isDirectory();
|
|
@@ -14706,7 +15631,7 @@ function hasNodeModulesBin(entry) {
|
|
|
14706
15631
|
}
|
|
14707
15632
|
function sanitizedEnv(env, nodeDir, project, home, win) {
|
|
14708
15633
|
const proj = canon(project, win);
|
|
14709
|
-
const broad = proj === canon(home, win) ||
|
|
15634
|
+
const broad = proj === canon(home, win) || dirname13(proj) === proj;
|
|
14710
15635
|
const out = {};
|
|
14711
15636
|
let pathKey = "PATH";
|
|
14712
15637
|
let oldPath = "";
|
|
@@ -14743,13 +15668,13 @@ async function relaunchViaNpx(latest, deps) {
|
|
|
14743
15668
|
if (!latest || !SEMVER.test(latest))
|
|
14744
15669
|
return;
|
|
14745
15670
|
const target = latest;
|
|
14746
|
-
const nodeDir =
|
|
14747
|
-
const npxCli = win ?
|
|
15671
|
+
const nodeDir = dirname13(deps.execPath);
|
|
15672
|
+
const npxCli = win ? join13(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join13(dirname13(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
|
|
14748
15673
|
const exists = deps.exists ?? existsSync6;
|
|
14749
15674
|
const distroNpxCli = "/usr/share/nodejs/npm/bin/npx-cli.js";
|
|
14750
15675
|
const healthy = (cli) => {
|
|
14751
|
-
const root =
|
|
14752
|
-
return exists(
|
|
15676
|
+
const root = join13(dirname13(cli), "..");
|
|
15677
|
+
return exists(join13(root, "package.json")) && exists(join13(root, "lib", "cli.js"));
|
|
14753
15678
|
};
|
|
14754
15679
|
let npxCliPath = npxCli;
|
|
14755
15680
|
if (!exists(npxCli)) {
|
|
@@ -14761,8 +15686,8 @@ async function relaunchViaNpx(latest, deps) {
|
|
|
14761
15686
|
if (!healthy(npxCliPath))
|
|
14762
15687
|
return;
|
|
14763
15688
|
const childEnv = sanitizedEnv(env, nodeDir, deps.cwd, deps.homedir, win);
|
|
14764
|
-
const readyDir = mkdtempSync2(
|
|
14765
|
-
const readyFile =
|
|
15689
|
+
const readyDir = mkdtempSync2(join13(tmpdir2(), "free-pi-relaunch-"));
|
|
15690
|
+
const readyFile = join13(readyDir, "started");
|
|
14766
15691
|
try {
|
|
14767
15692
|
deps.log(`Updating free-pi to ${target}…`);
|
|
14768
15693
|
return await new Promise((resolve) => {
|
|
@@ -14804,13 +15729,13 @@ async function relaunchViaNpx(latest, deps) {
|
|
|
14804
15729
|
|
|
14805
15730
|
// src/run.ts
|
|
14806
15731
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
14807
|
-
import { mkdirSync as
|
|
15732
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
14808
15733
|
import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
|
|
14809
15734
|
|
|
14810
15735
|
// src/onboarding.ts
|
|
14811
15736
|
import { existsSync as existsSync7, readdirSync as readdirSync3, realpathSync as realpathSync6 } from "node:fs";
|
|
14812
15737
|
import { homedir as homedir8 } from "node:os";
|
|
14813
|
-
import { join as
|
|
15738
|
+
import { join as join14 } from "node:path";
|
|
14814
15739
|
var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
|
|
14815
15740
|
var INTRO_TEXT = "Tell free-pi what to build, fix, or explain. Resume later with: npx free-pi-cli@latest --continue";
|
|
14816
15741
|
function introSkipped(env = process.env) {
|
|
@@ -14830,7 +15755,7 @@ function looksLikeProject(cwd, home = homedir8()) {
|
|
|
14830
15755
|
try {
|
|
14831
15756
|
if (realpathSync6(cwd) === realpathSync6(home))
|
|
14832
15757
|
return false;
|
|
14833
|
-
if (PROJECT_MARKERS.some((m) => existsSync7(
|
|
15758
|
+
if (PROJECT_MARKERS.some((m) => existsSync7(join14(cwd, m))))
|
|
14834
15759
|
return true;
|
|
14835
15760
|
return readdirSync3(cwd).some((f) => /\.(sln|csproj)$/.test(f));
|
|
14836
15761
|
} catch {
|
|
@@ -14859,7 +15784,7 @@ ${text}
|
|
|
14859
15784
|
// src/starter.ts
|
|
14860
15785
|
import { existsSync as existsSync8, readdirSync as readdirSync4, realpathSync as realpathSync7 } from "node:fs";
|
|
14861
15786
|
import { homedir as homedir9 } from "node:os";
|
|
14862
|
-
import { join as
|
|
15787
|
+
import { join as join15 } from "node:path";
|
|
14863
15788
|
var STARTER_EXPERIMENT = "first_session_starter";
|
|
14864
15789
|
var STARTER_PROJECT = [
|
|
14865
15790
|
{
|
|
@@ -14892,7 +15817,7 @@ var STARTER_EMPTY = [
|
|
|
14892
15817
|
var STARTER_HEADER_PROJECT = "Not sure where to start? Press 1, 2 or 3 to send one of these, or any other key to skip:";
|
|
14893
15818
|
var STARTER_HEADER_EMPTY = "This folder is empty. Press 1, 2 or 3 to start with one of these, or any other key to skip:";
|
|
14894
15819
|
function getStarterMarkerPath(agentDir = getFreePiAgentDir()) {
|
|
14895
|
-
return
|
|
15820
|
+
return join15(agentDir, "starter-shown");
|
|
14896
15821
|
}
|
|
14897
15822
|
function starterShownBefore(agentDir) {
|
|
14898
15823
|
return existsSync8(getStarterMarkerPath(agentDir));
|
|
@@ -14924,12 +15849,12 @@ function parseStarterKey(raw, count) {
|
|
|
14924
15849
|
}
|
|
14925
15850
|
|
|
14926
15851
|
// src/whats-new.ts
|
|
14927
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
14928
|
-
import { dirname as
|
|
15852
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
15853
|
+
import { dirname as dirname14 } from "node:path";
|
|
14929
15854
|
function maybeWhatsNew(cliVersion, agentDir) {
|
|
14930
15855
|
try {
|
|
14931
15856
|
const path = getLastSeenVersionPath(agentDir);
|
|
14932
|
-
const lastSeen = existsSync9(path) ?
|
|
15857
|
+
const lastSeen = existsSync9(path) ? readFileSync7(path, "utf-8").trim() : undefined;
|
|
14933
15858
|
writeLastSeen(path, cliVersion);
|
|
14934
15859
|
if (lastSeen === undefined)
|
|
14935
15860
|
return;
|
|
@@ -14945,8 +15870,8 @@ function maybeWhatsNew(cliVersion, agentDir) {
|
|
|
14945
15870
|
}
|
|
14946
15871
|
}
|
|
14947
15872
|
function writeLastSeen(path, version) {
|
|
14948
|
-
|
|
14949
|
-
|
|
15873
|
+
mkdirSync5(dirname14(path), { recursive: true });
|
|
15874
|
+
writeFileSync5(path, version, "utf-8");
|
|
14950
15875
|
}
|
|
14951
15876
|
|
|
14952
15877
|
// src/run.ts
|
|
@@ -15197,7 +16122,7 @@ Your free-pi sign-in has expired. Signing in with GitHub again.
|
|
|
15197
16122
|
deps.log(SIGNIN_UNFINISHED_LINE);
|
|
15198
16123
|
return 0;
|
|
15199
16124
|
}
|
|
15200
|
-
const reason = err instanceof Error && err.message ? err.message : "unknown error";
|
|
16125
|
+
const reason = (err instanceof Error && err.message ? err.message : "unknown error").replace(/\.$/, "");
|
|
15201
16126
|
deps.log(`Couldn't finish GitHub sign-in: ${reason}. Run npx free-pi-cli again to retry.`);
|
|
15202
16127
|
return 1;
|
|
15203
16128
|
}
|
|
@@ -15310,6 +16235,7 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
|
|
|
15310
16235
|
getGravityContext = deps.startGravityPixel?.(jwt);
|
|
15311
16236
|
} catch {}
|
|
15312
16237
|
}
|
|
16238
|
+
const adContext = consentCurrent && process.env.FREEPI_NO_GRAVITY_PIXEL !== "1" ? { redact: redactSecrets } : undefined;
|
|
15313
16239
|
if (me)
|
|
15314
16240
|
updateAllowanceCache(me);
|
|
15315
16241
|
guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
|
|
@@ -15393,8 +16319,8 @@ ${r.text}
|
|
|
15393
16319
|
const choices = starterChoices();
|
|
15394
16320
|
if (choices) {
|
|
15395
16321
|
try {
|
|
15396
|
-
|
|
15397
|
-
|
|
16322
|
+
mkdirSync6(deps.agentDir, { recursive: true });
|
|
16323
|
+
writeFileSync6(getStarterMarkerPath(deps.agentDir), "1");
|
|
15398
16324
|
} catch {}
|
|
15399
16325
|
const report = (event) => deps.reportStarterEvent ? deps.reportStarterEvent(event) : reportStarterEvent(deps, jwt, event);
|
|
15400
16326
|
deps.log(formatStarterMenu(choices));
|
|
@@ -15435,6 +16361,7 @@ ${r.text}
|
|
|
15435
16361
|
costArm: versionCheck.costArm,
|
|
15436
16362
|
updateLatest,
|
|
15437
16363
|
getGravityContext,
|
|
16364
|
+
adContext,
|
|
15438
16365
|
guest: Boolean(me?.guest),
|
|
15439
16366
|
refillCard: refillSession,
|
|
15440
16367
|
allowanceLine: allowanceLine(),
|
|
@@ -15547,7 +16474,7 @@ async function main() {
|
|
|
15547
16474
|
}
|
|
15548
16475
|
if (process.env.FREEPI_RELAUNCH_READY) {
|
|
15549
16476
|
try {
|
|
15550
|
-
|
|
16477
|
+
writeFileSync7(process.env.FREEPI_RELAUNCH_READY, "");
|
|
15551
16478
|
} catch {}
|
|
15552
16479
|
}
|
|
15553
16480
|
const command = parseCliArgs(process.argv.slice(2));
|
|
@@ -15608,7 +16535,7 @@ ${HELP_TEXT}`);
|
|
|
15608
16535
|
launchPi,
|
|
15609
16536
|
log: (message) => console.log(message),
|
|
15610
16537
|
checkClientVersion: (baseUrl, cliVersion, jwt) => checkClientVersion(baseUrl, cliVersion, fetch, jwt),
|
|
15611
|
-
startGravityPixel: (jwt) => startGravityPixel(jwt,
|
|
16538
|
+
startGravityPixel: (jwt) => startGravityPixel(jwt, join16(dirname15(agentDir), "gravity")),
|
|
15612
16539
|
relaunchLatest: (latest) => relaunchViaNpx(latest, {
|
|
15613
16540
|
env: process.env,
|
|
15614
16541
|
argv: process.argv.slice(2),
|