pi-freeflow 1.2.0 → 1.2.1
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/README.md +7 -11
- package/extensions/index.ts +4 -2137
- package/package.json +25 -5
- package/src/catalog.ts +255 -0
- package/src/commands.ts +448 -0
- package/src/config.ts +145 -0
- package/src/deploy.ts +126 -0
- package/src/index.ts +243 -0
- package/src/logger.ts +327 -0
- package/src/models.ts +343 -0
- package/src/normalizer.ts +173 -0
- package/src/proxy.ts +467 -0
- package/src/rate-limiter.ts +148 -0
- package/src/relay-state.ts +208 -0
- package/src/relay.ts +198 -0
- package/src/stream-pipe.ts +154 -0
- package/src/types.ts +164 -0
package/src/commands.ts
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive CLI slash commands and status bar manager for pi-freeflow
|
|
3
|
+
*
|
|
4
|
+
* log viewing, debug level configuration, and live catalog refreshing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { refreshCatalog, setAliveCatalog } from "./catalog.ts";
|
|
8
|
+
import { DEBUG_STATE_FILE, DEFAULT_RELAY_URL, LOG_FILE } from "./config.ts";
|
|
9
|
+
import { deployVercelRelay } from "./deploy.ts";
|
|
10
|
+
import {
|
|
11
|
+
LOG_LEVEL_ORDER,
|
|
12
|
+
getMinLogLevel,
|
|
13
|
+
isDebugEnabled,
|
|
14
|
+
loadDebugState,
|
|
15
|
+
readRecentLogs,
|
|
16
|
+
saveDebugState,
|
|
17
|
+
} from "./logger.ts";
|
|
18
|
+
import {
|
|
19
|
+
ensureRelay,
|
|
20
|
+
getActiveRelayState,
|
|
21
|
+
removeRelay,
|
|
22
|
+
saveRelayState,
|
|
23
|
+
setActiveRelayState,
|
|
24
|
+
setStatusUi,
|
|
25
|
+
shortRelayLabel,
|
|
26
|
+
} from "./relay-state.ts";
|
|
27
|
+
import type {
|
|
28
|
+
ExtensionAPI,
|
|
29
|
+
ExtensionContext,
|
|
30
|
+
ExtensionUIContext,
|
|
31
|
+
KnownRelay,
|
|
32
|
+
LogLevel,
|
|
33
|
+
RegisteredCommand,
|
|
34
|
+
RegisteredModel,
|
|
35
|
+
RelayState,
|
|
36
|
+
} from "./types.ts";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Update the TUI status bar widget with active relay state.
|
|
40
|
+
*/
|
|
41
|
+
export function updateStatusBar(ui?: ExtensionUIContext): void {
|
|
42
|
+
if (!ui) return;
|
|
43
|
+
const relayState = getActiveRelayState();
|
|
44
|
+
if (relayState.enabled && relayState.relays.length > 0) {
|
|
45
|
+
const label = shortRelayLabel(relayState.url);
|
|
46
|
+
const idx = Math.max(
|
|
47
|
+
1,
|
|
48
|
+
relayState.relays.findIndex((r) => r.url === relayState.url) + 1,
|
|
49
|
+
);
|
|
50
|
+
const total = relayState.relays.length;
|
|
51
|
+
ui.setStatus("freeflow", `relay: ON | ${label} ${idx}/${total}`);
|
|
52
|
+
} else {
|
|
53
|
+
ui.setStatus("freeflow", undefined);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createCommandSpec(
|
|
58
|
+
_pi: ExtensionAPI,
|
|
59
|
+
onCatalogRefreshed?: (models: RegisteredModel[]) => void,
|
|
60
|
+
): Omit<RegisteredCommand, "name"> {
|
|
61
|
+
return {
|
|
62
|
+
description:
|
|
63
|
+
"Relay egress: on | off | status | logs [level] [n] | debug on|off|status | url [URL] | deploy | list | use <URL> | remove <URL> | refresh",
|
|
64
|
+
getArgumentCompletions: (prefix: string) =>
|
|
65
|
+
[
|
|
66
|
+
"on",
|
|
67
|
+
"off",
|
|
68
|
+
"status",
|
|
69
|
+
"url",
|
|
70
|
+
"deploy",
|
|
71
|
+
"list",
|
|
72
|
+
"use",
|
|
73
|
+
"remove",
|
|
74
|
+
"refresh",
|
|
75
|
+
"models",
|
|
76
|
+
"logs",
|
|
77
|
+
"debug",
|
|
78
|
+
"trace",
|
|
79
|
+
]
|
|
80
|
+
.filter((s) => s.startsWith(prefix))
|
|
81
|
+
.map((s) => ({ value: s, label: s })),
|
|
82
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
83
|
+
const parts = String(args || "")
|
|
84
|
+
.trim()
|
|
85
|
+
.split(/\s+/);
|
|
86
|
+
const sub = parts[0] || "";
|
|
87
|
+
const rest = parts.slice(1).join(" ");
|
|
88
|
+
const relayState = getActiveRelayState();
|
|
89
|
+
|
|
90
|
+
const flash = () => {
|
|
91
|
+
const activeLabel = shortRelayLabel(relayState.url);
|
|
92
|
+
const activeIdx = Math.max(
|
|
93
|
+
1,
|
|
94
|
+
relayState.relays.findIndex((r) => r.url === relayState.url) + 1,
|
|
95
|
+
);
|
|
96
|
+
const total = relayState.relays.length || 1;
|
|
97
|
+
ctx.ui.notify(
|
|
98
|
+
`Relay ${relayState.enabled ? "ON" : "OFF"}${relayState.enabled ? ` → ${activeLabel} (${activeIdx}/${total})` : " (direct)"} | saved=${relayState.relays.length} (auto-fallback rolling)`,
|
|
99
|
+
"info",
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const persist = () => {
|
|
104
|
+
saveRelayState(relayState);
|
|
105
|
+
setStatusUi(ctx.ui);
|
|
106
|
+
updateStatusBar(ctx.ui);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const setRelay = (
|
|
110
|
+
enabled: boolean,
|
|
111
|
+
url: string,
|
|
112
|
+
addLabel?: string,
|
|
113
|
+
) => {
|
|
114
|
+
relayState.enabled = enabled;
|
|
115
|
+
relayState.url = (url || "").trim() || DEFAULT_RELAY_URL;
|
|
116
|
+
if (relayState.url) {
|
|
117
|
+
ensureRelay(relayState, relayState.url, addLabel);
|
|
118
|
+
}
|
|
119
|
+
setActiveRelayState(relayState);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const doDeploy = async () => {
|
|
123
|
+
const defaultName = `relay-${Date.now().toString(36)}`;
|
|
124
|
+
const token = (
|
|
125
|
+
await ctx.ui.input("Vercel API token (vercel-…):", "")
|
|
126
|
+
)?.trim();
|
|
127
|
+
if (!token) {
|
|
128
|
+
ctx.ui.notify("Deploy cancelled — no token", "warning");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const name =
|
|
132
|
+
(
|
|
133
|
+
await ctx.ui.input(
|
|
134
|
+
"Project name (empty = auto):",
|
|
135
|
+
defaultName,
|
|
136
|
+
)
|
|
137
|
+
)?.trim() || defaultName;
|
|
138
|
+
|
|
139
|
+
ctx.ui.setStatus("freeflow", "deploying relay…");
|
|
140
|
+
try {
|
|
141
|
+
const url = await deployVercelRelay(token, name, (m) =>
|
|
142
|
+
ctx.ui.notify(m, "info"),
|
|
143
|
+
);
|
|
144
|
+
setRelay(true, url, `deployed ${name}`);
|
|
145
|
+
persist();
|
|
146
|
+
ctx.ui.notify(`✓ Deployed & active: ${url}`, "info");
|
|
147
|
+
} catch (e) {
|
|
148
|
+
updateStatusBar(ctx.ui);
|
|
149
|
+
ctx.ui.notify(
|
|
150
|
+
`Deploy failed: ${(e as Error).message}`,
|
|
151
|
+
"error",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const switchRelay = async () => {
|
|
157
|
+
if (!relayState.relays.length) {
|
|
158
|
+
ctx.ui.notify("No saved relays yet", "warning");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const fmt = (r: KnownRelay) =>
|
|
162
|
+
`${r.url === relayState.url ? "★ " : " "}${r.url}${r.label ? ` (${r.label})` : ""}`;
|
|
163
|
+
const opts = relayState.relays.map(fmt);
|
|
164
|
+
const choice = await ctx.ui.select("Switch relay", opts);
|
|
165
|
+
if (!choice) return;
|
|
166
|
+
const match = relayState.relays.find((r) => fmt(r) === choice);
|
|
167
|
+
if (!match) return;
|
|
168
|
+
setRelay(true, match.url);
|
|
169
|
+
persist();
|
|
170
|
+
flash();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const showList = () => {
|
|
174
|
+
if (!relayState.relays.length) {
|
|
175
|
+
ctx.ui.notify("No saved relays", "info");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const lines = relayState.relays.map(
|
|
179
|
+
(r) =>
|
|
180
|
+
`${r.url === relayState.url ? "★" : " "} ${r.url}${r.label ? ` [${r.label}]` : ""}`,
|
|
181
|
+
);
|
|
182
|
+
ctx.ui.notify(
|
|
183
|
+
`Saved relays (${relayState.relays.length}):\n${lines.join("\n")}`,
|
|
184
|
+
"info",
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const removeRelayMenu = async () => {
|
|
189
|
+
const removable = relayState.relays.filter(
|
|
190
|
+
(r) => r.url !== relayState.url,
|
|
191
|
+
);
|
|
192
|
+
if (!removable.length) {
|
|
193
|
+
ctx.ui.notify(
|
|
194
|
+
"Nothing to remove — the active relay cannot be removed (switch first)",
|
|
195
|
+
"warning",
|
|
196
|
+
);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const fmt = (r: KnownRelay) =>
|
|
200
|
+
`${r.url}${r.label ? ` (${r.label})` : ""}`;
|
|
201
|
+
const choice = await ctx.ui.select(
|
|
202
|
+
"Remove relay",
|
|
203
|
+
removable.map(fmt),
|
|
204
|
+
);
|
|
205
|
+
if (!choice) return;
|
|
206
|
+
const match = removable.find((r) => fmt(r) === choice);
|
|
207
|
+
if (!match) return;
|
|
208
|
+
removeRelay(relayState, match.url);
|
|
209
|
+
persist();
|
|
210
|
+
ctx.ui.notify(`Removed: ${match.url}`, "info");
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (sub === "on") {
|
|
214
|
+
setRelay(true, relayState.url || DEFAULT_RELAY_URL);
|
|
215
|
+
persist();
|
|
216
|
+
flash();
|
|
217
|
+
} else if (sub === "off") {
|
|
218
|
+
relayState.enabled = false;
|
|
219
|
+
setActiveRelayState(relayState);
|
|
220
|
+
persist();
|
|
221
|
+
flash();
|
|
222
|
+
} else if (sub === "status") {
|
|
223
|
+
flash();
|
|
224
|
+
} else if (sub === "list") {
|
|
225
|
+
showList();
|
|
226
|
+
} else if (sub === "use") {
|
|
227
|
+
const url = (
|
|
228
|
+
rest ||
|
|
229
|
+
(await ctx.ui.input("Relay URL to activate:", "")) ||
|
|
230
|
+
""
|
|
231
|
+
).trim();
|
|
232
|
+
if (!url) {
|
|
233
|
+
ctx.ui.notify("No URL given", "warning");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
setRelay(true, url, "manual");
|
|
237
|
+
persist();
|
|
238
|
+
flash();
|
|
239
|
+
} else if (sub === "debug") {
|
|
240
|
+
const arg = rest.trim().toLowerCase();
|
|
241
|
+
if (arg === "on" || arg === "enable" || arg === "true") {
|
|
242
|
+
saveDebugState({ debug: true });
|
|
243
|
+
ctx.ui.notify(
|
|
244
|
+
"🔍 Debug ON — verbose trace enabled (level=debug). Logs now include request IDs, thinking sniffing, and payload normalize details.",
|
|
245
|
+
"info",
|
|
246
|
+
);
|
|
247
|
+
} else if (
|
|
248
|
+
arg === "off" ||
|
|
249
|
+
arg === "disable" ||
|
|
250
|
+
arg === "false"
|
|
251
|
+
) {
|
|
252
|
+
saveDebugState({ debug: false });
|
|
253
|
+
ctx.ui.notify(
|
|
254
|
+
`🔇 Debug OFF — level restored to info. File: ${DEBUG_STATE_FILE}`,
|
|
255
|
+
"info",
|
|
256
|
+
);
|
|
257
|
+
} else if (arg.startsWith("level")) {
|
|
258
|
+
const lvl = arg.split(/\s+/)[1] as LogLevel | undefined;
|
|
259
|
+
if (lvl && lvl in LOG_LEVEL_ORDER) {
|
|
260
|
+
saveDebugState({ debug: false, level: lvl });
|
|
261
|
+
ctx.ui.notify(
|
|
262
|
+
`Log level set to ${lvl} (persisted to ${DEBUG_STATE_FILE})`,
|
|
263
|
+
"info",
|
|
264
|
+
);
|
|
265
|
+
} else {
|
|
266
|
+
ctx.ui.notify(
|
|
267
|
+
`Unknown level: ${lvl} (use debug/info/warn/error)`,
|
|
268
|
+
"warning",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
const st = loadDebugState();
|
|
273
|
+
const cur = st?.debug
|
|
274
|
+
? "debug (ON)"
|
|
275
|
+
: st?.level ||
|
|
276
|
+
process.env.FREEFLOW_LOG_LEVEL ||
|
|
277
|
+
"info";
|
|
278
|
+
ctx.ui.notify(
|
|
279
|
+
`Debug status: ${cur}\nFile: ${DEBUG_STATE_FILE}\nMinLevel: ${getMinLogLevel()} | isDebug=${isDebugEnabled()}\nUsage: /freeflow debug on|off | /freeflow debug level debug`,
|
|
280
|
+
"info",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
} else if (sub === "logs" || sub === "log" || sub === "trace") {
|
|
284
|
+
try {
|
|
285
|
+
const rawRest = rest.trim();
|
|
286
|
+
let filterLevel: LogLevel | null = null;
|
|
287
|
+
let filterReqId: string | null = null;
|
|
288
|
+
let count = 25;
|
|
289
|
+
|
|
290
|
+
if (sub === "trace" && rawRest) {
|
|
291
|
+
filterReqId = rawRest.split(/\s+/)[0];
|
|
292
|
+
} else if (rawRest) {
|
|
293
|
+
const tokens = rawRest.split(/\s+/);
|
|
294
|
+
for (const t of tokens) {
|
|
295
|
+
const lower = t.toLowerCase();
|
|
296
|
+
if (lower in LOG_LEVEL_ORDER) {
|
|
297
|
+
filterLevel = lower as LogLevel;
|
|
298
|
+
} else if (/^\d+$/.test(t)) {
|
|
299
|
+
count = Math.min(
|
|
300
|
+
200,
|
|
301
|
+
Math.max(5, Number.parseInt(t, 10)),
|
|
302
|
+
);
|
|
303
|
+
} else if (
|
|
304
|
+
/^[a-f0-9]{6,8}$/i.test(t) ||
|
|
305
|
+
t.startsWith("req=")
|
|
306
|
+
) {
|
|
307
|
+
filterReqId = t.replace(/^req=/, "");
|
|
308
|
+
} else if (lower === "trace" || lower === "req") {
|
|
309
|
+
continue;
|
|
310
|
+
} else {
|
|
311
|
+
filterReqId = t;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const result = readRecentLogs(
|
|
317
|
+
filterLevel,
|
|
318
|
+
filterReqId,
|
|
319
|
+
count,
|
|
320
|
+
);
|
|
321
|
+
if (result.lines.length === 0) {
|
|
322
|
+
if (result.totalLines === 0) {
|
|
323
|
+
ctx.ui.notify(
|
|
324
|
+
"Log file is empty (no logs yet)",
|
|
325
|
+
"info",
|
|
326
|
+
);
|
|
327
|
+
} else {
|
|
328
|
+
ctx.ui.notify(
|
|
329
|
+
`No logs matched (level=${filterLevel || "any"} reqId=${filterReqId || "any"} count=${count})`,
|
|
330
|
+
"warning",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const header = `pi-freeflow logs (last ${result.lines.length}/${result.totalMatched} matched, total ${result.totalLines} lines, file: ${LOG_FILE}${filterLevel ? ` level=${filterLevel}` : ""}${filterReqId ? ` req=${filterReqId}` : ""}):`;
|
|
337
|
+
ctx.ui.notify(
|
|
338
|
+
`${header}\n\n${result.lines.join("\n")}`,
|
|
339
|
+
"info",
|
|
340
|
+
);
|
|
341
|
+
} catch (e) {
|
|
342
|
+
ctx.ui.notify(
|
|
343
|
+
`Could not read log file: ${(e as Error).message}`,
|
|
344
|
+
"error",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
} else if (
|
|
348
|
+
sub === "refresh" ||
|
|
349
|
+
sub === "reload" ||
|
|
350
|
+
sub === "models"
|
|
351
|
+
) {
|
|
352
|
+
ctx.ui.notify(
|
|
353
|
+
"Refreshing model catalog from live upstreams…",
|
|
354
|
+
"info",
|
|
355
|
+
);
|
|
356
|
+
const updated = await refreshCatalog(true);
|
|
357
|
+
setAliveCatalog(updated);
|
|
358
|
+
persist();
|
|
359
|
+
onCatalogRefreshed?.(updated);
|
|
360
|
+
ctx.ui.notify(
|
|
361
|
+
`✓ Refreshed ${updated.length} models with full-spec metadata!`,
|
|
362
|
+
"info",
|
|
363
|
+
);
|
|
364
|
+
} else if (sub === "remove") {
|
|
365
|
+
const url = (
|
|
366
|
+
rest ||
|
|
367
|
+
(await ctx.ui.input("Relay URL to remove:", "")) ||
|
|
368
|
+
""
|
|
369
|
+
).trim();
|
|
370
|
+
if (!url) {
|
|
371
|
+
ctx.ui.notify("No URL given", "warning");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (url === relayState.url) {
|
|
375
|
+
ctx.ui.notify(
|
|
376
|
+
"Cannot remove the active relay — switch first",
|
|
377
|
+
"warning",
|
|
378
|
+
);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (!relayState.relays.some((r) => r.url === url)) {
|
|
382
|
+
ctx.ui.notify("Not in saved list", "warning");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
removeRelay(relayState, url);
|
|
386
|
+
persist();
|
|
387
|
+
ctx.ui.notify(`Removed: ${url}`, "info");
|
|
388
|
+
} else if (sub === "url") {
|
|
389
|
+
const input =
|
|
390
|
+
rest ||
|
|
391
|
+
(await ctx.ui.input(
|
|
392
|
+
"Relay URL (empty = default):",
|
|
393
|
+
relayState.url || DEFAULT_RELAY_URL,
|
|
394
|
+
));
|
|
395
|
+
setRelay(
|
|
396
|
+
relayState.enabled,
|
|
397
|
+
(input || "").trim() || DEFAULT_RELAY_URL,
|
|
398
|
+
"manual",
|
|
399
|
+
);
|
|
400
|
+
persist();
|
|
401
|
+
flash();
|
|
402
|
+
} else if (sub === "deploy") {
|
|
403
|
+
await doDeploy();
|
|
404
|
+
} else {
|
|
405
|
+
const choice = await ctx.ui.select("freeflow relay", [
|
|
406
|
+
`Relay: ${relayState.enabled ? "ON" : "OFF"} → ${relayState.url || "direct"}`,
|
|
407
|
+
"Turn ON",
|
|
408
|
+
"Turn OFF",
|
|
409
|
+
"Switch relay…",
|
|
410
|
+
"Remove relay…",
|
|
411
|
+
"Set URL",
|
|
412
|
+
"Deploy Vercel relay…",
|
|
413
|
+
"List saved relays",
|
|
414
|
+
]);
|
|
415
|
+
if (choice === "Turn ON") {
|
|
416
|
+
setRelay(true, relayState.url || DEFAULT_RELAY_URL);
|
|
417
|
+
persist();
|
|
418
|
+
flash();
|
|
419
|
+
} else if (choice === "Turn OFF") {
|
|
420
|
+
relayState.enabled = false;
|
|
421
|
+
setActiveRelayState(relayState);
|
|
422
|
+
persist();
|
|
423
|
+
flash();
|
|
424
|
+
} else if (choice === "Switch relay…") {
|
|
425
|
+
await switchRelay();
|
|
426
|
+
} else if (choice === "Remove relay…") {
|
|
427
|
+
await removeRelayMenu();
|
|
428
|
+
} else if (choice === "Set URL") {
|
|
429
|
+
const input = await ctx.ui.input(
|
|
430
|
+
"Relay URL (empty = default):",
|
|
431
|
+
relayState.url || DEFAULT_RELAY_URL,
|
|
432
|
+
);
|
|
433
|
+
setRelay(
|
|
434
|
+
relayState.enabled,
|
|
435
|
+
(input || "").trim() || DEFAULT_RELAY_URL,
|
|
436
|
+
"manual",
|
|
437
|
+
);
|
|
438
|
+
persist();
|
|
439
|
+
flash();
|
|
440
|
+
} else if (choice === "Deploy Vercel relay…") {
|
|
441
|
+
await doDeploy();
|
|
442
|
+
} else if (choice === "List saved relays") {
|
|
443
|
+
showList();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration and path resolution for pi-freeflow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import type { Upstream } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
// ── Upstream endpoints ──────────────────────────────────────────────
|
|
12
|
+
export const UPSTREAM_OPENCODE = "https://opencode.ai/zen";
|
|
13
|
+
export const KILO_CHAT_URL = "https://api.kilo.ai/api/gateway/chat/completions";
|
|
14
|
+
export const OPENCODE_API_URL = `${UPSTREAM_OPENCODE}/v1`;
|
|
15
|
+
|
|
16
|
+
// ── Network & Server defaults ───────────────────────────────────────
|
|
17
|
+
export const DEFAULT_PORT = 18080;
|
|
18
|
+
export const HOST = "127.0.0.1";
|
|
19
|
+
export const DEFAULT_HOST = "127.0.0.1";
|
|
20
|
+
|
|
21
|
+
export function resolvePort(): number {
|
|
22
|
+
const envPort = process.env.FREEFLOW_PORT;
|
|
23
|
+
if (envPort) {
|
|
24
|
+
const parsed = Number(envPort);
|
|
25
|
+
if (Number.isFinite(parsed) && parsed > 0 && parsed <= 65535) {
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return DEFAULT_PORT;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const PORT = resolvePort();
|
|
33
|
+
|
|
34
|
+
// ── OpenCode client headers ─────────────────────────────────────────
|
|
35
|
+
export const OPENCODE_USER_AGENT = "opencode/latest/1.14.50/cli";
|
|
36
|
+
export const OPENCODE_CLIENT = "cli";
|
|
37
|
+
export const OPENCODE_PROJECT = "default";
|
|
38
|
+
export const OPENCODE_SESSION = randomUUID();
|
|
39
|
+
|
|
40
|
+
export function opencodeHeaders(): Record<string, string> {
|
|
41
|
+
return {
|
|
42
|
+
"User-Agent": OPENCODE_USER_AGENT,
|
|
43
|
+
"x-opencode-client": OPENCODE_CLIENT,
|
|
44
|
+
"x-opencode-project": OPENCODE_PROJECT,
|
|
45
|
+
"x-opencode-session": OPENCODE_SESSION,
|
|
46
|
+
"x-opencode-request": randomUUID(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Relay and Deployment constants ──────────────────────────────────
|
|
51
|
+
export const DEFAULT_RELAY_URL = "";
|
|
52
|
+
export const VERCEL_API = "https://api.vercel.com";
|
|
53
|
+
export const RELAY_MAX_TOKENS = 131_072;
|
|
54
|
+
|
|
55
|
+
// ── Catalog & Logging constants ─────────────────────────────────────
|
|
56
|
+
export const CATALOG_CACHE_TTL_MS = 3_600_000; // 1 hour
|
|
57
|
+
export const LOG_MAX_BYTES = 5 * 1024 * 1024; // 5MB
|
|
58
|
+
export const LOG_MAX_FILES = 3;
|
|
59
|
+
|
|
60
|
+
// ── Rate Limit Maxima ───────────────────────────────────────────────
|
|
61
|
+
export const RATE_LIMIT_MAX: Record<Upstream, number> = {
|
|
62
|
+
opencode: 200, // public free quota: requests per UTC day per IP
|
|
63
|
+
kilo: 200, // documented gateway quota: requests per 1-hour window per IP
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ── Whitelists & Security ───────────────────────────────────────────
|
|
67
|
+
export const ALLOWED_PATH_PATTERN = /^\/v1\/[a-zA-Z0-9/_.,\-?&=]*$/;
|
|
68
|
+
export const PATH_TRAVERSAL_PATTERN = /\.\./;
|
|
69
|
+
export const ALLOWED_METHODS = new Set(["GET", "POST", "OPTIONS", "HEAD"]);
|
|
70
|
+
|
|
71
|
+
export const STRIP_HEADERS = new Set([
|
|
72
|
+
"authorization",
|
|
73
|
+
"host",
|
|
74
|
+
"content-length",
|
|
75
|
+
"x-forwarded-for",
|
|
76
|
+
"x-forwarded-host",
|
|
77
|
+
"x-forwarded-proto",
|
|
78
|
+
"x-real-ip",
|
|
79
|
+
"x-client-ip",
|
|
80
|
+
"x-originate-ip",
|
|
81
|
+
"cookie",
|
|
82
|
+
"set-cookie",
|
|
83
|
+
"proxy-connection",
|
|
84
|
+
"proxy-authorization",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// ── File Path Resolvers ─────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export function resolveRelayStatePath(): string {
|
|
90
|
+
try {
|
|
91
|
+
return path.join(homedir(), ".pi", "agent", "pi-freeflow-relay-state.json");
|
|
92
|
+
} catch {
|
|
93
|
+
return path.join(
|
|
94
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
95
|
+
"..",
|
|
96
|
+
".relay-state.json",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveLogFilePath(): string {
|
|
102
|
+
try {
|
|
103
|
+
return path.join(homedir(), ".pi", "agent", "pi-freeflow.log");
|
|
104
|
+
} catch {
|
|
105
|
+
return path.join(
|
|
106
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
107
|
+
"..",
|
|
108
|
+
"pi-freeflow.log",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolveCatalogCachePath(): string {
|
|
114
|
+
try {
|
|
115
|
+
return path.join(
|
|
116
|
+
homedir(),
|
|
117
|
+
".pi",
|
|
118
|
+
"agent",
|
|
119
|
+
"pi-freeflow-catalog-cache.json",
|
|
120
|
+
);
|
|
121
|
+
} catch {
|
|
122
|
+
return path.join(
|
|
123
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
124
|
+
"..",
|
|
125
|
+
".catalog-cache.json",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function resolveDebugStatePath(): string {
|
|
131
|
+
try {
|
|
132
|
+
return path.join(homedir(), ".pi", "agent", "pi-freeflow-debug.json");
|
|
133
|
+
} catch {
|
|
134
|
+
return path.join(
|
|
135
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
136
|
+
"..",
|
|
137
|
+
".debug-state.json",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const RELAY_STATE_FILE = resolveRelayStatePath();
|
|
143
|
+
export const LOG_FILE = resolveLogFilePath();
|
|
144
|
+
export const CATALOG_CACHE_FILE = resolveCatalogCachePath();
|
|
145
|
+
export const DEBUG_STATE_FILE = resolveDebugStatePath();
|