armin-opencode 0.1.2 → 0.1.4
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 +3 -0
- package/bin/armin.js +87 -1
- package/package.json +1 -1
- package/plugins/armin.ts +46 -0
package/README.md
CHANGED
|
@@ -8,6 +8,9 @@ npx armin-opencode install # or: npm i -g armin-opencode && armin install
|
|
|
8
8
|
export ARMIN_ENABLED=1
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Prefer a global install over one-off `npx` runs: the plugin registration
|
|
12
|
+
points at the package's install location, and `npx` caches are evicted.
|
|
13
|
+
|
|
11
14
|
`armin install` downloads the `armin-engine` sidecar binary for your platform
|
|
12
15
|
from the [GitHub releases](https://github.com/bastian-seifert/armin/releases)
|
|
13
16
|
and registers the plugin in your global opencode config. Enable it per
|
package/bin/armin.js
CHANGED
|
@@ -94,6 +94,44 @@ function buildFromSource() {
|
|
|
94
94
|
return true;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
let pendingTypesafeKey = null;
|
|
98
|
+
let pendingOpenrouterKey = null;
|
|
99
|
+
let pendingJevProvider = null; // "openrouter" | "skip" | null (typesafe needs no config entry)
|
|
100
|
+
|
|
101
|
+
/** Ask which relay serves Jev and for the matching key (TTY only; values
|
|
102
|
+
* go into the opencode config — the plugin passes them to the sidecar).
|
|
103
|
+
* A provider detected from the environment skips the prompt entirely. */
|
|
104
|
+
async function askForExtractionSetup() {
|
|
105
|
+
if (!process.stdin.isTTY) return;
|
|
106
|
+
let provider;
|
|
107
|
+
if (process.env.ARMIN_JEV_PROVIDER) provider = process.env.ARMIN_JEV_PROVIDER;
|
|
108
|
+
else if (process.env.TYPESAFE_AI_API_KEY) provider = "typesafe";
|
|
109
|
+
else if (process.env.OPENROUTER_API_KEY) provider = "openrouter";
|
|
110
|
+
if (provider) return;
|
|
111
|
+
|
|
112
|
+
const readline = require("readline");
|
|
113
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
114
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
115
|
+
try {
|
|
116
|
+
console.log("\nExtraction backend (jev is the default — verbatim nodes, no generative LLM):");
|
|
117
|
+
console.log(" 1) TypeSafe System One (typesafe.ai) [default]");
|
|
118
|
+
console.log(" 2) Jev via OpenRouter (openrouter.ai — same API, OpenRouter billing)");
|
|
119
|
+
console.log(" 3) skip (LLM fallback / deterministic-only)");
|
|
120
|
+
const choice = ((await ask("Choose [1/2/3, Enter=1]: ")) || "1").trim();
|
|
121
|
+
if (choice === "3") { pendingJevProvider = "skip"; return; }
|
|
122
|
+
if (choice === "2") {
|
|
123
|
+
pendingJevProvider = "openrouter";
|
|
124
|
+
const key = ((await ask("OpenRouter API key (openrouter.ai/settings/keys) — paste, or Enter to skip: ")) || "").trim();
|
|
125
|
+
if (key) pendingOpenrouterKey = key;
|
|
126
|
+
} else {
|
|
127
|
+
const key = ((await ask("TypeSafe System One API key — paste, or Enter to skip: ")) || "").trim();
|
|
128
|
+
if (key) pendingTypesafeKey = key;
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
rl.close();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
97
135
|
function registerPlugin() {
|
|
98
136
|
const plugin = path.join(PKG_ROOT, "plugins", "armin.ts");
|
|
99
137
|
if (!fs.existsSync(plugin)) throw new Error(`plugin not found at ${plugin}`);
|
|
@@ -115,10 +153,29 @@ function registerPlugin() {
|
|
|
115
153
|
}
|
|
116
154
|
}
|
|
117
155
|
const entry = "file://" + plugin;
|
|
156
|
+
let changed = false;
|
|
118
157
|
const plugins = Array.isArray(data.plugin) ? data.plugin : [];
|
|
119
158
|
if (!plugins.includes(entry)) {
|
|
120
159
|
plugins.push(entry);
|
|
121
160
|
data.plugin = plugins;
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
if (pendingTypesafeKey && !process.env.TYPESAFE_AI_API_KEY) {
|
|
164
|
+
data.armin = { ...(data.armin || {}), typesafeKey: pendingTypesafeKey };
|
|
165
|
+
changed = true;
|
|
166
|
+
}
|
|
167
|
+
if (pendingOpenrouterKey && !process.env.OPENROUTER_API_KEY) {
|
|
168
|
+
data.armin = { ...(data.armin || {}), openrouterKey: pendingOpenrouterKey };
|
|
169
|
+
changed = true;
|
|
170
|
+
}
|
|
171
|
+
// Persist the relay choice only when the user explicitly picked
|
|
172
|
+
// OpenRouter in the prompt (env-detected providers drive the sidecar
|
|
173
|
+
// via their env vars; writing nothing avoids overriding that later).
|
|
174
|
+
if (pendingJevProvider === "openrouter" && data.armin.jevProvider !== "openrouter") {
|
|
175
|
+
data.armin = { ...(data.armin || {}), jevProvider: "openrouter" };
|
|
176
|
+
changed = true;
|
|
177
|
+
}
|
|
178
|
+
if (changed) {
|
|
122
179
|
fs.mkdirSync(cfgDir, { recursive: true });
|
|
123
180
|
fs.writeFileSync(cfgPath, JSON.stringify(data, null, 2));
|
|
124
181
|
}
|
|
@@ -144,7 +201,31 @@ async function install() {
|
|
|
144
201
|
}
|
|
145
202
|
}
|
|
146
203
|
}
|
|
204
|
+
await askForExtractionSetup();
|
|
147
205
|
registerPlugin();
|
|
206
|
+
const tsKey = process.env.TYPESAFE_AI_API_KEY || pendingTypesafeKey;
|
|
207
|
+
const orKey = process.env.OPENROUTER_API_KEY || pendingOpenrouterKey;
|
|
208
|
+
const llmKey = process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
|
|
209
|
+
if (tsKey) {
|
|
210
|
+
console.log("extraction: jev (TypeSafe System One) — ready");
|
|
211
|
+
} else if (orKey) {
|
|
212
|
+
console.log("extraction: jev via OpenRouter — ready");
|
|
213
|
+
} else if (llmKey) {
|
|
214
|
+
console.log(
|
|
215
|
+
"extraction: LLM fallback — jev is the default but no key for it is set.\n" +
|
|
216
|
+
" Set TYPESAFE_AI_API_KEY (typesafe.ai) or OPENROUTER_API_KEY (openrouter.ai),\n" +
|
|
217
|
+
' or put "armin": { "typesafeKey": "..." } / { "jevProvider": "openrouter",\n' +
|
|
218
|
+
' "openrouterKey": "..." } in your opencode config.',
|
|
219
|
+
);
|
|
220
|
+
} else {
|
|
221
|
+
console.log(
|
|
222
|
+
"extraction: NONE (deterministic-only) — import, capture and unverified-edit\n" +
|
|
223
|
+
" warnings work; prose extraction does not.\n" +
|
|
224
|
+
" Set TYPESAFE_AI_API_KEY (typesafe.ai) or OPENROUTER_API_KEY (openrouter.ai)\n" +
|
|
225
|
+
" for jev extraction — the default — or ANTHROPIC_API_KEY / OPENAI_API_KEY\n" +
|
|
226
|
+
" for LLM fallback.",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
148
229
|
console.log(`
|
|
149
230
|
── done ────────────────────────────────────────────────────────
|
|
150
231
|
Enable ARMIN per environment:
|
|
@@ -152,7 +233,12 @@ Enable ARMIN per environment:
|
|
|
152
233
|
export ARMIN_ENABLED=1
|
|
153
234
|
|
|
154
235
|
Optional:
|
|
155
|
-
ARMIN_EXTRACTION_MODE=jev
|
|
236
|
+
ARMIN_EXTRACTION_MODE=jev System One extraction (default; needs
|
|
237
|
+
TYPESAFE_AI_API_KEY or OPENROUTER_API_KEY)
|
|
238
|
+
ARMIN_JEV_PROVIDER=openrouter route jev via OpenRouter (auto-detected
|
|
239
|
+
when only that key is set)
|
|
240
|
+
ARMIN_JEV_MODEL=<model> jev model override (default jev-1.13.0,
|
|
241
|
+
or jev-latest via OpenRouter)
|
|
156
242
|
ARMIN_MODEL=<model> extraction model override
|
|
157
243
|
ARMIN_DEBUG=1 verbose logging + /ui URL
|
|
158
244
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "armin-opencode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* "enabled": true,
|
|
17
17
|
* "provider": "anthropic", // anthropic | openai (default: infer from keys)
|
|
18
18
|
* "apiKey": "sk-...", // sent to the sidecar only; env key wins if both set
|
|
19
|
+
* "typesafeKey": "...", // Typesafe System One key (jev is the
|
|
20
|
+
* // default extraction mode); env wins
|
|
21
|
+
* "jevProvider": "typesafe", // typesafe | openrouter — which relay serves
|
|
22
|
+
* // the System One API (openrouter bills
|
|
23
|
+
* // your OpenRouter account); env wins
|
|
24
|
+
* "openrouterKey": "sk-or-...", // OpenRouter key, used when jevProvider is
|
|
25
|
+
* // "openrouter"; env wins
|
|
19
26
|
* "model": "session", // "session" = follow the live session model
|
|
20
27
|
* // "small" = reuse opencode's small_model setting
|
|
21
28
|
* // any string = fixed extraction model
|
|
@@ -490,6 +497,16 @@ const ArminPlugin: Plugin = async (ctx) => {
|
|
|
490
497
|
if (provider === "anthropic" || provider === "openai") {
|
|
491
498
|
spawnEnv.LLM_PROVIDER = provider
|
|
492
499
|
}
|
|
500
|
+
// Typesafe key (jev is the default extraction mode): config value is
|
|
501
|
+
// passed to the sidecar only; a key already in the environment wins.
|
|
502
|
+
const typesafeKey = cfgStr("typesafeKey", "TYPESAFE_AI_API_KEY")
|
|
503
|
+
if (typesafeKey) spawnEnv.TYPESAFE_AI_API_KEY = typesafeKey
|
|
504
|
+
// Jev relay: "typesafe" (api.typesafe.ai, default) or "openrouter"
|
|
505
|
+
// (openrouter.ai/api — same System One API, OpenRouter billing).
|
|
506
|
+
const jevProvider = cfgStr("jevProvider", "ARMIN_JEV_PROVIDER")
|
|
507
|
+
if (jevProvider) spawnEnv.ARMIN_JEV_PROVIDER = jevProvider
|
|
508
|
+
const openrouterKey = cfgStr("openrouterKey", "OPENROUTER_API_KEY")
|
|
509
|
+
if (openrouterKey) spawnEnv.OPENROUTER_API_KEY = openrouterKey
|
|
493
510
|
// API key: config value is passed to the sidecar only; a key already in
|
|
494
511
|
// the environment wins (no override).
|
|
495
512
|
if (typeof armin.apiKey === "string" && armin.apiKey) {
|
|
@@ -519,6 +536,35 @@ const ArminPlugin: Plugin = async (ctx) => {
|
|
|
519
536
|
if (sidecar.port) {
|
|
520
537
|
log(`reasoning-state UI: http://127.0.0.1:${sidecar.port}/ui`)
|
|
521
538
|
}
|
|
539
|
+
// Surface the effective extraction mode once — this is the difference
|
|
540
|
+
// between "memory works" and a silent deterministic downgrade.
|
|
541
|
+
void (async () => {
|
|
542
|
+
try {
|
|
543
|
+
const health = await api.get<{ extraction: string; jev_provider?: string }>("/health")
|
|
544
|
+
const mode = health?.extraction ?? "unknown"
|
|
545
|
+
if (mode === "jev") {
|
|
546
|
+
const via = health?.jev_provider ?? "typesafe"
|
|
547
|
+
log(`extraction: jev (System One via ${via}) — ready`)
|
|
548
|
+
} else if (mode === "llm") {
|
|
549
|
+
log(
|
|
550
|
+
`extraction: llm (fallback) — jev is the default but no key is ` +
|
|
551
|
+
`configured. Set TYPESAFE_AI_API_KEY (typesafe.ai) or OPENROUTER_API_KEY ` +
|
|
552
|
+
`(openrouter.ai) in your environment, or \`"armin": { "typesafeKey": "..." }\`` +
|
|
553
|
+
` / \`{ "jevProvider": "openrouter", "openrouterKey": "..." }\` in your ` +
|
|
554
|
+
`opencode config.`,
|
|
555
|
+
)
|
|
556
|
+
} else {
|
|
557
|
+
log(
|
|
558
|
+
`extraction: deterministic-only — no API keys found. Import and ` +
|
|
559
|
+
`unverified-edit warnings work; prose extraction does not. Set ` +
|
|
560
|
+
`TYPESAFE_AI_API_KEY or OPENROUTER_API_KEY (or ANTHROPIC/OPENAI_API_KEY) ` +
|
|
561
|
+
`to enable it.`,
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
// best-effort status only
|
|
566
|
+
}
|
|
567
|
+
})()
|
|
522
568
|
|
|
523
569
|
// ── Cold start: import AGENTS.md / CLAUDE.md into an empty graph ──────
|
|
524
570
|
// Deterministic parse on the engine side (content-hash IDs, idempotent).
|