pi-freeflow 1.44.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +11 -13
- package/package.json +1 -1
- package/src/catalog.ts +38 -14
- package/src/client-recovery.ts +238 -0
- package/src/client-staging.ts +316 -0
- package/src/client.ts +117 -472
- package/src/cline-accounts.ts +26 -1223
- package/src/cline-device-auth.ts +5 -3
- package/src/cline-pool.ts +691 -0
- package/src/cline-roll.ts +642 -0
- package/src/commands-cline.ts +283 -0
- package/src/commands-relay.ts +808 -0
- package/src/commands-startup.ts +264 -0
- package/src/commands.ts +119 -1202
- package/src/config-paths.ts +254 -0
- package/src/config.ts +6 -687
- package/src/daemon.ts +16 -1
- package/src/deploy.ts +3 -1
- package/src/fingerprint-env.ts +334 -0
- package/src/fingerprint-tools.ts +348 -0
- package/src/index.ts +10 -6
- package/src/lane-translation.ts +263 -0
- package/src/models.ts +70 -112
- package/src/opencode-fingerprint.ts +29 -1310
- package/src/opencode-identity.ts +464 -0
- package/src/proxy-lanes.ts +1125 -0
- package/src/proxy-ports.ts +317 -0
- package/src/proxy.ts +74 -1385
- package/src/relay-codec.ts +365 -0
- package/src/relay-state.ts +10 -367
- package/src/relay.ts +78 -31
- package/src/sse-aggregator.ts +659 -0
- package/src/stream-cloak.ts +882 -0
- package/src/stream-pipe.ts +22 -871
- package/src/system-ca-fetch.ts +15 -4
- package/src/token.ts +7 -2
- package/src/tool-args.ts +10 -3
- package/src/tool-translation.ts +1 -236
- package/src/toolcall-audit.ts +20 -7
- package/src/update-checker.ts +17 -1
- package/src/update-core.ts +5 -2
- package/src/updater.ts +8 -3
- package/src/upstream-health.ts +22 -2
- package/src/zen-sidecar.ts +1 -0
- package/src/zen-transport.ts +48 -18
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data directory and persistent path resolution for pi-freeflow.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
/** Env override that re-roots ALL pi-freeflow data files (tests/CI sandbox).
|
|
10
|
+
* Unset → pi's agent dir (PI_CODING_AGENT_DIR, else ~/.pi/agent). */
|
|
11
|
+
export const DATA_DIR_ENV = "PI_FREEFLOW_DATA_DIR";
|
|
12
|
+
|
|
13
|
+
/** Env var pi itself uses to re-root its config dir: pi names it
|
|
14
|
+
* `<APP_NAME>_CODING_AGENT_DIR` (stock pi → PI_CODING_AGENT_DIR, else
|
|
15
|
+
* ~/.pi/agent). Freeflow data files follow it so CI/containers only need to
|
|
16
|
+
* relocate one root instead of granting write access to a hardcoded home
|
|
17
|
+
* path; when another host (e.g. the omp plugin loader) does not set it, the
|
|
18
|
+
* ~/.pi/agent default keeps the shared daemon/log location. */
|
|
19
|
+
export const AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
|
|
20
|
+
|
|
21
|
+
/** Expand a leading ~ the way pi expands its own agent-dir value. */
|
|
22
|
+
function expandHome(value: string): string {
|
|
23
|
+
if (value === "~") return homedir();
|
|
24
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(homedir(), value.slice(2));
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function dataDirOverride(): string | null {
|
|
29
|
+
const d = process.env[DATA_DIR_ENV];
|
|
30
|
+
return typeof d === "string" && d.trim() !== "" ? expandHome(d) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function defaultAgentDir(): string {
|
|
34
|
+
const d = process.env[AGENT_DIR_ENV];
|
|
35
|
+
if (typeof d === "string" && d.trim() !== "") return expandHome(d);
|
|
36
|
+
return path.join(homedir(), ".pi", "agent");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function dataFilePath(name: string): string {
|
|
40
|
+
const override = dataDirOverride();
|
|
41
|
+
if (override) return path.join(override, name);
|
|
42
|
+
return path.join(defaultAgentDir(), name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Per-installation proxy token file (src/token.ts reads/creates it at 0600).
|
|
47
|
+
* Sits with the other data files so a sandboxed data dir relocates it too.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveTokenPath(): string {
|
|
50
|
+
try {
|
|
51
|
+
return dataFilePath("token");
|
|
52
|
+
} catch {
|
|
53
|
+
return path.join(
|
|
54
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
55
|
+
"..",
|
|
56
|
+
".pi-freeflow-token",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveRelayStatePath(): string {
|
|
62
|
+
try {
|
|
63
|
+
return dataFilePath("pi-freeflow-relay-state.json");
|
|
64
|
+
} catch {
|
|
65
|
+
return path.join(
|
|
66
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
67
|
+
"..",
|
|
68
|
+
".relay-state.json",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveLogFilePath(): string {
|
|
74
|
+
try {
|
|
75
|
+
return dataFilePath("pi-freeflow.log");
|
|
76
|
+
} catch {
|
|
77
|
+
return path.join(
|
|
78
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
79
|
+
"..",
|
|
80
|
+
"pi-freeflow.log",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Root directory for staged daemon sources — copies taken outside
|
|
87
|
+
* node_modules because Node's type stripping refuses files under a
|
|
88
|
+
* node_modules path (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING, issue #52).
|
|
89
|
+
* Version subdirectories live inside it.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveDaemonSrcCacheDir(): string {
|
|
92
|
+
try {
|
|
93
|
+
return dataFilePath("pi-freeflow-daemon-src");
|
|
94
|
+
} catch {
|
|
95
|
+
return path.join(
|
|
96
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
97
|
+
"..",
|
|
98
|
+
".pi-freeflow-daemon-src",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolveCatalogCachePath(): string {
|
|
104
|
+
try {
|
|
105
|
+
return dataFilePath("pi-freeflow-catalog-cache.json");
|
|
106
|
+
} catch {
|
|
107
|
+
return path.join(
|
|
108
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
109
|
+
"..",
|
|
110
|
+
".catalog-cache.json",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveDebugStatePath(): string {
|
|
116
|
+
try {
|
|
117
|
+
return dataFilePath("pi-freeflow-debug.json");
|
|
118
|
+
} catch {
|
|
119
|
+
return path.join(
|
|
120
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
121
|
+
"..",
|
|
122
|
+
".debug-state.json",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveUpdateCachePath(): string {
|
|
128
|
+
try {
|
|
129
|
+
return dataFilePath("pi-freeflow-update.json");
|
|
130
|
+
} catch {
|
|
131
|
+
return path.join(
|
|
132
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
133
|
+
"..",
|
|
134
|
+
".update-cache.json",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function resolveOnboardedFlagPath(): string {
|
|
140
|
+
try {
|
|
141
|
+
return dataFilePath("pi-freeflow-onboarded");
|
|
142
|
+
} catch {
|
|
143
|
+
return path.join(
|
|
144
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
145
|
+
"..",
|
|
146
|
+
".onboarded",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function resolveUpstreamHealthPath(): string {
|
|
152
|
+
try {
|
|
153
|
+
return dataFilePath("pi-freeflow-upstream-health.json");
|
|
154
|
+
} catch {
|
|
155
|
+
return path.join(
|
|
156
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
157
|
+
"..",
|
|
158
|
+
".upstream-health.json",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** JSONL audit of tool-call turns that failed or needed repair (src/toolcall-audit.ts). */
|
|
164
|
+
export function resolveToolcallAuditPath(): string {
|
|
165
|
+
try {
|
|
166
|
+
return dataFilePath("toolcall-audit.jsonl");
|
|
167
|
+
} catch {
|
|
168
|
+
return path.join(
|
|
169
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
170
|
+
"..",
|
|
171
|
+
".toolcall-audit.jsonl",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function resolveSessionPinPath(): string {
|
|
177
|
+
try {
|
|
178
|
+
return dataFilePath("pi-freeflow-session-pins.json");
|
|
179
|
+
} catch {
|
|
180
|
+
return path.join(
|
|
181
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
182
|
+
"..",
|
|
183
|
+
".session-pins.json",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const RELAY_STATE_FILE = resolveRelayStatePath();
|
|
189
|
+
export const LOG_FILE = resolveLogFilePath();
|
|
190
|
+
export const CATALOG_CACHE_FILE = resolveCatalogCachePath();
|
|
191
|
+
export const DEBUG_STATE_FILE = resolveDebugStatePath();
|
|
192
|
+
export const UPDATE_CACHE_FILE = resolveUpdateCachePath();
|
|
193
|
+
export const ONBOARDED_FLAG_FILE = resolveOnboardedFlagPath();
|
|
194
|
+
export const UPDATE_CHECK_TTL_MS = 86_400_000;
|
|
195
|
+
|
|
196
|
+
// ── Security & Relay Validation ─────────────────────────────────────
|
|
197
|
+
/** Opt-out for tests/dev: when set to "1", relay URLs on http:// and private hosts are allowed. */
|
|
198
|
+
export const ALLOW_UNSAFE_RELAY_ENV = "PI_FREEFLOW_ALLOW_UNSAFE_RELAY";
|
|
199
|
+
/** Opt-out for the stale-daemon replace: when "1", a version-mismatched daemon is never killed. */
|
|
200
|
+
export const NO_KILL_ENV = ALLOW_UNSAFE_RELAY_ENV.replace("_ALLOW_UNSAFE_RELAY", "_NO_KILL");
|
|
201
|
+
/** When "0", the extension never spawns a detached proxy daemon (tests/CI). */
|
|
202
|
+
export const DAEMON_SPAWN_ENV = DATA_DIR_ENV.replace("_DATA_DIR", "_DAEMON_SPAWN");
|
|
203
|
+
/** Explicit runtime executable path for daemon.ts (overrides process.execPath and PATH search). */
|
|
204
|
+
export const DAEMON_RUNTIME_ENV = DATA_DIR_ENV.replace("_DATA_DIR", "_DAEMON_RUNTIME");
|
|
205
|
+
/** Lease TTL for attached clients (ms); expired leases are dropped by the daemon GC. */
|
|
206
|
+
export const DAEMON_TTL_MS_ENV = DATA_DIR_ENV.replace("_DATA_DIR", "_DAEMON_TTL_MS");
|
|
207
|
+
/** Client heartbeat interval (ms) — must stay well under the lease TTL. */
|
|
208
|
+
export const DAEMON_HEARTBEAT_MS_ENV = DATA_DIR_ENV.replace("_DATA_DIR", "_DAEMON_HEARTBEAT_MS");
|
|
209
|
+
/** Daemon GC sweep interval (ms). */
|
|
210
|
+
export const DAEMON_GC_MS_ENV = DATA_DIR_ENV.replace("_DATA_DIR", "_DAEMON_GC_MS");
|
|
211
|
+
/** Zero-lease persistence window before a lease-less daemon exits (ms). */
|
|
212
|
+
export const DAEMON_GRACE_MS_ENV = DATA_DIR_ENV.replace("_DATA_DIR", "_DAEMON_GRACE_MS");
|
|
213
|
+
/** Max time a client waits for a freshly spawned daemon to answer /_health (ms). */
|
|
214
|
+
export const DAEMON_READY_TIMEOUT_MS_ENV = DATA_DIR_ENV.replace("_DATA_DIR", "_DAEMON_READY_TIMEOUT_MS");
|
|
215
|
+
/** Timeout for a single loopback control call (attach/heartbeat/detach) (ms). */
|
|
216
|
+
export const DAEMON_CONTROL_TIMEOUT_MS_ENV = DATA_DIR_ENV.replace("_DATA_DIR", "_DAEMON_CONTROL_TIMEOUT_MS");
|
|
217
|
+
/** Client watchdog tick: how often /_health is polled for version + failed-SSE rate (ms). */
|
|
218
|
+
export const DAEMON_WATCHDOG_MS_ENV = DATA_DIR_ENV.replace("_DATA_DIR", "_DAEMON_WATCHDOG_MS");
|
|
219
|
+
|
|
220
|
+
function envMs(name: string, fallback: number): number {
|
|
221
|
+
const raw = process.env[name];
|
|
222
|
+
if (raw) {
|
|
223
|
+
const parsed = Number(raw);
|
|
224
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
225
|
+
}
|
|
226
|
+
return fallback;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const IS_WINDOWS_HOST = process.platform === "win32";
|
|
230
|
+
export const DAEMON_SPAWN_ENABLED = process.env[DAEMON_SPAWN_ENV] !== "0";
|
|
231
|
+
export const DAEMON_TTL_MS = envMs(DAEMON_TTL_MS_ENV, 30_000);
|
|
232
|
+
export const DAEMON_HEARTBEAT_MS = envMs(DAEMON_HEARTBEAT_MS_ENV, IS_WINDOWS_HOST ? 8_000 : 10_000);
|
|
233
|
+
export const DAEMON_GC_MS = envMs(DAEMON_GC_MS_ENV, 5_000);
|
|
234
|
+
export const DAEMON_GRACE_MS = envMs(DAEMON_GRACE_MS_ENV, IS_WINDOWS_HOST ? 15_000 : 10_000);
|
|
235
|
+
export const DAEMON_READY_TIMEOUT_MS = envMs(DAEMON_READY_TIMEOUT_MS_ENV, 5_000);
|
|
236
|
+
export const DAEMON_CONTROL_TIMEOUT_MS = envMs(DAEMON_CONTROL_TIMEOUT_MS_ENV, IS_WINDOWS_HOST ? 3_000 : 1_500);
|
|
237
|
+
export const DAEMON_WATCHDOG_MS = envMs(DAEMON_WATCHDOG_MS_ENV, 5_000);
|
|
238
|
+
/** Failed-SSE rolling window (streams) and the failure rate that marks a daemon degraded. */
|
|
239
|
+
export const WATCHDOG_SSE_WINDOW = 20;
|
|
240
|
+
export const WATCHDOG_SSE_FAIL_RATE = 0.5;
|
|
241
|
+
/** Minimum samples before the failed-SSE rate can trigger recovery (avoids single-sample flapping). */
|
|
242
|
+
export const WATCHDOG_SSE_MIN_SAMPLES = 5;
|
|
243
|
+
/** Respawn backoff: min(2s * 2^n, 60s) + jitter. */
|
|
244
|
+
export const RECOVERY_BACKOFF_BASE_MS = 2_000;
|
|
245
|
+
export const RECOVERY_BACKOFF_CAP_MS = 60_000;
|
|
246
|
+
/** Breaker: 5 straight failures inside 5min halts respawn for 10min (in-process fallback meanwhile). */
|
|
247
|
+
export const BREAKER_FAILURES = 5;
|
|
248
|
+
export const BREAKER_WINDOW_MS = 5 * 60_000;
|
|
249
|
+
export const BREAKER_HALT_MS = 10 * 60_000;
|
|
250
|
+
/** Busy bypass: a busy daemon is replaced only after 5min continuous busy with zero forwarded bytes for 60s+. */
|
|
251
|
+
export const BUSY_BYPASS_CONTINUOUS_MS = 5 * 60_000;
|
|
252
|
+
export const BUSY_BYPASS_QUIET_MS = 60_000;
|
|
253
|
+
/** Re-probe of the base port before accepting a walked port (ms). */
|
|
254
|
+
export const BASE_PORT_REPROBE_MS = 3_000;
|