armin-opencode 0.1.1 → 0.1.3

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 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,10 +94,35 @@ function buildFromSource() {
94
94
  return true;
95
95
  }
96
96
 
97
+ let pendingTypesafeKey = null;
98
+
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
+ const readline = require("readline");
105
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
106
+ return new Promise((resolve) => {
107
+ rl.question(
108
+ "TypeSafe System One API key (jev extraction is the default) — paste, or Enter to skip: ",
109
+ (answer) => {
110
+ rl.close();
111
+ const key = (answer || "").trim();
112
+ if (key) pendingTypesafeKey = key;
113
+ resolve();
114
+ },
115
+ );
116
+ });
117
+ }
118
+
97
119
  function registerPlugin() {
98
120
  const plugin = path.join(PKG_ROOT, "plugins", "armin.ts");
99
121
  if (!fs.existsSync(plugin)) throw new Error(`plugin not found at ${plugin}`);
100
- const cfgDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config", "opencode");
122
+ const cfgDir = path.join(
123
+ process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"),
124
+ "opencode",
125
+ );
101
126
  const cfgPath = fs.existsSync(path.join(cfgDir, "opencode.jsonc"))
102
127
  ? path.join(cfgDir, "opencode.jsonc")
103
128
  : path.join(cfgDir, "opencode.json");
@@ -112,10 +137,18 @@ function registerPlugin() {
112
137
  }
113
138
  }
114
139
  const entry = "file://" + plugin;
140
+ let changed = false;
115
141
  const plugins = Array.isArray(data.plugin) ? data.plugin : [];
116
142
  if (!plugins.includes(entry)) {
117
143
  plugins.push(entry);
118
144
  data.plugin = plugins;
145
+ changed = true;
146
+ }
147
+ if (pendingTypesafeKey && !process.env.TYPESAFE_AI_API_KEY) {
148
+ data.armin = { ...(data.armin || {}), typesafeKey: pendingTypesafeKey };
149
+ changed = true;
150
+ }
151
+ if (changed) {
119
152
  fs.mkdirSync(cfgDir, { recursive: true });
120
153
  fs.writeFileSync(cfgPath, JSON.stringify(data, null, 2));
121
154
  }
@@ -141,7 +174,26 @@ async function install() {
141
174
  }
142
175
  }
143
176
  }
177
+ await askForTypesafeKey();
144
178
  registerPlugin();
179
+ const tsKey = process.env.TYPESAFE_AI_API_KEY || pendingTypesafeKey;
180
+ const llmKey = process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
181
+ if (tsKey) {
182
+ console.log("extraction: jev (TypeSafe System One) — ready");
183
+ } else if (llmKey) {
184
+ console.log(
185
+ "extraction: LLM fallback — jev is the default but no Typesafe key is set.\n" +
186
+ " Get a key at typesafe.ai and export TYPESAFE_AI_API_KEY (or put\n" +
187
+ ' "armin": { "typesafeKey": "..." } in your opencode config).',
188
+ );
189
+ } else {
190
+ console.log(
191
+ "extraction: NONE (deterministic-only) — import, capture and unverified-edit\n" +
192
+ " warnings work; prose extraction does not.\n" +
193
+ " Set TYPESAFE_AI_API_KEY (typesafe.ai) for jev extraction — the default —\n" +
194
+ " or ANTHROPIC_API_KEY / OPENAI_API_KEY for LLM fallback.",
195
+ );
196
+ }
145
197
  console.log(`
146
198
  ── done ────────────────────────────────────────────────────────
147
199
  Enable ARMIN per environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "armin-opencode",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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,8 @@
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
19
21
  * "model": "session", // "session" = follow the live session model
20
22
  * // "small" = reuse opencode's small_model setting
21
23
  * // any string = fixed extraction model
@@ -490,6 +492,10 @@ const ArminPlugin: Plugin = async (ctx) => {
490
492
  if (provider === "anthropic" || provider === "openai") {
491
493
  spawnEnv.LLM_PROVIDER = provider
492
494
  }
495
+ // Typesafe key (jev is the default extraction mode): config value is
496
+ // passed to the sidecar only; a key already in the environment wins.
497
+ const typesafeKey = cfgStr("typesafeKey", "TYPESAFE_AI_API_KEY")
498
+ if (typesafeKey) spawnEnv.TYPESAFE_AI_API_KEY = typesafeKey
493
499
  // API key: config value is passed to the sidecar only; a key already in
494
500
  // the environment wins (no override).
495
501
  if (typeof armin.apiKey === "string" && armin.apiKey) {
@@ -519,6 +525,31 @@ const ArminPlugin: Plugin = async (ctx) => {
519
525
  if (sidecar.port) {
520
526
  log(`reasoning-state UI: http://127.0.0.1:${sidecar.port}/ui`)
521
527
  }
528
+ // Surface the effective extraction mode once — this is the difference
529
+ // between "memory works" and a silent deterministic downgrade.
530
+ void (async () => {
531
+ try {
532
+ const health = await api.get<{ extraction: string }>("/health")
533
+ const mode = health?.extraction ?? "unknown"
534
+ if (mode === "jev") {
535
+ log(`extraction: jev (TypeSafe System One) — ready`)
536
+ } else if (mode === "llm") {
537
+ log(
538
+ `extraction: llm (fallback) — jev is the default but no Typesafe key is ` +
539
+ `configured. Set TYPESAFE_AI_API_KEY in your environment or ` +
540
+ `"armin": { "typesafeKey": "..." } in your opencode config.`,
541
+ )
542
+ } else {
543
+ log(
544
+ `extraction: deterministic-only — no API keys found. Import and ` +
545
+ `unverified-edit warnings work; prose extraction does not. Set ` +
546
+ `TYPESAFE_AI_API_KEY (or ANTHROPIC/OPENAI_API_KEY) to enable it.`,
547
+ )
548
+ }
549
+ } catch {
550
+ // best-effort status only
551
+ }
552
+ })()
522
553
 
523
554
  // ── Cold start: import AGENTS.md / CLAUDE.md into an empty graph ──────
524
555
  // Deterministic parse on the engine side (content-hash IDs, idempotent).