armin-opencode 0.1.3 → 0.1.5
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 -3
- package/bin/armin.js +75 -30
- package/package.json +1 -1
- package/plugins/armin.ts +21 -6
package/README.md
CHANGED
|
@@ -5,7 +5,6 @@ for AI coding agents — packaged for [opencode](https://opencode.ai).
|
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
7
|
npx armin-opencode install # or: npm i -g armin-opencode && armin install
|
|
8
|
-
export ARMIN_ENABLED=1
|
|
9
8
|
```
|
|
10
9
|
|
|
11
10
|
Prefer a global install over one-off `npx` runs: the plugin registration
|
|
@@ -13,8 +12,9 @@ points at the package's install location, and `npx` caches are evicted.
|
|
|
13
12
|
|
|
14
13
|
`armin install` downloads the `armin-engine` sidecar binary for your platform
|
|
15
14
|
from the [GitHub releases](https://github.com/bastian-seifert/armin/releases)
|
|
16
|
-
and registers the plugin in your global opencode config
|
|
17
|
-
|
|
15
|
+
and registers + enables the plugin in your global opencode config
|
|
16
|
+
(`"armin": { "enabled": true }`). `ARMIN_ENABLED=1` remains as a
|
|
17
|
+
per-environment escape hatch.
|
|
18
18
|
|
|
19
19
|
See the [main repo](https://github.com/bastian-seifert/armin) for what the
|
|
20
20
|
agent gets (durable decisions/rules/open items, scoped reasoning-state brief,
|
package/bin/armin.js
CHANGED
|
@@ -95,25 +95,41 @@ function buildFromSource() {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
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;
|
|
98
111
|
|
|
99
|
-
/** Ask for the Typesafe key when nothing is configured (TTY only; the key
|
|
100
|
-
* goes into the opencode config — the plugin passes it to the sidecar). */
|
|
101
|
-
function askForTypesafeKey() {
|
|
102
|
-
if (process.env.TYPESAFE_AI_API_KEY) return Promise.resolve();
|
|
103
|
-
if (!process.stdin.isTTY) return Promise.resolve();
|
|
104
112
|
const readline = require("readline");
|
|
105
113
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
function registerPlugin() {
|
|
@@ -132,8 +148,8 @@ function registerPlugin() {
|
|
|
132
148
|
try {
|
|
133
149
|
data = JSON.parse(text.replace(/^\s*\/\/.*$/gm, ""));
|
|
134
150
|
} catch {
|
|
135
|
-
console.log(`\nCould not parse ${cfgPath} —
|
|
136
|
-
return;
|
|
151
|
+
console.log(`\nCould not parse ${cfgPath} — set up manually:\n add "plugin": ["file://${plugin}"] and "armin": { "enabled": true } to your opencode config\n`);
|
|
152
|
+
return false;
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
155
|
const entry = "file://" + plugin;
|
|
@@ -144,15 +160,33 @@ function registerPlugin() {
|
|
|
144
160
|
data.plugin = plugins;
|
|
145
161
|
changed = true;
|
|
146
162
|
}
|
|
163
|
+
// `armin install` is an explicit opt-in — enable in config so no
|
|
164
|
+
// per-shell env var is needed (ARMIN_ENABLED stays as escape hatch).
|
|
165
|
+
if (data.armin?.enabled !== true) {
|
|
166
|
+
data.armin = { ...(data.armin || {}), enabled: true };
|
|
167
|
+
changed = true;
|
|
168
|
+
}
|
|
147
169
|
if (pendingTypesafeKey && !process.env.TYPESAFE_AI_API_KEY) {
|
|
148
170
|
data.armin = { ...(data.armin || {}), typesafeKey: pendingTypesafeKey };
|
|
149
171
|
changed = true;
|
|
150
172
|
}
|
|
173
|
+
if (pendingOpenrouterKey && !process.env.OPENROUTER_API_KEY) {
|
|
174
|
+
data.armin = { ...(data.armin || {}), openrouterKey: pendingOpenrouterKey };
|
|
175
|
+
changed = true;
|
|
176
|
+
}
|
|
177
|
+
// Persist the relay choice only when the user explicitly picked
|
|
178
|
+
// OpenRouter in the prompt (env-detected providers drive the sidecar
|
|
179
|
+
// via their env vars; writing nothing avoids overriding that later).
|
|
180
|
+
if (pendingJevProvider === "openrouter" && data.armin.jevProvider !== "openrouter") {
|
|
181
|
+
data.armin = { ...(data.armin || {}), jevProvider: "openrouter" };
|
|
182
|
+
changed = true;
|
|
183
|
+
}
|
|
151
184
|
if (changed) {
|
|
152
185
|
fs.mkdirSync(cfgDir, { recursive: true });
|
|
153
186
|
fs.writeFileSync(cfgPath, JSON.stringify(data, null, 2));
|
|
154
187
|
}
|
|
155
188
|
console.log(`plugin registered in ${cfgPath}`);
|
|
189
|
+
return true;
|
|
156
190
|
}
|
|
157
191
|
|
|
158
192
|
async function install() {
|
|
@@ -174,34 +208,45 @@ async function install() {
|
|
|
174
208
|
}
|
|
175
209
|
}
|
|
176
210
|
}
|
|
177
|
-
await
|
|
178
|
-
registerPlugin();
|
|
211
|
+
await askForExtractionSetup();
|
|
212
|
+
const registered = registerPlugin();
|
|
179
213
|
const tsKey = process.env.TYPESAFE_AI_API_KEY || pendingTypesafeKey;
|
|
214
|
+
const orKey = process.env.OPENROUTER_API_KEY || pendingOpenrouterKey;
|
|
180
215
|
const llmKey = process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
|
|
181
216
|
if (tsKey) {
|
|
182
217
|
console.log("extraction: jev (TypeSafe System One) — ready");
|
|
218
|
+
} else if (orKey) {
|
|
219
|
+
console.log("extraction: jev via OpenRouter — ready");
|
|
183
220
|
} else if (llmKey) {
|
|
184
221
|
console.log(
|
|
185
|
-
"extraction: LLM fallback — jev is the default but no
|
|
186
|
-
"
|
|
187
|
-
' "armin": { "typesafeKey": "..." }
|
|
222
|
+
"extraction: LLM fallback — jev is the default but no key for it is set.\n" +
|
|
223
|
+
" Set TYPESAFE_AI_API_KEY (typesafe.ai) or OPENROUTER_API_KEY (openrouter.ai),\n" +
|
|
224
|
+
' or put "armin": { "typesafeKey": "..." } / { "jevProvider": "openrouter",\n' +
|
|
225
|
+
' "openrouterKey": "..." } in your opencode config.',
|
|
188
226
|
);
|
|
189
227
|
} else {
|
|
190
228
|
console.log(
|
|
191
229
|
"extraction: NONE (deterministic-only) — import, capture and unverified-edit\n" +
|
|
192
230
|
" warnings work; prose extraction does not.\n" +
|
|
193
|
-
" Set TYPESAFE_AI_API_KEY (typesafe.ai)
|
|
194
|
-
" or ANTHROPIC_API_KEY / OPENAI_API_KEY
|
|
231
|
+
" Set TYPESAFE_AI_API_KEY (typesafe.ai) or OPENROUTER_API_KEY (openrouter.ai)\n" +
|
|
232
|
+
" for jev extraction — the default — or ANTHROPIC_API_KEY / OPENAI_API_KEY\n" +
|
|
233
|
+
" for LLM fallback.",
|
|
195
234
|
);
|
|
196
235
|
}
|
|
197
236
|
console.log(`
|
|
198
237
|
── done ────────────────────────────────────────────────────────
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
238
|
+
${registered
|
|
239
|
+
? 'ARMIN is enabled in your opencode config ("armin": { "enabled": true }).\nRestart opencode to activate it; remove "enabled" to turn it off.'
|
|
240
|
+
: "ARMIN is not enabled yet — follow the manual setup steps above."}
|
|
202
241
|
|
|
203
|
-
Optional:
|
|
204
|
-
|
|
242
|
+
Optional overrides (env wins over config):
|
|
243
|
+
ARMIN_ENABLED=1 force-enable without the config entry
|
|
244
|
+
ARMIN_EXTRACTION_MODE=jev System One extraction (default; needs
|
|
245
|
+
TYPESAFE_AI_API_KEY or OPENROUTER_API_KEY)
|
|
246
|
+
ARMIN_JEV_PROVIDER=openrouter route jev via OpenRouter (auto-detected
|
|
247
|
+
when only that key is set)
|
|
248
|
+
ARMIN_JEV_MODEL=<model> jev model override (default jev-1.13.0,
|
|
249
|
+
or jev-latest via OpenRouter)
|
|
205
250
|
ARMIN_MODEL=<model> extraction model override
|
|
206
251
|
ARMIN_DEBUG=1 verbose logging + /ui URL
|
|
207
252
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "armin-opencode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* "apiKey": "sk-...", // sent to the sidecar only; env key wins if both set
|
|
19
19
|
* "typesafeKey": "...", // Typesafe System One key (jev is the
|
|
20
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
|
|
21
26
|
* "model": "session", // "session" = follow the live session model
|
|
22
27
|
* // "small" = reuse opencode's small_model setting
|
|
23
28
|
* // any string = fixed extraction model
|
|
@@ -496,6 +501,12 @@ const ArminPlugin: Plugin = async (ctx) => {
|
|
|
496
501
|
// passed to the sidecar only; a key already in the environment wins.
|
|
497
502
|
const typesafeKey = cfgStr("typesafeKey", "TYPESAFE_AI_API_KEY")
|
|
498
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
|
|
499
510
|
// API key: config value is passed to the sidecar only; a key already in
|
|
500
511
|
// the environment wins (no override).
|
|
501
512
|
if (typeof armin.apiKey === "string" && armin.apiKey) {
|
|
@@ -529,21 +540,25 @@ const ArminPlugin: Plugin = async (ctx) => {
|
|
|
529
540
|
// between "memory works" and a silent deterministic downgrade.
|
|
530
541
|
void (async () => {
|
|
531
542
|
try {
|
|
532
|
-
const health = await api.get<{ extraction: string }>("/health")
|
|
543
|
+
const health = await api.get<{ extraction: string; jev_provider?: string }>("/health")
|
|
533
544
|
const mode = health?.extraction ?? "unknown"
|
|
534
545
|
if (mode === "jev") {
|
|
535
|
-
|
|
546
|
+
const via = health?.jev_provider ?? "typesafe"
|
|
547
|
+
log(`extraction: jev (System One via ${via}) — ready`)
|
|
536
548
|
} else if (mode === "llm") {
|
|
537
549
|
log(
|
|
538
|
-
`extraction: llm (fallback) — jev is the default but no
|
|
539
|
-
`configured. Set TYPESAFE_AI_API_KEY
|
|
540
|
-
`"armin": { "typesafeKey": "..." }
|
|
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.`,
|
|
541
555
|
)
|
|
542
556
|
} else {
|
|
543
557
|
log(
|
|
544
558
|
`extraction: deterministic-only — no API keys found. Import and ` +
|
|
545
559
|
`unverified-edit warnings work; prose extraction does not. Set ` +
|
|
546
|
-
`TYPESAFE_AI_API_KEY (or ANTHROPIC/OPENAI_API_KEY)
|
|
560
|
+
`TYPESAFE_AI_API_KEY or OPENROUTER_API_KEY (or ANTHROPIC/OPENAI_API_KEY) ` +
|
|
561
|
+
`to enable it.`,
|
|
547
562
|
)
|
|
548
563
|
}
|
|
549
564
|
} catch {
|