armin-opencode 0.1.5 → 0.1.6
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/bin/armin.js +32 -0
- package/package.json +1 -1
- package/plugins/armin.ts +35 -5
package/bin/armin.js
CHANGED
|
@@ -97,6 +97,7 @@ function buildFromSource() {
|
|
|
97
97
|
let pendingTypesafeKey = null;
|
|
98
98
|
let pendingOpenrouterKey = null;
|
|
99
99
|
let pendingJevProvider = null; // "openrouter" | "skip" | null (typesafe needs no config entry)
|
|
100
|
+
let pendingPort = null; // fixed engine port, or null for OS auto-assign
|
|
100
101
|
|
|
101
102
|
/** Ask which relay serves Jev and for the matching key (TTY only; values
|
|
102
103
|
* go into the opencode config — the plugin passes them to the sidecar).
|
|
@@ -132,6 +133,30 @@ async function askForExtractionSetup() {
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
/** Ask for an optional fixed engine port (TTY only; ARMIN_PORT in the
|
|
137
|
+
* environment is the runtime escape hatch and skips the prompt). A pinned
|
|
138
|
+
* port is written to the opencode config; the sidecar auto-falls back to a
|
|
139
|
+
* free port when it is busy. */
|
|
140
|
+
async function askForPort() {
|
|
141
|
+
if (!process.stdin.isTTY) return;
|
|
142
|
+
if (process.env.ARMIN_PORT) return;
|
|
143
|
+
const readline = require("readline");
|
|
144
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
145
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
146
|
+
try {
|
|
147
|
+
const answer = ((await ask("\nFixed engine port (Enter = auto-assign per session): ")) || "").trim();
|
|
148
|
+
if (!answer) return;
|
|
149
|
+
const port = Number(answer);
|
|
150
|
+
if (Number.isInteger(port) && port >= 1024 && port <= 65535) {
|
|
151
|
+
pendingPort = port;
|
|
152
|
+
} else {
|
|
153
|
+
console.log(`invalid port "${answer}" (need 1024-65535) — engine port: auto-assign`);
|
|
154
|
+
}
|
|
155
|
+
} finally {
|
|
156
|
+
rl.close();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
135
160
|
function registerPlugin() {
|
|
136
161
|
const plugin = path.join(PKG_ROOT, "plugins", "armin.ts");
|
|
137
162
|
if (!fs.existsSync(plugin)) throw new Error(`plugin not found at ${plugin}`);
|
|
@@ -181,6 +206,10 @@ function registerPlugin() {
|
|
|
181
206
|
data.armin = { ...(data.armin || {}), jevProvider: "openrouter" };
|
|
182
207
|
changed = true;
|
|
183
208
|
}
|
|
209
|
+
if (pendingPort && data.armin.port !== pendingPort) {
|
|
210
|
+
data.armin = { ...(data.armin || {}), port: pendingPort };
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
184
213
|
if (changed) {
|
|
185
214
|
fs.mkdirSync(cfgDir, { recursive: true });
|
|
186
215
|
fs.writeFileSync(cfgPath, JSON.stringify(data, null, 2));
|
|
@@ -209,6 +238,7 @@ async function install() {
|
|
|
209
238
|
}
|
|
210
239
|
}
|
|
211
240
|
await askForExtractionSetup();
|
|
241
|
+
await askForPort();
|
|
212
242
|
const registered = registerPlugin();
|
|
213
243
|
const tsKey = process.env.TYPESAFE_AI_API_KEY || pendingTypesafeKey;
|
|
214
244
|
const orKey = process.env.OPENROUTER_API_KEY || pendingOpenrouterKey;
|
|
@@ -241,6 +271,8 @@ ${registered
|
|
|
241
271
|
|
|
242
272
|
Optional overrides (env wins over config):
|
|
243
273
|
ARMIN_ENABLED=1 force-enable without the config entry
|
|
274
|
+
ARMIN_PORT=<port> fixed engine port (default: OS auto-assign;
|
|
275
|
+
a busy pinned port auto-falls back)
|
|
244
276
|
ARMIN_EXTRACTION_MODE=jev System One extraction (default; needs
|
|
245
277
|
TYPESAFE_AI_API_KEY or OPENROUTER_API_KEY)
|
|
246
278
|
ARMIN_JEV_PROVIDER=openrouter route jev via OpenRouter (auto-detected
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "armin-opencode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "ARMIN — queryable decision memory for AI coding agents. Installs the armin-engine sidecar and registers the opencode plugin.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Bastian Seifert",
|
package/plugins/armin.ts
CHANGED
|
@@ -31,12 +31,15 @@
|
|
|
31
31
|
* "dbDir": "~/.opencode/armin" // per-project graph databases
|
|
32
32
|
* // (keyed by git origin remote; path
|
|
33
33
|
* // fallback for non-git dirs)
|
|
34
|
+
* "port": 4545, // optional fixed engine port (default:
|
|
35
|
+
* // OS auto-assign; auto-falls back when
|
|
36
|
+
* // the port is busy); env wins
|
|
34
37
|
* }
|
|
35
38
|
* }
|
|
36
39
|
*
|
|
37
40
|
* Precedence: engine defaults < config files (global < project) < env vars
|
|
38
41
|
* (ARMIN_MODEL, ARMIN_BATCH_MS, ARMIN_BATCH_EVENTS, ARMIN_DB_DIR,
|
|
39
|
-
* ARMIN_ENGINE_BIN, ARMIN_DEBUG) — env is the escape hatch.
|
|
42
|
+
* ARMIN_PORT, ARMIN_ENGINE_BIN, ARMIN_DEBUG) — env is the escape hatch.
|
|
40
43
|
*
|
|
41
44
|
* Note: opencode's auth store (OAuth logins) is not exposed to plugins, so
|
|
42
45
|
* extraction uses `apiKey`/environment API keys; the session *model* can
|
|
@@ -207,6 +210,8 @@ class Sidecar {
|
|
|
207
210
|
private readonly _token = crypto.randomUUID().replace(/-/g, "")
|
|
208
211
|
private starts = 0
|
|
209
212
|
private disposed = false
|
|
213
|
+
/** Set after a pinned port failed to start; later starts auto-assign. */
|
|
214
|
+
private pinnedPortFailed = false
|
|
210
215
|
port: number | null = null
|
|
211
216
|
|
|
212
217
|
get token(): string {
|
|
@@ -217,6 +222,7 @@ class Sidecar {
|
|
|
217
222
|
private engineBin: string,
|
|
218
223
|
private dbFile: string,
|
|
219
224
|
private spawnEnv: Record<string, string> = {},
|
|
225
|
+
private pinnedPort?: number,
|
|
220
226
|
) {}
|
|
221
227
|
|
|
222
228
|
/** Start the engine and wait for the port handshake + health check. */
|
|
@@ -229,9 +235,12 @@ class Sidecar {
|
|
|
229
235
|
}
|
|
230
236
|
this.starts++
|
|
231
237
|
|
|
238
|
+
// With a pinned port (armin.port / ARMIN_PORT) the engine binds that
|
|
239
|
+
// exact port; once it has failed once, later starts auto-assign again.
|
|
240
|
+
const usePinned = this.pinnedPort !== undefined && !this.pinnedPortFailed
|
|
232
241
|
const args = [
|
|
233
242
|
this.engineBin,
|
|
234
|
-
"--port", "0",
|
|
243
|
+
"--port", usePinned ? String(this.pinnedPort) : "0",
|
|
235
244
|
"--db-path", this.dbFile,
|
|
236
245
|
"--auth-token", this.token,
|
|
237
246
|
]
|
|
@@ -263,15 +272,22 @@ class Sidecar {
|
|
|
263
272
|
|
|
264
273
|
const port = await this.readPort(proc, 5_000)
|
|
265
274
|
if (!port) {
|
|
266
|
-
log("sidecar did not report ARMIN_PORT in time")
|
|
267
275
|
proc.kill()
|
|
276
|
+
if (usePinned) {
|
|
277
|
+
// The engine exited without a handshake — the pinned port is
|
|
278
|
+
// almost certainly busy. Fall back to OS auto-assign and retry.
|
|
279
|
+
log(`sidecar could not bind pinned port ${this.pinnedPort}; falling back to auto-assign`)
|
|
280
|
+
this.pinnedPortFailed = true
|
|
281
|
+
return this.start()
|
|
282
|
+
}
|
|
283
|
+
log("sidecar did not report ARMIN_PORT in time")
|
|
268
284
|
return false
|
|
269
285
|
}
|
|
270
286
|
this.port = port
|
|
271
287
|
|
|
272
288
|
for (let i = 0; i < 50; i++) {
|
|
273
289
|
if (await this.healthy()) {
|
|
274
|
-
log(`sidecar ready on 127.0.0.1:${port}`)
|
|
290
|
+
log(`sidecar ready on 127.0.0.1:${port}${usePinned ? " (pinned)" : ""}`)
|
|
275
291
|
return true
|
|
276
292
|
}
|
|
277
293
|
if (this.disposed) return false
|
|
@@ -321,6 +337,7 @@ class Sidecar {
|
|
|
321
337
|
break
|
|
322
338
|
}
|
|
323
339
|
if (!chunk) break
|
|
340
|
+
if (chunk.done) break // engine exited (e.g. bind failure) — stop waiting
|
|
324
341
|
buf += new TextDecoder().decode(chunk.value ?? new Uint8Array())
|
|
325
342
|
const m = buf.match(/ARMIN_PORT=(\d+)/)
|
|
326
343
|
if (m) {
|
|
@@ -523,7 +540,20 @@ const ArminPlugin: Plugin = async (ctx) => {
|
|
|
523
540
|
(typeof armin.batchEvents === "number" ? String(armin.batchEvents) : undefined)
|
|
524
541
|
if (batchEvents) spawnEnv.ARMIN_BATCH_EVENTS = batchEvents
|
|
525
542
|
|
|
526
|
-
|
|
543
|
+
// Optional fixed engine port: ARMIN_PORT env > armin.port config. Invalid
|
|
544
|
+
// values (non-integer, 0, out of range) are ignored → OS auto-assign; a
|
|
545
|
+
// busy pinned port falls back to auto-assign when the sidecar starts.
|
|
546
|
+
const portEnv = Number(process.env.ARMIN_PORT)
|
|
547
|
+
const portCfg = typeof armin.port === "number" ? armin.port : Number.NaN
|
|
548
|
+
const pinnedPort =
|
|
549
|
+
Number.isInteger(portEnv) && portEnv >= 1 && portEnv <= 65535
|
|
550
|
+
? portEnv
|
|
551
|
+
: Number.isInteger(portCfg) && portCfg >= 1 && portCfg <= 65535
|
|
552
|
+
? portCfg
|
|
553
|
+
: undefined
|
|
554
|
+
if (pinnedPort) log("fixed engine port:", pinnedPort)
|
|
555
|
+
|
|
556
|
+
const sidecar = new Sidecar(engineBin, dbFile, spawnEnv, pinnedPort)
|
|
527
557
|
const started = await sidecar.start()
|
|
528
558
|
if (!started) {
|
|
529
559
|
log("sidecar unavailable — plugin inert")
|