@lawaty/lacode 1.3.0-beta.9 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -1
- package/dist/commands.js +4 -0
- package/dist/index.js +4 -0
- package/package.json +5 -3
- package/scripts/refresh-pool.mjs +335 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,16 @@ commit as the changelog entry it describes.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.3.1] — reprocess-timeline rebuilds the caller's timeline, and the pool refreshes daily
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Pool: `scripts/refresh-pool.mjs` rebuilds `~/.config/lacode/pool.txt` from the score output — opencode-go free models first, preserved unscored providers in the middle, the best opencode model last — with an atomic, fail-closed write. `--install-timer` installs a daily systemd user timer; `--from-scores` reuses an existing `model-scores.json`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Timeline: `/reprocess-timeline` now runs inline (`subtask: false`) instead of in a child session, so `timeline_reprocess` rebuilds the caller's timeline rather than the child's.
|
|
21
|
+
|
|
12
22
|
## [1.3.0-beta.9] — a timeline reply without bullets still lands
|
|
13
23
|
|
|
14
24
|
### Fixed
|
|
@@ -1105,7 +1115,8 @@ experimental; `latest` remains 0.4.2.
|
|
|
1105
1115
|
|
|
1106
1116
|
- First published release: pooled subagents over a shared set of models.
|
|
1107
1117
|
|
|
1108
|
-
[Unreleased]: https://github.com/lawaty/lacode/compare/v1.3.
|
|
1118
|
+
[Unreleased]: https://github.com/lawaty/lacode/compare/v1.3.1...HEAD
|
|
1119
|
+
[1.3.1]: https://github.com/lawaty/lacode/releases/tag/v1.3.1
|
|
1109
1120
|
[1.3.0-beta.9]: https://github.com/lawaty/lacode/releases/tag/v1.3.0-beta.9
|
|
1110
1121
|
[1.3.0-beta.8]: https://github.com/lawaty/lacode/releases/tag/v1.3.0-beta.8
|
|
1111
1122
|
[1.3.0-beta.0]: https://github.com/lawaty/lacode/releases/tag/v1.3.0-beta.0
|
package/dist/commands.js
CHANGED
|
@@ -225,9 +225,13 @@ $ARGUMENTS
|
|
|
225
225
|
|
|
226
226
|
Report the tool's output as it came back, then add one short line naming what changed: re-admitted, still limited, or benched because the provider does not serve the model. If a model was benched as \`model-missing\`, say so and name the replacement in \`~/.config/lacode/pool.txt\` \u2014 but change nothing yourself.`
|
|
227
227
|
},
|
|
228
|
+
// `agent: "implement"` is a subagent, so without `subtask: false` this command
|
|
229
|
+
// would run in a child session and `timeline_reprocess` would rebuild the
|
|
230
|
+
// child's timeline instead of the caller's.
|
|
228
231
|
"reprocess-timeline": {
|
|
229
232
|
description: "Rebuild this session's timeline from its full transcript",
|
|
230
233
|
agent: "implement",
|
|
234
|
+
subtask: false,
|
|
231
235
|
template: "Call the `timeline_reprocess` tool exactly once with no arguments. Then reply with only its output."
|
|
232
236
|
},
|
|
233
237
|
"image-in-cb": {
|
package/dist/index.js
CHANGED
|
@@ -2085,9 +2085,13 @@ $ARGUMENTS
|
|
|
2085
2085
|
|
|
2086
2086
|
Report the tool's output as it came back, then add one short line naming what changed: re-admitted, still limited, or benched because the provider does not serve the model. If a model was benched as \`model-missing\`, say so and name the replacement in \`~/.config/lacode/pool.txt\` \u2014 but change nothing yourself.`
|
|
2087
2087
|
},
|
|
2088
|
+
// `agent: "implement"` is a subagent, so without `subtask: false` this command
|
|
2089
|
+
// would run in a child session and `timeline_reprocess` would rebuild the
|
|
2090
|
+
// child's timeline instead of the caller's.
|
|
2088
2091
|
"reprocess-timeline": {
|
|
2089
2092
|
description: "Rebuild this session's timeline from its full transcript",
|
|
2090
2093
|
agent: "implement",
|
|
2094
|
+
subtask: false,
|
|
2091
2095
|
template: "Call the `timeline_reprocess` tool exactly once with no arguments. Then reply with only its output."
|
|
2092
2096
|
},
|
|
2093
2097
|
"image-in-cb": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lawaty/lacode",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"CHANGELOG.md",
|
|
51
51
|
"README.md",
|
|
52
52
|
"docs",
|
|
53
|
-
"scripts/score-models.mjs"
|
|
53
|
+
"scripts/score-models.mjs",
|
|
54
|
+
"scripts/refresh-pool.mjs"
|
|
54
55
|
],
|
|
55
56
|
"scripts": {
|
|
56
57
|
"build": "node scripts/build.mjs",
|
|
@@ -58,7 +59,8 @@
|
|
|
58
59
|
"postpublish": "node scripts/sync-hosts.mjs && ./bin/oc-sync",
|
|
59
60
|
"test": "node --test tests/",
|
|
60
61
|
"benchmark": "node tests/benchmark/run.mjs",
|
|
61
|
-
"score": "node scripts/score-models.mjs"
|
|
62
|
+
"score": "node scripts/score-models.mjs",
|
|
63
|
+
"refresh-pool": "node scripts/refresh-pool.mjs"
|
|
62
64
|
},
|
|
63
65
|
"devDependencies": {
|
|
64
66
|
"esbuild": "^0.28.2"
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Daily-refreshable pool builder.
|
|
3
|
+
//
|
|
4
|
+
// Reads the scorer's output (~/.config/lacode/model-scores.json) and proposes a
|
|
5
|
+
// rewritten ~/.config/lacode/pool.txt. It never partially writes and never
|
|
6
|
+
// writes on a fail-closed condition.
|
|
7
|
+
//
|
|
8
|
+
// Layout (exact, per spec):
|
|
9
|
+
// 1. every free opencode-go model with ok:true and a numeric score, one slot
|
|
10
|
+
// each, sorted by score descending, weight 4
|
|
11
|
+
// 2. every slot from the current pool whose provider is NOT present in the
|
|
12
|
+
// score file, preserved exactly (raw line text, weight/bucket/notify, order)
|
|
13
|
+
// 3. exactly one free opencode model — the best ok:true numeric score —
|
|
14
|
+
// weight 2, with b:opencode
|
|
15
|
+
//
|
|
16
|
+
// Total = #opencode-go free + #preserved unscored + 1.
|
|
17
|
+
//
|
|
18
|
+
// This file ships as source and must run with plain `node` (ESM, no TS).
|
|
19
|
+
|
|
20
|
+
import { mkdirSync, renameSync, writeFileSync, readFileSync, existsSync } from "node:fs"
|
|
21
|
+
import { execFileSync } from "node:child_process"
|
|
22
|
+
import { homedir } from "node:os"
|
|
23
|
+
import { dirname, join, resolve } from "node:path"
|
|
24
|
+
import { fileURLToPath } from "node:url"
|
|
25
|
+
|
|
26
|
+
const HOME = homedir()
|
|
27
|
+
export const DEFAULT_SCORES_PATH = join(HOME, ".config", "lacode", "model-scores.json")
|
|
28
|
+
export const DEFAULT_POOL_PATH = join(HOME, ".config", "lacode", "pool.txt")
|
|
29
|
+
export const SYSTEMD_USER_DIR = (home = HOME) => join(home, ".config", "systemd", "user")
|
|
30
|
+
export const REFRESH_SERVICE_NAME = "lacode-refresh-pool.service"
|
|
31
|
+
export const REFRESH_TIMER_NAME = "lacode-refresh-pool.timer"
|
|
32
|
+
|
|
33
|
+
const isSlotLine = (line) => {
|
|
34
|
+
const stripped = String(line).split("#")[0].trim()
|
|
35
|
+
return stripped.length > 0 && stripped.includes("/")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const providerOf = (modelOrLine) => {
|
|
39
|
+
const stripped = String(modelOrLine).split("#")[0].trim()
|
|
40
|
+
const token = stripped.split(/\s+/)[0] || ""
|
|
41
|
+
const slash = token.indexOf("/")
|
|
42
|
+
return slash < 0 ? token : token.slice(0, slash)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isFree = (modelId) => {
|
|
46
|
+
const id = String(modelId || "")
|
|
47
|
+
return id.endsWith(":free") || id.endsWith("-free")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const numericScore = (row) => typeof row?.score === "number" && Number.isFinite(row.score)
|
|
51
|
+
|
|
52
|
+
const okFreeRows = (providerEntry) => {
|
|
53
|
+
const rows = Array.isArray(providerEntry?.rows) ? providerEntry.rows : []
|
|
54
|
+
return rows.filter((row) => row?.ok === true && numericScore(row) && isFree(row.model))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function splitHeaderAndSlots(text) {
|
|
58
|
+
const lines = String(text ?? "").split(/\r?\n/)
|
|
59
|
+
// Drop a single trailing empty element produced by a final newline so we can
|
|
60
|
+
// reason about the file's logical lines; we re-add a final newline on output.
|
|
61
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop()
|
|
62
|
+
let first = -1
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
if (isSlotLine(lines[i])) { first = i; break }
|
|
65
|
+
}
|
|
66
|
+
if (first === -1) return { header: lines, slots: [] }
|
|
67
|
+
return { header: lines.slice(0, first), slots: lines.slice(first) }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function refreshComment(now, scoresGeneratedAt) {
|
|
71
|
+
const when = now instanceof Date ? now.toISOString() : String(now)
|
|
72
|
+
const gen = scoresGeneratedAt ? ` (scores generated ${scoresGeneratedAt})` : ""
|
|
73
|
+
return `# Refreshed ${when} from model-scores.json${gen}: opencode-go free models first, best opencode last.`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function withRefreshComment(headerLines, comment) {
|
|
77
|
+
const kept = headerLines.filter((line) => !/^#\s*Refreshed\b/.test(String(line)))
|
|
78
|
+
return [...kept, comment]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatGeneratedLine(model, weight, bucket) {
|
|
82
|
+
const parts = [String(model), String(weight)]
|
|
83
|
+
if (bucket) parts.push(`b:${bucket}`)
|
|
84
|
+
return parts.join(" ")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pure builder. Throws on every fail-closed condition; never touches the filesystem.
|
|
89
|
+
*
|
|
90
|
+
* @param {{ scores: any, currentPoolText: string, now?: Date|string }} input
|
|
91
|
+
* @returns {{ text: string, slots: string[], warnings: string[], chosen: { opencodeGo: string[], opencode: string, preserved: string[] } }}
|
|
92
|
+
*/
|
|
93
|
+
export function buildPool(input) {
|
|
94
|
+
const warnings = []
|
|
95
|
+
const scores = input?.scores
|
|
96
|
+
if (!scores || typeof scores !== "object" || !Array.isArray(scores.providers)) {
|
|
97
|
+
throw new Error("score file is missing or invalid (expected { providers: [...] })")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const providerEntries = scores.providers
|
|
101
|
+
const scoredProviders = new Set(
|
|
102
|
+
providerEntries.map((p) => String(p?.provider ?? "").trim()).filter(Boolean),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const goEntry = providerEntries.find((p) => p?.provider === "opencode-go")
|
|
106
|
+
const goRows = okFreeRows(goEntry).sort((a, b) => b.score - a.score)
|
|
107
|
+
if (goRows.length === 0) {
|
|
108
|
+
throw new Error("no free opencode-go model with ok:true and a numeric score is available; refusing to write pool.txt")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const opEntry = providerEntries.find((p) => p?.provider === "opencode")
|
|
112
|
+
const opRows = okFreeRows(opEntry).sort((a, b) => b.score - a.score)
|
|
113
|
+
if (opRows.length === 0) {
|
|
114
|
+
throw new Error("no free opencode model with ok:true and a numeric score is available; refusing to write pool.txt")
|
|
115
|
+
}
|
|
116
|
+
const bestOp = opRows[0].model
|
|
117
|
+
|
|
118
|
+
const { header, slots: currentSlots } = splitHeaderAndSlots(input?.currentPoolText ?? "")
|
|
119
|
+
const preserved = currentSlots.filter((line) => {
|
|
120
|
+
if (!isSlotLine(line)) return false
|
|
121
|
+
return !scoredProviders.has(providerOf(line))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const block1 = goRows.map((row) => formatGeneratedLine(row.model, 4))
|
|
125
|
+
const block3 = [formatGeneratedLine(bestOp, 2, "opencode")]
|
|
126
|
+
const outSlots = [...block1, ...preserved, ...block3]
|
|
127
|
+
|
|
128
|
+
const comment = refreshComment(input?.now ?? new Date(), scores.generatedAt)
|
|
129
|
+
const outHeader = withRefreshComment(header, comment)
|
|
130
|
+
|
|
131
|
+
const text = [...outHeader, "", ...outSlots, ""].join("\n")
|
|
132
|
+
return {
|
|
133
|
+
text,
|
|
134
|
+
slots: outSlots,
|
|
135
|
+
warnings,
|
|
136
|
+
chosen: {
|
|
137
|
+
opencodeGo: goRows.map((r) => r.model),
|
|
138
|
+
opencode: bestOp,
|
|
139
|
+
preserved: preserved.map((l) => String(l).split("#")[0].trim().split(/\s+/)[0]),
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readJsonFile(path) {
|
|
145
|
+
if (!existsSync(path)) throw new Error(`file not found: ${path}`)
|
|
146
|
+
const raw = readFileSync(path, "utf8")
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(raw)
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new Error(`invalid JSON in ${path}: ${String(error?.message ?? error)}`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readTextFile(path) {
|
|
155
|
+
if (!existsSync(path)) return ""
|
|
156
|
+
return readFileSync(path, "utf8")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function atomicWriteSimple(path, text) {
|
|
160
|
+
const dir = dirname(path)
|
|
161
|
+
const tmp = join(dir, `.pool.txt.tmp-${process.pid}-${Date.now()}`)
|
|
162
|
+
writeFileSync(tmp, text, "utf8")
|
|
163
|
+
renameSync(tmp, path)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function runScorer(max) {
|
|
167
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
168
|
+
const scorer = resolve(here, "score-models.mjs")
|
|
169
|
+
if (!existsSync(scorer)) throw new Error(`scorer not found: ${scorer}`)
|
|
170
|
+
const args = [scorer, "--max", String(max)]
|
|
171
|
+
execFileSync(process.execPath, args, { stdio: "inherit" })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function renderTimerUnits(input = {}) {
|
|
175
|
+
const nodePath = input.nodePath || process.execPath
|
|
176
|
+
const scriptPath = input.scriptPath || fileURLToPath(import.meta.url)
|
|
177
|
+
const home = input.home || HOME
|
|
178
|
+
const servicePath = join(SYSTEMD_USER_DIR(home), REFRESH_SERVICE_NAME)
|
|
179
|
+
const timerPath = join(SYSTEMD_USER_DIR(home), REFRESH_TIMER_NAME)
|
|
180
|
+
const service = `[Unit]
|
|
181
|
+
Description=Refresh the LaCode model pool from free-model scores
|
|
182
|
+
After=network-online.target
|
|
183
|
+
Wants=network-online.target
|
|
184
|
+
|
|
185
|
+
[Service]
|
|
186
|
+
Type=oneshot
|
|
187
|
+
ExecStart=${nodePath} ${scriptPath}
|
|
188
|
+
Environment=PATH=${home}/.opencode/bin:/usr/local/bin:/usr/bin:/bin
|
|
189
|
+
Nice=10
|
|
190
|
+
`
|
|
191
|
+
const timer = `[Unit]
|
|
192
|
+
Description=Daily LaCode model pool refresh
|
|
193
|
+
|
|
194
|
+
[Timer]
|
|
195
|
+
OnCalendar=daily
|
|
196
|
+
RandomizedDelaySec=2h
|
|
197
|
+
Persistent=true
|
|
198
|
+
Unit=${REFRESH_SERVICE_NAME}
|
|
199
|
+
|
|
200
|
+
[Install]
|
|
201
|
+
WantedBy=timers.target
|
|
202
|
+
`
|
|
203
|
+
return { service, timer, servicePath, timerPath }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function installTimerUnits() {
|
|
207
|
+
const units = renderTimerUnits({
|
|
208
|
+
nodePath: process.execPath,
|
|
209
|
+
scriptPath: fileURLToPath(import.meta.url),
|
|
210
|
+
home: HOME,
|
|
211
|
+
})
|
|
212
|
+
mkdirSync(dirname(units.servicePath), { recursive: true })
|
|
213
|
+
writeFileSync(units.servicePath, units.service, "utf8")
|
|
214
|
+
writeFileSync(units.timerPath, units.timer, "utf8")
|
|
215
|
+
try {
|
|
216
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" })
|
|
217
|
+
execFileSync("systemctl", ["--user", "enable", "--now", REFRESH_TIMER_NAME], { stdio: "inherit" })
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(`failed to install systemd user timer: ${String(error?.message ?? error)}`)
|
|
220
|
+
console.error(`unit files written:`)
|
|
221
|
+
console.error(` ${units.servicePath}`)
|
|
222
|
+
console.error(` ${units.timerPath}`)
|
|
223
|
+
console.error(`manual commands:`)
|
|
224
|
+
console.error(` systemctl --user daemon-reload`)
|
|
225
|
+
console.error(` systemctl --user enable --now ${REFRESH_TIMER_NAME}`)
|
|
226
|
+
process.exitCode = 1
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
console.log(`installed ${units.servicePath}`)
|
|
230
|
+
console.log(`installed ${units.timerPath}`)
|
|
231
|
+
console.log(`enabled ${REFRESH_TIMER_NAME}`)
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function usage() {
|
|
236
|
+
console.log(`Usage: node scripts/refresh-pool.mjs [options]
|
|
237
|
+
|
|
238
|
+
--from-scores skip scoring, use the existing ~/.config/lacode/model-scores.json
|
|
239
|
+
--install-timer install and enable a user systemd oneshot service + daily timer
|
|
240
|
+
--print print the proposed pool.txt and make no changes (alias: --dry-run)
|
|
241
|
+
--dry-run same as --print
|
|
242
|
+
--max N models per provider passed to the scorer (default: 20)
|
|
243
|
+
--scores PATH scores file (default: ${DEFAULT_SCORES_PATH})
|
|
244
|
+
--pool PATH pool file to read/compare/write (default: ${DEFAULT_POOL_PATH})
|
|
245
|
+
-h, --help this message`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function main() {
|
|
249
|
+
const argv = process.argv.slice(2)
|
|
250
|
+
if (argv.includes("--help") || argv.includes("-h")) { usage(); return }
|
|
251
|
+
if (argv.includes("--install-timer")) {
|
|
252
|
+
installTimerUnits()
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
const fromScores = argv.includes("--from-scores")
|
|
256
|
+
const printOnly = argv.includes("--print") || argv.includes("--dry-run")
|
|
257
|
+
const value = (flag, fallback) => {
|
|
258
|
+
const hit = argv.find((a) => a === flag || a.startsWith(`${flag}=`))
|
|
259
|
+
if (!hit) return fallback
|
|
260
|
+
return hit.includes("=") ? hit.slice(hit.indexOf("=") + 1) : argv[argv.indexOf(hit) + 1]
|
|
261
|
+
}
|
|
262
|
+
const max = Math.max(1, Number(value("--max", "20")) || 20)
|
|
263
|
+
const scoresPath = value("--scores", DEFAULT_SCORES_PATH)
|
|
264
|
+
const poolPath = value("--pool", DEFAULT_POOL_PATH)
|
|
265
|
+
|
|
266
|
+
if (!fromScores) {
|
|
267
|
+
try {
|
|
268
|
+
runScorer(max)
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error(`scorer failed; pool.txt not written: ${String(error?.message ?? error)}`)
|
|
271
|
+
process.exitCode = 1
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let scores
|
|
277
|
+
try {
|
|
278
|
+
scores = readJsonFile(scoresPath)
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error(String(error?.message ?? error))
|
|
281
|
+
process.exitCode = 1
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const currentPoolText = readTextFile(poolPath)
|
|
286
|
+
let built
|
|
287
|
+
try {
|
|
288
|
+
built = buildPool({ scores, currentPoolText, now: new Date() })
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error(`refresh refused (fail closed): ${String(error?.message ?? error)}`)
|
|
291
|
+
process.exitCode = 1
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const oldSlotCount = splitHeaderAndSlots(currentPoolText).slots.filter(isSlotLine).length
|
|
296
|
+
const newSlotCount = built.slots.filter(isSlotLine).length
|
|
297
|
+
const summary = [
|
|
298
|
+
`opencode-go [${built.chosen.opencodeGo.join(", ")}]`,
|
|
299
|
+
`preserved-unscored [${built.chosen.preserved.join(", ")}]`,
|
|
300
|
+
`opencode-last [${built.chosen.opencode}]`,
|
|
301
|
+
].join(" | ")
|
|
302
|
+
|
|
303
|
+
if (printOnly) {
|
|
304
|
+
process.stdout.write(built.text)
|
|
305
|
+
console.error(`--print: proposed ${oldSlotCount} -> ${newSlotCount} slots; no changes written. ${summary}`)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (existsSync(poolPath)) {
|
|
310
|
+
try {
|
|
311
|
+
const existing = readFileSync(poolPath, "utf8")
|
|
312
|
+
if (existing === built.text) {
|
|
313
|
+
console.log(`unchanged: ${poolPath} (${newSlotCount} slots). ${summary}`)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
} catch {}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
atomicWriteSimple(poolPath, built.text)
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error(`failed to write ${poolPath}: ${String(error?.message ?? error)}`)
|
|
323
|
+
process.exitCode = 1
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
console.log(`wrote ${poolPath}: ${oldSlotCount} -> ${newSlotCount} slots. ${summary}`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
330
|
+
if (isMain) {
|
|
331
|
+
main().catch((error) => {
|
|
332
|
+
console.error(String(error?.stack ?? error?.message ?? error))
|
|
333
|
+
process.exitCode = 1
|
|
334
|
+
})
|
|
335
|
+
}
|