free-pi-cli 0.6.14 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.js +1 -1
- package/dist/{main-j1ey9ghj.js → main-gckhcg3t.js} +1926 -1045
- package/package.json +9 -2
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
4
|
import { spawn as spawn3 } from "node:child_process";
|
|
5
|
-
import { realpathSync as realpathSync8, writeFileSync as
|
|
6
|
-
import { homedir as
|
|
7
|
-
import { dirname as
|
|
5
|
+
import { realpathSync as realpathSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6
|
+
import { homedir as homedir10 } from "node:os";
|
|
7
|
+
import { dirname as dirname14, join as join15 } 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.16",
|
|
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",
|
|
@@ -530,6 +530,13 @@ var package_default = {
|
|
|
530
530
|
],
|
|
531
531
|
homepage: "https://freepi.ai",
|
|
532
532
|
license: "MIT",
|
|
533
|
+
repository: {
|
|
534
|
+
type: "git",
|
|
535
|
+
url: "git+https://github.com/dennisonbertram/free-pi-cli.git"
|
|
536
|
+
},
|
|
537
|
+
bugs: {
|
|
538
|
+
url: "https://github.com/dennisonbertram/free-pi-cli/issues"
|
|
539
|
+
},
|
|
533
540
|
type: "module",
|
|
534
541
|
bin: {
|
|
535
542
|
"free-pi-cli": "dist/index.js"
|
|
@@ -543,7 +550,7 @@ var package_default = {
|
|
|
543
550
|
node: ">=22.15.0"
|
|
544
551
|
},
|
|
545
552
|
scripts: {
|
|
546
|
-
build: "
|
|
553
|
+
build: "bun scripts/build.ts",
|
|
547
554
|
prepublishOnly: "bun run build"
|
|
548
555
|
},
|
|
549
556
|
dependencies: {
|
|
@@ -605,13 +612,13 @@ function splitCatalogModels(models) {
|
|
|
605
612
|
defense: models.filter((m) => m.section === "defense")
|
|
606
613
|
};
|
|
607
614
|
}
|
|
608
|
-
function buildProviderConfig(baseUrl, jwt, sessionId, models, name = PROVIDER_NAME) {
|
|
615
|
+
function buildProviderConfig(baseUrl, jwt, sessionId, models, name = PROVIDER_NAME, extraHeaders = {}) {
|
|
609
616
|
return {
|
|
610
617
|
name,
|
|
611
618
|
baseUrl: completionsBaseUrl(baseUrl),
|
|
612
619
|
apiKey: jwt,
|
|
613
620
|
api: "openai-completions",
|
|
614
|
-
headers: { "x-session-id": sessionId, "x-client-version": CLI_VERSION },
|
|
621
|
+
headers: { "x-session-id": sessionId, "x-client-version": CLI_VERSION, ...extraHeaders },
|
|
615
622
|
models: models.map((m) => ({
|
|
616
623
|
id: m.id,
|
|
617
624
|
name: m.name,
|
|
@@ -1057,6 +1064,9 @@ function getLastTipPath(agentDir = getFreePiAgentDir()) {
|
|
|
1057
1064
|
function getConfigPath() {
|
|
1058
1065
|
return join2(homedir(), FREE_PI_DIR_NAME, "config.json");
|
|
1059
1066
|
}
|
|
1067
|
+
function getToolModePath(agentDir = getFreePiAgentDir()) {
|
|
1068
|
+
return join2(agentDir, "tools-mode");
|
|
1069
|
+
}
|
|
1060
1070
|
|
|
1061
1071
|
// src/tips.ts
|
|
1062
1072
|
var TIPS = [
|
|
@@ -1787,7 +1797,6 @@ var string = (params) => {
|
|
|
1787
1797
|
const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`;
|
|
1788
1798
|
return new RegExp(`^${regex}$`);
|
|
1789
1799
|
};
|
|
1790
|
-
var bigint = /^-?\d+n?$/;
|
|
1791
1800
|
var integer = /^-?\d+$/;
|
|
1792
1801
|
var number = /^-?\d+(?:\.\d+)?$/;
|
|
1793
1802
|
var boolean = /^(?:true|false)$/i;
|
|
@@ -2692,25 +2701,6 @@ var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
|
|
|
2692
2701
|
return payload;
|
|
2693
2702
|
};
|
|
2694
2703
|
});
|
|
2695
|
-
var $ZodBigInt = /* @__PURE__ */ $constructor("$ZodBigInt", (inst, def) => {
|
|
2696
|
-
$ZodType.init(inst, def);
|
|
2697
|
-
inst._zod.pattern = bigint;
|
|
2698
|
-
inst._zod.parse = (payload, _ctx) => {
|
|
2699
|
-
if (def.coerce)
|
|
2700
|
-
try {
|
|
2701
|
-
payload.value = BigInt(payload.value);
|
|
2702
|
-
} catch (_) {}
|
|
2703
|
-
if (typeof payload.value === "bigint")
|
|
2704
|
-
return payload;
|
|
2705
|
-
payload.issues.push({
|
|
2706
|
-
expected: "bigint",
|
|
2707
|
-
code: "invalid_type",
|
|
2708
|
-
input: payload.value,
|
|
2709
|
-
inst
|
|
2710
|
-
});
|
|
2711
|
-
return payload;
|
|
2712
|
-
};
|
|
2713
|
-
});
|
|
2714
2704
|
var $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
|
|
2715
2705
|
$ZodType.init(inst, def);
|
|
2716
2706
|
inst._zod.parse = (payload) => payload;
|
|
@@ -2727,29 +2717,6 @@ var $ZodNever = /* @__PURE__ */ $constructor("$ZodNever", (inst, def) => {
|
|
|
2727
2717
|
return payload;
|
|
2728
2718
|
};
|
|
2729
2719
|
});
|
|
2730
|
-
var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
|
|
2731
|
-
$ZodType.init(inst, def);
|
|
2732
|
-
inst._zod.parse = (payload, _ctx) => {
|
|
2733
|
-
if (def.coerce) {
|
|
2734
|
-
try {
|
|
2735
|
-
payload.value = new Date(payload.value);
|
|
2736
|
-
} catch (_err) {}
|
|
2737
|
-
}
|
|
2738
|
-
const input = payload.value;
|
|
2739
|
-
const isDate = input instanceof Date;
|
|
2740
|
-
const isValidDate = isDate && !Number.isNaN(input.getTime());
|
|
2741
|
-
if (isValidDate)
|
|
2742
|
-
return payload;
|
|
2743
|
-
payload.issues.push({
|
|
2744
|
-
expected: "date",
|
|
2745
|
-
code: "invalid_type",
|
|
2746
|
-
input,
|
|
2747
|
-
...isDate ? { received: "Invalid Date" } : {},
|
|
2748
|
-
inst
|
|
2749
|
-
});
|
|
2750
|
-
return payload;
|
|
2751
|
-
};
|
|
2752
|
-
});
|
|
2753
2720
|
function handleArrayResult(result, final, index) {
|
|
2754
2721
|
if (result.issues.length) {
|
|
2755
2722
|
final.issues.push(...prefixIssues(index, result.issues));
|
|
@@ -4039,12 +4006,6 @@ function _boolean(Class, params) {
|
|
|
4039
4006
|
...normalizeParams(params)
|
|
4040
4007
|
});
|
|
4041
4008
|
}
|
|
4042
|
-
function _bigint(Class, params) {
|
|
4043
|
-
return new Class({
|
|
4044
|
-
type: "bigint",
|
|
4045
|
-
...normalizeParams(params)
|
|
4046
|
-
});
|
|
4047
|
-
}
|
|
4048
4009
|
function _unknown(Class) {
|
|
4049
4010
|
return new Class({
|
|
4050
4011
|
type: "unknown"
|
|
@@ -4056,12 +4017,6 @@ function _never(Class, params) {
|
|
|
4056
4017
|
...normalizeParams(params)
|
|
4057
4018
|
});
|
|
4058
4019
|
}
|
|
4059
|
-
function _date(Class, params) {
|
|
4060
|
-
return new Class({
|
|
4061
|
-
type: "date",
|
|
4062
|
-
...normalizeParams(params)
|
|
4063
|
-
});
|
|
4064
|
-
}
|
|
4065
4020
|
function _lt(value, params) {
|
|
4066
4021
|
return new $ZodCheckLessThan({
|
|
4067
4022
|
check: "less_than",
|
|
@@ -4662,20 +4617,10 @@ var numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
4662
4617
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
4663
4618
|
json.type = "boolean";
|
|
4664
4619
|
};
|
|
4665
|
-
var bigintProcessor = (_schema, ctx, _json, _params) => {
|
|
4666
|
-
if (ctx.unrepresentable === "throw") {
|
|
4667
|
-
throw new Error("BigInt cannot be represented in JSON Schema");
|
|
4668
|
-
}
|
|
4669
|
-
};
|
|
4670
4620
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
4671
4621
|
json.not = {};
|
|
4672
4622
|
};
|
|
4673
4623
|
var unknownProcessor = (_schema, _ctx, _json, _params) => {};
|
|
4674
|
-
var dateProcessor = (_schema, ctx, _json, _params) => {
|
|
4675
|
-
if (ctx.unrepresentable === "throw") {
|
|
4676
|
-
throw new Error("Date cannot be represented in JSON Schema");
|
|
4677
|
-
}
|
|
4678
|
-
};
|
|
4679
4624
|
var enumProcessor = (schema, _ctx, json, _params) => {
|
|
4680
4625
|
const def = schema._zod.def;
|
|
4681
4626
|
const values = getEnumValues(def.entries);
|
|
@@ -5406,31 +5351,6 @@ var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
|
|
|
5406
5351
|
function boolean2(params) {
|
|
5407
5352
|
return _boolean(ZodBoolean, params);
|
|
5408
5353
|
}
|
|
5409
|
-
var ZodBigInt = /* @__PURE__ */ $constructor("ZodBigInt", (inst, def) => {
|
|
5410
|
-
$ZodBigInt.init(inst, def);
|
|
5411
|
-
ZodType.init(inst, def);
|
|
5412
|
-
inst._zod.processJSONSchema = (ctx, json, params) => bigintProcessor(inst, ctx, json, params);
|
|
5413
|
-
inst.gte = (value, params) => inst.check(_gte(value, params));
|
|
5414
|
-
inst.min = (value, params) => inst.check(_gte(value, params));
|
|
5415
|
-
inst.gt = (value, params) => inst.check(_gt(value, params));
|
|
5416
|
-
inst.gte = (value, params) => inst.check(_gte(value, params));
|
|
5417
|
-
inst.min = (value, params) => inst.check(_gte(value, params));
|
|
5418
|
-
inst.lt = (value, params) => inst.check(_lt(value, params));
|
|
5419
|
-
inst.lte = (value, params) => inst.check(_lte(value, params));
|
|
5420
|
-
inst.max = (value, params) => inst.check(_lte(value, params));
|
|
5421
|
-
inst.positive = (params) => inst.check(_gt(BigInt(0), params));
|
|
5422
|
-
inst.negative = (params) => inst.check(_lt(BigInt(0), params));
|
|
5423
|
-
inst.nonpositive = (params) => inst.check(_lte(BigInt(0), params));
|
|
5424
|
-
inst.nonnegative = (params) => inst.check(_gte(BigInt(0), params));
|
|
5425
|
-
inst.multipleOf = (value, params) => inst.check(_multipleOf(value, params));
|
|
5426
|
-
const bag = inst._zod.bag;
|
|
5427
|
-
inst.minValue = bag.minimum ?? null;
|
|
5428
|
-
inst.maxValue = bag.maximum ?? null;
|
|
5429
|
-
inst.format = bag.format ?? null;
|
|
5430
|
-
});
|
|
5431
|
-
function bigint2(params) {
|
|
5432
|
-
return _bigint(ZodBigInt, params);
|
|
5433
|
-
}
|
|
5434
5354
|
var ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
|
|
5435
5355
|
$ZodUnknown.init(inst, def);
|
|
5436
5356
|
ZodType.init(inst, def);
|
|
@@ -5447,19 +5367,6 @@ var ZodNever = /* @__PURE__ */ $constructor("ZodNever", (inst, def) => {
|
|
|
5447
5367
|
function never(params) {
|
|
5448
5368
|
return _never(ZodNever, params);
|
|
5449
5369
|
}
|
|
5450
|
-
var ZodDate = /* @__PURE__ */ $constructor("ZodDate", (inst, def) => {
|
|
5451
|
-
$ZodDate.init(inst, def);
|
|
5452
|
-
ZodType.init(inst, def);
|
|
5453
|
-
inst._zod.processJSONSchema = (ctx, json, params) => dateProcessor(inst, ctx, json, params);
|
|
5454
|
-
inst.min = (value, params) => inst.check(_gte(value, params));
|
|
5455
|
-
inst.max = (value, params) => inst.check(_lte(value, params));
|
|
5456
|
-
const c = inst._zod.bag;
|
|
5457
|
-
inst.minDate = c.minimum ? new Date(c.minimum) : null;
|
|
5458
|
-
inst.maxDate = c.maximum ? new Date(c.maximum) : null;
|
|
5459
|
-
});
|
|
5460
|
-
function date3(params) {
|
|
5461
|
-
return _date(ZodDate, params);
|
|
5462
|
-
}
|
|
5463
5370
|
var ZodArray = /* @__PURE__ */ $constructor("ZodArray", (inst, def) => {
|
|
5464
5371
|
$ZodArray.init(inst, def);
|
|
5465
5372
|
ZodType.init(inst, def);
|
|
@@ -5885,8 +5792,10 @@ var GithubDeviceResponseSchema = object({
|
|
|
5885
5792
|
expires_in: number2().optional(),
|
|
5886
5793
|
verification_uri_complete: string2().optional()
|
|
5887
5794
|
});
|
|
5795
|
+
var DeviceReportShape = { timezone: unknown().optional(), locale: unknown().optional() };
|
|
5888
5796
|
var AuthTokenRequestSchema = object({
|
|
5889
|
-
session_id: string2().uuid()
|
|
5797
|
+
session_id: string2().uuid(),
|
|
5798
|
+
...DeviceReportShape
|
|
5890
5799
|
});
|
|
5891
5800
|
var AuthTokenPendingSchema = object({
|
|
5892
5801
|
status: literal("pending"),
|
|
@@ -5936,7 +5845,8 @@ var GuestStartRequestSchema = object({
|
|
|
5936
5845
|
consent_version: string2().min(1).max(32),
|
|
5937
5846
|
signup_src: string2().optional(),
|
|
5938
5847
|
referral_code: string2().optional(),
|
|
5939
|
-
client: object({ os: string2().max(64).optional(), cli_version: string2().max(64).optional() }).optional()
|
|
5848
|
+
client: object({ os: string2().max(64).optional(), cli_version: string2().max(64).optional() }).optional(),
|
|
5849
|
+
...DeviceReportShape
|
|
5940
5850
|
});
|
|
5941
5851
|
var ExperimentVariantSchema = _enum(["control", "treatment"]);
|
|
5942
5852
|
var ExperimentAssignmentSchema = object({
|
|
@@ -6003,6 +5913,7 @@ var EarnRewardSurfaceSchema = object({
|
|
|
6003
5913
|
remaining_micros: string2(),
|
|
6004
5914
|
tokens_about: number2().optional()
|
|
6005
5915
|
});
|
|
5916
|
+
var SlowTierSurfaceSchema = object({ until: string2(), line: string2() });
|
|
6006
5917
|
var MeResponseSchema = object({
|
|
6007
5918
|
user_id: string2(),
|
|
6008
5919
|
handle: string2(),
|
|
@@ -6036,8 +5947,10 @@ var MeResponseSchema = object({
|
|
|
6036
5947
|
return_bonus: ReturnBonusSurfaceSchema.optional(),
|
|
6037
5948
|
streak: StreakSurfaceSchema.optional(),
|
|
6038
5949
|
visible_allowance: boolean2().optional(),
|
|
5950
|
+
choice_prompts_arm: _enum(["control", "treatment"]).optional(),
|
|
6039
5951
|
earn_reward: EarnRewardSurfaceSchema.nullable().optional(),
|
|
6040
|
-
active_perks: array(object({ kind: string2(), models: array(string2()), ends_at: string2() })).optional()
|
|
5952
|
+
active_perks: array(object({ kind: string2(), models: array(string2()), ends_at: string2() })).optional(),
|
|
5953
|
+
slow_tier: SlowTierSurfaceSchema.optional()
|
|
6041
5954
|
});
|
|
6042
5955
|
var MeStatsResponseSchema = object({
|
|
6043
5956
|
user_id: string2(),
|
|
@@ -6063,6 +5976,7 @@ var MeStatsResponseSchema = object({
|
|
|
6063
5976
|
credit_tiers: boolean2().optional(),
|
|
6064
5977
|
premium_get_line: string2().optional(),
|
|
6065
5978
|
use_bonus: string2().optional(),
|
|
5979
|
+
slow_tier: SlowTierSurfaceSchema.optional(),
|
|
6066
5980
|
request_count_today: number2(),
|
|
6067
5981
|
prompt_tokens_today: number2(),
|
|
6068
5982
|
completion_tokens_today: number2(),
|
|
@@ -6164,501 +6078,12 @@ var ReferralStatusResponseSchema = object({
|
|
|
6164
6078
|
var ReferralLeaderboardResponseSchema = object({
|
|
6165
6079
|
entries: array(object({ rank: number2(), qualified_count: number2(), handle: string2().nullable() }))
|
|
6166
6080
|
});
|
|
6081
|
+
|
|
6167
6082
|
// ../shared/src/errors.ts
|
|
6168
6083
|
function assertExhaustiveErrorCode(code) {
|
|
6169
6084
|
throw new Error(`unhandled error code: ${String(code)}`);
|
|
6170
6085
|
}
|
|
6171
|
-
|
|
6172
|
-
function usdToMicros(value) {
|
|
6173
|
-
const s = typeof value === "number" ? value.toFixed(6) : String(value).trim();
|
|
6174
|
-
const negative = s.startsWith("-");
|
|
6175
|
-
const [whole = "0", frac = ""] = (negative ? s.slice(1) : s).split(".");
|
|
6176
|
-
const micros = BigInt(whole || "0") * 1000000n + BigInt(`${frac}000000`.slice(0, 6));
|
|
6177
|
-
return negative ? -micros : micros;
|
|
6178
|
-
}
|
|
6179
|
-
|
|
6180
|
-
// ../shared/src/config.ts
|
|
6181
|
-
var ConfigSchema = object({
|
|
6182
|
-
perUserDailyCapUsd: number2().default(1.125),
|
|
6183
|
-
perUserRollingCapUsd: number2().default(0),
|
|
6184
|
-
rollingCapWindowHours: number2().int().default(5),
|
|
6185
|
-
globalDailyCapUsd: number2().default(50),
|
|
6186
|
-
contextCharBudget: number2().int().default(1200000),
|
|
6187
|
-
maxOutputTokens: number2().int().min(1).default(32768),
|
|
6188
|
-
accountsPerDayCeiling: number2().int().default(500),
|
|
6189
|
-
authRatePerIpPerHour: number2().int().default(30),
|
|
6190
|
-
siteLoginStartRateLimitEnabled: boolean2().default(false),
|
|
6191
|
-
siteLoginStartPerIp: number2().int().min(0).default(5),
|
|
6192
|
-
siteLoginStartPerSubject: number2().int().min(0).default(5),
|
|
6193
|
-
siteLoginStartWindowSeconds: number2().int().min(1).default(600),
|
|
6194
|
-
siteLoginStartCooldownSeconds: number2().int().min(0).default(60),
|
|
6195
|
-
webBuyUrlRatePerProviderPerHour: number2().int().default(20),
|
|
6196
|
-
handoffTokenRatePerProviderPerHour: number2().int().default(60),
|
|
6197
|
-
handoffHourlyCompletionCap: number2().int().default(120),
|
|
6198
|
-
tokenRatePerIpPerHour: number2().int().default(3600),
|
|
6199
|
-
consentRatePerIpPerHour: number2().int().default(60),
|
|
6200
|
-
adInlineTurnFrequency: number2().int().default(5),
|
|
6201
|
-
adMinColumns: number2().int().default(60),
|
|
6202
|
-
proxyBaseUrl: string2().default("https://api.freepi.ai"),
|
|
6203
|
-
modelId: string2().default("deepseek/deepseek-v4-flash"),
|
|
6204
|
-
modelDisplay: string2().optional(),
|
|
6205
|
-
modelNotice: string2().optional(),
|
|
6206
|
-
providerDataCollection: _enum(["allow", "deny"]).optional(),
|
|
6207
|
-
promptCacheEnabled: boolean2().default(false),
|
|
6208
|
-
emailCaptureEnabled: boolean2().default(false),
|
|
6209
|
-
lifecycleEmailExperimentEnabled: boolean2().default(false),
|
|
6210
|
-
lifecycleEmailEnabled: boolean2().default(false),
|
|
6211
|
-
lifecycleEmailSend: boolean2().default(false),
|
|
6212
|
-
emailProviderConfigured: boolean2().default(false),
|
|
6213
|
-
emailFrom: string2().optional(),
|
|
6214
|
-
emailReplyTo: string2().optional(),
|
|
6215
|
-
emailPostalAddress: string2().optional(),
|
|
6216
|
-
emailProviderUrl: string2().optional(),
|
|
6217
|
-
hiveEnabled: boolean2().default(false),
|
|
6218
|
-
earnEnabled: boolean2().default(false),
|
|
6219
|
-
earnShare: number2().min(0).max(1).default(0.1),
|
|
6220
|
-
earnBudgetMicros: bigint2().default(300000000n),
|
|
6221
|
-
earnTestGithubIds: array(string2().regex(/^\d{1,20}$/)).default([]),
|
|
6222
|
-
providerOrder: array(string2()).optional(),
|
|
6223
|
-
openrouterFlashProviderOrder: array(string2()).default([
|
|
6224
|
-
"streamlake/fp8",
|
|
6225
|
-
"deepinfra/fp8",
|
|
6226
|
-
"gmicloud/fp8",
|
|
6227
|
-
"digitalocean"
|
|
6228
|
-
]),
|
|
6229
|
-
openrouterFlashProviderIgnore: array(string2()).default(["baidu", "relace"]),
|
|
6230
|
-
openrouterFlashMaxPricePrompt: string2().default("0.12"),
|
|
6231
|
-
openrouterFlashMaxPriceCompletion: string2().default("0.25"),
|
|
6232
|
-
openrouterSessionPinTtlSeconds: number2().int().min(1).default(1800),
|
|
6233
|
-
costAlertMultiplier: number2().int().min(1).default(2),
|
|
6234
|
-
costAlertMinHours: number2().int().min(1).default(7),
|
|
6235
|
-
tierNewMaxDays: number2().int().default(30),
|
|
6236
|
-
tierYoungMaxDays: number2().int().default(365),
|
|
6237
|
-
tierCapNewUsd: number2().default(0.45),
|
|
6238
|
-
tierCapYoungUsd: number2().default(1.125),
|
|
6239
|
-
fallbackLaneUsd: number2().default(0.225),
|
|
6240
|
-
fallbackHourlyCap: number2().int().default(10),
|
|
6241
|
-
fallbackLaneMinHeadroomBps: number2().int().default(1000),
|
|
6242
|
-
queueWaitRetryAfterSeconds: number2().int().default(0),
|
|
6243
|
-
sessionLeaseTtlMinutes: number2().int().default(30),
|
|
6244
|
-
signedInMaxCliSessions: number2().int().min(1).default(4),
|
|
6245
|
-
shutdownDrainMs: number2().int().min(0).max(25000).default(20000),
|
|
6246
|
-
premiumRatioThreshold: number2().int().min(1).default(5),
|
|
6247
|
-
fallbackExpensiveRatio: number2().int().min(1).default(5),
|
|
6248
|
-
larprouterTrialModels: array(string2()).default([]),
|
|
6249
|
-
larprouterTrialSharePct: number2().int().min(0).max(100).default(0),
|
|
6250
|
-
larprouterBaseUrl: string2().default("https://larprouter.com/v1"),
|
|
6251
|
-
larprouterFallbackModels: array(string2()).default([]),
|
|
6252
|
-
larprouterFallbackTerminal: boolean2().default(false),
|
|
6253
|
-
larprouterTtftMs: number2().int().min(1).default(8000),
|
|
6254
|
-
larprouterPrices: record(string2(), object({ input: string2(), cacheRead: string2(), output: string2() })).default({ "deepseek-v4.1-flash-1017-non-abliterated": { input: "80000", cacheRead: "10000", output: "350000" } }),
|
|
6255
|
-
larprouterModelMap: record(string2(), string2()).default({
|
|
6256
|
-
"deepseek-v4-flash": "deepseek-v4.1-flash-1017-non-abliterated",
|
|
6257
|
-
"deepseek-v4.1-flash": "deepseek-v4.1-flash-1017-non-abliterated"
|
|
6258
|
-
}),
|
|
6259
|
-
experientialBaseUrl: string2().default("https://api.experientiallabs.ai/v1"),
|
|
6260
|
-
experientialModelMap: record(string2(), string2()).default({
|
|
6261
|
-
"deepseek-v4.1-flash": "deepseek-v4.1-flash-1017",
|
|
6262
|
-
"deepseek-v4-flash": "deepseek-v4.1-flash-1017",
|
|
6263
|
-
"gpt-6-luna": "gpt-6-luna"
|
|
6264
|
-
}),
|
|
6265
|
-
experientialPrimaryModels: array(string2()).default([]),
|
|
6266
|
-
experientialFallbackModels: array(string2()).default([]),
|
|
6267
|
-
experientialTtftMs: number2().int().min(1).default(1e4),
|
|
6268
|
-
experientialMaxCostUsdPerRequest: string2().default("0.0005"),
|
|
6269
|
-
experientialPriceCooldownMs: number2().int().min(1).default(3600000),
|
|
6270
|
-
particleEnabled: boolean2().default(false),
|
|
6271
|
-
particleBaseUrl: string2().default("https://api.particle.ai/v1"),
|
|
6272
|
-
particleModelMap: record(string2(), string2()).default({
|
|
6273
|
-
particle: "particle-1",
|
|
6274
|
-
"deepseek-v4.1-flash": "deepseek-ai/deepseek-v41-flash-1017",
|
|
6275
|
-
"glm-5.3-flash": "z-ai/glm-5.3-flash"
|
|
6276
|
-
}),
|
|
6277
|
-
particleTtftMs: number2().int().min(1).default(1e4),
|
|
6278
|
-
minCliVersion: string2().default("0.0.0"),
|
|
6279
|
-
latestCliVersion: string2().default("0.2.0"),
|
|
6280
|
-
minDesktopVersion: string2().default("0.0.0"),
|
|
6281
|
-
latestDesktopVersion: string2().default("0.0.0"),
|
|
6282
|
-
desktopDownloadUrl: string2().default(""),
|
|
6283
|
-
minWebVersion: string2().default("0.0.0"),
|
|
6284
|
-
latestWebVersion: string2().default("0.0.0"),
|
|
6285
|
-
workspaceMaxStreams: number2().int().min(1).default(1),
|
|
6286
|
-
gravityDesktopEnabled: boolean2().default(false),
|
|
6287
|
-
abuseWindowMinutes: number2().int().default(30),
|
|
6288
|
-
abuseSettleGraceSeconds: number2().int().default(120),
|
|
6289
|
-
abuseGraceSettledStarts: number2().int().default(2),
|
|
6290
|
-
abuseUnverifiedMinStarts: number2().int().default(3),
|
|
6291
|
-
abuseUnverifiedMinFinishes: number2().int().default(2),
|
|
6292
|
-
abuseUnverifiedRestrictHours: number2().int().default(6),
|
|
6293
|
-
abuseLikelyBypassMinStarts: number2().int().default(5),
|
|
6294
|
-
abuseLikelyBypassRestrictHours: number2().int().default(24),
|
|
6295
|
-
abuseRepeatWindowDays: number2().int().default(7),
|
|
6296
|
-
abuseRepeatMinOccurrences: number2().int().default(3),
|
|
6297
|
-
abuseRestrictedCapUsd: number2().default(0.5),
|
|
6298
|
-
abuseRestrictedHourlyCap: number2().int().default(5),
|
|
6299
|
-
abuseNormalHourlyCap: number2().int().default(30),
|
|
6300
|
-
abuseSignedInNormalHourlyCap: number2().int().default(0),
|
|
6301
|
-
abuseDay0SkipHourly: boolean2().default(true),
|
|
6302
|
-
abuseFlipAlertThreshold: number2().int().default(10),
|
|
6303
|
-
abuseAdBypassMinRequests: number2().int().default(20),
|
|
6304
|
-
abuseAdBypassRatio: number2().int().default(10),
|
|
6305
|
-
traceMaxInflightWrites: number2().int().default(32),
|
|
6306
|
-
catalogCacheTtlSeconds: number2().int().default(60),
|
|
6307
|
-
eventMaxInflightWrites: number2().int().default(128),
|
|
6308
|
-
guestEnabled: boolean2().default(false),
|
|
6309
|
-
guestAllowanceMicros: bigint2().default(30000n),
|
|
6310
|
-
guestGlobalDailyCapMicros: bigint2().default(3000000n),
|
|
6311
|
-
guestHoldMicros: bigint2().default(5000n),
|
|
6312
|
-
guestIpSessionsPerDay: number2().int().default(8),
|
|
6313
|
-
guestIpAllowanceMicrosPerDay: bigint2().default(240000n),
|
|
6314
|
-
guestMaxConcurrentStreams: number2().int().default(10),
|
|
6315
|
-
guestMinRequestHeadroomMicros: bigint2().default(5000n),
|
|
6316
|
-
guestIpHashRetentionDays: number2().int().default(7),
|
|
6317
|
-
returnBonusDay2Credits: number2().int().default(2000),
|
|
6318
|
-
returnBonusDay7Credits: number2().int().default(1000),
|
|
6319
|
-
guestGithubBonusCredits: number2().int().default(1e4),
|
|
6320
|
-
returnNudgeEmailEnabled: boolean2().default(false),
|
|
6321
|
-
referralBonusCreditsPerDay: number2().int().default(5000),
|
|
6322
|
-
referralBonusDays: number2().int().default(30),
|
|
6323
|
-
referralMaxActivePerReferrer: number2().int().default(10),
|
|
6324
|
-
referralGithubMinAgeDays: number2().int().default(30),
|
|
6325
|
-
referralCreditGrantCredits: number2().int().default(0),
|
|
6326
|
-
webClaimCredits: number2().int().default(0),
|
|
6327
|
-
bountyCredits: number2().int().positive().default(5000),
|
|
6328
|
-
bountyMonthlyCapUsd: string2().regex(/^\d+(\.\d{1,6})?$/).default("50"),
|
|
6329
|
-
geoipDbPath: string2().optional(),
|
|
6330
|
-
sanctionedCountries: array(string2()).default(["IR", "CU", "KP", "SY"]),
|
|
6331
|
-
sanctionedTimezones: array(string2()).default(["Asia/Tehran", "Asia/Pyongyang", "America/Havana", "Asia/Damascus", "Europe/Simferopol"]),
|
|
6332
|
-
regionLimitsStart: date3().optional(),
|
|
6333
|
-
fullAllowanceCountries: array(string2()).default(["US", "CA", "GB", "AU", "NZ", "IE"]),
|
|
6334
|
-
priorityCountries: array(string2()).default(["US", "CA", "AU", "NZ"]),
|
|
6335
|
-
freeSpeed: _enum(["off", "log", "on"]).default("log"),
|
|
6336
|
-
freeSpeedDelaysS: array(number2().int().min(0).max(120)).length(4).default([0, 10, 5, 20]),
|
|
6337
|
-
winbackEmail: _enum(["off", "dry_run", "live"]).default("off"),
|
|
6338
|
-
winbackMinDays: number2().int().default(3),
|
|
6339
|
-
winbackMaxDays: number2().int().default(14),
|
|
6340
|
-
winbackMinRequests: number2().int().default(5),
|
|
6341
|
-
winbackDailyMax: number2().int().default(100),
|
|
6342
|
-
winbackClaimHours: number2().int().default(72),
|
|
6343
|
-
winbackContentImportBullet: boolean2().default(false),
|
|
6344
|
-
winbackOfferPriority: string2().default("credits:10000"),
|
|
6345
|
-
winbackOfferOther: string2().default("credits:3000"),
|
|
6346
|
-
winbackArmsPriority: array(string2()).default([]),
|
|
6347
|
-
winbackArmsOther: array(string2()).default([]),
|
|
6348
|
-
regionLowCapMicros: bigint2().default(100000n),
|
|
6349
|
-
streaksEnabled: _enum(["off", "log", "on"]).default("log"),
|
|
6350
|
-
streakMinRequests: number2().int().min(1).default(10),
|
|
6351
|
-
streakRewards: array(object({ day: number2().int().min(1), credits: number2().int().min(1) })).default([
|
|
6352
|
-
{ day: 3, credits: 2000 },
|
|
6353
|
-
{ day: 7, credits: 5000 },
|
|
6354
|
-
{ day: 14, credits: 1e4 },
|
|
6355
|
-
{ day: 30, credits: 20000 }
|
|
6356
|
-
])
|
|
6357
|
-
});
|
|
6358
|
-
function envNumber(value) {
|
|
6359
|
-
if (value === undefined || value === "")
|
|
6360
|
-
return;
|
|
6361
|
-
const n = Number(value);
|
|
6362
|
-
return Number.isNaN(n) ? undefined : n;
|
|
6363
|
-
}
|
|
6364
|
-
function envEmailAddress(name, value) {
|
|
6365
|
-
const v = value?.trim();
|
|
6366
|
-
if (!v)
|
|
6367
|
-
return;
|
|
6368
|
-
if (!/^([^<>"\\,;\r\n]+ <[^\s<>@,;]+@[^\s<>@,;]+\.[^\s<>@,;]+>|[^\s<>@,;]+@[^\s<>@,;]+\.[^\s<>@,;]+)$/.test(v)) {
|
|
6369
|
-
console.warn(`${name} ignored: must be an address like "help@freepi.ai" or "Name <help@freepi.ai>"`);
|
|
6370
|
-
return;
|
|
6371
|
-
}
|
|
6372
|
-
return v;
|
|
6373
|
-
}
|
|
6374
|
-
function envMicros(name, value) {
|
|
6375
|
-
if (value === undefined || value === "")
|
|
6376
|
-
return;
|
|
6377
|
-
const v = value.trim();
|
|
6378
|
-
if (!/^\d+(\.\d{1,6})?$/.test(v)) {
|
|
6379
|
-
throw new Error(`${name} must be a non-negative decimal USD amount with at most 6 decimals, got "${value}"`);
|
|
6380
|
-
}
|
|
6381
|
-
return usdToMicros(v);
|
|
6382
|
-
}
|
|
6383
|
-
function envStrictInt(name, value) {
|
|
6384
|
-
if (value === undefined || value === "")
|
|
6385
|
-
return;
|
|
6386
|
-
const v = value.trim();
|
|
6387
|
-
if (!/^\d+$/.test(v))
|
|
6388
|
-
throw new Error(`${name} must be a non-negative integer, got "${value}"`);
|
|
6389
|
-
return Number(v);
|
|
6390
|
-
}
|
|
6391
|
-
function envPairs(value) {
|
|
6392
|
-
const out = {};
|
|
6393
|
-
for (const part of (value ?? "").split(",")) {
|
|
6394
|
-
const bits = part.split("=").map((s) => s.trim());
|
|
6395
|
-
if (bits.length === 2 && bits[0] && bits[1])
|
|
6396
|
-
out[bits[0]] = bits[1];
|
|
6397
|
-
}
|
|
6398
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
6399
|
-
}
|
|
6400
|
-
function usdToMicrosString(v) {
|
|
6401
|
-
const m = /^(\d{1,6})(?:\.(\d{1,6}))?$/.exec(v.trim());
|
|
6402
|
-
return m ? (BigInt(m[1]) * 1000000n + BigInt((m[2] ?? "").padEnd(6, "0"))).toString() : null;
|
|
6403
|
-
}
|
|
6404
|
-
function envPrices(value) {
|
|
6405
|
-
const out = {};
|
|
6406
|
-
for (const part of (value ?? "").split(",")) {
|
|
6407
|
-
const bits = part.split("=").map((s) => s.trim());
|
|
6408
|
-
const nums = bits.length === 2 ? bits[1].split("/").map(usdToMicrosString) : [];
|
|
6409
|
-
if (bits[0] && nums.length === 3 && nums.every((n) => n !== null))
|
|
6410
|
-
out[bits[0]] = { input: nums[0], cacheRead: nums[1], output: nums[2] };
|
|
6411
|
-
}
|
|
6412
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
6413
|
-
}
|
|
6414
|
-
function envPercent(value) {
|
|
6415
|
-
const n = envNumber(value);
|
|
6416
|
-
return n === undefined ? undefined : Math.min(100, Math.max(0, Math.trunc(n)));
|
|
6417
|
-
}
|
|
6418
|
-
function envBoolean(value) {
|
|
6419
|
-
if (value === undefined || value === "")
|
|
6420
|
-
return;
|
|
6421
|
-
return value === "true" || value === "1";
|
|
6422
|
-
}
|
|
6423
|
-
function envFlagOn(value) {
|
|
6424
|
-
return ["true", "1", "on", "yes"].includes((value ?? "").trim().toLowerCase());
|
|
6425
|
-
}
|
|
6426
|
-
function envList(value) {
|
|
6427
|
-
if (!value)
|
|
6428
|
-
return;
|
|
6429
|
-
const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6430
|
-
return items.length > 0 ? items : undefined;
|
|
6431
|
-
}
|
|
6432
|
-
function envCountries(value) {
|
|
6433
|
-
return envList(value)?.map((s) => s.toUpperCase());
|
|
6434
|
-
}
|
|
6435
|
-
function envTimezones(value) {
|
|
6436
|
-
if (value === undefined)
|
|
6437
|
-
return;
|
|
6438
|
-
return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6439
|
-
}
|
|
6440
|
-
function envStreakRewards(value) {
|
|
6441
|
-
if (!value?.trim())
|
|
6442
|
-
return;
|
|
6443
|
-
const seen = new Set;
|
|
6444
|
-
return value.split(",").map((part) => {
|
|
6445
|
-
const m = /^\s*(\d+)\s*:\s*(\d+)\s*$/.exec(part);
|
|
6446
|
-
const day = m ? Number(m[1]) : 0;
|
|
6447
|
-
const credits = m ? Number(m[2]) : 0;
|
|
6448
|
-
if (!m || day < 1 || credits < 1 || seen.has(day)) {
|
|
6449
|
-
throw new Error(`STREAK_REWARDS must be "day:credits" pairs with distinct days, got "${value}"`);
|
|
6450
|
-
}
|
|
6451
|
-
seen.add(day);
|
|
6452
|
-
return { day, credits };
|
|
6453
|
-
});
|
|
6454
|
-
}
|
|
6455
|
-
function envStreaksMode(value) {
|
|
6456
|
-
if (value === undefined || value.trim() === "")
|
|
6457
|
-
return;
|
|
6458
|
-
const v = value.trim().toLowerCase();
|
|
6459
|
-
if (v !== "off" && v !== "log" && v !== "on")
|
|
6460
|
-
throw new Error(`STREAKS_ENABLED must be off, log or on, got "${value}"`);
|
|
6461
|
-
return v;
|
|
6462
|
-
}
|
|
6463
|
-
function envFreeSpeedMode(value) {
|
|
6464
|
-
if (value === undefined || value.trim() === "")
|
|
6465
|
-
return;
|
|
6466
|
-
const v = value.trim().toLowerCase();
|
|
6467
|
-
if (v !== "off" && v !== "log" && v !== "on")
|
|
6468
|
-
throw new Error(`FREE_SPEED must be off, log or on, got "${value}"`);
|
|
6469
|
-
return v;
|
|
6470
|
-
}
|
|
6471
|
-
function envFreeSpeedDelays(value) {
|
|
6472
|
-
if (!value?.trim())
|
|
6473
|
-
return;
|
|
6474
|
-
const parts = value.split(",").map((s) => s.trim());
|
|
6475
|
-
const nums = parts.map((s) => /^\d+$/.test(s) ? Number(s) : NaN);
|
|
6476
|
-
if (nums.length !== 4 || nums.some((n) => !Number.isInteger(n) || n > 120)) {
|
|
6477
|
-
throw new Error(`FREE_SPEED_DELAYS must be four whole seconds 0-120 (priority+clicking, priority+not_clicking, other+clicking, other+not_clicking), got "${value}"`);
|
|
6478
|
-
}
|
|
6479
|
-
return nums;
|
|
6480
|
-
}
|
|
6481
|
-
function envDate(value) {
|
|
6482
|
-
if (!value?.trim())
|
|
6483
|
-
return;
|
|
6484
|
-
const d = new Date(value.trim());
|
|
6485
|
-
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
6486
|
-
}
|
|
6487
|
-
function envDecimalString(value) {
|
|
6488
|
-
if (value === undefined || value.trim() === "")
|
|
6489
|
-
return;
|
|
6490
|
-
const t = value.trim();
|
|
6491
|
-
return /^\d+(\.\d+)?$/.test(t) ? t : undefined;
|
|
6492
|
-
}
|
|
6493
|
-
function envBountyCap(value) {
|
|
6494
|
-
const t = value?.trim();
|
|
6495
|
-
if (!t)
|
|
6496
|
-
return;
|
|
6497
|
-
if (!/^\d+(\.\d{1,6})?$/.test(t))
|
|
6498
|
-
throw new Error(`BOUNTY_MONTHLY_CAP_USD must be a decimal like "50" or "0.50", got "${value}"`);
|
|
6499
|
-
return t;
|
|
6500
|
-
}
|
|
6501
|
-
function loadConfig(env = process.env) {
|
|
6502
|
-
return ConfigSchema.parse({
|
|
6503
|
-
perUserDailyCapUsd: envNumber(env.PER_USER_DAILY_CAP_USD),
|
|
6504
|
-
perUserRollingCapUsd: envNumber(env.PER_USER_ROLLING_CAP_USD),
|
|
6505
|
-
rollingCapWindowHours: envNumber(env.ROLLING_CAP_WINDOW_HOURS),
|
|
6506
|
-
globalDailyCapUsd: envNumber(env.GLOBAL_DAILY_CAP_USD),
|
|
6507
|
-
contextCharBudget: envNumber(env.CONTEXT_CHAR_BUDGET),
|
|
6508
|
-
maxOutputTokens: envNumber(env.MAX_OUTPUT_TOKENS),
|
|
6509
|
-
accountsPerDayCeiling: envNumber(env.ACCOUNTS_PER_DAY_CEILING),
|
|
6510
|
-
authRatePerIpPerHour: envNumber(env.AUTH_RATE_PER_IP_PER_HOUR),
|
|
6511
|
-
siteLoginStartRateLimitEnabled: envFlagOn(env.SITE_LOGIN_START_RATE_LIMIT),
|
|
6512
|
-
siteLoginStartPerIp: envNumber(env.SITE_LOGIN_START_PER_IP),
|
|
6513
|
-
siteLoginStartPerSubject: envNumber(env.SITE_LOGIN_START_PER_SUBJECT),
|
|
6514
|
-
siteLoginStartWindowSeconds: envNumber(env.SITE_LOGIN_START_WINDOW_SECONDS),
|
|
6515
|
-
siteLoginStartCooldownSeconds: envNumber(env.SITE_LOGIN_START_COOLDOWN_SECONDS),
|
|
6516
|
-
webBuyUrlRatePerProviderPerHour: envNumber(env.WEB_BUY_URL_RATE_PER_PROVIDER_PER_HOUR),
|
|
6517
|
-
handoffTokenRatePerProviderPerHour: envNumber(env.HANDOFF_TOKEN_RATE_PER_PROVIDER_PER_HOUR),
|
|
6518
|
-
handoffHourlyCompletionCap: envNumber(env.HANDOFF_HOURLY_COMPLETION_CAP),
|
|
6519
|
-
tokenRatePerIpPerHour: envNumber(env.TOKEN_RATE_PER_IP_PER_HOUR),
|
|
6520
|
-
consentRatePerIpPerHour: envNumber(env.CONSENT_RATE_PER_IP_PER_HOUR),
|
|
6521
|
-
adInlineTurnFrequency: envNumber(env.AD_INLINE_TURN_FREQUENCY),
|
|
6522
|
-
adMinColumns: envNumber(env.AD_MIN_COLUMNS),
|
|
6523
|
-
proxyBaseUrl: env.PROXY_BASE_URL || undefined,
|
|
6524
|
-
modelId: env.MODEL_ID || undefined,
|
|
6525
|
-
modelDisplay: env.MODEL_DISPLAY || undefined,
|
|
6526
|
-
modelNotice: env.MODEL_NOTICE || undefined,
|
|
6527
|
-
providerDataCollection: env.PROVIDER_DATA_COLLECTION || undefined,
|
|
6528
|
-
promptCacheEnabled: envBoolean(env.PROMPT_CACHE_ENABLED),
|
|
6529
|
-
emailCaptureEnabled: envBoolean(env.EMAIL_CAPTURE_ENABLED),
|
|
6530
|
-
lifecycleEmailExperimentEnabled: envBoolean(env.LIFECYCLE_EMAIL_EXPERIMENT),
|
|
6531
|
-
lifecycleEmailEnabled: envBoolean(env.LIFECYCLE_EMAIL_ENABLED),
|
|
6532
|
-
lifecycleEmailSend: envBoolean(env.LIFECYCLE_EMAIL_SEND),
|
|
6533
|
-
emailProviderConfigured: env.EMAIL_PROVIDER_KEY ? env.EMAIL_PROVIDER_KEY.trim().length > 0 : undefined,
|
|
6534
|
-
emailFrom: env.EMAIL_FROM?.trim() || undefined,
|
|
6535
|
-
emailReplyTo: envEmailAddress("EMAIL_REPLY_TO", env.EMAIL_REPLY_TO),
|
|
6536
|
-
emailPostalAddress: env.EMAIL_POSTAL_ADDRESS?.trim() || undefined,
|
|
6537
|
-
emailProviderUrl: env.EMAIL_PROVIDER_URL?.trim() || undefined,
|
|
6538
|
-
hiveEnabled: envBoolean(env.HIVE_ENABLED),
|
|
6539
|
-
earnEnabled: envBoolean(env.EARN_ENABLED),
|
|
6540
|
-
earnShare: (() => {
|
|
6541
|
-
const n = envNumber(env.EARN_SHARE);
|
|
6542
|
-
return n === undefined ? undefined : Math.min(1, Math.max(0, n));
|
|
6543
|
-
})(),
|
|
6544
|
-
earnBudgetMicros: envStrictInt("EARN_BUDGET_MICROS", env.EARN_BUDGET_MICROS) === undefined ? undefined : BigInt(env.EARN_BUDGET_MICROS.trim()),
|
|
6545
|
-
earnTestGithubIds: envList(env.EARN_TEST_GITHUB_IDS)?.filter((id) => /^\d{1,20}$/.test(id)),
|
|
6546
|
-
providerOrder: envList(env.PROVIDER_ORDER),
|
|
6547
|
-
openrouterFlashProviderOrder: envList(env.OPENROUTER_FLASH_PROVIDER_ORDER),
|
|
6548
|
-
openrouterFlashProviderIgnore: envList(env.OPENROUTER_FLASH_PROVIDER_IGNORE),
|
|
6549
|
-
openrouterFlashMaxPricePrompt: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_PROMPT),
|
|
6550
|
-
openrouterFlashMaxPriceCompletion: envDecimalString(env.OPENROUTER_FLASH_MAX_PRICE_COMPLETION),
|
|
6551
|
-
openrouterSessionPinTtlSeconds: envNumber(env.OPENROUTER_SESSION_PIN_TTL_SECONDS),
|
|
6552
|
-
costAlertMultiplier: envNumber(env.COST_ALERT_MULTIPLIER),
|
|
6553
|
-
costAlertMinHours: envNumber(env.COST_ALERT_MIN_HOURS),
|
|
6554
|
-
tierNewMaxDays: envNumber(env.TIER_NEW_MAX_DAYS),
|
|
6555
|
-
tierYoungMaxDays: envNumber(env.TIER_YOUNG_MAX_DAYS),
|
|
6556
|
-
tierCapNewUsd: envNumber(env.TIER_CAP_NEW_USD),
|
|
6557
|
-
tierCapYoungUsd: envNumber(env.TIER_CAP_YOUNG_USD),
|
|
6558
|
-
fallbackLaneUsd: envNumber(env.FALLBACK_LANE_USD),
|
|
6559
|
-
fallbackHourlyCap: envNumber(env.FALLBACK_HOURLY_CAP),
|
|
6560
|
-
fallbackLaneMinHeadroomBps: envNumber(env.FALLBACK_LANE_MIN_HEADROOM_BPS),
|
|
6561
|
-
queueWaitRetryAfterSeconds: envNumber(env.QUEUE_WAIT_RETRY_AFTER_SECONDS),
|
|
6562
|
-
sessionLeaseTtlMinutes: envNumber(env.SESSION_LEASE_TTL_MINUTES),
|
|
6563
|
-
signedInMaxCliSessions: envNumber(env.SIGNED_IN_MAX_CLI_SESSIONS),
|
|
6564
|
-
shutdownDrainMs: envNumber(env.SHUTDOWN_DRAIN_MS),
|
|
6565
|
-
premiumRatioThreshold: envNumber(env.PREMIUM_RATIO_THRESHOLD),
|
|
6566
|
-
fallbackExpensiveRatio: envNumber(env.FALLBACK_EXPENSIVE_RATIO),
|
|
6567
|
-
larprouterTrialModels: envList(env.LARPROUTER_TRIAL_MODELS),
|
|
6568
|
-
larprouterTrialSharePct: envPercent(env.LARPROUTER_TRIAL_SHARE_PCT),
|
|
6569
|
-
larprouterFallbackModels: envList(env.LARPROUTER_FALLBACK_MODELS),
|
|
6570
|
-
larprouterTtftMs: envNumber(env.LARPROUTER_TTFT_MS),
|
|
6571
|
-
larprouterFallbackTerminal: envBoolean(env.LARPROUTER_FALLBACK_TERMINAL),
|
|
6572
|
-
larprouterModelMap: envPairs(env.LARPROUTER_MODEL_MAP),
|
|
6573
|
-
larprouterPrices: envPrices(env.LARPROUTER_PRICES),
|
|
6574
|
-
larprouterBaseUrl: env.LARPROUTER_BASE_URL?.trim().replace(/\/$/, "") || undefined,
|
|
6575
|
-
experientialBaseUrl: env.EXPERIENTIAL_BASE_URL?.trim().replace(/\/$/, "") || undefined,
|
|
6576
|
-
experientialModelMap: envPairs(env.EXPERIENTIAL_MODEL_MAP),
|
|
6577
|
-
experientialPrimaryModels: envList(env.EXPERIENTIAL_PRIMARY_MODELS),
|
|
6578
|
-
experientialFallbackModels: envList(env.EXPERIENTIAL_FALLBACK_MODELS),
|
|
6579
|
-
experientialTtftMs: envNumber(env.EXPERIENTIAL_TTFT_MS),
|
|
6580
|
-
experientialMaxCostUsdPerRequest: /^\d{1,6}(\.\d{1,6})?$/.test(env.EXPERIENTIAL_MAX_COST_USD_PER_REQUEST?.trim() ?? "") ? env.EXPERIENTIAL_MAX_COST_USD_PER_REQUEST.trim() : undefined,
|
|
6581
|
-
experientialPriceCooldownMs: envNumber(env.EXPERIENTIAL_PRICE_COOLDOWN_MS),
|
|
6582
|
-
particleEnabled: envBoolean(env.PARTICLE_ENABLED),
|
|
6583
|
-
particleBaseUrl: env.PARTICLE_BASE_URL?.trim().replace(/\/$/, "") || undefined,
|
|
6584
|
-
particleModelMap: envPairs(env.PARTICLE_MODEL_MAP),
|
|
6585
|
-
particleTtftMs: envNumber(env.PARTICLE_TTFT_MS),
|
|
6586
|
-
minCliVersion: env.MIN_CLI_VERSION || undefined,
|
|
6587
|
-
latestCliVersion: env.LATEST_CLI_VERSION || undefined,
|
|
6588
|
-
minDesktopVersion: env.MIN_DESKTOP_VERSION || undefined,
|
|
6589
|
-
latestDesktopVersion: env.LATEST_DESKTOP_VERSION || undefined,
|
|
6590
|
-
desktopDownloadUrl: env.DESKTOP_DOWNLOAD_URL || undefined,
|
|
6591
|
-
minWebVersion: env.MIN_WEB_VERSION || undefined,
|
|
6592
|
-
latestWebVersion: env.LATEST_WEB_VERSION || undefined,
|
|
6593
|
-
workspaceMaxStreams: envNumber(env.WORKSPACE_MAX_STREAMS),
|
|
6594
|
-
gravityDesktopEnabled: env.GRAVITY_DESKTOP_ENABLED === "true" ? true : undefined,
|
|
6595
|
-
abuseWindowMinutes: envNumber(env.ABUSE_WINDOW_MINUTES),
|
|
6596
|
-
abuseSettleGraceSeconds: envNumber(env.ABUSE_SETTLE_GRACE_SECONDS),
|
|
6597
|
-
abuseGraceSettledStarts: envNumber(env.ABUSE_GRACE_SETTLED_STARTS),
|
|
6598
|
-
abuseUnverifiedMinStarts: envNumber(env.ABUSE_UNVERIFIED_MIN_STARTS),
|
|
6599
|
-
abuseUnverifiedMinFinishes: envNumber(env.ABUSE_UNVERIFIED_MIN_FINISHES),
|
|
6600
|
-
abuseUnverifiedRestrictHours: envNumber(env.ABUSE_UNVERIFIED_RESTRICT_HOURS),
|
|
6601
|
-
abuseLikelyBypassMinStarts: envNumber(env.ABUSE_LIKELY_BYPASS_MIN_STARTS),
|
|
6602
|
-
abuseLikelyBypassRestrictHours: envNumber(env.ABUSE_LIKELY_BYPASS_RESTRICT_HOURS),
|
|
6603
|
-
abuseRepeatWindowDays: envNumber(env.ABUSE_REPEAT_WINDOW_DAYS),
|
|
6604
|
-
abuseRepeatMinOccurrences: envNumber(env.ABUSE_REPEAT_MIN_OCCURRENCES),
|
|
6605
|
-
abuseRestrictedCapUsd: envNumber(env.ABUSE_RESTRICTED_CAP_USD),
|
|
6606
|
-
abuseRestrictedHourlyCap: envNumber(env.ABUSE_RESTRICTED_HOURLY_CAP),
|
|
6607
|
-
abuseNormalHourlyCap: envNumber(env.ABUSE_NORMAL_HOURLY_CAP),
|
|
6608
|
-
abuseSignedInNormalHourlyCap: envNumber(env.ABUSE_SIGNED_IN_NORMAL_HOURLY_CAP),
|
|
6609
|
-
abuseDay0SkipHourly: envBoolean(env.ABUSE_DAY0_SKIP_HOURLY),
|
|
6610
|
-
abuseFlipAlertThreshold: envNumber(env.ABUSE_FLIP_ALERT_THRESHOLD),
|
|
6611
|
-
abuseAdBypassMinRequests: envNumber(env.ABUSE_AD_BYPASS_MIN_REQUESTS),
|
|
6612
|
-
abuseAdBypassRatio: envNumber(env.ABUSE_AD_BYPASS_RATIO),
|
|
6613
|
-
traceMaxInflightWrites: envNumber(env.TRACE_MAX_INFLIGHT_WRITES),
|
|
6614
|
-
catalogCacheTtlSeconds: envNumber(env.CATALOG_CACHE_TTL_SECONDS),
|
|
6615
|
-
eventMaxInflightWrites: envNumber(env.EVENT_MAX_INFLIGHT_WRITES),
|
|
6616
|
-
guestEnabled: envBoolean(env.GUEST_ENABLED),
|
|
6617
|
-
guestAllowanceMicros: envMicros("GUEST_ALLOWANCE_USD", env.GUEST_ALLOWANCE_USD),
|
|
6618
|
-
guestGlobalDailyCapMicros: envMicros("GUEST_GLOBAL_DAILY_CAP_USD", env.GUEST_GLOBAL_DAILY_CAP_USD),
|
|
6619
|
-
guestHoldMicros: envMicros("GUEST_HOLD_USD", env.GUEST_HOLD_USD),
|
|
6620
|
-
guestIpSessionsPerDay: envStrictInt("GUEST_IP_SESSIONS_PER_DAY", env.GUEST_IP_SESSIONS_PER_DAY),
|
|
6621
|
-
guestIpAllowanceMicrosPerDay: envMicros("GUEST_IP_ALLOWANCE_USD_PER_DAY", env.GUEST_IP_ALLOWANCE_USD_PER_DAY),
|
|
6622
|
-
guestMaxConcurrentStreams: envStrictInt("GUEST_MAX_CONCURRENT_STREAMS", env.GUEST_MAX_CONCURRENT_STREAMS),
|
|
6623
|
-
guestMinRequestHeadroomMicros: envMicros("GUEST_MIN_REQUEST_HEADROOM_USD", env.GUEST_MIN_REQUEST_HEADROOM_USD),
|
|
6624
|
-
guestIpHashRetentionDays: envStrictInt("GUEST_IP_HASH_RETENTION_DAYS", env.GUEST_IP_HASH_RETENTION_DAYS),
|
|
6625
|
-
returnBonusDay2Credits: envStrictInt("RETURN_BONUS_DAY2_CREDITS", env.RETURN_BONUS_DAY2_CREDITS),
|
|
6626
|
-
returnBonusDay7Credits: envStrictInt("RETURN_BONUS_DAY7_CREDITS", env.RETURN_BONUS_DAY7_CREDITS),
|
|
6627
|
-
guestGithubBonusCredits: envStrictInt("GUEST_GITHUB_BONUS_CREDITS", env.GUEST_GITHUB_BONUS_CREDITS),
|
|
6628
|
-
returnNudgeEmailEnabled: envBoolean(env.RETURN_NUDGE_EMAIL_ENABLED),
|
|
6629
|
-
referralBonusCreditsPerDay: envStrictInt("REFERRAL_BONUS_CREDITS_PER_DAY", env.REFERRAL_BONUS_CREDITS_PER_DAY),
|
|
6630
|
-
referralBonusDays: envStrictInt("REFERRAL_BONUS_DAYS", env.REFERRAL_BONUS_DAYS),
|
|
6631
|
-
referralMaxActivePerReferrer: envStrictInt("REFERRAL_MAX_ACTIVE_PER_REFERRER", env.REFERRAL_MAX_ACTIVE_PER_REFERRER),
|
|
6632
|
-
referralGithubMinAgeDays: envStrictInt("REFERRAL_GITHUB_MIN_AGE_DAYS", env.REFERRAL_GITHUB_MIN_AGE_DAYS),
|
|
6633
|
-
referralCreditGrantCredits: envStrictInt("REFERRAL_CREDIT_GRANT_CREDITS", env.REFERRAL_CREDIT_GRANT_CREDITS),
|
|
6634
|
-
webClaimCredits: envStrictInt("WEB_CLAIM_CREDITS", env.WEB_CLAIM_CREDITS),
|
|
6635
|
-
bountyCredits: envStrictInt("BOUNTY_CREDITS", env.BOUNTY_CREDITS),
|
|
6636
|
-
bountyMonthlyCapUsd: envBountyCap(env.BOUNTY_MONTHLY_CAP_USD),
|
|
6637
|
-
geoipDbPath: env.GEOIP_DB_PATH?.trim() || undefined,
|
|
6638
|
-
sanctionedCountries: envCountries(env.SANCTIONED_COUNTRIES),
|
|
6639
|
-
sanctionedTimezones: envTimezones(env.SANCTIONED_TIMEZONES),
|
|
6640
|
-
regionLimitsStart: envDate(env.REGION_LIMITS_START),
|
|
6641
|
-
fullAllowanceCountries: envCountries(env.FULL_ALLOWANCE_COUNTRIES),
|
|
6642
|
-
regionLowCapMicros: envMicros("REGION_LOW_CAP_USD", env.REGION_LOW_CAP_USD),
|
|
6643
|
-
priorityCountries: envCountries(env.PRIORITY_COUNTRIES),
|
|
6644
|
-
freeSpeed: envFreeSpeedMode(env.FREE_SPEED),
|
|
6645
|
-
freeSpeedDelaysS: envFreeSpeedDelays(env.FREE_SPEED_DELAYS),
|
|
6646
|
-
streaksEnabled: envStreaksMode(env.STREAKS_ENABLED),
|
|
6647
|
-
streakMinRequests: envStrictInt("STREAK_MIN_REQUESTS", env.STREAK_MIN_REQUESTS),
|
|
6648
|
-
streakRewards: envStreakRewards(env.STREAK_REWARDS),
|
|
6649
|
-
winbackEmail: env.WINBACK_EMAIL?.trim().toLowerCase() === "live" ? "live" : env.WINBACK_EMAIL?.trim().toLowerCase() === "dry_run" ? "dry_run" : undefined,
|
|
6650
|
-
winbackMinDays: envStrictInt("WINBACK_MIN_DAYS", env.WINBACK_MIN_DAYS),
|
|
6651
|
-
winbackMaxDays: envStrictInt("WINBACK_MAX_DAYS", env.WINBACK_MAX_DAYS),
|
|
6652
|
-
winbackMinRequests: envStrictInt("WINBACK_MIN_REQUESTS", env.WINBACK_MIN_REQUESTS),
|
|
6653
|
-
winbackDailyMax: envStrictInt("WINBACK_DAILY_MAX", env.WINBACK_DAILY_MAX),
|
|
6654
|
-
winbackClaimHours: envStrictInt("WINBACK_CLAIM_HOURS", env.WINBACK_CLAIM_HOURS),
|
|
6655
|
-
winbackContentImportBullet: envFlagOn(env.WINBACK_CONTENT_IMPORT_BULLET),
|
|
6656
|
-
winbackOfferPriority: env.WINBACK_OFFER_PRIORITY?.trim() || undefined,
|
|
6657
|
-
winbackOfferOther: env.WINBACK_OFFER_OTHER?.trim() || undefined,
|
|
6658
|
-
winbackArmsPriority: envList(env.WINBACK_ARMS_PRIORITY),
|
|
6659
|
-
winbackArmsOther: envList(env.WINBACK_ARMS_OTHER)
|
|
6660
|
-
});
|
|
6661
|
-
}
|
|
6086
|
+
|
|
6662
6087
|
// ../shared/src/credits.ts
|
|
6663
6088
|
var CREDITS_PER_USD = 40000 / 3;
|
|
6664
6089
|
function usdToCredits(usd) {
|
|
@@ -6769,6 +6194,7 @@ function formatLimitNotice(fields, opts = {}) {
|
|
|
6769
6194
|
bits.push(`${fields.remaining_credits_today.toLocaleString("en-US")} credits left today`);
|
|
6770
6195
|
return bits.join(", ");
|
|
6771
6196
|
}
|
|
6197
|
+
|
|
6772
6198
|
// ../shared/src/version.ts
|
|
6773
6199
|
function compareVersions(a, b) {
|
|
6774
6200
|
const pa = a.split(".").map(Number);
|
|
@@ -6785,6 +6211,7 @@ function compareVersions(a, b) {
|
|
|
6785
6211
|
}
|
|
6786
6212
|
return 0;
|
|
6787
6213
|
}
|
|
6214
|
+
|
|
6788
6215
|
// ../shared/src/hive.ts
|
|
6789
6216
|
var HIVE_MAX_CHAT_CHARS = 2048;
|
|
6790
6217
|
var HIVE_MAX_PROMPT_CHARS = 8192;
|
|
@@ -6895,6 +6322,7 @@ var HiveClassifyResponseSchema = object({
|
|
|
6895
6322
|
decision: _enum(["allow", "ask"]),
|
|
6896
6323
|
reason: string2()
|
|
6897
6324
|
});
|
|
6325
|
+
|
|
6898
6326
|
// ../shared/src/credit-tiers.ts
|
|
6899
6327
|
var n = (v) => v.toLocaleString("en-US");
|
|
6900
6328
|
var day = (iso, timeZone) => new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", ...timeZone ? { timeZone } : {} }).format(new Date(iso));
|
|
@@ -6934,6 +6362,7 @@ function premiumExpiryWarning(i) {
|
|
|
6934
6362
|
return;
|
|
6935
6363
|
return `${n(i.expiring)} premium credits expire today at ${formatRefillEt(at, tz)}`;
|
|
6936
6364
|
}
|
|
6365
|
+
|
|
6937
6366
|
// src/cli-args.ts
|
|
6938
6367
|
var HELP_TEXT = `
|
|
6939
6368
|
free-pi — free AI coding in your terminal; ads pay for the inference.
|
|
@@ -7093,6 +6522,14 @@ function readAutoUpdate(configPath) {
|
|
|
7093
6522
|
return true;
|
|
7094
6523
|
}
|
|
7095
6524
|
}
|
|
6525
|
+
function readChoicePrompts(configPath) {
|
|
6526
|
+
try {
|
|
6527
|
+
const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
|
|
6528
|
+
return parsed?.choicePrompts !== false;
|
|
6529
|
+
} catch {
|
|
6530
|
+
return true;
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
7096
6533
|
|
|
7097
6534
|
// src/credentials.ts
|
|
7098
6535
|
import { mkdir, readFile, rm, writeFile, chmod } from "node:fs/promises";
|
|
@@ -7238,17 +6675,31 @@ function startGravityPixel(jwt, storageDir, env = process.env, load = () => impo
|
|
|
7238
6675
|
|
|
7239
6676
|
// src/pi-launch.ts
|
|
7240
6677
|
import {
|
|
7241
|
-
createAgentSessionFromServices,
|
|
6678
|
+
createAgentSessionFromServices as createAgentSessionFromServices2,
|
|
7242
6679
|
createAgentSessionRuntime,
|
|
7243
|
-
createAgentSessionServices,
|
|
6680
|
+
createAgentSessionServices as createAgentSessionServices2,
|
|
7244
6681
|
createCodemodeExtension,
|
|
7245
6682
|
createMcpExtension,
|
|
7246
6683
|
createToolSearchExtension,
|
|
7247
6684
|
InteractiveMode,
|
|
7248
|
-
SessionManager,
|
|
7249
|
-
SettingsManager
|
|
6685
|
+
SessionManager as SessionManager2,
|
|
6686
|
+
SettingsManager as SettingsManager2
|
|
7250
6687
|
} from "@earendil-works/pi-coding-agent";
|
|
7251
6688
|
|
|
6689
|
+
// ../pi-ads/src/config.ts
|
|
6690
|
+
function envInt(value, fallback) {
|
|
6691
|
+
if (value === undefined || value === "")
|
|
6692
|
+
return fallback;
|
|
6693
|
+
const n = Number(value);
|
|
6694
|
+
return Number.isInteger(n) ? n : fallback;
|
|
6695
|
+
}
|
|
6696
|
+
function loadConfig(env = process.env) {
|
|
6697
|
+
return {
|
|
6698
|
+
adInlineTurnFrequency: envInt(env.AD_INLINE_TURN_FREQUENCY, 5),
|
|
6699
|
+
adMinColumns: envInt(env.AD_MIN_COLUMNS, 60)
|
|
6700
|
+
};
|
|
6701
|
+
}
|
|
6702
|
+
|
|
7252
6703
|
// ../pi-ads/src/api.ts
|
|
7253
6704
|
var REQUEST_TIMEOUT_MS = 5000;
|
|
7254
6705
|
async function authedFetch(deps, path, init) {
|
|
@@ -7402,6 +6853,7 @@ function createBannerRenderer(opts = {}) {
|
|
|
7402
6853
|
return;
|
|
7403
6854
|
if (!ad) {
|
|
7404
6855
|
screen.banner = undefined;
|
|
6856
|
+
screen.bannerAdId = undefined;
|
|
7405
6857
|
ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
|
|
7406
6858
|
opts.onBannerAd?.(undefined);
|
|
7407
6859
|
return;
|
|
@@ -7416,8 +6868,10 @@ function createBannerRenderer(opts = {}) {
|
|
|
7416
6868
|
if (isGravity && screen.inline)
|
|
7417
6869
|
screen.clearInline?.();
|
|
7418
6870
|
screen.banner = isGravity ? "gravity" : "own";
|
|
6871
|
+
screen.bannerAdId = ad.ad_id;
|
|
7419
6872
|
screen.clearBanner = () => {
|
|
7420
6873
|
screen.banner = undefined;
|
|
6874
|
+
screen.bannerAdId = undefined;
|
|
7421
6875
|
ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
|
|
7422
6876
|
opts.onBannerAd?.(undefined);
|
|
7423
6877
|
};
|
|
@@ -7458,6 +6912,8 @@ function createInlineRenderer(screen = {}) {
|
|
|
7458
6912
|
const isGravity = ad.source === "gravity";
|
|
7459
6913
|
if (screen.banner === "gravity")
|
|
7460
6914
|
return;
|
|
6915
|
+
if (!isGravity && screen.banner && screen.bannerAdId === ad.ad_id)
|
|
6916
|
+
return;
|
|
7461
6917
|
if (isGravity && screen.banner)
|
|
7462
6918
|
screen.clearBanner?.();
|
|
7463
6919
|
ctx.ui.setWidget(INLINE_WIDGET_ID, adWidget(ad.creative, ad.click_url, config.adMinColumns), { placement: "belowEditor" });
|
|
@@ -7525,6 +6981,8 @@ function mapErrorCode(code) {
|
|
|
7525
6981
|
return "this conversation is too long — run /compact or start a new session with /new";
|
|
7526
6982
|
case "reward_required":
|
|
7527
6983
|
return "This model is only for reward holders. Finish an EARN task from your terminal to unlock it, or switch models with /model.";
|
|
6984
|
+
case "subagents_unavailable":
|
|
6985
|
+
return "Subagents run on the free Particle model, or with credits. Switch with /model or run /buy-credits.";
|
|
7528
6986
|
default:
|
|
7529
6987
|
return assertExhaustiveErrorCode(code);
|
|
7530
6988
|
}
|
|
@@ -7572,11 +7030,14 @@ function formatRewardStatus(r, now, columns) {
|
|
|
7572
7030
|
}
|
|
7573
7031
|
async function renderMeter(deps, ctx, opts = {}) {
|
|
7574
7032
|
const me = await fetchMe(deps);
|
|
7575
|
-
ctx.ui.setStatus(METER_STATUS_KEY, undefined);
|
|
7576
7033
|
if (!me) {
|
|
7577
|
-
|
|
7034
|
+
if (await deps.getToken() !== lastToken) {
|
|
7035
|
+
ctx.ui.setStatus(METER_STATUS_KEY, undefined);
|
|
7036
|
+
ctx.ui.setWidget(METER_WIDGET_ID, undefined);
|
|
7037
|
+
}
|
|
7578
7038
|
return;
|
|
7579
7039
|
}
|
|
7040
|
+
ctx.ui.setStatus(METER_STATUS_KEY, undefined);
|
|
7580
7041
|
const theme = ctx.ui.theme;
|
|
7581
7042
|
const reward = formatRewardStatus(me.earn_reward, opts.now ?? Date.now(), opts.columns ?? (process.stdout.columns || 80));
|
|
7582
7043
|
if (reward)
|
|
@@ -7716,7 +7177,7 @@ function createAdsExtension(opts) {
|
|
|
7716
7177
|
}
|
|
7717
7178
|
|
|
7718
7179
|
// src/pi-launch.ts
|
|
7719
|
-
import { dirname as
|
|
7180
|
+
import { dirname as dirname11, join as join11, resolve as resolve4 } from "node:path";
|
|
7720
7181
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
7721
7182
|
|
|
7722
7183
|
// ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/system/memory/metrics.mjs
|
|
@@ -7921,6 +7382,11 @@ function AddOptionalAction(type, options) {
|
|
|
7921
7382
|
return result;
|
|
7922
7383
|
}
|
|
7923
7384
|
|
|
7385
|
+
// ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/types/array.mjs
|
|
7386
|
+
function _Array_(items, options) {
|
|
7387
|
+
return Create({ "~kind": "Array" }, { type: "array", items }, options);
|
|
7388
|
+
}
|
|
7389
|
+
|
|
7924
7390
|
// ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/action/_add_optional.mjs
|
|
7925
7391
|
function AddOptional(type, options = {}) {
|
|
7926
7392
|
return AddOptionalAction(type, options);
|
|
@@ -8260,6 +7726,439 @@ function createImageToolExtension(opts) {
|
|
|
8260
7726
|
};
|
|
8261
7727
|
}
|
|
8262
7728
|
|
|
7729
|
+
// src/choice-columns.ts
|
|
7730
|
+
import { matchesKey, truncateToWidth as truncateToWidth3, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
7731
|
+
var STACK_BELOW = 72;
|
|
7732
|
+
var COLUMN_FOOTER = "left/right compare enter select o something else d let the agent decide esc cancel";
|
|
7733
|
+
var pad2 = (s, w) => s + " ".repeat(Math.max(0, w - visibleWidth(s)));
|
|
7734
|
+
function cell(s, w, st) {
|
|
7735
|
+
const out = [];
|
|
7736
|
+
const add = (mark, t) => wrapTextWithAnsi(t, Math.max(1, w - 2)).forEach((l, i) => out.push((i ? " " : mark + " ") + l));
|
|
7737
|
+
for (const p of s.pros ?? [])
|
|
7738
|
+
add(st.success("+"), p);
|
|
7739
|
+
for (const c of s.cons ?? [])
|
|
7740
|
+
add(st.error("-"), c);
|
|
7741
|
+
if (s.best_for) {
|
|
7742
|
+
if (out.length)
|
|
7743
|
+
out.push("");
|
|
7744
|
+
out.push(st.dim("Best for"));
|
|
7745
|
+
wrapTextWithAnsi(s.best_for, w).forEach((l) => out.push(l));
|
|
7746
|
+
}
|
|
7747
|
+
return out;
|
|
7748
|
+
}
|
|
7749
|
+
function build(o, width, st, rows, scroll) {
|
|
7750
|
+
const budget = Math.max(8, rows - 3);
|
|
7751
|
+
const hw = Math.max(1, width - 2);
|
|
7752
|
+
const headAll = [...wrapTextWithAnsi(st.bold(o.question), hw), ...o.context ? wrapTextWithAnsi(st.dim(o.context), hw) : []];
|
|
7753
|
+
const headMax = Math.max(2, Math.floor(budget / 3));
|
|
7754
|
+
const head = headAll.slice(0, headMax).map((l, i, a) => " " + (i === a.length - 1 && headAll.length > headMax ? truncateToWidth3(l, hw - 1) + "…" : l));
|
|
7755
|
+
head.push("");
|
|
7756
|
+
const foot = wrapTextWithAnsi(st.dim(COLUMN_FOOTER), hw).map((l) => " " + l);
|
|
7757
|
+
const title = (i, w) => {
|
|
7758
|
+
const t = truncateToWidth3(o.sides[i].label, Math.max(1, w - 2));
|
|
7759
|
+
return o.selected === i ? st.accent(st.bold("> " + t)) : " " + t;
|
|
7760
|
+
};
|
|
7761
|
+
const hint = (from, n, total) => " " + st.dim(`up/down scroll (${from + 1}-${from + n} of ${total})`);
|
|
7762
|
+
const clamp = (lines) => lines.map((l) => visibleWidth(l) > width ? truncateToWidth3(l, width) : l);
|
|
7763
|
+
const windowed = (detail, avail) => {
|
|
7764
|
+
if (detail.length <= avail)
|
|
7765
|
+
return { rows: detail, hint: [], max: 0 };
|
|
7766
|
+
const n = Math.max(1, avail - 1);
|
|
7767
|
+
const max = detail.length - n;
|
|
7768
|
+
const sc = Math.min(Math.max(0, scroll), max);
|
|
7769
|
+
return { rows: detail.slice(sc, sc + n), hint: [hint(sc, n, detail.length)], max };
|
|
7770
|
+
};
|
|
7771
|
+
if (width < STACK_BELOW) {
|
|
7772
|
+
const w = Math.max(1, width - 4);
|
|
7773
|
+
const cells = o.sides.map((s) => cell(s, w, st));
|
|
7774
|
+
const full = o.sides.flatMap((s, i) => [" " + title(i, w + 2), ...cells[i].map((l) => " " + l), ""]);
|
|
7775
|
+
if (head.length + full.length + foot.length <= budget)
|
|
7776
|
+
return { lines: clamp([...head, ...full, ...foot]), maxScroll: 0 };
|
|
7777
|
+
const titles = o.sides.map((_, i) => " " + title(i, w + 2));
|
|
7778
|
+
const avail = Math.max(1, budget - head.length - titles.length - foot.length);
|
|
7779
|
+
const detail = cells[o.selected].map((l) => " " + l);
|
|
7780
|
+
const win = windowed(detail, avail);
|
|
7781
|
+
return { lines: clamp([...head, ...titles, ...win.rows, ...win.hint, ...foot]), maxScroll: win.max };
|
|
7782
|
+
}
|
|
7783
|
+
const w = Math.floor((width - 8) / 2);
|
|
7784
|
+
const rule = (l, m, r) => " " + l + "─".repeat(w + 2) + m + "─".repeat(w + 2) + r;
|
|
7785
|
+
const rowOf = (x, y) => " │ " + pad2(x, w) + " │ " + pad2(y, w) + " │";
|
|
7786
|
+
const ca = cell(o.sides[0], w, st), cb = cell(o.sides[1], w, st);
|
|
7787
|
+
const total = Math.max(ca.length, cb.length);
|
|
7788
|
+
const fixed = head.length + 4 + foot.length;
|
|
7789
|
+
const avail = Math.max(1, budget - fixed);
|
|
7790
|
+
const n = total <= avail ? total : Math.max(1, avail - 1);
|
|
7791
|
+
const max = Math.max(0, total - n);
|
|
7792
|
+
const sc = Math.min(Math.max(0, scroll), max);
|
|
7793
|
+
const body = [];
|
|
7794
|
+
for (let i = sc;i < sc + n; i++)
|
|
7795
|
+
body.push(rowOf(ca[i] ?? "", cb[i] ?? ""));
|
|
7796
|
+
const more = max > 0 ? [hint(sc, n, total)] : [];
|
|
7797
|
+
return { lines: clamp([...head, rule("┌", "┬", "┐"), rowOf(title(0, w), title(1, w)), rule("├", "┼", "┤"), ...body, rule("└", "┴", "┘"), ...more, ...foot]), maxScroll: max };
|
|
7798
|
+
}
|
|
7799
|
+
function renderColumns(o, width, st, rows = 1000, scroll = 0) {
|
|
7800
|
+
return build(o, width, st, rows, scroll).lines;
|
|
7801
|
+
}
|
|
7802
|
+
function columnsMaxScroll(o, width, st, rows) {
|
|
7803
|
+
return build(o, width, st, rows, 0).maxScroll;
|
|
7804
|
+
}
|
|
7805
|
+
function columnsComponent(o, st, tui, done) {
|
|
7806
|
+
let selected = 0;
|
|
7807
|
+
let scroll = 0;
|
|
7808
|
+
let lastWidth = 100;
|
|
7809
|
+
const rows = () => tui.terminal?.rows ?? 1000;
|
|
7810
|
+
let finished = false;
|
|
7811
|
+
const finish = (r) => {
|
|
7812
|
+
if (!finished) {
|
|
7813
|
+
finished = true;
|
|
7814
|
+
o.signal?.removeEventListener("abort", onAbort);
|
|
7815
|
+
done(r);
|
|
7816
|
+
}
|
|
7817
|
+
};
|
|
7818
|
+
const onAbort = () => finish(undefined);
|
|
7819
|
+
if (o.signal?.aborted)
|
|
7820
|
+
queueMicrotask(onAbort);
|
|
7821
|
+
else
|
|
7822
|
+
o.signal?.addEventListener("abort", onAbort, { once: true });
|
|
7823
|
+
return {
|
|
7824
|
+
invalidate() {},
|
|
7825
|
+
dispose() {
|
|
7826
|
+
o.signal?.removeEventListener("abort", onAbort);
|
|
7827
|
+
},
|
|
7828
|
+
render: (width) => {
|
|
7829
|
+
lastWidth = width;
|
|
7830
|
+
return renderColumns({ question: o.question, context: o.context, sides: o.sides, selected }, width, st, rows(), scroll);
|
|
7831
|
+
},
|
|
7832
|
+
handleInput(d) {
|
|
7833
|
+
if (matchesKey(d, "left") || matchesKey(d, "right") || matchesKey(d, "tab")) {
|
|
7834
|
+
selected = selected === 0 ? 1 : 0;
|
|
7835
|
+
scroll = 0;
|
|
7836
|
+
tui.requestRender();
|
|
7837
|
+
} else if (matchesKey(d, "up") || matchesKey(d, "down")) {
|
|
7838
|
+
const max = columnsMaxScroll({ question: o.question, context: o.context, sides: o.sides, selected }, lastWidth, st, rows());
|
|
7839
|
+
scroll = Math.min(max, Math.max(0, scroll + (matchesKey(d, "down") ? 1 : -1)));
|
|
7840
|
+
tui.requestRender();
|
|
7841
|
+
} else if (matchesKey(d, "enter"))
|
|
7842
|
+
finish(o.labels[selected]);
|
|
7843
|
+
else if (d === "o")
|
|
7844
|
+
finish(o.else_);
|
|
7845
|
+
else if (d === "d")
|
|
7846
|
+
finish(o.decide);
|
|
7847
|
+
else if (matchesKey(d, "escape") || matchesKey(d, "ctrl+c"))
|
|
7848
|
+
finish(undefined);
|
|
7849
|
+
}
|
|
7850
|
+
};
|
|
7851
|
+
}
|
|
7852
|
+
|
|
7853
|
+
// src/choice-tool.ts
|
|
7854
|
+
var ASK_USER_TOOL_NAME = "ask_user";
|
|
7855
|
+
var CHOICE_NO_ANSWER = "No answer; decide yourself and state your pick in one line.";
|
|
7856
|
+
var CHOICE_LIMIT_TEXT = "Ask limit reached; decide yourself.";
|
|
7857
|
+
var AGENT_DECIDES = "Let the agent decide";
|
|
7858
|
+
var SOMETHING_ELSE = "Something else (type it)";
|
|
7859
|
+
var COMPARE = "Compare pros and cons";
|
|
7860
|
+
var CHOICE_COMPARE_TEXT = "The user asked to compare the options. Explain the pros and cons of each option briefly as visible text, then call ask_user again with the same options.";
|
|
7861
|
+
var OTHER_MAX_CHARS = 500;
|
|
7862
|
+
var CAPS_ENTRY = "free-pi-choice-caps";
|
|
7863
|
+
var MAX_PER_SESSION = 10;
|
|
7864
|
+
var MAX_QUESTIONS_PER_SESSION = 5;
|
|
7865
|
+
var ANSWER_MAX_CHARS = 2000;
|
|
7866
|
+
var INT_MAX = 2147483647;
|
|
7867
|
+
var POST_TIMEOUT_MS = 3000;
|
|
7868
|
+
var COUNTER_MAX = 50;
|
|
7869
|
+
var CHOICE_RULES = `- Before choosing a library, service, language or other tool that the request and
|
|
7870
|
+
existing code do not already settle, call ask_user. Narrow to the best two options
|
|
7871
|
+
where you can and give pros, cons and best_for for each; use 3 or 4 options only for
|
|
7872
|
+
a genuinely wider choice. Pass your recommendation.
|
|
7873
|
+
- When an answer would change what you do and you cannot find it in the request or the
|
|
7874
|
+
code, call ask_user with just a question. Do not ask for permission to continue or to
|
|
7875
|
+
confirm what you already know.
|
|
7876
|
+
- Wait for the answer before continuing. Do not ask about trivia such as names,
|
|
7877
|
+
formatting or file layout, or twice on the same topic. If there is no answer, pick
|
|
7878
|
+
yourself and say what you picked.`;
|
|
7879
|
+
function choiceEnabled(o) {
|
|
7880
|
+
if (o.env.FREEPI_NO_CHOICE === "1")
|
|
7881
|
+
return false;
|
|
7882
|
+
if (!readChoicePrompts(o.configPath))
|
|
7883
|
+
return false;
|
|
7884
|
+
return o.env.FREEPI_CHOICE === "1" || o.arm === "treatment";
|
|
7885
|
+
}
|
|
7886
|
+
var capsBySession = new Map;
|
|
7887
|
+
var emptyCounters = () => ({ attempted: 0, shown: 0, suppressed: {}, delivery_failures: 0 });
|
|
7888
|
+
var norm = (s) => s.trim().replace(/\s+/g, " ").toLowerCase();
|
|
7889
|
+
function createChoiceToolExtension(opts) {
|
|
7890
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
7891
|
+
const now = opts.now ?? Date.now;
|
|
7892
|
+
const random = opts.random ?? Math.random;
|
|
7893
|
+
const shutdownBoundMs = opts.shutdownBoundMs ?? POST_TIMEOUT_MS;
|
|
7894
|
+
const flushMs = opts.counterFlushMs ?? 60000;
|
|
7895
|
+
return {
|
|
7896
|
+
name: "free-pi-choice-tool",
|
|
7897
|
+
factory(pi) {
|
|
7898
|
+
function capsFor(sm) {
|
|
7899
|
+
const id = sm.getSessionId();
|
|
7900
|
+
let caps = capsBySession.get(id);
|
|
7901
|
+
if (!caps) {
|
|
7902
|
+
caps = { shownCount: 0, sets: new Set, asked: 0, questions: new Set };
|
|
7903
|
+
const saved = [...sm.getEntries()].reverse().find((e) => e.type === "custom" && e.customType === CAPS_ENTRY);
|
|
7904
|
+
if (saved?.data) {
|
|
7905
|
+
caps.shownCount = saved.data.shownCount ?? 0;
|
|
7906
|
+
caps.sets = new Set(saved.data.sets ?? []);
|
|
7907
|
+
caps.asked = saved.data.asked ?? 0;
|
|
7908
|
+
caps.questions = new Set(saved.data.questions ?? []);
|
|
7909
|
+
}
|
|
7910
|
+
capsBySession.set(id, caps);
|
|
7911
|
+
}
|
|
7912
|
+
return caps;
|
|
7913
|
+
}
|
|
7914
|
+
let counters = emptyCounters();
|
|
7915
|
+
let flushTimer;
|
|
7916
|
+
const dirty = (c) => c.attempted + c.shown + c.delivery_failures + Object.values(c.suppressed).reduce((a, b) => a + (b ?? 0), 0) > 0;
|
|
7917
|
+
function takeCounters() {
|
|
7918
|
+
if (!dirty(counters))
|
|
7919
|
+
return;
|
|
7920
|
+
const out = emptyCounters();
|
|
7921
|
+
const left = emptyCounters();
|
|
7922
|
+
const cut = (v) => [Math.min(v, COUNTER_MAX), v - Math.min(v, COUNTER_MAX)];
|
|
7923
|
+
[out.attempted, left.attempted] = cut(counters.attempted);
|
|
7924
|
+
[out.shown, left.shown] = cut(counters.shown);
|
|
7925
|
+
[out.delivery_failures, left.delivery_failures] = cut(counters.delivery_failures);
|
|
7926
|
+
for (const [k, v] of Object.entries(counters.suppressed)) {
|
|
7927
|
+
const [a, b] = cut(v ?? 0);
|
|
7928
|
+
if (a)
|
|
7929
|
+
out.suppressed[k] = a;
|
|
7930
|
+
if (b)
|
|
7931
|
+
left.suppressed[k] = b;
|
|
7932
|
+
}
|
|
7933
|
+
counters = left;
|
|
7934
|
+
return out;
|
|
7935
|
+
}
|
|
7936
|
+
function giveBack(c) {
|
|
7937
|
+
counters.attempted += c.attempted;
|
|
7938
|
+
counters.shown += c.shown;
|
|
7939
|
+
counters.delivery_failures += c.delivery_failures + 1;
|
|
7940
|
+
for (const [k, v] of Object.entries(c.suppressed))
|
|
7941
|
+
counters.suppressed[k] = (counters.suppressed[k] ?? 0) + (v ?? 0);
|
|
7942
|
+
}
|
|
7943
|
+
async function post(path, body, attempts = 2) {
|
|
7944
|
+
for (let attempt = 0;attempt < attempts; attempt++) {
|
|
7945
|
+
try {
|
|
7946
|
+
const token = await opts.getToken();
|
|
7947
|
+
const res = await doFetch(`${opts.baseUrl}${path}`, {
|
|
7948
|
+
method: "POST",
|
|
7949
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
7950
|
+
body: JSON.stringify(body),
|
|
7951
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS)
|
|
7952
|
+
});
|
|
7953
|
+
if (res.ok)
|
|
7954
|
+
return true;
|
|
7955
|
+
if (res.status >= 400 && res.status < 500)
|
|
7956
|
+
return false;
|
|
7957
|
+
} catch {}
|
|
7958
|
+
}
|
|
7959
|
+
return false;
|
|
7960
|
+
}
|
|
7961
|
+
function scheduleFlush() {
|
|
7962
|
+
if (flushTimer)
|
|
7963
|
+
return;
|
|
7964
|
+
flushTimer = setTimeout(() => {
|
|
7965
|
+
flushTimer = undefined;
|
|
7966
|
+
const c = takeCounters();
|
|
7967
|
+
if (!c)
|
|
7968
|
+
return;
|
|
7969
|
+
post("/telemetry/choice/counters", c).then((ok) => {
|
|
7970
|
+
if (!ok)
|
|
7971
|
+
giveBack(c);
|
|
7972
|
+
});
|
|
7973
|
+
if (dirty(counters))
|
|
7974
|
+
scheduleFlush();
|
|
7975
|
+
}, flushMs);
|
|
7976
|
+
flushTimer.unref?.();
|
|
7977
|
+
}
|
|
7978
|
+
function suppress(reason) {
|
|
7979
|
+
counters.suppressed[reason] = (counters.suppressed[reason] ?? 0) + 1;
|
|
7980
|
+
scheduleFlush();
|
|
7981
|
+
}
|
|
7982
|
+
function deliver(event) {
|
|
7983
|
+
setTimeout(() => {
|
|
7984
|
+
const c = takeCounters();
|
|
7985
|
+
post("/telemetry/choice", c ? { ...event, counters: c } : event).then((ok) => {
|
|
7986
|
+
if (ok)
|
|
7987
|
+
return;
|
|
7988
|
+
if (c)
|
|
7989
|
+
giveBack(c);
|
|
7990
|
+
else
|
|
7991
|
+
counters.delivery_failures += 1;
|
|
7992
|
+
scheduleFlush();
|
|
7993
|
+
});
|
|
7994
|
+
}, 0);
|
|
7995
|
+
}
|
|
7996
|
+
pi.on("session_shutdown", async () => {
|
|
7997
|
+
clearTimeout(flushTimer);
|
|
7998
|
+
flushTimer = undefined;
|
|
7999
|
+
const c = takeCounters();
|
|
8000
|
+
if (!c)
|
|
8001
|
+
return;
|
|
8002
|
+
const bound = new Promise((r) => {
|
|
8003
|
+
const t = setTimeout(() => r(false), shutdownBoundMs);
|
|
8004
|
+
t.unref?.();
|
|
8005
|
+
});
|
|
8006
|
+
await Promise.race([post("/telemetry/choice/counters", c, 1), bound]);
|
|
8007
|
+
});
|
|
8008
|
+
pi.registerTool({
|
|
8009
|
+
name: ASK_USER_TOOL_NAME,
|
|
8010
|
+
executionMode: "sequential",
|
|
8011
|
+
label: "Ask User",
|
|
8012
|
+
description: "Ask the user something. With options: pick between them when the request and existing code do not already settle a choice of library, service, language or tool; prefer the best two options with pros, cons and best_for for each (shown side by side), and 3 or 4 only for a wider choice. With no options: ask an open question when the answer would change what you do and you cannot find it in the request or code. Returns the user's answer or choice, or says to decide yourself.",
|
|
8013
|
+
parameters: _Object_({
|
|
8014
|
+
question: String2({ minLength: 1, maxLength: 200 }),
|
|
8015
|
+
options: Optional(_Array_(_Object_({
|
|
8016
|
+
id: Optional(String2({ minLength: 1, maxLength: 40 })),
|
|
8017
|
+
label: String2({ minLength: 1, maxLength: 40 }),
|
|
8018
|
+
note: Optional(String2({ maxLength: 120 })),
|
|
8019
|
+
pros: Optional(_Array_(String2({ minLength: 1, maxLength: 100 }), { maxItems: 4 })),
|
|
8020
|
+
cons: Optional(_Array_(String2({ minLength: 1, maxLength: 100 }), { maxItems: 4 })),
|
|
8021
|
+
best_for: Optional(String2({ maxLength: 120 }))
|
|
8022
|
+
}), { maxItems: 4 })),
|
|
8023
|
+
context: Optional(String2({ maxLength: 300 })),
|
|
8024
|
+
recommended: Optional(Integer({ minimum: 0, maximum: 3 }))
|
|
8025
|
+
}),
|
|
8026
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
8027
|
+
counters.attempted += 1;
|
|
8028
|
+
const text = (t, details = {}) => ({ content: [{ type: "text", text: t }], details });
|
|
8029
|
+
if (params.options?.length === 1)
|
|
8030
|
+
throw new Error("ask_user takes either no options (an open question) or 2 to 4 options. Add a second option, or omit options to ask a free-text question.");
|
|
8031
|
+
const open = !params.options?.length;
|
|
8032
|
+
const options = (params.options ?? []).map((o, i) => ({ id: o.id ?? `opt${i + 1}`, label: o.label.trim(), ...o.note ? { note: o.note } : {}, ...o.pros?.length ? { pros: o.pros } : {}, ...o.cons?.length ? { cons: o.cons } : {}, ...o.best_for ? { best_for: o.best_for } : {} }));
|
|
8033
|
+
const labels = options.map((o) => norm(o.label));
|
|
8034
|
+
if (new Set(labels).size !== labels.length || [AGENT_DECIDES, SOMETHING_ELSE, COMPARE].some((r) => labels.includes(norm(r)))) {
|
|
8035
|
+
suppress("duplicate_labels");
|
|
8036
|
+
throw new Error(`ask_user options must have distinct labels (case-insensitive) and none may be "${AGENT_DECIDES}", "${SOMETHING_ELSE}" or "${COMPARE}". Fix the labels and call again.`);
|
|
8037
|
+
}
|
|
8038
|
+
if (new Set(options.map((o) => o.id)).size !== options.length)
|
|
8039
|
+
throw new Error("ask_user option ids must be distinct. Fix them or omit the ids.");
|
|
8040
|
+
if (opts.headless || !ctx.hasUI || opts.isRoomOpen?.()) {
|
|
8041
|
+
suppress("headless");
|
|
8042
|
+
return text(CHOICE_NO_ANSWER, { suppressed: "headless" });
|
|
8043
|
+
}
|
|
8044
|
+
const state = capsFor(ctx.sessionManager);
|
|
8045
|
+
const setKey = open ? norm(params.question) : [...labels].sort().join("\x00");
|
|
8046
|
+
if ((open ? state.questions : state.sets).has(setKey)) {
|
|
8047
|
+
suppress("repeat");
|
|
8048
|
+
return text(CHOICE_LIMIT_TEXT, { suppressed: "repeat" });
|
|
8049
|
+
}
|
|
8050
|
+
if (open ? state.asked >= MAX_QUESTIONS_PER_SESSION : state.shownCount >= MAX_PER_SESSION) {
|
|
8051
|
+
suppress("capped_session");
|
|
8052
|
+
return text(CHOICE_LIMIT_TEXT, { suppressed: "capped_session" });
|
|
8053
|
+
}
|
|
8054
|
+
const saveCaps = () => pi.appendEntry(CAPS_ENTRY, { shownCount: state.shownCount, sets: [...state.sets], asked: state.asked, questions: [...state.questions] });
|
|
8055
|
+
if (open) {
|
|
8056
|
+
state.asked += 1;
|
|
8057
|
+
state.questions.add(setKey);
|
|
8058
|
+
} else {
|
|
8059
|
+
state.shownCount += 1;
|
|
8060
|
+
state.sets.add(setKey);
|
|
8061
|
+
}
|
|
8062
|
+
saveCaps();
|
|
8063
|
+
counters.shown += 1;
|
|
8064
|
+
const displayOrder = options.map((_, i) => i);
|
|
8065
|
+
for (let i = displayOrder.length - 1;i > 0; i--) {
|
|
8066
|
+
const j = Math.floor(random() * (i + 1));
|
|
8067
|
+
[displayOrder[i], displayOrder[j]] = [displayOrder[j], displayOrder[i]];
|
|
8068
|
+
}
|
|
8069
|
+
const byLabel = new Map(options.map((o, i) => [o.label, i]));
|
|
8070
|
+
const notes = options.filter((o) => o.note).map((o) => ` ${o.label}: ${o.note}`);
|
|
8071
|
+
const title = [params.question, params.context, ...notes.length ? ["", ...notes] : []].filter((l) => l !== undefined).join(`
|
|
8072
|
+
`);
|
|
8073
|
+
const startedAt = now();
|
|
8074
|
+
let picked;
|
|
8075
|
+
let typed;
|
|
8076
|
+
let threw = false;
|
|
8077
|
+
try {
|
|
8078
|
+
const ui = ctx.ui;
|
|
8079
|
+
if (open) {
|
|
8080
|
+
typed = (await ctx.ui.input([params.question, params.context].filter(Boolean).join(`
|
|
8081
|
+
`), "Type your answer", { signal }))?.trim().slice(0, ANSWER_MAX_CHARS);
|
|
8082
|
+
} else if (options.length === 2 && options.some((o) => o.pros || o.cons) && typeof ui.custom === "function") {
|
|
8083
|
+
const sides = displayOrder.map((i) => options[i]);
|
|
8084
|
+
picked = await ctx.ui.custom((tui, theme, _kb, done) => columnsComponent({
|
|
8085
|
+
question: params.question,
|
|
8086
|
+
context: params.context,
|
|
8087
|
+
sides,
|
|
8088
|
+
labels: [sides[0].label, sides[1].label],
|
|
8089
|
+
else_: SOMETHING_ELSE,
|
|
8090
|
+
decide: AGENT_DECIDES,
|
|
8091
|
+
signal
|
|
8092
|
+
}, {
|
|
8093
|
+
accent: (x) => theme.fg("accent", x),
|
|
8094
|
+
success: (x) => theme.fg("success", x),
|
|
8095
|
+
error: (x) => theme.fg("error", x),
|
|
8096
|
+
dim: (x) => theme.fg("dim", x),
|
|
8097
|
+
bold: (x) => theme.bold(x)
|
|
8098
|
+
}, tui, done));
|
|
8099
|
+
} else
|
|
8100
|
+
picked = await ctx.ui.select(title, [...displayOrder.map((i) => options[i].label), SOMETHING_ELSE, COMPARE, AGENT_DECIDES], { signal });
|
|
8101
|
+
if (!open && picked === SOMETHING_ELSE) {
|
|
8102
|
+
typed = (await ctx.ui.input(params.question, "Type your answer", { signal }))?.trim().slice(0, OTHER_MAX_CHARS);
|
|
8103
|
+
}
|
|
8104
|
+
} catch {
|
|
8105
|
+
threw = true;
|
|
8106
|
+
}
|
|
8107
|
+
const latency = Math.min(INT_MAX, Math.max(0, Math.round(now() - startedAt)));
|
|
8108
|
+
let outcome;
|
|
8109
|
+
let chosenIndex = null;
|
|
8110
|
+
if (threw)
|
|
8111
|
+
outcome = "cancel_ambiguous";
|
|
8112
|
+
else if (open)
|
|
8113
|
+
outcome = typed ? "answered" : "aborted";
|
|
8114
|
+
else if (picked === AGENT_DECIDES)
|
|
8115
|
+
outcome = "delegated";
|
|
8116
|
+
else if (picked === COMPARE)
|
|
8117
|
+
outcome = "compare";
|
|
8118
|
+
else if (picked === SOMETHING_ELSE && typed)
|
|
8119
|
+
outcome = "other";
|
|
8120
|
+
else if (picked === SOMETHING_ELSE)
|
|
8121
|
+
outcome = "aborted";
|
|
8122
|
+
else if (picked !== undefined && byLabel.has(picked)) {
|
|
8123
|
+
outcome = "chosen";
|
|
8124
|
+
chosenIndex = byLabel.get(picked);
|
|
8125
|
+
} else if (picked === undefined)
|
|
8126
|
+
outcome = "aborted";
|
|
8127
|
+
else
|
|
8128
|
+
outcome = "cancel_ambiguous";
|
|
8129
|
+
if (outcome === "compare") {
|
|
8130
|
+
state.sets.delete(setKey);
|
|
8131
|
+
state.shownCount -= 1;
|
|
8132
|
+
saveCaps();
|
|
8133
|
+
}
|
|
8134
|
+
const rec = typeof params.recommended === "number" && params.recommended < options.length ? params.recommended : null;
|
|
8135
|
+
const model = ctx.model?.id ?? "unknown";
|
|
8136
|
+
const event = {
|
|
8137
|
+
session_id: opts.sessionId,
|
|
8138
|
+
tool_call_id: toolCallId,
|
|
8139
|
+
question: params.question,
|
|
8140
|
+
context: params.context ?? "",
|
|
8141
|
+
options,
|
|
8142
|
+
display_order: displayOrder,
|
|
8143
|
+
outcome,
|
|
8144
|
+
chosen_index: chosenIndex,
|
|
8145
|
+
...outcome === "other" || outcome === "answered" ? { other_text: typed } : {},
|
|
8146
|
+
agent_pick_index: rec,
|
|
8147
|
+
model,
|
|
8148
|
+
latency_ms: latency,
|
|
8149
|
+
cli_version: CLI_VERSION,
|
|
8150
|
+
arm: opts.arm()
|
|
8151
|
+
};
|
|
8152
|
+
deliver(event);
|
|
8153
|
+
if (outcome === "compare")
|
|
8154
|
+
return text(CHOICE_COMPARE_TEXT, event);
|
|
8155
|
+
return text(outcome === "other" || outcome === "answered" ? `User replied: ${typed}` : chosenIndex !== null ? `User chose: ${options[chosenIndex].label}` : CHOICE_NO_ANSWER, event);
|
|
8156
|
+
}
|
|
8157
|
+
});
|
|
8158
|
+
}
|
|
8159
|
+
};
|
|
8160
|
+
}
|
|
8161
|
+
|
|
8263
8162
|
// src/vision-tool.ts
|
|
8264
8163
|
import { closeSync, fstatSync, openSync, readSync, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
|
|
8265
8164
|
import { isAbsolute, relative, resolve, sep as sep2 } from "node:path";
|
|
@@ -8651,14 +8550,14 @@ function createDocsToolExtension() {
|
|
|
8651
8550
|
}
|
|
8652
8551
|
|
|
8653
8552
|
// src/todo-tool.ts
|
|
8654
|
-
import { truncateToWidth as
|
|
8553
|
+
import { truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
|
|
8655
8554
|
|
|
8656
8555
|
// src/todo-view.ts
|
|
8657
|
-
import { truncateToWidth as
|
|
8556
|
+
import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
|
|
8658
8557
|
var PLAIN_STYLE = { bold: (s) => s, dim: (s) => s, accent: (s) => s, error: (s) => s };
|
|
8659
8558
|
var GAP = " ";
|
|
8660
8559
|
function truncate(text, max) {
|
|
8661
|
-
return max <= 0 ? "" :
|
|
8560
|
+
return max <= 0 ? "" : truncateToWidth4(text, max, "…");
|
|
8662
8561
|
}
|
|
8663
8562
|
function counts(todos) {
|
|
8664
8563
|
return { done: todos.filter((t) => t.done).length, total: todos.length };
|
|
@@ -8670,7 +8569,7 @@ function widgetLines(todos, width, st = PLAIN_STYLE) {
|
|
|
8670
8569
|
const header = `Todos ${done}/${total}`;
|
|
8671
8570
|
const firstOpen = todos.findIndex((t) => !t.done);
|
|
8672
8571
|
const marks = todos.map((t, i) => t.done ? `✓ ${t.text}` : i === firstOpen ? `▸ ${t.text}` : `○ ${t.text}`);
|
|
8673
|
-
const compactLength =
|
|
8572
|
+
const compactLength = visibleWidth2([header, ...marks].join(GAP));
|
|
8674
8573
|
if (compactLength <= width) {
|
|
8675
8574
|
return [
|
|
8676
8575
|
[
|
|
@@ -8695,7 +8594,7 @@ function widgetLines(todos, width, st = PLAIN_STYLE) {
|
|
|
8695
8594
|
return lines;
|
|
8696
8595
|
}
|
|
8697
8596
|
function resultLines(d, st = PLAIN_STYLE, width = Infinity) {
|
|
8698
|
-
return rawResultLines(d, st).map((line) => Number.isFinite(width) ?
|
|
8597
|
+
return rawResultLines(d, st).map((line) => Number.isFinite(width) ? truncateToWidth4(line, Math.max(0, width), "…") : line);
|
|
8699
8598
|
}
|
|
8700
8599
|
function rawResultLines(d, st) {
|
|
8701
8600
|
if (d.error)
|
|
@@ -8892,7 +8791,7 @@ function createTodoToolExtension(options = {}) {
|
|
|
8892
8791
|
const details = res.details;
|
|
8893
8792
|
const text = res.content[0]?.type === "text" ? res.content[0].text : "";
|
|
8894
8793
|
return {
|
|
8895
|
-
render: (width) => details?.action ? resultLines(details, styleFrom(theme), width) : [
|
|
8794
|
+
render: (width) => details?.action ? resultLines(details, styleFrom(theme), width) : [truncateToWidth5(text, width, "…")],
|
|
8896
8795
|
invalidate() {}
|
|
8897
8796
|
};
|
|
8898
8797
|
}
|
|
@@ -9015,40 +8914,237 @@ function createToolAliasesExtension(opts = {}) {
|
|
|
9015
8914
|
};
|
|
9016
8915
|
}
|
|
9017
8916
|
|
|
9018
|
-
// src/
|
|
9019
|
-
var
|
|
9020
|
-
var
|
|
9021
|
-
var
|
|
9022
|
-
|
|
9023
|
-
|
|
8917
|
+
// src/fusion.ts
|
|
8918
|
+
var FUSION_ID = "fusion";
|
|
8919
|
+
var FUSION_PICKER_ID = "\uD83E\uDDE8 fusion";
|
|
8920
|
+
var FUSION_NAME = "\uD83E\uDDE8 Fusion · DeepSeek Pro plans, Flash builds";
|
|
8921
|
+
var FUSION_FALLBACK_NAME = "\uD83E\uDDE8 Fusion · DeepSeek Flash plans and builds";
|
|
8922
|
+
var FUSION_PLANNER_ID = "deepseek-v4-pro";
|
|
8923
|
+
var FUSION_EXECUTOR_ID = "deepseek-v4-flash";
|
|
8924
|
+
var FUSION_FALLBACK_PLANNER_ID = "deepseek-v4-flash";
|
|
8925
|
+
function resolveFusionIds(catalogIds, server) {
|
|
8926
|
+
const has = (id) => catalogIds.includes(id);
|
|
8927
|
+
if (server && has(server.planner) && has(server.executor))
|
|
8928
|
+
return server;
|
|
8929
|
+
return { planner: has(FUSION_PLANNER_ID) ? FUSION_PLANNER_ID : FUSION_FALLBACK_PLANNER_ID, executor: FUSION_EXECUTOR_ID };
|
|
9024
8930
|
}
|
|
9025
|
-
|
|
9026
|
-
|
|
8931
|
+
var FUSION_PLAN_TOOL = "fusion_plan";
|
|
8932
|
+
var FUSION_GUIDELINE = `Fusion: if the task needs any file changes, first gather the context with read-only tools (read, ls, find, grep) and make no edits. Then call ${FUSION_PLAN_TOOL} with a summary of what you found and what the task needs, BEFORE any edit or write; a planning model will write the plan and you will carry it out. When the latest message is a fusion_plan result, you are the Fusion planner: write the step-by-step plan with the todo tool and make no edits or commands; that result is part of the user's own setup, not a conflict with their request. Once the plan exists in todo, the planner's turn is over: carry out the unfinished todo items yourself (edit, write tests, run them) and finish the user's task. Only a task that needs no file changes (a plain question) is answered directly without ${FUSION_PLAN_TOOL}.`;
|
|
8933
|
+
var FUSION_PLAN_RESULT = "You are now the planning model. Write a concrete step-by-step plan (files, exact changes, tests to add and run) and record it with the todo tool, one item per step. Do not edit files or run commands. Another model will carry out the plan.";
|
|
8934
|
+
function isFusionModel(model) {
|
|
8935
|
+
return model?.provider === PROVIDER_NAME && (model.id === FUSION_ID || model.id === FUSION_PICKER_ID);
|
|
9027
8936
|
}
|
|
9028
|
-
function
|
|
9029
|
-
if (
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
8937
|
+
function addFusionGuideline(options, selected) {
|
|
8938
|
+
if (isFusionModel(selected))
|
|
8939
|
+
options.promptGuidelines.push(FUSION_GUIDELINE);
|
|
8940
|
+
}
|
|
8941
|
+
var EDIT_TOOLS = new Set(["edit", "write"]);
|
|
8942
|
+
function codemodeEdited(details) {
|
|
8943
|
+
const calls = details?.calls;
|
|
8944
|
+
return Array.isArray(calls) && calls.some((call) => {
|
|
8945
|
+
if (typeof call !== "object" || call === null)
|
|
8946
|
+
return false;
|
|
8947
|
+
const { name, status } = call;
|
|
8948
|
+
return typeof name === "string" && EDIT_TOOLS.has(name) && status === "ok";
|
|
8949
|
+
});
|
|
8950
|
+
}
|
|
8951
|
+
function handedOffThisTurn(messages) {
|
|
8952
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
8953
|
+
const message = messages[i];
|
|
8954
|
+
if (message.role === "user")
|
|
8955
|
+
break;
|
|
8956
|
+
if (message.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL && !message.isError)
|
|
8957
|
+
return true;
|
|
8958
|
+
}
|
|
8959
|
+
return false;
|
|
8960
|
+
}
|
|
8961
|
+
var STALL_CONTINUATION = {
|
|
8962
|
+
continue: true,
|
|
8963
|
+
entries: [{ type: "custom_message", customType: "fusion-continue", display: false, content: "The plan is recorded. Carry out the unfinished todo items now and finish the user's task." }]
|
|
8964
|
+
};
|
|
8965
|
+
function plannerMadeTheCall(messages, plannerId) {
|
|
8966
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
8967
|
+
const message = messages[i];
|
|
8968
|
+
if (message.role === "assistant")
|
|
8969
|
+
return message.provider === PROVIDER_NAME && message.model === plannerId;
|
|
8970
|
+
}
|
|
8971
|
+
return false;
|
|
8972
|
+
}
|
|
8973
|
+
function branchMessages(ctx) {
|
|
8974
|
+
return ctx.sessionManager.getBranch().flatMap((entry) => entry.type === "message" ? [entry.message] : []);
|
|
8975
|
+
}
|
|
8976
|
+
function alreadyContinued(ctx) {
|
|
8977
|
+
const branch = ctx.sessionManager.getBranch();
|
|
8978
|
+
for (let i = branch.length - 1;i >= 0; i--) {
|
|
8979
|
+
const entry = branch[i];
|
|
8980
|
+
if (entry.type === "custom_message" && entry.customType === STALL_CONTINUATION.entries[0].customType)
|
|
8981
|
+
return true;
|
|
8982
|
+
const message = entry.type === "message" ? entry.message : undefined;
|
|
8983
|
+
if (message?.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL)
|
|
8984
|
+
return false;
|
|
8985
|
+
}
|
|
8986
|
+
return false;
|
|
8987
|
+
}
|
|
8988
|
+
function plannerStalled(messages, plannerId) {
|
|
8989
|
+
const last = messages[messages.length - 1];
|
|
8990
|
+
if (!last || last.role !== "assistant" || last.provider !== PROVIDER_NAME || last.model !== plannerId)
|
|
8991
|
+
return false;
|
|
8992
|
+
if (last.stopReason !== "stop")
|
|
8993
|
+
return false;
|
|
8994
|
+
return handedOffThisTurn(messages) && !last.content.some((part) => part.type === "toolCall");
|
|
8995
|
+
}
|
|
8996
|
+
function editedThisTurn(messages) {
|
|
8997
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
8998
|
+
const message = messages[i];
|
|
8999
|
+
if (message.role === "user")
|
|
9000
|
+
break;
|
|
9001
|
+
if (message.role !== "toolResult")
|
|
9002
|
+
continue;
|
|
9003
|
+
if (message.toolName === "codemode" ? codemodeEdited(message.details) : EDIT_TOOLS.has(message.toolName) && !message.isError) {
|
|
9004
|
+
return true;
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
return false;
|
|
9008
|
+
}
|
|
9009
|
+
function hasNewUserInput(request) {
|
|
9010
|
+
const failedAt = request.failed?.message.timestamp;
|
|
9011
|
+
if (failedAt === undefined)
|
|
9012
|
+
return false;
|
|
9013
|
+
for (let i = request.messages.length - 1;i >= 0; i--) {
|
|
9014
|
+
const message = request.messages[i];
|
|
9015
|
+
if (message.role === "assistant")
|
|
9016
|
+
return false;
|
|
9017
|
+
if (message.role === "user")
|
|
9018
|
+
return message.timestamp > failedAt;
|
|
9019
|
+
}
|
|
9020
|
+
return false;
|
|
9021
|
+
}
|
|
9022
|
+
function routeFusion(request, models) {
|
|
9023
|
+
const phase = request.state?.phase ?? "explore";
|
|
9024
|
+
const restart = request.reason === "user" || request.reason === "retry" && hasNewUserInput(request);
|
|
9025
|
+
const route = (model, nextPhase = phase) => ({
|
|
9026
|
+
model,
|
|
9027
|
+
thinkingLevel: request.thinkingLevel,
|
|
9028
|
+
state: request.reason === "direct" ? undefined : request.state?.phase === nextPhase ? request.state : { phase: nextPhase }
|
|
9029
|
+
});
|
|
9030
|
+
if (!models.planner || !models.executor) {
|
|
9031
|
+
return route(models.defaultModel, restart ? "explore" : phase);
|
|
9032
|
+
}
|
|
9033
|
+
switch (request.reason) {
|
|
9034
|
+
case "user":
|
|
9035
|
+
return route(models.executor, "explore");
|
|
9036
|
+
case "direct":
|
|
9037
|
+
return route(models.executor);
|
|
9038
|
+
case "retry": {
|
|
9039
|
+
if (restart)
|
|
9040
|
+
return route(models.executor, "explore");
|
|
9041
|
+
const failed = request.failed;
|
|
9042
|
+
if (!failed || failed.model.provider === PROVIDER_NAME && failed.model.id === models.planner?.id) {
|
|
9043
|
+
return route(models.executor, "build");
|
|
9044
|
+
}
|
|
9045
|
+
return { ...route(failed.model), thinkingLevel: failed.thinkingLevel ?? request.thinkingLevel };
|
|
9046
|
+
}
|
|
9047
|
+
case "continuation":
|
|
9048
|
+
if (phase !== "explore")
|
|
9049
|
+
return route(models.executor, "build");
|
|
9050
|
+
if (editedThisTurn(request.messages))
|
|
9051
|
+
return route(models.executor, "build");
|
|
9052
|
+
return handedOffThisTurn(request.messages) ? route(models.planner, "plan") : route(models.executor, "explore");
|
|
9053
|
+
}
|
|
9054
|
+
}
|
|
9055
|
+
function createFusionExtension(defaultModelId, ids = resolveFusionIds([])) {
|
|
9056
|
+
const name = ids.planner === FUSION_PLANNER_ID && ids.executor === FUSION_EXECUTOR_ID ? FUSION_NAME : ids.planner === FUSION_FALLBACK_PLANNER_ID ? FUSION_FALLBACK_NAME : "\uD83E\uDDE8 Fusion";
|
|
9057
|
+
return {
|
|
9058
|
+
name: "free-pi-fusion",
|
|
9059
|
+
factory: (pi) => {
|
|
9060
|
+
const syncPlanTool = (selected) => {
|
|
9061
|
+
const on = isFusionModel(selected);
|
|
9062
|
+
const active = pi.getActiveTools();
|
|
9063
|
+
if (on === active.includes(FUSION_PLAN_TOOL))
|
|
9064
|
+
return;
|
|
9065
|
+
pi.setActiveTools(on ? [...active, FUSION_PLAN_TOOL] : active.filter((name) => name !== FUSION_PLAN_TOOL));
|
|
9066
|
+
};
|
|
9067
|
+
pi.on("session_start", (_event, ctx) => syncPlanTool(ctx.model));
|
|
9068
|
+
pi.on("model_select", (event) => syncPlanTool(event.model));
|
|
9069
|
+
pi.registerTool({
|
|
9070
|
+
name: FUSION_PLAN_TOOL,
|
|
9071
|
+
label: "Fusion plan",
|
|
9072
|
+
description: "Hand off to the planning model once you have gathered enough context. Takes a summary of what you found and what the task needs.",
|
|
9073
|
+
promptSnippet: "Fusion only: hand off to the planning model after read-only exploration",
|
|
9074
|
+
exposure: "model-only",
|
|
9075
|
+
parameters: _Object_({ summary: String2({ description: "What you found and what the task needs" }) }),
|
|
9076
|
+
async execute() {
|
|
9077
|
+
return { content: [{ type: "text", text: FUSION_PLAN_RESULT }], details: {} };
|
|
9078
|
+
}
|
|
9079
|
+
});
|
|
9080
|
+
const isFusion = isFusionModel;
|
|
9081
|
+
pi.on("before_agent_start", (event, ctx) => addFusionGuideline(event.systemPromptOptions, ctx.model));
|
|
9082
|
+
pi.on("tool_call", (event, ctx) => {
|
|
9083
|
+
if (!isFusion(ctx.model) || !EDIT_TOOLS.has(event.toolName))
|
|
9084
|
+
return;
|
|
9085
|
+
const messages = branchMessages(ctx);
|
|
9086
|
+
if (plannerMadeTheCall(messages, ids.planner))
|
|
9087
|
+
return { block: true, reason: "Planner: record the plan with todo only; the executor makes the changes." };
|
|
9088
|
+
if (!handedOffThisTurn(messages)) {
|
|
9089
|
+
return { block: true, reason: `Blocked: call ${FUSION_PLAN_TOOL} first (summarize what you found); a planning model writes the plan before any edit.` };
|
|
9090
|
+
}
|
|
9091
|
+
});
|
|
9092
|
+
pi.on("agent_before_settle", (event, ctx) => isFusion(ctx.model) && event.outcome === "completed" && !alreadyContinued(ctx) && plannerStalled(branchMessages(ctx), ids.planner) ? STALL_CONTINUATION : undefined);
|
|
9093
|
+
for (const id of [FUSION_PICKER_ID, FUSION_ID]) {
|
|
9094
|
+
pi.registerVirtualModel({
|
|
9095
|
+
provider: PROVIDER_NAME,
|
|
9096
|
+
id,
|
|
9097
|
+
name,
|
|
9098
|
+
input: ["text"],
|
|
9099
|
+
route: (request, ctx) => routeFusion(request, {
|
|
9100
|
+
planner: ctx.modelRegistry.find(PROVIDER_NAME, ids.planner),
|
|
9101
|
+
executor: ctx.modelRegistry.find(PROVIDER_NAME, ids.executor),
|
|
9102
|
+
defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
|
|
9103
|
+
})
|
|
9104
|
+
});
|
|
9105
|
+
}
|
|
9106
|
+
}
|
|
9107
|
+
};
|
|
9108
|
+
}
|
|
9109
|
+
|
|
9110
|
+
// src/error-notice.ts
|
|
9111
|
+
var UPSTREAM_ERROR_FRAME_MESSAGE = "upstream returned an error frame";
|
|
9112
|
+
var PROVIDER_FAILING_NOTICE = "This model's provider is failing right now. Try again in a moment, or switch models with /model.";
|
|
9113
|
+
var FREE_OFFER_TITLE = "Switch to the free model and resend this prompt?";
|
|
9114
|
+
var FREE_OFFER_ACCEPT = "Switch and resend";
|
|
9115
|
+
var FREE_OFFER_DECLINE = "Stay on this model";
|
|
9116
|
+
var FREE_OFFER_SWITCHED = "Switched to the free model. Sending your prompt again.";
|
|
9117
|
+
var NOTICE_DEDUPE_MS = 15000;
|
|
9118
|
+
function optionalNumber(value) {
|
|
9119
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9120
|
+
}
|
|
9121
|
+
function optionalString(value) {
|
|
9122
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
9123
|
+
}
|
|
9124
|
+
function parseErrorEnvelope(errorMessage) {
|
|
9125
|
+
if (typeof errorMessage !== "string")
|
|
9126
|
+
return;
|
|
9127
|
+
const m = /^(\d{3}): (\{[\s\S]*\})\s*$/.exec(errorMessage);
|
|
9128
|
+
if (!m)
|
|
9129
|
+
return;
|
|
9130
|
+
try {
|
|
9131
|
+
const body = JSON.parse(m[2]);
|
|
9132
|
+
const message = body.message ?? body.error?.message;
|
|
9133
|
+
const code = body.code ?? body.error?.code;
|
|
9134
|
+
if (typeof message !== "string" || message.trim() === "")
|
|
9135
|
+
return;
|
|
9136
|
+
const parsed = {
|
|
9137
|
+
status: Number(m[1]),
|
|
9138
|
+
code: typeof code === "string" ? code : "unknown",
|
|
9139
|
+
message
|
|
9140
|
+
};
|
|
9141
|
+
const limit = optionalString(body.limit);
|
|
9142
|
+
const retryAfter = optionalNumber(body.retry_after_seconds);
|
|
9143
|
+
const clearsAt = optionalString(body.clears_at);
|
|
9144
|
+
const remaining = optionalNumber(body.remaining_credits_today);
|
|
9145
|
+
const allowance = optionalNumber(body.allowance_credits);
|
|
9146
|
+
if (limit)
|
|
9147
|
+
parsed.limit = limit;
|
|
9052
9148
|
if (retryAfter !== undefined)
|
|
9053
9149
|
parsed.retry_after_seconds = retryAfter;
|
|
9054
9150
|
if (clearsAt)
|
|
@@ -9107,15 +9203,51 @@ var LIMIT_CODES = new Set([
|
|
|
9107
9203
|
"context_too_long",
|
|
9108
9204
|
"reward_required"
|
|
9109
9205
|
]);
|
|
9110
|
-
var SERVER_MESSAGE_CODES = new Set(["reward_required", "premium_allowance", "model_at_capacity", "context_too_long_for_fallback", "context_too_long"]);
|
|
9206
|
+
var SERVER_MESSAGE_CODES = new Set(["concurrent", "reward_required", "premium_allowance", "model_at_capacity", "context_too_long_for_fallback", "context_too_long"]);
|
|
9207
|
+
var FREE_OFFER_CODES = new Set(["daily_cap", "queue_wait", "rolling_cap", "premium_allowance", "model_at_capacity", "model_requires_credits"]);
|
|
9208
|
+
var isFusion = (model) => model?.provider === PROVIDER_NAME && (model.id === FUSION_ID || model.id === FUSION_PICKER_ID);
|
|
9209
|
+
function lastUserEntryId(ctx) {
|
|
9210
|
+
const branch = ctx.sessionManager.getBranch();
|
|
9211
|
+
for (let i = branch.length - 1;i >= 0; i--) {
|
|
9212
|
+
if (branch[i].type === "message" && branch[i].message?.role === "user")
|
|
9213
|
+
return branch[i].id;
|
|
9214
|
+
}
|
|
9215
|
+
return "";
|
|
9216
|
+
}
|
|
9111
9217
|
function createErrorNoticeExtension(opts = {}) {
|
|
9112
9218
|
const lastAt = new Map;
|
|
9113
9219
|
const now = opts.now ?? Date.now;
|
|
9114
9220
|
const timeZone = opts.timeZone;
|
|
9221
|
+
const offered = new Set;
|
|
9222
|
+
let lastTurnEntryId;
|
|
9223
|
+
let acceptedEntryId;
|
|
9224
|
+
async function offerFreeModel(pi, env, entryId, ctx) {
|
|
9225
|
+
if (!opts.freeModels || opts.terminal !== true || !FREE_OFFER_CODES.has(env.code) || ctx.mode !== "tui" || !ctx.hasUI || isFusion(ctx.model))
|
|
9226
|
+
return;
|
|
9227
|
+
const free = opts.freeModels();
|
|
9228
|
+
const target = free[0];
|
|
9229
|
+
if (!target || free.some((m) => m.provider === ctx.model?.provider && m.id === ctx.model?.id))
|
|
9230
|
+
return;
|
|
9231
|
+
const prompt = lastUserEntryId(ctx);
|
|
9232
|
+
if (offered.has(prompt))
|
|
9233
|
+
return;
|
|
9234
|
+
offered.add(prompt);
|
|
9235
|
+
const choice = await ctx.ui.select(FREE_OFFER_TITLE, [FREE_OFFER_ACCEPT, FREE_OFFER_DECLINE]).catch(() => {
|
|
9236
|
+
return;
|
|
9237
|
+
});
|
|
9238
|
+
if (choice !== FREE_OFFER_ACCEPT)
|
|
9239
|
+
return;
|
|
9240
|
+
const model = ctx.modelRegistry.find(target.provider, target.id);
|
|
9241
|
+
if (!model || !await pi.setModel(model))
|
|
9242
|
+
return;
|
|
9243
|
+
ctx.ui.notify(FREE_OFFER_SWITCHED, "info");
|
|
9244
|
+
acceptedEntryId = entryId;
|
|
9245
|
+
}
|
|
9115
9246
|
return {
|
|
9116
9247
|
name: "free-pi-error-notice",
|
|
9117
9248
|
factory(pi) {
|
|
9118
|
-
pi.on("turn_end", (event, ctx) => {
|
|
9249
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
9250
|
+
lastTurnEntryId = event.messageEntryId;
|
|
9119
9251
|
const msg = event.message;
|
|
9120
9252
|
if (!msg || msg.stopReason !== "error")
|
|
9121
9253
|
return;
|
|
@@ -9123,13 +9255,23 @@ function createErrorNoticeExtension(opts = {}) {
|
|
|
9123
9255
|
if (!env)
|
|
9124
9256
|
return;
|
|
9125
9257
|
const t = now();
|
|
9126
|
-
if (t - (lastAt.get(env.code) ?? -Infinity)
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
const text = (env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : formatLimitEnvelope(env, { timeZone })) + (extra ? `
|
|
9258
|
+
if (t - (lastAt.get(env.code) ?? -Infinity) >= NOTICE_DEDUPE_MS) {
|
|
9259
|
+
lastAt.set(env.code, t);
|
|
9260
|
+
const extra = opts.extraNotice?.(env);
|
|
9261
|
+
const text = (env.code === "upstream_error" ? PROVIDER_FAILING_NOTICE : formatLimitEnvelope(env, { timeZone })) + (extra ? `
|
|
9131
9262
|
${extra}` : "");
|
|
9132
|
-
|
|
9263
|
+
ctx.ui.notify(text, LIMIT_CODES.has(env.code) ? "info" : env.status >= 500 ? "error" : "warning");
|
|
9264
|
+
}
|
|
9265
|
+
await offerFreeModel(pi, env, event.messageEntryId, ctx);
|
|
9266
|
+
});
|
|
9267
|
+
if (!opts.freeModels)
|
|
9268
|
+
return;
|
|
9269
|
+
pi.on("agent_before_settle", (event) => {
|
|
9270
|
+
const entryId = acceptedEntryId;
|
|
9271
|
+
acceptedEntryId = undefined;
|
|
9272
|
+
if (!entryId || event.outcome !== "error" || entryId !== lastTurnEntryId)
|
|
9273
|
+
return;
|
|
9274
|
+
return { entries: [{ type: "context_edit", targetId: entryId, replacement: null }], continue: true };
|
|
9133
9275
|
});
|
|
9134
9276
|
}
|
|
9135
9277
|
};
|
|
@@ -9533,31 +9675,84 @@ function registerBugAutocomplete(pi) {
|
|
|
9533
9675
|
}
|
|
9534
9676
|
|
|
9535
9677
|
// src/footer-align.ts
|
|
9536
|
-
import { truncateToWidth as
|
|
9678
|
+
import { truncateToWidth as truncateToWidth6, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
|
|
9537
9679
|
var MIN_GAP = 2;
|
|
9538
9680
|
var MIN_CWD = 8;
|
|
9539
9681
|
var LEADING_SGR = /^(?:\x1b\[[0-9;]*m)+/;
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
const
|
|
9682
|
+
var dim = (s) => `\x1B[2m${s}\x1B[22m`;
|
|
9683
|
+
function footerLines(lines, width, freeName) {
|
|
9684
|
+
return footerLinesRaw(lines, width, freeName).map((l) => visibleWidth3(l) > width ? truncateToWidth6(l, Math.max(0, width), "…") : l);
|
|
9685
|
+
}
|
|
9686
|
+
function footerLinesRaw(lines, width, freeName) {
|
|
9687
|
+
const cwd = lines[0] ?? "";
|
|
9688
|
+
const status = lines.length >= 3 ? lines[lines.length - 1].replace("credits left today", "credits left").replace(/([AP]M) [A-Z]{2,5}\b/, "$1") : "";
|
|
9689
|
+
const tag = freeName ? dim(`\uD83D\uDD25 ${freeName.toLowerCase()} · free`) : "";
|
|
9690
|
+
const right = status && tag ? `${status}${dim(" · ")}${tag}` : status || tag;
|
|
9691
|
+
if (!right)
|
|
9692
|
+
return [cwd];
|
|
9693
|
+
const rightW = visibleWidth3(right);
|
|
9546
9694
|
let left = cwd;
|
|
9547
|
-
if (
|
|
9548
|
-
const room = width - MIN_GAP -
|
|
9695
|
+
if (visibleWidth3(left) + MIN_GAP + rightW > width) {
|
|
9696
|
+
const room = width - MIN_GAP - rightW;
|
|
9549
9697
|
if (room < MIN_CWD)
|
|
9550
|
-
return
|
|
9698
|
+
return [cwd, right];
|
|
9551
9699
|
const style = LEADING_SGR.exec(cwd)?.[0] ?? "";
|
|
9552
|
-
left =
|
|
9700
|
+
left = truncateToWidth6(cwd, room, style ? `${style}...\x1B[0m` : "...");
|
|
9553
9701
|
}
|
|
9554
|
-
|
|
9555
|
-
return [left + gap + status, stats, ...lines.slice(2, -1)];
|
|
9702
|
+
return [left + " ".repeat(Math.max(MIN_GAP, width - visibleWidth3(left) - rightW)) + right];
|
|
9556
9703
|
}
|
|
9557
|
-
function installFooterRightAlign(mode) {
|
|
9704
|
+
function installFooterRightAlign(mode, freeName = () => {
|
|
9705
|
+
return;
|
|
9706
|
+
}) {
|
|
9558
9707
|
const footer = mode.footer;
|
|
9559
9708
|
const render = footer.render.bind(footer);
|
|
9560
|
-
footer.render = (w) =>
|
|
9709
|
+
footer.render = (w) => footerLines(render(w), w, freeName());
|
|
9710
|
+
}
|
|
9711
|
+
var INPUT_PLACEHOLDER = 'Try "explain this repo" · /help for commands';
|
|
9712
|
+
function withPlaceholder(lines, width, empty) {
|
|
9713
|
+
if (!empty || lines.length < 3)
|
|
9714
|
+
return lines;
|
|
9715
|
+
const base = lines[1].replace(/ +$/, "");
|
|
9716
|
+
const room = Math.max(0, width - visibleWidth3(base) - 1);
|
|
9717
|
+
return [lines[0], `${base} ${dim(truncateToWidth6(INPUT_PLACEHOLDER, room, "…"))}`, ...lines.slice(2)];
|
|
9718
|
+
}
|
|
9719
|
+
function installEditorPlaceholder(mode) {
|
|
9720
|
+
const ed = mode.defaultEditor;
|
|
9721
|
+
const render = ed.render.bind(ed);
|
|
9722
|
+
ed.render = (w) => withPlaceholder(render(w), w, ed.getText() === "");
|
|
9723
|
+
}
|
|
9724
|
+
function bottomPadding(headerHeight, otherHeight, rows) {
|
|
9725
|
+
return Math.max(0, rows - headerHeight - otherHeight);
|
|
9726
|
+
}
|
|
9727
|
+
function installBottomPin(mode) {
|
|
9728
|
+
const m = mode;
|
|
9729
|
+
const render = m.headerContainer.render.bind(m.headerContainer);
|
|
9730
|
+
let measuring = false;
|
|
9731
|
+
m.headerContainer.render = (w) => {
|
|
9732
|
+
const lines = render(w);
|
|
9733
|
+
const rows = process.stdout.rows;
|
|
9734
|
+
if (!rows || measuring)
|
|
9735
|
+
return lines;
|
|
9736
|
+
measuring = true;
|
|
9737
|
+
try {
|
|
9738
|
+
return pad(lines, w, rows);
|
|
9739
|
+
} finally {
|
|
9740
|
+
measuring = false;
|
|
9741
|
+
}
|
|
9742
|
+
};
|
|
9743
|
+
const pad = (lines, w, rows) => {
|
|
9744
|
+
const below = [
|
|
9745
|
+
...m.documentContainer.children.filter((c) => c !== m.headerContainer),
|
|
9746
|
+
...["pendingMessagesContainer", "statusContainer", "widgetContainerAbove", "editorContainer", "widgetContainerBelow", "footerContainer"].map((k) => m[k])
|
|
9747
|
+
];
|
|
9748
|
+
let other = 0;
|
|
9749
|
+
for (const c of below) {
|
|
9750
|
+
other += c.render(w).length;
|
|
9751
|
+
if (lines.length + other >= rows)
|
|
9752
|
+
return lines;
|
|
9753
|
+
}
|
|
9754
|
+
return [...lines, ...Array(bottomPadding(lines.length, other, rows)).fill("")];
|
|
9755
|
+
};
|
|
9561
9756
|
}
|
|
9562
9757
|
|
|
9563
9758
|
// src/guest-handoff.ts
|
|
@@ -9648,6 +9843,20 @@ function mergeAttribution(a, b) {
|
|
|
9648
9843
|
|
|
9649
9844
|
// src/changelog.ts
|
|
9650
9845
|
var CHANGELOG_HIGHLIGHTS = [
|
|
9846
|
+
{
|
|
9847
|
+
version: "0.6.16",
|
|
9848
|
+
highlights: [
|
|
9849
|
+
"New premium model: DeepSeek V4.1 Flash Ultrafast. Find it in /model as \uD83D\uDD25\uD83D\uDD25\uD83D\uDD25 ultrafast. It uses premium credits only.",
|
|
9850
|
+
"Subagents now need credits: they use premium credit, and up to 4 run at once.",
|
|
9851
|
+
"Fusion plans with DeepSeek Flash, since the Claude models are retired."
|
|
9852
|
+
]
|
|
9853
|
+
},
|
|
9854
|
+
{
|
|
9855
|
+
version: "0.6.15",
|
|
9856
|
+
highlights: [
|
|
9857
|
+
"A cleaner start: the screen clears and a one-line header shows the model. Commands are behind /help, and the status line shows your folder and credits."
|
|
9858
|
+
]
|
|
9859
|
+
},
|
|
9651
9860
|
{
|
|
9652
9861
|
version: "0.6.14",
|
|
9653
9862
|
highlights: [
|
|
@@ -10054,6 +10263,14 @@ async function submitConsent(baseUrl, sessionId, consentVersion, attribution = {
|
|
|
10054
10263
|
throw new Error(refusalMessage(res.status, await readRefusal(res)) ?? `consent submission failed: HTTP ${res.status}`);
|
|
10055
10264
|
}
|
|
10056
10265
|
}
|
|
10266
|
+
function deviceReport() {
|
|
10267
|
+
try {
|
|
10268
|
+
const { timeZone, locale } = Intl.DateTimeFormat().resolvedOptions();
|
|
10269
|
+
return { ...timeZone ? { timezone: timeZone } : {}, ...locale ? { locale } : {} };
|
|
10270
|
+
} catch {
|
|
10271
|
+
return {};
|
|
10272
|
+
}
|
|
10273
|
+
}
|
|
10057
10274
|
|
|
10058
10275
|
class GuestUnavailableError extends Error {
|
|
10059
10276
|
reason;
|
|
@@ -10066,7 +10283,8 @@ async function startGuestSession(baseUrl, consentVersion, attribution = {}) {
|
|
|
10066
10283
|
const res = await postJsonRetry(baseUrl, "/auth/guest", {
|
|
10067
10284
|
consent_version: consentVersion,
|
|
10068
10285
|
...attribution.signup_src ? { signup_src: attribution.signup_src } : {},
|
|
10069
|
-
...attribution.referral_code ? { referral_code: attribution.referral_code } : {}
|
|
10286
|
+
...attribution.referral_code ? { referral_code: attribution.referral_code } : {},
|
|
10287
|
+
...deviceReport()
|
|
10070
10288
|
});
|
|
10071
10289
|
if (res.status === 403) {
|
|
10072
10290
|
const body = await readRefusal(res);
|
|
@@ -10147,7 +10365,7 @@ async function pollForToken(baseUrl, sessionId, interval, sleep, opts = {}) {
|
|
|
10147
10365
|
res = await doFetch(new URL(opts.endpoint ?? "/auth/token", baseUrl), {
|
|
10148
10366
|
method: "POST",
|
|
10149
10367
|
headers,
|
|
10150
|
-
body: JSON.stringify({ session_id: sessionId, ...opts.pollSecret ? { poll_secret: opts.pollSecret } : {} }),
|
|
10368
|
+
body: JSON.stringify({ session_id: sessionId, ...opts.pollSecret ? { poll_secret: opts.pollSecret } : {}, ...deviceReport() }),
|
|
10151
10369
|
signal: opts.signal
|
|
10152
10370
|
});
|
|
10153
10371
|
consecutiveNetworkFailures = 0;
|
|
@@ -10212,9 +10430,13 @@ async function checkToken(baseUrl, jwt) {
|
|
|
10212
10430
|
const v = body?.consent_version;
|
|
10213
10431
|
return typeof v === "string" || v === null ? v : undefined;
|
|
10214
10432
|
}
|
|
10215
|
-
async function fetchMe2(baseUrl, jwt) {
|
|
10433
|
+
async function fetchMe2(baseUrl, jwt, o = { env: process.env, configPath: getConfigPath() }) {
|
|
10434
|
+
const optedOut = o.env.FREEPI_NO_CHOICE === "1" || !readChoicePrompts(o.configPath);
|
|
10216
10435
|
const res = await fetch(new URL("/me", baseUrl), {
|
|
10217
|
-
headers: {
|
|
10436
|
+
headers: {
|
|
10437
|
+
authorization: `Bearer ${jwt}`,
|
|
10438
|
+
...optedOut ? {} : { "x-freepi-features": "choice-prompts" }
|
|
10439
|
+
}
|
|
10218
10440
|
});
|
|
10219
10441
|
if (res.status === 401)
|
|
10220
10442
|
throw new UnauthorizedError;
|
|
@@ -10427,6 +10649,12 @@ function formatExitRefillCard(session, end, timeZone) {
|
|
|
10427
10649
|
timeZone
|
|
10428
10650
|
});
|
|
10429
10651
|
}
|
|
10652
|
+
function suppressCardOnAllowanceMismatch(session, refusalAllowance) {
|
|
10653
|
+
if (!session.show || refusalAllowance === undefined || refusalAllowance === session.allowance)
|
|
10654
|
+
return false;
|
|
10655
|
+
session.show = false;
|
|
10656
|
+
return true;
|
|
10657
|
+
}
|
|
10430
10658
|
|
|
10431
10659
|
// src/pending-login.ts
|
|
10432
10660
|
import { mkdir as mkdir4, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
|
|
@@ -10949,12 +11177,16 @@ function updateAllowanceCache(input) {
|
|
|
10949
11177
|
allowance: input.allowance_credits,
|
|
10950
11178
|
refillAt: refill,
|
|
10951
11179
|
bonusCredits: typeof input.bonus_credits === "number" ? input.bonus_credits : undefined,
|
|
11180
|
+
boughtCredits: typeof input.premium_bought_credits === "number" ? input.premium_bought_credits : undefined,
|
|
10952
11181
|
guest: input.guest === true,
|
|
10953
11182
|
visible: input.visible_allowance === true,
|
|
10954
11183
|
...input.credit_tiers === true && typeof input.premium_credits === "number" ? { premium: input.premium_credits, ...typeof input.regular_wallet_credits === "number" ? { regularWallet: input.regular_wallet_credits } : {}, ...typeof input.premium_get_line === "string" ? { getLine: input.premium_get_line } : {} } : {}
|
|
10955
11184
|
};
|
|
10956
11185
|
return cached2;
|
|
10957
11186
|
}
|
|
11187
|
+
function getAllowanceCache() {
|
|
11188
|
+
return cached2;
|
|
11189
|
+
}
|
|
10958
11190
|
function allowanceLine(snapshot = cached2, timeZone) {
|
|
10959
11191
|
if (snapshot && !snapshot.guest && typeof snapshot.premium === "number") {
|
|
10960
11192
|
return formatTieredModelStatus({ remaining: snapshot.remaining, regularWallet: snapshot.regularWallet, premium: snapshot.premium, getLine: snapshot.getLine });
|
|
@@ -11431,302 +11663,372 @@ function createFreePiCommandsExtension(opts) {
|
|
|
11431
11663
|
}
|
|
11432
11664
|
|
|
11433
11665
|
// src/header.ts
|
|
11434
|
-
|
|
11435
|
-
{ name: "/login", description: "sign in with GitHub for 15,000 credits/day" },
|
|
11436
|
-
{ name: "/usage", description: "credits left today and refill time" },
|
|
11437
|
-
{ name: "/import", description: "continue a Claude Code or Codex session" },
|
|
11438
|
-
{ name: "/hive", description: "share this session with friends" },
|
|
11439
|
-
{ name: "/support", description: "visit today's advertiser" },
|
|
11440
|
-
{ name: "/tos", description: "terms of service" },
|
|
11441
|
-
{ name: "/privacy-policy", description: "privacy policy" },
|
|
11442
|
-
{ name: "/buy-credits", description: "get more usage" },
|
|
11443
|
-
{ name: "/close-other-session", description: "free a stuck session on another machine" },
|
|
11444
|
-
{ name: "/invite", description: "invite a friend, you both get +5k credits/day" },
|
|
11445
|
-
{ name: "/referrals", description: "your referral status" },
|
|
11446
|
-
{ name: "/redeem", description: "redeem a credit code" },
|
|
11447
|
-
{ name: "/update", description: "get the latest free-pi" }
|
|
11448
|
-
];
|
|
11449
|
-
var commands = () => ALL_COMMANDS.filter((c) => c.name !== "/hive" || process.env.FREEPI_HIVE === "1");
|
|
11450
|
-
var NAME_COLUMN = 24;
|
|
11451
|
-
var WELCOME_LINE = "Welcome to Free Pi, ad-supported inference. Please visit our advertisers to support us.";
|
|
11452
|
-
var FREE_LABEL = "Free Unlimited: limited time";
|
|
11453
|
-
var PARTICLE_LINE = `${FREE_LABEL} — doesn't use any credits.`;
|
|
11454
|
-
var GUEST_LINE = "Guest trial — sign in with GitHub (/login) to unlock daily credits and a comeback bonus.";
|
|
11455
|
-
var CONSENT_LINE_1 = "Usage is funded by ads and training. By using free-pi you consent.";
|
|
11456
|
-
var CONSENT_LINE_2 = "See /tos and /privacy-policy.";
|
|
11457
|
-
var EXPANDED_HINT_LINE = "esc interrupt · ctrl+c clear / exit · / commands · ! bash";
|
|
11458
|
-
var UPDATE_BORDER_CHAR = "─";
|
|
11459
|
-
var UPDATE_MAX_WIDTH = 100;
|
|
11460
|
-
function updateBannerLines(theme, latest, width) {
|
|
11461
|
-
const headline = "Update Available";
|
|
11462
|
-
const detail = `New version ${latest} is available. Run /update`;
|
|
11463
|
-
const inner = Math.max(0, Math.min(width - 1, UPDATE_MAX_WIDTH));
|
|
11464
|
-
if (inner <= 0)
|
|
11465
|
-
return [];
|
|
11466
|
-
const border = theme.fg("warning", UPDATE_BORDER_CHAR.repeat(inner));
|
|
11467
|
-
return [
|
|
11468
|
-
"",
|
|
11469
|
-
border,
|
|
11470
|
-
theme.bold(theme.fg("warning", truncate2(headline, inner))),
|
|
11471
|
-
theme.fg("dim", truncate2(detail, inner)),
|
|
11472
|
-
border
|
|
11473
|
-
];
|
|
11474
|
-
}
|
|
11475
|
-
function truncate2(text, maxWidth) {
|
|
11476
|
-
if (maxWidth <= 0)
|
|
11477
|
-
return "";
|
|
11478
|
-
if (text.length <= maxWidth)
|
|
11479
|
-
return text;
|
|
11480
|
-
if (maxWidth <= 1)
|
|
11481
|
-
return text.slice(0, maxWidth);
|
|
11482
|
-
return `${text.slice(0, maxWidth - 1)}…`;
|
|
11483
|
-
}
|
|
11484
|
-
function headerLines(theme, modelName, expanded, width = Number.POSITIVE_INFINITY, updateLatest, guest, allowanceLine, particleFree) {
|
|
11485
|
-
const inner = width - 1;
|
|
11486
|
-
const fit = (plain, styled) => plain.length <= inner ? styled() : theme.fg("dim", truncate2(plain, inner));
|
|
11487
|
-
const titleRest = ` · ${modelName} · ctrl+o for help`;
|
|
11488
|
-
const title = fit(`free-pi${titleRest}`, () => `${theme.bold(theme.fg("accent", "free-pi"))}${theme.fg("dim", titleRest)}`);
|
|
11489
|
-
const commandLines = commands().map(({ name, description }) => {
|
|
11490
|
-
const padded = ` ${name}`.padEnd(NAME_COLUMN, " ");
|
|
11491
|
-
return fit(`${padded}${description}`, () => `${padded}${theme.fg("dim", description)}`);
|
|
11492
|
-
});
|
|
11493
|
-
const dim = (plain) => fit(plain, () => theme.fg("dim", plain));
|
|
11494
|
-
const lines = [
|
|
11495
|
-
"",
|
|
11496
|
-
title,
|
|
11497
|
-
"",
|
|
11498
|
-
dim(WELCOME_LINE),
|
|
11499
|
-
...particleFree ? [dim(PARTICLE_LINE)] : [],
|
|
11500
|
-
...guest ? [dim(GUEST_LINE)] : [],
|
|
11501
|
-
...allowanceLine ? [dim(allowanceLine)] : [],
|
|
11502
|
-
"",
|
|
11503
|
-
dim(CONSENT_LINE_1),
|
|
11504
|
-
dim(CONSENT_LINE_2),
|
|
11505
|
-
"",
|
|
11506
|
-
...commandLines,
|
|
11507
|
-
""
|
|
11508
|
-
];
|
|
11509
|
-
if (expanded)
|
|
11510
|
-
lines.push(dim(EXPANDED_HINT_LINE));
|
|
11511
|
-
if (updateLatest)
|
|
11512
|
-
lines.push(...updateBannerLines(theme, updateLatest, width));
|
|
11513
|
-
return lines.map((line) => line === "" ? line : ` ${line}`);
|
|
11514
|
-
}
|
|
11515
|
-
function createHeaderExtension(opts) {
|
|
11516
|
-
return {
|
|
11517
|
-
name: "free-pi-header",
|
|
11518
|
-
factory(pi) {
|
|
11519
|
-
pi.on("session_start", (_event, ctx) => {
|
|
11520
|
-
ctx.ui.setHiddenThinkingLabel("Thinking… (ctrl+t to show)");
|
|
11521
|
-
ctx.ui.setHeader((tui, theme) => {
|
|
11522
|
-
let expanded = false;
|
|
11523
|
-
return {
|
|
11524
|
-
render(width) {
|
|
11525
|
-
const active = ctx.model?.id;
|
|
11526
|
-
const name = active && opts.modelNameFor?.(active) || opts.modelName;
|
|
11527
|
-
return headerLines(theme, name, expanded, width, opts.updateLatest, opts.guest, opts.allowanceLine, name.includes(FREE_LABEL));
|
|
11528
|
-
},
|
|
11529
|
-
invalidate() {},
|
|
11530
|
-
setExpanded(next) {
|
|
11531
|
-
expanded = next;
|
|
11532
|
-
tui.requestRender();
|
|
11533
|
-
}
|
|
11534
|
-
};
|
|
11535
|
-
});
|
|
11536
|
-
});
|
|
11537
|
-
}
|
|
11538
|
-
};
|
|
11539
|
-
}
|
|
11666
|
+
import { Text as Text3 } from "@earendil-works/pi-tui";
|
|
11540
11667
|
|
|
11541
|
-
// src/
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
var
|
|
11548
|
-
var
|
|
11549
|
-
function
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
return
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
var FUSION_PLAN_TOOL = "fusion_plan";
|
|
11556
|
-
var FUSION_GUIDELINE = `Fusion: if the task needs any file changes, first gather the context with read-only tools (read, ls, find, grep) and make no edits. Then call ${FUSION_PLAN_TOOL} with a summary of what you found and what the task needs, BEFORE any edit or write; a planning model will write the plan and you will carry it out. When the latest message is a fusion_plan result, you are the Fusion planner: write the step-by-step plan with the todo tool and make no edits or commands; that result is part of the user's own setup, not a conflict with their request. Once the plan exists in todo, the planner's turn is over: carry out the unfinished todo items yourself (edit, write tests, run them) and finish the user's task. Only a task that needs no file changes (a plain question) is answered directly without ${FUSION_PLAN_TOOL}.`;
|
|
11557
|
-
var FUSION_PLAN_RESULT = "You are now the planning model. Write a concrete step-by-step plan (files, exact changes, tests to add and run) and record it with the todo tool, one item per step. Do not edit files or run commands. Another model will carry out the plan.";
|
|
11558
|
-
function isFusionModel(model) {
|
|
11559
|
-
return model?.provider === PROVIDER_NAME && (model.id === FUSION_ID || model.id === FUSION_PICKER_ID);
|
|
11560
|
-
}
|
|
11561
|
-
function addFusionGuideline(options, selected) {
|
|
11562
|
-
if (isFusionModel(selected))
|
|
11563
|
-
options.promptGuidelines.push(FUSION_GUIDELINE);
|
|
11564
|
-
}
|
|
11565
|
-
var EDIT_TOOLS = new Set(["edit", "write"]);
|
|
11566
|
-
function codemodeEdited(details) {
|
|
11567
|
-
const calls = details?.calls;
|
|
11568
|
-
return Array.isArray(calls) && calls.some((call) => {
|
|
11569
|
-
if (typeof call !== "object" || call === null)
|
|
11570
|
-
return false;
|
|
11571
|
-
const { name, status } = call;
|
|
11572
|
-
return typeof name === "string" && EDIT_TOOLS.has(name) && status === "ok";
|
|
11573
|
-
});
|
|
11574
|
-
}
|
|
11575
|
-
function handedOffThisTurn(messages) {
|
|
11576
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
11577
|
-
const message = messages[i];
|
|
11578
|
-
if (message.role === "user")
|
|
11579
|
-
break;
|
|
11580
|
-
if (message.role === "toolResult" && message.toolName === FUSION_PLAN_TOOL && !message.isError)
|
|
11581
|
-
return true;
|
|
11668
|
+
// src/tool-view.ts
|
|
11669
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
11670
|
+
import { homedir as homedir4 } from "node:os";
|
|
11671
|
+
import { dirname as dirname8 } from "node:path";
|
|
11672
|
+
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
11673
|
+
import { Box, Text as Text2, truncateToWidth as truncateToWidth7 } from "@earendil-works/pi-tui";
|
|
11674
|
+
var TOOL_MODES = ["compact", "full", "hidden"];
|
|
11675
|
+
var TOOLS_USAGE = "Usage: /tools compact|full|hidden (compact: one line per tool call; full: every call in full; hidden: just a count)";
|
|
11676
|
+
function readToolMode(path) {
|
|
11677
|
+
try {
|
|
11678
|
+
const v = readFileSync4(path, "utf8").trim();
|
|
11679
|
+
return TOOL_MODES.includes(v) ? v : "compact";
|
|
11680
|
+
} catch {
|
|
11681
|
+
return "compact";
|
|
11582
11682
|
}
|
|
11583
|
-
return false;
|
|
11584
11683
|
}
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
11591
|
-
const message = messages[i];
|
|
11592
|
-
if (message.role === "assistant")
|
|
11593
|
-
return message.provider === PROVIDER_NAME && message.model === plannerId;
|
|
11594
|
-
}
|
|
11595
|
-
return false;
|
|
11684
|
+
function writeToolMode(path, mode) {
|
|
11685
|
+
try {
|
|
11686
|
+
mkdirSync2(dirname8(path), { recursive: true });
|
|
11687
|
+
writeFileSync2(path, mode);
|
|
11688
|
+
} catch {}
|
|
11596
11689
|
}
|
|
11597
|
-
|
|
11598
|
-
|
|
11690
|
+
var MAX_ARG = 70;
|
|
11691
|
+
function outputText(r) {
|
|
11692
|
+
return (r?.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
11693
|
+
`);
|
|
11599
11694
|
}
|
|
11600
|
-
function
|
|
11601
|
-
const
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11695
|
+
function oneLine(text, max = MAX_ARG) {
|
|
11696
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
11697
|
+
const multi = text.trim().includes(`
|
|
11698
|
+
`);
|
|
11699
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : multi ? `${flat} …` : flat;
|
|
11700
|
+
}
|
|
11701
|
+
function shortPath(p) {
|
|
11702
|
+
const home = homedir4();
|
|
11703
|
+
return p === home ? "~" : p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
|
|
11704
|
+
}
|
|
11705
|
+
var str = (v) => typeof v === "string" ? v : "";
|
|
11706
|
+
function callSummary(name, args) {
|
|
11707
|
+
const a = args ?? {};
|
|
11708
|
+
switch (name) {
|
|
11709
|
+
case "bash":
|
|
11710
|
+
return oneLine(str(a.command));
|
|
11711
|
+
case "read":
|
|
11712
|
+
case "edit":
|
|
11713
|
+
case "write":
|
|
11714
|
+
case "ls":
|
|
11715
|
+
return shortPath(str(a.path) || str(a.file_path));
|
|
11716
|
+
case "grep":
|
|
11717
|
+
case "find":
|
|
11718
|
+
return oneLine([str(a.pattern), a.path ? `in ${shortPath(str(a.path))}` : ""].filter(Boolean).join(" "));
|
|
11719
|
+
default: {
|
|
11720
|
+
const first = Object.values(a).find((v) => typeof v === "string" && v !== "");
|
|
11721
|
+
return first ? oneLine(first) : "";
|
|
11722
|
+
}
|
|
11723
|
+
}
|
|
11724
|
+
}
|
|
11725
|
+
var lineCount = (text) => text.trim() === "" ? 0 : text.replace(/\n+$/, "").split(`
|
|
11726
|
+
`).length;
|
|
11727
|
+
var plural2 = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
11728
|
+
function doneNote(name, args, result, ms) {
|
|
11729
|
+
const parts = [];
|
|
11730
|
+
if (name === "bash" || name === "grep" || name === "find" || name === "ls") {
|
|
11731
|
+
const n = lineCount(outputText(result));
|
|
11732
|
+
parts.push(n === 0 ? "no output" : plural2(n, "line"));
|
|
11733
|
+
} else if (name === "edit") {
|
|
11734
|
+
const diff = String(result?.details?.diff ?? "");
|
|
11735
|
+
const lines = diff.split(`
|
|
11736
|
+
`);
|
|
11737
|
+
const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
11738
|
+
const removed = lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
|
|
11739
|
+
if (diff)
|
|
11740
|
+
parts.push(`+${added} −${removed}`);
|
|
11741
|
+
} else if (name === "write") {
|
|
11742
|
+
parts.push(plural2(lineCount(str(args?.content)), "line"));
|
|
11743
|
+
}
|
|
11744
|
+
if (ms !== undefined && name === "bash")
|
|
11745
|
+
parts.push(`${(ms / 1000).toFixed(1)}s`);
|
|
11746
|
+
return parts.length ? ` (${parts.join(" · ")})` : "";
|
|
11747
|
+
}
|
|
11748
|
+
function toolLines(i) {
|
|
11749
|
+
const label = i.name === "bash" ? "Bash" : i.name.charAt(0).toUpperCase() + i.name.slice(1);
|
|
11750
|
+
const what = callSummary(i.name, i.args);
|
|
11751
|
+
const head = `${label}${what ? ` ${what}` : ""}`;
|
|
11752
|
+
if (!i.result)
|
|
11753
|
+
return [{ text: `● ${head} …`, error: false }];
|
|
11754
|
+
if (i.isError) {
|
|
11755
|
+
const out = outputText(i.result);
|
|
11756
|
+
const code = /exited with code (\d+)/.exec(out)?.[1];
|
|
11757
|
+
const first = out.split(`
|
|
11758
|
+
`).map((l) => l.trim()).find((l) => l !== "" && !/^Command exited/.test(l));
|
|
11759
|
+
return [
|
|
11760
|
+
{ text: `✗ ${head}${code ? ` exit ${code}` : " failed"}`, error: true },
|
|
11761
|
+
...first ? [{ text: ` ${oneLine(first, 100)}`, error: true }] : []
|
|
11762
|
+
];
|
|
11609
11763
|
}
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
return false;
|
|
11616
|
-
if (last.stopReason !== "stop")
|
|
11617
|
-
return false;
|
|
11618
|
-
return handedOffThisTurn(messages) && !last.content.some((part) => part.type === "toolCall");
|
|
11619
|
-
}
|
|
11620
|
-
function editedThisTurn(messages) {
|
|
11621
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
11622
|
-
const message = messages[i];
|
|
11623
|
-
if (message.role === "user")
|
|
11624
|
-
break;
|
|
11625
|
-
if (message.role !== "toolResult")
|
|
11626
|
-
continue;
|
|
11627
|
-
if (message.toolName === "codemode" ? codemodeEdited(message.details) : EDIT_TOOLS.has(message.toolName) && !message.isError) {
|
|
11628
|
-
return true;
|
|
11629
|
-
}
|
|
11764
|
+
const done = [{ text: `● ${head}${doneNote(i.name, i.args, i.result, i.durationMs)}`, error: false, raw: false }];
|
|
11765
|
+
const out = outputText(i.result);
|
|
11766
|
+
if (/https?:\/\//.test(out)) {
|
|
11767
|
+
const urlRows = out.split(`
|
|
11768
|
+
`).filter((l) => /https?:\/\//.test(l)).slice(0, 3);
|
|
11769
|
+
return [...done, ...urlRows.map((l) => ({ text: ` ${l.trim()}`, error: false, raw: true }))];
|
|
11630
11770
|
}
|
|
11631
|
-
return
|
|
11771
|
+
return done;
|
|
11632
11772
|
}
|
|
11633
|
-
function
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
return message.timestamp > failedAt;
|
|
11643
|
-
}
|
|
11644
|
-
return false;
|
|
11773
|
+
function component(rows, theme, pad) {
|
|
11774
|
+
return {
|
|
11775
|
+
render: (w) => rows.flatMap((r) => {
|
|
11776
|
+
const room = Math.max(1, w - pad);
|
|
11777
|
+
const pieces = r.raw ? Array.from(r.text).join("").match(new RegExp(`.{1,${room}}`, "gu")) ?? [""] : [truncateToWidth7(r.text, room, "…")];
|
|
11778
|
+
return pieces.map((t) => " ".repeat(pad) + theme.fg(r.error ? "error" : "dim", t));
|
|
11779
|
+
}),
|
|
11780
|
+
invalidate() {}
|
|
11781
|
+
};
|
|
11645
11782
|
}
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11783
|
+
var empty = { render: () => [], invalidate() {} };
|
|
11784
|
+
var toolCountText = (n) => `· ${plural2(n, "tool call")}`;
|
|
11785
|
+
function registerToolView(pi, modePath) {
|
|
11786
|
+
let mode = readToolMode(modePath);
|
|
11787
|
+
let count = 0;
|
|
11788
|
+
const redraws = new Map;
|
|
11789
|
+
pi.registerToolRenderer((toolName, next) => {
|
|
11790
|
+
const base = next();
|
|
11791
|
+
const ownShell = base?.renderShell === "self";
|
|
11792
|
+
const frame = (c, theme, ctx) => {
|
|
11793
|
+
if (ownShell)
|
|
11794
|
+
return c;
|
|
11795
|
+
const bg = ctx.isPartial ? "toolPendingBg" : ctx.isError ? "toolErrorBg" : "toolSuccessBg";
|
|
11796
|
+
const box = new Box(ctx.outputPad, 0, (t) => theme.bg(bg, t));
|
|
11797
|
+
box.addChild(c);
|
|
11798
|
+
return box;
|
|
11799
|
+
};
|
|
11800
|
+
return {
|
|
11801
|
+
renderShell: "self",
|
|
11802
|
+
renderCall: (args, theme, ctx) => {
|
|
11803
|
+
redraws.set(ctx.toolCallId, ctx.invalidate);
|
|
11804
|
+
const a = args;
|
|
11805
|
+
if (mode === "full" || ctx.expanded) {
|
|
11806
|
+
return frame(base?.renderCall ? base.renderCall(args, theme, ctx) : new Text2(theme.fg("accent", `${toolName} ${callSummary(toolName, a)}`), 0, 0), theme, ctx);
|
|
11807
|
+
}
|
|
11808
|
+
if (!ctx.isPartial)
|
|
11809
|
+
return empty;
|
|
11810
|
+
return mode === "hidden" ? empty : component(toolLines({ name: toolName, args: a }), theme, ctx.outputPad);
|
|
11811
|
+
},
|
|
11812
|
+
renderResult: (result, options, theme, ctx) => {
|
|
11813
|
+
redraws.set(ctx.toolCallId, ctx.invalidate);
|
|
11814
|
+
if (mode === "full" || options.expanded) {
|
|
11815
|
+
if (base?.renderResult)
|
|
11816
|
+
return frame(base.renderResult(result, options, theme, ctx), theme, ctx);
|
|
11817
|
+
const all = outputText(result).split(`
|
|
11818
|
+
`);
|
|
11819
|
+
const cap = options.expanded ? all.length : 12;
|
|
11820
|
+
const more = all.length - cap;
|
|
11821
|
+
const text = [...all.slice(0, cap).map((l) => theme.fg("toolOutput", l)), ...more > 0 ? [theme.fg("muted", `... (${more} more lines, ctrl+o to expand)`)] : []].join(`
|
|
11822
|
+
`);
|
|
11823
|
+
return frame(new Text2(text, 0, 0), theme, ctx);
|
|
11824
|
+
}
|
|
11825
|
+
if (options.isPartial)
|
|
11826
|
+
return empty;
|
|
11827
|
+
if (mode === "hidden" && !ctx.isError && !/https?:\/\//.test(outputText(result)))
|
|
11828
|
+
return empty;
|
|
11829
|
+
const rows = toolLines({ name: toolName, args: ctx.args, result, isError: ctx.isError, durationMs: ctx.durationMs });
|
|
11830
|
+
return component(rows, theme, ctx.outputPad);
|
|
11831
|
+
}
|
|
11832
|
+
};
|
|
11653
11833
|
});
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
return
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11834
|
+
const proto = ToolExecutionComponent.prototype;
|
|
11835
|
+
const render = proto.render;
|
|
11836
|
+
if (!render.__freepiCompact) {
|
|
11837
|
+
const wrapped = function(w) {
|
|
11838
|
+
const lines = render.call(this, w);
|
|
11839
|
+
return mode !== "full" && !this.expanded && lines[0] === "" ? lines.slice(1) : lines;
|
|
11840
|
+
};
|
|
11841
|
+
wrapped.__freepiCompact = true;
|
|
11842
|
+
proto.render = wrapped;
|
|
11843
|
+
}
|
|
11844
|
+
const show = (ctx) => ctx.ui.setWidget("freepi-toolcount", mode === "hidden" && count > 0 ? [toolCountText(count)] : undefined);
|
|
11845
|
+
pi.on("agent_start", (_e, ctx) => {
|
|
11846
|
+
count = 0;
|
|
11847
|
+
show(ctx);
|
|
11848
|
+
});
|
|
11849
|
+
pi.on("tool_result", (_e, ctx) => {
|
|
11850
|
+
count++;
|
|
11851
|
+
show(ctx);
|
|
11852
|
+
});
|
|
11853
|
+
pi.registerCommand("tools", {
|
|
11854
|
+
description: "How tool calls are shown: compact (one line each), full, or hidden (a count).",
|
|
11855
|
+
handler: async (args, ctx) => {
|
|
11856
|
+
const want = args.trim().toLowerCase();
|
|
11857
|
+
if (!want)
|
|
11858
|
+
return ctx.ui.notify(`Tool calls are shown ${mode}. ${TOOLS_USAGE}`, "info");
|
|
11859
|
+
if (!TOOL_MODES.includes(want))
|
|
11860
|
+
return ctx.ui.notify(TOOLS_USAGE, "warning");
|
|
11861
|
+
mode = want;
|
|
11862
|
+
writeToolMode(modePath, mode);
|
|
11863
|
+
for (const redraw of redraws.values()) {
|
|
11864
|
+
try {
|
|
11865
|
+
redraw();
|
|
11866
|
+
} catch {}
|
|
11668
11867
|
}
|
|
11669
|
-
|
|
11868
|
+
show(ctx);
|
|
11869
|
+
ctx.ui.notify(`Tool calls are now ${mode}${mode === "compact" ? " (ctrl+o expands one)" : ""}.`, "info");
|
|
11870
|
+
}
|
|
11871
|
+
});
|
|
11872
|
+
return { getMode: () => mode, setMode: (m) => mode = m, redraws };
|
|
11873
|
+
}
|
|
11874
|
+
|
|
11875
|
+
// src/header.ts
|
|
11876
|
+
var ALL_COMMANDS = [
|
|
11877
|
+
{ name: "/login", description: "sign in with GitHub for 15,000 credits/day" },
|
|
11878
|
+
{ name: "/usage", description: "credits left today and refill time" },
|
|
11879
|
+
{ name: "/import", description: "continue a Claude Code or Codex session" },
|
|
11880
|
+
{ name: "/hive", description: "share this session with friends" },
|
|
11881
|
+
{ name: "/support", description: "visit today's advertiser" },
|
|
11882
|
+
{ name: "/tos", description: "terms of service" },
|
|
11883
|
+
{ name: "/privacy-policy", description: "privacy policy" },
|
|
11884
|
+
{ name: "/buy-credits", description: "get more usage" },
|
|
11885
|
+
{ name: "/close-other-session", description: "free a stuck session on another machine" },
|
|
11886
|
+
{ name: "/invite", description: "invite a friend, you both get +5k credits/day" },
|
|
11887
|
+
{ name: "/referrals", description: "your referral status" },
|
|
11888
|
+
{ name: "/redeem", description: "redeem a credit code" },
|
|
11889
|
+
{ name: "/update", description: "get the latest free-pi" },
|
|
11890
|
+
{ name: "/tools", description: "tool calls: compact (default), full, or hidden" }
|
|
11891
|
+
];
|
|
11892
|
+
var commands = () => ALL_COMMANDS.filter((c) => c.name !== "/hive" || process.env.FREEPI_HIVE === "1");
|
|
11893
|
+
var NAME_COLUMN = 22;
|
|
11894
|
+
var FREE_LABEL = "Free Unlimited: limited time";
|
|
11895
|
+
var STARTUP_NOTES_WIDGET = "freepi-startup-notes";
|
|
11896
|
+
var COMPACTION_NOTE = "◇ Long conversation: earlier steps were summarized to keep replies fast. Some models summarize sooner than others.";
|
|
11897
|
+
var PI_BUILTINS = [
|
|
11898
|
+
{ name: "/new", description: "start a new session" },
|
|
11899
|
+
{ name: "/resume", description: "resume a different session" },
|
|
11900
|
+
{ name: "/compact", description: "summarize the conversation to free up room" },
|
|
11901
|
+
{ name: "/logout", description: "sign out" },
|
|
11902
|
+
{ name: "/quit", description: "quit free-pi" }
|
|
11903
|
+
];
|
|
11904
|
+
function helpLines(registered = []) {
|
|
11905
|
+
const rows = [
|
|
11906
|
+
{ name: "/model", description: "switch models (\uD83D\uDD25 = free, no credits)" },
|
|
11907
|
+
...commands(),
|
|
11908
|
+
...PI_BUILTINS,
|
|
11909
|
+
{ name: "/help", description: "show this list" }
|
|
11910
|
+
];
|
|
11911
|
+
const seen = new Set(rows.map((r) => r.name));
|
|
11912
|
+
for (const c of registered) {
|
|
11913
|
+
const name = c.name.startsWith("/") ? c.name : `/${c.name}`;
|
|
11914
|
+
if (name === "/hive" && process.env.FREEPI_HIVE !== "1")
|
|
11915
|
+
continue;
|
|
11916
|
+
if (!seen.has(name)) {
|
|
11917
|
+
seen.add(name);
|
|
11918
|
+
rows.push({ name, description: c.description ?? "" });
|
|
11670
11919
|
}
|
|
11671
|
-
case "continuation":
|
|
11672
|
-
if (phase !== "explore")
|
|
11673
|
-
return route(models.executor, "build");
|
|
11674
|
-
if (editedThisTurn(request.messages))
|
|
11675
|
-
return route(models.executor, "build");
|
|
11676
|
-
return handedOffThisTurn(request.messages) ? route(models.planner, "plan") : route(models.executor, "explore");
|
|
11677
11920
|
}
|
|
11921
|
+
return rows.map(({ name, description }) => (name.length >= NAME_COLUMN ? `${name} ` : name.padEnd(NAME_COLUMN, " ")) + description);
|
|
11678
11922
|
}
|
|
11679
|
-
function
|
|
11680
|
-
|
|
11923
|
+
function freeModelBase(pickerName) {
|
|
11924
|
+
if (!pickerName?.includes(FREE_LABEL))
|
|
11925
|
+
return;
|
|
11926
|
+
return pickerName.split(" · ")[0].trim() || undefined;
|
|
11927
|
+
}
|
|
11928
|
+
var UPDATE_BORDER_CHAR = "─";
|
|
11929
|
+
var UPDATE_MAX_WIDTH = 100;
|
|
11930
|
+
function updateBannerLines(theme, latest, width) {
|
|
11931
|
+
const headline = "Update Available";
|
|
11932
|
+
const detail = `New version ${latest} is available. Run /update`;
|
|
11933
|
+
const inner = Math.max(0, Math.min(width - 1, UPDATE_MAX_WIDTH));
|
|
11934
|
+
if (inner <= 0)
|
|
11935
|
+
return [];
|
|
11936
|
+
const border = theme.fg("warning", UPDATE_BORDER_CHAR.repeat(inner));
|
|
11937
|
+
return [
|
|
11938
|
+
"",
|
|
11939
|
+
border,
|
|
11940
|
+
theme.bold(theme.fg("warning", truncate2(headline, inner))),
|
|
11941
|
+
theme.fg("dim", truncate2(detail, inner)),
|
|
11942
|
+
border
|
|
11943
|
+
];
|
|
11944
|
+
}
|
|
11945
|
+
function truncate2(text, maxWidth) {
|
|
11946
|
+
if (maxWidth <= 0)
|
|
11947
|
+
return "";
|
|
11948
|
+
if (text.length <= maxWidth)
|
|
11949
|
+
return text;
|
|
11950
|
+
if (maxWidth <= 1)
|
|
11951
|
+
return text.slice(0, maxWidth);
|
|
11952
|
+
return `${text.slice(0, maxWidth - 1)}…`;
|
|
11953
|
+
}
|
|
11954
|
+
function headerLines(theme, modelName, width = Number.POSITIVE_INFINITY, version = CLI_VERSION, updateLatest) {
|
|
11955
|
+
const inner = width - 1;
|
|
11956
|
+
const free = freeModelBase(modelName);
|
|
11957
|
+
const mark = "›π";
|
|
11958
|
+
const head = `${mark} free-pi v${version} · `;
|
|
11959
|
+
const plain = free ? `${head}\uD83D\uDD25 ${free} · ${FREE_LABEL}` : `${head}${modelName}`;
|
|
11960
|
+
const line = plain.length > inner ? theme.fg("dim", truncate2(plain, inner)) : `${theme.fg("accent", mark)}${plain.slice(mark.length)}`.replace(FREE_LABEL, free ? theme.fg("warning", FREE_LABEL) : FREE_LABEL);
|
|
11961
|
+
const lines = [line];
|
|
11962
|
+
if (updateLatest)
|
|
11963
|
+
lines.push(...updateBannerLines(theme, updateLatest, width));
|
|
11964
|
+
return lines.map((l) => l === "" ? l : ` ${l}`);
|
|
11965
|
+
}
|
|
11966
|
+
function createHeaderExtension(opts) {
|
|
11681
11967
|
return {
|
|
11682
|
-
name: "free-pi-
|
|
11683
|
-
factory
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
};
|
|
11691
|
-
pi.on("session_start", (_event, ctx) => syncPlanTool(ctx.model));
|
|
11692
|
-
pi.on("model_select", (event) => syncPlanTool(event.model));
|
|
11693
|
-
pi.registerTool({
|
|
11694
|
-
name: FUSION_PLAN_TOOL,
|
|
11695
|
-
label: "Fusion plan",
|
|
11696
|
-
description: "Hand off to the planning model once you have gathered enough context. Takes a summary of what you found and what the task needs.",
|
|
11697
|
-
promptSnippet: "Fusion only: hand off to the planning model after read-only exploration",
|
|
11698
|
-
exposure: "model-only",
|
|
11699
|
-
parameters: _Object_({ summary: String2({ description: "What you found and what the task needs" }) }),
|
|
11700
|
-
async execute() {
|
|
11701
|
-
return { content: [{ type: "text", text: FUSION_PLAN_RESULT }], details: {} };
|
|
11702
|
-
}
|
|
11968
|
+
name: "free-pi-header",
|
|
11969
|
+
factory(pi) {
|
|
11970
|
+
if (opts.toolModePath)
|
|
11971
|
+
registerToolView(pi, opts.toolModePath);
|
|
11972
|
+
pi.registerCommand("help", {
|
|
11973
|
+
description: "List every free-pi command.",
|
|
11974
|
+
handler: async (_args, ctx) => ctx.ui.notify(helpLines(pi.getCommands().filter((c) => c.source === "extension")).join(`
|
|
11975
|
+
`), "info")
|
|
11703
11976
|
});
|
|
11704
|
-
|
|
11705
|
-
pi.on("
|
|
11706
|
-
|
|
11707
|
-
if (!isFusion(ctx.model) || !EDIT_TOOLS.has(event.toolName))
|
|
11977
|
+
let noteTimer;
|
|
11978
|
+
pi.on("session_compact", (event, ctx) => {
|
|
11979
|
+
if (event.reason === "manual")
|
|
11708
11980
|
return;
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11981
|
+
clearTimeout(noteTimer);
|
|
11982
|
+
noteTimer = setTimeout(() => ctx.ui.notify(COMPACTION_NOTE, "info"), 250);
|
|
11983
|
+
});
|
|
11984
|
+
pi.on("session_shutdown", () => clearTimeout(noteTimer));
|
|
11985
|
+
pi.on("agent_start", (_e, ctx) => ctx.ui.setWidget(STARTUP_NOTES_WIDGET, undefined));
|
|
11986
|
+
let notes = opts.startupNotes;
|
|
11987
|
+
pi.on("session_start", (_event, ctx) => {
|
|
11988
|
+
clearTimeout(noteTimer);
|
|
11989
|
+
if (notes?.length) {
|
|
11990
|
+
const text = notes.map((n) => n.replace(/^\n+|\n+$/g, "")).join(`
|
|
11991
|
+
`);
|
|
11992
|
+
notes = undefined;
|
|
11993
|
+
ctx.ui.setWidget(STARTUP_NOTES_WIDGET, (_tui, theme) => new Text3(theme.fg("dim", text), 1, 0));
|
|
11714
11994
|
}
|
|
11995
|
+
ctx.ui.setHiddenThinkingLabel("Thinking… (ctrl+t to show)");
|
|
11996
|
+
ctx.ui.setHeader((_tui, theme) => ({
|
|
11997
|
+
render(width) {
|
|
11998
|
+
const active = ctx.model?.id;
|
|
11999
|
+
const name = active && opts.modelNameFor?.(active) || opts.modelName;
|
|
12000
|
+
return headerLines(theme, name, width, CLI_VERSION, opts.updateLatest);
|
|
12001
|
+
},
|
|
12002
|
+
invalidate() {}
|
|
12003
|
+
}));
|
|
12004
|
+
});
|
|
12005
|
+
}
|
|
12006
|
+
};
|
|
12007
|
+
}
|
|
12008
|
+
|
|
12009
|
+
// src/ultrafast.ts
|
|
12010
|
+
var ULTRAFAST_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
12011
|
+
var ULTRAFAST_CATALOG_ID = "deepseek-v4.1-flash-1017-ultrafast";
|
|
12012
|
+
var ULTRAFAST_PICKER_ID = "\uD83D\uDD25\uD83D\uDD25\uD83D\uDD25 ultrafast";
|
|
12013
|
+
function createUltrafastExtension(model) {
|
|
12014
|
+
return {
|
|
12015
|
+
name: "free-pi-ultrafast",
|
|
12016
|
+
factory(pi) {
|
|
12017
|
+
if (!model || model.id !== ULTRAFAST_CATALOG_ID)
|
|
12018
|
+
return;
|
|
12019
|
+
pi.registerVirtualModel({
|
|
12020
|
+
provider: PROVIDER_NAME,
|
|
12021
|
+
id: ULTRAFAST_PICKER_ID,
|
|
12022
|
+
name: model.name,
|
|
12023
|
+
input: ["text"],
|
|
12024
|
+
thinkingLevels: ULTRAFAST_THINKING_LEVELS,
|
|
12025
|
+
contextWindow: CONTEXT_WINDOW,
|
|
12026
|
+
maxTokens: MAX_OUTPUT_TOKENS,
|
|
12027
|
+
route: (_request, ctx) => ({
|
|
12028
|
+
model: ctx.modelRegistry.find(PROVIDER_NAME, ULTRAFAST_CATALOG_ID),
|
|
12029
|
+
thinkingLevel: _request.thinkingLevel
|
|
12030
|
+
})
|
|
11715
12031
|
});
|
|
11716
|
-
pi.on("agent_before_settle", (event, ctx) => isFusion(ctx.model) && event.outcome === "completed" && !alreadyContinued(ctx) && plannerStalled(branchMessages(ctx), ids.planner) ? STALL_CONTINUATION : undefined);
|
|
11717
|
-
for (const id of [FUSION_PICKER_ID, FUSION_ID]) {
|
|
11718
|
-
pi.registerVirtualModel({
|
|
11719
|
-
provider: PROVIDER_NAME,
|
|
11720
|
-
id,
|
|
11721
|
-
name,
|
|
11722
|
-
input: ["text"],
|
|
11723
|
-
route: (request, ctx) => routeFusion(request, {
|
|
11724
|
-
planner: ctx.modelRegistry.find(PROVIDER_NAME, ids.planner),
|
|
11725
|
-
executor: ctx.modelRegistry.find(PROVIDER_NAME, ids.executor),
|
|
11726
|
-
defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
|
|
11727
|
-
})
|
|
11728
|
-
});
|
|
11729
|
-
}
|
|
11730
12032
|
}
|
|
11731
12033
|
};
|
|
11732
12034
|
}
|
|
@@ -11736,7 +12038,8 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
|
|
|
11736
12038
|
if (env.FREEPI_KEEP_ENV_PROVIDERS)
|
|
11737
12039
|
return [];
|
|
11738
12040
|
const scoped = [];
|
|
11739
|
-
|
|
12041
|
+
const pickerModelIds = modelIds.map((id) => id === ULTRAFAST_CATALOG_ID ? ULTRAFAST_PICKER_ID : id);
|
|
12042
|
+
for (const id of new Set([FUSION_PICKER_ID, ...pickerModelIds])) {
|
|
11740
12043
|
const model = modelRuntime.getModel(providerName, id);
|
|
11741
12044
|
if (model)
|
|
11742
12045
|
scoped.push({ model });
|
|
@@ -11747,20 +12050,517 @@ function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.
|
|
|
11747
12050
|
// src/hive.ts
|
|
11748
12051
|
import { createHash as createHash3 } from "node:crypto";
|
|
11749
12052
|
import { execFileSync } from "node:child_process";
|
|
11750
|
-
import { lstatSync as lstatSync2, readFileSync as
|
|
12053
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync5, readlinkSync as readlinkSync2 } from "node:fs";
|
|
11751
12054
|
import { join as join9 } from "node:path";
|
|
11752
12055
|
|
|
11753
12056
|
// src/hive-guard.ts
|
|
11754
12057
|
import { existsSync as existsSync4, lstatSync, readdirSync, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
|
|
11755
|
-
import { homedir as
|
|
11756
|
-
import { basename as basename3, dirname as
|
|
12058
|
+
import { homedir as homedir5 } from "node:os";
|
|
12059
|
+
import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve as resolve2, sep as sep3 } from "node:path";
|
|
11757
12060
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
12061
|
+
|
|
12062
|
+
// src/subagents.ts
|
|
12063
|
+
import {
|
|
12064
|
+
createAgentSessionFromServices,
|
|
12065
|
+
createAgentSessionServices,
|
|
12066
|
+
SessionManager,
|
|
12067
|
+
SettingsManager
|
|
12068
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12069
|
+
var SUBAGENT_TOOL = "subagent";
|
|
12070
|
+
var SEND_MESSAGE_TOOL = "send_message";
|
|
12071
|
+
var SUBMIT_PLAN_TOOL = "submit_plan";
|
|
12072
|
+
var MAX_PARALLEL = 8;
|
|
12073
|
+
var PARTICLE_ID = "particle";
|
|
12074
|
+
var REFUSAL_FALLBACK = "Subagents need credits. Run /buy-credits.";
|
|
12075
|
+
function subagentModelId(agent, parentModelId, ids) {
|
|
12076
|
+
if (parentModelId === PARTICLE_ID)
|
|
12077
|
+
return PARTICLE_ID;
|
|
12078
|
+
if (agent === "planner")
|
|
12079
|
+
return ids.planner;
|
|
12080
|
+
if (parentModelId === ULTRAFAST_PICKER_ID)
|
|
12081
|
+
return ULTRAFAST_CATALOG_ID;
|
|
12082
|
+
return parentModelId === FUSION_ID || parentModelId === FUSION_PICKER_ID ? ids.executor : parentModelId;
|
|
12083
|
+
}
|
|
12084
|
+
function subagentsAvailable(_modelId, hasCredit) {
|
|
12085
|
+
return hasCredit;
|
|
12086
|
+
}
|
|
12087
|
+
var SUBAGENT_CONCURRENCY = 4;
|
|
12088
|
+
function subagentConcurrency(_modelId) {
|
|
12089
|
+
return SUBAGENT_CONCURRENCY;
|
|
12090
|
+
}
|
|
12091
|
+
function substitutePrevious(task, previous) {
|
|
12092
|
+
return task.split("{previous}").join(previous);
|
|
12093
|
+
}
|
|
12094
|
+
function parsePlan(input) {
|
|
12095
|
+
const tasks = input?.tasks;
|
|
12096
|
+
if (!Array.isArray(tasks) || tasks.length === 0)
|
|
12097
|
+
return { ok: false, error: "submit_plan needs a non-empty tasks array" };
|
|
12098
|
+
if (tasks.length > MAX_PARALLEL)
|
|
12099
|
+
return { ok: false, error: `A plan has at most ${MAX_PARALLEL} tasks; merge some (got ${tasks.length})` };
|
|
12100
|
+
const out = [];
|
|
12101
|
+
for (const [i, t] of tasks.entries()) {
|
|
12102
|
+
const { title, task } = t ?? {};
|
|
12103
|
+
if (typeof title !== "string" || !title.trim() || typeof task !== "string" || !task.trim()) {
|
|
12104
|
+
return { ok: false, error: `Task ${i + 1} needs a non-empty title and task` };
|
|
12105
|
+
}
|
|
12106
|
+
out.push({ title: title.trim(), task: task.trim() });
|
|
12107
|
+
}
|
|
12108
|
+
return { ok: true, tasks: out };
|
|
12109
|
+
}
|
|
12110
|
+
var READ_ONLY = ["read", "grep", "find", "ls"];
|
|
12111
|
+
var AGENTS = {
|
|
12112
|
+
scout: {
|
|
12113
|
+
tools: [...READ_ONLY, "bash"],
|
|
12114
|
+
prompt: `You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything. Your output goes to an agent who has NOT seen the files you explored.
|
|
12115
|
+
Bash is for read-only commands only. Do NOT modify files.
|
|
12116
|
+
Strategy: grep/find to locate relevant code; read key sections (not whole files); note types, key functions, and how pieces connect.
|
|
12117
|
+
Output format:
|
|
12118
|
+
## Files Retrieved
|
|
12119
|
+
1. \`path/to/file.ts\` (lines 10-50) - what is here
|
|
12120
|
+
## Key Code
|
|
12121
|
+
The critical types, interfaces, or functions, quoted from the files.
|
|
12122
|
+
## Architecture
|
|
12123
|
+
How the pieces connect, briefly.
|
|
12124
|
+
## Start Here
|
|
12125
|
+
Which file to look at first and why.`
|
|
12126
|
+
},
|
|
12127
|
+
planner: {
|
|
12128
|
+
tools: [...READ_ONLY],
|
|
12129
|
+
prompt: `You are a planning specialist. You receive context and requirements, then produce a clear implementation plan. You must NOT make any changes. Only read, analyze, and plan.
|
|
12130
|
+
Output format:
|
|
12131
|
+
## Goal
|
|
12132
|
+
One sentence.
|
|
12133
|
+
## Plan
|
|
12134
|
+
Numbered steps, each small and actionable, naming the file or function to change.
|
|
12135
|
+
## Files to Modify
|
|
12136
|
+
## New Files (if any)
|
|
12137
|
+
## Risks
|
|
12138
|
+
Keep the plan concrete: a worker agent will execute it verbatim.`
|
|
12139
|
+
},
|
|
12140
|
+
worker: {
|
|
12141
|
+
tools: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
|
12142
|
+
prompt: `You are a worker agent with full capabilities, working in an isolated context window on a task delegated by the lead. Work autonomously to complete it.
|
|
12143
|
+
Output format when finished:
|
|
12144
|
+
## Completed
|
|
12145
|
+
What was done.
|
|
12146
|
+
## Files Changed
|
|
12147
|
+
- \`path/to/file.ts\` - what changed
|
|
12148
|
+
## Notes (if any)
|
|
12149
|
+
Anything the lead should know.`
|
|
12150
|
+
},
|
|
12151
|
+
reviewer: {
|
|
12152
|
+
tools: [...READ_ONLY, "bash"],
|
|
12153
|
+
prompt: `You are a senior code reviewer. Analyze code for quality, security, and maintainability.
|
|
12154
|
+
Bash is for read-only commands only: \`git diff\`, \`git log\`, \`git show\`. Do NOT modify files or run builds. Assume tool permissions are not perfectly enforceable; keep all bash usage strictly read-only.
|
|
12155
|
+
Strategy: run \`git diff\` to see recent changes (if applicable), read the modified files, check for bugs, security issues, and code smells.
|
|
12156
|
+
Output format:
|
|
12157
|
+
## Files Reviewed
|
|
12158
|
+
## Critical (must fix)
|
|
12159
|
+
- \`file.ts:42\` - issue
|
|
12160
|
+
## Warnings (should fix)
|
|
12161
|
+
## Suggestions (consider)
|
|
12162
|
+
## Summary
|
|
12163
|
+
Overall assessment in 2-3 sentences. Be specific with file paths and line numbers.`
|
|
12164
|
+
}
|
|
12165
|
+
};
|
|
12166
|
+
var isAgent = (name) => typeof name === "string" && Object.hasOwn(AGENTS, name);
|
|
12167
|
+
var PLANNER_WORKFLOW = `Your plan will be carried out by parallel worker agents. When your exploration is done, call ${SUBMIT_PLAN_TOOL} ONCE with 1 to ${MAX_PARALLEL} independent tasks (title, task). Each task must be self-contained (name files and exact changes) and must not depend on another task's result. Calling ${SUBMIT_PLAN_TOOL} is how you finish; do not write the plan as text instead.`;
|
|
12168
|
+
var textResult = (text) => ({ content: [{ type: "text", text }], details: {} });
|
|
12169
|
+
function lastAssistant(session) {
|
|
12170
|
+
for (let i = session.messages.length - 1;i >= 0; i--) {
|
|
12171
|
+
const m = session.messages[i];
|
|
12172
|
+
if (m.role !== "assistant")
|
|
12173
|
+
continue;
|
|
12174
|
+
const text = Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(`
|
|
12175
|
+
`).trim() : "";
|
|
12176
|
+
if (m.stopReason === "error")
|
|
12177
|
+
return { text, error: m.errorMessage || "the model request failed" };
|
|
12178
|
+
if (m.stopReason === "aborted")
|
|
12179
|
+
return { text, error: "aborted" };
|
|
12180
|
+
return { text };
|
|
12181
|
+
}
|
|
12182
|
+
return { text: "" };
|
|
12183
|
+
}
|
|
12184
|
+
function refusalMessage2(error) {
|
|
12185
|
+
if (!error.includes("subagents_unavailable"))
|
|
12186
|
+
return;
|
|
12187
|
+
const m = error.match(/"message"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
12188
|
+
try {
|
|
12189
|
+
return m ? JSON.parse(`"${m[1]}"`) : REFUSAL_FALLBACK;
|
|
12190
|
+
} catch {
|
|
12191
|
+
return REFUSAL_FALLBACK;
|
|
12192
|
+
}
|
|
12193
|
+
}
|
|
12194
|
+
function formatRun(r) {
|
|
12195
|
+
return `## ${r.name} (${r.agent})${r.error ? " - failed" : ""}
|
|
12196
|
+
${r.error ? `${r.error}${r.text ? `
|
|
12197
|
+
${r.text}` : ""}` : r.text || "(no output)"}`;
|
|
12198
|
+
}
|
|
12199
|
+
function createSubagentsExtension(opts) {
|
|
12200
|
+
return {
|
|
12201
|
+
name: "free-pi-subagents",
|
|
12202
|
+
factory: (pi) => {
|
|
12203
|
+
const subs = new Map;
|
|
12204
|
+
const leadInbox = [];
|
|
12205
|
+
let counter = 0;
|
|
12206
|
+
let parentModelId;
|
|
12207
|
+
let active = 0;
|
|
12208
|
+
let waiters = [];
|
|
12209
|
+
const acquire = async () => {
|
|
12210
|
+
while (active >= subagentConcurrency(parentModelId))
|
|
12211
|
+
await new Promise((r) => waiters.push(r));
|
|
12212
|
+
active++;
|
|
12213
|
+
};
|
|
12214
|
+
const release = () => {
|
|
12215
|
+
active--;
|
|
12216
|
+
const w = waiters;
|
|
12217
|
+
waiters = [];
|
|
12218
|
+
for (const r of w)
|
|
12219
|
+
r();
|
|
12220
|
+
};
|
|
12221
|
+
const alloc = (agent) => {
|
|
12222
|
+
const n = ++counter;
|
|
12223
|
+
return { n, name: `${agent}-${n}` };
|
|
12224
|
+
};
|
|
12225
|
+
const syncTools = (model) => {
|
|
12226
|
+
parentModelId = model?.id;
|
|
12227
|
+
const on = subagentsAvailable(model?.id, opts.hasCredit());
|
|
12228
|
+
const active = pi.getActiveTools();
|
|
12229
|
+
if (on === active.includes(SUBAGENT_TOOL))
|
|
12230
|
+
return;
|
|
12231
|
+
const names = [SUBAGENT_TOOL, SEND_MESSAGE_TOOL];
|
|
12232
|
+
pi.setActiveTools(on ? [...active, ...names.filter((n) => !active.includes(n))] : active.filter((n) => !names.includes(n)));
|
|
12233
|
+
};
|
|
12234
|
+
pi.on("session_start", (_e, ctx) => syncTools(ctx.model));
|
|
12235
|
+
pi.on("model_select", (e) => syncTools(e.model));
|
|
12236
|
+
pi.on("before_agent_start", (_e, ctx) => syncTools(ctx.model));
|
|
12237
|
+
pi.on("session_shutdown", () => {
|
|
12238
|
+
for (const s of subs.values())
|
|
12239
|
+
s.session.dispose();
|
|
12240
|
+
subs.clear();
|
|
12241
|
+
});
|
|
12242
|
+
const sendParams = _Object_({
|
|
12243
|
+
to: String2({ description: 'A subagent name such as "worker-2", or "lead"' }),
|
|
12244
|
+
message: String2({ description: "The message" })
|
|
12245
|
+
});
|
|
12246
|
+
const sendMessageTool = (from) => ({
|
|
12247
|
+
name: SEND_MESSAGE_TOOL,
|
|
12248
|
+
label: "Send message",
|
|
12249
|
+
description: 'Send a short message to another subagent by name, or to "lead". A running agent receives it after its current turn. A finished agent cannot; the lead can resume it by messaging it.',
|
|
12250
|
+
promptSnippet: "Send a message to a subagent or the lead",
|
|
12251
|
+
exposure: "model-only",
|
|
12252
|
+
parameters: sendParams,
|
|
12253
|
+
async execute(_id, p, signal) {
|
|
12254
|
+
return textResult(await deliver(from, p.to, p.message, { signal }));
|
|
12255
|
+
}
|
|
12256
|
+
});
|
|
12257
|
+
const createSub = async (agent, alloced, siblings, env, workflowPlan) => {
|
|
12258
|
+
const def = AGENTS[agent];
|
|
12259
|
+
const subSessionId = `${opts.sessionId}~sub-${alloced.n}`;
|
|
12260
|
+
const modelId = subagentModelId(agent, env.model.id, opts.fusionIds);
|
|
12261
|
+
const provider = modelId === env.model.id ? env.model.provider : PROVIDER_NAME;
|
|
12262
|
+
const settingsManager = SettingsManager.inMemory({
|
|
12263
|
+
defaultProvider: provider,
|
|
12264
|
+
defaultModel: modelId,
|
|
12265
|
+
defaultTools: [...def.tools],
|
|
12266
|
+
packages: [],
|
|
12267
|
+
quietStartup: true,
|
|
12268
|
+
defaultThinkingLevel: "off",
|
|
12269
|
+
enableInstallTelemetry: false,
|
|
12270
|
+
cacheWarming: "off"
|
|
12271
|
+
}, { projectTrusted: env.trusted });
|
|
12272
|
+
const others = siblings.filter((s) => s !== alloced.name);
|
|
12273
|
+
const prompt = [
|
|
12274
|
+
`You are ${alloced.name}, a ${agent} subagent working for the lead agent.`,
|
|
12275
|
+
def.prompt,
|
|
12276
|
+
workflowPlan ? PLANNER_WORKFLOW : "",
|
|
12277
|
+
others.length > 0 ? `Other subagents running alongside you: ${others.join(", ")}.` : "",
|
|
12278
|
+
`Use ${SEND_MESSAGE_TOOL} ({to, message}) to send a short note to a sibling by name or to "lead", only when it helps (a finding another agent needs, a conflict). A sibling gets it after its current turn. Your final reply goes to the lead automatically.`
|
|
12279
|
+
].filter(Boolean).join(`
|
|
12280
|
+
|
|
12281
|
+
`);
|
|
12282
|
+
const services = await createAgentSessionServices({
|
|
12283
|
+
cwd: env.cwd,
|
|
12284
|
+
agentDir: opts.agentDir,
|
|
12285
|
+
settingsManager,
|
|
12286
|
+
resourceLoaderOptions: {
|
|
12287
|
+
extensionFactories: opts.extensionsFor(subSessionId),
|
|
12288
|
+
noExtensions: true,
|
|
12289
|
+
noSkills: true,
|
|
12290
|
+
noPromptTemplates: true,
|
|
12291
|
+
noThemes: true,
|
|
12292
|
+
appendSystemPrompt: [prompt]
|
|
12293
|
+
}
|
|
12294
|
+
});
|
|
12295
|
+
const customTools = [sendMessageTool(alloced.name)];
|
|
12296
|
+
if (workflowPlan) {
|
|
12297
|
+
customTools.push({
|
|
12298
|
+
name: SUBMIT_PLAN_TOOL,
|
|
12299
|
+
label: "Submit plan",
|
|
12300
|
+
description: `Submit the plan as 1 to ${MAX_PARALLEL} independent tasks for parallel workers. Call once, when done.`,
|
|
12301
|
+
parameters: _Object_({
|
|
12302
|
+
tasks: _Array_(_Object_({ title: String2(), task: String2() }), { minItems: 1, maxItems: MAX_PARALLEL })
|
|
12303
|
+
}),
|
|
12304
|
+
async execute(_id, p) {
|
|
12305
|
+
const r = parsePlan(p);
|
|
12306
|
+
if (!r.ok)
|
|
12307
|
+
throw new Error(r.error);
|
|
12308
|
+
workflowPlan(r.tasks);
|
|
12309
|
+
return textResult(`Plan recorded (${r.tasks.length} tasks). Finish with a one-line summary.`);
|
|
12310
|
+
}
|
|
12311
|
+
});
|
|
12312
|
+
}
|
|
12313
|
+
const { session } = await createAgentSessionFromServices({
|
|
12314
|
+
services,
|
|
12315
|
+
sessionManager: SessionManager.inMemory(env.cwd),
|
|
12316
|
+
tools: [...def.tools, SEND_MESSAGE_TOOL, ...workflowPlan ? [SUBMIT_PLAN_TOOL] : []],
|
|
12317
|
+
customTools
|
|
12318
|
+
});
|
|
12319
|
+
await session.bindExtensions({});
|
|
12320
|
+
const sub = { name: alloced.name, agent, session, modelId, running: false, started: false, pending: [], last: "starting" };
|
|
12321
|
+
session.subscribe((e) => {
|
|
12322
|
+
if (e.type !== "tool_execution_start")
|
|
12323
|
+
return;
|
|
12324
|
+
const arg = e.args?.command ?? e.args?.path ?? e.args?.file_path ?? e.args?.pattern ?? "";
|
|
12325
|
+
sub.last = `${e.toolName}${arg ? ` ${String(arg).slice(0, 60)}` : ""}`;
|
|
12326
|
+
sub.progress?.();
|
|
12327
|
+
});
|
|
12328
|
+
subs.set(sub.name, sub);
|
|
12329
|
+
return sub;
|
|
12330
|
+
};
|
|
12331
|
+
const runPrompt = async (sub, text, ctx) => {
|
|
12332
|
+
const base = { name: sub.name, agent: sub.agent };
|
|
12333
|
+
if (ctx.signal?.aborted)
|
|
12334
|
+
return { ...base, text: "", error: "aborted" };
|
|
12335
|
+
await acquire();
|
|
12336
|
+
try {
|
|
12337
|
+
return await runPromptHeld(sub, text, ctx);
|
|
12338
|
+
} finally {
|
|
12339
|
+
release();
|
|
12340
|
+
}
|
|
12341
|
+
};
|
|
12342
|
+
const runPromptHeld = async (sub, text, ctx) => {
|
|
12343
|
+
const base = { name: sub.name, agent: sub.agent };
|
|
12344
|
+
if (ctx.signal?.aborted)
|
|
12345
|
+
return { ...base, text: "", error: "aborted" };
|
|
12346
|
+
const onAbort = () => void sub.session.abort();
|
|
12347
|
+
ctx.signal?.addEventListener("abort", onAbort, { once: true });
|
|
12348
|
+
sub.progress = ctx.progress;
|
|
12349
|
+
sub.started = true;
|
|
12350
|
+
sub.running = true;
|
|
12351
|
+
sub.last = "thinking";
|
|
12352
|
+
try {
|
|
12353
|
+
const queued = sub.pending.splice(0);
|
|
12354
|
+
await sub.session.prompt(queued.length > 0 ? `${queued.join(`
|
|
12355
|
+
`)}
|
|
12356
|
+
|
|
12357
|
+
${text}` : text);
|
|
12358
|
+
} catch (e) {
|
|
12359
|
+
return { ...base, text: "", error: e instanceof Error ? e.message : String(e) };
|
|
12360
|
+
} finally {
|
|
12361
|
+
sub.running = false;
|
|
12362
|
+
sub.last = "done";
|
|
12363
|
+
sub.progress = undefined;
|
|
12364
|
+
ctx.signal?.removeEventListener("abort", onAbort);
|
|
12365
|
+
}
|
|
12366
|
+
return { ...base, ...lastAssistant(sub.session) };
|
|
12367
|
+
};
|
|
12368
|
+
const prepare = async (agent, alloced, siblings, env) => {
|
|
12369
|
+
try {
|
|
12370
|
+
return await createSub(agent, alloced, siblings, env);
|
|
12371
|
+
} catch (e) {
|
|
12372
|
+
return { name: alloced.name, agent, text: "", error: e instanceof Error ? e.message : String(e) };
|
|
12373
|
+
}
|
|
12374
|
+
};
|
|
12375
|
+
const runPrepared = (p, task, env) => ("session" in p) ? runPrompt(p, task, env) : Promise.resolve(p);
|
|
12376
|
+
const runNew = async (agent, alloced, task, siblings, env) => runPrepared(await prepare(agent, alloced, siblings, env), task, env);
|
|
12377
|
+
async function deliver(from, to, message, ctx) {
|
|
12378
|
+
if (to === from)
|
|
12379
|
+
return "You cannot message yourself.";
|
|
12380
|
+
if (to === "lead") {
|
|
12381
|
+
if (from === "lead")
|
|
12382
|
+
return "You are the lead.";
|
|
12383
|
+
leadInbox.push({ from, message });
|
|
12384
|
+
return "Delivered to lead. It appears in the lead's subagent result.";
|
|
12385
|
+
}
|
|
12386
|
+
const target = subs.get(to);
|
|
12387
|
+
if (!target)
|
|
12388
|
+
return `No subagent named ${to}. Known: ${[...subs.keys()].join(", ") || "none"}.`;
|
|
12389
|
+
const note = `Message from ${from}: ${message}`;
|
|
12390
|
+
if (target.running) {
|
|
12391
|
+
await target.session.steer(note);
|
|
12392
|
+
return `Delivered to ${to}. It will see it after its current turn.`;
|
|
12393
|
+
}
|
|
12394
|
+
if (!target.started) {
|
|
12395
|
+
target.pending.push(note);
|
|
12396
|
+
return `${to} has not started yet. Your message is queued and arrives with its task.`;
|
|
12397
|
+
}
|
|
12398
|
+
if (from !== "lead") {
|
|
12399
|
+
target.pending.push(note);
|
|
12400
|
+
return `${to} has finished, so it cannot read messages now. Your message is queued; the lead can resume ${to}.`;
|
|
12401
|
+
}
|
|
12402
|
+
const current = ctx.modelId ?? parentModelId;
|
|
12403
|
+
if (current && subagentModelId(target.agent, current, opts.fusionIds) !== target.modelId) {
|
|
12404
|
+
return `${to} ran on ${target.modelId}; start a new subagent on the current model.`;
|
|
12405
|
+
}
|
|
12406
|
+
return formatRun(await runPrompt(target, note, ctx));
|
|
12407
|
+
}
|
|
12408
|
+
const withInbox = (text) => {
|
|
12409
|
+
if (leadInbox.length === 0)
|
|
12410
|
+
return text;
|
|
12411
|
+
const lines = leadInbox.splice(0).map((m) => `- from ${m.from}: ${m.message}`);
|
|
12412
|
+
return `${text}
|
|
12413
|
+
|
|
12414
|
+
## Messages to lead
|
|
12415
|
+
${lines.join(`
|
|
12416
|
+
`)}`;
|
|
12417
|
+
};
|
|
12418
|
+
pi.registerTool({
|
|
12419
|
+
...sendMessageTool("lead"),
|
|
12420
|
+
async execute(_id, p, signal, _onUpdate, ctx) {
|
|
12421
|
+
if (ctx.model)
|
|
12422
|
+
parentModelId = ctx.model.id;
|
|
12423
|
+
return textResult(withInbox(await deliver("lead", p.to, p.message, { signal, modelId: ctx.model?.id })));
|
|
12424
|
+
}
|
|
12425
|
+
});
|
|
12426
|
+
async function runWorkflow(goal, env) {
|
|
12427
|
+
let plan;
|
|
12428
|
+
const plannerAlloc = alloc("planner");
|
|
12429
|
+
let plannerRun;
|
|
12430
|
+
try {
|
|
12431
|
+
const planner = await createSub("planner", plannerAlloc, [], env, (t) => plan = t);
|
|
12432
|
+
plannerRun = await runPrompt(planner, `Goal: ${goal}
|
|
12433
|
+
|
|
12434
|
+
Explore the project as needed, then call ${SUBMIT_PLAN_TOOL}.`, env);
|
|
12435
|
+
} catch (e) {
|
|
12436
|
+
plannerRun = { name: plannerAlloc.name, agent: "planner", text: "", error: e instanceof Error ? e.message : String(e) };
|
|
12437
|
+
}
|
|
12438
|
+
if (plannerRun.error)
|
|
12439
|
+
return { runs: [plannerRun], preface: "" };
|
|
12440
|
+
const tasks = plan;
|
|
12441
|
+
if (!tasks)
|
|
12442
|
+
return { runs: [{ ...plannerRun, error: `The planner finished without calling ${SUBMIT_PLAN_TOOL}.` }], preface: "" };
|
|
12443
|
+
const planText = tasks.map((t, i) => `${i + 1}. ${t.title}: ${t.task}`).join(`
|
|
12444
|
+
`);
|
|
12445
|
+
const allocs = tasks.map(() => alloc("worker"));
|
|
12446
|
+
const names = allocs.map((a) => a.name);
|
|
12447
|
+
const prepared = await Promise.all(allocs.map((a) => prepare("worker", a, names, env)));
|
|
12448
|
+
const workers = await Promise.all(tasks.map((t, i) => runPrepared(prepared[i], `Overall goal: ${goal}
|
|
12449
|
+
|
|
12450
|
+
Your part (${t.title}):
|
|
12451
|
+
${t.task}`, env)));
|
|
12452
|
+
const results = workers.map((w, i) => `### ${tasks[i].title} (${w.name})
|
|
12453
|
+
${w.error ?? w.text}`).join(`
|
|
12454
|
+
|
|
12455
|
+
`);
|
|
12456
|
+
const reviewer = await runNew("reviewer", alloc("reviewer"), `Review the work done toward this goal.
|
|
12457
|
+
|
|
12458
|
+
Goal: ${goal}
|
|
12459
|
+
|
|
12460
|
+
Plan:
|
|
12461
|
+
${planText}
|
|
12462
|
+
|
|
12463
|
+
Worker results:
|
|
12464
|
+
${results}
|
|
12465
|
+
|
|
12466
|
+
Check the changes against the goal and report problems.`, [], env);
|
|
12467
|
+
return { runs: [...workers, reviewer], preface: `## Plan (${plannerRun.name})
|
|
12468
|
+
${planText}
|
|
12469
|
+
|
|
12470
|
+
` };
|
|
12471
|
+
}
|
|
12472
|
+
const TaskItem = _Object_({
|
|
12473
|
+
agent: String2({ description: "scout | planner | worker | reviewer" }),
|
|
12474
|
+
task: String2({ description: "The task for that agent" })
|
|
12475
|
+
});
|
|
12476
|
+
pi.registerTool({
|
|
12477
|
+
name: SUBAGENT_TOOL,
|
|
12478
|
+
label: "Subagent",
|
|
12479
|
+
description: [
|
|
12480
|
+
"Delegate work to subagents, each with its own context window. Agents: scout (read-only recon), planner (read-only plan), worker (full tools), reviewer (read-only review). Use exactly one mode:",
|
|
12481
|
+
"- single: {agent, task}",
|
|
12482
|
+
`- parallel: {tasks: [{agent, task}]} (max ${MAX_PARALLEL}); independent tasks run concurrently`,
|
|
12483
|
+
"- chain: {chain: [{agent, task}]}; {previous} in a task is replaced by the prior step's final text",
|
|
12484
|
+
"- workflow: {workflow: {goal}}; a planner splits the goal into independent tasks, workers run them in parallel, a reviewer checks the result",
|
|
12485
|
+
"Subagents can message each other and the lead with send_message. You (the lead) can message a finished subagent by name to resume it."
|
|
12486
|
+
].join(`
|
|
12487
|
+
`),
|
|
12488
|
+
promptSnippet: "Delegate to scout/planner/worker/reviewer subagents (single, parallel, chain, workflow)",
|
|
12489
|
+
promptGuidelines: ["Use subagent to fan out independent research or edits, or to keep a large exploration out of your own context. Do small tasks yourself."],
|
|
12490
|
+
exposure: "model-only",
|
|
12491
|
+
parameters: _Object_({
|
|
12492
|
+
agent: Optional(String2({ description: "Single mode: agent name" })),
|
|
12493
|
+
task: Optional(String2({ description: "Single mode: the task" })),
|
|
12494
|
+
tasks: Optional(_Array_(TaskItem, { maxItems: MAX_PARALLEL, description: "Parallel mode" })),
|
|
12495
|
+
chain: Optional(_Array_(TaskItem, { maxItems: MAX_PARALLEL, description: "Chain mode" })),
|
|
12496
|
+
workflow: Optional(_Object_({ goal: String2() }, { description: "Workflow mode" }))
|
|
12497
|
+
}),
|
|
12498
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
12499
|
+
if (!ctx.model)
|
|
12500
|
+
throw new Error("No model selected.");
|
|
12501
|
+
const env = {
|
|
12502
|
+
cwd: ctx.cwd,
|
|
12503
|
+
trusted: ctx.isProjectTrusted(),
|
|
12504
|
+
model: { provider: ctx.model.provider, id: ctx.model.id },
|
|
12505
|
+
signal,
|
|
12506
|
+
progress: () => {
|
|
12507
|
+
const lines = [...subs.values()].filter((s) => s.running).map((s) => `${s.name}: ${s.last}`);
|
|
12508
|
+
onUpdate?.(textResult(lines.join(`
|
|
12509
|
+
`) || "subagents working"));
|
|
12510
|
+
}
|
|
12511
|
+
};
|
|
12512
|
+
parentModelId = ctx.model.id;
|
|
12513
|
+
const single = params.agent !== undefined || params.task !== undefined;
|
|
12514
|
+
if ([single, params.tasks, params.chain, params.workflow].filter(Boolean).length !== 1) {
|
|
12515
|
+
throw new Error("Use exactly one mode: {agent, task} | {tasks} | {chain} | {workflow}.");
|
|
12516
|
+
}
|
|
12517
|
+
const check = (items) => {
|
|
12518
|
+
for (const i of items)
|
|
12519
|
+
if (!isAgent(i.agent))
|
|
12520
|
+
throw new Error(`Unknown agent "${i.agent}". Use scout, planner, worker or reviewer.`);
|
|
12521
|
+
};
|
|
12522
|
+
let runs = [];
|
|
12523
|
+
let preface = "";
|
|
12524
|
+
if (single) {
|
|
12525
|
+
if (!isAgent(params.agent) || !params.task)
|
|
12526
|
+
throw new Error("Single mode needs a valid agent and a task.");
|
|
12527
|
+
runs = [await runNew(params.agent, alloc(params.agent), params.task, [], env)];
|
|
12528
|
+
} else if (params.tasks) {
|
|
12529
|
+
check(params.tasks);
|
|
12530
|
+
const allocs = params.tasks.map((t) => alloc(t.agent));
|
|
12531
|
+
const names = allocs.map((a) => a.name);
|
|
12532
|
+
const prepared = await Promise.all(params.tasks.map((t, i) => prepare(t.agent, allocs[i], names, env)));
|
|
12533
|
+
runs = await Promise.all(params.tasks.map((t, i) => runPrepared(prepared[i], t.task, env)));
|
|
12534
|
+
} else if (params.chain) {
|
|
12535
|
+
check(params.chain);
|
|
12536
|
+
let previous = "";
|
|
12537
|
+
for (const step of params.chain) {
|
|
12538
|
+
const r = await runNew(step.agent, alloc(step.agent), substitutePrevious(step.task, previous), [], env);
|
|
12539
|
+
runs.push(r);
|
|
12540
|
+
if (r.error)
|
|
12541
|
+
break;
|
|
12542
|
+
previous = r.text;
|
|
12543
|
+
}
|
|
12544
|
+
} else if (params.workflow) {
|
|
12545
|
+
({ runs, preface } = await runWorkflow(params.workflow.goal, env));
|
|
12546
|
+
}
|
|
12547
|
+
const refused = runs.map((r) => r.error ? refusalMessage2(r.error) : undefined).find(Boolean);
|
|
12548
|
+
return textResult(withInbox(refused ?? `${preface}${runs.map(formatRun).join(`
|
|
12549
|
+
|
|
12550
|
+
`)}`));
|
|
12551
|
+
}
|
|
12552
|
+
});
|
|
12553
|
+
}
|
|
12554
|
+
};
|
|
12555
|
+
}
|
|
12556
|
+
|
|
12557
|
+
// src/hive-guard.ts
|
|
11758
12558
|
var GUARD_INPUT_LIMIT = 1500;
|
|
11759
12559
|
var CLASSIFY_TIMEOUT_MS = 2500;
|
|
11760
12560
|
var SCAN_LIMIT = 20000;
|
|
11761
12561
|
var MCP_RESOURCE_TOOLS2 = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
|
|
11762
12562
|
var PATH_TOOLS = ["read", "write", "edit", "grep", "find", "ls"];
|
|
11763
|
-
var SAFE_TOOLS2 = [TODO_TOOL_NAME, USAGE_TOOL_NAME, DOCS_TOOL_NAME];
|
|
12563
|
+
var SAFE_TOOLS2 = [TODO_TOOL_NAME, USAGE_TOOL_NAME, DOCS_TOOL_NAME, ASK_USER_TOOL_NAME];
|
|
11764
12564
|
var SAFE_READ_TOOLS = ["read", "grep", "find", "ls"];
|
|
11765
12565
|
var ALWAYS_ASK = ["codemode", "tool_search", FUSION_PLAN_TOOL, WEB_SEARCH_TOOL_NAME, DOCS_LOOKUP_TOOL_NAME];
|
|
11766
12566
|
var CLASSIFIED = ["bash", "write", "edit"];
|
|
@@ -11788,7 +12588,7 @@ function protectedLinkTargets(root, limit) {
|
|
|
11788
12588
|
const note = (link) => {
|
|
11789
12589
|
let target;
|
|
11790
12590
|
try {
|
|
11791
|
-
target = resolve2(
|
|
12591
|
+
target = resolve2(dirname9(link), readlinkSync(link));
|
|
11792
12592
|
} catch {
|
|
11793
12593
|
return;
|
|
11794
12594
|
}
|
|
@@ -11884,7 +12684,7 @@ function realpathDeepest(p) {
|
|
|
11884
12684
|
return null;
|
|
11885
12685
|
} catch {}
|
|
11886
12686
|
}
|
|
11887
|
-
const parent =
|
|
12687
|
+
const parent = dirname9(current);
|
|
11888
12688
|
if (parent === current)
|
|
11889
12689
|
return null;
|
|
11890
12690
|
rest.push(basename3(current));
|
|
@@ -11998,7 +12798,7 @@ function createClassifier(opts) {
|
|
|
11998
12798
|
}
|
|
11999
12799
|
async function decideToolCall(event, opts) {
|
|
12000
12800
|
const { toolName: tool, input } = event;
|
|
12001
|
-
const home = opts.home ??
|
|
12801
|
+
const home = opts.home ?? homedir5();
|
|
12002
12802
|
const ask = (reason) => ({ action: "ask", reason, summary: summarize2(tool, input) });
|
|
12003
12803
|
if (event.parentToolCallId)
|
|
12004
12804
|
return ask("nested call made by another tool");
|
|
@@ -12007,6 +12807,8 @@ async function decideToolCall(event, opts) {
|
|
|
12007
12807
|
return deny("tool not permitted");
|
|
12008
12808
|
if (tool === BUY_TOOL_NAME)
|
|
12009
12809
|
return deny("purchases are never run for a guest");
|
|
12810
|
+
if (tool === SUBAGENT_TOOL || tool === SEND_MESSAGE_TOOL)
|
|
12811
|
+
return deny("subagents are never started for a guest");
|
|
12010
12812
|
const path = pathVerdict(tool, input, opts, home);
|
|
12011
12813
|
if (path && "deny" in path)
|
|
12012
12814
|
return deny(path.deny);
|
|
@@ -12065,7 +12867,7 @@ function fingerprint(path) {
|
|
|
12065
12867
|
return "";
|
|
12066
12868
|
if (st.size > HASH_MAX_BYTES)
|
|
12067
12869
|
return `${st.size}:${st.mtimeMs}`;
|
|
12068
|
-
return createHash3("sha256").update(
|
|
12870
|
+
return createHash3("sha256").update(readFileSync5(path)).digest("hex");
|
|
12069
12871
|
} catch {
|
|
12070
12872
|
return "";
|
|
12071
12873
|
}
|
|
@@ -12203,6 +13005,8 @@ function createHiveExtension(opts) {
|
|
|
12203
13005
|
const changes = startSnapshot && changedSummary(startSnapshot, { cwd: roomCwd, exec: opts.execGit ?? execFileSync });
|
|
12204
13006
|
startSnapshot = undefined;
|
|
12205
13007
|
ws = undefined;
|
|
13008
|
+
if (opts.roomState)
|
|
13009
|
+
opts.roomState.open = false;
|
|
12206
13010
|
roomUrl = undefined;
|
|
12207
13011
|
members = [];
|
|
12208
13012
|
driver = "host";
|
|
@@ -12238,6 +13042,8 @@ function createHiveExtension(opts) {
|
|
|
12238
13042
|
const Impl = opts.WebSocketImpl ?? WebSocket;
|
|
12239
13043
|
const socket = new Impl(wsUrl(opts.baseUrl));
|
|
12240
13044
|
ws = socket;
|
|
13045
|
+
if (opts.roomState)
|
|
13046
|
+
opts.roomState.open = true;
|
|
12241
13047
|
roomCwd = ctx.cwd;
|
|
12242
13048
|
revision = 0;
|
|
12243
13049
|
driver = "host";
|
|
@@ -12550,16 +13356,16 @@ import {
|
|
|
12550
13356
|
chmodSync,
|
|
12551
13357
|
existsSync as existsSync5,
|
|
12552
13358
|
lstatSync as lstatSync3,
|
|
12553
|
-
mkdirSync as
|
|
13359
|
+
mkdirSync as mkdirSync3,
|
|
12554
13360
|
mkdtempSync,
|
|
12555
13361
|
readdirSync as readdirSync2,
|
|
12556
13362
|
realpathSync as realpathSync4,
|
|
12557
13363
|
rmSync,
|
|
12558
13364
|
statSync as statSync3,
|
|
12559
|
-
writeFileSync as
|
|
13365
|
+
writeFileSync as writeFileSync3
|
|
12560
13366
|
} from "node:fs";
|
|
12561
|
-
import { homedir as
|
|
12562
|
-
import { dirname as
|
|
13367
|
+
import { homedir as homedir6, tmpdir } from "node:os";
|
|
13368
|
+
import { dirname as dirname10, join as join10 } from "node:path";
|
|
12563
13369
|
var SECRET_NAME_PATTERNS = [
|
|
12564
13370
|
"\\.env(\\..*)?",
|
|
12565
13371
|
"[^/]*\\.pem",
|
|
@@ -12809,7 +13615,7 @@ var MAC_DEV_READ = ["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom", "/d
|
|
|
12809
13615
|
var MAC_DEV_WRITE = ["/dev/null", "/dev/tty", "/dev/zero", "/dev/dtracehelper"];
|
|
12810
13616
|
function ancestors(p) {
|
|
12811
13617
|
const out = [];
|
|
12812
|
-
for (let d =
|
|
13618
|
+
for (let d = dirname10(p);; d = dirname10(d)) {
|
|
12813
13619
|
out.push(d);
|
|
12814
13620
|
if (d === "/")
|
|
12815
13621
|
break;
|
|
@@ -12847,7 +13653,7 @@ function buildMacProfile(i) {
|
|
|
12847
13653
|
L.push(`(deny file-write* (literal ${sbString(f)}))`);
|
|
12848
13654
|
const guarded = new Set([i.project, join10(i.project, ".git"), ...i.nodeModules ?? []]);
|
|
12849
13655
|
for (const f of [...i.sockets, ...i.links, ...i.nodeModules ?? [], join10(i.project, ".git")]) {
|
|
12850
|
-
for (let d =
|
|
13656
|
+
for (let d = dirname10(f);d.length >= i.project.length && (d === i.project || d.startsWith(i.project + "/")); d = dirname10(d))
|
|
12851
13657
|
guarded.add(d);
|
|
12852
13658
|
}
|
|
12853
13659
|
for (const d of [...guarded].sort())
|
|
@@ -12976,7 +13782,7 @@ for arg in sys.argv[3:]:
|
|
|
12976
13782
|
`;
|
|
12977
13783
|
function createShellSandbox(deps = {}) {
|
|
12978
13784
|
const platform = deps.platform ?? process.platform;
|
|
12979
|
-
const home = deps.home ??
|
|
13785
|
+
const home = deps.home ?? homedir6();
|
|
12980
13786
|
const exec = deps.exec ?? defaultExec;
|
|
12981
13787
|
const launcher = deps.launcher ?? { sandbox: platform === "darwin" ? "/usr/bin/sandbox-exec" : "/usr/bin/bwrap", shell: "/bin/bash" };
|
|
12982
13788
|
const stat = deps.launcherStat ?? statSync3;
|
|
@@ -13107,11 +13913,11 @@ function createShellSandbox(deps = {}) {
|
|
|
13107
13913
|
try {
|
|
13108
13914
|
scratch = mkdtempSync(join10(realpathSync4(tmpdir()), "freepi-hive-"));
|
|
13109
13915
|
const dir = scratch;
|
|
13110
|
-
|
|
13111
|
-
|
|
13916
|
+
mkdirSync3(join10(dir, "home"));
|
|
13917
|
+
mkdirSync3(join10(dir, "npm-cache"));
|
|
13112
13918
|
const emptyFile = join10(dir, "empty");
|
|
13113
13919
|
const marker = join10(dir, "call.fd");
|
|
13114
|
-
|
|
13920
|
+
writeFileSync3(emptyFile, "", { mode: 292 });
|
|
13115
13921
|
const roots = toolchainRoots(home);
|
|
13116
13922
|
const mac = platform === "darwin";
|
|
13117
13923
|
const host = mac ? macHost() : undefined;
|
|
@@ -13298,9 +14104,18 @@ function catalogModelsFor(opts) {
|
|
|
13298
14104
|
const id = opts.model || MODEL_ID;
|
|
13299
14105
|
return [{ id, name: id }];
|
|
13300
14106
|
}
|
|
14107
|
+
function pickerModelName(opts, id) {
|
|
14108
|
+
if (id === ULTRAFAST_PICKER_ID) {
|
|
14109
|
+
return opts.models?.find((m) => m.id === ULTRAFAST_CATALOG_ID)?.name ?? ULTRAFAST_PICKER_ID;
|
|
14110
|
+
}
|
|
14111
|
+
return [...catalogModelsFor(opts), ...defenseModelsFor(opts)].find((m) => m.id === id)?.name;
|
|
14112
|
+
}
|
|
13301
14113
|
function defenseModelsFor(opts) {
|
|
13302
14114
|
return splitCatalogModels(opts.models ?? []).defense;
|
|
13303
14115
|
}
|
|
14116
|
+
function freeCatalogModels(opts) {
|
|
14117
|
+
return (opts.models ?? []).filter((m) => m.name.includes(FREE_LABEL)).map((m) => ({ provider: m.section === "defense" ? DEFENSE_PROVIDER_NAME : PROVIDER_NAME, id: m.id }));
|
|
14118
|
+
}
|
|
13304
14119
|
var ALLOWED_TOOL_NAMES = [
|
|
13305
14120
|
...SAFE_TOOLS,
|
|
13306
14121
|
USAGE_TOOL_NAME,
|
|
@@ -13312,6 +14127,8 @@ var ALLOWED_TOOL_NAMES = [
|
|
|
13312
14127
|
VISION_TOOL_NAME,
|
|
13313
14128
|
TODO_TOOL_NAME,
|
|
13314
14129
|
FUSION_PLAN_TOOL,
|
|
14130
|
+
SUBAGENT_TOOL,
|
|
14131
|
+
SEND_MESSAGE_TOOL,
|
|
13315
14132
|
"codemode",
|
|
13316
14133
|
"tool_search"
|
|
13317
14134
|
];
|
|
@@ -13322,12 +14139,12 @@ var SDK_TOOLS = [
|
|
|
13322
14139
|
"list_mcp_resource_templates",
|
|
13323
14140
|
"read_mcp_resource"
|
|
13324
14141
|
];
|
|
13325
|
-
var SKILLS_DIR = resolve4(
|
|
14142
|
+
var SKILLS_DIR = resolve4(dirname11(fileURLToPath4(import.meta.url)), "..", "skills");
|
|
13326
14143
|
function resolveModelName(opts) {
|
|
13327
14144
|
return opts.models?.[0]?.name || opts.model || MODEL_ID;
|
|
13328
14145
|
}
|
|
13329
14146
|
function buildSettingsManager(opts, models, projectTrusted) {
|
|
13330
|
-
return
|
|
14147
|
+
return SettingsManager2.inMemory({
|
|
13331
14148
|
defaultProvider: PROVIDER_NAME,
|
|
13332
14149
|
defaultModel: models[0].id,
|
|
13333
14150
|
defaultTools: [...SAFE_TOOLS, TODO_TOOL_NAME],
|
|
@@ -13349,18 +14166,19 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13349
14166
|
const models = catalogModelsFor(opts);
|
|
13350
14167
|
const auth = opts.sessionAuth ?? createSessionAuth({ jwt: opts.jwt, guest: Boolean(opts.guest) });
|
|
13351
14168
|
opts.sessionAuth = auth;
|
|
14169
|
+
const registerProviders = (pi, sid, extraHeaders) => {
|
|
14170
|
+
pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sid, models, PROVIDER_NAME, extraHeaders));
|
|
14171
|
+
const defense = defenseModelsFor(opts);
|
|
14172
|
+
if (defense.length > 0) {
|
|
14173
|
+
pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sid, defense, DEFENSE_PROVIDER_NAME, extraHeaders));
|
|
14174
|
+
}
|
|
14175
|
+
pi.on("before_provider_headers", (event) => {
|
|
14176
|
+
event.headers.Authorization = `Bearer ${auth.getJwt()}`;
|
|
14177
|
+
});
|
|
14178
|
+
};
|
|
13352
14179
|
const providerExtension = {
|
|
13353
14180
|
name: "free-pi-provider",
|
|
13354
|
-
factory: (pi) =>
|
|
13355
|
-
pi.registerProvider(PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, models));
|
|
13356
|
-
const defense = defenseModelsFor(opts);
|
|
13357
|
-
if (defense.length > 0) {
|
|
13358
|
-
pi.registerProvider(DEFENSE_PROVIDER_NAME, buildProviderConfig(opts.baseUrl, auth.getJwt(), sessionId, defense, DEFENSE_PROVIDER_NAME));
|
|
13359
|
-
}
|
|
13360
|
-
pi.on("before_provider_headers", (event) => {
|
|
13361
|
-
event.headers.Authorization = `Bearer ${auth.getJwt()}`;
|
|
13362
|
-
});
|
|
13363
|
-
}
|
|
14181
|
+
factory: (pi) => registerProviders(pi, sessionId)
|
|
13364
14182
|
};
|
|
13365
14183
|
let advertiserUrl;
|
|
13366
14184
|
const adsExtension = createAdsExtension({
|
|
@@ -13397,11 +14215,16 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13397
14215
|
baseUrl: opts.baseUrl,
|
|
13398
14216
|
getToken: () => auth.getJwt()
|
|
13399
14217
|
});
|
|
14218
|
+
const tty = opts.tty ?? { stdin: process.stdin.isTTY === true, stdout: process.stdout.isTTY === true };
|
|
13400
14219
|
const errorNoticeExtension = createErrorNoticeExtension({
|
|
14220
|
+
freeModels: () => freeCatalogModels(opts),
|
|
14221
|
+
terminal: tty.stdin && tty.stdout,
|
|
13401
14222
|
extraNotice: (env) => {
|
|
13402
14223
|
const session = opts.refillCard;
|
|
13403
14224
|
if (!session || typeof env.remaining_credits_today !== "number")
|
|
13404
14225
|
return;
|
|
14226
|
+
if (suppressCardOnAllowanceMismatch(session, env.allowance_credits))
|
|
14227
|
+
return;
|
|
13405
14228
|
const card = formatLowAllowanceCard(session, env.remaining_credits_today);
|
|
13406
14229
|
if (!card)
|
|
13407
14230
|
return;
|
|
@@ -13410,10 +14233,12 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13410
14233
|
return card;
|
|
13411
14234
|
}
|
|
13412
14235
|
});
|
|
14236
|
+
const choiceOn = choiceEnabled({ env: process.env, arm: opts.choiceArm, configPath: opts.choiceConfigPath ?? getConfigPath() });
|
|
14237
|
+
const allowedTools = choiceOn ? [...ALLOWED_TOOL_NAMES, ASK_USER_TOOL_NAME] : ALLOWED_TOOL_NAMES;
|
|
13413
14238
|
const toolGuardExtension = createToolGuardExtension({
|
|
13414
14239
|
baseUrl: opts.baseUrl,
|
|
13415
14240
|
getToken: () => auth.getJwt(),
|
|
13416
|
-
allowedTools
|
|
14241
|
+
allowedTools
|
|
13417
14242
|
});
|
|
13418
14243
|
const commandsExtension = createFreePiCommandsExtension({
|
|
13419
14244
|
baseUrl: opts.baseUrl,
|
|
@@ -13437,9 +14262,9 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13437
14262
|
const headerExtension = createHeaderExtension({
|
|
13438
14263
|
modelName: resolveModelName(opts),
|
|
13439
14264
|
updateLatest: opts.updateLatest,
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
modelNameFor: (id) =>
|
|
14265
|
+
startupNotes: opts.startupNotes,
|
|
14266
|
+
toolModePath: getToolModePath(opts.agentDir),
|
|
14267
|
+
modelNameFor: (id) => pickerModelName(opts, id)
|
|
13443
14268
|
});
|
|
13444
14269
|
const todoToolExtension = createTodoToolExtension();
|
|
13445
14270
|
const shellSandbox = opts.shellSandbox ?? createShellSandbox();
|
|
@@ -13448,14 +14273,40 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13448
14273
|
const mcpExtension = { name: "pi-mcp", factory: createMcpExtension() };
|
|
13449
14274
|
const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
|
|
13450
14275
|
const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
|
|
14276
|
+
const hiveRoom = { open: false };
|
|
13451
14277
|
const hiveExtension = createHiveExtension({
|
|
13452
14278
|
baseUrl: opts.baseUrl,
|
|
13453
14279
|
getToken: () => auth.getJwt(),
|
|
13454
|
-
knownTools:
|
|
13455
|
-
sandbox: shellSandbox
|
|
14280
|
+
knownTools: allowedTools,
|
|
14281
|
+
sandbox: shellSandbox,
|
|
14282
|
+
roomState: hiveRoom
|
|
14283
|
+
});
|
|
14284
|
+
const choiceExtension = choiceOn ? createChoiceToolExtension({
|
|
14285
|
+
baseUrl: opts.baseUrl,
|
|
14286
|
+
getToken: () => auth.getJwt(),
|
|
14287
|
+
sessionId,
|
|
14288
|
+
arm: () => opts.choiceArm ?? null,
|
|
14289
|
+
headless: !(tty.stdin && tty.stdout),
|
|
14290
|
+
isRoomOpen: () => hiveRoom.open
|
|
14291
|
+
}) : undefined;
|
|
14292
|
+
const subagentsExtension = createSubagentsExtension({
|
|
14293
|
+
sessionId,
|
|
14294
|
+
agentDir: opts.agentDir,
|
|
14295
|
+
fusionIds: resolveFusionIds(models.map((m) => m.id), opts.fusion),
|
|
14296
|
+
hasCredit: () => (getAllowanceCache()?.boughtCredits ?? 0) > 0,
|
|
14297
|
+
extensionsFor: (subSessionId) => [
|
|
14298
|
+
{ name: "free-pi-subagent-provider", factory: (pi) => registerProviders(pi, subSessionId, { "x-freepi-subagent": "1" }) },
|
|
14299
|
+
createToolGuardExtension({
|
|
14300
|
+
baseUrl: opts.baseUrl,
|
|
14301
|
+
getToken: () => auth.getJwt(),
|
|
14302
|
+
allowedTools: [...SAFE_TOOLS, SEND_MESSAGE_TOOL, SUBMIT_PLAN_TOOL]
|
|
14303
|
+
}),
|
|
14304
|
+
createToolAliasesExtension({ sandbox: shellSandbox, shell })
|
|
14305
|
+
]
|
|
13456
14306
|
});
|
|
13457
14307
|
const extensionFactories = [
|
|
13458
14308
|
providerExtension,
|
|
14309
|
+
...models.some((m) => m.id === ULTRAFAST_CATALOG_ID) ? [createUltrafastExtension(models.find((m) => m.id === ULTRAFAST_CATALOG_ID))] : [],
|
|
13459
14310
|
createFusionExtension(models[0].id, resolveFusionIds(models.map((m) => m.id), opts.fusion)),
|
|
13460
14311
|
adsExtension,
|
|
13461
14312
|
usageToolExtension,
|
|
@@ -13464,6 +14315,7 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13464
14315
|
webToolsExtension,
|
|
13465
14316
|
imageToolExtension,
|
|
13466
14317
|
visionToolExtension,
|
|
14318
|
+
...choiceExtension ? [choiceExtension] : [],
|
|
13467
14319
|
errorNoticeExtension,
|
|
13468
14320
|
createUpgradeNoticeExtension({ baseUrl: opts.baseUrl }),
|
|
13469
14321
|
createOutputLimitExtension(),
|
|
@@ -13477,6 +14329,7 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13477
14329
|
mcpExtension,
|
|
13478
14330
|
codemodeExtension,
|
|
13479
14331
|
toolSearchExtension,
|
|
14332
|
+
subagentsExtension,
|
|
13480
14333
|
hiveExtension,
|
|
13481
14334
|
...guestLoginExtension ? [guestLoginExtension] : [],
|
|
13482
14335
|
...opts.guest ? [createGuestNotesExtension({ isGuest: () => auth.isGuest() })] : []
|
|
@@ -13487,10 +14340,11 @@ function buildRuntimeOptions(opts, sessionId) {
|
|
|
13487
14340
|
resourceLoaderOptions: {
|
|
13488
14341
|
extensionFactories,
|
|
13489
14342
|
additionalSkillPaths: [SKILLS_DIR],
|
|
13490
|
-
appendSystemPromptOverride: (base) => [...base, agentRulesFor(opts.costArm)
|
|
14343
|
+
appendSystemPromptOverride: (base) => [...base, choiceOn ? `${agentRulesFor(opts.costArm)}
|
|
14344
|
+
${CHOICE_RULES}` : agentRulesFor(opts.costArm), ...shellPromptLine(shell) ? [shellPromptLine(shell)] : []]
|
|
13491
14345
|
},
|
|
13492
14346
|
extensionNames: extensionFactories.map((e) => e.name),
|
|
13493
|
-
tools: SDK_TOOLS
|
|
14347
|
+
tools: choiceOn ? [...SDK_TOOLS, ASK_USER_TOOL_NAME] : SDK_TOOLS
|
|
13494
14348
|
};
|
|
13495
14349
|
}
|
|
13496
14350
|
function getSessionDir(cwd, agentDir) {
|
|
@@ -13498,7 +14352,7 @@ function getSessionDir(cwd, agentDir) {
|
|
|
13498
14352
|
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
13499
14353
|
return join11(resolve4(agentDir), "sessions", safePath);
|
|
13500
14354
|
}
|
|
13501
|
-
async function findLatestSession(cwd, sessionDir, api =
|
|
14355
|
+
async function findLatestSession(cwd, sessionDir, api = SessionManager2) {
|
|
13502
14356
|
const infos = await api.list(cwd, sessionDir);
|
|
13503
14357
|
if (infos.length === 0)
|
|
13504
14358
|
return;
|
|
@@ -13511,7 +14365,7 @@ async function findLatestSession(cwd, sessionDir, api = SessionManager) {
|
|
|
13511
14365
|
firstMessage: latest.firstMessage ?? ""
|
|
13512
14366
|
};
|
|
13513
14367
|
}
|
|
13514
|
-
async function createSessionManager(cwd, sessionDir, sessionId, api =
|
|
14368
|
+
async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager2) {
|
|
13515
14369
|
if (sessionId) {
|
|
13516
14370
|
const infos = await api.list(cwd, sessionDir);
|
|
13517
14371
|
const info = infos.find((session) => session.id === sessionId);
|
|
@@ -13554,7 +14408,7 @@ async function launchPi(opts) {
|
|
|
13554
14408
|
sessionManager,
|
|
13555
14409
|
sessionStartEvent
|
|
13556
14410
|
}) => {
|
|
13557
|
-
const services = await
|
|
14411
|
+
const services = await createAgentSessionServices2({
|
|
13558
14412
|
cwd: rtCwd,
|
|
13559
14413
|
agentDir: opts.agentDir,
|
|
13560
14414
|
settingsManager: settingsForRuntime(opts, rtCwd, launchTrust),
|
|
@@ -13562,7 +14416,7 @@ async function launchPi(opts) {
|
|
|
13562
14416
|
});
|
|
13563
14417
|
const scopedModels = pickerScopeFor(services.modelRuntime, opts);
|
|
13564
14418
|
return {
|
|
13565
|
-
...await
|
|
14419
|
+
...await createAgentSessionFromServices2({
|
|
13566
14420
|
services,
|
|
13567
14421
|
sessionManager,
|
|
13568
14422
|
sessionStartEvent,
|
|
@@ -13597,7 +14451,15 @@ async function launchPi(opts) {
|
|
|
13597
14451
|
});
|
|
13598
14452
|
installLogoutHandler(mode, { credentialsPath: opts.credentialsPath, isGuest: () => auth.isGuest() });
|
|
13599
14453
|
installModelAllowanceHandler(mode);
|
|
13600
|
-
installFooterRightAlign(mode)
|
|
14454
|
+
installFooterRightAlign(mode, () => {
|
|
14455
|
+
const id = runtime.session?.model?.id;
|
|
14456
|
+
return freeModelBase(pickerModelName(opts, id));
|
|
14457
|
+
});
|
|
14458
|
+
installEditorPlaceholder(mode);
|
|
14459
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
14460
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
14461
|
+
installBottomPin(mode);
|
|
14462
|
+
}
|
|
13601
14463
|
await mode.run();
|
|
13602
14464
|
return sm.getSessionId();
|
|
13603
14465
|
}
|
|
@@ -13630,14 +14492,14 @@ async function promptConsentInteractive(text) {
|
|
|
13630
14492
|
|
|
13631
14493
|
// src/real-spawn.ts
|
|
13632
14494
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
13633
|
-
import { homedir as
|
|
14495
|
+
import { homedir as homedir7 } from "node:os";
|
|
13634
14496
|
function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawnSync2 }) {
|
|
13635
14497
|
return (cmd, args, opts) => {
|
|
13636
14498
|
const result = io.spawnSyncImpl(cmd, args, {
|
|
13637
14499
|
timeout: opts.timeout,
|
|
13638
14500
|
encoding: "utf8",
|
|
13639
14501
|
shell: io.platform === "win32",
|
|
13640
|
-
cwd:
|
|
14502
|
+
cwd: homedir7()
|
|
13641
14503
|
});
|
|
13642
14504
|
return { status: result.status, stdout: result.stdout ?? "", error: result.error };
|
|
13643
14505
|
};
|
|
@@ -13646,7 +14508,7 @@ function createRealSpawn(io = { platform: process.platform, spawnSyncImpl: spawn
|
|
|
13646
14508
|
// src/relaunch.ts
|
|
13647
14509
|
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, realpathSync as realpathSync5, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
|
|
13648
14510
|
import { constants as osConstants, tmpdir as tmpdir2 } from "node:os";
|
|
13649
|
-
import { delimiter, dirname as
|
|
14511
|
+
import { delimiter, dirname as dirname12, isAbsolute as isAbsolute3, join as join12, resolve as resolve5, sep as sep4, win32 } from "node:path";
|
|
13650
14512
|
function applyRelaunchCwd(env, chdir = (d) => process.chdir(d), isDir = (p) => {
|
|
13651
14513
|
try {
|
|
13652
14514
|
return statSync4(p).isDirectory();
|
|
@@ -13687,7 +14549,7 @@ function hasNodeModulesBin(entry) {
|
|
|
13687
14549
|
}
|
|
13688
14550
|
function sanitizedEnv(env, nodeDir, project, home, win) {
|
|
13689
14551
|
const proj = canon(project, win);
|
|
13690
|
-
const broad = proj === canon(home, win) ||
|
|
14552
|
+
const broad = proj === canon(home, win) || dirname12(proj) === proj;
|
|
13691
14553
|
const out = {};
|
|
13692
14554
|
let pathKey = "PATH";
|
|
13693
14555
|
let oldPath = "";
|
|
@@ -13724,12 +14586,12 @@ async function relaunchViaNpx(latest, deps) {
|
|
|
13724
14586
|
if (!latest || !SEMVER.test(latest))
|
|
13725
14587
|
return;
|
|
13726
14588
|
const target = latest;
|
|
13727
|
-
const nodeDir =
|
|
13728
|
-
const npxCli = win ? join12(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join12(
|
|
14589
|
+
const nodeDir = dirname12(deps.execPath);
|
|
14590
|
+
const npxCli = win ? join12(nodeDir, "node_modules", "npm", "bin", "npx-cli.js") : join12(dirname12(nodeDir), "lib", "node_modules", "npm", "bin", "npx-cli.js");
|
|
13729
14591
|
const exists = deps.exists ?? existsSync6;
|
|
13730
14592
|
const distroNpxCli = "/usr/share/nodejs/npm/bin/npx-cli.js";
|
|
13731
14593
|
const healthy = (cli) => {
|
|
13732
|
-
const root = join12(
|
|
14594
|
+
const root = join12(dirname12(cli), "..");
|
|
13733
14595
|
return exists(join12(root, "package.json")) && exists(join12(root, "lib", "cli.js"));
|
|
13734
14596
|
};
|
|
13735
14597
|
let npxCliPath = npxCli;
|
|
@@ -13785,12 +14647,12 @@ async function relaunchViaNpx(latest, deps) {
|
|
|
13785
14647
|
|
|
13786
14648
|
// src/run.ts
|
|
13787
14649
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
13788
|
-
import { mkdirSync as
|
|
13789
|
-
import { SessionManager as
|
|
14650
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
14651
|
+
import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
|
|
13790
14652
|
|
|
13791
14653
|
// src/onboarding.ts
|
|
13792
14654
|
import { existsSync as existsSync7, readdirSync as readdirSync3, realpathSync as realpathSync6 } from "node:fs";
|
|
13793
|
-
import { homedir as
|
|
14655
|
+
import { homedir as homedir8 } from "node:os";
|
|
13794
14656
|
import { join as join13 } from "node:path";
|
|
13795
14657
|
var INTRO_ENV_SKIP = "FREEPI_NO_INTRO";
|
|
13796
14658
|
var INTRO_TEXT = "Tell free-pi what to build, fix, or explain. Resume later with: npx free-pi-cli@latest --continue";
|
|
@@ -13807,7 +14669,7 @@ ${INTRO_TEXT}
|
|
|
13807
14669
|
}
|
|
13808
14670
|
var FIRST_TASK_EXPERIMENT = "retain.first_task";
|
|
13809
14671
|
var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "Gemfile", "composer.json"];
|
|
13810
|
-
function looksLikeProject(cwd, home =
|
|
14672
|
+
function looksLikeProject(cwd, home = homedir8()) {
|
|
13811
14673
|
try {
|
|
13812
14674
|
if (realpathSync6(cwd) === realpathSync6(home))
|
|
13813
14675
|
return false;
|
|
@@ -13829,7 +14691,7 @@ This folder doesn't look like a project. free-pi works best on code you
|
|
|
13829
14691
|
already have: quit, "cd" into a project, and run "npx free-pi-cli@latest" there.
|
|
13830
14692
|
Or just type: Create a small runnable hello-world app here and run it
|
|
13831
14693
|
`.trim();
|
|
13832
|
-
function showFirstTaskHint(log, cwd = process.cwd(), home =
|
|
14694
|
+
function showFirstTaskHint(log, cwd = process.cwd(), home = homedir8()) {
|
|
13833
14695
|
const text = looksLikeProject(cwd, home) ? FIRST_TASK_PROJECT_TEXT : FIRST_TASK_EMPTY_TEXT;
|
|
13834
14696
|
log(`
|
|
13835
14697
|
${text}
|
|
@@ -13839,7 +14701,7 @@ ${text}
|
|
|
13839
14701
|
|
|
13840
14702
|
// src/starter.ts
|
|
13841
14703
|
import { existsSync as existsSync8, readdirSync as readdirSync4, realpathSync as realpathSync7 } from "node:fs";
|
|
13842
|
-
import { homedir as
|
|
14704
|
+
import { homedir as homedir9 } from "node:os";
|
|
13843
14705
|
import { join as join14 } from "node:path";
|
|
13844
14706
|
var STARTER_EXPERIMENT = "first_session_starter";
|
|
13845
14707
|
var STARTER_PROJECT = [
|
|
@@ -13878,7 +14740,7 @@ function getStarterMarkerPath(agentDir = getFreePiAgentDir()) {
|
|
|
13878
14740
|
function starterShownBefore(agentDir) {
|
|
13879
14741
|
return existsSync8(getStarterMarkerPath(agentDir));
|
|
13880
14742
|
}
|
|
13881
|
-
function starterChoices(cwd = process.cwd(), home =
|
|
14743
|
+
function starterChoices(cwd = process.cwd(), home = homedir9()) {
|
|
13882
14744
|
if (looksLikeProject(cwd, home))
|
|
13883
14745
|
return STARTER_PROJECT;
|
|
13884
14746
|
try {
|
|
@@ -13905,12 +14767,12 @@ function parseStarterKey(raw, count) {
|
|
|
13905
14767
|
}
|
|
13906
14768
|
|
|
13907
14769
|
// src/whats-new.ts
|
|
13908
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
13909
|
-
import { dirname as
|
|
14770
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
14771
|
+
import { dirname as dirname13 } from "node:path";
|
|
13910
14772
|
function maybeWhatsNew(cliVersion, agentDir) {
|
|
13911
14773
|
try {
|
|
13912
14774
|
const path = getLastSeenVersionPath(agentDir);
|
|
13913
|
-
const lastSeen = existsSync9(path) ?
|
|
14775
|
+
const lastSeen = existsSync9(path) ? readFileSync6(path, "utf-8").trim() : undefined;
|
|
13914
14776
|
writeLastSeen(path, cliVersion);
|
|
13915
14777
|
if (lastSeen === undefined)
|
|
13916
14778
|
return;
|
|
@@ -13926,8 +14788,8 @@ function maybeWhatsNew(cliVersion, agentDir) {
|
|
|
13926
14788
|
}
|
|
13927
14789
|
}
|
|
13928
14790
|
function writeLastSeen(path, version) {
|
|
13929
|
-
|
|
13930
|
-
|
|
14791
|
+
mkdirSync4(dirname13(path), { recursive: true });
|
|
14792
|
+
writeFileSync4(path, version, "utf-8");
|
|
13931
14793
|
}
|
|
13932
14794
|
|
|
13933
14795
|
// src/run.ts
|
|
@@ -14016,7 +14878,7 @@ var SIGNIN_UNFINISHED_LINE = "Sign-in didn't finish. Run npx free-pi-cli again w
|
|
|
14016
14878
|
var DECLINED_CALM_LINE = "OK, free-pi won't start. Run it again if you change your mind.";
|
|
14017
14879
|
function validateSessionId(value) {
|
|
14018
14880
|
try {
|
|
14019
|
-
|
|
14881
|
+
SessionManager3.inMemory(process.cwd(), { id: value });
|
|
14020
14882
|
return;
|
|
14021
14883
|
} catch (err) {
|
|
14022
14884
|
return `Invalid --session value: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -14067,6 +14929,13 @@ or: npm i -g free-pi-cli@latest`);
|
|
|
14067
14929
|
}
|
|
14068
14930
|
return outcome === "notice" ? { updateLatest: versionCheck.latest } : {};
|
|
14069
14931
|
}
|
|
14932
|
+
function guestTrialNote(variant) {
|
|
14933
|
+
return isHandoffTreatment(variant) ? `
|
|
14934
|
+
You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
|
|
14935
|
+
` : `
|
|
14936
|
+
You're on a short guest trial — ads pay for it. Sign in with GitHub anytime (/login) to get 15,000 free credits a day and unlock a one-time comeback bonus.
|
|
14937
|
+
`;
|
|
14938
|
+
}
|
|
14070
14939
|
async function run(deps, argv = process.argv) {
|
|
14071
14940
|
const resumeFlags = parseResumeFlags(argv);
|
|
14072
14941
|
if (resumeFlags.sessionError) {
|
|
@@ -14104,6 +14973,12 @@ ${FINISH_SIGNIN_LINE}
|
|
|
14104
14973
|
let startedGuestThisRun = false;
|
|
14105
14974
|
let signedInAfterExpiry = false;
|
|
14106
14975
|
let guestHandoffVariant;
|
|
14976
|
+
const startupNotes = [];
|
|
14977
|
+
const note = (text) => void startupNotes.push(text);
|
|
14978
|
+
const flushNotes = () => {
|
|
14979
|
+
for (const n of startupNotes.splice(0))
|
|
14980
|
+
deps.log(n);
|
|
14981
|
+
};
|
|
14107
14982
|
let showFirstTask = false;
|
|
14108
14983
|
let consentCurrent = false;
|
|
14109
14984
|
if (jwt) {
|
|
@@ -14207,13 +15082,9 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
|
|
|
14207
15082
|
jwt = guest.token;
|
|
14208
15083
|
startedGuestThisRun = true;
|
|
14209
15084
|
showIntro(deps.log);
|
|
14210
|
-
|
|
14211
|
-
You're on a short guest trial — ads pay for it. ${LOGIN_VALUE_LINE}
|
|
14212
|
-
` : `
|
|
14213
|
-
You're on a short guest trial — ads pay for it. Sign in with GitHub anytime (/login) to get 15,000 free credits a day and unlock a one-time comeback bonus.
|
|
14214
|
-
`);
|
|
15085
|
+
note(guestTrialNote(guest.guestHandoffVariant));
|
|
14215
15086
|
if (guest.claimCode && guest.githubBonusCredits) {
|
|
14216
|
-
|
|
15087
|
+
note(emphasize(formatGuestBonusBox(guest.claimCode, guest.githubBonusCredits).join(`
|
|
14217
15088
|
`)) + `
|
|
14218
15089
|
`);
|
|
14219
15090
|
}
|
|
@@ -14247,14 +15118,15 @@ ${GUEST_REFUSAL_LINE}`);
|
|
|
14247
15118
|
}
|
|
14248
15119
|
const whatsNew = maybeWhatsNew(CLI_VERSION, deps.agentDir);
|
|
14249
15120
|
if (whatsNew)
|
|
14250
|
-
|
|
15121
|
+
note(whatsNew);
|
|
14251
15122
|
if (versionCheck.notice)
|
|
14252
|
-
|
|
15123
|
+
note(versionCheck.notice);
|
|
14253
15124
|
let me = await fetchMe2(deps.baseUrl, jwt).catch(() => null);
|
|
14254
15125
|
if (me?.guest && !startedGuestThisRun && canPrompt && consentCurrent) {
|
|
14255
15126
|
try {
|
|
14256
15127
|
const choice = await (deps.promptGuestSignin ?? promptGuestSigninInteractive)();
|
|
14257
15128
|
if (choice === "aborted") {
|
|
15129
|
+
flushNotes();
|
|
14258
15130
|
deps.log("Aborted.");
|
|
14259
15131
|
return 1;
|
|
14260
15132
|
}
|
|
@@ -14284,8 +15156,10 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
|
|
|
14284
15156
|
if (me)
|
|
14285
15157
|
updateAllowanceCache(me);
|
|
14286
15158
|
guestHandoffVariant = me?.experiments?.guest_handoff ?? guestHandoffVariant;
|
|
15159
|
+
if (me?.guest && !startedGuestThisRun)
|
|
15160
|
+
note(guestTrialNote(guestHandoffVariant));
|
|
14287
15161
|
if (showFirstTask) {
|
|
14288
|
-
showFirstTaskHint(
|
|
15162
|
+
showFirstTaskHint(note);
|
|
14289
15163
|
reportResumeExposure(deps, jwt, "offer_shown", FIRST_TASK_EXPERIMENT);
|
|
14290
15164
|
}
|
|
14291
15165
|
const startRefill = await fetchRefillStats(deps.baseUrl, jwt).catch(() => {
|
|
@@ -14294,14 +15168,14 @@ Couldn't finish GitHub sign-in. Continuing as a guest.
|
|
|
14294
15168
|
const refillSession = sessionFromStats(startRefill);
|
|
14295
15169
|
const creditsBack = startRefill ? formatCreditsBackNudge(startRefill) : undefined;
|
|
14296
15170
|
if (creditsBack) {
|
|
14297
|
-
|
|
15171
|
+
note(`
|
|
14298
15172
|
${creditsBack}`);
|
|
14299
15173
|
reportRefillCardShown({ baseUrl: deps.baseUrl, jwt });
|
|
14300
15174
|
}
|
|
14301
15175
|
const redeemFlag = parseRedeemFlag(argv);
|
|
14302
15176
|
if (redeemFlag.requested) {
|
|
14303
15177
|
const r = await redeemCode({ baseUrl: deps.baseUrl, getToken: () => jwt, isGuest: () => Boolean(me?.guest) }, redeemFlag.code ?? "");
|
|
14304
|
-
|
|
15178
|
+
note(`
|
|
14305
15179
|
${r.text}
|
|
14306
15180
|
`);
|
|
14307
15181
|
}
|
|
@@ -14311,7 +15185,7 @@ ${r.text}
|
|
|
14311
15185
|
if (!session && resumeFlags.continueLast && !resumeFlags.startNew) {
|
|
14312
15186
|
session = (await lookupLatest(process.cwd(), deps.agentDir))?.id;
|
|
14313
15187
|
if (!session)
|
|
14314
|
-
|
|
15188
|
+
note("No earlier session in this folder; starting a new one.");
|
|
14315
15189
|
}
|
|
14316
15190
|
const experiment = versionCheck.experiments?.[RESUME_ON_LAUNCH_EXPERIMENT];
|
|
14317
15191
|
const variant = experiment?.variant === "treatment" ? "treatment" : "control";
|
|
@@ -14323,6 +15197,7 @@ ${r.text}
|
|
|
14323
15197
|
reportResumeExposure(deps, jwt, "offer_shown");
|
|
14324
15198
|
const answer = await (deps.promptResume ?? promptResumeInteractive)(latest);
|
|
14325
15199
|
if (answer === "aborted") {
|
|
15200
|
+
flushNotes();
|
|
14326
15201
|
deps.log("Aborted.");
|
|
14327
15202
|
return 1;
|
|
14328
15203
|
}
|
|
@@ -14334,11 +15209,12 @@ ${r.text}
|
|
|
14334
15209
|
}
|
|
14335
15210
|
let initialMessage;
|
|
14336
15211
|
let hintShown = false;
|
|
15212
|
+
let importHintNote;
|
|
14337
15213
|
if (importFlag.requested && deps.prepareImport) {
|
|
14338
15214
|
if (resumeFlags.session || resumeFlags.continueLast) {
|
|
14339
|
-
|
|
15215
|
+
note("--import starts a new session, so it is ignored together with --session or --continue.");
|
|
14340
15216
|
} else if (!interactive) {
|
|
14341
|
-
|
|
15217
|
+
note("--import needs an interactive terminal. Starting normally.");
|
|
14342
15218
|
} else {
|
|
14343
15219
|
initialMessage = await deps.prepareImport({ source: importFlag.source, jwt, model: versionCheck.model }).catch(() => {
|
|
14344
15220
|
return;
|
|
@@ -14352,7 +15228,7 @@ ${r.text}
|
|
|
14352
15228
|
return;
|
|
14353
15229
|
});
|
|
14354
15230
|
if (hint) {
|
|
14355
|
-
|
|
15231
|
+
importHintNote = hint;
|
|
14356
15232
|
hintShown = true;
|
|
14357
15233
|
}
|
|
14358
15234
|
}
|
|
@@ -14360,33 +15236,36 @@ ${r.text}
|
|
|
14360
15236
|
const choices = starterChoices();
|
|
14361
15237
|
if (choices) {
|
|
14362
15238
|
try {
|
|
14363
|
-
|
|
14364
|
-
|
|
15239
|
+
mkdirSync5(deps.agentDir, { recursive: true });
|
|
15240
|
+
writeFileSync5(getStarterMarkerPath(deps.agentDir), "1");
|
|
14365
15241
|
} catch {}
|
|
14366
15242
|
const report = (event) => deps.reportStarterEvent ? deps.reportStarterEvent(event) : reportStarterEvent(deps, jwt, event);
|
|
14367
15243
|
deps.log(formatStarterMenu(choices));
|
|
14368
15244
|
report("starter_shown");
|
|
14369
15245
|
const picked = parseStarterKey(await (deps.askStarterKey ?? (() => readOneKeypress("")))(), choices.length);
|
|
14370
15246
|
if (picked === "abort") {
|
|
15247
|
+
flushNotes();
|
|
14371
15248
|
deps.log("Aborted.");
|
|
14372
15249
|
return 1;
|
|
14373
15250
|
}
|
|
14374
15251
|
if (picked !== "dismiss") {
|
|
14375
15252
|
report("starter_picked");
|
|
14376
15253
|
initialMessage = choices[picked].prompt;
|
|
14377
|
-
|
|
15254
|
+
note(`Sending: ${choices[picked].label}
|
|
14378
15255
|
`);
|
|
14379
15256
|
}
|
|
14380
15257
|
}
|
|
14381
15258
|
}
|
|
14382
|
-
if (deps.startupTip && !
|
|
15259
|
+
if (deps.startupTip && !importFlag.requested && canPrompt && hadJwt) {
|
|
14383
15260
|
try {
|
|
14384
|
-
const importAvailable = await deps.hasImportableSession?.().catch(() => false) ?? false;
|
|
15261
|
+
const importAvailable = !hintShown && (await deps.hasImportableSession?.().catch(() => false) ?? false);
|
|
14385
15262
|
const tip = deps.startupTip({ importAvailable });
|
|
14386
15263
|
if (tip)
|
|
14387
|
-
|
|
15264
|
+
note(tip);
|
|
14388
15265
|
} catch {}
|
|
14389
15266
|
}
|
|
15267
|
+
if (importHintNote)
|
|
15268
|
+
note(importHintNote);
|
|
14390
15269
|
const finalSessionId = await deps.launchPi({
|
|
14391
15270
|
baseUrl: deps.baseUrl,
|
|
14392
15271
|
jwt,
|
|
@@ -14403,7 +15282,9 @@ ${r.text}
|
|
|
14403
15282
|
refillCard: refillSession,
|
|
14404
15283
|
allowanceLine: allowanceLine(),
|
|
14405
15284
|
guestHandoffVariant,
|
|
14406
|
-
|
|
15285
|
+
choiceArm: me?.choice_prompts_arm,
|
|
15286
|
+
initialMessage,
|
|
15287
|
+
startupNotes: startupNotes.splice(0)
|
|
14407
15288
|
});
|
|
14408
15289
|
const endRefill = await fetchRefillStats(deps.baseUrl, jwt).catch(() => {
|
|
14409
15290
|
return;
|
|
@@ -14509,7 +15390,7 @@ async function main() {
|
|
|
14509
15390
|
}
|
|
14510
15391
|
if (process.env.FREEPI_RELAUNCH_READY) {
|
|
14511
15392
|
try {
|
|
14512
|
-
|
|
15393
|
+
writeFileSync6(process.env.FREEPI_RELAUNCH_READY, "");
|
|
14513
15394
|
} catch {}
|
|
14514
15395
|
}
|
|
14515
15396
|
const command = parseCliArgs(process.argv.slice(2));
|
|
@@ -14551,7 +15432,7 @@ ${HELP_TEXT}`);
|
|
|
14551
15432
|
baseUrl,
|
|
14552
15433
|
prepareImport: ({ source, jwt, model }) => prepareStartupImport({
|
|
14553
15434
|
cwd: process.cwd(),
|
|
14554
|
-
home:
|
|
15435
|
+
home: homedir10(),
|
|
14555
15436
|
baseUrl,
|
|
14556
15437
|
jwt,
|
|
14557
15438
|
model,
|
|
@@ -14560,8 +15441,8 @@ ${HELP_TEXT}`);
|
|
|
14560
15441
|
askKey: (prompt) => readOneKeypress(prompt)
|
|
14561
15442
|
}),
|
|
14562
15443
|
startupTip: ({ importAvailable }) => maybeTip({ agentDir, importAvailable }),
|
|
14563
|
-
hasImportableSession: async () => (await listFolderSessions({ cwd: process.cwd(), home:
|
|
14564
|
-
importHint: (latestFreePi) => startupImportHint({ cwd: process.cwd(), home:
|
|
15444
|
+
hasImportableSession: async () => (await listFolderSessions({ cwd: process.cwd(), home: homedir10(), limit: 1 })).length > 0,
|
|
15445
|
+
importHint: (latestFreePi) => startupImportHint({ cwd: process.cwd(), home: homedir10(), now: new Date, latestFreePi }),
|
|
14565
15446
|
agentDir,
|
|
14566
15447
|
credentialsPath: getCredentialsPath(agentDir),
|
|
14567
15448
|
promptConsent: promptConsentInteractive,
|
|
@@ -14570,13 +15451,13 @@ ${HELP_TEXT}`);
|
|
|
14570
15451
|
launchPi,
|
|
14571
15452
|
log: (message) => console.log(message),
|
|
14572
15453
|
checkClientVersion: (baseUrl, cliVersion, jwt) => checkClientVersion(baseUrl, cliVersion, fetch, jwt),
|
|
14573
|
-
startGravityPixel: (jwt) => startGravityPixel(jwt, join15(
|
|
15454
|
+
startGravityPixel: (jwt) => startGravityPixel(jwt, join15(dirname14(agentDir), "gravity")),
|
|
14574
15455
|
relaunchLatest: (latest) => relaunchViaNpx(latest, {
|
|
14575
15456
|
env: process.env,
|
|
14576
15457
|
argv: process.argv.slice(2),
|
|
14577
15458
|
platform: process.platform,
|
|
14578
15459
|
cwd: process.cwd(),
|
|
14579
|
-
homedir:
|
|
15460
|
+
homedir: homedir10(),
|
|
14580
15461
|
execPath: process.execPath,
|
|
14581
15462
|
autoUpdateEnabled: () => readAutoUpdate(getConfigPath()),
|
|
14582
15463
|
spawn: (cmd, args, opts) => spawn3(cmd, args, opts),
|