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 +23 -0
- package/package.json +1 -1
- package/skills/jev-review/SKILL.md +4 -0
- package/src/commands.ts +123 -13
- package/src/config.ts +60 -11
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
|
@@ -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
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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 =
|
|
592
|
+
target = defaultKeyFileFor(backend);
|
|
494
593
|
}
|
|
495
594
|
let key: string | undefined;
|
|
496
595
|
try {
|
|
497
596
|
key = await ctx.ui.input(
|
|
498
|
-
|
|
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
|
|
518
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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,
|
|
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",
|
|
321
|
-
|
|
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
|
|