@monster-orange/den 0.4.7 → 0.4.9
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/CHANGELOG.md +174 -0
- package/README.md +7 -7
- package/docs/distributions.md +243 -1
- package/package.json +1 -1
- package/schemas/den.plugin.v1.json +3 -2
- package/src/shared/agents/den-claude.ts +57 -3
- package/src/shared/agents/guardrails/hooks.json +23 -0
- package/src/shared/agents/guardrails/instructions.md +23 -0
- package/src/shared/agents/guardrails/rotation-hook.ts +127 -0
- package/src/shared/agents/guardrails/stop-hook.ts +48 -2
- package/src/shared/agents/guardrails.ts +38 -7
- package/src/shared/agents/handoff.ts +118 -0
- package/src/shared/agents/personas.ts +11 -1
- package/src/shared/agents/records.ts +122 -1
- package/src/shared/agents/rotation.ts +362 -0
- package/src/shared/agents/runtime.ts +27 -0
- package/src/shared/den-config.ts +72 -1
- package/src/shared/git/gh-pr-state.ts +32 -0
- package/src/shared/plugins/manifest.ts +3 -0
- package/src/shared/usage/paths.ts +16 -0
- package/src/shared/usage/report.ts +377 -0
- package/src/shared/usage/sources.ts +127 -0
- package/src/shared/usage/transcript.ts +319 -0
- package/src/ui/cli/agent-ops.ts +678 -51
- package/src/ui/cli/agents-command.ts +192 -22
- package/src/ui/cli/index.ts +13 -1
- package/src/ui/cli/usage-command.ts +323 -0
- package/src/ui/mcp/api.ts +10 -1
- package/src/ui/mcp/server.ts +34 -0
- package/src/ui/mcp/tools.ts +50 -0
- package/src/ui/server/agent-resume-grant.ts +53 -0
- package/src/ui/server/agent-routes.ts +247 -9
- package/src/ui/server/audit.ts +68 -1
- package/src/ui/server/index.ts +16 -5
- package/src/ui/server/public/agents-page.js +400 -13
- package/src/ui/server/public/agents-section.js +17 -3
- package/src/ui/server/public/agents.html +44 -0
- package/src/ui/server/public/i18n/es.js +40 -0
- package/src/ui/server/public/i18n/ja.js +36 -0
- package/src/ui/server/public/shell.js +79 -0
- package/src/ui/server/request-guard.ts +21 -1
- package/src/ui/server/route-classes.ts +29 -0
- package/src/ui/server/session-routes.ts +60 -5
- package/src/ui/server/sudo-store.ts +36 -7
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Claude Code transcript, read for what it cost and what it came to
|
|
3
|
+
* (den#6). Ported from the agent-usage miner: per file, inside a time
|
|
4
|
+
* window, it counts tokens by kind (once per message id), model calls by
|
|
5
|
+
* model, tool calls and tool errors, permission denials, polling loops and
|
|
6
|
+
* their sleeps, compactions, people's prompts and the ones that read as
|
|
7
|
+
* corrections, and interrupts. It also finds the outcome: a pull request
|
|
8
|
+
* opened or merged, whether the last test run passed, and the wall-clock
|
|
9
|
+
* from the first message to the last.
|
|
10
|
+
*
|
|
11
|
+
* It reads the file on this machine and keeps counts only: never a
|
|
12
|
+
* prompt's text, a command's output or a file's contents, and it sends
|
|
13
|
+
* nothing anywhere (this module imports no network code).
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as readline from "readline";
|
|
17
|
+
|
|
18
|
+
export interface TokenCounts {
|
|
19
|
+
input: number;
|
|
20
|
+
output: number;
|
|
21
|
+
cacheRead: number;
|
|
22
|
+
cacheWrite: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TranscriptStats {
|
|
26
|
+
file: string;
|
|
27
|
+
/** Under a session's subagents/ directory. */
|
|
28
|
+
subagent: boolean;
|
|
29
|
+
/** Its title (custom, agent name or the runtime's own), when it has one. */
|
|
30
|
+
title?: string;
|
|
31
|
+
/** First and last message inside the window (ms since the epoch). */
|
|
32
|
+
first?: number;
|
|
33
|
+
last?: number;
|
|
34
|
+
/** Model calls: assistant messages, once per message id. */
|
|
35
|
+
calls: number;
|
|
36
|
+
tokens: TokenCounts;
|
|
37
|
+
models: Record<string, number>;
|
|
38
|
+
tools: Record<string, number>;
|
|
39
|
+
toolErrors: Record<string, number>;
|
|
40
|
+
denials: number;
|
|
41
|
+
polls: number;
|
|
42
|
+
sleepSeconds: number;
|
|
43
|
+
compactions: number;
|
|
44
|
+
prompts: number;
|
|
45
|
+
corrections: number;
|
|
46
|
+
interrupts: number;
|
|
47
|
+
agentSpawns: number;
|
|
48
|
+
/** Time between messages less than 5 minutes apart (seconds). */
|
|
49
|
+
activeSeconds: number;
|
|
50
|
+
/** The last pull request URL it opened. */
|
|
51
|
+
prOpened?: string;
|
|
52
|
+
/**
|
|
53
|
+
* `gh pr merge` commands that exited 0: claims, which `usage` checks
|
|
54
|
+
* with `gh pr view` before it marks the pull request merged.
|
|
55
|
+
*/
|
|
56
|
+
prMerges: PrMergeClaim[];
|
|
57
|
+
/** The last test run's result; undefined when it ran none. */
|
|
58
|
+
testsGreen?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A `gh pr merge` that ran: its PR (number, URL, or "" for the branch's). */
|
|
62
|
+
export interface PrMergeClaim {
|
|
63
|
+
ref: string;
|
|
64
|
+
repo?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Words of one simple command, with quotes removed (no expansion). */
|
|
68
|
+
function words(part: string): string[] {
|
|
69
|
+
const out: string[] = [];
|
|
70
|
+
for (const m of part.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g))
|
|
71
|
+
out.push(m[1] ?? m[2] ?? m[3]);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The merge a shell command runs: one of its parts (split on `&&`, `||`,
|
|
77
|
+
* `;`, `|`, newlines), after `VAR=value` words, `env` or `command`, starts
|
|
78
|
+
* with `gh pr merge`, and is not asking for help. Null otherwise:
|
|
79
|
+
* `echo gh pr merge 1`, a grep for it, or a comment are not merges.
|
|
80
|
+
*/
|
|
81
|
+
export function ghPrMergeRef(cmd: string): PrMergeClaim | null {
|
|
82
|
+
for (const part of cmd.split(/&&|\|\||;|\||\n/)) {
|
|
83
|
+
const w = words(part.trim());
|
|
84
|
+
let i = 0;
|
|
85
|
+
while (
|
|
86
|
+
i < w.length &&
|
|
87
|
+
(/^[A-Za-z_][A-Za-z0-9_]*=/.test(w[i]) ||
|
|
88
|
+
w[i] === "env" ||
|
|
89
|
+
w[i] === "command")
|
|
90
|
+
)
|
|
91
|
+
i++;
|
|
92
|
+
if (w[i] !== "gh" || w[i + 1] !== "pr" || w[i + 2] !== "merge") continue;
|
|
93
|
+
const rest = w.slice(i + 3);
|
|
94
|
+
if (rest.includes("--help") || rest.includes("-h")) return null;
|
|
95
|
+
const claim: PrMergeClaim = { ref: "" };
|
|
96
|
+
for (let j = 0; j < rest.length; j++) {
|
|
97
|
+
const a = rest[j];
|
|
98
|
+
if (a === "-R" || a === "--repo") {
|
|
99
|
+
if (rest[j + 1]) claim.repo = rest[++j];
|
|
100
|
+
} else if (a.startsWith("--repo=")) claim.repo = a.slice(7);
|
|
101
|
+
else if (
|
|
102
|
+
!claim.ref &&
|
|
103
|
+
/^(\d+|https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+)$/.test(a)
|
|
104
|
+
)
|
|
105
|
+
claim.ref = a;
|
|
106
|
+
}
|
|
107
|
+
return claim;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A correction, as the miner reads one (emphatic ones; polite ones slip by). */
|
|
113
|
+
const CORRECTION =
|
|
114
|
+
/\b(wtf|fuck|shit|damn|not what i (said|asked)|that('s| is) wrong|you (missed|forgot|broke)|again\??$|still (not|broken|failing|404)|where (is|the)|why (did|didn't|are|is)|i told you|stop|no,|nope|ugh|boo)\b/i;
|
|
115
|
+
/** A polling loop in a shell command. */
|
|
116
|
+
const POLL = /\bsleep\s+\d|for i in \$\(seq|until |while .*sleep|watch /;
|
|
117
|
+
/** A test run in a shell command. */
|
|
118
|
+
const TEST_RUN =
|
|
119
|
+
/\b(jest|vitest|pytest|playwright test|go test|cargo test|(npm|pnpm|yarn)( run)? test\w*|make (test|check|gate)\w*)\b/;
|
|
120
|
+
const PR_URL = /https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+/;
|
|
121
|
+
/** Entries with nothing to count (checked on the line's head, before JSON). */
|
|
122
|
+
const SKIP = [
|
|
123
|
+
"api-",
|
|
124
|
+
"file-history",
|
|
125
|
+
"queue-",
|
|
126
|
+
"bridge",
|
|
127
|
+
"last-prompt",
|
|
128
|
+
"mode",
|
|
129
|
+
"permission-mode",
|
|
130
|
+
"tools-used",
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
function ts(s: unknown): number | undefined {
|
|
134
|
+
if (typeof s !== "string") return undefined;
|
|
135
|
+
const t = Date.parse(s);
|
|
136
|
+
return Number.isFinite(t) ? t : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function textOf(content: unknown): string {
|
|
140
|
+
if (typeof content === "string") return content;
|
|
141
|
+
if (Array.isArray(content))
|
|
142
|
+
return content
|
|
143
|
+
.map((c) =>
|
|
144
|
+
c && typeof c === "object" && (c as { type?: unknown }).type === "text"
|
|
145
|
+
? String((c as { text?: unknown }).text ?? "")
|
|
146
|
+
: "",
|
|
147
|
+
)
|
|
148
|
+
.join("\n");
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const bump = (m: Record<string, number>, k: string, n = 1) => {
|
|
153
|
+
m[k] = (m[k] ?? 0) + n;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** The line's `"type"`, read from its head without parsing it. */
|
|
157
|
+
function headType(line: string): string | null {
|
|
158
|
+
const m = /^\{\s*"type"\s*:\s*"([^"]+)"/.exec(line.slice(0, 120));
|
|
159
|
+
return m ? m[1] : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Mine `file`. `since`: only messages at or after it count (a session that
|
|
164
|
+
* runs for weeks contributes its recent calls). Lines it can't parse are
|
|
165
|
+
* skipped.
|
|
166
|
+
*/
|
|
167
|
+
export async function mineTranscript(
|
|
168
|
+
file: string,
|
|
169
|
+
opts: { since?: number } = {},
|
|
170
|
+
): Promise<TranscriptStats> {
|
|
171
|
+
const r: TranscriptStats = {
|
|
172
|
+
file,
|
|
173
|
+
subagent: /[\\/]subagents[\\/]/.test(file),
|
|
174
|
+
calls: 0,
|
|
175
|
+
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
176
|
+
models: {},
|
|
177
|
+
tools: {},
|
|
178
|
+
toolErrors: {},
|
|
179
|
+
denials: 0,
|
|
180
|
+
polls: 0,
|
|
181
|
+
sleepSeconds: 0,
|
|
182
|
+
compactions: 0,
|
|
183
|
+
prompts: 0,
|
|
184
|
+
corrections: 0,
|
|
185
|
+
interrupts: 0,
|
|
186
|
+
agentSpawns: 0,
|
|
187
|
+
activeSeconds: 0,
|
|
188
|
+
prMerges: [],
|
|
189
|
+
};
|
|
190
|
+
const seen = new Set<string>();
|
|
191
|
+
const toolNames = new Map<string, string>();
|
|
192
|
+
/** Tool-use ids of the commands whose result decides an outcome. */
|
|
193
|
+
const tests = new Map<string, number>();
|
|
194
|
+
const merges = new Map<string, PrMergeClaim>();
|
|
195
|
+
const creates = new Set<string>();
|
|
196
|
+
let order = 0;
|
|
197
|
+
let lastTest: { at: number; green: boolean } | undefined;
|
|
198
|
+
let lastAt: number | undefined;
|
|
199
|
+
const since = opts.since ?? -Infinity;
|
|
200
|
+
const lines = readline.createInterface({
|
|
201
|
+
input: fs.createReadStream(file, { encoding: "utf-8" }),
|
|
202
|
+
crlfDelay: Infinity,
|
|
203
|
+
});
|
|
204
|
+
for await (const line of lines) {
|
|
205
|
+
const head = headType(line);
|
|
206
|
+
if (head && SKIP.some((p) => head.startsWith(p))) continue;
|
|
207
|
+
if (head === "pr-link") {
|
|
208
|
+
const m = PR_URL.exec(line);
|
|
209
|
+
if (m) r.prOpened = m[0];
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
let d: Record<string, unknown>;
|
|
213
|
+
try {
|
|
214
|
+
const v: unknown = JSON.parse(line);
|
|
215
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) continue;
|
|
216
|
+
d = v as Record<string, unknown>;
|
|
217
|
+
} catch {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const type = d.type;
|
|
221
|
+
if (
|
|
222
|
+
type === "ai-title" ||
|
|
223
|
+
type === "custom-title" ||
|
|
224
|
+
type === "agent-name"
|
|
225
|
+
) {
|
|
226
|
+
const t = d.customTitle ?? d.agentName ?? d.aiTitle;
|
|
227
|
+
if (typeof t === "string" && t) r.title = t.slice(0, 200);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const at = ts(d.timestamp);
|
|
231
|
+
if (at !== undefined && at < since) continue;
|
|
232
|
+
if (at !== undefined) {
|
|
233
|
+
r.first ??= at;
|
|
234
|
+
r.last = at;
|
|
235
|
+
if (lastAt !== undefined && at - lastAt > 0 && at - lastAt < 300_000)
|
|
236
|
+
r.activeSeconds += (at - lastAt) / 1000;
|
|
237
|
+
lastAt = at;
|
|
238
|
+
}
|
|
239
|
+
if (type === "system") {
|
|
240
|
+
if (String(d.subtype ?? "").includes("compact")) r.compactions++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const m = (d.message ?? {}) as Record<string, unknown>;
|
|
244
|
+
if (type === "assistant") {
|
|
245
|
+
const id = typeof m.id === "string" ? m.id : undefined;
|
|
246
|
+
if (id && !seen.has(id)) {
|
|
247
|
+
seen.add(id);
|
|
248
|
+
r.calls++;
|
|
249
|
+
const u = (m.usage ?? {}) as Record<string, unknown>;
|
|
250
|
+
const n = (k: string) =>
|
|
251
|
+
typeof u[k] === "number" ? (u[k] as number) : 0;
|
|
252
|
+
r.tokens.input += n("input_tokens");
|
|
253
|
+
r.tokens.output += n("output_tokens");
|
|
254
|
+
r.tokens.cacheRead += n("cache_read_input_tokens");
|
|
255
|
+
r.tokens.cacheWrite += n("cache_creation_input_tokens");
|
|
256
|
+
bump(r.models, typeof m.model === "string" ? m.model : "?");
|
|
257
|
+
}
|
|
258
|
+
for (const c of Array.isArray(m.content) ? m.content : []) {
|
|
259
|
+
if (!c || typeof c !== "object") continue;
|
|
260
|
+
const tu = c as Record<string, unknown>;
|
|
261
|
+
if (tu.type !== "tool_use") continue;
|
|
262
|
+
const name = typeof tu.name === "string" ? tu.name : "?";
|
|
263
|
+
const useId = typeof tu.id === "string" ? tu.id : "";
|
|
264
|
+
toolNames.set(useId, name);
|
|
265
|
+
bump(r.tools, name);
|
|
266
|
+
if (name === "Agent" || name === "Task") r.agentSpawns++;
|
|
267
|
+
if (name !== "Bash") continue;
|
|
268
|
+
const cmd = String(
|
|
269
|
+
((tu.input ?? {}) as Record<string, unknown>).command ?? "",
|
|
270
|
+
);
|
|
271
|
+
if (POLL.test(cmd)) r.polls++;
|
|
272
|
+
for (const s of cmd.matchAll(/\bsleep\s+(\d+)/g))
|
|
273
|
+
r.sleepSeconds += Number(s[1]);
|
|
274
|
+
if (TEST_RUN.test(cmd)) tests.set(useId, order++);
|
|
275
|
+
const merge = ghPrMergeRef(cmd);
|
|
276
|
+
if (merge) merges.set(useId, merge);
|
|
277
|
+
if (/\bgh pr create\b/.test(cmd)) creates.add(useId);
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (type !== "user") continue;
|
|
282
|
+
const content = m.content;
|
|
283
|
+
if (Array.isArray(content)) {
|
|
284
|
+
for (const c of content) {
|
|
285
|
+
if (!c || typeof c !== "object") continue;
|
|
286
|
+
const tr = c as Record<string, unknown>;
|
|
287
|
+
if (tr.type !== "tool_result") continue;
|
|
288
|
+
const useId = typeof tr.tool_use_id === "string" ? tr.tool_use_id : "";
|
|
289
|
+
const err = tr.is_error === true;
|
|
290
|
+
if (err) {
|
|
291
|
+
bump(r.toolErrors, toolNames.get(useId) ?? "?");
|
|
292
|
+
const low = textOf(tr.content).toLowerCase();
|
|
293
|
+
if (/doesn't want to proceed|denied|rejected|blocked/.test(low))
|
|
294
|
+
r.denials++;
|
|
295
|
+
}
|
|
296
|
+
const t = tests.get(useId);
|
|
297
|
+
if (t !== undefined && (!lastTest || t >= lastTest.at))
|
|
298
|
+
lastTest = { at: t, green: !err };
|
|
299
|
+
const merge = merges.get(useId);
|
|
300
|
+
if (merge && !err) r.prMerges.push(merge);
|
|
301
|
+
if (creates.has(useId) && !err) {
|
|
302
|
+
const u = PR_URL.exec(textOf(tr.content));
|
|
303
|
+
if (u) r.prOpened = u[0];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (d.isMeta || d.isSidechain || r.subagent) continue;
|
|
308
|
+
const text = textOf(content).trim();
|
|
309
|
+
if (text.includes("[Request interrupted")) {
|
|
310
|
+
r.interrupts++;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (!text || text.startsWith("<") || text.startsWith("Caveat:")) continue;
|
|
314
|
+
r.prompts++;
|
|
315
|
+
if (CORRECTION.test(text.slice(0, 600))) r.corrections++;
|
|
316
|
+
}
|
|
317
|
+
if (lastTest) r.testsGreen = lastTest.green;
|
|
318
|
+
return r;
|
|
319
|
+
}
|