opencode-skill-audit 0.1.2 → 0.2.0-dev.pr3.36976113543
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 +45 -14
- package/dist/core.d.ts +84 -0
- package/dist/index.js +5 -1
- package/dist/log.d.ts +2 -62
- package/dist/tui.js +51 -41
- package/dist/view.d.ts +8 -4
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ Deterministic audit trail for [Claude Code](https://code.claude.com),
|
|
|
12
12
|
observable **skills** were invoked, when, and which **files** were changed afterwards. It gives you
|
|
13
13
|
a quick skill-compliance signal before you read the code.
|
|
14
14
|
|
|
15
|
-
On opencode it also renders live in the TUI sidebar,
|
|
16
|
-
with no command to run.
|
|
15
|
+
On opencode it also renders live in the TUI sidebar, and on Claude Code in a live pane, so the audit
|
|
16
|
+
sits next to the conversation with no command to run.
|
|
17
17
|
|
|
18
18
|
```text
|
|
19
19
|
● Skill audit — f3a91c2e · ~/Dev/Web · 14:02→16:40
|
|
@@ -90,7 +90,8 @@ Claude Code / Codex ──hooks───▶ logger.sh ──┐
|
|
|
90
90
|
├─▶ ~/.claude/skill-audit/<sid>.ndjson
|
|
91
91
|
opencode ──tool.execute.after──▶ plugin ────┘ │
|
|
92
92
|
├─▶ skill-audit status/report/watch/list
|
|
93
|
-
|
|
93
|
+
├─▶ opencode sidebar (live)
|
|
94
|
+
└─▶ Claude Code pane (live)
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
The log is the only contract between the writers and the viewers, so the CLI reads opencode
|
|
@@ -109,6 +110,22 @@ Subagent hook calls use the parent session ID, so delegated edits appear in the
|
|
|
109
110
|
|
|
110
111
|
Restart Claude Code so the hooks load.
|
|
111
112
|
|
|
113
|
+
#### Live pane
|
|
114
|
+
|
|
115
|
+
The plugin also opens a **Skill audit** pane showing the current session's timeline. It refreshes
|
|
116
|
+
after every skill and file-edit tool call, and polls the log every two seconds.
|
|
117
|
+
|
|
118
|
+
- **Opening:** it opens by itself at session start once the terminal is 144 columns or wider.
|
|
119
|
+
Narrower terminals hold it back until you run `/skill-audit-pane`, which opens it at any width.
|
|
120
|
+
- **Placement:** in fullscreen from 110 columns it docks beside the transcript and shows the full
|
|
121
|
+
timeline. Press a `▼`/`▶` marker to collapse or expand a run or an hour. Elsewhere it sits above
|
|
122
|
+
the prompt in a compact form: the header and the latest run.
|
|
123
|
+
- **Other panes:** panes from other plugins become tabs beside it (click a tab, or ctrl+x tab).
|
|
124
|
+
Only one is shown at a time. Esc or the close mark closes it.
|
|
125
|
+
|
|
126
|
+
The pane is display only. The shell hooks keep writing the log through `logger.sh`, so recording
|
|
127
|
+
works the same on Claude Code builds without plugin function hooks.
|
|
128
|
+
|
|
112
129
|
> **Migrating from a manual setup?** Remove any `logger.sh` entries from the `hooks` block of
|
|
113
130
|
> `~/.claude/settings.json` first, or every event is logged twice.
|
|
114
131
|
|
|
@@ -234,6 +251,7 @@ If `list` prints `no session logs in ...`, nothing was written. Go to
|
|
|
234
251
|
| `skill-audit --help` | Usage summary | 0 |
|
|
235
252
|
| `! skill-audit status` | Run inside a Claude Code session. Queues while the model is busy | 0 |
|
|
236
253
|
| opencode sidebar | Live timeline beside the conversation. No command to run | 0 |
|
|
254
|
+
| Claude Code pane | Live timeline in a pane. `/skill-audit-pane` opens it | 0 |
|
|
237
255
|
| `/skill-audit` | Show the report inside Claude Code | Model turn |
|
|
238
256
|
| `$skill-audit` | Show the report inside Codex | Model turn |
|
|
239
257
|
|
|
@@ -361,6 +379,8 @@ and sidebar are TypeScript (`src/`) built with [Bun](https://bun.sh).
|
|
|
361
379
|
|-------------------|----------------------------------------------------------------------------|
|
|
362
380
|
| `scripts/` | `logger.sh` hook target, the `skill-audit` CLI, `sync-versions.mjs` |
|
|
363
381
|
| `src/` | opencode plugin (`index.ts`), sidebar (`tui.ts`), shared log and view code |
|
|
382
|
+
| `hooks/` | Claude Code shell hooks (`hooks.json`) and the live pane (`claude.tsx`) |
|
|
383
|
+
| `types/` | `$.state` contract for the Claude Code pane |
|
|
364
384
|
| `test/` | Bun tests, fixtures, and `test/format-contract.sh` |
|
|
365
385
|
| `.claude-plugin/` | Claude Code plugin manifest and marketplace entry |
|
|
366
386
|
| `.codex-plugin/` | Codex plugin manifest |
|
|
@@ -375,17 +395,28 @@ bun run check # format:check + lint + typecheck (src and test) + tests
|
|
|
375
395
|
bun run build # dist/index.js (plugin) and dist/tui.js (sidebar)
|
|
376
396
|
```
|
|
377
397
|
|
|
378
|
-
| Script
|
|
379
|
-
|
|
380
|
-
| `bun run format`
|
|
381
|
-
| `bun run lint`
|
|
382
|
-
| `bun run typecheck`
|
|
383
|
-
| `bun test`
|
|
384
|
-
| `bun run check`
|
|
385
|
-
| `bun run
|
|
386
|
-
| `bun run
|
|
387
|
-
|
|
388
|
-
|
|
398
|
+
| Script | What |
|
|
399
|
+
|----------------------------|----------------------------------------------------------|
|
|
400
|
+
| `bun run format` | Prettier, write mode |
|
|
401
|
+
| `bun run lint` | ESLint over `src` and `test` |
|
|
402
|
+
| `bun run typecheck` | `tsc --noEmit` for `src` |
|
|
403
|
+
| `bun test` | Bun test suite in `test/` |
|
|
404
|
+
| `bun run check` | Everything CI runs |
|
|
405
|
+
| `bun run test:claude` | Validate and test the Claude Code pane (needs `claude`) |
|
|
406
|
+
| `bun run typecheck:claude` | Type-check the pane (needs `.claude-plugin/types/`) |
|
|
407
|
+
| `bun run check:versions` | Assert `package.json` and both `plugin.json` files agree |
|
|
408
|
+
| `bun run sync:versions` | Rewrite the plugin manifests from `package.json` |
|
|
409
|
+
|
|
410
|
+
Tests live in `test/`. The Claude Code pane is the exception: its tests (`hooks/*.test.tsx`) run
|
|
411
|
+
in Claude Code's mod sandbox, which has no Node or Bun, so `bun test` is rooted at `test/`
|
|
412
|
+
(`bunfig.toml`). `test:claude` copies the pane's files into a scratch folder and runs
|
|
413
|
+
`claude plugin validate` and `claude plugin test` there. `src/core.ts` and `src/view.ts` must stay
|
|
414
|
+
free of `node:*` imports and `process` because the pane imports them; `test/core.test.ts` checks
|
|
415
|
+
this. `typecheck:claude` reads the API types the engine writes to `.claude-plugin/types/` once it
|
|
416
|
+
has loaded the plugin from a local folder. That folder is gitignored and not available in CI, so
|
|
417
|
+
neither pane check runs in CI.
|
|
418
|
+
|
|
419
|
+
`test/format-contract.sh` pins the rendered CLI output against fixtures, so
|
|
389
420
|
a change to the timeline format has to be updated there deliberately. That output is the contract
|
|
390
421
|
the sidebar and any third-party viewer rely on.
|
|
391
422
|
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The log model with no Node dependency. The Claude Code pane runs in a
|
|
3
|
+
* sandbox without `node:*` modules or `process`, so everything it shares with
|
|
4
|
+
* the CLI and the opencode sidebar lives here; `log.ts` adds the file I/O.
|
|
5
|
+
*/
|
|
6
|
+
export type SkillEvent = {
|
|
7
|
+
ts: string;
|
|
8
|
+
kind: "skill";
|
|
9
|
+
name: string;
|
|
10
|
+
args?: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
source?: string;
|
|
13
|
+
};
|
|
14
|
+
export type FileEvent = {
|
|
15
|
+
ts: string;
|
|
16
|
+
kind: "file";
|
|
17
|
+
tool: string;
|
|
18
|
+
path: string;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
};
|
|
21
|
+
export type AuditEvent = SkillEvent | FileEvent;
|
|
22
|
+
/**
|
|
23
|
+
* An environment variable where the host has one, `undefined` where it does
|
|
24
|
+
* not. Read through `globalThis` because a bare `process` throws in the Claude
|
|
25
|
+
* Code sandbox.
|
|
26
|
+
*/
|
|
27
|
+
export declare function env(name: string): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Parse NDJSON log text. Malformed lines are dropped rather than thrown on: the
|
|
30
|
+
* sidebar reads the log while the logger is appending to it, so a truncated
|
|
31
|
+
* trailing line is expected, not exceptional.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parse(text: string): AuditEvent[];
|
|
34
|
+
export declare const NO_SKILL = "(no skill active)";
|
|
35
|
+
export type TimelineFile = {
|
|
36
|
+
ts: string;
|
|
37
|
+
tool: string;
|
|
38
|
+
path: string;
|
|
39
|
+
};
|
|
40
|
+
export type SkillRun = {
|
|
41
|
+
skill: string;
|
|
42
|
+
ts: string;
|
|
43
|
+
files: TimelineFile[];
|
|
44
|
+
};
|
|
45
|
+
/** How long a skill run may sit idle before it stops claiming later edits. */
|
|
46
|
+
export declare const DEFAULT_IDLE_GAP_MS: number;
|
|
47
|
+
/** `SKILL_AUDIT_IDLE_MINUTES` as milliseconds, or the default when unusable. */
|
|
48
|
+
export declare function idleGapFrom(minutes: string | undefined): number;
|
|
49
|
+
/**
|
|
50
|
+
* The idle gap, in milliseconds. Without one a single skill invoked in the
|
|
51
|
+
* morning claims every edit made for the rest of the day.
|
|
52
|
+
*/
|
|
53
|
+
export declare function idleGapMs(): number;
|
|
54
|
+
/**
|
|
55
|
+
* Group a flat event list into skill runs, each carrying the files edited after
|
|
56
|
+
* it. Port of the jq reduce in scripts/skill-audit; the two must agree.
|
|
57
|
+
*
|
|
58
|
+
* A run only claims edits that keep arriving: once `gapMs` passes with no
|
|
59
|
+
* activity, later files fall into a synthetic run instead. An unparseable
|
|
60
|
+
* timestamp compares as NaN, which never exceeds the gap, so logs written
|
|
61
|
+
* before this rule existed group exactly as they did before.
|
|
62
|
+
*/
|
|
63
|
+
export declare function group(events: AuditEvent[], gapMs?: number): SkillRun[];
|
|
64
|
+
export type Summary = {
|
|
65
|
+
runs: number;
|
|
66
|
+
distinct: number;
|
|
67
|
+
files: number;
|
|
68
|
+
orphan: number;
|
|
69
|
+
};
|
|
70
|
+
export declare function summarize(events: AuditEvent[], gapMs?: number): Summary;
|
|
71
|
+
export type SessionView = {
|
|
72
|
+
runs: SkillRun[];
|
|
73
|
+
summary: Summary;
|
|
74
|
+
cwd: string;
|
|
75
|
+
};
|
|
76
|
+
/** A session's view from its raw log text; empty text is an empty session. */
|
|
77
|
+
export declare function toView(text: string, gapMs?: number): SessionView;
|
|
78
|
+
/** `node:path` basename for POSIX paths: the last segment, trailing slashes ignored. */
|
|
79
|
+
export declare function baseName(path: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* `path` relative to `cwd` when it sits under it, else `path` as given. The
|
|
82
|
+
* sidebar only ever shortens paths inside the session directory.
|
|
83
|
+
*/
|
|
84
|
+
export declare function relativeTo(path: string, cwd: string): string;
|
package/dist/index.js
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
import { appendFileSync, mkdirSync, readFileSync } from "fs";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { isAbsolute, join } from "path";
|
|
6
|
+
|
|
7
|
+
// src/core.ts
|
|
8
|
+
var DEFAULT_IDLE_GAP_MS = 30 * 60000;
|
|
9
|
+
|
|
10
|
+
// src/log.ts
|
|
6
11
|
function logDir() {
|
|
7
12
|
return process.env.SKILL_AUDIT_DIR || join(homedir(), ".claude", "skill-audit");
|
|
8
13
|
}
|
|
9
14
|
function logPath(sessionID) {
|
|
10
15
|
return join(logDir(), `${sessionID}.ndjson`);
|
|
11
16
|
}
|
|
12
|
-
var DEFAULT_IDLE_GAP_MS = 30 * 60000;
|
|
13
17
|
function append(sessionID, event) {
|
|
14
18
|
try {
|
|
15
19
|
mkdirSync(logDir(), { recursive: true });
|
package/dist/log.d.ts
CHANGED
|
@@ -1,63 +1,8 @@
|
|
|
1
|
+
import { type SessionView } from "./core";
|
|
2
|
+
export * from "./core";
|
|
1
3
|
/** Directory holding the per-session NDJSON logs, shared with the bash logger. */
|
|
2
4
|
export declare function logDir(): string;
|
|
3
5
|
export declare function logPath(sessionID: string): string;
|
|
4
|
-
export type SkillEvent = {
|
|
5
|
-
ts: string;
|
|
6
|
-
kind: "skill";
|
|
7
|
-
name: string;
|
|
8
|
-
args?: string;
|
|
9
|
-
cwd?: string;
|
|
10
|
-
source?: string;
|
|
11
|
-
};
|
|
12
|
-
export type FileEvent = {
|
|
13
|
-
ts: string;
|
|
14
|
-
kind: "file";
|
|
15
|
-
tool: string;
|
|
16
|
-
path: string;
|
|
17
|
-
cwd?: string;
|
|
18
|
-
};
|
|
19
|
-
export type AuditEvent = SkillEvent | FileEvent;
|
|
20
|
-
/**
|
|
21
|
-
* Parse NDJSON log text. Malformed lines are dropped rather than thrown on: the
|
|
22
|
-
* sidebar reads the log while the logger is appending to it, so a truncated
|
|
23
|
-
* trailing line is expected, not exceptional.
|
|
24
|
-
*/
|
|
25
|
-
export declare function parse(text: string): AuditEvent[];
|
|
26
|
-
export declare const NO_SKILL = "(no skill active)";
|
|
27
|
-
export type TimelineFile = {
|
|
28
|
-
ts: string;
|
|
29
|
-
tool: string;
|
|
30
|
-
path: string;
|
|
31
|
-
};
|
|
32
|
-
export type SkillRun = {
|
|
33
|
-
skill: string;
|
|
34
|
-
ts: string;
|
|
35
|
-
files: TimelineFile[];
|
|
36
|
-
};
|
|
37
|
-
/** How long a skill run may sit idle before it stops claiming later edits. */
|
|
38
|
-
export declare const DEFAULT_IDLE_GAP_MS: number;
|
|
39
|
-
/**
|
|
40
|
-
* The idle gap, in milliseconds. Without one a single skill invoked in the
|
|
41
|
-
* morning claims every edit made for the rest of the day.
|
|
42
|
-
*/
|
|
43
|
-
export declare function idleGapMs(): number;
|
|
44
|
-
/**
|
|
45
|
-
* Group a flat event list into skill runs, each carrying the files edited after
|
|
46
|
-
* it. Port of the jq reduce in scripts/skill-audit; the two must agree.
|
|
47
|
-
*
|
|
48
|
-
* A run only claims edits that keep arriving: once `gapMs` passes with no
|
|
49
|
-
* activity, later files fall into a synthetic run instead. An unparseable
|
|
50
|
-
* timestamp compares as NaN, which never exceeds the gap, so logs written
|
|
51
|
-
* before this rule existed group exactly as they did before.
|
|
52
|
-
*/
|
|
53
|
-
export declare function group(events: AuditEvent[], gapMs?: number): SkillRun[];
|
|
54
|
-
export type Summary = {
|
|
55
|
-
runs: number;
|
|
56
|
-
distinct: number;
|
|
57
|
-
files: number;
|
|
58
|
-
orphan: number;
|
|
59
|
-
};
|
|
60
|
-
export declare function summarize(events: AuditEvent[]): Summary;
|
|
61
6
|
export declare function appendSkill(sessionID: string, input: {
|
|
62
7
|
ts: string;
|
|
63
8
|
name: string;
|
|
@@ -77,11 +22,6 @@ export declare function appendFile(sessionID: string, input: {
|
|
|
77
22
|
export declare function parsePatch(args: unknown): string[];
|
|
78
23
|
/** Second-precision UTC timestamp, matching `date -u +%FT%TZ` in logger.sh. */
|
|
79
24
|
export declare function nowTs(date?: Date): string;
|
|
80
|
-
export type SessionView = {
|
|
81
|
-
runs: SkillRun[];
|
|
82
|
-
summary: Summary;
|
|
83
|
-
cwd: string;
|
|
84
|
-
};
|
|
85
25
|
/**
|
|
86
26
|
* Read one session's log. A missing or unreadable file is an empty session, not
|
|
87
27
|
* an error: the sidebar renders before the first event is ever written.
|
package/dist/tui.js
CHANGED
|
@@ -7,11 +7,11 @@ import { createSignal, onCleanup } from "solid-js";
|
|
|
7
7
|
import { appendFileSync, mkdirSync, readFileSync } from "fs";
|
|
8
8
|
import { homedir } from "os";
|
|
9
9
|
import { isAbsolute, join } from "path";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return
|
|
10
|
+
|
|
11
|
+
// src/core.ts
|
|
12
|
+
function env(name) {
|
|
13
|
+
const host = globalThis;
|
|
14
|
+
return host.process?.env?.[name];
|
|
15
15
|
}
|
|
16
16
|
function parse(text) {
|
|
17
17
|
const events = [];
|
|
@@ -31,9 +31,12 @@ function parse(text) {
|
|
|
31
31
|
}
|
|
32
32
|
var NO_SKILL = "(no skill active)";
|
|
33
33
|
var DEFAULT_IDLE_GAP_MS = 30 * 60000;
|
|
34
|
+
function idleGapFrom(minutes) {
|
|
35
|
+
const value = Number(minutes);
|
|
36
|
+
return Number.isFinite(value) && value > 0 ? value * 60000 : DEFAULT_IDLE_GAP_MS;
|
|
37
|
+
}
|
|
34
38
|
function idleGapMs() {
|
|
35
|
-
|
|
36
|
-
return Number.isFinite(minutes) && minutes > 0 ? minutes * 60000 : DEFAULT_IDLE_GAP_MS;
|
|
39
|
+
return idleGapFrom(env("SKILL_AUDIT_IDLE_MINUTES"));
|
|
37
40
|
}
|
|
38
41
|
function epoch(ts) {
|
|
39
42
|
return new Date(ts).getTime();
|
|
@@ -69,7 +72,7 @@ function group(events, gapMs = idleGapMs()) {
|
|
|
69
72
|
}
|
|
70
73
|
return runs;
|
|
71
74
|
}
|
|
72
|
-
function summarize(events) {
|
|
75
|
+
function summarize(events, gapMs = idleGapMs()) {
|
|
73
76
|
const names = [];
|
|
74
77
|
const paths = new Set;
|
|
75
78
|
for (const event of events) {
|
|
@@ -79,7 +82,7 @@ function summarize(events) {
|
|
|
79
82
|
paths.add(event.path);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
|
-
const orphan = group(events).filter((run) => run.skill === NO_SKILL).reduce((total, run) => total + run.files.length, 0);
|
|
85
|
+
const orphan = group(events, gapMs).filter((run) => run.skill === NO_SKILL).reduce((total, run) => total + run.files.length, 0);
|
|
83
86
|
return {
|
|
84
87
|
runs: names.length,
|
|
85
88
|
distinct: new Set(names).size,
|
|
@@ -87,34 +90,41 @@ function summarize(events) {
|
|
|
87
90
|
orphan
|
|
88
91
|
};
|
|
89
92
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
function toView(text, gapMs = idleGapMs()) {
|
|
94
|
+
const events = parse(text);
|
|
95
|
+
const cwd = events.find((event) => event.cwd)?.cwd ?? "";
|
|
96
|
+
return {
|
|
97
|
+
runs: group(events, gapMs),
|
|
98
|
+
summary: summarize(events, gapMs),
|
|
99
|
+
cwd
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function baseName(path) {
|
|
103
|
+
const trimmed = path.replace(/\/+$/, "");
|
|
104
|
+
return trimmed.slice(trimmed.lastIndexOf("/") + 1);
|
|
105
|
+
}
|
|
106
|
+
function relativeTo(path, cwd) {
|
|
107
|
+
return cwd && path.startsWith(`${cwd}/`) ? path.slice(cwd.length + 1) : path;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/log.ts
|
|
111
|
+
function logDir() {
|
|
112
|
+
return process.env.SKILL_AUDIT_DIR || join(homedir(), ".claude", "skill-audit");
|
|
113
|
+
}
|
|
114
|
+
function logPath(sessionID) {
|
|
115
|
+
return join(logDir(), `${sessionID}.ndjson`);
|
|
116
|
+
}
|
|
100
117
|
function readSession(sessionID) {
|
|
101
118
|
let text;
|
|
102
119
|
try {
|
|
103
120
|
text = readFileSync(logPath(sessionID), "utf8");
|
|
104
121
|
} catch {
|
|
105
|
-
|
|
122
|
+
text = "";
|
|
106
123
|
}
|
|
107
|
-
|
|
108
|
-
const cwd = events.find((event) => event.cwd)?.cwd ?? "";
|
|
109
|
-
return {
|
|
110
|
-
runs: group(events),
|
|
111
|
-
summary: summarize(events),
|
|
112
|
-
cwd
|
|
113
|
-
};
|
|
124
|
+
return toView(text);
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
// src/view.ts
|
|
117
|
-
import { basename, relative } from "path";
|
|
118
128
|
var TEXT_ICONS = {
|
|
119
129
|
run: "\xB7",
|
|
120
130
|
file: "\u270E",
|
|
@@ -129,8 +139,11 @@ var EMOJI_ICONS = {
|
|
|
129
139
|
open: "\u25BC",
|
|
130
140
|
closed: "\u25B6"
|
|
131
141
|
};
|
|
142
|
+
function iconsFor(style) {
|
|
143
|
+
return style === "emoji" ? EMOJI_ICONS : TEXT_ICONS;
|
|
144
|
+
}
|
|
132
145
|
function icons() {
|
|
133
|
-
return
|
|
146
|
+
return iconsFor(env("SKILL_AUDIT_ICONS"));
|
|
134
147
|
}
|
|
135
148
|
function pad(value) {
|
|
136
149
|
return String(value).padStart(2, "0");
|
|
@@ -168,30 +181,27 @@ function bucketByHour(files) {
|
|
|
168
181
|
}
|
|
169
182
|
return buckets;
|
|
170
183
|
}
|
|
171
|
-
function headerLine(summary) {
|
|
172
|
-
const icon = icons();
|
|
184
|
+
function headerLine(summary, icon = icons()) {
|
|
173
185
|
const counts = `${icon.run}${summary.runs} ${icon.file}${summary.files}${summary.orphan > 0 ? ` ${icon.warn}${summary.orphan}` : ""}`;
|
|
174
186
|
return `Skill audit ${counts}`;
|
|
175
187
|
}
|
|
176
|
-
function runTitle(run, collapsed) {
|
|
177
|
-
const icon = icons();
|
|
188
|
+
function runTitle(run, collapsed, icon = icons()) {
|
|
178
189
|
const name = run.skill === NO_SKILL ? `${icon.warn} no skill` : shortName(run.skill);
|
|
179
190
|
if (run.files.length === 0) {
|
|
180
191
|
return ` ${name}`;
|
|
181
192
|
}
|
|
182
193
|
return collapsed ? `${icon.closed} ${name} (${run.files.length})` : `${icon.open} ${name}`;
|
|
183
194
|
}
|
|
184
|
-
function hourTitle(bucket, collapsed) {
|
|
185
|
-
const icon = icons();
|
|
195
|
+
function hourTitle(bucket, collapsed, icon = icons()) {
|
|
186
196
|
const marker = collapsed ? icon.closed : icon.open;
|
|
187
197
|
return `${marker} ${bucket.hour} (${bucket.files.length})`;
|
|
188
198
|
}
|
|
189
199
|
function displayPath(path, cwd, width) {
|
|
190
|
-
const rel =
|
|
200
|
+
const rel = relativeTo(path, cwd);
|
|
191
201
|
if (rel.length <= width) {
|
|
192
202
|
return rel;
|
|
193
203
|
}
|
|
194
|
-
const base =
|
|
204
|
+
const base = baseName(rel);
|
|
195
205
|
if (base.length <= width) {
|
|
196
206
|
return base;
|
|
197
207
|
}
|
|
@@ -206,11 +216,11 @@ function hourKeyOf(index, bucket) {
|
|
|
206
216
|
return `${runKey(index)}/hour:${bucket.day} ${bucket.hour}`;
|
|
207
217
|
}
|
|
208
218
|
function sidebarLines(view, options) {
|
|
209
|
-
const icon = icons();
|
|
219
|
+
const icon = options.icons ?? icons();
|
|
210
220
|
const marker = options.sectionOpen ? icon.open : icon.closed;
|
|
211
221
|
const lines = [
|
|
212
222
|
{
|
|
213
|
-
text: `${marker} ${headerLine(view.summary)}`,
|
|
223
|
+
text: `${marker} ${headerLine(view.summary, icon)}`,
|
|
214
224
|
tone: "text"
|
|
215
225
|
}
|
|
216
226
|
];
|
|
@@ -228,7 +238,7 @@ function sidebarLines(view, options) {
|
|
|
228
238
|
const key = runKey(index);
|
|
229
239
|
const runCollapsed = options.collapsed.has(key);
|
|
230
240
|
lines.push({
|
|
231
|
-
text: runTitle(run, runCollapsed),
|
|
241
|
+
text: runTitle(run, runCollapsed, icon),
|
|
232
242
|
tone: run.skill === NO_SKILL ? "warning" : "accent",
|
|
233
243
|
key
|
|
234
244
|
});
|
|
@@ -239,7 +249,7 @@ function sidebarLines(view, options) {
|
|
|
239
249
|
const bucketKey = hourKeyOf(index, bucket);
|
|
240
250
|
const hourCollapsed = options.collapsed.has(bucketKey);
|
|
241
251
|
lines.push({
|
|
242
|
-
text: `${HOUR_INDENT}${hourTitle(bucket, hourCollapsed)}`,
|
|
252
|
+
text: `${HOUR_INDENT}${hourTitle(bucket, hourCollapsed, icon)}`,
|
|
243
253
|
tone: "text",
|
|
244
254
|
key: bucketKey
|
|
245
255
|
});
|
package/dist/view.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SessionView, type SkillRun, type Summary, type TimelineFile } from "./
|
|
1
|
+
import { type SessionView, type SkillRun, type Summary, type TimelineFile } from "./core";
|
|
2
2
|
export type IconSet = {
|
|
3
3
|
run: string;
|
|
4
4
|
file: string;
|
|
@@ -6,6 +6,8 @@ export type IconSet = {
|
|
|
6
6
|
open: string;
|
|
7
7
|
closed: string;
|
|
8
8
|
};
|
|
9
|
+
/** The icon set for a `SKILL_AUDIT_ICONS` value; anything but `emoji` is the text set. */
|
|
10
|
+
export declare function iconsFor(style: string | undefined): IconSet;
|
|
9
11
|
export declare function icons(): IconSet;
|
|
10
12
|
export declare function hhmm(ts: string): string;
|
|
11
13
|
/** The local hour a timestamp belongs to, as a bucket label. */
|
|
@@ -24,10 +26,10 @@ export type HourBucket = {
|
|
|
24
26
|
* folding a day's gap into one row.
|
|
25
27
|
*/
|
|
26
28
|
export declare function bucketByHour(files: TimelineFile[]): HourBucket[];
|
|
27
|
-
export declare function headerLine(summary: Summary): string;
|
|
29
|
+
export declare function headerLine(summary: Summary, icon?: IconSet): string;
|
|
28
30
|
/** The skill name leads the row; its times live on the hour buckets below it. */
|
|
29
|
-
export declare function runTitle(run: SkillRun, collapsed: boolean): string;
|
|
30
|
-
export declare function hourTitle(bucket: HourBucket, collapsed: boolean): string;
|
|
31
|
+
export declare function runTitle(run: SkillRun, collapsed: boolean, icon?: IconSet): string;
|
|
32
|
+
export declare function hourTitle(bucket: HourBucket, collapsed: boolean, icon?: IconSet): string;
|
|
31
33
|
/**
|
|
32
34
|
* Fit a path into the sidebar: relative to the session directory, then the
|
|
33
35
|
* basename, then a truncated basename.
|
|
@@ -47,6 +49,8 @@ export type SidebarOptions = {
|
|
|
47
49
|
collapsed: Set<string>;
|
|
48
50
|
/** Usable sidebar width, in columns. */
|
|
49
51
|
width: number;
|
|
52
|
+
/** Icons to draw with; the environment's set when omitted. */
|
|
53
|
+
icons?: IconSet;
|
|
50
54
|
};
|
|
51
55
|
export declare function runKey(index: number): string;
|
|
52
56
|
export declare function hourKeyOf(index: number, bucket: HourBucket): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-skill-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-dev.pr3.36976113543",
|
|
4
4
|
"description": "Audit trail of skill invocations and file changes, with a live timeline in the opencode sidebar",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,16 +40,18 @@
|
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
42
|
"clean": "rm -rf dist",
|
|
43
|
-
"format": "prettier --write src test package.json tsconfig.json tsconfig.test.json eslint.config.js .github/workflows/ci.yml",
|
|
44
|
-
"format:check": "prettier --check src test package.json tsconfig.json tsconfig.test.json eslint.config.js .github/workflows/ci.yml",
|
|
43
|
+
"format": "prettier --write src test hooks/claude.tsx hooks/claude.test.tsx types package.json tsconfig.json tsconfig.test.json tsconfig.claude.json eslint.config.js .github/workflows/ci.yml",
|
|
44
|
+
"format:check": "prettier --check src test hooks/claude.tsx hooks/claude.test.tsx types package.json tsconfig.json tsconfig.test.json tsconfig.claude.json eslint.config.js .github/workflows/ci.yml",
|
|
45
45
|
"lint": "eslint src test",
|
|
46
46
|
"build:types": "tsc --emitDeclarationOnly",
|
|
47
47
|
"build:index": "bun build src/index.ts --outfile dist/index.js --target bun --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk",
|
|
48
48
|
"build:tui": "bun build src/tui.ts --outfile dist/tui.js --target bun --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk --external @opentui/core --external @opentui/solid --external solid-js",
|
|
49
49
|
"build": "bun run clean && bun run build:types && bun run build:index && bun run build:tui",
|
|
50
50
|
"test": "TZ=UTC bun test && TZ=UTC ./test/format-contract.sh",
|
|
51
|
+
"test:claude": "./test/claude-pane.sh",
|
|
51
52
|
"typecheck": "tsc --noEmit",
|
|
52
53
|
"typecheck:test": "tsc -p tsconfig.test.json",
|
|
54
|
+
"typecheck:claude": "tsc -p tsconfig.claude.json",
|
|
53
55
|
"check": "bun run format:check && bun run lint && bun run typecheck && bun run typecheck:test && TZ=UTC bun test && TZ=UTC ./test/format-contract.sh",
|
|
54
56
|
"check:versions": "node scripts/sync-versions.mjs --check",
|
|
55
57
|
"sync:versions": "node scripts/sync-versions.mjs",
|