@raingor/pi-web-switch 0.4.1 → 0.4.2

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.
@@ -0,0 +1,2453 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ const path = require("path");
25
+ const fs = require("fs");
26
+ const url = require("url");
27
+ const os = require("os");
28
+ const child_process = require("child_process");
29
+ const node_sqlite = require("node:sqlite");
30
+ const http = require("http");
31
+ var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
32
+ const PI_DIR = path.join(os.homedir(), ".pi", "agent");
33
+ function getCindySessionsDir() {
34
+ const home = os.homedir();
35
+ if (os.platform() === "darwin") {
36
+ return path.join(home, "Library", "Application Support", "Cindy", "pi-agent-home", "sessions");
37
+ }
38
+ return path.join(home, ".config", "cindy", "pi-agent-home", "sessions");
39
+ }
40
+ function piPath(filename) {
41
+ return path.join(PI_DIR, filename);
42
+ }
43
+ function readJson(filename) {
44
+ const path2 = piPath(filename);
45
+ try {
46
+ if (!fs.existsSync(path2)) return null;
47
+ const raw = fs.readFileSync(path2, "utf-8");
48
+ return JSON.parse(raw);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ function readSettings() {
54
+ return readJson("settings.json");
55
+ }
56
+ function writeSettings(settings) {
57
+ try {
58
+ const path2 = piPath("settings.json");
59
+ const backup = fs.existsSync(path2) ? fs.readFileSync(path2, "utf-8") : null;
60
+ const raw = JSON.stringify(settings, null, 2);
61
+ fs.writeFileSync(path2, raw, "utf-8");
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ function readAuth() {
68
+ return readJson("auth.json");
69
+ }
70
+ function writeAuth(auth) {
71
+ try {
72
+ const path2 = piPath("auth.json");
73
+ fs.writeFileSync(path2, JSON.stringify(auth, null, 2), "utf-8");
74
+ return true;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+ function readModels() {
80
+ return readJson("models.json");
81
+ }
82
+ function writeModels(models) {
83
+ try {
84
+ const path2 = piPath("models.json");
85
+ fs.writeFileSync(path2, JSON.stringify(models, null, 2), "utf-8");
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+ function getSessionDirs() {
92
+ const sessionsPath = path.join(PI_DIR, "sessions");
93
+ if (!fs.existsSync(sessionsPath)) return [];
94
+ return fs.readdirSync(sessionsPath).filter((name) => name.startsWith("--")).map((name) => path.join(sessionsPath, name)).filter((dir) => fs.statSync(dir).isDirectory());
95
+ }
96
+ function walkSessionJsonl(dir, out) {
97
+ let entries;
98
+ try {
99
+ entries = fs.readdirSync(dir);
100
+ } catch {
101
+ return;
102
+ }
103
+ for (const name of entries) {
104
+ const p = path.join(dir, name);
105
+ try {
106
+ const stat = fs.statSync(p);
107
+ if (stat.isDirectory()) {
108
+ walkSessionJsonl(p, out);
109
+ } else if (name === "session.jsonl" || name.endsWith(".jsonl")) {
110
+ out.push(p);
111
+ }
112
+ } catch {
113
+ }
114
+ }
115
+ }
116
+ function getAllSessionFiles() {
117
+ const files = [];
118
+ for (const dir of getSessionDirs()) {
119
+ walkSessionJsonl(dir, files);
120
+ }
121
+ return files;
122
+ }
123
+ const CN_TZ = "Asia/Shanghai";
124
+ function cnDateParts(ts) {
125
+ const d = new Date(ts);
126
+ if (isNaN(d.getTime())) return { date: "unknown", hour: 0 };
127
+ const date = new Intl.DateTimeFormat("en-CA", {
128
+ timeZone: CN_TZ,
129
+ year: "numeric",
130
+ month: "2-digit",
131
+ day: "2-digit"
132
+ }).format(d);
133
+ const hour = Number(
134
+ new Intl.DateTimeFormat("en-US", {
135
+ timeZone: CN_TZ,
136
+ hour: "2-digit",
137
+ hour12: false
138
+ }).format(d)
139
+ );
140
+ return { date, hour: hour === 24 ? 0 : hour };
141
+ }
142
+ function parseSessionFile(filePath) {
143
+ const records = [];
144
+ try {
145
+ const raw = fs.readFileSync(filePath, "utf-8");
146
+ const lines = raw.split("\n").filter((l) => l.trim());
147
+ let currentProvider = "unknown";
148
+ let currentModel = "unknown";
149
+ for (const line of lines) {
150
+ try {
151
+ const obj = JSON.parse(line);
152
+ const type = obj.type;
153
+ if (type === "model_change") {
154
+ currentProvider = obj.provider || currentProvider;
155
+ currentModel = obj.modelId || currentModel;
156
+ continue;
157
+ }
158
+ if (type === "message" && obj.message?.role === "assistant") {
159
+ const usage = obj.message.usage;
160
+ if (!usage || !usage.input) continue;
161
+ const timestamp = obj.timestamp || obj.message.timestamp;
162
+ const { date, hour } = cnDateParts(timestamp);
163
+ records.push({
164
+ date,
165
+ hour,
166
+ providerId: obj.message.provider || currentProvider,
167
+ modelId: obj.message.model || currentModel,
168
+ inputTokens: usage.input ?? 0,
169
+ outputTokens: usage.output ?? 0,
170
+ cacheReadTokens: usage.cacheRead ?? 0,
171
+ cacheWriteTokens: usage.cacheWrite ?? 0,
172
+ requests: 1,
173
+ cost: usage.cost?.total ?? 0
174
+ });
175
+ }
176
+ } catch {
177
+ }
178
+ }
179
+ } catch {
180
+ }
181
+ return records;
182
+ }
183
+ const USAGE_CACHE_TTL_MS = 3e4;
184
+ let usageCache = null;
185
+ function clearUsageCache() {
186
+ usageCache = null;
187
+ }
188
+ function readAllUsage() {
189
+ if (usageCache && Date.now() - usageCache.at < USAGE_CACHE_TTL_MS) {
190
+ return usageCache.records;
191
+ }
192
+ const allRecords = [];
193
+ const files = getAllSessionFiles();
194
+ for (const filePath of files) {
195
+ const records = parseSessionFile(filePath);
196
+ allRecords.push(...records);
197
+ }
198
+ allRecords.sort((a, b) => a.date.localeCompare(b.date));
199
+ usageCache = { records: allRecords, at: Date.now() };
200
+ return allRecords;
201
+ }
202
+ function readCindyUsage() {
203
+ const allRecords = [];
204
+ const cindyDir = getCindySessionsDir();
205
+ if (!fs.existsSync(cindyDir)) return allRecords;
206
+ try {
207
+ const files = fs.readdirSync(cindyDir).filter((f) => f.endsWith(".jsonl"));
208
+ for (const file of files) {
209
+ const filePath = path.join(cindyDir, file);
210
+ if (!fs.statSync(filePath).isFile()) continue;
211
+ const records = parseSessionFile(filePath);
212
+ allRecords.push(...records);
213
+ }
214
+ } catch {
215
+ }
216
+ allRecords.sort((a, b) => a.date.localeCompare(b.date));
217
+ return allRecords;
218
+ }
219
+ function getCindyDbPaths() {
220
+ const home = os.homedir();
221
+ let cindyAppDir;
222
+ if (os.platform() === "darwin") {
223
+ cindyAppDir = path.join(home, "Library", "Application Support", "Cindy");
224
+ } else if (os.platform() === "win32") {
225
+ cindyAppDir = path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "Cindy");
226
+ } else {
227
+ cindyAppDir = path.join(home, ".config", "Cindy");
228
+ }
229
+ if (!fs.existsSync(cindyAppDir)) return [];
230
+ try {
231
+ return fs.readdirSync(cindyAppDir).filter((f) => f.startsWith("cindy-cms") && f.endsWith(".db")).map((f) => path.join(cindyAppDir, f));
232
+ } catch {
233
+ return [];
234
+ }
235
+ }
236
+ function readClaudeUsage() {
237
+ const allRecords = [];
238
+ const dbPaths = getCindyDbPaths();
239
+ if (dbPaths.length === 0) return allRecords;
240
+ for (const dbPath of dbPaths) {
241
+ try {
242
+ const query = "SELECT day, model, cost_usd, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens FROM daily_model_usage WHERE agent_kind = 'claude-code' ORDER BY day";
243
+ const result = child_process.spawnSync("sqlite3", [dbPath, "-json", query], {
244
+ encoding: "utf8",
245
+ timeout: 1e4
246
+ });
247
+ if (result.status !== 0) continue;
248
+ const output = result.stdout?.trim();
249
+ if (!output) continue;
250
+ const rows = JSON.parse(output);
251
+ for (const row of rows) {
252
+ allRecords.push({
253
+ date: row.day,
254
+ providerId: "claude",
255
+ modelId: row.model,
256
+ inputTokens: row.input_tokens ?? 0,
257
+ outputTokens: row.output_tokens ?? 0,
258
+ cacheReadTokens: row.cache_read_tokens ?? 0,
259
+ cacheWriteTokens: row.cache_create_tokens ?? 0,
260
+ requests: 1,
261
+ cost: row.cost_usd ?? 0
262
+ });
263
+ }
264
+ } catch {
265
+ }
266
+ }
267
+ allRecords.sort((a, b) => a.date.localeCompare(b.date));
268
+ return allRecords;
269
+ }
270
+ function readCodexUsage() {
271
+ const allRecords = [];
272
+ const dbPaths = getCindyDbPaths();
273
+ if (dbPaths.length === 0) return allRecords;
274
+ for (const dbPath of dbPaths) {
275
+ try {
276
+ const query = "SELECT day, model, cost_usd, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens FROM daily_model_usage WHERE agent_kind = 'codex' ORDER BY day";
277
+ const result = child_process.spawnSync("sqlite3", [dbPath, "-json", query], {
278
+ encoding: "utf8",
279
+ timeout: 1e4
280
+ });
281
+ if (result.status !== 0) continue;
282
+ const output = result.stdout?.trim();
283
+ if (!output) continue;
284
+ const rows = JSON.parse(output);
285
+ for (const row of rows) {
286
+ allRecords.push({
287
+ date: row.day,
288
+ providerId: "codex",
289
+ modelId: row.model,
290
+ inputTokens: row.input_tokens ?? 0,
291
+ outputTokens: row.output_tokens ?? 0,
292
+ cacheReadTokens: row.cache_read_tokens ?? 0,
293
+ cacheWriteTokens: row.cache_create_tokens ?? 0,
294
+ requests: 1,
295
+ cost: row.cost_usd ?? 0
296
+ });
297
+ }
298
+ } catch {
299
+ }
300
+ }
301
+ allRecords.sort((a, b) => a.date.localeCompare(b.date));
302
+ return allRecords;
303
+ }
304
+ function readAllCombinedUsage() {
305
+ const all = [
306
+ ...readAllUsage(),
307
+ ...readCindyUsage(),
308
+ ...readClaudeUsage(),
309
+ ...readCodexUsage(),
310
+ ...readAtomcodeUsage(),
311
+ ...readCopilotUsage()
312
+ ];
313
+ all.sort((a, b) => a.date.localeCompare(b.date));
314
+ return all;
315
+ }
316
+ const ATOMCODE_DIR = path.join(os.homedir(), ".atomcode");
317
+ function readAtomcodeUsage() {
318
+ const allRecords = [];
319
+ const sessionsDir = path.join(ATOMCODE_DIR, "sessions");
320
+ if (!fs.existsSync(sessionsDir)) return allRecords;
321
+ let sessionDirs = [];
322
+ try {
323
+ sessionDirs = fs.readdirSync(sessionsDir).map((name) => path.join(sessionsDir, name)).filter((dir) => fs.statSync(dir).isDirectory());
324
+ } catch {
325
+ return allRecords;
326
+ }
327
+ for (const dir of sessionDirs) {
328
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
329
+ for (const file of files) {
330
+ const filePath = path.join(dir, file);
331
+ try {
332
+ const raw = fs.readFileSync(filePath, "utf-8");
333
+ const model = sniffAtomcodeModel(path.join(dir, file.replace(/\.jsonl$/, ".snapshot")));
334
+ for (const line of raw.split("\n").filter((l) => l.trim())) {
335
+ try {
336
+ const obj = JSON.parse(line);
337
+ const usage = obj.usage;
338
+ if (!usage || typeof usage.prompt !== "number") continue;
339
+ const { date, hour } = cnDateParts(obj.iso ?? obj.ts ?? "");
340
+ allRecords.push({
341
+ date,
342
+ hour,
343
+ providerId: "atomcode",
344
+ modelId: model,
345
+ inputTokens: usage.prompt ?? 0,
346
+ outputTokens: usage.completion ?? 0,
347
+ cacheReadTokens: usage.cached ?? 0,
348
+ cacheWriteTokens: 0,
349
+ requests: 1,
350
+ cost: 0
351
+ });
352
+ } catch {
353
+ }
354
+ }
355
+ } catch {
356
+ }
357
+ }
358
+ }
359
+ allRecords.sort((a, b) => a.date.localeCompare(b.date));
360
+ return allRecords;
361
+ }
362
+ function sniffAtomcodeModel(snapshotPath) {
363
+ try {
364
+ if (!fs.existsSync(snapshotPath)) return "atomcode";
365
+ const txt = fs.readFileSync(snapshotPath, "utf-8");
366
+ const m = txt.match(/running the ([\w.-]+) model/i);
367
+ return m?.[1] ?? "atomcode";
368
+ } catch {
369
+ return "atomcode";
370
+ }
371
+ }
372
+ const COPILOT_CONFIG_PATH = path.join(PI_DIR, "copilot.json");
373
+ function readCopilotConfig() {
374
+ try {
375
+ if (!fs.existsSync(COPILOT_CONFIG_PATH)) return {};
376
+ const raw = fs.readFileSync(COPILOT_CONFIG_PATH, "utf-8");
377
+ const parsed = JSON.parse(raw);
378
+ return {
379
+ username: typeof parsed.username === "string" ? parsed.username : void 0,
380
+ token: typeof parsed.token === "string" ? parsed.token : void 0
381
+ };
382
+ } catch {
383
+ return {};
384
+ }
385
+ }
386
+ function writeCopilotConfig(cfg) {
387
+ try {
388
+ const clean = {
389
+ username: cfg.username?.trim() || void 0,
390
+ token: cfg.token?.trim() || void 0
391
+ };
392
+ fs.writeFileSync(COPILOT_CONFIG_PATH, JSON.stringify(clean, null, 2), "utf-8");
393
+ return true;
394
+ } catch {
395
+ return false;
396
+ }
397
+ }
398
+ const COPILOT_STORE_PATH = path.join(os.homedir(), ".copilot", "session-store.db");
399
+ function copilotNum(v) {
400
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
401
+ }
402
+ function readCopilotStore() {
403
+ const records = [];
404
+ let db = null;
405
+ try {
406
+ db = new node_sqlite.DatabaseSync(COPILOT_STORE_PATH, { readOnly: true });
407
+ const rows = db.prepare(
408
+ `SELECT model, input_tokens, output_tokens, cache_read_tokens,
409
+ cache_write_tokens, created_at
410
+ FROM assistant_usage_events`
411
+ ).all();
412
+ for (const row of rows) {
413
+ const ts = row.created_at;
414
+ if (typeof ts !== "string" || !ts) continue;
415
+ const { date, hour } = cnDateParts(ts);
416
+ records.push({
417
+ date,
418
+ hour,
419
+ providerId: "copilot",
420
+ modelId: typeof row.model === "string" && row.model ? row.model : "copilot",
421
+ inputTokens: copilotNum(row.input_tokens),
422
+ outputTokens: copilotNum(row.output_tokens),
423
+ cacheReadTokens: copilotNum(row.cache_read_tokens),
424
+ cacheWriteTokens: copilotNum(row.cache_write_tokens),
425
+ requests: 1,
426
+ cost: 0
427
+ });
428
+ }
429
+ } catch {
430
+ } finally {
431
+ try {
432
+ db?.close();
433
+ } catch {
434
+ }
435
+ }
436
+ records.sort((a, b) => a.date.localeCompare(b.date));
437
+ return records;
438
+ }
439
+ const COPILOT_USAGE_TTL_MS = 3e4;
440
+ let copilotUsageCache = null;
441
+ function readCopilotUsage() {
442
+ if (copilotUsageCache && Date.now() - copilotUsageCache.at < COPILOT_USAGE_TTL_MS) {
443
+ return copilotUsageCache.records;
444
+ }
445
+ const records = readCopilotStore();
446
+ copilotUsageCache = { records, at: Date.now() };
447
+ return records;
448
+ }
449
+ function clearCopilotCaches() {
450
+ copilotUsageCache = null;
451
+ }
452
+ const PROVIDER_FILTERS = [
453
+ {
454
+ id: "copilot",
455
+ label: "Copilot",
456
+ patterns: [/^copilot$/i]
457
+ },
458
+ {
459
+ id: "atomcode",
460
+ label: "AtomCode",
461
+ patterns: [/^atomcode$/i]
462
+ },
463
+ {
464
+ id: "opencode",
465
+ label: "OpenCode",
466
+ patterns: [/^opencode$/, /^opencode-go$/i]
467
+ },
468
+ {
469
+ id: "gemini",
470
+ label: "Gemini",
471
+ patterns: [/^google$/, /gemini/i]
472
+ },
473
+ {
474
+ id: "grok",
475
+ label: "Grok",
476
+ patterns: [/^xai$/, /grok/i]
477
+ }
478
+ ];
479
+ function filterByProvider(records, providerId) {
480
+ const filter = PROVIDER_FILTERS.find((f) => f.id === providerId);
481
+ if (!filter) return records;
482
+ return records.filter(
483
+ (r) => filter.patterns.some((p) => p.test(r.providerId) || p.test(r.modelId))
484
+ );
485
+ }
486
+ function getDailyAggregates(records) {
487
+ const daily = /* @__PURE__ */ new Map();
488
+ for (const r of records) {
489
+ const d = daily.get(r.date) ?? {
490
+ totalTokens: 0,
491
+ totalCost: 0,
492
+ totalRequests: 0,
493
+ inputTokens: 0,
494
+ outputTokens: 0
495
+ };
496
+ d.totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
497
+ d.totalCost += r.cost;
498
+ d.totalRequests += r.requests;
499
+ d.inputTokens += r.inputTokens;
500
+ d.outputTokens += r.outputTokens;
501
+ daily.set(r.date, d);
502
+ }
503
+ return Array.from(daily.entries()).map(([date, agg]) => ({ date, ...agg })).sort((a, b) => a.date.localeCompare(b.date));
504
+ }
505
+ function getProviderSummaries(records) {
506
+ const sums = /* @__PURE__ */ new Map();
507
+ for (const r of records) {
508
+ const s = sums.get(r.providerId) ?? {
509
+ totalTokens: 0,
510
+ totalCost: 0,
511
+ totalRequests: 0
512
+ };
513
+ s.totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
514
+ s.totalCost += r.cost;
515
+ s.totalRequests += r.requests;
516
+ sums.set(r.providerId, s);
517
+ }
518
+ return Array.from(sums.entries()).map(([providerId, s]) => ({
519
+ providerId,
520
+ ...s
521
+ }));
522
+ }
523
+ function getModelSummaries(records) {
524
+ const sums = /* @__PURE__ */ new Map();
525
+ for (const r of records) {
526
+ const key = `${r.providerId}/${r.modelId}`;
527
+ const s = sums.get(key) ?? {
528
+ providerId: r.providerId,
529
+ totalTokens: 0,
530
+ totalCost: 0,
531
+ totalRequests: 0,
532
+ count: 0
533
+ };
534
+ s.totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
535
+ s.totalCost += r.cost;
536
+ s.totalRequests += r.requests;
537
+ s.count++;
538
+ sums.set(key, s);
539
+ }
540
+ return Array.from(sums.entries()).map(([key, s]) => {
541
+ const [providerId, modelId] = key.split("/");
542
+ return {
543
+ modelId,
544
+ providerId: s.providerId,
545
+ totalTokens: s.totalTokens,
546
+ totalCost: s.totalCost,
547
+ totalRequests: s.totalRequests,
548
+ avgTokensPerRequest: s.totalRequests > 0 ? Math.round(s.totalTokens / s.totalRequests) : 0
549
+ };
550
+ });
551
+ }
552
+ function getTotals(records) {
553
+ let totalTokens = 0;
554
+ let totalCost = 0;
555
+ let totalRequests = 0;
556
+ for (const r of records) {
557
+ totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
558
+ totalCost += r.cost;
559
+ totalRequests += r.requests;
560
+ }
561
+ return { totalTokens, totalCost, totalRequests };
562
+ }
563
+ function getUsageByRange(records, fromDate, toDate) {
564
+ const filtered = records.filter((r) => r.date >= fromDate && r.date <= toDate);
565
+ let totalInput = 0;
566
+ let totalOutput = 0;
567
+ let totalCacheRead = 0;
568
+ let totalCacheWrite = 0;
569
+ let totalCost = 0;
570
+ let totalRequests = 0;
571
+ for (const r of filtered) {
572
+ totalInput += r.inputTokens;
573
+ totalOutput += r.outputTokens;
574
+ totalCacheRead += r.cacheReadTokens;
575
+ totalCacheWrite += r.cacheWriteTokens;
576
+ totalCost += r.cost;
577
+ totalRequests += r.requests;
578
+ }
579
+ const totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;
580
+ const cacheHitRate = totalTokens > 0 ? (totalCacheRead + totalCacheWrite) / totalTokens * 100 : 0;
581
+ const daily = /* @__PURE__ */ new Map();
582
+ for (const r of filtered) {
583
+ const d = daily.get(r.date) ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, requests: 0 };
584
+ d.input += r.inputTokens;
585
+ d.output += r.outputTokens;
586
+ d.cacheRead += r.cacheReadTokens;
587
+ d.cacheWrite += r.cacheWriteTokens;
588
+ d.cost += r.cost;
589
+ d.requests += r.requests;
590
+ daily.set(r.date, d);
591
+ }
592
+ const hourly = /* @__PURE__ */ new Map();
593
+ for (const r of filtered) {
594
+ if (r.hour !== void 0) {
595
+ const hKey = `${r.date} ${String(r.hour).padStart(2, "0")}:00`;
596
+ const h = hourly.get(hKey) ?? {
597
+ hour: hKey,
598
+ input: 0,
599
+ output: 0,
600
+ cacheRead: 0,
601
+ cacheWrite: 0,
602
+ cost: 0,
603
+ requests: 0
604
+ };
605
+ h.input += r.inputTokens;
606
+ h.output += r.outputTokens;
607
+ h.cacheRead += r.cacheReadTokens;
608
+ h.cacheWrite += r.cacheWriteTokens;
609
+ h.cost += r.cost;
610
+ h.requests += r.requests;
611
+ hourly.set(hKey, h);
612
+ }
613
+ }
614
+ const requestLog = /* @__PURE__ */ new Map();
615
+ for (const r of filtered) {
616
+ const key = `${r.date}|${r.providerId}|${r.modelId}`;
617
+ const existing = requestLog.get(key) ?? {
618
+ timestamp: r.date,
619
+ providerId: r.providerId,
620
+ modelId: r.modelId,
621
+ input: 0,
622
+ output: 0,
623
+ cost: 0,
624
+ requests: 0
625
+ };
626
+ existing.input += r.inputTokens;
627
+ existing.output += r.outputTokens;
628
+ existing.cost += r.cost;
629
+ existing.requests += r.requests;
630
+ requestLog.set(key, existing);
631
+ }
632
+ const providerStats = /* @__PURE__ */ new Map();
633
+ const modelStats = /* @__PURE__ */ new Map();
634
+ for (const r of filtered) {
635
+ const ps = providerStats.get(r.providerId) ?? {
636
+ providerId: r.providerId,
637
+ totalTokens: 0,
638
+ totalInput: 0,
639
+ totalOutput: 0,
640
+ totalCost: 0,
641
+ totalRequests: 0,
642
+ modelCount: /* @__PURE__ */ new Set()
643
+ };
644
+ ps.totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
645
+ ps.totalInput += r.inputTokens;
646
+ ps.totalOutput += r.outputTokens;
647
+ ps.totalCost += r.cost;
648
+ ps.totalRequests += r.requests;
649
+ ps.modelCount.add(r.modelId);
650
+ providerStats.set(r.providerId, ps);
651
+ const mk = `${r.providerId}/${r.modelId}`;
652
+ const ms = modelStats.get(mk) ?? {
653
+ modelId: r.modelId,
654
+ providerId: r.providerId,
655
+ totalTokens: 0,
656
+ totalInput: 0,
657
+ totalOutput: 0,
658
+ totalCost: 0,
659
+ totalRequests: 0
660
+ };
661
+ ms.totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
662
+ ms.totalInput += r.inputTokens;
663
+ ms.totalOutput += r.outputTokens;
664
+ ms.totalCost += r.cost;
665
+ ms.totalRequests += r.requests;
666
+ modelStats.set(mk, ms);
667
+ }
668
+ return {
669
+ totalTokens,
670
+ totalInput,
671
+ totalOutput,
672
+ totalCacheRead,
673
+ totalCacheWrite,
674
+ totalCost,
675
+ totalRequests,
676
+ cacheHitRate: Math.round(cacheHitRate * 10) / 10,
677
+ dailyBreakdown: Array.from(daily.entries()).map(([date, d]) => ({ date, ...d })).sort((a, b) => a.date.localeCompare(b.date)),
678
+ hourlyBreakdown: Array.from(hourly.entries()).map(([, h]) => ({ hour: h.hour, input: h.input, output: h.output, cacheRead: h.cacheRead, cacheWrite: h.cacheWrite, cost: h.cost, requests: h.requests })).sort((a, b) => a.hour.localeCompare(b.hour)),
679
+ requestLog: Array.from(requestLog.values()).sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
680
+ providerStats: Array.from(providerStats.values()).map((ps) => ({ ...ps, modelCount: ps.modelCount.size })).sort((a, b) => b.totalCost - a.totalCost),
681
+ modelStats: Array.from(modelStats.values()).sort((a, b) => b.totalCost - a.totalCost)
682
+ };
683
+ }
684
+ const HERMES_DIR = path.join(PI_DIR, "pi-hermes-memory");
685
+ function readMemoryFiles() {
686
+ const files = [
687
+ { name: "Project Memories", filename: "MEMORY.md" },
688
+ { name: "User Profile", filename: "USER.md" },
689
+ { name: "Failure Records", filename: "failures.md" }
690
+ ];
691
+ return files.map(({ name, filename }) => {
692
+ const filePath = path.join(HERMES_DIR, filename);
693
+ let content = "";
694
+ let updatedAt = "";
695
+ try {
696
+ if (fs.existsSync(filePath)) {
697
+ content = fs.readFileSync(filePath, "utf-8");
698
+ const stat = fs.statSync(filePath);
699
+ updatedAt = stat.mtime.toISOString();
700
+ }
701
+ } catch {
702
+ content = "// Error reading file";
703
+ }
704
+ return { name, filename, content, updatedAt };
705
+ });
706
+ }
707
+ function decodeProjectName(dirName) {
708
+ let decoded = dirName.replace(/^--|--$/g, "").replace(/--/g, "/");
709
+ const home = os.homedir();
710
+ let displayName = decoded;
711
+ if (displayName.startsWith(home)) {
712
+ displayName = "~" + displayName.slice(home.length);
713
+ }
714
+ const segments = displayName.split("/").filter(Boolean);
715
+ const projectName = segments.length > 0 ? segments[segments.length - 1] ?? dirName : dirName;
716
+ return { projectPath: decoded, projectName };
717
+ }
718
+ function listSessions() {
719
+ const dirs = getSessionDirs();
720
+ const groups = /* @__PURE__ */ new Map();
721
+ for (const dir of dirs) {
722
+ const dirName = dir.split("/").pop() || dir;
723
+ const { projectPath, projectName } = decodeProjectName(dirName);
724
+ if (!groups.has(projectPath)) {
725
+ groups.set(projectPath, {
726
+ projectPath,
727
+ projectName,
728
+ sessions: [],
729
+ totalSessions: 0,
730
+ lastActive: ""
731
+ });
732
+ }
733
+ const group = groups.get(projectPath);
734
+ const files = [];
735
+ walkSessionJsonl(dir, files);
736
+ files.sort().reverse();
737
+ for (const filePath of files) {
738
+ const session = parseSessionFileInfo(filePath);
739
+ if (session) {
740
+ group.sessions.push(session);
741
+ }
742
+ }
743
+ group.totalSessions = group.sessions.length;
744
+ if (group.sessions.length > 0) {
745
+ group.lastActive = group.sessions[0]?.timestamp ?? "";
746
+ }
747
+ }
748
+ return Array.from(groups.values()).filter((g) => g.sessions.length > 0).sort((a, b) => b.lastActive.localeCompare(a.lastActive));
749
+ }
750
+ function parseSessionFileInfo(filePath) {
751
+ try {
752
+ const raw = fs.readFileSync(filePath, "utf-8");
753
+ const lines = raw.split("\n").filter((l) => l.trim());
754
+ let id = "";
755
+ let timestamp = "";
756
+ let name;
757
+ let provider = "unknown";
758
+ let model = "unknown";
759
+ let messageCount = 0;
760
+ let firstTs = 0;
761
+ let lastTs = 0;
762
+ for (const line of lines) {
763
+ try {
764
+ const obj = JSON.parse(line);
765
+ const type = obj.type;
766
+ if (type === "session") {
767
+ id = obj.id || "";
768
+ timestamp = obj.timestamp || "";
769
+ const ts = new Date(timestamp).getTime();
770
+ firstTs = ts;
771
+ lastTs = ts;
772
+ } else if (type === "session_info") {
773
+ name = obj.name || name;
774
+ } else if (type === "model_change") {
775
+ provider = obj.provider || provider;
776
+ model = obj.modelId || model;
777
+ } else if (type === "message") {
778
+ messageCount++;
779
+ const ts = new Date(obj.timestamp).getTime();
780
+ if (ts > lastTs) lastTs = ts;
781
+ if (firstTs === 0) firstTs = ts;
782
+ }
783
+ } catch {
784
+ }
785
+ }
786
+ const duration = lastTs > firstTs ? lastTs - firstTs : void 0;
787
+ const fileName = filePath.split("/").pop() || filePath;
788
+ return {
789
+ id,
790
+ fileName,
791
+ filePath,
792
+ timestamp,
793
+ lastActive: lastTs > 0 ? new Date(lastTs).toISOString() : timestamp,
794
+ name,
795
+ provider,
796
+ model,
797
+ messageCount,
798
+ duration
799
+ };
800
+ } catch {
801
+ return null;
802
+ }
803
+ }
804
+ const SESSIONS_DIR = path.join(PI_DIR, "sessions");
805
+ const TRASH_DIR = path.join(PI_DIR, ".trash");
806
+ function walkJsonl(dir, out) {
807
+ let entries;
808
+ try {
809
+ entries = fs.readdirSync(dir);
810
+ } catch {
811
+ return;
812
+ }
813
+ for (const name of entries) {
814
+ const p = path.join(dir, name);
815
+ try {
816
+ if (fs.statSync(p).isDirectory()) walkJsonl(p, out);
817
+ else if (name.endsWith(".jsonl")) out.push(p);
818
+ } catch {
819
+ }
820
+ }
821
+ }
822
+ function trashSessionFile(filePath) {
823
+ try {
824
+ const resolved = path.resolve(filePath);
825
+ if (!resolved.startsWith(SESSIONS_DIR + path.sep)) return false;
826
+ if (!resolved.endsWith(".jsonl") || !fs.existsSync(resolved)) return false;
827
+ const rel = path.relative(SESSIONS_DIR, resolved);
828
+ const trashPath = path.join(TRASH_DIR, rel);
829
+ fs.mkdirSync(path.dirname(trashPath), { recursive: true });
830
+ fs.renameSync(resolved, trashPath);
831
+ return true;
832
+ } catch {
833
+ return false;
834
+ }
835
+ }
836
+ function listTrash() {
837
+ const files = [];
838
+ walkJsonl(TRASH_DIR, files);
839
+ const entries = [];
840
+ for (const trashPath of files) {
841
+ const info = parseSessionFileInfo(trashPath);
842
+ let trashedAt = "";
843
+ try {
844
+ trashedAt = fs.statSync(trashPath).ctime.toISOString();
845
+ } catch {
846
+ }
847
+ const rel = path.relative(TRASH_DIR, trashPath);
848
+ entries.push({
849
+ trashPath,
850
+ originalPath: path.join(SESSIONS_DIR, rel),
851
+ fileName: trashPath.split("/").pop() || trashPath,
852
+ trashedAt,
853
+ sessionId: info?.id || "",
854
+ sessionName: info?.name || "",
855
+ lastActive: info?.lastActive || "",
856
+ messageCount: info?.messageCount || 0
857
+ });
858
+ }
859
+ return entries.sort((a, b) => b.trashedAt.localeCompare(a.trashedAt));
860
+ }
861
+ function restoreFromTrash(trashPath) {
862
+ try {
863
+ const resolved = path.resolve(trashPath);
864
+ if (!resolved.startsWith(TRASH_DIR + path.sep) || !fs.existsSync(resolved)) return false;
865
+ const rel = path.relative(TRASH_DIR, resolved);
866
+ const original = path.join(SESSIONS_DIR, rel);
867
+ fs.mkdirSync(path.dirname(original), { recursive: true });
868
+ fs.renameSync(resolved, original);
869
+ return true;
870
+ } catch {
871
+ return false;
872
+ }
873
+ }
874
+ function permanentlyDeleteTrash(trashPath) {
875
+ try {
876
+ const resolved = path.resolve(trashPath);
877
+ if (!resolved.startsWith(TRASH_DIR + path.sep) || !fs.existsSync(resolved)) return false;
878
+ fs.unlinkSync(resolved);
879
+ return true;
880
+ } catch {
881
+ return false;
882
+ }
883
+ }
884
+ const AUTO_EXPIRE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
885
+ let autoExpireTimer = null;
886
+ function getSessionExpiryDays() {
887
+ const settings = readSettings();
888
+ const val = settings?.sessionExpiryDays;
889
+ const n = typeof val === "number" ? val : Number(val);
890
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 7;
891
+ }
892
+ function autoExpireSessions() {
893
+ const result = { expired: [], skipped: [], errors: [] };
894
+ const expiryDays = getSessionExpiryDays();
895
+ const cutoffMs = Date.now() - expiryDays * 24 * 60 * 60 * 1e3;
896
+ const dirs = getSessionDirs();
897
+ for (const dir of dirs) {
898
+ let files;
899
+ try {
900
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path.join(dir, f));
901
+ } catch {
902
+ continue;
903
+ }
904
+ for (const filePath of files) {
905
+ try {
906
+ const info = parseSessionFileInfo(filePath);
907
+ if (!info) {
908
+ result.skipped.push(filePath);
909
+ continue;
910
+ }
911
+ const lastActiveTs = new Date(info.lastActive || info.timestamp).getTime();
912
+ if (isNaN(lastActiveTs)) {
913
+ result.skipped.push(filePath);
914
+ continue;
915
+ }
916
+ if (lastActiveTs < cutoffMs) {
917
+ const ok = trashSessionFile(filePath);
918
+ if (ok) {
919
+ result.expired.push(filePath);
920
+ } else {
921
+ result.errors.push(`trash failed: ${filePath}`);
922
+ }
923
+ }
924
+ } catch {
925
+ result.errors.push(`scan failed: ${filePath}`);
926
+ }
927
+ }
928
+ }
929
+ return result;
930
+ }
931
+ function startAutoExpiryTimer() {
932
+ if (autoExpireTimer) return;
933
+ try {
934
+ autoExpireSessions();
935
+ } catch {
936
+ }
937
+ autoExpireTimer = setInterval(() => {
938
+ try {
939
+ autoExpireSessions();
940
+ } catch {
941
+ }
942
+ }, AUTO_EXPIRE_INTERVAL_MS);
943
+ }
944
+ function readSessionPreview(filePath, limit = 20) {
945
+ try {
946
+ const resolved = path.resolve(filePath);
947
+ const inSessions = resolved.startsWith(SESSIONS_DIR + path.sep);
948
+ const inTrash = resolved.startsWith(TRASH_DIR + path.sep);
949
+ if (!inSessions && !inTrash || !resolved.endsWith(".jsonl") || !fs.existsSync(resolved)) return null;
950
+ const lines = fs.readFileSync(resolved, "utf-8").split("\n").filter((l) => l.trim());
951
+ const messages = [];
952
+ let total = 0;
953
+ for (const line of lines) {
954
+ try {
955
+ const obj = JSON.parse(line);
956
+ if (obj.type !== "message") continue;
957
+ const msg = obj.message || {};
958
+ const role = msg.role || "";
959
+ if (role !== "user" && role !== "assistant") continue;
960
+ total++;
961
+ if (messages.length >= limit) continue;
962
+ let text = "";
963
+ if (typeof msg.content === "string") {
964
+ text = msg.content;
965
+ } else if (Array.isArray(msg.content)) {
966
+ text = msg.content.filter((c) => c?.type === "text" && c.text).map((c) => c.text).join("\n");
967
+ if (!text) {
968
+ const tools = msg.content.filter((c) => c?.type === "toolCall").length;
969
+ if (tools > 0) text = `[${tools} tool call${tools > 1 ? "s" : ""}]`;
970
+ }
971
+ }
972
+ text = text.trim();
973
+ if (text.length > 400) text = text.slice(0, 400) + "…";
974
+ messages.push({ role, text, timestamp: obj.timestamp || "" });
975
+ } catch {
976
+ }
977
+ }
978
+ return { messages, total };
979
+ } catch {
980
+ return null;
981
+ }
982
+ }
983
+ const MEMORY_FILENAMES = ["MEMORY.md", "USER.md", "failures.md"];
984
+ function sectionText(section) {
985
+ return section.replace(/<!--\s*created\s*=[^>]*-->\s*$/, "").trim();
986
+ }
987
+ function deleteMemoryEntry(filename, entryText) {
988
+ try {
989
+ if (!MEMORY_FILENAMES.includes(filename)) return false;
990
+ const filePath = path.join(HERMES_DIR, filename);
991
+ if (!fs.existsSync(filePath)) return false;
992
+ const content = fs.readFileSync(filePath, "utf-8");
993
+ const sections = content.split("§");
994
+ const target = entryText.trim();
995
+ const idx = sections.findIndex((s) => s.trim().length > 0 && sectionText(s) === target);
996
+ if (idx === -1) return false;
997
+ sections.splice(idx, 1);
998
+ fs.writeFileSync(filePath, sections.join("§"));
999
+ return true;
1000
+ } catch {
1001
+ return false;
1002
+ }
1003
+ }
1004
+ const PI_CORE_PACKAGE = "@earendil-works/pi-coding-agent";
1005
+ const REGISTRY_TIMEOUT_MS = 8e3;
1006
+ function getPiVersion() {
1007
+ const home = os.homedir();
1008
+ const candidates = [
1009
+ process.env.PI_BINARY,
1010
+ "pi",
1011
+ `${home}/.npm-global/bin/pi`,
1012
+ `${home}/.npm-packages/bin/pi`,
1013
+ `${home}/.config/yarn/global/node_modules/.bin/pi`,
1014
+ `${home}/.local/share/pnpm/pi`
1015
+ ].filter(Boolean);
1016
+ for (const bin of candidates) {
1017
+ try {
1018
+ const out = child_process.spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 15e3 });
1019
+ if (out.status === 0) {
1020
+ const v = out.stdout.trim();
1021
+ if (v) return v;
1022
+ }
1023
+ } catch {
1024
+ }
1025
+ }
1026
+ return null;
1027
+ }
1028
+ function readJsonFile(filePath) {
1029
+ try {
1030
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1031
+ } catch {
1032
+ return null;
1033
+ }
1034
+ }
1035
+ function isNewerVersion(installed, latest) {
1036
+ const parse = (v) => (v.replace(/^v/, "").split("-")[0] ?? "").split(".").map((s) => parseInt(s, 10) || 0);
1037
+ const a = parse(installed);
1038
+ const b = parse(latest);
1039
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
1040
+ const ai = a[i] ?? 0;
1041
+ const bi = b[i] ?? 0;
1042
+ if (bi > ai) return true;
1043
+ if (bi < ai) return false;
1044
+ }
1045
+ return false;
1046
+ }
1047
+ let undiciPromise = null;
1048
+ function getUndici() {
1049
+ if (!undiciPromise) {
1050
+ undiciPromise = import("undici").catch(() => null);
1051
+ }
1052
+ return undiciPromise;
1053
+ }
1054
+ let proxyCache = null;
1055
+ function detectProxyUrl() {
1056
+ if (proxyCache && Date.now() - proxyCache.at < 6e4) return proxyCache.url;
1057
+ let found = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || null;
1058
+ if (found && !found.startsWith("http")) found = null;
1059
+ if (!found && process.platform === "darwin") {
1060
+ try {
1061
+ const out = child_process.spawnSync("scutil", ["--proxy"], { encoding: "utf8", timeout: 3e3 }).stdout || "";
1062
+ const get = (k) => out.match(new RegExp(`${k} : (\\S+)`))?.[1];
1063
+ if (get("HTTPSEnable") === "1" && get("HTTPSProxy")) {
1064
+ found = `http://${get("HTTPSProxy")}:${get("HTTPSPort") ?? "80"}`;
1065
+ } else if (get("HTTPEnable") === "1" && get("HTTPProxy")) {
1066
+ found = `http://${get("HTTPProxy")}:${get("HTTPPort") ?? "80"}`;
1067
+ }
1068
+ } catch {
1069
+ }
1070
+ }
1071
+ proxyCache = { url: found, at: Date.now() };
1072
+ return found;
1073
+ }
1074
+ const proxyAgents = /* @__PURE__ */ new Map();
1075
+ async function fetchExternal(url2, init) {
1076
+ const target = url2 instanceof URL ? url2.toString() : url2;
1077
+ const proxy = detectProxyUrl();
1078
+ const undici = proxy ? await getUndici() : null;
1079
+ if (proxy && undici) {
1080
+ try {
1081
+ let agent = proxyAgents.get(proxy);
1082
+ if (!agent) {
1083
+ agent = new undici.ProxyAgent(proxy);
1084
+ proxyAgents.set(proxy, agent);
1085
+ }
1086
+ return await undici.fetch(target, { ...init, dispatcher: agent });
1087
+ } catch {
1088
+ }
1089
+ }
1090
+ return await fetch(target, init);
1091
+ }
1092
+ async function fetchLatestVersion(pkgName) {
1093
+ try {
1094
+ const res = await fetchExternal(`https://registry.npmjs.org/${encodeURIComponent(pkgName)}/latest`, {
1095
+ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
1096
+ headers: { accept: "application/json" }
1097
+ });
1098
+ if (!res.ok) return null;
1099
+ const data = await res.json();
1100
+ return typeof data.version === "string" ? data.version : null;
1101
+ } catch {
1102
+ return null;
1103
+ }
1104
+ }
1105
+ function listInstalledExtensions() {
1106
+ const dir = path.join(PI_DIR, "npm");
1107
+ const manifest = readJsonFile(path.join(dir, "package.json"));
1108
+ if (!manifest?.dependencies) return [];
1109
+ return Object.keys(manifest.dependencies).map((name) => {
1110
+ const pkg = readJsonFile(path.join(dir, "node_modules", name, "package.json"));
1111
+ return { name, installed: pkg?.version ?? "unknown" };
1112
+ });
1113
+ }
1114
+ async function checkUpdates() {
1115
+ const piVersion = getPiVersion();
1116
+ const extensions = listInstalledExtensions();
1117
+ const toItem = async (name, installed) => {
1118
+ const latest = await fetchLatestVersion(name);
1119
+ return {
1120
+ name,
1121
+ installed,
1122
+ latest,
1123
+ hasUpdate: latest !== null && installed !== "unknown" && isNewerVersion(installed, latest)
1124
+ };
1125
+ };
1126
+ const [piItem, ...extItems] = await Promise.all([
1127
+ piVersion ? toItem(PI_CORE_PACKAGE, piVersion) : Promise.resolve(null),
1128
+ ...extensions.map((e) => toItem(e.name, e.installed))
1129
+ ]);
1130
+ return { pi: piItem, extensions: extItems, checkedAt: Date.now() };
1131
+ }
1132
+ function realpathOr(p) {
1133
+ try {
1134
+ return fs.realpathSync(p);
1135
+ } catch {
1136
+ return null;
1137
+ }
1138
+ }
1139
+ function npmBinCandidates() {
1140
+ const home = os.homedir();
1141
+ const fromPath = (process.env.PATH || "").split(":").filter(Boolean).map((d) => path.join(d, "npm"));
1142
+ return [
1143
+ ...fromPath,
1144
+ `${home}/.npm-global/bin/npm`,
1145
+ `${home}/.npm-packages/bin/npm`,
1146
+ `${home}/.local/share/pnpm/npm`,
1147
+ "/usr/local/bin/npm",
1148
+ "/opt/homebrew/bin/npm",
1149
+ // pi-node bundles its own node/npm under ~/.local/share/pi-node/node-*/
1150
+ ...(() => {
1151
+ const base = path.join(home, ".local", "share", "pi-node");
1152
+ try {
1153
+ return fs.readdirSync(base).filter((n) => n.startsWith("node-")).map((n) => path.join(base, n, "bin", "npm"));
1154
+ } catch {
1155
+ return [];
1156
+ }
1157
+ })()
1158
+ ];
1159
+ }
1160
+ function resolveNpmCliJs() {
1161
+ for (const bin of npmBinCandidates()) {
1162
+ const real = realpathOr(bin);
1163
+ if (real && fs.existsSync(real) && /npm-cli\.js$/.test(real)) return real;
1164
+ }
1165
+ return null;
1166
+ }
1167
+ function resolveNodeBin() {
1168
+ const home = os.homedir();
1169
+ const fromPath = (process.env.PATH || "").split(":").filter(Boolean).map((d) => path.join(d, "node"));
1170
+ const candidates = [
1171
+ ...fromPath,
1172
+ `${home}/.npm-global/bin/node`,
1173
+ `${home}/.npm-packages/bin/node`,
1174
+ "/usr/local/bin/node",
1175
+ "/opt/homebrew/bin/node",
1176
+ ...(() => {
1177
+ const base = path.join(home, ".local", "share", "pi-node");
1178
+ try {
1179
+ return fs.readdirSync(base).filter((n) => n.startsWith("node-")).map((n) => path.join(base, n, "bin", "node"));
1180
+ } catch {
1181
+ return [];
1182
+ }
1183
+ })()
1184
+ ];
1185
+ for (const bin of candidates) {
1186
+ try {
1187
+ const out = child_process.spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 5e3 });
1188
+ if (out.status === 0 && out.stdout) return bin;
1189
+ } catch {
1190
+ }
1191
+ }
1192
+ return null;
1193
+ }
1194
+ function applyExtensionUpdates(names) {
1195
+ const dir = path.join(PI_DIR, "npm");
1196
+ const installed = new Set(listInstalledExtensions().map((e) => e.name));
1197
+ const npmCliJs = resolveNpmCliJs();
1198
+ const nodeBin = resolveNodeBin();
1199
+ return names.map((name) => {
1200
+ if (!installed.has(name)) {
1201
+ return { name, success: false, message: "not an installed extension" };
1202
+ }
1203
+ try {
1204
+ const args = ["install", `${name}@latest`, "--no-audit", "--no-fund", "--legacy-peer-deps"];
1205
+ let out;
1206
+ if (nodeBin && npmCliJs) {
1207
+ out = child_process.spawnSync(nodeBin, [npmCliJs, ...args], {
1208
+ cwd: dir,
1209
+ encoding: "utf8",
1210
+ timeout: 12e4
1211
+ });
1212
+ } else {
1213
+ out = child_process.spawnSync("npm", args, { cwd: dir, encoding: "utf8", timeout: 12e4 });
1214
+ }
1215
+ if (out.status === 0) return { name, success: true };
1216
+ const stderr = (out.stderr || "").trim().split("\n").slice(-3).join(" ");
1217
+ return { name, success: false, message: stderr || `npm exited with ${out.status}` };
1218
+ } catch (e) {
1219
+ return { name, success: false, message: String(e) };
1220
+ }
1221
+ });
1222
+ }
1223
+ async function testProviderConnection(baseUrl, apiKey) {
1224
+ let url2;
1225
+ try {
1226
+ url2 = new URL(baseUrl.replace(/\/+$/, "") + "/models");
1227
+ } catch {
1228
+ return { success: false, message: "invalid URL" };
1229
+ }
1230
+ if (url2.protocol !== "http:" && url2.protocol !== "https:") {
1231
+ return { success: false, message: "invalid URL" };
1232
+ }
1233
+ let key = apiKey ?? "";
1234
+ if (key.startsWith("$")) key = process.env[key.slice(1)] ?? "";
1235
+ const headers = {};
1236
+ if (key) headers["Authorization"] = `Bearer ${key}`;
1237
+ const started = Date.now();
1238
+ try {
1239
+ const res = await fetchExternal(url2, {
1240
+ headers,
1241
+ signal: AbortSignal.timeout(1e4)
1242
+ });
1243
+ const latencyMs = Date.now() - started;
1244
+ if (res.ok) return { success: true, status: res.status, latencyMs };
1245
+ return { success: false, status: res.status, latencyMs, message: `HTTP ${res.status}` };
1246
+ } catch (e) {
1247
+ const latencyMs = Date.now() - started;
1248
+ const msg = e?.name === "TimeoutError" ? "timeout" : e?.cause?.code || e?.message || String(e);
1249
+ return { success: false, latencyMs, message: msg };
1250
+ }
1251
+ }
1252
+ function toNum(v) {
1253
+ if (typeof v === "number") return v;
1254
+ if (typeof v !== "string") return void 0;
1255
+ const m = v.match(/^([0-9]+)([KkMm]?)$/);
1256
+ if (!m) return void 0;
1257
+ const n = parseInt(m[1] ?? "0", 10);
1258
+ const u = (m[2] ?? "").toUpperCase();
1259
+ return u === "K" ? n * 1e3 : u === "M" ? n * 1e6 : n;
1260
+ }
1261
+ const REASONING_RE = /(^|[/_\-])(r1|o1|o3|o4|z1|reasoner|reasoning|qwq|deepseek-r|think)([/_\-:]|$)/i;
1262
+ const VISION_RE = /(vision|[-_]vl\b|multimodal|gpt-4o|gpt-5|claude-(sonnet|opus)|gemini|llama-.*vision|qwen.*vl|glm-.*v\b)/i;
1263
+ const AUDIO_RE = /(audio|whisper|tts|speech)/i;
1264
+ function heuristicFlags(id) {
1265
+ const k = id.toLowerCase();
1266
+ const reasoning = REASONING_RE.test(k);
1267
+ const vision = VISION_RE.test(k);
1268
+ const audio = AUDIO_RE.test(k);
1269
+ let contextWindow;
1270
+ if (/[-_](1m|1024k|1048576)\b/i.test(k)) contextWindow = 1048576;
1271
+ else if (/[-_](256k)\b/i.test(k)) contextWindow = 262144;
1272
+ else if (/[-_](128k)\b/i.test(k)) contextWindow = 131072;
1273
+ else if (/[-_](64k)\b/i.test(k)) contextWindow = 65536;
1274
+ else if (/[-_](32k)\b/i.test(k)) contextWindow = 32768;
1275
+ else if (/[-_](16k)\b/i.test(k)) contextWindow = 16384;
1276
+ else if (/[-_](8k)\b/i.test(k)) contextWindow = 8192;
1277
+ return { reasoning, vision, audio, contextWindow };
1278
+ }
1279
+ function isOpenRouter(baseUrl, host) {
1280
+ return host === "openrouter.ai" || host.endsWith(".openrouter.ai") || baseUrl.includes("openrouter.ai");
1281
+ }
1282
+ async function fetchJson(url2, headers, timeoutMs = 15e3) {
1283
+ const res = await fetchExternal(url2, { headers, signal: AbortSignal.timeout(timeoutMs) });
1284
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1285
+ const text = await res.text();
1286
+ const trimmed = text.trimStart();
1287
+ const ctype = res.headers.get("content-type") ?? "";
1288
+ if (trimmed.startsWith("<") || ctype.includes("text/html")) {
1289
+ throw new Error(`endpoint returned HTML, not JSON (check base URL): ${url2.toString()}`);
1290
+ }
1291
+ try {
1292
+ return JSON.parse(text);
1293
+ } catch {
1294
+ throw new Error(`invalid JSON from ${url2.toString()}`);
1295
+ }
1296
+ }
1297
+ function makeHeaders(key, providerId, host) {
1298
+ const headers = {};
1299
+ if (!key) return headers;
1300
+ if (providerId === "anthropic" || host && host.endsWith("api.anthropic.com")) {
1301
+ headers["x-api-key"] = key;
1302
+ headers["anthropic-version"] = "2023-06-01";
1303
+ } else if (providerId === "google" || host && host.endsWith("generativelanguage.googleapis.com")) ;
1304
+ else {
1305
+ headers["Authorization"] = `Bearer ${key}`;
1306
+ }
1307
+ return headers;
1308
+ }
1309
+ async function fetchProviderModels(baseUrl, apiKey, providerId) {
1310
+ let key = apiKey ?? "";
1311
+ if (key.startsWith("$")) key = process.env[key.slice(1)] ?? "";
1312
+ let base;
1313
+ try {
1314
+ base = new URL(baseUrl.replace(/\/+$/, ""));
1315
+ } catch {
1316
+ return { models: [], error: "invalid URL" };
1317
+ }
1318
+ if (base.protocol !== "http:" && base.protocol !== "https:") {
1319
+ return { models: [], error: "invalid URL" };
1320
+ }
1321
+ const host = base.hostname;
1322
+ try {
1323
+ const isOllama = host === "localhost" && base.port === "11434";
1324
+ if (isOllama) {
1325
+ const tagsUrl = new URL("/api/tags", base);
1326
+ const data2 = await fetchJson(tagsUrl, {});
1327
+ const models2 = [];
1328
+ const models_ = data2?.models ?? [];
1329
+ for (const m of models_) {
1330
+ const id = typeof m === "string" ? m : m.name ?? m.model ?? "";
1331
+ if (!id) continue;
1332
+ const flags = heuristicFlags(id);
1333
+ let cw = flags.contextWindow;
1334
+ let mt;
1335
+ try {
1336
+ const showUrl = new URL("/api/show", base);
1337
+ const show = await fetch(showUrl.toString(), {
1338
+ method: "POST",
1339
+ headers: { "Content-Type": "application/json" },
1340
+ body: JSON.stringify({ name: id }),
1341
+ signal: AbortSignal.timeout(5e3)
1342
+ }).then((r) => r.json());
1343
+ cw = toNum(show?.model_info?.[`${show?.modelfile?.split("\n").find((l) => l.startsWith("FROM")) ?? ""}`]) ?? cw;
1344
+ if (show?.context_length) cw = toNum(show.context_length) ?? cw;
1345
+ } catch {
1346
+ }
1347
+ models2.push({
1348
+ id,
1349
+ contextWindow: cw,
1350
+ maxTokens: mt,
1351
+ reasoning: flags.reasoning,
1352
+ vision: flags.vision,
1353
+ audio: flags.audio,
1354
+ source: "ollama"
1355
+ });
1356
+ }
1357
+ return { models: models2 };
1358
+ }
1359
+ const modelsUrl = new URL(base.toString().replace(/\/+$/, "") + "/models");
1360
+ if (providerId === "google" || host.endsWith("generativelanguage.googleapis.com")) {
1361
+ if (key) modelsUrl.searchParams.set("key", key);
1362
+ }
1363
+ const headers = makeHeaders(key, providerId, host);
1364
+ const data = await fetchJson(modelsUrl, headers, isOpenRouter(baseUrl, host) ? 2e4 : 15e3);
1365
+ const seen = /* @__PURE__ */ new Set();
1366
+ const models = [];
1367
+ const pushModel = (m) => {
1368
+ const v = (m.id ?? "").trim();
1369
+ if (!v || seen.has(v)) return;
1370
+ seen.add(v);
1371
+ const flags = heuristicFlags(v);
1372
+ m.reasoning = m.reasoning ?? flags.reasoning;
1373
+ m.vision = m.vision ?? flags.vision;
1374
+ m.audio = m.audio ?? flags.audio;
1375
+ m.contextWindow = m.contextWindow ?? flags.contextWindow;
1376
+ models.push(m);
1377
+ };
1378
+ const visionOf = (item) => {
1379
+ if (!item || typeof item !== "object") return void 0;
1380
+ if (typeof item.capabilities?.vision === "boolean") return item.capabilities.vision;
1381
+ if (item.supports_vision === true || item.vision === true) return true;
1382
+ const mods = item.architecture?.input_modalities ?? item.input_modalities ?? item.modalities;
1383
+ if (Array.isArray(mods)) return mods.includes("image");
1384
+ const modality = item.architecture?.modality;
1385
+ if (typeof modality === "string") {
1386
+ return (modality.split("->")[0] ?? "").includes("image");
1387
+ }
1388
+ return void 0;
1389
+ };
1390
+ const audioOf = (item) => {
1391
+ const mods = item.architecture?.input_modalities ?? item.input_modalities ?? item.modalities;
1392
+ if (Array.isArray(mods)) return mods.includes("audio");
1393
+ return void 0;
1394
+ };
1395
+ const reasoningOf = (item) => {
1396
+ if (item?.reasoning === true || item?.supports_reasoning === true) return true;
1397
+ return void 0;
1398
+ };
1399
+ const parseCost = (pricing) => {
1400
+ if (!pricing) return void 0;
1401
+ const toDollar = (v) => typeof v === "string" ? parseFloat(v) * 1e6 : typeof v === "number" ? v * 1e6 : void 0;
1402
+ const input = toDollar(pricing.prompt ?? pricing.input);
1403
+ const output = toDollar(pricing.completion ?? pricing.output);
1404
+ const cacheRead = toDollar(pricing.cache_read ?? pricing.cacheRead);
1405
+ const cacheWrite = toDollar(pricing.cache_write ?? pricing.cacheWrite);
1406
+ if (input === void 0 && output === void 0) return void 0;
1407
+ return { input: input ?? 0, output: output ?? 0, cacheRead, cacheWrite };
1408
+ };
1409
+ const parseItem = (item) => {
1410
+ const rawId = typeof item === "string" ? item : item?.id ?? item?.model ?? item?.name ?? "";
1411
+ const id = typeof rawId === "string" ? rawId.replace(/^models\//, "") : "";
1412
+ if (!id) return;
1413
+ const name = typeof item?.name === "string" ? item.name : void 0;
1414
+ const cw = toNum(item?.context_length) ?? toNum(item?.max_context) ?? toNum(item?.context_window) ?? toNum(item?.inputTokenLimit) ?? toNum(item?.max_tokens) ?? void 0;
1415
+ const mt = toNum(item?.max_output_tokens) ?? toNum(item?.top_provider?.max_completion_tokens) ?? toNum(item?.max_completion_tokens) ?? toNum(item?.outputTokenLimit) ?? toNum(item?.max_tokens) ?? void 0;
1416
+ const isOR = isOpenRouter(baseUrl, host);
1417
+ pushModel({
1418
+ id,
1419
+ name: name !== id ? name : void 0,
1420
+ contextWindow: cw,
1421
+ maxTokens: mt,
1422
+ reasoning: reasoningOf(item),
1423
+ vision: visionOf(item),
1424
+ audio: audioOf(item),
1425
+ cost: isOR ? parseCost(item.pricing) : void 0,
1426
+ source: isOR ? "openrouter" : "openai"
1427
+ });
1428
+ };
1429
+ if (Array.isArray(data)) {
1430
+ data.forEach(parseItem);
1431
+ } else if (data && typeof data === "object") {
1432
+ const dataArr = data.data ?? data.models ?? data.models_list ?? null;
1433
+ if (Array.isArray(dataArr)) dataArr.forEach(parseItem);
1434
+ }
1435
+ return { models };
1436
+ } catch (e) {
1437
+ const msg = e?.name === "TimeoutError" ? "timeout" : e?.cause?.code || e?.message || String(e);
1438
+ return { models: [], error: msg };
1439
+ }
1440
+ }
1441
+ async function testModel(baseUrl, modelId, apiKey, apiType = "openai-completions") {
1442
+ let url2;
1443
+ try {
1444
+ url2 = new URL(baseUrl.replace(/\/+$/, "") + "/chat/completions");
1445
+ } catch {
1446
+ return { success: false, message: "invalid URL" };
1447
+ }
1448
+ if (url2.protocol !== "http:" && url2.protocol !== "https:") {
1449
+ return { success: false, message: "invalid URL" };
1450
+ }
1451
+ let key = apiKey ?? "";
1452
+ if (key.startsWith("$")) key = process.env[key.slice(1)] ?? "";
1453
+ const headers = { "Content-Type": "application/json" };
1454
+ if (key) headers["Authorization"] = `Bearer ${key}`;
1455
+ const body = {
1456
+ model: modelId,
1457
+ messages: [{ role: "user", content: "Reply with a single word: ok" }],
1458
+ max_tokens: 4,
1459
+ temperature: 0
1460
+ };
1461
+ const started = Date.now();
1462
+ try {
1463
+ const res = await fetchExternal(url2, {
1464
+ method: "POST",
1465
+ headers,
1466
+ body: JSON.stringify(body),
1467
+ signal: AbortSignal.timeout(15e3)
1468
+ });
1469
+ const latencyMs = Date.now() - started;
1470
+ if (res.ok) {
1471
+ const data = await res.json();
1472
+ const choice = data?.choices?.[0];
1473
+ const hasContent = choice && (choice.message?.content || choice.delta?.content);
1474
+ if (hasContent) {
1475
+ return { success: true, latencyMs };
1476
+ }
1477
+ const hasUsage = !!data?.usage;
1478
+ if (hasUsage) {
1479
+ return { success: true, latencyMs, message: "response received (no content)" };
1480
+ }
1481
+ return { success: false, latencyMs, message: "invalid response: " + JSON.stringify(data).slice(0, 150) };
1482
+ }
1483
+ try {
1484
+ const d = await res.json();
1485
+ return { success: false, status: res.status, latencyMs, message: d?.error?.message || `HTTP ${res.status}` };
1486
+ } catch {
1487
+ return { success: false, status: res.status, latencyMs, message: `HTTP ${res.status}` };
1488
+ }
1489
+ } catch (e) {
1490
+ const latencyMs = Date.now() - started;
1491
+ const msg = e?.name === "TimeoutError" ? "timeout" : e?.message || String(e);
1492
+ return { success: false, latencyMs, message: msg };
1493
+ }
1494
+ }
1495
+ const AGENTS_DIR = path.join(PI_DIR, "agents");
1496
+ const CHAINS_DIR = path.join(PI_DIR, "chains");
1497
+ const RUN_HISTORY_PATH = path.join(PI_DIR, "run-history.jsonl");
1498
+ function parseFrontmatter(raw) {
1499
+ const frontmatter = {};
1500
+ const first = raw.indexOf("---");
1501
+ if (first !== 0) return { frontmatter, body: raw };
1502
+ const second = raw.indexOf("---", 3);
1503
+ if (second === -1) return { frontmatter, body: raw };
1504
+ const yamlLines = raw.slice(3, second).trim().split("\n");
1505
+ const body = raw.slice(second + 3).trim();
1506
+ for (const line of yamlLines) {
1507
+ const colonIdx = line.indexOf(":");
1508
+ if (colonIdx === -1) continue;
1509
+ const key = line.slice(0, colonIdx).trim();
1510
+ let value = line.slice(colonIdx + 1).trim();
1511
+ if (value.startsWith("[") && value.endsWith("]")) {
1512
+ value = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, ""));
1513
+ } else if (value === "true" || value === "false") {
1514
+ value = value === "true";
1515
+ } else if (/^\d+$/.test(value)) {
1516
+ value = parseInt(value, 10);
1517
+ } else if (/^\d+\.\d+$/.test(value)) {
1518
+ value = parseFloat(value);
1519
+ } else {
1520
+ value = value.replace(/^["']|["']$/g, "");
1521
+ }
1522
+ frontmatter[key] = value;
1523
+ }
1524
+ return { frontmatter, body };
1525
+ }
1526
+ function listAgents() {
1527
+ try {
1528
+ if (!fs.existsSync(AGENTS_DIR)) return [];
1529
+ const files = fs.readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".md"));
1530
+ return files.map((fileName) => {
1531
+ const filePath = path.join(AGENTS_DIR, fileName);
1532
+ try {
1533
+ let splitMaybe = function(val) {
1534
+ if (Array.isArray(val)) return val.map(String);
1535
+ if (typeof val === "string" && val.trim()) return val.split(/\s*,\s*/).filter(Boolean);
1536
+ return void 0;
1537
+ };
1538
+ const raw = fs.readFileSync(filePath, "utf-8");
1539
+ const { frontmatter, body } = parseFrontmatter(raw);
1540
+ return {
1541
+ name: frontmatter.name || fileName.replace(/\.md$/, ""),
1542
+ fileName,
1543
+ filePath,
1544
+ package: frontmatter.package || "custom",
1545
+ description: frontmatter.description || "",
1546
+ model: frontmatter.model,
1547
+ tools: splitMaybe(frontmatter.tools),
1548
+ thinking: frontmatter.thinking,
1549
+ systemPromptMode: frontmatter.systemPromptMode,
1550
+ inheritProjectContext: frontmatter.inheritProjectContext,
1551
+ inheritSkills: frontmatter.inheritSkills,
1552
+ input: splitMaybe(frontmatter.input),
1553
+ body: body.slice(0, 500)
1554
+ };
1555
+ } catch {
1556
+ return null;
1557
+ }
1558
+ }).filter(Boolean);
1559
+ } catch {
1560
+ return [];
1561
+ }
1562
+ }
1563
+ function listChains() {
1564
+ try {
1565
+ if (!fs.existsSync(CHAINS_DIR)) return [];
1566
+ const files = fs.readdirSync(CHAINS_DIR).filter((f) => f.endsWith(".chain.md"));
1567
+ return files.map((fileName) => {
1568
+ const filePath = path.join(CHAINS_DIR, fileName);
1569
+ try {
1570
+ const raw = fs.readFileSync(filePath, "utf-8");
1571
+ const { frontmatter, body } = parseFrontmatter(raw);
1572
+ const steps = [];
1573
+ const stepRegex = /##\s+(\([^)]+\)\s*\|[^\n]+|[^\n]+)/g;
1574
+ let match;
1575
+ while ((match = stepRegex.exec(body)) !== null) {
1576
+ const header = match[1].trim();
1577
+ const parallelMatch = header.match(/^\(([^)]+)\)/);
1578
+ if (parallelMatch) {
1579
+ const agents = parallelMatch[1].split("|").map((s) => s.trim());
1580
+ agents.forEach((agent) => steps.push({ agent }));
1581
+ } else {
1582
+ steps.push({ agent: header });
1583
+ }
1584
+ }
1585
+ return {
1586
+ name: frontmatter.name || fileName.replace(/\.chain\.md$/, ""),
1587
+ fileName,
1588
+ filePath,
1589
+ description: frontmatter.description || "",
1590
+ steps,
1591
+ body: raw.slice(0, 300)
1592
+ };
1593
+ } catch {
1594
+ return null;
1595
+ }
1596
+ }).filter(Boolean);
1597
+ } catch {
1598
+ return [];
1599
+ }
1600
+ }
1601
+ function readRunHistory(limit = 100) {
1602
+ try {
1603
+ if (!fs.existsSync(RUN_HISTORY_PATH)) return [];
1604
+ const raw = fs.readFileSync(RUN_HISTORY_PATH, "utf-8");
1605
+ const lines = raw.split("\n").filter(Boolean);
1606
+ return lines.slice(-limit).map((line) => {
1607
+ try {
1608
+ return JSON.parse(line);
1609
+ } catch {
1610
+ return null;
1611
+ }
1612
+ }).filter((r) => r !== null).reverse();
1613
+ } catch {
1614
+ return [];
1615
+ }
1616
+ }
1617
+ function readSubagents() {
1618
+ return {
1619
+ agents: listAgents(),
1620
+ chains: listChains(),
1621
+ runHistory: readRunHistory()
1622
+ };
1623
+ }
1624
+ function findPiAiProvidersDir() {
1625
+ const home = os.homedir();
1626
+ const roots = [];
1627
+ const which = child_process.spawnSync("which", ["pi"], { encoding: "utf8", timeout: 5e3 });
1628
+ const bin = which.status === 0 ? which.stdout.trim() : "";
1629
+ if (bin) {
1630
+ const real = child_process.spawnSync("readlink", ["-f", bin], { encoding: "utf8", timeout: 5e3 });
1631
+ const cli = real.status === 0 ? real.stdout.trim() : "";
1632
+ if (cli) roots.push(path.resolve(path.dirname(cli), ".."));
1633
+ }
1634
+ const piNode = path.join(home, ".local", "share", "pi-node");
1635
+ try {
1636
+ for (const v of fs.readdirSync(piNode)) {
1637
+ roots.push(path.join(piNode, v, "lib", "node_modules", PI_CORE_PACKAGE));
1638
+ }
1639
+ } catch {
1640
+ }
1641
+ for (const root of roots) {
1642
+ const dir = path.join(root, "node_modules", "@earendil-works", "pi-ai", "dist", "providers");
1643
+ if (fs.existsSync(path.join(dir, "data"))) return dir;
1644
+ }
1645
+ return null;
1646
+ }
1647
+ let catalogCache = null;
1648
+ function readBuiltinCatalog() {
1649
+ if (catalogCache && Date.now() - catalogCache.at < 3e5) return catalogCache.providers;
1650
+ const dir = findPiAiProvidersDir();
1651
+ if (!dir) return null;
1652
+ const providers = [];
1653
+ let files;
1654
+ try {
1655
+ files = fs.readdirSync(path.join(dir, "data")).filter(
1656
+ (f) => f.endsWith(".json") && !f.startsWith(".")
1657
+ );
1658
+ } catch {
1659
+ return null;
1660
+ }
1661
+ for (const file of files) {
1662
+ const id = file.replace(/\.json$/, "");
1663
+ const data = readJsonFile(path.join(dir, "data", file));
1664
+ if (!data) continue;
1665
+ const models = [];
1666
+ let baseUrl;
1667
+ let api;
1668
+ for (const apiKey of Object.keys(data)) {
1669
+ for (const m of Object.values(data[apiKey] ?? {})) {
1670
+ if (!m?.id) continue;
1671
+ baseUrl = baseUrl ?? m.baseUrl;
1672
+ api = api ?? m.api;
1673
+ models.push({
1674
+ id: m.id,
1675
+ name: m.name,
1676
+ reasoning: !!m.reasoning,
1677
+ input: Array.isArray(m.input) ? m.input : ["text"],
1678
+ contextWindow: m.contextWindow,
1679
+ maxTokens: m.maxTokens,
1680
+ cost: m.cost
1681
+ });
1682
+ }
1683
+ }
1684
+ if (models.length === 0) continue;
1685
+ let name = "";
1686
+ try {
1687
+ const src = fs.readFileSync(path.join(dir, `${id}.js`), "utf-8");
1688
+ const esc = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1689
+ name = src.match(new RegExp(`id:\\s*"${esc}",\\s*name:\\s*"([^"]+)"`))?.[1] ?? src.match(/createProvider\(\{[^}]*?name:\s*"([^"]+)"/)?.[1] ?? "";
1690
+ } catch {
1691
+ }
1692
+ if (!name) {
1693
+ name = id.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
1694
+ }
1695
+ providers.push({
1696
+ id,
1697
+ name,
1698
+ type: "builtin",
1699
+ api,
1700
+ baseUrl,
1701
+ hasAuth: false,
1702
+ authMethod: "env",
1703
+ models: models.sort((a, b) => a.id.localeCompare(b.id))
1704
+ });
1705
+ }
1706
+ if (providers.length === 0) return null;
1707
+ providers.sort((a, b) => a.id.localeCompare(b.id));
1708
+ catalogCache = { providers, at: Date.now() };
1709
+ return providers;
1710
+ }
1711
+ const BUILTIN_PROVIDERS = [
1712
+ {
1713
+ id: "anthropic",
1714
+ name: "Anthropic",
1715
+ type: "builtin",
1716
+ api: "anthropic-messages",
1717
+ baseUrl: "https://api.anthropic.com/v1",
1718
+ hasAuth: true,
1719
+ authMethod: "env",
1720
+ models: [
1721
+ { id: "claude-sonnet-4", name: "Claude 4 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 2e5, maxTokens: 16384, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
1722
+ { id: "claude-sonnet-4-5", name: "Claude 4.5 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 2e5, maxTokens: 16384, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: false },
1723
+ { id: "claude-opus-4", name: "Claude 4 Opus", reasoning: true, input: ["text", "image"], contextWindow: 2e5, maxTokens: 32e3, cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, enabled: false },
1724
+ { id: "claude-haiku-3-5", name: "Claude 3.5 Haiku", reasoning: false, input: ["text", "image"], contextWindow: 2e5, maxTokens: 8192, cost: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 }, enabled: true }
1725
+ ]
1726
+ },
1727
+ {
1728
+ id: "openai",
1729
+ name: "OpenAI",
1730
+ type: "builtin",
1731
+ api: "openai-completions",
1732
+ baseUrl: "https://api.openai.com/v1",
1733
+ hasAuth: true,
1734
+ authMethod: "env",
1735
+ models: [
1736
+ { id: "gpt-4o", name: "GPT-4o", reasoning: false, input: ["text", "image"], contextWindow: 128e3, maxTokens: 16384, cost: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 3.75 }, enabled: true },
1737
+ { id: "gpt-4o-mini", name: "GPT-4o Mini", reasoning: false, input: ["text", "image"], contextWindow: 128e3, maxTokens: 16384, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.225 }, enabled: true },
1738
+ { id: "gpt-5.1", name: "GPT-5.1", reasoning: true, input: ["text", "image"], contextWindow: 256e3, maxTokens: 65536, cost: { input: 10, output: 40, cacheRead: 5, cacheWrite: 10 }, enabled: false },
1739
+ { id: "o3-mini", name: "o3-mini", reasoning: true, input: ["text"], contextWindow: 2e5, maxTokens: 1e5, cost: { input: 1.1, output: 4.4, cacheRead: 0.55, cacheWrite: 1.65 }, enabled: false }
1740
+ ]
1741
+ },
1742
+ {
1743
+ id: "deepseek",
1744
+ name: "DeepSeek",
1745
+ type: "builtin",
1746
+ api: "openai-completions",
1747
+ baseUrl: "https://api.deepseek.com/v1",
1748
+ hasAuth: true,
1749
+ authMethod: "env",
1750
+ models: [
1751
+ { id: "deepseek-chat", name: "DeepSeek V3", reasoning: false, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 0.27, output: 1.1, cacheRead: 0.07, cacheWrite: 0.27 }, enabled: true },
1752
+ { id: "deepseek-reasoner", name: "DeepSeek R1", reasoning: true, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 }, enabled: true }
1753
+ ]
1754
+ },
1755
+ {
1756
+ id: "opencode",
1757
+ name: "OpenCode",
1758
+ type: "builtin",
1759
+ api: "openai-completions",
1760
+ hasAuth: true,
1761
+ authMethod: "file",
1762
+ models: [
1763
+ { id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash (Free)", reasoning: false, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, enabled: true },
1764
+ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true }
1765
+ ]
1766
+ },
1767
+ {
1768
+ id: "opencode-go",
1769
+ name: "OpenCode Go",
1770
+ type: "builtin",
1771
+ api: "openai-completions",
1772
+ hasAuth: true,
1773
+ authMethod: "file",
1774
+ models: [
1775
+ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true },
1776
+ { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 2, output: 8, cacheRead: 1, cacheWrite: 2 }, enabled: true },
1777
+ { id: "glm-5.1", name: "GLM 5.1", reasoning: false, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 0.5, output: 2, cacheRead: 0.25, cacheWrite: 0.5 }, enabled: true },
1778
+ { id: "qwen3.7-max", name: "Qwen 3.7 Max", reasoning: true, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 1.5, output: 6, cacheRead: 0.75, cacheWrite: 1.5 }, enabled: true },
1779
+ { id: "mimo-v2.5", name: "MiMo V2.5", reasoning: true, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 1.2, output: 4.8, cacheRead: 0.6, cacheWrite: 1.2 }, enabled: true }
1780
+ ]
1781
+ },
1782
+ {
1783
+ id: "google",
1784
+ name: "Google Gemini",
1785
+ type: "builtin",
1786
+ api: "google-generative-ai",
1787
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta",
1788
+ hasAuth: true,
1789
+ authMethod: "env",
1790
+ models: [
1791
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: false, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.15 }, enabled: true },
1792
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 65536, cost: { input: 1.25, output: 10, cacheRead: 0.625, cacheWrite: 1.25 }, enabled: false }
1793
+ ]
1794
+ },
1795
+ {
1796
+ id: "openrouter",
1797
+ name: "OpenRouter",
1798
+ type: "builtin",
1799
+ api: "openai-completions",
1800
+ baseUrl: "https://openrouter.ai/api/v1",
1801
+ hasAuth: false,
1802
+ authMethod: "none",
1803
+ models: [
1804
+ { id: "openrouter/anthropic/claude-sonnet-4", name: "Claude 4 Sonnet (OpenRouter)", reasoning: true, input: ["text", "image"], contextWindow: 2e5, maxTokens: 16384, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: false },
1805
+ { id: "openrouter/deepseek/deepseek-r1", name: "DeepSeek R1 (OpenRouter)", reasoning: true, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 }, enabled: false }
1806
+ ]
1807
+ },
1808
+ {
1809
+ id: "mistral",
1810
+ name: "Mistral",
1811
+ type: "builtin",
1812
+ api: "mistral-conversations",
1813
+ baseUrl: "https://api.mistral.ai/v1",
1814
+ hasAuth: false,
1815
+ authMethod: "none",
1816
+ models: [
1817
+ { id: "mistral-large", name: "Mistral Large", reasoning: false, input: ["text"], contextWindow: 128e3, maxTokens: 8192, cost: { input: 2, output: 6, cacheRead: 1, cacheWrite: 2 }, enabled: false }
1818
+ ]
1819
+ },
1820
+ {
1821
+ id: "github-copilot",
1822
+ name: "GitHub Copilot",
1823
+ type: "builtin",
1824
+ api: "openai-completions",
1825
+ baseUrl: "https://api.githubcopilot.com",
1826
+ hasAuth: false,
1827
+ authMethod: "none",
1828
+ models: [
1829
+ { id: "copilot-gpt-4o", name: "Copilot GPT-4o", reasoning: false, input: ["text"], contextWindow: 128e3, maxTokens: 4096, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, enabled: false }
1830
+ ]
1831
+ },
1832
+ {
1833
+ id: "groq",
1834
+ name: "Groq",
1835
+ type: "builtin",
1836
+ api: "openai-completions",
1837
+ baseUrl: "https://api.groq.com/openai/v1",
1838
+ hasAuth: false,
1839
+ authMethod: "none",
1840
+ models: [
1841
+ { id: "llama-3.3-70b", name: "Llama 3.3 70B", reasoning: false, input: ["text"], contextWindow: 128e3, maxTokens: 4096, cost: { input: 0.59, output: 0.79, cacheRead: 0, cacheWrite: 0 }, enabled: false }
1842
+ ]
1843
+ }
1844
+ ];
1845
+ function getBuiltinProviders() {
1846
+ return BUILTIN_PROVIDERS;
1847
+ }
1848
+ const __dirname$2 = path.dirname(url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("main.cjs", document.baseURI).href));
1849
+ const DIST_DIR = path.join(__dirname$2, "../../dist");
1850
+ const MIME = {
1851
+ ".html": "text/html; charset=utf-8",
1852
+ ".js": "application/javascript; charset=utf-8",
1853
+ ".css": "text/css; charset=utf-8",
1854
+ ".json": "application/json; charset=utf-8",
1855
+ ".svg": "image/svg+xml",
1856
+ ".png": "image/png",
1857
+ ".ico": "image/x-icon",
1858
+ ".webmanifest": "application/manifest+json",
1859
+ ".woff": "font/woff",
1860
+ ".woff2": "font/woff2"
1861
+ };
1862
+ function sendJson(res, status, data) {
1863
+ res.statusCode = status;
1864
+ res.setHeader("Content-Type", "application/json");
1865
+ res.end(JSON.stringify(data));
1866
+ }
1867
+ function readBody(req) {
1868
+ return new Promise((resolve, reject) => {
1869
+ let body = "";
1870
+ req.on("data", (chunk) => body += chunk);
1871
+ req.on("end", () => resolve(body));
1872
+ req.on("error", reject);
1873
+ });
1874
+ }
1875
+ function localDateStr(dt) {
1876
+ return new Intl.DateTimeFormat("en-CA", {
1877
+ timeZone: "Asia/Shanghai",
1878
+ year: "numeric",
1879
+ month: "2-digit",
1880
+ day: "2-digit"
1881
+ }).format(dt);
1882
+ }
1883
+ function resolveDateRange(range, fromParam, toParam) {
1884
+ const now = /* @__PURE__ */ new Date();
1885
+ let fromDate;
1886
+ let toDate = localDateStr(now);
1887
+ if (range === "today") fromDate = toDate;
1888
+ else if (range === "7d") {
1889
+ const d = new Date(now);
1890
+ d.setDate(d.getDate() - 6);
1891
+ fromDate = localDateStr(d);
1892
+ } else if (range === "30d") {
1893
+ const d = new Date(now);
1894
+ d.setDate(d.getDate() - 29);
1895
+ fromDate = localDateStr(d);
1896
+ } else if (range === "custom" && fromParam) {
1897
+ fromDate = fromParam;
1898
+ if (toParam) toDate = toParam;
1899
+ } else fromDate = toDate;
1900
+ return { fromDate, toDate };
1901
+ }
1902
+ async function handleApi(method, pathOnly, parsedUrl, req, res) {
1903
+ if (method === "DELETE" && (pathOnly === "/api/pi/session" || pathOnly === "/api/pi/trash")) {
1904
+ const filePath = parsedUrl.searchParams.get("path");
1905
+ if (!filePath) return sendJson(res, 400, { success: false, error: "Missing path" });
1906
+ const decoded = decodeURIComponent(filePath);
1907
+ const ok = pathOnly === "/api/pi/session" ? trashSessionFile(decoded) : permanentlyDeleteTrash(decoded);
1908
+ return sendJson(res, 200, { success: ok });
1909
+ }
1910
+ if (method === "GET" && pathOnly.endsWith("usage-range")) {
1911
+ if (parsedUrl.searchParams.get("refresh") === "1") {
1912
+ clearUsageCache();
1913
+ }
1914
+ const range = parsedUrl.searchParams.get("range") || "today";
1915
+ const fromParam = parsedUrl.searchParams.get("from") || "";
1916
+ const toParam = parsedUrl.searchParams.get("to") || "";
1917
+ const { fromDate, toDate } = resolveDateRange(range, fromParam, toParam);
1918
+ let records = null;
1919
+ if (pathOnly === "/api/pi/usage-range") {
1920
+ records = readAllUsage();
1921
+ } else if (pathOnly === "/api/pi/cindy-usage-range") {
1922
+ records = readCindyUsage();
1923
+ } else if (pathOnly === "/api/pi/claude-usage-range") {
1924
+ records = readClaudeUsage();
1925
+ } else if (pathOnly === "/api/pi/codex-usage-range") {
1926
+ records = readCodexUsage();
1927
+ } else if (pathOnly === "/api/pi/all-usage-range") {
1928
+ records = readAllCombinedUsage();
1929
+ } else {
1930
+ const providerMatch = pathOnly.match(/^\/api\/pi\/(atomcode|copilot|opencode|gemini|grok)-usage-range$/);
1931
+ if (providerMatch) {
1932
+ records = filterByProvider(readAllCombinedUsage(), providerMatch[1]);
1933
+ }
1934
+ }
1935
+ if (!records) return sendJson(res, 404, { error: "Not found" });
1936
+ const usage = getUsageByRange(records, fromDate, toDate);
1937
+ return sendJson(res, 200, usage);
1938
+ }
1939
+ const key = `${method} ${pathOnly}`;
1940
+ const body = await readBody(req).catch(() => "");
1941
+ try {
1942
+ switch (key) {
1943
+ case "GET /api/pi/settings":
1944
+ return sendJson(res, 200, readSettings() ?? {});
1945
+ case "POST /api/pi/settings":
1946
+ return sendJson(res, 200, { success: writeSettings(JSON.parse(body)) });
1947
+ case "GET /api/pi/auth":
1948
+ return sendJson(res, 200, readAuth() ?? {});
1949
+ case "POST /api/pi/auth":
1950
+ return sendJson(res, 200, { success: writeAuth(JSON.parse(body)) });
1951
+ case "GET /api/pi/models":
1952
+ return sendJson(res, 200, readModels() ?? { providers: {} });
1953
+ case "POST /api/pi/models":
1954
+ return sendJson(res, 200, { success: writeModels(JSON.parse(body)) });
1955
+ case "GET /api/pi/builtin-providers": {
1956
+ const catalog = readBuiltinCatalog();
1957
+ return sendJson(res, 200, catalog ?? getBuiltinProviders());
1958
+ }
1959
+ case "GET /api/pi/usage": {
1960
+ const records = readAllUsage();
1961
+ return sendJson(res, 200, {
1962
+ records,
1963
+ dailyAggregates: getDailyAggregates(records),
1964
+ providerSummaries: getProviderSummaries(records),
1965
+ modelSummaries: getModelSummaries(records),
1966
+ totals: getTotals(records)
1967
+ });
1968
+ }
1969
+ case "GET /api/pi/sessions":
1970
+ return sendJson(res, 200, listSessions());
1971
+ case "GET /api/pi/memory":
1972
+ return sendJson(res, 200, readMemoryFiles());
1973
+ case "GET /api/pi/subagents":
1974
+ return sendJson(res, 200, readSubagents());
1975
+ case "POST /api/pi/memory/delete-entry": {
1976
+ const { filename, text } = JSON.parse(body);
1977
+ return sendJson(res, 200, { success: deleteMemoryEntry(filename, text) });
1978
+ }
1979
+ case "GET /api/pi/trash":
1980
+ return sendJson(res, 200, listTrash());
1981
+ case "GET /api/pi/copilot-config":
1982
+ return sendJson(res, 200, readCopilotConfig() ?? {});
1983
+ case "POST /api/pi/copilot-config": {
1984
+ const cfg = JSON.parse(body);
1985
+ const ok = writeCopilotConfig(cfg);
1986
+ clearCopilotCaches();
1987
+ return sendJson(res, 200, { success: ok });
1988
+ }
1989
+ case "POST /api/pi/session/trash": {
1990
+ const { path: p } = JSON.parse(body);
1991
+ return sendJson(res, 200, { success: trashSessionFile(p) });
1992
+ }
1993
+ case "POST /api/pi/session/restore": {
1994
+ const { trashPath } = JSON.parse(body);
1995
+ return sendJson(res, 200, { success: restoreFromTrash(trashPath) });
1996
+ }
1997
+ case "POST /api/pi/session/auto-expire": {
1998
+ try {
1999
+ const result = autoExpireSessions();
2000
+ return sendJson(res, 200, { success: true, ...result });
2001
+ } catch {
2002
+ return sendJson(res, 500, { success: false, error: "Auto-expire failed" });
2003
+ }
2004
+ }
2005
+ case "GET /api/pi/session-preview": {
2006
+ const p = parsedUrl.searchParams.get("path") || "";
2007
+ const preview = readSessionPreview(decodeURIComponent(p));
2008
+ if (!preview) return sendJson(res, 404, { error: "Session not found" });
2009
+ return sendJson(res, 200, preview);
2010
+ }
2011
+ case "GET /api/pi/check-updates": {
2012
+ try {
2013
+ const result = await checkUpdates();
2014
+ return sendJson(res, 200, result);
2015
+ } catch {
2016
+ return sendJson(res, 500, { error: "Update check failed" });
2017
+ }
2018
+ }
2019
+ case "POST /api/pi/apply-updates": {
2020
+ const { names } = JSON.parse(body);
2021
+ return sendJson(res, 200, { results: applyExtensionUpdates(Array.isArray(names) ? names : []) });
2022
+ }
2023
+ case "POST /api/pi/provider-models": {
2024
+ const { baseUrl, apiKey, providerId } = JSON.parse(body);
2025
+ if (!baseUrl) throw new Error("missing baseUrl");
2026
+ const result = await fetchProviderModels(baseUrl, apiKey, providerId);
2027
+ return sendJson(res, 200, result);
2028
+ }
2029
+ case "POST /api/pi/model-test": {
2030
+ const { baseUrl, modelId, apiKey, apiType } = JSON.parse(body);
2031
+ if (!baseUrl || !modelId) throw new Error("missing baseUrl or modelId");
2032
+ const result = await testModel(baseUrl, modelId, apiKey, apiType ?? "openai-completions");
2033
+ return sendJson(res, 200, result);
2034
+ }
2035
+ case "POST /api/pi/provider-test": {
2036
+ const { baseUrl, apiKey } = JSON.parse(body);
2037
+ if (!baseUrl) throw new Error("missing baseUrl");
2038
+ const result = await testProviderConnection(baseUrl, apiKey);
2039
+ return sendJson(res, 200, result);
2040
+ }
2041
+ default:
2042
+ return sendJson(res, 404, { error: "Not found" });
2043
+ }
2044
+ } catch {
2045
+ return sendJson(res, 400, { success: false, error: "Invalid request body" });
2046
+ }
2047
+ }
2048
+ function serveStatic(pathOnly, res) {
2049
+ let filePath = path.join(DIST_DIR, pathOnly === "/" ? "index.html" : pathOnly);
2050
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
2051
+ filePath = path.join(DIST_DIR, "index.html");
2052
+ }
2053
+ const ext = path.extname(filePath).toLowerCase();
2054
+ res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
2055
+ fs.createReadStream(filePath).pipe(res);
2056
+ }
2057
+ function startApiServer() {
2058
+ return new Promise((resolve, reject) => {
2059
+ const server = http.createServer((req, res) => {
2060
+ const method = req.method ?? "GET";
2061
+ const url2 = req.url ?? "/";
2062
+ const parsedUrl = new URL(url2, "http://localhost");
2063
+ const pathOnly = parsedUrl.pathname;
2064
+ if (pathOnly.startsWith("/api/pi/")) {
2065
+ handleApi(method, pathOnly, parsedUrl, req, res).catch(() => {
2066
+ sendJson(res, 500, { error: "Internal error" });
2067
+ });
2068
+ } else {
2069
+ serveStatic(pathOnly, res);
2070
+ }
2071
+ });
2072
+ server.on("error", reject);
2073
+ server.listen(0, "127.0.0.1", () => {
2074
+ const addr = server.address();
2075
+ const port = typeof addr === "object" && addr ? addr.port : 0;
2076
+ resolve({ server, port, url: `http://127.0.0.1:${port}` });
2077
+ });
2078
+ });
2079
+ }
2080
+ const { app, BrowserWindow, Menu, Tray, ipcMain, nativeImage, shell } = process.mainModule?.require("electron") || require("electron");
2081
+ const __dirname$1 = path.dirname(url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("main.cjs", document.baseURI).href));
2082
+ let mainWindow = null;
2083
+ let popupWindow = null;
2084
+ let tray = null;
2085
+ let popupHideTimer = null;
2086
+ let apiServerUrl = null;
2087
+ function pickPath(...candidates) {
2088
+ for (const p of candidates) {
2089
+ if (fs.existsSync(p)) return p;
2090
+ }
2091
+ return null;
2092
+ }
2093
+ function resolvePreload() {
2094
+ return path.join(__dirname$1, "../preload/preload.cjs");
2095
+ }
2096
+ function resolveIndexHtml() {
2097
+ return path.join(__dirname$1, "../../dist/index.html");
2098
+ }
2099
+ function resolvePopupHtml() {
2100
+ return path.join(__dirname$1, "../../dist/electron/popup.html");
2101
+ }
2102
+ function resolveAppIcon() {
2103
+ return pickPath(
2104
+ path.join(__dirname$1, "../../public/icon-512.png"),
2105
+ path.join(__dirname$1, "../../dist/icon-512.png")
2106
+ );
2107
+ }
2108
+ function createTrayIcon() {
2109
+ const iconPath = pickPath(
2110
+ path.join(__dirname$1, "../../build/trayIconTemplate.png"),
2111
+ path.join(__dirname$1, "../../dist/trayIconTemplate.png"),
2112
+ resolveAppIcon() ?? ""
2113
+ );
2114
+ const image = iconPath ? nativeImage.createFromPath(iconPath) : nativeImage.createEmpty();
2115
+ image.setTemplateImage(true);
2116
+ return image;
2117
+ }
2118
+ function windowStatePath() {
2119
+ return path.join(app.getPath("userData"), "window-state.json");
2120
+ }
2121
+ function loadWindowState() {
2122
+ const fallback = { width: 1280, height: 800 };
2123
+ try {
2124
+ const raw = fs.readFileSync(windowStatePath(), "utf-8");
2125
+ const state = JSON.parse(raw);
2126
+ const width = typeof state.width === "number" && state.width >= 800 ? Math.round(state.width) : fallback.width;
2127
+ const height = typeof state.height === "number" && state.height >= 600 ? Math.round(state.height) : fallback.height;
2128
+ return { width, height };
2129
+ } catch {
2130
+ return fallback;
2131
+ }
2132
+ }
2133
+ function saveWindowState(state) {
2134
+ try {
2135
+ const file = windowStatePath();
2136
+ fs.mkdirSync(path.dirname(file), { recursive: true });
2137
+ fs.writeFileSync(file, JSON.stringify(state, null, 2), "utf-8");
2138
+ } catch {
2139
+ }
2140
+ }
2141
+ function createMainWindow() {
2142
+ const state = loadWindowState();
2143
+ const win = new BrowserWindow({
2144
+ width: state.width,
2145
+ height: state.height,
2146
+ minWidth: 800,
2147
+ minHeight: 600,
2148
+ title: "pi-web-switch",
2149
+ icon: resolveAppIcon() ?? void 0,
2150
+ // macOS immersive title bar: hides the window chrome/title bar while
2151
+ // keeping the traffic-light buttons inset into the content. Non-macOS
2152
+ // platforms ignore this option and keep the native frame.
2153
+ titleBarStyle: "hiddenInset",
2154
+ webPreferences: {
2155
+ preload: resolvePreload(),
2156
+ contextIsolation: true,
2157
+ nodeIntegration: false
2158
+ }
2159
+ });
2160
+ mainWindow = win;
2161
+ let resizeTimer = null;
2162
+ win.on("resize", () => {
2163
+ if (resizeTimer) clearTimeout(resizeTimer);
2164
+ resizeTimer = setTimeout(() => {
2165
+ const [width, height] = win.getSize();
2166
+ saveWindowState({ width, height });
2167
+ }, 400);
2168
+ });
2169
+ if (process.env.VITE_DEV_SERVER_URL) {
2170
+ win.loadURL(process.env.VITE_DEV_SERVER_URL);
2171
+ } else if (apiServerUrl) {
2172
+ win.loadURL(apiServerUrl);
2173
+ } else {
2174
+ win.loadFile(resolveIndexHtml());
2175
+ }
2176
+ win.on("closed", () => {
2177
+ mainWindow = null;
2178
+ });
2179
+ return win;
2180
+ }
2181
+ function createPopupWindow() {
2182
+ const trayBounds = tray?.getBounds();
2183
+ const x = trayBounds ? Math.round(trayBounds.x + trayBounds.width / 2 - 175) : void 0;
2184
+ const y = trayBounds ? trayBounds.y + trayBounds.height + 4 : void 0;
2185
+ const win = new BrowserWindow({
2186
+ width: 350,
2187
+ height: 420,
2188
+ x,
2189
+ y,
2190
+ show: false,
2191
+ frame: false,
2192
+ resizable: false,
2193
+ maximizable: false,
2194
+ fullscreenable: false,
2195
+ skipTaskbar: true,
2196
+ movable: false,
2197
+ transparent: false,
2198
+ hasShadow: true,
2199
+ backgroundColor: "#ffffff",
2200
+ webPreferences: {
2201
+ preload: resolvePreload(),
2202
+ contextIsolation: true,
2203
+ nodeIntegration: false
2204
+ }
2205
+ });
2206
+ const popupDevUrl = process.env.VITE_DEV_SERVER_URL ? `${process.env.VITE_DEV_SERVER_URL}/electron/popup.html` : null;
2207
+ const popupFile = resolvePopupHtml();
2208
+ if (popupDevUrl) {
2209
+ win.loadURL(popupDevUrl);
2210
+ } else if (fs.existsSync(popupFile)) {
2211
+ win.loadFile(popupFile);
2212
+ } else {
2213
+ win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(
2214
+ '<html><body style="font-family:-apple-system;padding:16px;">Popup HTML not found.</body></html>'
2215
+ ));
2216
+ }
2217
+ win.on("blur", () => {
2218
+ if (popupHideTimer) clearTimeout(popupHideTimer);
2219
+ popupHideTimer = setTimeout(() => {
2220
+ if (popupWindow && popupWindow.isVisible()) {
2221
+ popupWindow.hide();
2222
+ }
2223
+ }, 150);
2224
+ });
2225
+ win.on("closed", () => {
2226
+ popupWindow = null;
2227
+ });
2228
+ popupWindow = win;
2229
+ return win;
2230
+ }
2231
+ function createTray() {
2232
+ const t = new Tray(createTrayIcon());
2233
+ tray = t;
2234
+ t.setToolTip("pi-web-switch — 点击查看使用量");
2235
+ t.on("click", () => {
2236
+ togglePopup();
2237
+ });
2238
+ t.on("right-click", () => {
2239
+ showTrayMenu();
2240
+ });
2241
+ t.on("mouse-down", (_e, bounds) => {
2242
+ });
2243
+ }
2244
+ function showTrayMenu() {
2245
+ if (!tray) return;
2246
+ const menu = Menu.buildFromTemplate([
2247
+ { label: "打开 Dashboard", click: showMainWindow },
2248
+ { label: "刷新使用量", click: () => popupWindow?.webContents.reload() },
2249
+ { type: "separator" },
2250
+ { label: "退出", accelerator: "Command+Q", click: () => app.quit() }
2251
+ ]);
2252
+ tray.popUpContextMenu(menu);
2253
+ }
2254
+ function togglePopup() {
2255
+ if (!popupWindow) {
2256
+ popupWindow = createPopupWindow();
2257
+ }
2258
+ const popup = popupWindow;
2259
+ if (!popup) return;
2260
+ if (popup.isVisible()) {
2261
+ popup.hide();
2262
+ } else {
2263
+ const t = tray;
2264
+ const trayBounds = t ? t.getBounds() : null;
2265
+ if (trayBounds) {
2266
+ const x = Math.round(trayBounds.x + trayBounds.width / 2 - 175);
2267
+ const y = trayBounds.y + trayBounds.height + 4;
2268
+ popup.setPosition(x, y);
2269
+ }
2270
+ popup.show();
2271
+ popup.focus();
2272
+ }
2273
+ }
2274
+ function showMainWindow() {
2275
+ if (!mainWindow) {
2276
+ mainWindow = createMainWindow();
2277
+ }
2278
+ const win = mainWindow;
2279
+ if (!win) return;
2280
+ if (win.isMinimized()) win.restore();
2281
+ win.show();
2282
+ win.focus();
2283
+ }
2284
+ function createMenu() {
2285
+ const template = [
2286
+ {
2287
+ label: "Application",
2288
+ submenu: [
2289
+ { label: "About Application", selector: "orderFrontStandardAboutPanel:" },
2290
+ { type: "separator" },
2291
+ { label: "Quit", accelerator: "Command+Q", click: () => app.quit() }
2292
+ ]
2293
+ },
2294
+ {
2295
+ label: "Edit",
2296
+ submenu: [
2297
+ { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
2298
+ { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
2299
+ { type: "separator" },
2300
+ { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
2301
+ { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
2302
+ { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
2303
+ { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
2304
+ ]
2305
+ }
2306
+ ];
2307
+ Menu.setApplicationMenu(Menu.buildFromTemplate(template));
2308
+ }
2309
+ const SUMMARY_CACHE_TTL_MS = 25e3;
2310
+ let summaryCache = null;
2311
+ function cnTodayStr() {
2312
+ return new Intl.DateTimeFormat("en-CA", {
2313
+ timeZone: "Asia/Shanghai",
2314
+ year: "numeric",
2315
+ month: "2-digit",
2316
+ day: "2-digit"
2317
+ }).format(/* @__PURE__ */ new Date());
2318
+ }
2319
+ function computeUsageSummary() {
2320
+ const records = readAllCombinedUsage();
2321
+ const today = cnTodayStr();
2322
+ const now = /* @__PURE__ */ new Date();
2323
+ const sevenDaysAgo = new Intl.DateTimeFormat("en-CA", {
2324
+ timeZone: "Asia/Shanghai",
2325
+ year: "numeric",
2326
+ month: "2-digit",
2327
+ day: "2-digit"
2328
+ }).format(new Date(now.getTime() - 6 * 24 * 60 * 60 * 1e3));
2329
+ const todayRecords = records.filter((r) => r.date === today);
2330
+ const sevenDayRecords = records.filter((r) => r.date >= sevenDaysAgo);
2331
+ const sum = (recs) => {
2332
+ let tokens = 0, input = 0, output = 0, cacheRead = 0, cacheWrite = 0;
2333
+ let cost = 0, requests = 0;
2334
+ for (const r of recs) {
2335
+ input += r.inputTokens;
2336
+ output += r.outputTokens;
2337
+ cacheRead += r.cacheReadTokens;
2338
+ cacheWrite += r.cacheWriteTokens;
2339
+ cost += r.cost;
2340
+ requests += r.requests;
2341
+ }
2342
+ tokens = input + output + cacheRead + cacheWrite;
2343
+ return { tokens, input, output, cacheRead, cacheWrite, cost, requests };
2344
+ };
2345
+ const todaySummary = sum(todayRecords);
2346
+ const sevenDaySummary = sum(sevenDayRecords);
2347
+ const dailyMap = /* @__PURE__ */ new Map();
2348
+ for (const r of sevenDayRecords) {
2349
+ const d = dailyMap.get(r.date) ?? { tokens: 0, cost: 0, requests: 0 };
2350
+ d.tokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
2351
+ d.cost += r.cost;
2352
+ d.requests += r.requests;
2353
+ dailyMap.set(r.date, d);
2354
+ }
2355
+ const daily = Array.from(dailyMap.entries()).map(([date, v]) => ({ date, ...v })).sort((a, b) => a.date.localeCompare(b.date));
2356
+ const providerMap = /* @__PURE__ */ new Map();
2357
+ for (const r of sevenDayRecords) {
2358
+ const p = providerMap.get(r.providerId) ?? { providerId: r.providerId, cost: 0, tokens: 0, requests: 0 };
2359
+ p.cost += r.cost;
2360
+ p.tokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
2361
+ p.requests += r.requests;
2362
+ providerMap.set(r.providerId, p);
2363
+ }
2364
+ const providers = Array.from(providerMap.values()).sort((a, b) => b.cost - a.cost).slice(0, 5);
2365
+ return {
2366
+ today: todaySummary,
2367
+ sevenDays: sevenDaySummary,
2368
+ daily,
2369
+ providers,
2370
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2371
+ };
2372
+ }
2373
+ function setupIPC() {
2374
+ ipcMain.handle("pi:settings:get", () => readSettings());
2375
+ ipcMain.handle("pi:settings:set", (_e, data) => writeSettings(data));
2376
+ ipcMain.handle("pi:auth:get", () => readAuth());
2377
+ ipcMain.handle("pi:auth:set", (_e, data) => writeAuth(data));
2378
+ ipcMain.handle("pi:models:get", () => readModels());
2379
+ ipcMain.handle("pi:models:set", (_e, data) => writeModels(data));
2380
+ ipcMain.handle("pi:usage:summary", (_e, opts) => {
2381
+ try {
2382
+ if (!opts?.force && summaryCache && Date.now() - summaryCache.at < SUMMARY_CACHE_TTL_MS) {
2383
+ return summaryCache.data;
2384
+ }
2385
+ const data = computeUsageSummary();
2386
+ summaryCache = { data, at: Date.now() };
2387
+ return data;
2388
+ } catch (err) {
2389
+ return { error: String(err) };
2390
+ }
2391
+ });
2392
+ ipcMain.handle("pi:open:dashboard", () => {
2393
+ showMainWindow();
2394
+ });
2395
+ ipcMain.handle("pi:open:external", (_e, url2) => {
2396
+ if (typeof url2 === "string" && /^https?:\/\//.test(url2)) {
2397
+ shell.openExternal(url2);
2398
+ }
2399
+ });
2400
+ }
2401
+ function warmUsageCache() {
2402
+ setTimeout(() => {
2403
+ try {
2404
+ if (!summaryCache) {
2405
+ summaryCache = { data: computeUsageSummary(), at: Date.now() };
2406
+ console.log("[pi-web-switch] usage summary warmed");
2407
+ }
2408
+ } catch (err) {
2409
+ console.error("[pi-web-switch] warm usage failed:", err);
2410
+ }
2411
+ }, 1500);
2412
+ }
2413
+ function setDockIcon() {
2414
+ if (process.platform !== "darwin" || !app.dock) return;
2415
+ const iconPath = resolveAppIcon();
2416
+ if (iconPath && fs.existsSync(iconPath)) {
2417
+ try {
2418
+ app.dock.setIcon(nativeImage.createFromPath(iconPath));
2419
+ } catch {
2420
+ }
2421
+ }
2422
+ }
2423
+ app.whenReady().then(async () => {
2424
+ try {
2425
+ setDockIcon();
2426
+ createMenu();
2427
+ setupIPC();
2428
+ if (!process.env.VITE_DEV_SERVER_URL) {
2429
+ const handle = await startApiServer();
2430
+ apiServerUrl = handle.url;
2431
+ console.log(`[pi-web-switch] api server: ${handle.url}`);
2432
+ }
2433
+ createTray();
2434
+ createPopupWindow();
2435
+ showMainWindow();
2436
+ warmUsageCache();
2437
+ try {
2438
+ startAutoExpiryTimer();
2439
+ } catch {
2440
+ }
2441
+ console.log("[pi-web-switch] tray + popup ready");
2442
+ } catch (err) {
2443
+ console.error("[pi-web-switch] startup failed:", err);
2444
+ }
2445
+ app.on("activate", () => {
2446
+ showMainWindow();
2447
+ });
2448
+ }).catch((err) => {
2449
+ console.error("[pi-web-switch] whenReady rejected:", err);
2450
+ });
2451
+ app.on("window-all-closed", () => {
2452
+ });
2453
+ if (process.platform === "darwin") ;