pi-jev-guard 0.2.2 → 0.3.0

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
@@ -54,6 +54,29 @@ In chat, l'LLM può chiamare `jev_validate` per controlli mirati.
54
54
  /jev mode automatic # seleziona il primario + attiva enforcement
55
55
  ```
56
56
 
57
+ ### Backend e chiavi
58
+
59
+ Due backend, stessa policy e stesse regole:
60
+
61
+ | Backend | Endpoint | Variabile | Modello | Timeout |
62
+ |---|---|---|---|---|
63
+ | `openrouter` (default) | OpenRouter | `OPENROUTER_API_KEY` | `typesafe/jev-1.13` | 4000 ms |
64
+ | `typesafe` | SDK ufficiale `@typesafe-ai/sdk` → `api.typesafe.ai` | `TYPESAFE_API_KEY` | `jev-1.13.0` | 1500 ms |
65
+ | `auto` | sceglie il backend che ha una chiave (entrambe → OpenRouter) | — | — | — |
66
+
67
+ ```text
68
+ /jev backend # stato: backend, modello, chiavi rilevate
69
+ /jev backend typesafe # cambia backend (salvato in jev-config.json)
70
+ /jev save-key typesafe # salva la chiave del backend (file 600)
71
+ /jev save-key openrouter # l'altra chiave resta dov'è
72
+ /jev key-file # dove viene cercata la chiave
73
+ ```
74
+
75
+ Le chiavi stanno in file separati (`jev-api-key.<backend>.txt` accanto alla
76
+ config), quindi i due backend possono coesistere. Precedenza: variabile
77
+ d'ambiente → file del backend → file generico `jev.apiKeyFile` → percorso
78
+ canonico. Vedi `config/jev-config.example.json`.
79
+
57
80
  **Più twin insieme**: ogni `/jev upstream` *aggiunge* un modello guarded, senza
58
81
  rimuovere i precedenti. I twin vengono registrati anche all'avvio (i modelli
59
82
  dinamici come Codex sono letti dal models-store), quando gli originali sono disponibili.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-guard",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Jev validation guard for pi: on-demand tool + automatic provider gate. Dual backend: TypeSafe direct + OpenRouter.",
6
6
  "keywords": [
@@ -23,3 +23,7 @@ Run the appropriate compiler, static checks, or targeted tests separately.
23
23
  For ad-hoc typed judgements (classification, relevance, rubric scores) use
24
24
  `jev_ask` with model-defined questions instead; read p >= 0.90 as yes,
25
25
  p <= 0.20 as no, and choice/score below 0.60 confidence as uncertain.
26
+
27
+ Questa skill riguarda i tool di verifica di pi-jev-guard. Per progettare
28
+ integrazioni con TypeSafe (System One, SDK, cookbook) usa la skill ufficiale
29
+ `typesafe-ai` se installata.
package/src/commands.ts CHANGED
@@ -6,7 +6,9 @@ import { isTwinId, untwinId } from "./automatic/overlay.ts";
6
6
  import type { JevConfig, TwinRef } from "./config.ts";
7
7
  import {
8
8
  defaultConfigPath,
9
+ defaultKeyFileFor,
9
10
  expandHome,
11
+ keyFileCandidates,
10
12
  parseUpstreamRef,
11
13
  resolveBackend,
12
14
  saveConfig,
@@ -86,6 +88,7 @@ export const JEV_SUBCOMMANDS = [
86
88
  "reload",
87
89
  "output",
88
90
  "last",
91
+ "backend",
89
92
  "save-key",
90
93
  "help",
91
94
  ] as const;
@@ -108,7 +111,12 @@ export const JEV_HELP_TEXT = [
108
111
  " /jev stats Conteggi pass/block/review/unavailable + latenze",
109
112
  " /jev output Ultimo output giudicato: leak + classe errore",
110
113
  " /jev last Ultimo verdetto del gate (twin): esito e controlli",
111
- " /jev save-key Salva la chiave Jev su file (permessi 600)",
114
+ " /jev backend Mostra backend attivo e chiavi rilevate",
115
+ " /jev backend auto|openrouter|typesafe",
116
+ " Cambia backend (salvato in jev-config.json)",
117
+ " /jev save-key [backend] Salva la chiave del backend (default: attivo), 600",
118
+ " auto e typesafe tengono file separati",
119
+ " /jev key-file [backend] Mostra dove viene cercata la chiave",
112
120
  " /jev reload Ricarica jev-config.json da disco",
113
121
  " /jev help Questo aiuto",
114
122
  "",
@@ -478,11 +486,102 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
478
486
  return;
479
487
  }
480
488
 
489
+ if (sub === "backend") {
490
+ const cfg = deps.config();
491
+ const resolved = resolveBackend(cfg);
492
+ const requested = parts[1];
493
+ if (requested === undefined) {
494
+ const backendStatus = (["openrouter", "typesafe"] as const)
495
+ .map((b) => {
496
+ const key = resolveBackend({ ...cfg, jev: { ...cfg.jev, backend: b } });
497
+ const files = keyFileCandidates(cfg, b).map((f) => expandHome(f));
498
+ return ` ${b}: ${key.apiKeyPresent ? "chiave OK" : "chiave assente"} (${key.keySource})` +
499
+ (files.length > 0 ? `\n file: ${files.join(", ")}` : "");
500
+ })
501
+ .join("\n");
502
+ ctx.ui.notify(
503
+ [
504
+ `Backend: ${cfg.jev.backend} → effettivo ${resolved.backend} (${resolved.reason})`,
505
+ `Modello: ${resolved.model}`,
506
+ backendStatus,
507
+ "",
508
+ `Cambia con: /jev backend auto|openrouter|typesafe. Salva la chiave con /jev save-key ${resolved.backend}.`,
509
+ ].join("\n"),
510
+ "info",
511
+ );
512
+ return;
513
+ }
514
+ if (requested !== "auto" && requested !== "openrouter" && requested !== "typesafe") {
515
+ ctx.ui.notify("Uso: /jev backend auto|openrouter|typesafe", "warning");
516
+ return;
517
+ }
518
+ const shared = deps.sharedConfig?.() ?? cfg;
519
+ const next: JevConfig = {
520
+ ...shared,
521
+ jev: { ...shared.jev, backend: requested },
522
+ };
523
+ let savedPath: string | undefined;
524
+ try {
525
+ savedPath = saveConfig(next);
526
+ } catch (error) {
527
+ ctx.ui.notify(
528
+ `Jev: backend non salvato: ${error instanceof Error ? error.message : String(error)}`,
529
+ "error",
530
+ );
531
+ return;
532
+ }
533
+ const reloaded = deps.reload();
534
+ const after = resolveBackend(reloaded);
535
+ ctx.ui.notify(
536
+ [
537
+ `Backend impostato: ${requested} → effettivo ${after.backend} (${after.reason}).`,
538
+ `Modello: ${after.model}`,
539
+ after.apiKeyPresent
540
+ ? `Chiave: ${after.keySource}`
541
+ : `ATTENZIONE: nessuna chiave per ${requested}. Salvala con /jev save-key${requested === "auto" ? "" : ` ${requested}`}.`,
542
+ savedPath ? `Config: ${savedPath}` : "",
543
+ ]
544
+ .filter((l) => l !== "")
545
+ .join("\n"),
546
+ after.apiKeyPresent ? "info" : "warning",
547
+ );
548
+ return;
549
+ }
550
+
551
+ if (sub === "key-file") {
552
+ const cfg = deps.config();
553
+ const which = parts[1];
554
+ const backends = (["openrouter", "typesafe"] as const).filter(
555
+ (b) => which === undefined || which === b,
556
+ );
557
+ if (backends.length === 0) {
558
+ ctx.ui.notify("Uso: /jev key-file [openrouter|typesafe]", "warning");
559
+ return;
560
+ }
561
+ const lines = backends.map((b) => {
562
+ const files = keyFileCandidates(cfg, b);
563
+ return `${b}: ` + (files.length > 0
564
+ ? files.map((f) => expandHome(f)).join(" → ")
565
+ : `(nessun file; default /jev save-key ${b} → ${defaultKeyFileFor(b)})`);
566
+ });
567
+ ctx.ui.notify(
568
+ [...lines, "", "Precedenza: variabile d'ambiente, poi file specifico del backend, poi il file generico."].join("\n"),
569
+ "info",
570
+ );
571
+ return;
572
+ }
573
+
481
574
  if (sub === "save-key") {
482
575
  const cfg = deps.config();
483
- let target = cfg.jev.apiKeyFile?.trim()
484
- ? expandHome(cfg.jev.apiKeyFile.trim())
485
- : resolve(dirname(defaultConfigPath()), "jev-api-key.txt");
576
+ const which = parts[1];
577
+ const backend: "openrouter" | "typesafe" =
578
+ which === "openrouter" || which === "typesafe"
579
+ ? which
580
+ : resolveBackend(cfg).backend;
581
+ // Le chiavi sono per-backend: auto e typesafe possono coesistere.
582
+ let target = cfg.jev.apiKeyFiles?.[backend]?.trim()
583
+ ? expandHome(cfg.jev.apiKeyFiles[backend]!.trim())
584
+ : defaultKeyFileFor(backend);
486
585
  // Mai chiavi in tmp: dir effimera (pulizia automatica = chiave persa).
487
586
  // Può capitare con config stantie caricate in memoria prima di un fix.
488
587
  if (target === tmpdir() || target.startsWith(tmpdir() + "/")) {
@@ -490,12 +589,12 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
490
589
  `Jev: percorso chiave sotto tmp ignorato (${target}); uso quello canonico.`,
491
590
  "warning",
492
591
  );
493
- target = resolve(dirname(defaultConfigPath()), "jev-api-key.txt");
592
+ target = defaultKeyFileFor(backend);
494
593
  }
495
594
  let key: string | undefined;
496
595
  try {
497
596
  key = await ctx.ui.input(
498
- "Jev API key",
597
+ `Jev API key (${backend})`,
499
598
  "Incolla la chiave (potrebbe fare echo nel terminale)",
500
599
  );
501
600
  } catch {
@@ -514,20 +613,31 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
514
613
  ctx.ui.notify(`Jev: salvataggio fallito: ${error instanceof Error ? error.message : String(error)}`, "error");
515
614
  return;
516
615
  }
517
- // Punta la config al file e ricarica gli handle col nuovo stato.
518
- cfg.jev.apiKeyFile = target;
616
+ // Punta la config al file del backend e ricarica gli handle.
617
+ const shared = deps.sharedConfig?.() ?? cfg;
618
+ const next: JevConfig = {
619
+ ...shared,
620
+ jev: {
621
+ ...shared.jev,
622
+ apiKeyFiles: { ...shared.jev.apiKeyFiles, [backend]: target },
623
+ },
624
+ };
519
625
  try {
520
- saveConfig({ ...cfg, mode: deps.sharedConfig?.().mode ?? cfg.mode });
626
+ saveConfig(next);
521
627
  } catch {
522
628
  // Resta in memoria anche se il salvataggio fallisce.
523
629
  }
524
630
  const reloaded = deps.reload();
525
631
  const resolved = resolveBackend(reloaded);
632
+ const legacyNote =
633
+ shared.jev.apiKeyFile && shared.jev.apiKeyFile !== target
634
+ ? ` Nota: resta anche il file generico ${expandHome(shared.jev.apiKeyFile)} (usato come ripiego).`
635
+ : "";
526
636
  ctx.ui.notify(
527
- resolved.apiKeyPresent
528
- ? `Jev: chiave salvata in ${target} (600). Sorgente: ${resolved.keySource}.`
529
- : `Jev: file scritto ma chiave non rilevata (${resolved.keySource}).`,
530
- resolved.apiKeyPresent ? "info" : "warning",
637
+ resolved.backend === backend && resolved.apiKeyPresent
638
+ ? `Jev: chiave ${backend} salvata in ${target} (600). Sorgente: ${resolved.keySource}.${legacyNote}`
639
+ : `Jev: file scritto, ma il backend effettivo è ${resolved.backend} (${resolved.keySource}).${legacyNote}`,
640
+ resolved.backend === backend && resolved.apiKeyPresent ? "info" : "warning",
531
641
  );
532
642
  return;
533
643
  }
package/src/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import type { JevBackend } from "./reviewer.ts";
5
5
 
6
6
  export type JevMode = "on-demand" | "automatic";
@@ -20,6 +20,8 @@ export interface JevConfig {
20
20
  retryTransients: { enabled: boolean; maxRetries: number };
21
21
  /** Percorso file con la chiave per il backend attivo (alternativa all'env, `~/` espanso). */
22
22
  apiKeyFile?: string;
23
+ /** File chiave dedicati per backend: permettono di tenerle entrambe. */
24
+ apiKeyFiles?: { openrouter?: string; typesafe?: string };
23
25
  };
24
26
  policy: {
25
27
  revision: string;
@@ -277,28 +279,69 @@ interface BackendKey {
277
279
  source: string;
278
280
  }
279
281
 
282
+ /**
283
+ * File chiave da provare, in ordine: specifico del backend, generico,
284
+ * poi il percorso canonico per-backend (chiave lasciata lì senza config).
285
+ */
286
+ /** Solo i file dichiarati in config (errori di lettura da segnalare). */
287
+ export function configuredKeyFiles(config: JevConfig, backend: JevBackend): string[] {
288
+ const paths: string[] = [];
289
+ const push = (path: string | undefined) => {
290
+ const trimmed = path?.trim();
291
+ if (trimmed && !paths.includes(trimmed)) paths.push(trimmed);
292
+ };
293
+ push(config.jev.apiKeyFiles?.[backend]);
294
+ push(config.jev.apiKeyFile);
295
+ return paths;
296
+ }
297
+
298
+ export function keyFileCandidates(config: JevConfig, backend: JevBackend): string[] {
299
+ const paths = configuredKeyFiles(config, backend);
300
+ const canonical = defaultKeyFileFor(backend);
301
+ if (!paths.includes(canonical)) paths.push(canonical);
302
+ return paths;
303
+ }
304
+
280
305
  function resolveKeyFor(
281
306
  backend: JevBackend,
282
- apiKeyFile: string | undefined,
307
+ files: { candidates: string[]; configured: string[] },
283
308
  ): BackendKey {
284
309
  const envVar = backend === "typesafe" ? "TYPESAFE_API_KEY" : "OPENROUTER_API_KEY";
285
310
  const fromEnv = process.env[envVar]?.trim();
286
311
  if (fromEnv) return { key: fromEnv, source: `env:${envVar}` };
287
- const file = readKeyFile(apiKeyFile);
288
- if (file.key) return { key: file.key, source: `file:${file.tried}` };
289
- if (file.tried) {
290
- return { key: undefined, source: `none (file ${file.tried}: ${file.error})` };
312
+
313
+ const problems: string[] = [];
314
+ for (const path of files.candidates) {
315
+ const file = readKeyFile(path);
316
+ if (file.key) return { key: file.key, source: `file:${file.tried}` };
317
+ // Il percorso canonico mancante è atteso: non è un problema da mostrare.
318
+ if (file.error && files.configured.includes(path)) {
319
+ problems.push(`file ${file.tried}: ${file.error}`);
320
+ }
291
321
  }
292
- return { key: undefined, source: `none (set ${envVar} or jev.apiKeyFile)` };
322
+ if (problems.length > 0) {
323
+ return { key: undefined, source: `none (${problems.join("; ")})` };
324
+ }
325
+ return {
326
+ key: undefined,
327
+ source: `none (set ${envVar} or /jev save-key ${backend})`,
328
+ };
329
+ }
330
+
331
+ /** Percorso canonico del file chiave per un backend (accanto alla config). */
332
+ export function defaultKeyFileFor(backend: JevBackend): string {
333
+ return resolve(dirname(defaultConfigPath()), `jev-api-key.${backend}.txt`);
293
334
  }
294
335
 
295
336
  export function resolveBackend(config: JevConfig): ResolvedBackend {
296
337
  const setting = config.jev.backend;
297
- const keyFile = config.jev.apiKeyFile;
298
338
 
299
339
  if (setting === "typesafe" || setting === "openrouter") {
300
340
  const backend = setting;
301
- const resolved = resolveKeyFor(backend, keyFile);
341
+ const resolved = resolveKeyFor(backend, {
342
+ candidates: keyFileCandidates(config, backend),
343
+ configured: configuredKeyFiles(config, backend),
344
+ });
302
345
  return {
303
346
  backend,
304
347
  model:
@@ -317,8 +360,14 @@ export function resolveBackend(config: JevConfig): ResolvedBackend {
317
360
  }
318
361
 
319
362
  // auto: preferisci backend con chiave disponibile, default openrouter.
320
- const openRouterKey = resolveKeyFor("openrouter", keyFile);
321
- const typesafeKey = resolveKeyFor("typesafe", keyFile);
363
+ const openRouterKey = resolveKeyFor("openrouter", {
364
+ candidates: keyFileCandidates(config, "openrouter"),
365
+ configured: configuredKeyFiles(config, "openrouter"),
366
+ });
367
+ const typesafeKey = resolveKeyFor("typesafe", {
368
+ candidates: keyFileCandidates(config, "typesafe"),
369
+ configured: configuredKeyFiles(config, "typesafe"),
370
+ });
322
371
  const hasOpenRouter = Boolean(openRouterKey.key);
323
372
  const hasTypesafe = Boolean(typesafeKey.key);
324
373