@delt/claude-jev-advisor 0.1.1 β 0.2.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 +52 -20
- package/dist/cli.js +248 -34
- package/dist/context-hook.js +111 -44
- package/dist/rm-hook.js +532 -25
- package/dist/statusline.js +84 -14
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +15 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Unofficial helpers for [Claude Code](https://claude.com/claude-code), installed as command hooks in `~/.claude/settings.json`. They use [TypeSafe](https://typesafe.ai) Jev for judgments that need to understand the conversation.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
What is sent to TypeSafe's Jev API (`api.typesafe.ai`):
|
|
6
|
+
|
|
7
|
+
- `context`: your last three requests (typed by you, or sent through a channel or from another Claude Code session), each cut to its first 1,000 characters, and Claude's last reply, cut to its first and last 1,500 characters.
|
|
8
|
+
- `rm`: only for a delete it would otherwise ask about, and only when the target is a file this session made or a folder git ignores. It sends the path, the `rm` command and its description, up to three tool calls of this session that name the target (each cut to 600 characters), and for a folder up to ten of its file names.
|
|
6
9
|
|
|
7
10
|
Not affiliated with Anthropic or TypeSafe.
|
|
8
11
|
|
|
9
12
|
| Helper | What it does | Systems |
|
|
10
13
|
|---|---|---|
|
|
11
14
|
| `context` | At the end of each turn, suggests `/compact` or `/clear` when the conversation is large and the work has reached a stopping point. Shown at the end of Claude Code's bottom row. | Any |
|
|
12
|
-
| `rm` | Asks before a Bash `rm` deletes real files. Lets temp files through and refuses an `rm` whose targets it cannot work out. | Windows |
|
|
15
|
+
| `rm` | Asks before a Bash `rm` deletes real files. Lets temp files through and refuses an `rm` whose targets it cannot work out. Jev can lift the ask for a test file this session made or a folder git ignores. | Windows |
|
|
13
16
|
|
|
14
17
|
## Requirements
|
|
15
18
|
|
|
16
19
|
- Node.js 18 or later
|
|
17
20
|
- Claude Code (the bottom-row display uses Claude Code mods, an early-access feature that may change between releases)
|
|
18
|
-
- A TypeSafe API key for the `context` helper, in `TYPESAFE_API_KEY` or in a key file (`--key-file`)
|
|
21
|
+
- A TypeSafe API key for the `context` helper and for the Jev check of the `rm` helper, in `TYPESAFE_API_KEY` or in a key file (`--key-file`). Without a key, `rm` asks about every real file.
|
|
19
22
|
- Windows for the `rm` helper. On other systems `install` skips it and says why.
|
|
20
23
|
|
|
21
24
|
## Install
|
|
@@ -44,6 +47,7 @@ The hooks also reach Claude Code sessions that are already open; the bottom-row
|
|
|
44
47
|
| `claude-jev-advisor uninstall [rm] [context]` | Removes this package's hooks and display from `settings.json` |
|
|
45
48
|
| `claude-jev-advisor on [rm] [context]` / `off [rm] [context]` | Switches helpers on or off without touching `settings.json` |
|
|
46
49
|
| `claude-jev-advisor status` | Shows what is registered and on, the display, whether a key is set, the thresholds and the last judgment |
|
|
50
|
+
| `claude-jev-advisor report [--days 7]` | Lists the `/compact` and `/clear` advice shown and what followed it, and the deletions Jev let through |
|
|
47
51
|
| `claude-jev-advisor key` | Asks for the TypeSafe API key, checks it and saves it (needs an interactive terminal) |
|
|
48
52
|
| `claude-jev-advisor help` | Prints the usage |
|
|
49
53
|
|
|
@@ -51,32 +55,33 @@ If you leave out the helper names, the command applies to all helpers. The displ
|
|
|
51
55
|
|
|
52
56
|
## The context helper
|
|
53
57
|
|
|
54
|
-
When a turn ends, a `Stop` hook reads the size of the conversation from the session transcript. From
|
|
58
|
+
When a turn ends, a `Stop` hook reads the size of the conversation from the session transcript. If the turn's last reply is not in the transcript yet, it waits up to 3 seconds for it. From 250k tokens it asks Jev two questions about the last three requests and the last reply:
|
|
55
59
|
|
|
56
|
-
- has the work asked for
|
|
57
|
-
-
|
|
60
|
+
- has Claude finished or handed back the work asked for, so this is a natural break?
|
|
61
|
+
- does the reply close a whole stage of the work (a plan carried out and pushed, a release, a finished investigation, a design or plan saved as a document), so the next stage can start from the saved results?
|
|
58
62
|
|
|
59
|
-
The answer is saved for the display. Nothing is added to what Claude sees, so it costs no Claude tokens. If Jev does not answer within 8 seconds, only the size is shown.
|
|
63
|
+
The answer is saved for the display. Nothing is added to what Claude sees, so it costs no Claude tokens. If Jev does not answer within 8 seconds, only the size is shown. While a subagent or a workflow runs in the background, the turn counts as work in progress and Jev is not asked; background shells and monitors do not count.
|
|
60
64
|
|
|
61
65
|
| Situation | Shown |
|
|
62
66
|
|---|---|
|
|
63
|
-
| Under
|
|
67
|
+
| Under 250k, or no judgment (no key, Jev unreachable) | `52k` |
|
|
64
68
|
| Work in progress | `π’ 312k` |
|
|
65
|
-
|
|
|
66
|
-
| Work
|
|
67
|
-
| Work unit finished, under 200k | `π’ 150k` |
|
|
69
|
+
| A whole stage closed (from 250k) | `π‘ 312k μλ‘κ² μμνλ 건 μ΄λ μΈμ? /clear` |
|
|
70
|
+
| Work finished, stage goes on (from 250k) | `π‘ 312k μ§κΈκΉμ§ μ 리νκ³ μ΄μ΄κ°λ 건 μ΄λ μΈμ? /compact` |
|
|
68
71
|
| Within 20% of auto-compact | `π΄ 790k 18%`, then ` Β· ` and the `/clear` or `/compact` advice above, or `μμ
μ΄ λλλ©΄ μ 리νκ³ μ΄μ΄κ°λ 건 μ΄λ μΈμ? /compact` while the work is still going |
|
|
69
72
|
|
|
70
73
|
With `--lang en` the advice reads `Start fresh? /clear`, `Wrap up what you have and continue? /compact` and `When this work is done, wrap up and continue? /compact`.
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
Judging starts at 250k because a conversation passes 100k after a request or two, and compacting a small conversation costs more than it saves. If you set `compactMinTokens` above `minTokens`, finished work between the two stays green. The suggestion is hidden while a new request runs, and a judgment made before a `/compact` is dropped.
|
|
73
76
|
|
|
74
77
|
| `--display` | Where |
|
|
75
78
|
|---|---|
|
|
76
79
|
| `mod` (default) | At the end of the bottom row, after `β΅β΅ β¦ mode on`. A small Claude Code mod in the package's `mod/` folder draws it. The mod is listed in `env.CLAUDE_CODE_PLUGIN_DIRS` and told the data folder through `pluginConfigs`. |
|
|
77
|
-
| `statusline` | Claude Code's status line, the row above the bottom row. An existing status line of yours keeps running first, with our text after it, and is put back when you switch away or uninstall. On Windows that command is
|
|
80
|
+
| `statusline` | Claude Code's status line, the row above the bottom row. An existing status line of yours keeps running first, with our text after it, and is put back when you switch away or uninstall. On Windows that command runs in Git Bash when it is installed and in PowerShell otherwise, as Claude Code runs it; it gets 2 seconds. No red zone, because the auto-compact threshold is not known there. |
|
|
78
81
|
| `message` | A `Stop says: β¦` line in the transcript when there is advice. Claude does not see it. No red zone. |
|
|
79
82
|
|
|
83
|
+
`claude-jev-advisor report` shows each piece of advice with the time, the session, the size, Jev's answers, the start of the last request and what followed: compacted or cleared within the next three requests, auto-compacted, or kept going.
|
|
84
|
+
|
|
80
85
|
## The rm helper
|
|
81
86
|
|
|
82
87
|
It runs before every Bash tool call (`PreToolUse`, matcher `Bash`, 15-second timeout) and looks only at `rm`, `rmdir` and `xargs`.
|
|
@@ -87,7 +92,7 @@ It runs before every Bash tool call (`PreToolUse`, matcher `Bash`, 15-second tim
|
|
|
87
92
|
| A path with a `.superpowers` folder in it | No decision |
|
|
88
93
|
| A path that does not exist | No decision |
|
|
89
94
|
| A git-ignored path inside a build folder (`target`, `build`, `dist`, `out`, `node_modules`, `coverage`, `__pycache__`, `.pytest_cache`, `.gradle`, `.next`, `.nuxt`, `.turbo`, `.cache`, `bin`, `obj`) | No decision |
|
|
90
|
-
| Any other existing file or folder | `ask`, with the reason `μ€μ νμΌ μμ : <paths>` ("deleting real files") |
|
|
95
|
+
| Any other existing file or folder | `ask`, with the reason `μ€μ νμΌ μμ : <paths>` ("deleting real files"), unless Jev lifts it (below) |
|
|
91
96
|
| A target it cannot work out | `deny`, with a hint to rewrite the command using literal paths |
|
|
92
97
|
|
|
93
98
|
A target cannot be worked out when it uses:
|
|
@@ -99,9 +104,34 @@ A target cannot be worked out when it uses:
|
|
|
99
104
|
- a wildcard in a folder name
|
|
100
105
|
- a `cd` to such a path earlier in the command
|
|
101
106
|
|
|
107
|
+
It does not see deletions made any other way:
|
|
108
|
+
|
|
109
|
+
- 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.
|
|
110
|
+
- through a nested shell: `sh -c`, `bash -c`, `powershell -Command`, `pwsh -c`, `cmd /c del`
|
|
111
|
+
- with other commands or programs, such as `find -delete`, `git clean` or a script that deletes files
|
|
112
|
+
|
|
113
|
+
What its decisions do depends on Claude Code's permission mode:
|
|
114
|
+
|
|
115
|
+
| Mode | `ask` | `deny` | No decision |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| `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 |
|
|
118
|
+
| `bypassPermissions` | A prompt. Claude Code's documentation does not say this; it was seen in a test on 2026-10-03. | Blocked | Runs |
|
|
119
|
+
| Other modes | A prompt (`dontAsk` refuses the call instead) | Blocked | Claude Code's normal permission handling |
|
|
120
|
+
|
|
121
|
+
### When Jev can lift the ask
|
|
122
|
+
|
|
123
|
+
The facts are checked by code; Jev only judges what the target is for. Jev is asked only when every target of the command is one of these (at most five):
|
|
124
|
+
|
|
125
|
+
| Target | Checked first | Jev is asked |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| A file or folder this session made | Not tracked by git; the first call of this session that names it created it: a Write that made a new file, or a shell command whose `>` redirect, `touch`, `mkdir` or `curl -o` makes it (deletes do not count, and a Read, an Edit or any other shell command first means it was already there); made after the session started; for a folder, every file inside too, and at most 50 files | Is it a throwaway made only to try something out? |
|
|
128
|
+
| A folder git ignores, outside the build folders above | It is a folder (a single ignored file such as `.env` still asks) | Is it generated output or a cache that is made again? |
|
|
129
|
+
|
|
130
|
+
When Jev answers 0.8 or more for every target, the hook makes no decision (the last column of the table above), and a line like `[jev-advisor] Jevκ° μ΄ μΈμ
μ μν νμΌλ‘ νλ¨ν΄ λ¬»μ§ μκ³ μ§μλλ€: out.json (0.93)` is shown. Otherwise the ask stays, with Jev's answer added: `μ€μ νμΌ μμ : β¦\out.json Β· Jev: μνμ© νμΌμΌ νλ₯ 0.42`. Without a key, when Jev fails, or with `"jev": false`, every real file asks as before. A `deny` is never sent to Jev.
|
|
131
|
+
|
|
102
132
|
`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.
|
|
103
133
|
|
|
104
|
-
If a hook of this package fails in any way, it prints nothing and exits 0, so it never blocks Claude Code.
|
|
134
|
+
If a hook of this package fails in any way, it prints nothing and exits 0, so it never blocks Claude Code. A failure during the Jev check of `rm` leaves the ask in place.
|
|
105
135
|
|
|
106
136
|
## Uninstall
|
|
107
137
|
|
|
@@ -120,26 +150,28 @@ A key saved by `install` or `key` stays in `~/.claude/claude-jev-advisor/jev-key
|
|
|
120
150
|
|
|
121
151
|
| Path | Contents |
|
|
122
152
|
|---|---|
|
|
123
|
-
| `~/.claude/claude-jev-advisor/config.json` | Switches and settings. A missing or broken file reads as the defaults. A value of the wrong type falls back to its default, so only `"enabled": false` turns a helper off. |
|
|
153
|
+
| `~/.claude/claude-jev-advisor/config.json` | Switches and settings that differ from the defaults. A missing or broken file reads as the defaults. A value of the wrong type falls back to its default, so only `"enabled": false` turns a helper off. |
|
|
124
154
|
| `~/.claude/claude-jev-advisor/state/<session>.json` | The last judgment of each open session, read by the display. Removed when the session ends. |
|
|
125
|
-
| `~/.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. |
|
|
155
|
+
| `~/.claude/claude-jev-advisor/log/YYYY-MM.jsonl` | One line per context judgment and per `rm` Jev check: the size or the command, what was sent to Jev (parts of your conversation), the answers and the result. Never the key. |
|
|
126
156
|
| `~/.claude/claude-jev-advisor/statusline-before.json` | Your own status line while `--display statusline` is in use |
|
|
127
157
|
| `~/.claude/claude-jev-advisor/jev-key.env` | Your TypeSafe API key, when you typed it in `install` or `key` |
|
|
128
158
|
| `~/.claude/backups/settings.json.*-before-claude-jev-advisor` | Copies of `settings.json` from before each change |
|
|
129
159
|
|
|
130
|
-
|
|
160
|
+
Defaults:
|
|
131
161
|
|
|
132
162
|
```json
|
|
133
163
|
{
|
|
134
164
|
"lang": "ko",
|
|
135
165
|
"keyFile": null,
|
|
136
166
|
"display": "mod",
|
|
137
|
-
"context": { "enabled": true, "minTokens":
|
|
167
|
+
"context": { "enabled": true, "minTokens": 250000, "compactMinTokens": 250000, "redRemainingPct": 20, "unitDoneYes": 0.6, "phaseDoneYes": 0.6 },
|
|
138
168
|
"rm": { "enabled": true, "jev": true, "throwawayYes": 0.8, "maxDirFiles": 50 }
|
|
139
169
|
}
|
|
140
170
|
```
|
|
141
171
|
|
|
142
|
-
`minTokens` is where judging starts, `compactMinTokens` where `/compact` is suggested, `redRemainingPct` where the red zone starts, and `unitDoneYes` / `
|
|
172
|
+
`minTokens` is where judging starts, `compactMinTokens` where `/compact` is suggested, `redRemainingPct` where the red zone starts, and `unitDoneYes` / `phaseDoneYes` are the Jev probabilities needed for "work finished" (`/compact`) and "stage closed" (`/clear`). For `rm`, `jev` switches the Jev check, `throwawayYes` is the probability needed to lift an ask, and `maxDirFiles` the most files a folder of this session may hold.
|
|
173
|
+
|
|
174
|
+
`config.json` keeps only the values you changed, with `"version": 2`. A config written by 0.1.x, which saved every value, has its old default thresholds (`minTokens` 100000, `compactMinTokens` 200000, `unitDoneYes` 0.7) read as unset, so the new defaults apply.
|
|
143
175
|
|
|
144
176
|
## Development
|
|
145
177
|
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import os from "os";
|
|
5
|
-
import
|
|
5
|
+
import path10 from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// src/cli/prompt.ts
|
|
@@ -71,21 +71,23 @@ function terminalPrompt(terminal = PROCESS_TERMINAL) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// src/cli/run.ts
|
|
74
|
-
import
|
|
75
|
-
import
|
|
74
|
+
import fs9 from "fs";
|
|
75
|
+
import path9 from "path";
|
|
76
76
|
|
|
77
77
|
// src/config.ts
|
|
78
78
|
import fs from "fs";
|
|
79
79
|
import path2 from "path";
|
|
80
80
|
|
|
81
81
|
// src/config-shape.ts
|
|
82
|
+
var CONFIG_VERSION = 2;
|
|
82
83
|
var DEFAULT_CONFIG = {
|
|
83
84
|
lang: "ko",
|
|
84
85
|
keyFile: null,
|
|
85
86
|
display: "mod",
|
|
86
|
-
context: { enabled: true, minTokens:
|
|
87
|
+
context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
|
|
87
88
|
rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
|
|
88
89
|
};
|
|
90
|
+
var LEGACY_CONTEXT_DEFAULTS = { minTokens: 1e5, compactMinTokens: 2e5, unitDoneYes: 0.7 };
|
|
89
91
|
var LANGS = ["ko", "en"];
|
|
90
92
|
var DISPLAYS = ["mod", "statusline", "message"];
|
|
91
93
|
function isRecord(value) {
|
|
@@ -99,16 +101,33 @@ function mergeSection(defaults, raw) {
|
|
|
99
101
|
}
|
|
100
102
|
return out;
|
|
101
103
|
}
|
|
104
|
+
function withoutLegacyDefaults(context) {
|
|
105
|
+
if (!isRecord(context)) return context;
|
|
106
|
+
return Object.fromEntries(Object.entries(context).filter(([key, value]) => LEGACY_CONTEXT_DEFAULTS[key] !== value));
|
|
107
|
+
}
|
|
102
108
|
function normalizeConfig(raw) {
|
|
103
109
|
const r = isRecord(raw) ? raw : {};
|
|
104
110
|
return {
|
|
105
111
|
lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
|
|
106
112
|
keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
|
|
107
113
|
display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
|
|
108
|
-
context: mergeSection(DEFAULT_CONFIG.context, r.context),
|
|
114
|
+
context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
|
|
109
115
|
rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
|
|
110
116
|
};
|
|
111
117
|
}
|
|
118
|
+
function changedValues(values, defaults) {
|
|
119
|
+
return Object.fromEntries(Object.entries(values).filter(([key, value]) => value !== defaults[key]));
|
|
120
|
+
}
|
|
121
|
+
function configToSave(config) {
|
|
122
|
+
const { context, rm, ...top } = config;
|
|
123
|
+
const { context: defaultContext, rm: defaultRm, ...defaultTop } = DEFAULT_CONFIG;
|
|
124
|
+
const out = { version: CONFIG_VERSION, ...changedValues(top, defaultTop) };
|
|
125
|
+
const changedContext = changedValues(context, defaultContext);
|
|
126
|
+
const changedRm = changedValues(rm, defaultRm);
|
|
127
|
+
if (Object.keys(changedContext).length) out.context = changedContext;
|
|
128
|
+
if (Object.keys(changedRm).length) out.rm = changedRm;
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
112
131
|
|
|
113
132
|
// src/paths.ts
|
|
114
133
|
import path from "path";
|
|
@@ -118,6 +137,7 @@ var configPath = (home) => path.join(dataDir(home), "config.json");
|
|
|
118
137
|
var settingsPath = (home) => path.join(claudeDir(home), "settings.json");
|
|
119
138
|
var backupsDir = (home) => path.join(claudeDir(home), "backups");
|
|
120
139
|
var stateDir = (home) => path.join(dataDir(home), "state");
|
|
140
|
+
var logDir = (home) => path.join(dataDir(home), "log");
|
|
121
141
|
var statusLineBeforePath = (home) => path.join(dataDir(home), "statusline-before.json");
|
|
122
142
|
var savedKeyPath = (home) => path.join(dataDir(home), "jev-key.env");
|
|
123
143
|
|
|
@@ -132,7 +152,7 @@ function readConfig(home) {
|
|
|
132
152
|
function writeConfig(home, config) {
|
|
133
153
|
const file = configPath(home);
|
|
134
154
|
fs.mkdirSync(path2.dirname(file), { recursive: true });
|
|
135
|
-
fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}
|
|
155
|
+
fs.writeFileSync(file, `${JSON.stringify(configToSave(config), null, 2)}
|
|
136
156
|
`);
|
|
137
157
|
}
|
|
138
158
|
function updateConfig(home, change) {
|
|
@@ -247,8 +267,8 @@ function withoutModDisplay(settings, delimiter) {
|
|
|
247
267
|
const dirs = env?.[PLUGIN_DIRS_ENV];
|
|
248
268
|
if (env && typeof dirs === "string") {
|
|
249
269
|
const all = dirs.split(delimiter);
|
|
250
|
-
|
|
251
|
-
|
|
270
|
+
if (all.some((dir) => MOD_DIR_PATTERN.test(forwardSlashes2(dir)))) {
|
|
271
|
+
const kept = all.filter((dir) => dir !== "" && !MOD_DIR_PATTERN.test(forwardSlashes2(dir)));
|
|
252
272
|
const { [PLUGIN_DIRS_ENV]: _ours, ...otherEnv } = env;
|
|
253
273
|
const nextEnv = kept.length ? { ...env, [PLUGIN_DIRS_ENV]: kept.join(delimiter) } : otherEnv;
|
|
254
274
|
next = Object.keys(nextEnv).length ? { ...next, env: nextEnv } : withoutKey(next, "env");
|
|
@@ -264,11 +284,11 @@ function withoutModDisplay(settings, delimiter) {
|
|
|
264
284
|
function withModDisplay(settings, modDir, dataDirPath, delimiter) {
|
|
265
285
|
const base = withoutModDisplay(settings, delimiter);
|
|
266
286
|
const env = isRecord2(base.env) ? base.env : {};
|
|
267
|
-
const dirs = typeof env[PLUGIN_DIRS_ENV] === "string"
|
|
287
|
+
const dirs = typeof env[PLUGIN_DIRS_ENV] === "string" ? env[PLUGIN_DIRS_ENV].split(delimiter).filter(Boolean) : [];
|
|
268
288
|
const configs = isRecord2(base.pluginConfigs) ? base.pluginConfigs : {};
|
|
269
289
|
return {
|
|
270
290
|
...base,
|
|
271
|
-
env: { ...env, [PLUGIN_DIRS_ENV]:
|
|
291
|
+
env: { ...env, [PLUGIN_DIRS_ENV]: [...dirs, forwardSlashes2(modDir)].join(delimiter) },
|
|
272
292
|
pluginConfigs: { ...configs, [MOD_PLUGIN_NAME]: { options: { dataDir: forwardSlashes2(dataDirPath) } } }
|
|
273
293
|
};
|
|
274
294
|
}
|
|
@@ -548,10 +568,42 @@ async function promptForKey(prompt, check) {
|
|
|
548
568
|
}
|
|
549
569
|
}
|
|
550
570
|
|
|
551
|
-
// src/
|
|
552
|
-
import
|
|
571
|
+
// src/report.ts
|
|
572
|
+
import fs7 from "fs";
|
|
553
573
|
import path7 from "path";
|
|
554
574
|
|
|
575
|
+
// src/context/transcript.ts
|
|
576
|
+
import fs6 from "fs";
|
|
577
|
+
var TAIL_START_BYTES = 1024 * 1024;
|
|
578
|
+
var TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
579
|
+
var REQUEST_ORIGINS = /* @__PURE__ */ new Set(["human", "channel", "peer"]);
|
|
580
|
+
function isRecord3(value) {
|
|
581
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
582
|
+
}
|
|
583
|
+
function parseLines(text) {
|
|
584
|
+
const entries = [];
|
|
585
|
+
for (const line of text.split("\n")) {
|
|
586
|
+
if (!line.trim()) continue;
|
|
587
|
+
try {
|
|
588
|
+
const value = JSON.parse(line);
|
|
589
|
+
if (isRecord3(value)) entries.push(value);
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return entries;
|
|
594
|
+
}
|
|
595
|
+
function messageText(content) {
|
|
596
|
+
if (typeof content === "string") return content;
|
|
597
|
+
if (!Array.isArray(content)) return "";
|
|
598
|
+
return content.filter((block) => isRecord3(block) && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
599
|
+
}
|
|
600
|
+
function isRequest(entry) {
|
|
601
|
+
return entry.type === "user" && entry.isSidechain !== true && isRecord3(entry.origin) && REQUEST_ORIGINS.has(String(entry.origin.kind));
|
|
602
|
+
}
|
|
603
|
+
function requestText(entry) {
|
|
604
|
+
return messageText(isRecord3(entry.message) ? entry.message.content : void 0).trim();
|
|
605
|
+
}
|
|
606
|
+
|
|
555
607
|
// src/display/line.ts
|
|
556
608
|
var PHRASES = {
|
|
557
609
|
ko: {
|
|
@@ -565,7 +617,7 @@ var PHRASES = {
|
|
|
565
617
|
compactLater: "When this work is done, wrap up and continue? /compact"
|
|
566
618
|
}
|
|
567
619
|
};
|
|
568
|
-
function
|
|
620
|
+
function isRecord4(value) {
|
|
569
621
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
570
622
|
}
|
|
571
623
|
function formatSize(tokens) {
|
|
@@ -574,6 +626,11 @@ function formatSize(tokens) {
|
|
|
574
626
|
function remainingPct(size, threshold) {
|
|
575
627
|
return Math.max(0, Math.round((threshold - size) / threshold * 100));
|
|
576
628
|
}
|
|
629
|
+
function adviceKind(size, judgment, config) {
|
|
630
|
+
if (size === null || size < config.context.minTokens || !judgment || judgment.phase === "working") return null;
|
|
631
|
+
if (judgment.clear) return "clear";
|
|
632
|
+
return size >= config.context.compactMinTokens ? "compact" : null;
|
|
633
|
+
}
|
|
577
634
|
function adviceLine({ size, threshold, judgment, config }) {
|
|
578
635
|
if (size === null) return "";
|
|
579
636
|
const k = formatSize(size);
|
|
@@ -586,41 +643,181 @@ function adviceLine({ size, threshold, judgment, config }) {
|
|
|
586
643
|
return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
|
|
587
644
|
}
|
|
588
645
|
if (!counted) return k;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (size >= config.context.compactMinTokens) return `\u{1F7E1} ${k} ${phrases.compact}`;
|
|
592
|
-
return `\u{1F7E2} ${k}`;
|
|
646
|
+
const kind = adviceKind(size, counted, config);
|
|
647
|
+
return kind ? `\u{1F7E1} ${k} ${phrases[kind]}` : `\u{1F7E2} ${k}`;
|
|
593
648
|
}
|
|
594
649
|
function parseState(raw) {
|
|
595
|
-
if (!
|
|
650
|
+
if (!isRecord4(raw) || typeof raw.sessionId !== "string" || typeof raw.at !== "number") return null;
|
|
596
651
|
const j = raw.judgment;
|
|
597
|
-
const judgment =
|
|
652
|
+
const judgment = isRecord4(j) && (j.phase === "working" || j.phase === "unit_done") && typeof j.clear === "boolean" ? { phase: j.phase, clear: j.clear } : null;
|
|
598
653
|
return { sessionId: raw.sessionId, at: raw.at, size: typeof raw.size === "number" ? raw.size : null, judgment };
|
|
599
654
|
}
|
|
600
655
|
|
|
656
|
+
// src/report.ts
|
|
657
|
+
var FOLLOW_REQUESTS = 3;
|
|
658
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
659
|
+
function isRecord5(value) {
|
|
660
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
661
|
+
}
|
|
662
|
+
function monthFiles(home, since, now) {
|
|
663
|
+
const files = [];
|
|
664
|
+
for (let d = new Date(since.getFullYear(), since.getMonth(), 1); d <= now; d = new Date(d.getFullYear(), d.getMonth() + 1, 1)) {
|
|
665
|
+
files.push(path7.join(logDir(home), `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}.jsonl`));
|
|
666
|
+
}
|
|
667
|
+
return files;
|
|
668
|
+
}
|
|
669
|
+
function readLogRecords(home, since, now) {
|
|
670
|
+
const records = [];
|
|
671
|
+
for (const file of monthFiles(home, since, now)) {
|
|
672
|
+
let text;
|
|
673
|
+
try {
|
|
674
|
+
text = fs7.readFileSync(file, "utf8");
|
|
675
|
+
} catch {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
for (const record of parseLines(text)) {
|
|
679
|
+
const at = typeof record.at === "string" ? Date.parse(record.at) : NaN;
|
|
680
|
+
if (at >= since.getTime() && at <= now.getTime()) records.push(record);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return records;
|
|
684
|
+
}
|
|
685
|
+
function adviceOf(record, config) {
|
|
686
|
+
if ("advice" in record) return record.advice === "clear" || record.advice === "compact" ? record.advice : null;
|
|
687
|
+
const state = parseState({ sessionId: record.sessionId, at: 0, size: record.size, judgment: record.judgment });
|
|
688
|
+
return state ? adviceKind(state.size, state.judgment, config) : null;
|
|
689
|
+
}
|
|
690
|
+
function outcomeAfter(entries, at, endedByClear) {
|
|
691
|
+
let requests = 0;
|
|
692
|
+
for (const entry of entries) {
|
|
693
|
+
const time = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
|
|
694
|
+
if (!(time > at)) continue;
|
|
695
|
+
if (entry.type === "system" && entry.subtype === "compact_boundary") {
|
|
696
|
+
const trigger = isRecord5(entry.compactMetadata) ? entry.compactMetadata.trigger : void 0;
|
|
697
|
+
return { kind: trigger === "auto" ? "auto_compact" : "compact" };
|
|
698
|
+
}
|
|
699
|
+
if (isRequest(entry) && requestText(entry)) {
|
|
700
|
+
requests += 1;
|
|
701
|
+
if (requests >= FOLLOW_REQUESTS) return { kind: "kept", requests };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return endedByClear ? { kind: "clear" } : { kind: "kept", requests };
|
|
705
|
+
}
|
|
706
|
+
function adviceRows(home, now, days) {
|
|
707
|
+
const config = readConfig(home);
|
|
708
|
+
const records = readLogRecords(home, new Date(now.getTime() - days * DAY_MS), now);
|
|
709
|
+
const cleared = new Set(records.filter((r) => r.event === "session_end" && r.reason === "clear").map((r) => r.sessionId));
|
|
710
|
+
const transcripts = /* @__PURE__ */ new Map();
|
|
711
|
+
const entriesOf = (file) => {
|
|
712
|
+
if (typeof file !== "string") return null;
|
|
713
|
+
if (!transcripts.has(file)) {
|
|
714
|
+
try {
|
|
715
|
+
transcripts.set(file, parseLines(fs7.readFileSync(file, "utf8")));
|
|
716
|
+
} catch {
|
|
717
|
+
transcripts.set(file, null);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return transcripts.get(file) ?? null;
|
|
721
|
+
};
|
|
722
|
+
const rows = [];
|
|
723
|
+
for (const record of records) {
|
|
724
|
+
if (record.helper !== "context" || record.event !== "stop" || typeof record.sessionId !== "string" || typeof record.size !== "number") continue;
|
|
725
|
+
const advice = adviceOf(record, config);
|
|
726
|
+
if (!advice) continue;
|
|
727
|
+
const at = new Date(record.at);
|
|
728
|
+
const entries = entriesOf(record.transcriptPath);
|
|
729
|
+
const jev = isRecord5(record.jev) ? record.jev : {};
|
|
730
|
+
const state = isRecord5(jev.state) ? jev.state : {};
|
|
731
|
+
const requests = Array.isArray(state.recent_requests) ? state.recent_requests : [];
|
|
732
|
+
const last = requests[requests.length - 1];
|
|
733
|
+
rows.push({
|
|
734
|
+
at,
|
|
735
|
+
sessionId: record.sessionId,
|
|
736
|
+
size: record.size,
|
|
737
|
+
advice,
|
|
738
|
+
answers: isRecord5(jev.answers) ? jev.answers : null,
|
|
739
|
+
request: typeof last === "string" ? last : "",
|
|
740
|
+
outcome: entries ? outcomeAfter(entries, at.getTime(), cleared.has(record.sessionId)) : { kind: "unknown" }
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
return rows;
|
|
744
|
+
}
|
|
745
|
+
function localTime(date) {
|
|
746
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
747
|
+
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}`;
|
|
748
|
+
}
|
|
749
|
+
function outcomeText(outcome) {
|
|
750
|
+
switch (outcome.kind) {
|
|
751
|
+
case "compact":
|
|
752
|
+
return "compacted";
|
|
753
|
+
case "clear":
|
|
754
|
+
return "cleared";
|
|
755
|
+
case "auto_compact":
|
|
756
|
+
return "auto-compacted";
|
|
757
|
+
case "kept":
|
|
758
|
+
return outcome.requests >= FOLLOW_REQUESTS ? `kept going, ${FOLLOW_REQUESTS}+ requests` : `kept going, ${outcome.requests} request${outcome.requests === 1 ? "" : "s"}`;
|
|
759
|
+
default:
|
|
760
|
+
return "transcript not found";
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function answersText(answers) {
|
|
764
|
+
if (!answers) return "";
|
|
765
|
+
return Object.entries(answers).filter(([, p]) => typeof p === "number").map(([id, p]) => `${id.replace(/_done$/, "")} ${p.toFixed(2)}`).join(" ");
|
|
766
|
+
}
|
|
767
|
+
function rmLines(home, now, days) {
|
|
768
|
+
const records = readLogRecords(home, new Date(now.getTime() - days * DAY_MS), now).filter((r) => r.helper === "rm");
|
|
769
|
+
const passed = records.filter((r) => r.decision === "pass");
|
|
770
|
+
const lines = [`rm deletions Jev let through: ${passed.length} (asked anyway after a Jev check: ${records.length - passed.length})`];
|
|
771
|
+
for (const record of passed) {
|
|
772
|
+
const targets = Array.isArray(record.targets) ? record.targets.filter(isRecord5) : [];
|
|
773
|
+
const listed = targets.map((t) => `${String(t.shown)} (${typeof t.p === "number" ? t.p.toFixed(2) : "?"})`).join(", ");
|
|
774
|
+
lines.push(` ${localTime(new Date(record.at))} ${String(record.sessionId ?? "").slice(0, 8)} ${listed}`);
|
|
775
|
+
}
|
|
776
|
+
return lines;
|
|
777
|
+
}
|
|
778
|
+
function reportLines(home, now, days) {
|
|
779
|
+
const rows = adviceRows(home, now, days);
|
|
780
|
+
const lines = [`Advice shown in the last ${days} day${days === 1 ? "" : "s"}: ${rows.length}`];
|
|
781
|
+
for (const row of rows) {
|
|
782
|
+
const request = row.request.replace(/\s+/g, " ").trim().slice(0, 40);
|
|
783
|
+
lines.push(` ${localTime(row.at)} ${row.sessionId.slice(0, 8)} ${formatSize(row.size)} /${row.advice} ${answersText(row.answers)} "${request}" -> ${outcomeText(row.outcome)}`);
|
|
784
|
+
}
|
|
785
|
+
for (const advice of ["clear", "compact"]) {
|
|
786
|
+
const mine = rows.filter((r) => r.advice === advice);
|
|
787
|
+
if (!mine.length) continue;
|
|
788
|
+
const count = (kind) => mine.filter((r) => r.outcome.kind === kind).length;
|
|
789
|
+
lines.push(
|
|
790
|
+
`/${advice}: ${mine.length} - compacted ${count("compact")}, cleared ${count("clear")}, auto-compacted ${count("auto_compact")}, kept going ${count("kept")}, unknown ${count("unknown")}`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
return [...lines, ...rmLines(home, now, days)];
|
|
794
|
+
}
|
|
795
|
+
|
|
601
796
|
// src/status.ts
|
|
797
|
+
import fs8 from "fs";
|
|
798
|
+
import path8 from "path";
|
|
602
799
|
var SCRIPT_IN_COMMAND = /^node "([^"]+)"/;
|
|
603
|
-
function
|
|
800
|
+
function isRecord6(value) {
|
|
604
801
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
605
802
|
}
|
|
606
803
|
function displaySetUp(settings, config) {
|
|
607
804
|
if (config.display === "message") return true;
|
|
608
805
|
if (config.display === "statusline") return isOurStatusLine(settings.statusLine);
|
|
609
|
-
const env =
|
|
806
|
+
const env = isRecord6(settings.env) ? settings.env : {};
|
|
610
807
|
const dirs = typeof env[PLUGIN_DIRS_ENV] === "string" ? env[PLUGIN_DIRS_ENV] : "";
|
|
611
|
-
const hasDir = dirs.split(
|
|
612
|
-
return hasDir &&
|
|
808
|
+
const hasDir = dirs.split(path8.delimiter).some((dir) => MOD_DIR_PATTERN.test(dir.replace(/\\/g, "/")));
|
|
809
|
+
return hasDir && isRecord6(settings.pluginConfigs) && MOD_PLUGIN_NAME in settings.pluginConfigs;
|
|
613
810
|
}
|
|
614
811
|
function newestState(home) {
|
|
615
812
|
let newest = null;
|
|
616
813
|
try {
|
|
617
|
-
for (const name of
|
|
814
|
+
for (const name of fs8.readdirSync(stateDir(home))) {
|
|
618
815
|
if (!name.endsWith(".json")) continue;
|
|
619
|
-
const file =
|
|
620
|
-
const mtime =
|
|
816
|
+
const file = path8.join(stateDir(home), name);
|
|
817
|
+
const mtime = fs8.statSync(file).mtimeMs;
|
|
621
818
|
if (!newest || mtime > newest.mtime) newest = { file, mtime };
|
|
622
819
|
}
|
|
623
|
-
return newest ? parseState(JSON.parse(
|
|
820
|
+
return newest ? parseState(JSON.parse(fs8.readFileSync(newest.file, "utf8"))) : null;
|
|
624
821
|
} catch {
|
|
625
822
|
return null;
|
|
626
823
|
}
|
|
@@ -630,7 +827,8 @@ function contextLines(home, settings, config, env) {
|
|
|
630
827
|
const lines = [
|
|
631
828
|
` display: ${config.display}${displaySetUp(settings, config) ? "" : ` (not set up in settings.json - run "claude-jev-advisor install context --display ${config.display}")`}`,
|
|
632
829
|
` key: ${readJevKey(env, config.keyFile) ? "set" : "missing"}`,
|
|
633
|
-
` thresholds: judge from ${formatSize(c.minTokens)}, /compact from ${formatSize(c.compactMinTokens)}, red at ${c.redRemainingPct}% left
|
|
830
|
+
` thresholds: judge from ${formatSize(c.minTokens)}, /compact from ${formatSize(c.compactMinTokens)}, red at ${c.redRemainingPct}% left`,
|
|
831
|
+
` Jev: work done from ${c.unitDoneYes}, stage done (/clear) from ${c.phaseDoneYes}`
|
|
634
832
|
];
|
|
635
833
|
const last = newestState(home);
|
|
636
834
|
if (last) {
|
|
@@ -639,6 +837,11 @@ function contextLines(home, settings, config, env) {
|
|
|
639
837
|
}
|
|
640
838
|
return lines;
|
|
641
839
|
}
|
|
840
|
+
function rmJevLine(config, env) {
|
|
841
|
+
if (!config.rm.jev) return " jev: off";
|
|
842
|
+
const key = readJevKey(env, config.keyFile) ? "" : " (no key - every real file asks)";
|
|
843
|
+
return ` jev: on, lifts the ask from ${config.rm.throwawayYes}${key}`;
|
|
844
|
+
}
|
|
642
845
|
function statusLines(home, env = process.env) {
|
|
643
846
|
const lines = [`settings: ${settingsPath(home)}`, `config: ${configPath(home)}`];
|
|
644
847
|
let settings;
|
|
@@ -658,11 +861,12 @@ function statusLines(home, env = process.env) {
|
|
|
658
861
|
lines.push(`${feature}: installed, ${state}`);
|
|
659
862
|
for (const c of commands) {
|
|
660
863
|
const script = SCRIPT_IN_COMMAND.exec(c.command)?.[1];
|
|
661
|
-
if (script && !
|
|
864
|
+
if (script && !fs8.existsSync(script)) {
|
|
662
865
|
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}"`);
|
|
663
866
|
}
|
|
664
867
|
}
|
|
665
868
|
if (feature === "context") lines.push(...contextLines(home, settings, config, env));
|
|
869
|
+
if (feature === "rm") lines.push(rmJevLine(config, env));
|
|
666
870
|
}
|
|
667
871
|
if (findCommands(settings, LEGACY_RM_GUARD).length) {
|
|
668
872
|
lines.push('legacy rm-guard hook (~/.claude/hooks/rm-guard) is still registered - "claude-jev-advisor install rm" replaces it');
|
|
@@ -681,6 +885,7 @@ var USAGE = [
|
|
|
681
885
|
" off [rm] [context] switch helpers off (open sessions too)",
|
|
682
886
|
" status show what is registered and switched on",
|
|
683
887
|
" key ask for the TypeSafe API key, check it and save it",
|
|
888
|
+
" report [--days 7] list the /compact and /clear advice shown and what followed",
|
|
684
889
|
"",
|
|
685
890
|
"Options for install: --lang ko|en --key-file <path> --display mod|statusline|message",
|
|
686
891
|
'The display is set up when context is installed: "install context --display <mode>" switches it.',
|
|
@@ -688,6 +893,7 @@ var USAGE = [
|
|
|
688
893
|
'Run "claude-jev-advisor uninstall" before "npm rm -g": npm does not run uninstall scripts.'
|
|
689
894
|
].join("\n");
|
|
690
895
|
var INSTALL_OPTIONS = ["lang", "key-file", "display"];
|
|
896
|
+
var REPORT_DAYS = 7;
|
|
691
897
|
var LANGS2 = ["ko", "en"];
|
|
692
898
|
var DISPLAYS2 = ["mod", "statusline", "message"];
|
|
693
899
|
function parse(argv) {
|
|
@@ -711,7 +917,12 @@ function parse(argv) {
|
|
|
711
917
|
}
|
|
712
918
|
function checkOptions(command, options) {
|
|
713
919
|
const names = Object.keys(options);
|
|
714
|
-
if (command
|
|
920
|
+
if (command === "report") {
|
|
921
|
+
const unknown2 = names.find((n) => n !== "days");
|
|
922
|
+
if (unknown2) return `unknown option --${unknown2}`;
|
|
923
|
+
return options.days === void 0 || /^[1-9]\d{0,3}$/.test(options.days) ? null : "--days takes a whole number of days from 1";
|
|
924
|
+
}
|
|
925
|
+
if (command !== "install") return names.length ? `options are only for install and report (got --${names[0]})` : null;
|
|
715
926
|
const unknown = names.find((n) => !INSTALL_OPTIONS.includes(n));
|
|
716
927
|
if (unknown) return `unknown option --${unknown}`;
|
|
717
928
|
if (options.lang !== void 0 && !LANGS2.includes(options.lang)) return "--lang takes ko or en";
|
|
@@ -728,7 +939,7 @@ async function askAndSaveKey(io, prompt) {
|
|
|
728
939
|
const file = saveJevKey(io.home, key);
|
|
729
940
|
updateConfig(io.home, (c) => ({ ...c, keyFile: file }));
|
|
730
941
|
io.out(`Saved the key to ${file}`);
|
|
731
|
-
if (before &&
|
|
942
|
+
if (before && path9.resolve(before) !== path9.resolve(file)) io.out(`The config now uses this file instead of ${before}.`);
|
|
732
943
|
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
944
|
}
|
|
734
945
|
async function ensureKey(io, keyFileGiven) {
|
|
@@ -763,7 +974,7 @@ async function runCli(argv, io) {
|
|
|
763
974
|
platform: io.platform,
|
|
764
975
|
now: io.now(),
|
|
765
976
|
lang: options.lang,
|
|
766
|
-
keyFile: options["key-file"] === void 0 ||
|
|
977
|
+
keyFile: options["key-file"] === void 0 || path9.isAbsolute(options["key-file"]) ? options["key-file"] : path9.resolve(options["key-file"]),
|
|
767
978
|
display: options.display
|
|
768
979
|
});
|
|
769
980
|
for (const f of r.installed) io.out(`installed ${f}`);
|
|
@@ -784,7 +995,7 @@ async function runCli(argv, io) {
|
|
|
784
995
|
if (!r.removed.length) io.out("nothing to remove");
|
|
785
996
|
for (const f of r.removed) io.out(`removed ${f}`);
|
|
786
997
|
if (r.backup) io.out(`backup: ${r.backup}`);
|
|
787
|
-
if (r.removed.includes("context") &&
|
|
998
|
+
if (r.removed.includes("context") && fs9.existsSync(savedKeyPath(io.home))) io.out(`kept your saved TypeSafe key at ${savedKeyPath(io.home)}`);
|
|
788
999
|
return 0;
|
|
789
1000
|
}
|
|
790
1001
|
case "key": {
|
|
@@ -803,6 +1014,9 @@ async function runCli(argv, io) {
|
|
|
803
1014
|
case "status":
|
|
804
1015
|
for (const line of statusLines(io.home)) io.out(line);
|
|
805
1016
|
return 0;
|
|
1017
|
+
case "report":
|
|
1018
|
+
for (const line of reportLines(io.home, io.now(), options.days === void 0 ? REPORT_DAYS : Number(options.days))) io.out(line);
|
|
1019
|
+
return 0;
|
|
806
1020
|
case "help":
|
|
807
1021
|
case "--help":
|
|
808
1022
|
case "-h":
|
|
@@ -822,7 +1036,7 @@ async function runCli(argv, io) {
|
|
|
822
1036
|
// src/cli.ts
|
|
823
1037
|
runCli(process.argv.slice(2), {
|
|
824
1038
|
home: os.homedir(),
|
|
825
|
-
distDir:
|
|
1039
|
+
distDir: path10.dirname(fileURLToPath(import.meta.url)),
|
|
826
1040
|
platform: process.platform,
|
|
827
1041
|
now: () => /* @__PURE__ */ new Date(),
|
|
828
1042
|
out: (line) => console.log(line),
|