tokmon 0.23.4 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -5
- package/dist/{bootstrap-ink-BIOLTP7F.js → bootstrap-ink-HL2BYNW3.js} +37 -292
- package/dist/{chunk-RM3ZE44Z.js → chunk-3TJVFKXV.js} +1 -1
- package/dist/chunk-42H7R376.js +298 -0
- package/dist/chunk-45ZP755H.js +275 -0
- package/dist/{chunk-DJPUYMZM.js → chunk-5CWOJMAH.js} +2 -0
- package/dist/{chunk-2AFQKH5O.js → chunk-O27I2XFN.js} +278 -539
- package/dist/{chunk-2K4L4QQK.js → chunk-REO4O5VE.js} +68 -79
- package/dist/{chunk-Q2DSDWO2.js → chunk-SMPY52EV.js} +1 -30
- package/dist/{daemon-handle-6F2IB6CU.js → chunk-W2WGBXZG.js} +8 -9
- package/dist/cli-command-TLGIGF63.js +526 -0
- package/dist/cli.js +30 -5
- package/dist/{config-5CNJN3P7.js → config-QC5QSP3G.js} +1 -1
- package/dist/{daemon-PP3QAT6E.js → daemon-F4EGY77J.js} +43 -17
- package/dist/daemon-handle-NLCLYWFU.js +10 -0
- package/dist/server-X63FGHIZ.js +11 -0
- package/dist/web/assets/{Area-BALPhJJp.js → Area-DCES17aZ.js} +1 -1
- package/dist/web/assets/{analytics-uHVeOhLh.js → analytics-DDW6zcnj.js} +2 -2
- package/dist/web/assets/{breakdown-CR5dNNSG.js → breakdown-D5LPPtqh.js} +1 -1
- package/dist/web/assets/{chart-DRkqw0fm.js → chart-bHY7MQV0.js} +1 -1
- package/dist/web/assets/{explore-C-bHaX-I.js → explore-DdWB-Pu7.js} +1 -1
- package/dist/web/assets/index-C9jxb2o3.js +83 -0
- package/dist/web/assets/index-m0KfWnN2.css +1 -0
- package/dist/web/assets/{models-Qc2daHCo.js → models-o1wJnG6w.js} +2 -2
- package/dist/web/assets/{overview-D4VG2awg.js → overview-wq2uAEk4.js} +2 -2
- package/dist/web/assets/{panel-C1CsBs1H.js → panel-DBnK1oy0.js} +1 -1
- package/dist/web/assets/{primitives-S4UDgnZq.js → primitives-CBmteetD.js} +1 -1
- package/dist/web/assets/settings-sheet-CZ6GEPl3.js +1 -0
- package/dist/web/assets/{share-sheet-BpZBju-k.js → share-sheet-DUee3FlK.js} +1 -1
- package/dist/web/assets/{timeline-Cp-fyeuK.js → timeline-BxHwOJA_.js} +1 -1
- package/dist/web/assets/{use-dialog-trap-DaKbCG1Z.js → use-dialog-trap-30EqxYF-.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/server-UHQEOVQH.js +0 -10
- package/dist/web/assets/index-C1jQdagn.js +0 -83
- package/dist/web/assets/index-DSj-Bxov.css +0 -1
- package/dist/web/assets/settings-sheet-DHi4Dt2z.js +0 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PROVIDERS,
|
|
4
|
+
PROVIDER_ORDER,
|
|
5
|
+
identityFromIdToken,
|
|
6
|
+
readClaudeIdentity,
|
|
7
|
+
readJson
|
|
8
|
+
} from "./chunk-O27I2XFN.js";
|
|
9
|
+
import {
|
|
10
|
+
expandHome,
|
|
11
|
+
slugify
|
|
12
|
+
} from "./chunk-5CWOJMAH.js";
|
|
13
|
+
|
|
14
|
+
// src/accounts.ts
|
|
15
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { basename, join as join2, resolve } from "path";
|
|
18
|
+
|
|
19
|
+
// src/providers/codex/identity.ts
|
|
20
|
+
import { readFileSync } from "fs";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
function codexAuthPaths(homeDir) {
|
|
23
|
+
return [join(homeDir, ".codex", "auth.json"), join(homeDir, "auth.json")];
|
|
24
|
+
}
|
|
25
|
+
function readCodexIdentity(homeDir) {
|
|
26
|
+
for (const path of codexAuthPaths(homeDir)) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
29
|
+
const { email, displayName, payload } = identityFromIdToken(parsed?.tokens?.id_token);
|
|
30
|
+
if (!payload) continue;
|
|
31
|
+
return { email, displayName };
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/accounts.ts
|
|
39
|
+
function accountKey(providerId, homeDir) {
|
|
40
|
+
return `${providerId}:${homeDir ? resolve(expandHome(homeDir)) : homedir()}`;
|
|
41
|
+
}
|
|
42
|
+
function uniqueId(base, used) {
|
|
43
|
+
let id = slugify(base) || "account";
|
|
44
|
+
if (!used.has(id)) {
|
|
45
|
+
used.add(id);
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
for (let i = 2; i < 1e3; i++) {
|
|
49
|
+
const next = `${id}_${i}`;
|
|
50
|
+
if (!used.has(next)) {
|
|
51
|
+
used.add(next);
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
id = `${id}_${Date.now()}`;
|
|
56
|
+
used.add(id);
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
function hasClaudeState(homeDir) {
|
|
60
|
+
return existsSync(join2(homeDir, ".claude.json")) || existsSync(join2(homeDir, ".claude", ".credentials.json")) || existsSync(join2(homeDir, ".claude", "projects")) || existsSync(join2(homeDir, ".config", "claude", ".credentials.json")) || existsSync(join2(homeDir, ".config", "claude", "projects"));
|
|
61
|
+
}
|
|
62
|
+
function candidateAlternateHomes(prefix) {
|
|
63
|
+
const home = homedir();
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = readdirSync(home);
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const out = [];
|
|
71
|
+
const pattern = new RegExp(`^\\.${prefix}[_-]`);
|
|
72
|
+
for (const name of entries) {
|
|
73
|
+
if (!pattern.test(name)) continue;
|
|
74
|
+
const path = join2(home, name);
|
|
75
|
+
try {
|
|
76
|
+
if (!statSync(path).isDirectory()) continue;
|
|
77
|
+
out.push(path);
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out.sort();
|
|
82
|
+
}
|
|
83
|
+
function labelForClaudeHome(homeDir) {
|
|
84
|
+
const identity = readClaudeIdentity(homeDir);
|
|
85
|
+
if (identity.email) return `Claude ${identity.email}`;
|
|
86
|
+
if (identity.displayName) return `Claude ${identity.displayName}`;
|
|
87
|
+
const raw = basename(homeDir).replace(/^\.claude[_-]?/, "").replace(/[_-]+/g, " ").trim();
|
|
88
|
+
return raw ? `Claude ${raw}` : "Claude";
|
|
89
|
+
}
|
|
90
|
+
function hasCodexAuth(homeDir) {
|
|
91
|
+
for (const path of codexAuthPaths(homeDir)) {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
94
|
+
const accessToken = parsed?.tokens?.access_token;
|
|
95
|
+
if (typeof accessToken === "string" && accessToken.trim()) return true;
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
function labelForCodexHome(homeDir) {
|
|
102
|
+
const identity = readCodexIdentity(homeDir);
|
|
103
|
+
if (identity.email) return `Codex ${identity.email}`;
|
|
104
|
+
if (identity.displayName) return `Codex ${identity.displayName}`;
|
|
105
|
+
const raw = basename(homeDir).replace(/^\.codex[_-]?/, "").replace(/[_-]+/g, " ").trim();
|
|
106
|
+
return raw ? `Codex ${raw}` : "Codex";
|
|
107
|
+
}
|
|
108
|
+
function discoverClaudeAccounts(usedIds) {
|
|
109
|
+
const provider = PROVIDERS.claude;
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const homeDir of candidateAlternateHomes("claude")) {
|
|
112
|
+
if (!hasClaudeState(homeDir)) continue;
|
|
113
|
+
const suffix = basename(homeDir).replace(/^\.claude[_-]?/, "") || basename(homeDir);
|
|
114
|
+
out.push({
|
|
115
|
+
id: uniqueId(`claude_${suffix}`, usedIds),
|
|
116
|
+
providerId: "claude",
|
|
117
|
+
name: labelForClaudeHome(homeDir),
|
|
118
|
+
color: provider.color,
|
|
119
|
+
homeDir
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
function discoverCodexAccounts(usedIds) {
|
|
125
|
+
const provider = PROVIDERS.codex;
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const homeDir of candidateAlternateHomes("codex")) {
|
|
128
|
+
if (!hasCodexAuth(homeDir)) continue;
|
|
129
|
+
const suffix = basename(homeDir).replace(/^\.codex[_-]?/, "") || basename(homeDir);
|
|
130
|
+
out.push({
|
|
131
|
+
id: uniqueId(`codex_${suffix}`, usedIds),
|
|
132
|
+
providerId: "codex",
|
|
133
|
+
name: labelForCodexHome(homeDir),
|
|
134
|
+
color: provider.color,
|
|
135
|
+
homeDir
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
function discoverProviderAccounts(providerId, usedIds) {
|
|
141
|
+
if (providerId === "claude") return discoverClaudeAccounts(usedIds);
|
|
142
|
+
if (providerId === "codex") return discoverCodexAccounts(usedIds);
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
function buildAccounts(config, detected) {
|
|
146
|
+
const out = [];
|
|
147
|
+
const usedIds = new Set(config.accounts.map((a) => a.id));
|
|
148
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
149
|
+
const add = (account) => {
|
|
150
|
+
const key = accountKey(account.providerId, account.homeDir);
|
|
151
|
+
if (seenKeys.has(key)) return;
|
|
152
|
+
seenKeys.add(key);
|
|
153
|
+
out.push(account);
|
|
154
|
+
};
|
|
155
|
+
for (const pid of PROVIDER_ORDER) {
|
|
156
|
+
if (config.disabledProviders.includes(pid)) continue;
|
|
157
|
+
const provider = PROVIDERS[pid];
|
|
158
|
+
const configured = config.accounts.filter((a) => a.providerId === pid);
|
|
159
|
+
for (const a of configured) {
|
|
160
|
+
add({
|
|
161
|
+
id: a.id,
|
|
162
|
+
providerId: pid,
|
|
163
|
+
name: a.name,
|
|
164
|
+
color: a.color || provider.color,
|
|
165
|
+
homeDir: a.homeDir && a.homeDir !== "~" ? expandHome(a.homeDir) : void 0
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const discovered = discoverProviderAccounts(pid, usedIds);
|
|
169
|
+
if (detected.includes(pid)) {
|
|
170
|
+
add({ id: pid, providerId: pid, name: provider.name, color: provider.color, homeDir: void 0 });
|
|
171
|
+
}
|
|
172
|
+
for (const account of discovered) {
|
|
173
|
+
add(account);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
function accountsByProvider(accounts) {
|
|
179
|
+
const groups = [];
|
|
180
|
+
for (const pid of PROVIDER_ORDER) {
|
|
181
|
+
const list = accounts.filter((a) => a.providerId === pid);
|
|
182
|
+
if (list.length > 0) groups.push({ provider: pid, accounts: list });
|
|
183
|
+
}
|
|
184
|
+
return groups;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/shared/colors.ts
|
|
188
|
+
var FALLBACK_HEX = "#8d9090";
|
|
189
|
+
var NAMED_HEX = {
|
|
190
|
+
green: "#6caa71",
|
|
191
|
+
greenBright: "#79be7e",
|
|
192
|
+
cyan: "#7ccbcd",
|
|
193
|
+
cyanBright: "#84dde0",
|
|
194
|
+
blue: "#6d96b4",
|
|
195
|
+
blueBright: "#67b5ed",
|
|
196
|
+
magenta: "#bd7bcd",
|
|
197
|
+
magentaBright: "#d389e5",
|
|
198
|
+
yellow: "#c4ac62",
|
|
199
|
+
yellowBright: "#d9c074",
|
|
200
|
+
red: "#b45648",
|
|
201
|
+
redBright: "#cf6a5a",
|
|
202
|
+
white: "#dee5eb",
|
|
203
|
+
whiteBright: "#f3f5f5",
|
|
204
|
+
gray: FALLBACK_HEX,
|
|
205
|
+
grey: FALLBACK_HEX
|
|
206
|
+
};
|
|
207
|
+
var PROVIDER_HEX = {
|
|
208
|
+
claude: NAMED_HEX.green,
|
|
209
|
+
codex: NAMED_HEX.cyan,
|
|
210
|
+
cursor: NAMED_HEX.magenta,
|
|
211
|
+
copilot: NAMED_HEX.white,
|
|
212
|
+
pi: NAMED_HEX.blue,
|
|
213
|
+
opencode: NAMED_HEX.yellow,
|
|
214
|
+
antigravity: NAMED_HEX.red,
|
|
215
|
+
gemini: NAMED_HEX.greenBright,
|
|
216
|
+
grok: NAMED_HEX.yellowBright
|
|
217
|
+
};
|
|
218
|
+
function namedHex(name) {
|
|
219
|
+
if (!name) return FALLBACK_HEX;
|
|
220
|
+
if (name.startsWith("#")) return name;
|
|
221
|
+
return NAMED_HEX[name] ?? FALLBACK_HEX;
|
|
222
|
+
}
|
|
223
|
+
function colorHex(accountColor, providerColorName) {
|
|
224
|
+
if (accountColor) {
|
|
225
|
+
if (accountColor.startsWith("#")) return accountColor;
|
|
226
|
+
const named = NAMED_HEX[accountColor];
|
|
227
|
+
if (named) return named;
|
|
228
|
+
}
|
|
229
|
+
return namedHex(providerColorName);
|
|
230
|
+
}
|
|
231
|
+
var MODEL_PALETTE = [
|
|
232
|
+
"#00d7ff",
|
|
233
|
+
"#00d787",
|
|
234
|
+
"#e6b450",
|
|
235
|
+
"#d75f87",
|
|
236
|
+
"#5f87ff",
|
|
237
|
+
"#af87ff",
|
|
238
|
+
"#5fd7a7",
|
|
239
|
+
"#ff8787",
|
|
240
|
+
"#d7af5f",
|
|
241
|
+
"#87d7ff",
|
|
242
|
+
"#d787d7",
|
|
243
|
+
"#9ee493",
|
|
244
|
+
"#ffb454",
|
|
245
|
+
"#7aa2f7",
|
|
246
|
+
"#bb9af7"
|
|
247
|
+
];
|
|
248
|
+
var modelColorCache = /* @__PURE__ */ new Map();
|
|
249
|
+
function modelColor(name) {
|
|
250
|
+
const cached = modelColorCache.get(name);
|
|
251
|
+
if (cached) return cached;
|
|
252
|
+
let hash = 0;
|
|
253
|
+
for (let i = 0; i < name.length; i++) hash = hash * 31 + name.charCodeAt(i) >>> 0;
|
|
254
|
+
const color = MODEL_PALETTE[hash % MODEL_PALETTE.length];
|
|
255
|
+
modelColorCache.set(name, color);
|
|
256
|
+
return color;
|
|
257
|
+
}
|
|
258
|
+
var TOKEN_BUCKET = {
|
|
259
|
+
input: NAMED_HEX.blue,
|
|
260
|
+
output: NAMED_HEX.green,
|
|
261
|
+
cacheCreate: NAMED_HEX.yellow,
|
|
262
|
+
cacheRead: NAMED_HEX.cyan
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// src/peak.ts
|
|
266
|
+
async function fetchPeak() {
|
|
267
|
+
try {
|
|
268
|
+
const res = await fetch("https://promoclock.co/api/status", {
|
|
269
|
+
headers: { "Accept": "application/json", "User-Agent": "tokmon" },
|
|
270
|
+
signal: AbortSignal.timeout(3e3)
|
|
271
|
+
});
|
|
272
|
+
if (!res.ok) return null;
|
|
273
|
+
const data = await readJson(res);
|
|
274
|
+
if (!data) return null;
|
|
275
|
+
let state;
|
|
276
|
+
if (data.isPeak === true || data.status === "peak") state = "peak";
|
|
277
|
+
else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
|
|
278
|
+
else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
|
|
279
|
+
else return null;
|
|
280
|
+
const minutesUntilChange = typeof data.minutesUntilChange === "number" && Number.isFinite(data.minutesUntilChange) ? data.minutesUntilChange : null;
|
|
281
|
+
return {
|
|
282
|
+
state,
|
|
283
|
+
label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
|
|
284
|
+
minutesUntilChange
|
|
285
|
+
};
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export {
|
|
292
|
+
buildAccounts,
|
|
293
|
+
accountsByProvider,
|
|
294
|
+
namedHex,
|
|
295
|
+
colorHex,
|
|
296
|
+
modelColor,
|
|
297
|
+
fetchPeak
|
|
298
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
TOKMON_WS_METHODS,
|
|
4
|
+
TOKMON_WS_PATH,
|
|
5
|
+
TokmonRpcGroup
|
|
6
|
+
} from "./chunk-O27I2XFN.js";
|
|
7
|
+
|
|
8
|
+
// src/client/daemon-rpc-client.ts
|
|
9
|
+
import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Schedule, Stream } from "effect";
|
|
10
|
+
import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
|
|
11
|
+
import * as Socket from "effect/unstable/socket/Socket";
|
|
12
|
+
var TokmonRpcClient = class extends Context.Service()(
|
|
13
|
+
"tokmon/client/DaemonRpcClient/TokmonRpcClient"
|
|
14
|
+
) {
|
|
15
|
+
};
|
|
16
|
+
var dynamicImport = new Function("specifier", "return import(specifier)");
|
|
17
|
+
function toWsUrl(baseUrl) {
|
|
18
|
+
const base = typeof window === "undefined" ? void 0 : window.location.origin;
|
|
19
|
+
const url = new URL(baseUrl, base);
|
|
20
|
+
if (url.protocol === "http:") url.protocol = "ws:";
|
|
21
|
+
else if (url.protocol === "https:") url.protocol = "wss:";
|
|
22
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
23
|
+
throw new Error(`unsupported daemon RPC protocol: ${url.protocol}`);
|
|
24
|
+
}
|
|
25
|
+
url.hash = "";
|
|
26
|
+
url.searchParams.delete("tokmonToken");
|
|
27
|
+
url.searchParams.delete("wsToken");
|
|
28
|
+
url.pathname = TOKMON_WS_PATH;
|
|
29
|
+
return url.toString();
|
|
30
|
+
}
|
|
31
|
+
function shouldUseNodeTransport(transport) {
|
|
32
|
+
if (transport === "node") return true;
|
|
33
|
+
if (transport === "browser") return false;
|
|
34
|
+
return typeof window === "undefined";
|
|
35
|
+
}
|
|
36
|
+
async function socketLayerFor(url, transport) {
|
|
37
|
+
if (shouldUseNodeTransport(transport)) {
|
|
38
|
+
const NodeSocket = await dynamicImport("@effect/platform-node/NodeSocket");
|
|
39
|
+
return NodeSocket.layerWebSocket(url);
|
|
40
|
+
}
|
|
41
|
+
return Socket.layerWebSocket(url).pipe(
|
|
42
|
+
Layer.provide(Socket.layerWebSocketConstructorGlobal)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function retryPolicy(options) {
|
|
46
|
+
const baseDelay = options.reconnectBaseDelayMs ?? 250;
|
|
47
|
+
const policy = Schedule.exponential(Duration.millis(baseDelay), 1.5).pipe(
|
|
48
|
+
Schedule.either(Schedule.spaced(Duration.millis(2500)))
|
|
49
|
+
);
|
|
50
|
+
return typeof options.reconnectAttempts === "number" ? policy.pipe(Schedule.both(Schedule.recurs(options.reconnectAttempts))) : policy;
|
|
51
|
+
}
|
|
52
|
+
function createDaemonRpcClient(baseUrl, options = {}) {
|
|
53
|
+
const url = toWsUrl(baseUrl);
|
|
54
|
+
const fibers = /* @__PURE__ */ new Set();
|
|
55
|
+
let session = null;
|
|
56
|
+
let sessionPromise = null;
|
|
57
|
+
let closed = false;
|
|
58
|
+
const setConn = (state, error) => {
|
|
59
|
+
options.onConn?.(state, error);
|
|
60
|
+
};
|
|
61
|
+
const resetSession = (active) => {
|
|
62
|
+
if (active !== void 0 && active !== null && active !== session) {
|
|
63
|
+
void active.runtime.dispose().catch(() => {
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const dead = active ?? session;
|
|
68
|
+
session = null;
|
|
69
|
+
sessionPromise = null;
|
|
70
|
+
if (dead) void dead.runtime.dispose().catch(() => {
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const makeProtocolLayer = async () => {
|
|
74
|
+
const socketLayer = await socketLayerFor(url, options.transport);
|
|
75
|
+
const connectionHooksLayer = Layer.succeed(
|
|
76
|
+
RpcClient.ConnectionHooks,
|
|
77
|
+
RpcClient.ConnectionHooks.of({
|
|
78
|
+
onConnect: Effect.sync(() => {
|
|
79
|
+
setConn("live");
|
|
80
|
+
}),
|
|
81
|
+
onDisconnect: Effect.sync(() => {
|
|
82
|
+
if (!closed) {
|
|
83
|
+
setConn("reconnecting");
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
return Layer.effect(
|
|
89
|
+
RpcClient.Protocol,
|
|
90
|
+
RpcClient.makeProtocolSocket({
|
|
91
|
+
retryPolicy: retryPolicy(options),
|
|
92
|
+
retryTransientErrors: true
|
|
93
|
+
})
|
|
94
|
+
).pipe(
|
|
95
|
+
Layer.provide(Layer.mergeAll(socketLayer, RpcSerialization.layerJson, connectionHooksLayer))
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
const ensureSession = async () => {
|
|
99
|
+
if (closed) throw new Error("daemon RPC client is closed");
|
|
100
|
+
if (session) return session;
|
|
101
|
+
if (sessionPromise) return sessionPromise;
|
|
102
|
+
setConn("connecting");
|
|
103
|
+
sessionPromise = (async () => {
|
|
104
|
+
let runtime;
|
|
105
|
+
try {
|
|
106
|
+
const protocolLayer = await makeProtocolLayer();
|
|
107
|
+
const clientLayer = Layer.effect(
|
|
108
|
+
TokmonRpcClient,
|
|
109
|
+
RpcClient.make(TokmonRpcGroup)
|
|
110
|
+
).pipe(
|
|
111
|
+
Layer.provide(protocolLayer)
|
|
112
|
+
);
|
|
113
|
+
runtime = ManagedRuntime.make(clientLayer);
|
|
114
|
+
await runtime.runPromise(TokmonRpcClient.asEffect());
|
|
115
|
+
if (closed) {
|
|
116
|
+
await runtime.dispose();
|
|
117
|
+
throw new Error("daemon RPC client is closed");
|
|
118
|
+
}
|
|
119
|
+
session = { runtime };
|
|
120
|
+
return session;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
sessionPromise = null;
|
|
123
|
+
await runtime?.dispose().catch(() => {
|
|
124
|
+
});
|
|
125
|
+
if (!closed) setConn("error", error);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
return sessionPromise;
|
|
130
|
+
};
|
|
131
|
+
const run = async (effectFor) => {
|
|
132
|
+
const active = await ensureSession();
|
|
133
|
+
try {
|
|
134
|
+
return await active.runtime.runPromise(
|
|
135
|
+
TokmonRpcClient.use((client) => effectFor(client))
|
|
136
|
+
);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (!closed) {
|
|
139
|
+
resetSession(active);
|
|
140
|
+
setConn("error", error);
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const subscribe = (streamFor, onValue, staleAfterFor) => {
|
|
146
|
+
if (closed) return () => {
|
|
147
|
+
};
|
|
148
|
+
let fiber = null;
|
|
149
|
+
let unsubscribed = false;
|
|
150
|
+
let retryTimer = null;
|
|
151
|
+
let watchdogTimer = null;
|
|
152
|
+
let lastValueAt = 0;
|
|
153
|
+
let staleAfterMs = Number.POSITIVE_INFINITY;
|
|
154
|
+
let watchdogRestart = false;
|
|
155
|
+
const stopFiber = () => {
|
|
156
|
+
if (!fiber) return;
|
|
157
|
+
const current = fiber;
|
|
158
|
+
fiber = null;
|
|
159
|
+
fibers.delete(current);
|
|
160
|
+
void (session?.runtime.runPromise(Fiber.interrupt(current)) ?? Effect.runPromise(Fiber.interrupt(current))).catch(() => {
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
const scheduleRetry = () => {
|
|
164
|
+
if (closed || unsubscribed || retryTimer) return;
|
|
165
|
+
retryTimer = setTimeout(() => {
|
|
166
|
+
retryTimer = null;
|
|
167
|
+
start();
|
|
168
|
+
}, options.reconnectBaseDelayMs ?? 250);
|
|
169
|
+
retryTimer.unref?.();
|
|
170
|
+
};
|
|
171
|
+
if (staleAfterFor) {
|
|
172
|
+
const checkEveryMs = Math.min(5e3, Math.max(10, (options.snapshotStaleFloorMs ?? 9e4) / 2));
|
|
173
|
+
watchdogTimer = setInterval(() => {
|
|
174
|
+
if (!fiber || lastValueAt === 0 || Date.now() - lastValueAt <= staleAfterMs) return;
|
|
175
|
+
watchdogRestart = true;
|
|
176
|
+
lastValueAt = Date.now();
|
|
177
|
+
setConn("reconnecting");
|
|
178
|
+
stopFiber();
|
|
179
|
+
}, checkEveryMs);
|
|
180
|
+
watchdogTimer.unref?.();
|
|
181
|
+
}
|
|
182
|
+
const start = () => {
|
|
183
|
+
void (async () => {
|
|
184
|
+
try {
|
|
185
|
+
const active = await ensureSession();
|
|
186
|
+
if (closed || unsubscribed) return;
|
|
187
|
+
lastValueAt = Date.now();
|
|
188
|
+
staleAfterMs = options.snapshotStaleFloorMs ?? 9e4;
|
|
189
|
+
const currentFiber = active.runtime.runFork(
|
|
190
|
+
TokmonRpcClient.use(
|
|
191
|
+
(client) => streamFor(client).pipe(
|
|
192
|
+
Stream.runForEach(
|
|
193
|
+
(value) => Effect.sync(() => {
|
|
194
|
+
lastValueAt = Date.now();
|
|
195
|
+
if (staleAfterFor) staleAfterMs = staleAfterFor(value);
|
|
196
|
+
try {
|
|
197
|
+
onValue(value);
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
fiber = currentFiber;
|
|
206
|
+
fibers.add(currentFiber);
|
|
207
|
+
currentFiber.addObserver((exit) => {
|
|
208
|
+
fibers.delete(currentFiber);
|
|
209
|
+
if (fiber === currentFiber) fiber = null;
|
|
210
|
+
if (closed || unsubscribed) return;
|
|
211
|
+
const stale = watchdogRestart;
|
|
212
|
+
watchdogRestart = false;
|
|
213
|
+
resetSession(active);
|
|
214
|
+
if (stale || Exit.isSuccess(exit)) setConn("reconnecting");
|
|
215
|
+
else setConn("error", Cause.squash(exit.cause));
|
|
216
|
+
scheduleRetry();
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (!closed && !unsubscribed) {
|
|
220
|
+
resetSession();
|
|
221
|
+
setConn("error", error);
|
|
222
|
+
scheduleRetry();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
})();
|
|
226
|
+
};
|
|
227
|
+
start();
|
|
228
|
+
return () => {
|
|
229
|
+
unsubscribed = true;
|
|
230
|
+
if (retryTimer) {
|
|
231
|
+
clearTimeout(retryTimer);
|
|
232
|
+
retryTimer = null;
|
|
233
|
+
}
|
|
234
|
+
if (watchdogTimer) {
|
|
235
|
+
clearInterval(watchdogTimer);
|
|
236
|
+
watchdogTimer = null;
|
|
237
|
+
}
|
|
238
|
+
stopFiber();
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
getConfig: () => run((client) => client[TOKMON_WS_METHODS.getConfig]({})).then((state) => state),
|
|
243
|
+
setConfig: (config) => run((client) => client[TOKMON_WS_METHODS.setConfig](config)).then((state) => state),
|
|
244
|
+
refresh: (scope = "all") => run((client) => client[TOKMON_WS_METHODS.refresh]({ scope })),
|
|
245
|
+
browseFs: (path) => run((client) => client[TOKMON_WS_METHODS.browseFs]({ path })),
|
|
246
|
+
subscribeSnapshot: (onSnapshot) => subscribe(
|
|
247
|
+
(client) => client[TOKMON_WS_METHODS.snapshot]({}).pipe(Stream.map((value) => value)),
|
|
248
|
+
onSnapshot,
|
|
249
|
+
(snapshot) => Math.max(options.snapshotStaleFloorMs ?? 9e4, snapshot.intervalMs * 3)
|
|
250
|
+
),
|
|
251
|
+
subscribeConfig: (onConfig) => subscribe((client) => client[TOKMON_WS_METHODS.config]({}).pipe(Stream.map((value) => value)), onConfig),
|
|
252
|
+
async close() {
|
|
253
|
+
if (closed) return;
|
|
254
|
+
closed = true;
|
|
255
|
+
setConn("closed");
|
|
256
|
+
const active = [...fibers];
|
|
257
|
+
fibers.clear();
|
|
258
|
+
const activeSession = session ?? await sessionPromise?.catch(() => null) ?? null;
|
|
259
|
+
await Promise.all(active.map(
|
|
260
|
+
(fiber) => (activeSession?.runtime.runPromise(Fiber.interrupt(fiber)) ?? Effect.runPromise(Fiber.interrupt(fiber))).catch(() => {
|
|
261
|
+
})
|
|
262
|
+
));
|
|
263
|
+
session = null;
|
|
264
|
+
sessionPromise = null;
|
|
265
|
+
if (activeSession) {
|
|
266
|
+
await activeSession.runtime.dispose().catch(() => {
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export {
|
|
274
|
+
createDaemonRpcClient
|
|
275
|
+
};
|
|
@@ -25,6 +25,7 @@ var DEFAULTS = {
|
|
|
25
25
|
dashboardLayout: "grid",
|
|
26
26
|
defaultFocus: "all",
|
|
27
27
|
ascii: "auto",
|
|
28
|
+
allowNetworkAccess: false,
|
|
28
29
|
knownProviders: []
|
|
29
30
|
};
|
|
30
31
|
var LEGACY_KNOWN = ["claude", "codex", "cursor"];
|
|
@@ -214,6 +215,7 @@ function repairConfig(input) {
|
|
|
214
215
|
dashboardLayout: parsed.dashboardLayout === "single" ? "single" : "grid",
|
|
215
216
|
defaultFocus: parsed.defaultFocus === "last" ? "last" : "all",
|
|
216
217
|
ascii: parsed.ascii === "on" ? "on" : parsed.ascii === "off" ? "off" : "auto",
|
|
218
|
+
allowNetworkAccess: parsed.allowNetworkAccess === true,
|
|
217
219
|
knownProviders
|
|
218
220
|
};
|
|
219
221
|
for (const key of Object.keys(DEFAULTS)) {
|