@delt/claude-jev-advisor 0.1.0 → 0.1.2
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 +20 -2
- package/dist/cli.js +259 -61
- package/mod/.claude-plugin/plugin.json +14 -14
- package/package.json +48 -48
package/README.md
CHANGED
|
@@ -27,10 +27,10 @@ npm i -g @delt/claude-jev-advisor
|
|
|
27
27
|
Then register the hooks:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
claude-jev-advisor install
|
|
30
|
+
claude-jev-advisor install
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
When the context helper is installed and no key is found, `install` asks for your TypeSafe API key. What you type is hidden. It checks the key with one small Jev call and saves it to `~/.claude/claude-jev-advisor/jev-key.env`. Press Enter to skip; `claude-jev-advisor key` asks again later. Instead of typing it, you can set `TYPESAFE_API_KEY`, or pass `--key-file <path>` to a file holding a line `TYPESAFE_API_KEY=...` (then only that path is saved). The key is never printed or logged. On Windows that file is protected by your user folder's permissions only.
|
|
34
34
|
|
|
35
35
|
Before it changes `~/.claude/settings.json`, `install` copies it to `~/.claude/backups/settings.json.<YYYY-MM-DD-HHmmss>-before-claude-jev-advisor`. If a backup from the same second already exists, it adds `-2`, `-3` and so on instead of overwriting it. It then adds or replaces only this package's entries. Running it again changes nothing.
|
|
36
36
|
|
|
@@ -44,6 +44,7 @@ The hooks also reach Claude Code sessions that are already open; the bottom-row
|
|
|
44
44
|
| `claude-jev-advisor uninstall [rm] [context]` | Removes this package's hooks and display from `settings.json` |
|
|
45
45
|
| `claude-jev-advisor on [rm] [context]` / `off [rm] [context]` | Switches helpers on or off without touching `settings.json` |
|
|
46
46
|
| `claude-jev-advisor status` | Shows what is registered and on, the display, whether a key is set, the thresholds and the last judgment |
|
|
47
|
+
| `claude-jev-advisor key` | Asks for the TypeSafe API key, checks it and saves it (needs an interactive terminal) |
|
|
47
48
|
| `claude-jev-advisor help` | Prints the usage |
|
|
48
49
|
|
|
49
50
|
If you leave out the helper names, the command applies to all helpers. The display is set up when `context` is installed; run `claude-jev-advisor install context --display <mode>` to switch it.
|
|
@@ -98,6 +99,20 @@ A target cannot be worked out when it uses:
|
|
|
98
99
|
- a wildcard in a folder name
|
|
99
100
|
- a `cd` to such a path earlier in the command
|
|
100
101
|
|
|
102
|
+
It does not see deletions made any other way:
|
|
103
|
+
|
|
104
|
+
- through the PowerShell tool (`Remove-Item` and its aliases). On Windows with Git Bash installed, Claude Code turns this tool on by default for claude.ai and Console accounts and uses PowerShell as its main shell, so many deletions go through it.
|
|
105
|
+
- through a nested shell: `sh -c`, `bash -c`, `powershell -Command`, `pwsh -c`, `cmd /c del`
|
|
106
|
+
- with other commands or programs, such as `find -delete`, `git clean` or a script that deletes files
|
|
107
|
+
|
|
108
|
+
What its decisions do depends on Claude Code's permission mode:
|
|
109
|
+
|
|
110
|
+
| Mode | `ask` | `deny` | No decision |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `auto` | A prompt. The auto mode classifier can still block the call but cannot approve it on its own (Claude Code 2.1.211 or later). | Blocked | The classifier decides |
|
|
113
|
+
| `bypassPermissions` | A prompt. Claude Code's documentation does not say this; it was seen in a test on 2026-10-03. | Blocked | Runs |
|
|
114
|
+
| Other modes | A prompt (`dontAsk` refuses the call instead) | Blocked | Claude Code's normal permission handling |
|
|
115
|
+
|
|
101
116
|
`install rm` also replaces an older personal hook at `~/.claude/hooks/rm-guard/rm-guard.mjs` if one is registered. Its files are left in place.
|
|
102
117
|
|
|
103
118
|
If a hook of this package fails in any way, it prints nothing and exits 0, so it never blocks Claude Code.
|
|
@@ -113,6 +128,8 @@ npm rm -g @delt/claude-jev-advisor
|
|
|
113
128
|
|
|
114
129
|
If the package is removed first, the hook entries point to a missing file. Claude Code keeps running, but the helpers are off. To clean up, reinstall the package and run `claude-jev-advisor uninstall`, or delete the entries from `settings.json` by hand.
|
|
115
130
|
|
|
131
|
+
A key saved by `install` or `key` stays in `~/.claude/claude-jev-advisor/jev-key.env`; delete that file if you no longer need it.
|
|
132
|
+
|
|
116
133
|
## Files
|
|
117
134
|
|
|
118
135
|
| Path | Contents |
|
|
@@ -121,6 +138,7 @@ If the package is removed first, the hook entries point to a missing file. Claud
|
|
|
121
138
|
| `~/.claude/claude-jev-advisor/state/<session>.json` | The last judgment of each open session, read by the display. Removed when the session ends. |
|
|
122
139
|
| `~/.claude/claude-jev-advisor/log/YYYY-MM.jsonl` | One line per judgment: the size, what was sent to Jev (parts of your conversation), the answers and the result. Never the key. |
|
|
123
140
|
| `~/.claude/claude-jev-advisor/statusline-before.json` | Your own status line while `--display statusline` is in use |
|
|
141
|
+
| `~/.claude/claude-jev-advisor/jev-key.env` | Your TypeSafe API key, when you typed it in `install` or `key` |
|
|
124
142
|
| `~/.claude/backups/settings.json.*-before-claude-jev-advisor` | Copies of `settings.json` from before each change |
|
|
125
143
|
|
|
126
144
|
Default config:
|
package/dist/cli.js
CHANGED
|
@@ -2,39 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import os from "os";
|
|
5
|
-
import
|
|
5
|
+
import path9 from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
|
-
// src/cli/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
8
|
+
// src/cli/prompt.ts
|
|
9
|
+
var ENTER = /* @__PURE__ */ new Set([10, 13]);
|
|
10
|
+
var BACKSPACE = /* @__PURE__ */ new Set([8, 127]);
|
|
11
|
+
var CTRL_C = 3;
|
|
12
|
+
var ESC = 27;
|
|
13
|
+
var ERASE = `${String.fromCharCode(8)} ${String.fromCharCode(8)}`;
|
|
14
|
+
var PROCESS_TERMINAL = { stdin: process.stdin, stdout: process.stdout, exit: (code) => process.exit(code) };
|
|
15
|
+
function terminalPrompt(terminal = PROCESS_TERMINAL) {
|
|
16
|
+
const { stdin, stdout, exit } = terminal;
|
|
17
|
+
if (!stdin.isTTY || !stdout.isTTY) return void 0;
|
|
18
|
+
return {
|
|
19
|
+
ask: (question) => new Promise((resolve) => {
|
|
20
|
+
stdout.write(question);
|
|
21
|
+
let value = "";
|
|
22
|
+
let inEscape = false;
|
|
23
|
+
const stop = () => {
|
|
24
|
+
stdin.removeListener("data", onData);
|
|
25
|
+
stdin.removeListener("end", onEnd);
|
|
26
|
+
stdin.setRawMode(false);
|
|
27
|
+
stdin.pause();
|
|
28
|
+
stdout.write("\n");
|
|
29
|
+
};
|
|
30
|
+
const onEnd = () => {
|
|
31
|
+
stop();
|
|
32
|
+
resolve("");
|
|
33
|
+
};
|
|
34
|
+
const onData = (chunk) => {
|
|
35
|
+
for (const ch of chunk) {
|
|
36
|
+
const code = ch.charCodeAt(0);
|
|
37
|
+
if (inEscape) {
|
|
38
|
+
if (ch !== "[" && code >= 64 && code <= 126) inEscape = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (code === ESC) {
|
|
42
|
+
inEscape = true;
|
|
43
|
+
} else if (ENTER.has(code)) {
|
|
44
|
+
stop();
|
|
45
|
+
resolve(value);
|
|
46
|
+
return;
|
|
47
|
+
} else if (code === CTRL_C) {
|
|
48
|
+
stop();
|
|
49
|
+
exit(130);
|
|
50
|
+
return;
|
|
51
|
+
} else if (BACKSPACE.has(code)) {
|
|
52
|
+
if (value) stdout.write(ERASE);
|
|
53
|
+
value = Array.from(value).slice(0, -1).join("");
|
|
54
|
+
} else if (code >= 32) {
|
|
55
|
+
value += ch;
|
|
56
|
+
stdout.write("*");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
stdin.setEncoding("utf8");
|
|
61
|
+
stdin.setRawMode(true);
|
|
62
|
+
stdin.resume();
|
|
63
|
+
stdin.on("data", onData);
|
|
64
|
+
stdin.on("end", onEnd);
|
|
65
|
+
}),
|
|
66
|
+
say: (line) => {
|
|
67
|
+
stdout.write(`${line}
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
34
71
|
}
|
|
35
72
|
|
|
36
|
-
// src/
|
|
37
|
-
import
|
|
73
|
+
// src/cli/run.ts
|
|
74
|
+
import fs7 from "fs";
|
|
75
|
+
import path8 from "path";
|
|
38
76
|
|
|
39
77
|
// src/config.ts
|
|
40
78
|
import fs from "fs";
|
|
@@ -81,6 +119,7 @@ var settingsPath = (home) => path.join(claudeDir(home), "settings.json");
|
|
|
81
119
|
var backupsDir = (home) => path.join(claudeDir(home), "backups");
|
|
82
120
|
var stateDir = (home) => path.join(dataDir(home), "state");
|
|
83
121
|
var statusLineBeforePath = (home) => path.join(dataDir(home), "statusline-before.json");
|
|
122
|
+
var savedKeyPath = (home) => path.join(dataDir(home), "jev-key.env");
|
|
84
123
|
|
|
85
124
|
// src/config.ts
|
|
86
125
|
function readConfig(home) {
|
|
@@ -102,6 +141,34 @@ function updateConfig(home, change) {
|
|
|
102
141
|
return next;
|
|
103
142
|
}
|
|
104
143
|
|
|
144
|
+
// src/features.ts
|
|
145
|
+
var FEATURES = ["rm", "context"];
|
|
146
|
+
var HOOK_SPECS = {
|
|
147
|
+
rm: [{ event: "PreToolUse", matcher: "Bash", script: "rm-hook.js", args: [] }],
|
|
148
|
+
context: [
|
|
149
|
+
{ event: "Stop", script: "context-hook.js", args: ["stop"] },
|
|
150
|
+
{ event: "SessionEnd", script: "context-hook.js", args: ["session-end"] }
|
|
151
|
+
]
|
|
152
|
+
};
|
|
153
|
+
var SUPPORTED_PLATFORMS = {
|
|
154
|
+
rm: ["win32"],
|
|
155
|
+
context: null
|
|
156
|
+
};
|
|
157
|
+
var HOOK_TIMEOUT_SECONDS = 15;
|
|
158
|
+
var LEGACY_RM_GUARD = /\/\.claude\/hooks\/rm-guard\/rm-guard\.mjs/;
|
|
159
|
+
var STATUSLINE_SCRIPT = "statusline.js";
|
|
160
|
+
var MOD_PLUGIN_NAME = "jev-advisor";
|
|
161
|
+
var MOD_DIR_PATTERN = /claude-jev-advisor\/mod\/?$/;
|
|
162
|
+
function ownScriptPattern(script) {
|
|
163
|
+
return new RegExp(`claude-jev-advisor/dist/${script.replace(/\./g, "\\.")}`);
|
|
164
|
+
}
|
|
165
|
+
function isFeature(value) {
|
|
166
|
+
return FEATURES.includes(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/install.ts
|
|
170
|
+
import path5 from "path";
|
|
171
|
+
|
|
105
172
|
// src/display/settings.ts
|
|
106
173
|
import fs2 from "fs";
|
|
107
174
|
import path3 from "path";
|
|
@@ -383,9 +450,107 @@ function setEnabled(home, features, enabled) {
|
|
|
383
450
|
}));
|
|
384
451
|
}
|
|
385
452
|
|
|
386
|
-
// src/
|
|
453
|
+
// src/jev.ts
|
|
454
|
+
import fs4 from "fs";
|
|
455
|
+
var JEV_URL = "https://api.typesafe.ai/v1/systemone";
|
|
456
|
+
var JEV_TIMEOUT_MS = 8e3;
|
|
457
|
+
var KEY_PREFIX = "TYPESAFE_API_KEY=";
|
|
458
|
+
function readJevKey(env, keyFile) {
|
|
459
|
+
const fromEnv = env.TYPESAFE_API_KEY?.trim();
|
|
460
|
+
if (fromEnv) return fromEnv;
|
|
461
|
+
if (!keyFile) return null;
|
|
462
|
+
let text;
|
|
463
|
+
try {
|
|
464
|
+
text = fs4.readFileSync(keyFile, "utf8");
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
const line = text.replace(/^\uFEFF/, "").split(/\r?\n/).find((l) => l.startsWith(KEY_PREFIX));
|
|
469
|
+
const key = line?.slice(KEY_PREFIX.length).trim().replace(/^["']|["']$/g, "");
|
|
470
|
+
return key || null;
|
|
471
|
+
}
|
|
472
|
+
async function callJev(request, opts) {
|
|
473
|
+
const now = opts.now ?? Date.now;
|
|
474
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
475
|
+
const limit = opts.timeoutMs ?? JEV_TIMEOUT_MS;
|
|
476
|
+
const redact = (text) => text.split(opts.key).join("[redacted]");
|
|
477
|
+
const started = now();
|
|
478
|
+
const controller = new AbortController();
|
|
479
|
+
let timer;
|
|
480
|
+
const expired = new Promise((_resolve, reject) => {
|
|
481
|
+
timer = setTimeout(() => {
|
|
482
|
+
const err = new Error(`Jev did not answer within ${limit} ms`);
|
|
483
|
+
controller.abort(err);
|
|
484
|
+
reject(err);
|
|
485
|
+
}, limit);
|
|
486
|
+
});
|
|
487
|
+
let requestId = null;
|
|
488
|
+
const exchange = (async () => {
|
|
489
|
+
const res = await fetchFn(JEV_URL, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: { Authorization: `Bearer ${opts.key}`, "Content-Type": "application/json" },
|
|
492
|
+
body: JSON.stringify({ model: "jev-latest", ...request }),
|
|
493
|
+
signal: controller.signal
|
|
494
|
+
});
|
|
495
|
+
requestId = res.headers.get("request-id") ?? res.headers.get("x-request-id");
|
|
496
|
+
return { res, text: await res.text() };
|
|
497
|
+
})();
|
|
498
|
+
exchange.catch(() => {
|
|
499
|
+
});
|
|
500
|
+
try {
|
|
501
|
+
const { res, text } = await Promise.race([exchange, expired]);
|
|
502
|
+
if (!res.ok) return { error: redact(`HTTP ${res.status}: ${text}`).slice(0, 300), requestId, ms: now() - started };
|
|
503
|
+
const body = JSON.parse(text);
|
|
504
|
+
const answers = {};
|
|
505
|
+
for (const [id, answer] of Object.entries(body.answers ?? {})) {
|
|
506
|
+
if (typeof answer?.noul === "number") answers[id] = answer.noul;
|
|
507
|
+
}
|
|
508
|
+
return { answers, model: typeof body.model === "string" ? body.model : null, requestId, ms: now() - started };
|
|
509
|
+
} catch (err) {
|
|
510
|
+
return { error: redact(String(err?.message ?? err)).slice(0, 300), requestId, ms: now() - started };
|
|
511
|
+
} finally {
|
|
512
|
+
clearTimeout(timer);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/key.ts
|
|
387
517
|
import fs5 from "fs";
|
|
388
518
|
import path6 from "path";
|
|
519
|
+
var CHECK_REQUEST = {
|
|
520
|
+
state: { text: "hello" },
|
|
521
|
+
questions: { ok: { type: "noul", instructions: "Is `text` a greeting?", criteria: { true: "It is a greeting.", false: "It is not a greeting." } } }
|
|
522
|
+
};
|
|
523
|
+
var REJECTED = /^HTTP 40[13]\b/;
|
|
524
|
+
async function checkJevKey(key, fetchFn) {
|
|
525
|
+
const result = await callJev(CHECK_REQUEST, { key, fetchFn });
|
|
526
|
+
if (!("error" in result)) return { status: "ok" };
|
|
527
|
+
return REJECTED.test(result.error) ? { status: "rejected", detail: result.error } : { status: "unchecked", detail: result.error };
|
|
528
|
+
}
|
|
529
|
+
function saveJevKey(home, key) {
|
|
530
|
+
const file = savedKeyPath(home);
|
|
531
|
+
fs5.mkdirSync(path6.dirname(file), { recursive: true });
|
|
532
|
+
fs5.writeFileSync(file, `TYPESAFE_API_KEY=${key}
|
|
533
|
+
`, { mode: 384 });
|
|
534
|
+
fs5.chmodSync(file, 384);
|
|
535
|
+
return file;
|
|
536
|
+
}
|
|
537
|
+
async function promptForKey(prompt, check) {
|
|
538
|
+
for (; ; ) {
|
|
539
|
+
const key = (await prompt.ask("TypeSafe API key (Enter to skip): ")).trim();
|
|
540
|
+
if (!key) return null;
|
|
541
|
+
const result = await check(key);
|
|
542
|
+
if (result.status === "ok") return key;
|
|
543
|
+
if (result.status === "unchecked") {
|
|
544
|
+
prompt.say(`Saved without a check: could not check the key (${result.detail}).`);
|
|
545
|
+
return key;
|
|
546
|
+
}
|
|
547
|
+
prompt.say(`That key did not work (${result.detail}). Try again, or press Enter to skip.`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/status.ts
|
|
552
|
+
import fs6 from "fs";
|
|
553
|
+
import path7 from "path";
|
|
389
554
|
|
|
390
555
|
// src/display/line.ts
|
|
391
556
|
var PHRASES = {
|
|
@@ -433,24 +598,6 @@ function parseState(raw) {
|
|
|
433
598
|
return { sessionId: raw.sessionId, at: raw.at, size: typeof raw.size === "number" ? raw.size : null, judgment };
|
|
434
599
|
}
|
|
435
600
|
|
|
436
|
-
// src/jev.ts
|
|
437
|
-
import fs4 from "fs";
|
|
438
|
-
var KEY_PREFIX = "TYPESAFE_API_KEY=";
|
|
439
|
-
function readJevKey(env, keyFile) {
|
|
440
|
-
const fromEnv = env.TYPESAFE_API_KEY?.trim();
|
|
441
|
-
if (fromEnv) return fromEnv;
|
|
442
|
-
if (!keyFile) return null;
|
|
443
|
-
let text;
|
|
444
|
-
try {
|
|
445
|
-
text = fs4.readFileSync(keyFile, "utf8");
|
|
446
|
-
} catch {
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
const line = text.replace(/^\uFEFF/, "").split(/\r?\n/).find((l) => l.startsWith(KEY_PREFIX));
|
|
450
|
-
const key = line?.slice(KEY_PREFIX.length).trim().replace(/^["']|["']$/g, "");
|
|
451
|
-
return key || null;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
601
|
// src/status.ts
|
|
455
602
|
var SCRIPT_IN_COMMAND = /^node "([^"]+)"/;
|
|
456
603
|
function isRecord4(value) {
|
|
@@ -461,19 +608,19 @@ function displaySetUp(settings, config) {
|
|
|
461
608
|
if (config.display === "statusline") return isOurStatusLine(settings.statusLine);
|
|
462
609
|
const env = isRecord4(settings.env) ? settings.env : {};
|
|
463
610
|
const dirs = typeof env[PLUGIN_DIRS_ENV] === "string" ? env[PLUGIN_DIRS_ENV] : "";
|
|
464
|
-
const hasDir = dirs.split(
|
|
611
|
+
const hasDir = dirs.split(path7.delimiter).some((dir) => MOD_DIR_PATTERN.test(dir.replace(/\\/g, "/")));
|
|
465
612
|
return hasDir && isRecord4(settings.pluginConfigs) && MOD_PLUGIN_NAME in settings.pluginConfigs;
|
|
466
613
|
}
|
|
467
614
|
function newestState(home) {
|
|
468
615
|
let newest = null;
|
|
469
616
|
try {
|
|
470
|
-
for (const name of
|
|
617
|
+
for (const name of fs6.readdirSync(stateDir(home))) {
|
|
471
618
|
if (!name.endsWith(".json")) continue;
|
|
472
|
-
const file =
|
|
473
|
-
const mtime =
|
|
619
|
+
const file = path7.join(stateDir(home), name);
|
|
620
|
+
const mtime = fs6.statSync(file).mtimeMs;
|
|
474
621
|
if (!newest || mtime > newest.mtime) newest = { file, mtime };
|
|
475
622
|
}
|
|
476
|
-
return newest ? parseState(JSON.parse(
|
|
623
|
+
return newest ? parseState(JSON.parse(fs6.readFileSync(newest.file, "utf8"))) : null;
|
|
477
624
|
} catch {
|
|
478
625
|
return null;
|
|
479
626
|
}
|
|
@@ -511,7 +658,7 @@ function statusLines(home, env = process.env) {
|
|
|
511
658
|
lines.push(`${feature}: installed, ${state}`);
|
|
512
659
|
for (const c of commands) {
|
|
513
660
|
const script = SCRIPT_IN_COMMAND.exec(c.command)?.[1];
|
|
514
|
-
if (script && !
|
|
661
|
+
if (script && !fs6.existsSync(script)) {
|
|
515
662
|
lines.push(` broken: ${c.event} hook points to a missing file (${script}) - run "claude-jev-advisor install ${feature}" again, or "claude-jev-advisor uninstall ${feature}"`);
|
|
516
663
|
}
|
|
517
664
|
}
|
|
@@ -533,6 +680,7 @@ var USAGE = [
|
|
|
533
680
|
" on [rm] [context] switch helpers on (open sessions too)",
|
|
534
681
|
" off [rm] [context] switch helpers off (open sessions too)",
|
|
535
682
|
" status show what is registered and switched on",
|
|
683
|
+
" key ask for the TypeSafe API key, check it and save it",
|
|
536
684
|
"",
|
|
537
685
|
"Options for install: --lang ko|en --key-file <path> --display mod|statusline|message",
|
|
538
686
|
'The display is set up when context is installed: "install context --display <mode>" switches it.',
|
|
@@ -570,7 +718,33 @@ function checkOptions(command, options) {
|
|
|
570
718
|
if (options.display !== void 0 && !DISPLAYS2.includes(options.display)) return "--display takes mod, statusline or message";
|
|
571
719
|
return null;
|
|
572
720
|
}
|
|
573
|
-
function
|
|
721
|
+
async function askAndSaveKey(io, prompt) {
|
|
722
|
+
const key = await promptForKey(prompt, io.checkKey ?? ((candidate) => checkJevKey(candidate)));
|
|
723
|
+
if (!key) {
|
|
724
|
+
io.out('No key saved. Run "claude-jev-advisor key" to add one later.');
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const before = readConfig(io.home).keyFile;
|
|
728
|
+
const file = saveJevKey(io.home, key);
|
|
729
|
+
updateConfig(io.home, (c) => ({ ...c, keyFile: file }));
|
|
730
|
+
io.out(`Saved the key to ${file}`);
|
|
731
|
+
if (before && path8.resolve(before) !== path8.resolve(file)) io.out(`The config now uses this file instead of ${before}.`);
|
|
732
|
+
if ((io.env ?? process.env).TYPESAFE_API_KEY?.trim()) io.out("TYPESAFE_API_KEY is set in your environment; the hooks use it before the saved key.");
|
|
733
|
+
}
|
|
734
|
+
async function ensureKey(io, keyFileGiven) {
|
|
735
|
+
const keyFile = readConfig(io.home).keyFile;
|
|
736
|
+
if (readJevKey(io.env ?? process.env, keyFile)) return;
|
|
737
|
+
if (keyFileGiven) {
|
|
738
|
+
io.out(`No TYPESAFE_API_KEY line found in ${keyFile}; context cannot judge until it has one.`);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (!io.prompt) {
|
|
742
|
+
io.out('No TypeSafe API key yet: run "claude-jev-advisor key" in a terminal to add one (context needs it to judge).');
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
await askAndSaveKey(io, io.prompt);
|
|
746
|
+
}
|
|
747
|
+
async function runCli(argv, io) {
|
|
574
748
|
const parsed = parse(argv);
|
|
575
749
|
const problem = typeof parsed === "string" ? parsed : checkOptions(parsed.command, parsed.options);
|
|
576
750
|
if (typeof parsed === "string" || problem) {
|
|
@@ -589,7 +763,7 @@ function runCli(argv, io) {
|
|
|
589
763
|
platform: io.platform,
|
|
590
764
|
now: io.now(),
|
|
591
765
|
lang: options.lang,
|
|
592
|
-
keyFile: options["key-file"] === void 0 ||
|
|
766
|
+
keyFile: options["key-file"] === void 0 || path8.isAbsolute(options["key-file"]) ? options["key-file"] : path8.resolve(options["key-file"]),
|
|
593
767
|
display: options.display
|
|
594
768
|
});
|
|
595
769
|
for (const f of r.installed) io.out(`installed ${f}`);
|
|
@@ -597,7 +771,12 @@ function runCli(argv, io) {
|
|
|
597
771
|
if (r.replacedLegacyRmGuard) io.out("replaced the legacy rm-guard hook (its files under ~/.claude/hooks/rm-guard were left in place)");
|
|
598
772
|
if (r.display) io.out(`display: ${r.display}`);
|
|
599
773
|
if (r.backup) io.out(`backup: ${r.backup}`);
|
|
600
|
-
if (r.installed.length)
|
|
774
|
+
if (r.installed.length) {
|
|
775
|
+
io.out(
|
|
776
|
+
r.display === "mod" ? "Hooks apply right away, also in open Claude Code sessions; the bottom-row display starts with the next new session." : "Hooks apply right away, also in open Claude Code sessions."
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
if (r.installed.includes("context")) await ensureKey(io, options["key-file"] !== void 0);
|
|
601
780
|
return 0;
|
|
602
781
|
}
|
|
603
782
|
case "uninstall": {
|
|
@@ -605,6 +784,15 @@ function runCli(argv, io) {
|
|
|
605
784
|
if (!r.removed.length) io.out("nothing to remove");
|
|
606
785
|
for (const f of r.removed) io.out(`removed ${f}`);
|
|
607
786
|
if (r.backup) io.out(`backup: ${r.backup}`);
|
|
787
|
+
if (r.removed.includes("context") && fs7.existsSync(savedKeyPath(io.home))) io.out(`kept your saved TypeSafe key at ${savedKeyPath(io.home)}`);
|
|
788
|
+
return 0;
|
|
789
|
+
}
|
|
790
|
+
case "key": {
|
|
791
|
+
if (!io.prompt) {
|
|
792
|
+
io.err("claude-jev-advisor key needs an interactive terminal (or set TYPESAFE_API_KEY, or use install --key-file).");
|
|
793
|
+
return 1;
|
|
794
|
+
}
|
|
795
|
+
await askAndSaveKey(io, io.prompt);
|
|
608
796
|
return 0;
|
|
609
797
|
}
|
|
610
798
|
case "on":
|
|
@@ -632,11 +820,21 @@ function runCli(argv, io) {
|
|
|
632
820
|
}
|
|
633
821
|
|
|
634
822
|
// src/cli.ts
|
|
635
|
-
|
|
823
|
+
runCli(process.argv.slice(2), {
|
|
636
824
|
home: os.homedir(),
|
|
637
|
-
distDir:
|
|
825
|
+
distDir: path9.dirname(fileURLToPath(import.meta.url)),
|
|
638
826
|
platform: process.platform,
|
|
639
827
|
now: () => /* @__PURE__ */ new Date(),
|
|
640
828
|
out: (line) => console.log(line),
|
|
641
|
-
err: (line) => console.error(line)
|
|
642
|
-
|
|
829
|
+
err: (line) => console.error(line),
|
|
830
|
+
env: process.env,
|
|
831
|
+
prompt: terminalPrompt()
|
|
832
|
+
}).then(
|
|
833
|
+
(code) => {
|
|
834
|
+
process.exitCode = code;
|
|
835
|
+
},
|
|
836
|
+
(err) => {
|
|
837
|
+
console.error(`claude-jev-advisor: ${err?.message ?? err}`);
|
|
838
|
+
process.exitCode = 1;
|
|
839
|
+
}
|
|
840
|
+
);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "jev-advisor",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Bottom-row compact/clear advice from @delt/claude-jev-advisor (unofficial)",
|
|
5
|
-
"author": { "name": "delt96" },
|
|
6
|
-
"userConfig": {
|
|
7
|
-
"dataDir": {
|
|
8
|
-
"type": "string",
|
|
9
|
-
"title": "Data folder",
|
|
10
|
-
"description": "The claude-jev-advisor data folder holding config.json and state/. Set by claude-jev-advisor install.",
|
|
11
|
-
"default": ""
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "jev-advisor",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Bottom-row compact/clear advice from @delt/claude-jev-advisor (unofficial)",
|
|
5
|
+
"author": { "name": "delt96" },
|
|
6
|
+
"userConfig": {
|
|
7
|
+
"dataDir": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"title": "Data folder",
|
|
10
|
+
"description": "The claude-jev-advisor data folder holding config.json and state/. Set by claude-jev-advisor install.",
|
|
11
|
+
"default": ""
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@delt/claude-jev-advisor",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Unofficial Claude Code helpers using TypeSafe Jev: compact/clear advice and an rm guard",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"claude-jev-advisor": "./dist/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"mod",
|
|
12
|
-
"README.md"
|
|
13
|
-
],
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=18.0.0"
|
|
16
|
-
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "tsup",
|
|
19
|
-
"typecheck": "tsc --noEmit",
|
|
20
|
-
"test": "node --import tsx --import ./test/isolate-home.ts --test --test-timeout=30000 \"test/**/*.test.ts\"",
|
|
21
|
-
"prepack": "npm run build"
|
|
22
|
-
},
|
|
23
|
-
"keywords": [
|
|
24
|
-
"claude",
|
|
25
|
-
"claude-code",
|
|
26
|
-
"hooks",
|
|
27
|
-
"compact",
|
|
28
|
-
"context",
|
|
29
|
-
"typesafe",
|
|
30
|
-
"jev",
|
|
31
|
-
"rm"
|
|
32
|
-
],
|
|
33
|
-
"license": "MIT",
|
|
34
|
-
"repository": {
|
|
35
|
-
"type": "git",
|
|
36
|
-
"url": "https://github.com/delt96/claude-jev-advisor.git"
|
|
37
|
-
},
|
|
38
|
-
"bugs": {
|
|
39
|
-
"url": "https://github.com/delt96/claude-jev-advisor/issues"
|
|
40
|
-
},
|
|
41
|
-
"homepage": "https://github.com/delt96/claude-jev-advisor#readme",
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"@types/node": "^22.0.0",
|
|
44
|
-
"tsup": "^8.0.0",
|
|
45
|
-
"tsx": "^4.23.15",
|
|
46
|
-
"typescript": "^5.5.0"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@delt/claude-jev-advisor",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Unofficial Claude Code helpers using TypeSafe Jev: compact/clear advice and an rm guard",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-jev-advisor": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"mod",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "node --import tsx --import ./test/isolate-home.ts --test --test-timeout=30000 \"test/**/*.test.ts\"",
|
|
21
|
+
"prepack": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"claude",
|
|
25
|
+
"claude-code",
|
|
26
|
+
"hooks",
|
|
27
|
+
"compact",
|
|
28
|
+
"context",
|
|
29
|
+
"typesafe",
|
|
30
|
+
"jev",
|
|
31
|
+
"rm"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/delt96/claude-jev-advisor.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/delt96/claude-jev-advisor/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/delt96/claude-jev-advisor#readme",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"tsup": "^8.0.0",
|
|
45
|
+
"tsx": "^4.23.15",
|
|
46
|
+
"typescript": "^5.5.0"
|
|
47
|
+
}
|
|
48
|
+
}
|