@lawaty/lacode 0.4.1 → 0.5.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 +44 -1
- package/README.md +130 -551
- package/dist/agents.js +115 -3
- package/dist/commands.js +62 -15
- package/dist/index.js +326 -62
- package/dist/lacode.js +253 -11
- package/dist/pool.js +17 -6
- package/docs/CONFIGURATION.md +113 -0
- package/docs/INSTALL.md +209 -0
- package/docs/OMNIROUTE.md +232 -0
- package/package.json +1 -1
- package/presets/README.md +2 -2
- package/presets/free-tier.json +4 -4
package/dist/lacode.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/lacode
|
|
4
|
-
import { fileURLToPath as
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { spawnSync as
|
|
4
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
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";
|
|
@@ -279,9 +279,17 @@ function compareSemver(a, b) {
|
|
|
279
279
|
}
|
|
280
280
|
return 0;
|
|
281
281
|
}
|
|
282
|
-
function readInstalledVersion(packageRoot) {
|
|
282
|
+
function readInstalledVersion(packageRoot, read = (path) => {
|
|
283
|
+
try {
|
|
284
|
+
return readFileSync2(path, "utf8");
|
|
285
|
+
} catch {
|
|
286
|
+
return void 0;
|
|
287
|
+
}
|
|
288
|
+
}) {
|
|
283
289
|
try {
|
|
284
|
-
const
|
|
290
|
+
const raw = read(join3(packageRoot, "package.json"));
|
|
291
|
+
if (raw === void 0) return void 0;
|
|
292
|
+
const manifest = JSON.parse(raw);
|
|
285
293
|
if (manifest.name !== PACKAGE_NAME) return void 0;
|
|
286
294
|
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
287
295
|
} catch {
|
|
@@ -395,16 +403,229 @@ async function refreshCache(opts) {
|
|
|
395
403
|
};
|
|
396
404
|
}
|
|
397
405
|
|
|
406
|
+
// src/lib/version.ts
|
|
407
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
408
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
|
|
409
|
+
import { homedir as homedir3 } from "node:os";
|
|
410
|
+
import { isAbsolute, join as join4, relative, sep } from "node:path";
|
|
411
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
412
|
+
|
|
413
|
+
// src/lib/jsonc.ts
|
|
414
|
+
var stripJsonc = (raw) => {
|
|
415
|
+
let out = "";
|
|
416
|
+
let i = 0;
|
|
417
|
+
while (i < raw.length) {
|
|
418
|
+
const ch = raw[i];
|
|
419
|
+
if (ch === '"') {
|
|
420
|
+
out += ch;
|
|
421
|
+
i++;
|
|
422
|
+
while (i < raw.length) {
|
|
423
|
+
out += raw[i];
|
|
424
|
+
if (raw[i] === "\\") {
|
|
425
|
+
out += raw[i + 1] ?? "";
|
|
426
|
+
i += 2;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (raw[i] === '"') {
|
|
430
|
+
i++;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
i++;
|
|
434
|
+
}
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (ch === "/" && raw[i + 1] === "/") {
|
|
438
|
+
while (i < raw.length && raw[i] !== "\n") i++;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (ch === "/" && raw[i + 1] === "*") {
|
|
442
|
+
i += 2;
|
|
443
|
+
while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
|
|
444
|
+
i += 2;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
out += ch;
|
|
448
|
+
i++;
|
|
449
|
+
}
|
|
450
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
451
|
+
};
|
|
452
|
+
var parseJsonc = (raw) => {
|
|
453
|
+
try {
|
|
454
|
+
return JSON.parse(stripJsonc(raw));
|
|
455
|
+
} catch {
|
|
456
|
+
return void 0;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// src/lib/version.ts
|
|
461
|
+
var nodeStat = (path) => {
|
|
462
|
+
try {
|
|
463
|
+
return statSync2(path).mtimeMs;
|
|
464
|
+
} catch {
|
|
465
|
+
return void 0;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
var nodeExists = (path) => existsSync2(path);
|
|
469
|
+
var nodeRead = (path) => {
|
|
470
|
+
try {
|
|
471
|
+
return readFileSync3(path, "utf8");
|
|
472
|
+
} catch {
|
|
473
|
+
return void 0;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
var realGit = (root) => (args) => {
|
|
477
|
+
try {
|
|
478
|
+
const result = spawnSync2("git", ["-C", root, ...args], { encoding: "utf8", timeout: 5e3 });
|
|
479
|
+
if (result.status !== 0) return void 0;
|
|
480
|
+
return (result.stdout ?? "").trim();
|
|
481
|
+
} catch {
|
|
482
|
+
return void 0;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
var walkSources = (root) => {
|
|
486
|
+
const found = [];
|
|
487
|
+
const stack = [join4(root, "src")];
|
|
488
|
+
while (stack.length > 0) {
|
|
489
|
+
const dir = stack.pop();
|
|
490
|
+
let names;
|
|
491
|
+
try {
|
|
492
|
+
names = readdirSync(dir);
|
|
493
|
+
} catch {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
for (const name of names) {
|
|
497
|
+
const full = join4(dir, name);
|
|
498
|
+
if (name.endsWith(".ts")) found.push(full);
|
|
499
|
+
else if (name === "plugins" || name === "lib" || name === "tunnel") stack.push(full);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return found;
|
|
503
|
+
};
|
|
504
|
+
var checkDist = (root, deps) => {
|
|
505
|
+
if (!deps.exists(join4(root, "dist"))) return { dist: "unknown" };
|
|
506
|
+
const built = deps.stat(join4(root, "dist", "index.js"));
|
|
507
|
+
if (built === void 0) return { dist: "unknown" };
|
|
508
|
+
const sources = deps.listSources(root).map((file) => ({ file, mtime: deps.stat(file) }));
|
|
509
|
+
const newer = sources.filter((s) => s.mtime !== void 0 && s.mtime > built).map((s) => relative(root, s.file));
|
|
510
|
+
if (newer.length > 0) return { dist: "stale", stale: newer };
|
|
511
|
+
return { dist: "current" };
|
|
512
|
+
};
|
|
513
|
+
var readPluginEntries = (raw) => {
|
|
514
|
+
const parsed = parseJsonc(raw);
|
|
515
|
+
const list = parsed?.plugin;
|
|
516
|
+
if (!Array.isArray(list)) return void 0;
|
|
517
|
+
const entries = [];
|
|
518
|
+
for (const item of list) {
|
|
519
|
+
if (typeof item === "string") entries.push({ specifier: item });
|
|
520
|
+
else if (Array.isArray(item) && typeof item[0] === "string") {
|
|
521
|
+
entries.push({ specifier: item[0], options: item[1] ?? void 0 });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return entries;
|
|
525
|
+
};
|
|
526
|
+
var isInside = (child, parent) => {
|
|
527
|
+
const rel = relative(parent, child);
|
|
528
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
529
|
+
};
|
|
530
|
+
var resolvesHere = (specifier, packageRoot) => {
|
|
531
|
+
if (specifier === PACKAGE_NAME) {
|
|
532
|
+
return packageRoot.split(sep).includes("node_modules");
|
|
533
|
+
}
|
|
534
|
+
if (specifier.startsWith("file://")) {
|
|
535
|
+
try {
|
|
536
|
+
const path = fileURLToPath2(specifier);
|
|
537
|
+
return isInside(path, packageRoot) || isInside(packageRoot, path);
|
|
538
|
+
} catch {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (specifier.startsWith(".")) return resolvesHere(new URL(specifier, `file://${packageRoot}/`).href, packageRoot);
|
|
543
|
+
return false;
|
|
544
|
+
};
|
|
545
|
+
function readVersionReport(deps = {}) {
|
|
546
|
+
const packageRoot = deps.packageRoot ?? PACKAGE_ROOT;
|
|
547
|
+
const home = deps.home ?? homedir3();
|
|
548
|
+
const git = deps.git ?? realGit(packageRoot);
|
|
549
|
+
const readFile = deps.readFile ?? nodeRead;
|
|
550
|
+
const stat = deps.stat ?? nodeStat;
|
|
551
|
+
const exists = deps.exists ?? nodeExists;
|
|
552
|
+
const listSources = deps.listSources ?? walkSources;
|
|
553
|
+
const isCheckout = exists(join4(packageRoot, ".git"));
|
|
554
|
+
const report = {
|
|
555
|
+
version: readInstalledVersion(packageRoot, readFile),
|
|
556
|
+
root: packageRoot,
|
|
557
|
+
source: isCheckout ? "checkout" : "installed"
|
|
558
|
+
};
|
|
559
|
+
if (isCheckout) {
|
|
560
|
+
report.commit = git(["rev-parse", "--short", "HEAD"]) || "(unknown)";
|
|
561
|
+
report.dirty = (git(["status", "--porcelain"]) ?? "").length > 0;
|
|
562
|
+
}
|
|
563
|
+
const dist = checkDist(packageRoot, { stat, exists, listSources });
|
|
564
|
+
report.dist = dist.dist;
|
|
565
|
+
if (dist.stale) report.staleSources = dist.stale;
|
|
566
|
+
const configPath = [join4(home, ".config", "opencode", "opencode.jsonc"), join4(home, ".config", "opencode", "opencode.json")].find(
|
|
567
|
+
(candidate) => exists(candidate)
|
|
568
|
+
);
|
|
569
|
+
if (!configPath) {
|
|
570
|
+
report.note = `No opencode config found under ${join4(home, ".config", "opencode")}; nothing to compare this copy against.`;
|
|
571
|
+
return report;
|
|
572
|
+
}
|
|
573
|
+
report.configPath = configPath;
|
|
574
|
+
const entries = readPluginEntries(readFile(configPath) ?? "");
|
|
575
|
+
if (!entries) {
|
|
576
|
+
report.note = `${configPath} has no readable \`plugin\` array.`;
|
|
577
|
+
return report;
|
|
578
|
+
}
|
|
579
|
+
report.loaded = entries.map((e) => e.specifier);
|
|
580
|
+
const lacodeEntries = entries.filter((e) => e.specifier.includes("lacode"));
|
|
581
|
+
const ours = lacodeEntries.filter((e) => resolvesHere(e.specifier, packageRoot));
|
|
582
|
+
if (lacodeEntries.length === 0) {
|
|
583
|
+
report.servedByThisCopy = false;
|
|
584
|
+
report.note = `${configPath} loads no LaCode plugin entry.`;
|
|
585
|
+
return report;
|
|
586
|
+
}
|
|
587
|
+
if (ours.length > 0) {
|
|
588
|
+
report.servedByThisCopy = ours.length === lacodeEntries.length;
|
|
589
|
+
if (!report.servedByThisCopy) {
|
|
590
|
+
report.note = `${configPath} loads LaCode from more than one place; only some entries resolve to this copy.`;
|
|
591
|
+
}
|
|
592
|
+
return report;
|
|
593
|
+
}
|
|
594
|
+
report.servedByThisCopy = false;
|
|
595
|
+
report.note = `opencode loads LaCode from ${lacodeEntries.map((e) => e.specifier).join(", ")} \u2014 NOT this copy at ${packageRoot}. The version above is this CLI, not necessarily what opencode runs.`;
|
|
596
|
+
return report;
|
|
597
|
+
}
|
|
598
|
+
var formatVersion = (report) => {
|
|
599
|
+
const lines = [
|
|
600
|
+
["version", report.version ?? "(unknown)"],
|
|
601
|
+
["root", report.root],
|
|
602
|
+
["source", report.source]
|
|
603
|
+
];
|
|
604
|
+
if (report.commit) {
|
|
605
|
+
lines.push(["commit", `${report.commit}${report.dirty ? " (dirty)" : ""}`]);
|
|
606
|
+
}
|
|
607
|
+
if (report.dist) {
|
|
608
|
+
const stale = report.staleSources?.length ? ` \u2014 ${report.staleSources.length} source file(s) newer than dist/index.js` : "";
|
|
609
|
+
lines.push(["dist", `${report.dist}${stale}`]);
|
|
610
|
+
}
|
|
611
|
+
if (report.configPath) lines.push(["config", report.configPath]);
|
|
612
|
+
if (report.loaded?.length) lines.push(["plugin", report.loaded.join(", ")]);
|
|
613
|
+
if (report.servedByThisCopy !== void 0) {
|
|
614
|
+
lines.push(["this copy", report.servedByThisCopy ? "yes \u2014 opencode loads this copy" : "NO \u2014 see note"]);
|
|
615
|
+
}
|
|
616
|
+
return lines.map(([key, value]) => `${key.padEnd(10)} ${String(value)}`).join("\n");
|
|
617
|
+
};
|
|
618
|
+
|
|
398
619
|
// bin/lacode
|
|
399
620
|
if (!process.features?.typescript) {
|
|
400
621
|
if (process.env.LACODE_STRIP_TYPECHECKED === "1") {
|
|
401
622
|
console.error("lacode: this node cannot load TypeScript sources; node >= 22.18 is required.");
|
|
402
623
|
process.exit(1);
|
|
403
624
|
}
|
|
404
|
-
const { spawnSync:
|
|
405
|
-
const result =
|
|
625
|
+
const { spawnSync: spawnSync4 } = await import("node:child_process");
|
|
626
|
+
const result = spawnSync4(
|
|
406
627
|
process.execPath,
|
|
407
|
-
["--experimental-strip-types", "--no-warnings",
|
|
628
|
+
["--experimental-strip-types", "--no-warnings", fileURLToPath3(import.meta.url), ...process.argv.slice(2)],
|
|
408
629
|
{ stdio: "inherit", env: { ...process.env, LACODE_STRIP_TYPECHECKED: "1" } }
|
|
409
630
|
);
|
|
410
631
|
process.exit(result.status ?? 1);
|
|
@@ -425,6 +646,15 @@ var USAGE = `lacode \u2014 portable notifications and remote exec for LaCode
|
|
|
425
646
|
lacode doctor
|
|
426
647
|
Print what the environment resolves to. Diagnostics only.
|
|
427
648
|
|
|
649
|
+
lacode version
|
|
650
|
+
Report WHICH LaCode is speaking and WHICH one opencode actually loads.
|
|
651
|
+
Version, source (checkout or install), the commit it was built from,
|
|
652
|
+
and whether dist/ is behind src/ after a git pull. Compares itself
|
|
653
|
+
against the plugin array in your opencode config, because a config may
|
|
654
|
+
load the plugin by absolute path and report a version you did not ask
|
|
655
|
+
about. Never reaches the network; \`lacode update --check\` is what asks
|
|
656
|
+
npm whether something newer exists.
|
|
657
|
+
|
|
428
658
|
lacode update [--check]
|
|
429
659
|
Bring this machine's installed LaCode up to the latest published
|
|
430
660
|
version. opencode's plugin cache never re-resolves on its own, so
|
|
@@ -485,6 +715,18 @@ switch (command) {
|
|
|
485
715
|
process.exit(0);
|
|
486
716
|
break;
|
|
487
717
|
}
|
|
718
|
+
case "version": {
|
|
719
|
+
const report = readVersionReport();
|
|
720
|
+
console.log(formatVersion(report));
|
|
721
|
+
if (report.note) console.log(`
|
|
722
|
+
${report.note}`);
|
|
723
|
+
if (report.dist === "stale") {
|
|
724
|
+
console.log("\nRebuild before trusting the rest of this report: npm run build");
|
|
725
|
+
for (const file of report.staleSources ?? []) console.log(` newer than dist: ${file}`);
|
|
726
|
+
}
|
|
727
|
+
process.exit(0);
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
488
730
|
case "update": {
|
|
489
731
|
const checkOnly = argv.includes("--check");
|
|
490
732
|
const check = await checkForUpdate({ packageRoot: PACKAGE_ROOT });
|
|
@@ -534,15 +776,15 @@ switch (command) {
|
|
|
534
776
|
}
|
|
535
777
|
const { user, host, key } = resolveTunnel();
|
|
536
778
|
const pub = `${key}.pub`;
|
|
537
|
-
if (!
|
|
538
|
-
const gen =
|
|
779
|
+
if (!existsSync3(key)) {
|
|
780
|
+
const gen = spawnSync3("ssh-keygen", ["-t", "ed25519", "-N", "", "-C", "lacode-notify", "-f", key], { stdio: ["ignore", "ignore", "inherit"] });
|
|
539
781
|
if (gen.status !== 0) {
|
|
540
782
|
console.error("lacode tunnel setup: ssh-keygen failed; create the key by hand and re-run.");
|
|
541
783
|
process.exit(1);
|
|
542
784
|
}
|
|
543
785
|
console.log(`Generated ${key}`);
|
|
544
786
|
}
|
|
545
|
-
const pubkey =
|
|
787
|
+
const pubkey = existsSync3(pub) ? readFileSync4(pub, "utf8").trim() : "(public key missing)";
|
|
546
788
|
const self = process.argv[1] ?? "lacode";
|
|
547
789
|
console.log("\nOn the REMOTE host (where opencode runs), export:");
|
|
548
790
|
console.log(` OC_LOCAL_USER=${user || "<desktop-user>"}`);
|
package/dist/pool.js
CHANGED
|
@@ -30,6 +30,7 @@ var ORPHAN_TMP_MS = 60 * 1e3;
|
|
|
30
30
|
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
31
31
|
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
32
32
|
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
33
|
+
var MAX_CLAIM_AGE_MS = 30 * 60 * 1e3;
|
|
33
34
|
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
34
35
|
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
35
36
|
var MAX_WEIGHT = 100;
|
|
@@ -48,9 +49,9 @@ var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
|
48
49
|
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
49
50
|
var FALLBACK_SLOTS = [
|
|
50
51
|
{ index: 1, model: "opencode/space-bunny-free", weight: 3 },
|
|
51
|
-
{ index: 2, model: "opencode-go/
|
|
52
|
+
{ index: 2, model: "opencode-go/longcat-2.5-preview-free", weight: 2 },
|
|
52
53
|
{ index: 3, model: "opencode/big-pickle", weight: 1 },
|
|
53
|
-
{ index: 4, model: "opencode/
|
|
54
|
+
{ index: 4, model: "opencode/longcat-2.5-preview-free", weight: 1 }
|
|
54
55
|
];
|
|
55
56
|
function parseSlots(parsed, origin) {
|
|
56
57
|
const problems = [];
|
|
@@ -203,6 +204,8 @@ var LIMIT_PATTERNS = [
|
|
|
203
204
|
];
|
|
204
205
|
var isLimitText = (text) => LIMIT_PATTERNS.some((re) => re.test(text));
|
|
205
206
|
var isUnavailableText = (text) => UNAVAILABLE_PATTERN.test(text);
|
|
207
|
+
var TRANSIENT_GATEWAY_PATTERNS = [/queue budget/i, /exceeded queue/i, /aborted due to timeout/i, /timeout/i];
|
|
208
|
+
var isTransientGatewayText = (text) => TRANSIENT_GATEWAY_PATTERNS.some((re) => re.test(text));
|
|
206
209
|
var statedCooldown = (text) => {
|
|
207
210
|
const hm = /resets?\s+in\s+(\d+)\s*(?:h|hr|hour)s?\s*(\d+)?\s*(?:m|min|minute)?s?/i.exec(text);
|
|
208
211
|
if (hm) return (Number(hm[1]) * 60 + (hm[2] ? Number(hm[2]) : 0)) * 60 * 1e3;
|
|
@@ -291,7 +294,7 @@ function publish(s) {
|
|
|
291
294
|
if (!s.dirReady) return;
|
|
292
295
|
const now = Date.now();
|
|
293
296
|
const payload = {};
|
|
294
|
-
for (const [key, claim] of s.own) if (now - claim.t <= CLAIM_TTL_MS) payload[key] = claim;
|
|
297
|
+
for (const [key, claim] of s.own) if (now - (claim.hb ?? claim.t) <= CLAIM_TTL_MS) payload[key] = claim;
|
|
295
298
|
try {
|
|
296
299
|
writeFileSync(join2(s.dir, `${selfFile(s.id)}.tmp`), JSON.stringify(payload));
|
|
297
300
|
renameSync(join2(s.dir, `${selfFile(s.id)}.tmp`), join2(s.dir, selfFile(s.id)));
|
|
@@ -572,12 +575,17 @@ function idleFor(s, claim, now) {
|
|
|
572
575
|
const seen = s.activity.get(target);
|
|
573
576
|
return seen ? now - seen : Infinity;
|
|
574
577
|
}
|
|
578
|
+
function condemnReason(s, claim, now = Date.now()) {
|
|
579
|
+
const age = now - claim.t;
|
|
580
|
+
if (age > MAX_CLAIM_AGE_MS) return "overran";
|
|
581
|
+
if (age > STUCK_MIN_AGE_MS && idleFor(s, claim, now) >= STUCK_IDLE_MS) return "hung";
|
|
582
|
+
return void 0;
|
|
583
|
+
}
|
|
575
584
|
function findStuck(s, now = Date.now()) {
|
|
576
585
|
const stuck = [];
|
|
577
586
|
for (const [key, claim] of s.own) {
|
|
578
|
-
|
|
579
|
-
if (
|
|
580
|
-
stuck.push({ key, claim });
|
|
587
|
+
const reason = condemnReason(s, claim, now);
|
|
588
|
+
if (reason) stuck.push({ key, claim, reason });
|
|
581
589
|
}
|
|
582
590
|
return stuck;
|
|
583
591
|
}
|
|
@@ -613,6 +621,7 @@ export {
|
|
|
613
621
|
DEFAULT_PRESET,
|
|
614
622
|
FALLBACK_SLOTS,
|
|
615
623
|
HANG_COOLDOWN_MS,
|
|
624
|
+
MAX_CLAIM_AGE_MS,
|
|
616
625
|
MAX_SLOTS,
|
|
617
626
|
MAX_WEIGHT,
|
|
618
627
|
ORPHAN_TMP_MS,
|
|
@@ -631,6 +640,7 @@ export {
|
|
|
631
640
|
backfillChild,
|
|
632
641
|
bindChild,
|
|
633
642
|
claimKey,
|
|
643
|
+
condemnReason,
|
|
634
644
|
coolProvider,
|
|
635
645
|
coolUnavailable,
|
|
636
646
|
ensureSlots,
|
|
@@ -641,6 +651,7 @@ export {
|
|
|
641
651
|
isAvailable,
|
|
642
652
|
isLimitText,
|
|
643
653
|
isQuotaReason,
|
|
654
|
+
isTransientGatewayText,
|
|
644
655
|
isUnavailableText,
|
|
645
656
|
manualVariant,
|
|
646
657
|
noteActivity,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
The plugin options, where the model pool gets its slots, and exactly what LaCode writes into your config. The short version is in the [README](../README.md#configure).
|
|
4
|
+
|
|
5
|
+
## Pool preset resolution
|
|
6
|
+
|
|
7
|
+
The slot list is the first of these that yields usable slots:
|
|
8
|
+
|
|
9
|
+
1. plugin options — `models` in the [tuple](../README.md#configure)
|
|
10
|
+
2. `~/.config/lacode/pool.json` (its parent directory is created for you)
|
|
11
|
+
3. the bundled `presets/free-tier.json`
|
|
12
|
+
4. a built-in fallback, so routing always works
|
|
13
|
+
|
|
14
|
+
A missing, unreadable or structurally broken source falls through to the next and
|
|
15
|
+
logs one warning; nothing here throws into opencode. Nothing hot-reloads —
|
|
16
|
+
change a preset, then restart every opencode process.
|
|
17
|
+
|
|
18
|
+
**Any model list is accepted, including paid ones** (US-7). There is no free-tier
|
|
19
|
+
allowlist anywhere in the package, and no cost check of any kind: only the
|
|
20
|
+
structure is validated. The bundled preset is built from free models because
|
|
21
|
+
that is a sensible default, not because the pool requires it.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Which models run which agent
|
|
25
|
+
|
|
26
|
+
Only the **pool** writes a model onto a pooled agent, from whichever source wins
|
|
27
|
+
above. The pooled bases (`explore`, `implement`, `context`)
|
|
28
|
+
and their hidden `-1`..`-4` variants therefore carry no model of their own: a
|
|
29
|
+
model written next to an agent would be a second source of truth that silently
|
|
30
|
+
wins whenever routing falls through. Spawn a base and let the pool route it.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## ⚠️ Per-agent tool grants must be in your own config
|
|
34
|
+
|
|
35
|
+
**LaCode cannot grant tools to its agents, and this is not an oversight you can
|
|
36
|
+
work around by configuring it.** If you keep a global `"tools"` block in your
|
|
37
|
+
config — for example `"tools": { "playwright_*": false }` — then the pooled
|
|
38
|
+
exploration and implementation bases (`explore` and `implement`, plus their
|
|
39
|
+
hidden `-1`..`-4` pool variants) lose browser access, and **nothing tells them why**: the tool call is simply
|
|
40
|
+
refused mid-task. `context` gets no grant either, which is correct — it
|
|
41
|
+
writes the context map and has no business in a browser.
|
|
42
|
+
|
|
43
|
+
opencode folds an agent's `tools` into its permission list **at config-load
|
|
44
|
+
time**, while decoding the config — before any plugin runs. A plugin's `config`
|
|
45
|
+
hook mutates the already-decoded config, so the fold never re-runs and anything
|
|
46
|
+
it puts in `agent.tools` is inert. (Verified on opencode 1.18.34/1.18.35 with
|
|
47
|
+
`opencode debug agent explore`: a grant written in your own config shows two
|
|
48
|
+
`playwright_*` entries — `deny` then `allow`, last one winning — while the same
|
|
49
|
+
grant injected by the plugin shows only the `deny`.) LaCode's other injected
|
|
50
|
+
fields are not affected; `permission`, `prompt`, `model`, `description`,
|
|
51
|
+
`temperature`, `mode` and `hidden` are all read after the plugin hook runs.
|
|
52
|
+
|
|
53
|
+
**So if you use a global `"tools"` block, declare the grants yourself:**
|
|
54
|
+
|
|
55
|
+
```jsonc
|
|
56
|
+
{
|
|
57
|
+
"tools": { "playwright_*": false },
|
|
58
|
+
"agent": {
|
|
59
|
+
"explore": { "tools": { "playwright_*": true } },
|
|
60
|
+
"implement": { "tools": { "playwright_*": true } }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The hidden pool variants (`explore-1`, `implement-2`, …) inherit
|
|
66
|
+
nothing here — add a line per variant, or drop the global
|
|
67
|
+
deny and scope it to the agents that should not have browser access instead.
|
|
68
|
+
Note that you cannot *spawn* a variant by name: the pool refuses
|
|
69
|
+
`explore-1` and friends and points you at the bare base, so per-variant
|
|
70
|
+
permissions only ever apply to a spawn the plugin routed itself.
|
|
71
|
+
|
|
72
|
+
Nothing hot-reloads: after editing your config, restart every running opencode
|
|
73
|
+
process.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## What gets injected
|
|
77
|
+
|
|
78
|
+
**18 agent keys**: the 5 bases `build`, `plan`, `explore`, `implement`,
|
|
79
|
+
`context`, plus one hidden
|
|
80
|
+
`<base>-1`..`-4` pool variant for each of the 3 pooled bases, plus the hidden
|
|
81
|
+
`manager` agent — the one writer of `.opencode/context/manager.md`, reachable
|
|
82
|
+
only from the `/manager-directive` command.
|
|
83
|
+
|
|
84
|
+
`build` and `plan` exist in opencode as built-ins, so on a stock config those
|
|
85
|
+
two keys are already taken and merge-without-clobber keeps the built-in — the
|
|
86
|
+
LaCode definitions of `build` and `plan` are what you get if you remove the
|
|
87
|
+
built-ins of those names first.
|
|
88
|
+
|
|
89
|
+
**Five keys are deliberately not injected.** `basic` is a byte-for-byte
|
|
90
|
+
duplicate of `plan` apart from a pinned model, and `plan` is injected instead.
|
|
91
|
+
`expert` pins a specific model and carries a personal benchmark note in its
|
|
92
|
+
description — a package cannot ship someone else's provider, quota or benchmark
|
|
93
|
+
results — though its escalation ladder still resolves, because the two deep
|
|
94
|
+
tiers it escalates to *are* injected. And `explore`, `general` and `implement`
|
|
95
|
+
are only `{ "disable": true }` stubs in the author's own config: turning off
|
|
96
|
+
opencode's built-in agents is a decision about one person's configuration, not
|
|
97
|
+
a default a package should impose on a stranger.
|
|
98
|
+
|
|
99
|
+
One **field** is deliberately not injected either: `tools`, for the
|
|
100
|
+
load-time-folding reason explained above. Nothing else the plugin writes into
|
|
101
|
+
an agent is subject to it.
|
|
102
|
+
|
|
103
|
+
**Installation merges, it does not clobber** (US-16). An agent, command or
|
|
104
|
+
instruction is added only when that key is absent from your config. A
|
|
105
|
+
user-defined agent of the same name is left byte-identical — not patched, not
|
|
106
|
+
partially merged. Skips are counted and logged once.
|
|
107
|
+
|
|
108
|
+
**`enabled: false` turns all of it off** (US-17). No pool model rewriting, no
|
|
109
|
+
agent injection, no command injection, no reaper timer armed, no
|
|
110
|
+
`~/.config/lacode` directory created.
|
|
111
|
+
|
|
112
|
+
These three are specified in [docs/USER-STORIES.md](USER-STORIES.md) as
|
|
113
|
+
US-7, US-16 and US-17, and each has tests.
|