@lawaty/lacode 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +174 -1
- package/README.md +20 -13
- package/dist/commands.js +14 -3
- package/dist/index.js +906 -383
- package/dist/lacode.js +211 -48
- package/dist/pool.js +41 -6
- package/dist/tunnel.js +185 -13
- package/docs/CONFIGURATION.md +21 -7
- package/docs/OMNIROUTE.md +1 -1
- package/docs/USER-STORIES.md +85 -6
- package/package.json +1 -1
- package/presets/README.md +4 -3
- package/presets/free-tier.json +3 -3
package/dist/lacode.js
CHANGED
|
@@ -2,12 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
// bin/lacode
|
|
4
4
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5
|
-
import { existsSync as
|
|
5
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "node:fs";
|
|
6
6
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
7
7
|
|
|
8
8
|
// src/tunnel/index.ts
|
|
9
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
10
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "node:fs";
|
|
11
|
+
import { homedir as homedir3 } from "node:os";
|
|
12
|
+
import { join as join4 } from "node:path";
|
|
13
|
+
|
|
14
|
+
// src/lib/notify.ts
|
|
15
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, statSync as statSync2, unlinkSync as unlinkSync2, writeSync } from "node:fs";
|
|
16
|
+
import { homedir as homedir2 } from "node:os";
|
|
17
|
+
import { join as join3 } from "node:path";
|
|
18
|
+
|
|
19
|
+
// src/lib/pool.ts
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
21
|
import { homedir } from "node:os";
|
|
12
22
|
import { join as join2 } from "node:path";
|
|
13
23
|
|
|
@@ -31,6 +41,36 @@ var findPackageRoot = (start) => {
|
|
|
31
41
|
};
|
|
32
42
|
var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
33
43
|
|
|
44
|
+
// src/lib/pool.ts
|
|
45
|
+
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
46
|
+
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
47
|
+
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
48
|
+
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
49
|
+
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
50
|
+
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
51
|
+
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
52
|
+
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
53
|
+
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
54
|
+
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
55
|
+
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
56
|
+
var QUOTA_COOLDOWN_BASE_MS = 15 * 60 * 1e3;
|
|
57
|
+
var QUOTA_COOLDOWN_MAX_MS = 6 * 60 * 60 * 1e3;
|
|
58
|
+
var STRIKE_DECAY_MS = 30 * 60 * 1e3;
|
|
59
|
+
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
60
|
+
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
61
|
+
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
62
|
+
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
63
|
+
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
64
|
+
var STATE_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
65
|
+
var CHILD_MEMORY_MS = 30 * 6e4;
|
|
66
|
+
|
|
67
|
+
// src/lib/notify.ts
|
|
68
|
+
var NOTIFY_SESSION_ENV = "LACODE_NOTIFY_SESSION";
|
|
69
|
+
var NOTIFY_CLOSE_ENV = "LACODE_NOTIFY_CLOSE";
|
|
70
|
+
var notificationStateDir = () => process.env.LACODE_NOTIFY_DIR ?? join3(homedir2(), ".local", "share", "opencode", "lacode", "notify");
|
|
71
|
+
var safeCorrelationKey = (correlationKey) => correlationKey.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64);
|
|
72
|
+
var notificationIdFile = (correlationKey) => join3(notificationStateDir(), `${safeCorrelationKey(correlationKey)}.ids`);
|
|
73
|
+
|
|
34
74
|
// src/tunnel/index.ts
|
|
35
75
|
var ICONS = [
|
|
36
76
|
"utilities-terminal",
|
|
@@ -53,7 +93,7 @@ function resolveTunnel(env = process.env) {
|
|
|
53
93
|
user: env.OC_LOCAL_USER ?? "",
|
|
54
94
|
host: env.OC_LOCAL_HOST ?? "",
|
|
55
95
|
port: Number.isFinite(port) && port > 0 ? port : 10022,
|
|
56
|
-
key: env.OC_NOTIFY_KEY ??
|
|
96
|
+
key: env.OC_NOTIFY_KEY ?? join4(homedir3(), ".ssh", "lacode_notify"),
|
|
57
97
|
connectTimeoutMs: Number(env.OC_CONNECT_TIMEOUT_MS) || 5e3
|
|
58
98
|
};
|
|
59
99
|
}
|
|
@@ -81,16 +121,32 @@ var safeRequestId = (raw) => {
|
|
|
81
121
|
if (!clean) return void 0;
|
|
82
122
|
return clean.length > 128 ? clean.slice(0, 128) : clean;
|
|
83
123
|
};
|
|
124
|
+
var CLOSE_REQUEST_PREFIX = "close:";
|
|
125
|
+
var wireCorrelationKey = (raw) => safeText(raw ?? "", "", 200);
|
|
126
|
+
var presentCorrelationKey = (raw) => {
|
|
127
|
+
const clean = wireCorrelationKey(raw);
|
|
128
|
+
return clean ? clean : void 0;
|
|
129
|
+
};
|
|
130
|
+
var isCloseRequest = (correlationKey) => typeof correlationKey === "string" && correlationKey.startsWith(CLOSE_REQUEST_PREFIX);
|
|
131
|
+
var closeRequestTarget = (correlationKey) => {
|
|
132
|
+
if (!isCloseRequest(correlationKey)) return void 0;
|
|
133
|
+
const target = correlationKey?.slice(CLOSE_REQUEST_PREFIX.length).trim() ?? "";
|
|
134
|
+
return target ? target : void 0;
|
|
135
|
+
};
|
|
84
136
|
var FIELD_SEPARATOR = "\n";
|
|
85
137
|
function parsePayload(raw) {
|
|
86
|
-
const [title, message, urgency, icon, sound, requestID] = raw.split(FIELD_SEPARATOR);
|
|
138
|
+
const [title, message, urgency, icon, sound, requestID, correlationKey] = raw.split(FIELD_SEPARATOR);
|
|
139
|
+
const key = wireCorrelationKey(correlationKey);
|
|
87
140
|
return {
|
|
88
141
|
title: safeTitle(title),
|
|
89
142
|
message: safeMessage(message),
|
|
90
143
|
urgency: normalizeUrgency(urgency),
|
|
91
144
|
icon: normalizeIcon(icon),
|
|
92
145
|
sound: normalizeSound(sound),
|
|
93
|
-
requestID: safeRequestId(requestID)
|
|
146
|
+
requestID: safeRequestId(requestID),
|
|
147
|
+
// The seventh field, empty when there is none. A close request keeps its
|
|
148
|
+
// marker here, so the receiver can still tell it apart from a session.
|
|
149
|
+
...key ? { correlationKey: key } : {}
|
|
94
150
|
};
|
|
95
151
|
}
|
|
96
152
|
var encodePayload = (request) => [
|
|
@@ -101,7 +157,11 @@ var encodePayload = (request) => [
|
|
|
101
157
|
normalizeSound(request.sound),
|
|
102
158
|
// The sixth field, empty when there is no request id: an absent id and an
|
|
103
159
|
// empty one mean the same thing, and the field count never changes.
|
|
104
|
-
safeRequestId(request.requestID) ?? ""
|
|
160
|
+
safeRequestId(request.requestID) ?? "",
|
|
161
|
+
// The seventh field, the correlation key, empty when there is none. The
|
|
162
|
+
// `close:` marker of a close request is part of the key and travels with
|
|
163
|
+
// it, so the far end closes instead of raising.
|
|
164
|
+
wireCorrelationKey(request.correlationKey)
|
|
105
165
|
].join(FIELD_SEPARATOR);
|
|
106
166
|
var SSH_OPTS = [
|
|
107
167
|
"-o",
|
|
@@ -121,11 +181,16 @@ var CONTROL_OPTS = [
|
|
|
121
181
|
"-o",
|
|
122
182
|
"ControlPersist=300",
|
|
123
183
|
"-o",
|
|
124
|
-
`ControlPath=${
|
|
184
|
+
`ControlPath=${join4(homedir3(), ".ssh", "lacode-cm-%C")}`
|
|
125
185
|
];
|
|
126
|
-
var run = (command2, args, timeout) => {
|
|
186
|
+
var run = (command2, args, timeout, extraEnv) => {
|
|
127
187
|
try {
|
|
128
|
-
const result = spawnSync(command2, args, {
|
|
188
|
+
const result = spawnSync(command2, args, {
|
|
189
|
+
timeout,
|
|
190
|
+
encoding: "utf8",
|
|
191
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
192
|
+
...extraEnv ? { env: { ...process.env, ...extraEnv } } : {}
|
|
193
|
+
});
|
|
129
194
|
return {
|
|
130
195
|
ok: result.status === 0,
|
|
131
196
|
stdout: result.stdout ?? "",
|
|
@@ -185,20 +250,104 @@ var NO_CHANNEL = {
|
|
|
185
250
|
darwin: "neither osascript nor terminal-notifier is installed"
|
|
186
251
|
};
|
|
187
252
|
var NEVER_EXPIRE_FLAG = "--expire-time=0";
|
|
253
|
+
var APP_NAME_FLAG = "--app-name=LaCode";
|
|
188
254
|
var NOTIFICATION_ACTIONS = [
|
|
189
255
|
["once", "Allow once"],
|
|
190
256
|
["always", "Always allow"],
|
|
191
257
|
["reject", "Deny"]
|
|
192
258
|
];
|
|
193
259
|
var ACTION_WAIT_MS = 10 * 6e4;
|
|
260
|
+
var isNotificationId = (id) => /^\d+$/.test(id);
|
|
261
|
+
var recordNotificationId = (correlationKey, id) => {
|
|
262
|
+
if (!isNotificationId(id)) return false;
|
|
263
|
+
try {
|
|
264
|
+
mkdirSync3(notificationStateDir(), { recursive: true });
|
|
265
|
+
appendFileSync(notificationIdFile(correlationKey), `${id}
|
|
266
|
+
`, "utf8");
|
|
267
|
+
return true;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
var sessionBusEnv = () => {
|
|
273
|
+
if (process.env.DBUS_SESSION_BUS_ADDRESS) return {};
|
|
274
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
275
|
+
return uid === void 0 ? {} : { DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus` };
|
|
276
|
+
};
|
|
277
|
+
var CLOSE_TIMEOUT_MS = 5e3;
|
|
278
|
+
function closeNotification(id) {
|
|
279
|
+
if (!isNotificationId(id)) return false;
|
|
280
|
+
const env = sessionBusEnv();
|
|
281
|
+
const viaGdbus = run(
|
|
282
|
+
"gdbus",
|
|
283
|
+
[
|
|
284
|
+
"call",
|
|
285
|
+
"--session",
|
|
286
|
+
"--dest",
|
|
287
|
+
"org.freedesktop.Notifications",
|
|
288
|
+
"--object-path",
|
|
289
|
+
"/org/freedesktop/Notifications",
|
|
290
|
+
"--method",
|
|
291
|
+
"org.freedesktop.Notifications.CloseNotification",
|
|
292
|
+
id
|
|
293
|
+
],
|
|
294
|
+
CLOSE_TIMEOUT_MS,
|
|
295
|
+
env
|
|
296
|
+
);
|
|
297
|
+
if (viaGdbus.ok) return true;
|
|
298
|
+
const viaDbusSend = run(
|
|
299
|
+
"dbus-send",
|
|
300
|
+
[
|
|
301
|
+
"--session",
|
|
302
|
+
"--dest=org.freedesktop.Notifications",
|
|
303
|
+
"--type=method_call",
|
|
304
|
+
"/org/freedesktop/Notifications",
|
|
305
|
+
"org.freedesktop.Notifications.CloseNotification",
|
|
306
|
+
`uint32:${id}`
|
|
307
|
+
],
|
|
308
|
+
CLOSE_TIMEOUT_MS,
|
|
309
|
+
env
|
|
310
|
+
);
|
|
311
|
+
return viaDbusSend.ok;
|
|
312
|
+
}
|
|
313
|
+
function closeCorrelationKey(correlationKey) {
|
|
314
|
+
const file = notificationIdFile(correlationKey);
|
|
315
|
+
let content = "";
|
|
316
|
+
try {
|
|
317
|
+
content = readFileSync4(file, "utf8");
|
|
318
|
+
} catch {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
let closed = true;
|
|
322
|
+
for (const line of content.split("\n")) {
|
|
323
|
+
const id = line.trim();
|
|
324
|
+
if (!isNotificationId(id)) continue;
|
|
325
|
+
if (!closeNotification(id)) closed = false;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
unlinkSync3(file);
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
return closed;
|
|
332
|
+
}
|
|
333
|
+
var splitNotifyOutput = (stdout) => {
|
|
334
|
+
const newline = stdout.indexOf("\n");
|
|
335
|
+
if (newline === -1) return { id: stdout.trim(), action: "" };
|
|
336
|
+
return { id: stdout.slice(0, newline).trim(), action: stdout.slice(newline + 1).trim() };
|
|
337
|
+
};
|
|
194
338
|
function sendLocal(request, platform = process.platform) {
|
|
339
|
+
const closing = closeRequestTarget(request.correlationKey);
|
|
340
|
+
if (closing !== void 0) {
|
|
341
|
+
const closed = closeCorrelationKey(closing);
|
|
342
|
+
return { ok: closed, detail: closed ? void 0 : "a popup could not be closed", transport: "local" };
|
|
343
|
+
}
|
|
195
344
|
const sound = normalizeSound(request.sound);
|
|
196
345
|
const playSound = () => {
|
|
197
346
|
if (sound === "none") return;
|
|
198
347
|
const channel2 = soundChannel(platform);
|
|
199
348
|
if (channel2 === "none") return;
|
|
200
|
-
const path =
|
|
201
|
-
if (!
|
|
349
|
+
const path = join4(PACKAGE_ROOT, "sounds", `${sound}.wav`);
|
|
350
|
+
if (!existsSync2(path)) return;
|
|
202
351
|
const child = spawn(channel2, [path], { detached: true, stdio: "ignore" });
|
|
203
352
|
child.on("error", () => {
|
|
204
353
|
});
|
|
@@ -214,13 +363,17 @@ function sendLocal(request, platform = process.platform) {
|
|
|
214
363
|
const requestID = safeRequestId(request.requestID);
|
|
215
364
|
const actionable = channel === "notify-send" && requestID !== void 0;
|
|
216
365
|
const actionArgs = actionable ? NOTIFICATION_ACTIONS.map(([name, label]) => `--action=${name}=${label}`) : [];
|
|
366
|
+
const correlationKey = presentCorrelationKey(request.correlationKey);
|
|
367
|
+
const trackable = channel === "notify-send" && correlationKey !== void 0;
|
|
217
368
|
const args = channel === "osascript" ? ["-e", DARWIN_SCRIPT, DARWIN_ARGV_MARKER, title, body] : channel === "terminal-notifier" ? ["-title", title, "-message", body, "-appName", "LaCode", "-priority", normalizeUrgency(request.urgency)] : (
|
|
218
369
|
// Never auto-dismissed: the notification waits for the user to close it.
|
|
219
|
-
[title, body, `--urgency=${normalizeUrgency(request.urgency)}`, `--icon=${normalizeIcon(request.icon)}`, NEVER_EXPIRE_FLAG, ...actionArgs]
|
|
370
|
+
[title, body, APP_NAME_FLAG, `--urgency=${normalizeUrgency(request.urgency)}`, `--icon=${normalizeIcon(request.icon)}`, NEVER_EXPIRE_FLAG, ...trackable ? ["--print-id"] : [], ...actionArgs]
|
|
220
371
|
);
|
|
221
372
|
const result = run(CHANNEL_BINARY[channel], args, actionable ? ACTION_WAIT_MS : 5e3);
|
|
222
373
|
playSound();
|
|
223
|
-
const
|
|
374
|
+
const printed = trackable ? splitNotifyOutput(result.stdout) : { id: "", action: result.stdout.trim() };
|
|
375
|
+
const action = actionable ? printed.action : "";
|
|
376
|
+
if (correlationKey !== void 0 && printed.id) recordNotificationId(correlationKey, printed.id);
|
|
224
377
|
return {
|
|
225
378
|
ok: result.ok,
|
|
226
379
|
detail: result.ok ? void 0 : result.stderr || result.detail,
|
|
@@ -269,7 +422,17 @@ function sendRemote(request, tunnel) {
|
|
|
269
422
|
}
|
|
270
423
|
function notify(request, env = process.env, platform = process.platform) {
|
|
271
424
|
try {
|
|
272
|
-
const
|
|
425
|
+
const closeKey = (env[NOTIFY_CLOSE_ENV] ?? "").trim();
|
|
426
|
+
if (closeKey) {
|
|
427
|
+
if (!isRemote(env)) {
|
|
428
|
+
const closed = closeCorrelationKey(closeKey);
|
|
429
|
+
return { ok: closed, detail: closed ? void 0 : "a popup could not be closed", transport: "local" };
|
|
430
|
+
}
|
|
431
|
+
return sendRemote({ ...request, correlationKey: `${CLOSE_REQUEST_PREFIX}${closeKey}` }, resolveTunnel(env));
|
|
432
|
+
}
|
|
433
|
+
const sessionKey = presentCorrelationKey(env[NOTIFY_SESSION_ENV]);
|
|
434
|
+
const tracked = sessionKey ? { ...request, correlationKey: request.correlationKey ?? sessionKey } : request;
|
|
435
|
+
const result = isRemote(env) ? sendRemote(tracked, resolveTunnel(env)) : sendLocal(tracked, platform);
|
|
273
436
|
return result.action ? { ...result, action: result.action } : result;
|
|
274
437
|
} catch (error) {
|
|
275
438
|
return { ok: false, detail: String(error), transport: "none" };
|
|
@@ -303,18 +466,18 @@ function describe(env = process.env, platform = process.platform) {
|
|
|
303
466
|
|
|
304
467
|
// src/lib/selfupdate.ts
|
|
305
468
|
import { spawn as spawn2 } from "node:child_process";
|
|
306
|
-
import { closeSync, mkdirSync, openSync, readFileSync as
|
|
307
|
-
import { homedir as
|
|
308
|
-
import { dirname as dirname2, join as
|
|
469
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync, statSync as statSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync2, writeSync as writeSync2 } from "node:fs";
|
|
470
|
+
import { homedir as homedir4 } from "node:os";
|
|
471
|
+
import { dirname as dirname2, join as join5, resolve } from "node:path";
|
|
309
472
|
var REGISTRY_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
310
473
|
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
311
474
|
var FETCH_TIMEOUT_MS = 4e3;
|
|
312
475
|
var INSTALL_TIMEOUT_MS = 12e4;
|
|
313
476
|
var LOCK_STALE_MS = 10 * 60 * 1e3;
|
|
314
477
|
var CHILD_ENV = "LACODE_SELFUPDATE_CHILD";
|
|
315
|
-
var STATE_DIR =
|
|
316
|
-
var STATE_PATH =
|
|
317
|
-
var LOCK_PATH =
|
|
478
|
+
var STATE_DIR = join5(homedir4(), ".local", "share", "opencode", "lacode");
|
|
479
|
+
var STATE_PATH = join5(STATE_DIR, "selfupdate.json");
|
|
480
|
+
var LOCK_PATH = join5(STATE_DIR, "selfupdate.lock");
|
|
318
481
|
function compareSemver(a, b) {
|
|
319
482
|
const parse = (v) => v.split("-")[0].split("+")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
320
483
|
const left = parse(a);
|
|
@@ -328,13 +491,13 @@ function compareSemver(a, b) {
|
|
|
328
491
|
}
|
|
329
492
|
function readInstalledVersion(packageRoot, read = (path) => {
|
|
330
493
|
try {
|
|
331
|
-
return
|
|
494
|
+
return readFileSync5(path, "utf8");
|
|
332
495
|
} catch {
|
|
333
496
|
return void 0;
|
|
334
497
|
}
|
|
335
498
|
}) {
|
|
336
499
|
try {
|
|
337
|
-
const raw = read(
|
|
500
|
+
const raw = read(join5(packageRoot, "package.json"));
|
|
338
501
|
if (raw === void 0) return void 0;
|
|
339
502
|
const manifest = JSON.parse(raw);
|
|
340
503
|
if (manifest.name !== PACKAGE_NAME) return void 0;
|
|
@@ -345,20 +508,20 @@ function readInstalledVersion(packageRoot, read = (path) => {
|
|
|
345
508
|
}
|
|
346
509
|
function resolveCacheRoot(packageRoot, exists = (p) => {
|
|
347
510
|
try {
|
|
348
|
-
return
|
|
511
|
+
return statSync3(p).isFile();
|
|
349
512
|
} catch {
|
|
350
513
|
return false;
|
|
351
514
|
}
|
|
352
515
|
}) {
|
|
353
|
-
const tail =
|
|
516
|
+
const tail = join5("node_modules", "@lawaty", "lacode");
|
|
354
517
|
if (!packageRoot.endsWith(tail)) return void 0;
|
|
355
518
|
const cacheRoot = resolve(packageRoot, "..", "..", "..");
|
|
356
|
-
if (!exists(
|
|
519
|
+
if (!exists(join5(cacheRoot, "package.json"))) return void 0;
|
|
357
520
|
return cacheRoot;
|
|
358
521
|
}
|
|
359
522
|
function resolveOpencodeBin(execPath = process.execPath, env = process.env, exists = (p) => {
|
|
360
523
|
try {
|
|
361
|
-
return
|
|
524
|
+
return statSync3(p).isFile();
|
|
362
525
|
} catch {
|
|
363
526
|
return false;
|
|
364
527
|
}
|
|
@@ -366,10 +529,10 @@ function resolveOpencodeBin(execPath = process.execPath, env = process.env, exis
|
|
|
366
529
|
if (execPath && /(^|[\\/])opencode(\.exe)?$/.test(execPath)) return execPath;
|
|
367
530
|
for (const dir of (env.PATH ?? "").split(":")) {
|
|
368
531
|
if (!dir) continue;
|
|
369
|
-
const candidate =
|
|
532
|
+
const candidate = join5(dir, "opencode");
|
|
370
533
|
if (exists(candidate)) return candidate;
|
|
371
534
|
}
|
|
372
|
-
const fallback =
|
|
535
|
+
const fallback = join5(homedir4(), ".opencode", "bin", "opencode");
|
|
373
536
|
return exists(fallback) ? fallback : void 0;
|
|
374
537
|
}
|
|
375
538
|
async function fetchLatest(fetchImpl = fetch, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
@@ -401,11 +564,11 @@ async function refreshCache(opts) {
|
|
|
401
564
|
const { cacheRoot, bin, latest } = opts;
|
|
402
565
|
const spawnImpl = opts.spawnImpl ?? spawn2;
|
|
403
566
|
const timeoutMs = opts.timeoutMs ?? INSTALL_TIMEOUT_MS;
|
|
404
|
-
const fresh =
|
|
567
|
+
const fresh = join5(cacheRoot, "node_modules", "@lawaty", "lacode");
|
|
405
568
|
const aside = `${cacheRoot}.stale-${Date.now()}`;
|
|
406
569
|
try {
|
|
407
570
|
rmSync(aside, { recursive: true, force: true });
|
|
408
|
-
|
|
571
|
+
renameSync2(cacheRoot, aside);
|
|
409
572
|
} catch (error) {
|
|
410
573
|
return { ok: false, detail: `could not set the old cache aside: ${String(error)}` };
|
|
411
574
|
}
|
|
@@ -440,7 +603,7 @@ async function refreshCache(opts) {
|
|
|
440
603
|
}
|
|
441
604
|
} else {
|
|
442
605
|
try {
|
|
443
|
-
|
|
606
|
+
renameSync2(aside, cacheRoot);
|
|
444
607
|
} catch {
|
|
445
608
|
}
|
|
446
609
|
}
|
|
@@ -452,9 +615,9 @@ async function refreshCache(opts) {
|
|
|
452
615
|
|
|
453
616
|
// src/lib/version.ts
|
|
454
617
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
455
|
-
import { existsSync as
|
|
456
|
-
import { homedir as
|
|
457
|
-
import { isAbsolute, join as
|
|
618
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
|
|
619
|
+
import { homedir as homedir5 } from "node:os";
|
|
620
|
+
import { isAbsolute, join as join6, relative, sep } from "node:path";
|
|
458
621
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
459
622
|
|
|
460
623
|
// src/lib/jsonc.ts
|
|
@@ -507,15 +670,15 @@ var parseJsonc = (raw) => {
|
|
|
507
670
|
// src/lib/version.ts
|
|
508
671
|
var nodeStat = (path) => {
|
|
509
672
|
try {
|
|
510
|
-
return
|
|
673
|
+
return statSync4(path).mtimeMs;
|
|
511
674
|
} catch {
|
|
512
675
|
return void 0;
|
|
513
676
|
}
|
|
514
677
|
};
|
|
515
|
-
var nodeExists = (path) =>
|
|
678
|
+
var nodeExists = (path) => existsSync3(path);
|
|
516
679
|
var nodeRead = (path) => {
|
|
517
680
|
try {
|
|
518
|
-
return
|
|
681
|
+
return readFileSync6(path, "utf8");
|
|
519
682
|
} catch {
|
|
520
683
|
return void 0;
|
|
521
684
|
}
|
|
@@ -531,17 +694,17 @@ var realGit = (root) => (args) => {
|
|
|
531
694
|
};
|
|
532
695
|
var walkSources = (root) => {
|
|
533
696
|
const found = [];
|
|
534
|
-
const stack = [
|
|
697
|
+
const stack = [join6(root, "src")];
|
|
535
698
|
while (stack.length > 0) {
|
|
536
699
|
const dir = stack.pop();
|
|
537
700
|
let names;
|
|
538
701
|
try {
|
|
539
|
-
names =
|
|
702
|
+
names = readdirSync2(dir);
|
|
540
703
|
} catch {
|
|
541
704
|
continue;
|
|
542
705
|
}
|
|
543
706
|
for (const name of names) {
|
|
544
|
-
const full =
|
|
707
|
+
const full = join6(dir, name);
|
|
545
708
|
if (name.endsWith(".ts")) found.push(full);
|
|
546
709
|
else if (name === "plugins" || name === "lib" || name === "tunnel") stack.push(full);
|
|
547
710
|
}
|
|
@@ -549,8 +712,8 @@ var walkSources = (root) => {
|
|
|
549
712
|
return found;
|
|
550
713
|
};
|
|
551
714
|
var checkDist = (root, deps) => {
|
|
552
|
-
if (!deps.exists(
|
|
553
|
-
const built = deps.stat(
|
|
715
|
+
if (!deps.exists(join6(root, "dist"))) return { dist: "unknown" };
|
|
716
|
+
const built = deps.stat(join6(root, "dist", "index.js"));
|
|
554
717
|
if (built === void 0) return { dist: "unknown" };
|
|
555
718
|
const sources = deps.listSources(root).map((file) => ({ file, mtime: deps.stat(file) }));
|
|
556
719
|
const newer = sources.filter((s) => s.mtime !== void 0 && s.mtime > built).map((s) => relative(root, s.file));
|
|
@@ -591,13 +754,13 @@ var resolvesHere = (specifier, packageRoot) => {
|
|
|
591
754
|
};
|
|
592
755
|
function readVersionReport(deps = {}) {
|
|
593
756
|
const packageRoot = deps.packageRoot ?? PACKAGE_ROOT;
|
|
594
|
-
const home = deps.home ??
|
|
757
|
+
const home = deps.home ?? homedir5();
|
|
595
758
|
const git = deps.git ?? realGit(packageRoot);
|
|
596
759
|
const readFile = deps.readFile ?? nodeRead;
|
|
597
760
|
const stat = deps.stat ?? nodeStat;
|
|
598
761
|
const exists = deps.exists ?? nodeExists;
|
|
599
762
|
const listSources = deps.listSources ?? walkSources;
|
|
600
|
-
const isCheckout = exists(
|
|
763
|
+
const isCheckout = exists(join6(packageRoot, ".git"));
|
|
601
764
|
const report = {
|
|
602
765
|
version: readInstalledVersion(packageRoot, readFile),
|
|
603
766
|
root: packageRoot,
|
|
@@ -610,11 +773,11 @@ function readVersionReport(deps = {}) {
|
|
|
610
773
|
const dist = checkDist(packageRoot, { stat, exists, listSources });
|
|
611
774
|
report.dist = dist.dist;
|
|
612
775
|
if (dist.stale) report.staleSources = dist.stale;
|
|
613
|
-
const configPath = [
|
|
776
|
+
const configPath = [join6(home, ".config", "opencode", "opencode.jsonc"), join6(home, ".config", "opencode", "opencode.json")].find(
|
|
614
777
|
(candidate) => exists(candidate)
|
|
615
778
|
);
|
|
616
779
|
if (!configPath) {
|
|
617
|
-
report.note = `No opencode config found under ${
|
|
780
|
+
report.note = `No opencode config found under ${join6(home, ".config", "opencode")}; nothing to compare this copy against.`;
|
|
618
781
|
return report;
|
|
619
782
|
}
|
|
620
783
|
report.configPath = configPath;
|
|
@@ -836,7 +999,7 @@ ${report.note}`);
|
|
|
836
999
|
}
|
|
837
1000
|
const { user, host, key } = resolveTunnel();
|
|
838
1001
|
const pub = `${key}.pub`;
|
|
839
|
-
if (!
|
|
1002
|
+
if (!existsSync4(key)) {
|
|
840
1003
|
const gen = spawnSync3("ssh-keygen", ["-t", "ed25519", "-N", "", "-C", "lacode-notify", "-f", key], { stdio: ["ignore", "ignore", "inherit"] });
|
|
841
1004
|
if (gen.status !== 0) {
|
|
842
1005
|
console.error("lacode tunnel setup: ssh-keygen failed; create the key by hand and re-run.");
|
|
@@ -844,7 +1007,7 @@ ${report.note}`);
|
|
|
844
1007
|
}
|
|
845
1008
|
console.log(`Generated ${key}`);
|
|
846
1009
|
}
|
|
847
|
-
const pubkey =
|
|
1010
|
+
const pubkey = existsSync4(pub) ? readFileSync7(pub, "utf8").trim() : "(public key missing)";
|
|
848
1011
|
const self = process.argv[1] ?? "lacode";
|
|
849
1012
|
console.log("\nOn the REMOTE host (where opencode runs), export:");
|
|
850
1013
|
console.log(` OC_LOCAL_USER=${user || "<desktop-user>"}`);
|
package/dist/pool.js
CHANGED
|
@@ -33,6 +33,7 @@ var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
|
33
33
|
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
34
34
|
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
35
35
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
36
|
+
var UNAVAILABLE_REASON = "endpoint-unavailable";
|
|
36
37
|
var MAX_WEIGHT = 100;
|
|
37
38
|
var SLOT_CEILING_FACTOR = 2;
|
|
38
39
|
var slotCeiling = (slot) => slot.weight * SLOT_CEILING_FACTOR;
|
|
@@ -44,6 +45,10 @@ var QUOTA_COOLDOWN_MAX_MS = 6 * 60 * 60 * 1e3;
|
|
|
44
45
|
var STRIKE_DECAY_MS = 30 * 60 * 1e3;
|
|
45
46
|
var STATED_COOLDOWN_MAX_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
46
47
|
var isQuotaReason = (reason) => /quota|limit|rate|too-many|overloaded|429/i.test(reason);
|
|
48
|
+
var MODEL_MISSING_REASON = "model-missing";
|
|
49
|
+
var MODEL_MISSING_COOLDOWN_MS = QUOTA_COOLDOWN_MAX_MS;
|
|
50
|
+
var MODEL_MISSING_PATTERN = /model\s+(?:not\s+found|does\s+not\s+exist|is\s+not\s+available)|(?:unknown|no\s+such)\s+model/i;
|
|
51
|
+
var isModelMissingText = (text) => MODEL_MISSING_PATTERN.test(text);
|
|
47
52
|
var BASES = ["explore", "implement", "context"];
|
|
48
53
|
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
49
54
|
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
@@ -52,7 +57,7 @@ var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
|
52
57
|
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
53
58
|
var FALLBACK_SLOTS = [
|
|
54
59
|
{ index: 1, model: "opencode/big-pickle", weight: 3 },
|
|
55
|
-
{ index: 2, model: "opencode-go/
|
|
60
|
+
{ index: 2, model: "opencode-go/beam-free", weight: 2 },
|
|
56
61
|
{ index: 3, model: "opencode/deepseek-v4.1-flash-free", weight: 1 },
|
|
57
62
|
{ index: 4, model: "opencode/glm-5.3-flash-free", weight: 1 }
|
|
58
63
|
];
|
|
@@ -205,7 +210,14 @@ var LIMIT_PATTERNS = [
|
|
|
205
210
|
/overloaded/i,
|
|
206
211
|
UNAVAILABLE_PATTERN
|
|
207
212
|
];
|
|
208
|
-
var
|
|
213
|
+
var LIMIT_REACHED_PATTERN = /\blimits?[\s_-]?(?:reached|exceeded)\b|\breached\s+(?:your|the|its)\s+limit\b/i;
|
|
214
|
+
var CONTEXT_LIMIT_PATTERN = /(?:context|token|length|window|prompt|char|character|byte|size)\s*(?:limit|length|size|window)/i;
|
|
215
|
+
var LIMIT_REASON_PATTERN = /rate[\s_-]?limit|usage limit|too many requests|quota|freelimiterror|overloaded|limits?[\s_-]?(?:reached|exceeded)/i;
|
|
216
|
+
var isLimitText = (text) => LIMIT_PATTERNS.some((re) => re.test(text)) || LIMIT_REACHED_PATTERN.test(text) && !CONTEXT_LIMIT_PATTERN.test(text);
|
|
217
|
+
var limitReason = (text) => {
|
|
218
|
+
if (!isLimitText(text)) return void 0;
|
|
219
|
+
return text.match(LIMIT_REASON_PATTERN)?.[0]?.toLowerCase().replace(/[\s_]/g, "-");
|
|
220
|
+
};
|
|
209
221
|
var isUnavailableText = (text) => UNAVAILABLE_PATTERN.test(text);
|
|
210
222
|
var TRANSIENT_GATEWAY_PATTERNS = [/queue budget/i, /exceeded queue/i, /aborted due to timeout/i, /timeout/i];
|
|
211
223
|
var isTransientGatewayText = (text) => TRANSIENT_GATEWAY_PATTERNS.some((re) => re.test(text));
|
|
@@ -276,6 +288,21 @@ function ensureSlots(s, onWarn) {
|
|
|
276
288
|
for (const problem of problems) onWarn(problem);
|
|
277
289
|
return slots;
|
|
278
290
|
}
|
|
291
|
+
var slotSignature = (origin, slots) => `${origin}\0${slots.map((slot) => `${slot.index}:${slot.model}:${slot.weight}`).join("|")}`;
|
|
292
|
+
function reloadSlots(s, onWarn) {
|
|
293
|
+
const { slots, problems, origin } = resolveModels({ models: s.inlineModels, modelsFile: s.modelsFile });
|
|
294
|
+
const changed = slotSignature(origin, slots) !== slotSignature(s.origin, s.slots);
|
|
295
|
+
if (!changed) return { changed, slots, origin };
|
|
296
|
+
s.slots = slots;
|
|
297
|
+
s.origin = origin;
|
|
298
|
+
for (const problem of problems) onWarn(problem);
|
|
299
|
+
return { changed, slots, origin };
|
|
300
|
+
}
|
|
301
|
+
function slotSources(input = {}) {
|
|
302
|
+
if (input.models !== void 0) return [];
|
|
303
|
+
if (input.modelsFile) return [input.modelsFile];
|
|
304
|
+
return [USER_MODELS_TXT, USER_MODELS_PATH, DEFAULT_PRESET];
|
|
305
|
+
}
|
|
279
306
|
var claimKey = (sessionID, callID) => `${sessionID}:${callID}`;
|
|
280
307
|
var selfFile = (id) => `claims.${id}.json`;
|
|
281
308
|
var limitFile = (model) => `limit.${model.replace(/[^a-zA-Z0-9._-]/g, "__")}.json`;
|
|
@@ -465,11 +492,12 @@ function readLimits(s) {
|
|
|
465
492
|
try {
|
|
466
493
|
const strike = JSON.parse(readFileSync2(path, "utf8"));
|
|
467
494
|
if (typeof strike.until !== "number" || typeof strike.model !== "string") continue;
|
|
495
|
+
const countsAsStrike = strike.reason !== UNAVAILABLE_REASON;
|
|
468
496
|
if (strike.until > now) {
|
|
469
497
|
cooling.set(strike.model, strike);
|
|
470
|
-
memory.set(strike.model, strike);
|
|
498
|
+
if (countsAsStrike) memory.set(strike.model, strike);
|
|
471
499
|
} else if (typeof strike.last === "number" && now - strike.last < STRIKE_DECAY_MS) {
|
|
472
|
-
memory.set(strike.model, strike);
|
|
500
|
+
if (countsAsStrike) memory.set(strike.model, strike);
|
|
473
501
|
} else {
|
|
474
502
|
unlinkSync(path);
|
|
475
503
|
}
|
|
@@ -486,6 +514,7 @@ function readLimits(s) {
|
|
|
486
514
|
}
|
|
487
515
|
function recordStrike(s, model, reason, stated) {
|
|
488
516
|
if (!s.dirReady) return;
|
|
517
|
+
if (reason === UNAVAILABLE_REASON) return coolUnavailable(s, model);
|
|
489
518
|
const now = Date.now();
|
|
490
519
|
const prior = s.strikes.get(model);
|
|
491
520
|
const strikes = prior && now - prior.last < STRIKE_DECAY_MS ? prior.strikes + 1 : 1;
|
|
@@ -510,9 +539,8 @@ function coolUnavailable(s, model) {
|
|
|
510
539
|
const prior = s.cooling.get(model);
|
|
511
540
|
const until = now + UNAVAILABLE_COOLDOWN_MS;
|
|
512
541
|
if (prior && prior.until > until) return prior;
|
|
513
|
-
const strike = { model, until, strikes: 1, reason:
|
|
542
|
+
const strike = { model, until, strikes: 1, reason: UNAVAILABLE_REASON, last: now };
|
|
514
543
|
s.cooling.set(model, strike);
|
|
515
|
-
s.strikes.set(model, strike);
|
|
516
544
|
const path = join2(s.dir, limitFile(model));
|
|
517
545
|
try {
|
|
518
546
|
writeFileSync(`${path}.tmp`, JSON.stringify(strike));
|
|
@@ -755,6 +783,8 @@ export {
|
|
|
755
783
|
MAX_CLAIM_AGE_MS,
|
|
756
784
|
MAX_SLOTS,
|
|
757
785
|
MAX_WEIGHT,
|
|
786
|
+
MODEL_MISSING_COOLDOWN_MS,
|
|
787
|
+
MODEL_MISSING_REASON,
|
|
758
788
|
ORPHAN_TMP_MS,
|
|
759
789
|
PACKAGE_ROOT,
|
|
760
790
|
PRESETS_DIR,
|
|
@@ -766,6 +796,7 @@ export {
|
|
|
766
796
|
STUCK_IDLE_MS,
|
|
767
797
|
STUCK_MIN_AGE_MS,
|
|
768
798
|
UNAVAILABLE_COOLDOWN_MS,
|
|
799
|
+
UNAVAILABLE_REASON,
|
|
769
800
|
USER_CONFIG_DIR,
|
|
770
801
|
USER_MODELS_PATH,
|
|
771
802
|
USER_MODELS_TXT,
|
|
@@ -785,10 +816,12 @@ export {
|
|
|
785
816
|
init,
|
|
786
817
|
isAvailable,
|
|
787
818
|
isLimitText,
|
|
819
|
+
isModelMissingText,
|
|
788
820
|
isProbe,
|
|
789
821
|
isQuotaReason,
|
|
790
822
|
isTransientGatewayText,
|
|
791
823
|
isUnavailableText,
|
|
824
|
+
limitReason,
|
|
792
825
|
manualVariant,
|
|
793
826
|
markProbe,
|
|
794
827
|
noteActivity,
|
|
@@ -807,12 +840,14 @@ export {
|
|
|
807
840
|
refreshCatalog,
|
|
808
841
|
release,
|
|
809
842
|
releaseStuck,
|
|
843
|
+
reloadSlots,
|
|
810
844
|
resolveModels,
|
|
811
845
|
retryAfterMs,
|
|
812
846
|
roundRobinSequence,
|
|
813
847
|
selfFile,
|
|
814
848
|
slotCeiling,
|
|
815
849
|
slotFor,
|
|
850
|
+
slotSources,
|
|
816
851
|
snapshot,
|
|
817
852
|
splitModel,
|
|
818
853
|
state,
|