@monster-orange/den 0.4.6 → 0.4.8
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 +160 -0
- package/README.md +7 -7
- package/docs/distributions.md +244 -1
- package/package.json +1 -1
- package/schemas/den.plugin.v1.json +3 -2
- package/skills/install-den/SKILL.md +2 -2
- package/src/shared/agents/den-claude.ts +38 -5
- package/src/shared/agents/guardrails/hooks.json +27 -0
- package/src/shared/agents/guardrails/instructions.md +39 -0
- package/src/shared/agents/guardrails/pretooluse.mjs +430 -0
- package/src/shared/agents/guardrails/stop-hook.ts +66 -0
- package/src/shared/agents/guardrails.ts +114 -0
- package/src/shared/agents/handback.ts +241 -0
- package/src/shared/agents/personas.ts +66 -7
- package/src/shared/agents/records.ts +114 -0
- package/src/shared/agents/runtime.ts +12 -0
- package/src/shared/den-config.ts +97 -6
- package/src/shared/dev-metrics/emit.ts +31 -4
- package/src/shared/dev-metrics/polling.ts +135 -0
- package/src/shared/engine.ts +14 -2
- package/src/shared/plugins/manifest.ts +3 -0
- package/src/shared/usage/paths.ts +16 -0
- package/src/shared/usage/report.ts +283 -0
- package/src/shared/usage/sources.ts +127 -0
- package/src/shared/usage/transcript.ts +262 -0
- package/src/ui/cli/agent-ops.ts +495 -53
- package/src/ui/cli/agents-command.ts +170 -14
- package/src/ui/cli/doctor-command.ts +20 -0
- package/src/ui/cli/flag-parser.ts +1 -0
- package/src/ui/cli/index.ts +119 -2
- package/src/ui/cli/usage-command.ts +256 -0
- package/src/ui/cli/web-command.ts +3 -1
- 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 +230 -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 +333 -11
- 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 +28 -0
- package/src/ui/server/public/i18n/ja.js +25 -0
- package/src/ui/server/public/shell.js +66 -0
- package/src/ui/server/request-guard.ts +21 -1
- package/src/ui/server/route-classes.ts +23 -0
- package/src/ui/server/session-routes.ts +52 -4
- package/src/ui/server/sudo-store.ts +36 -7
package/src/shared/den-config.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { OrgConfig } from "./org.js";
|
|
|
19
19
|
import { validateOrgSources, type WorkOrgsConfig } from "./org-sources.js";
|
|
20
20
|
import { ConfigError, isConfigError } from "./config-error.js";
|
|
21
21
|
import { validateCatalog, type CatalogEntry } from "./agents/catalog.js";
|
|
22
|
+
import type { Guardrails } from "./agents/guardrails.js";
|
|
22
23
|
import type { TestAccountsConfig } from "./auth/test-accounts.js";
|
|
23
24
|
|
|
24
25
|
import { product } from "./product.js";
|
|
@@ -390,6 +391,11 @@ export interface UserConfig {
|
|
|
390
391
|
runtimes?: Record<string, { bin?: unknown } | null>;
|
|
391
392
|
/** More runtime catalog entries (AG-0.5, agents/catalog.ts). */
|
|
392
393
|
runtime_catalog?: unknown;
|
|
394
|
+
/**
|
|
395
|
+
* AG-14.1b: forget stopped and exited agents after this long: never
|
|
396
|
+
* (the default), 1d or 7d. Interrupted agents are never forgotten.
|
|
397
|
+
*/
|
|
398
|
+
remove_stopped_after?: unknown;
|
|
393
399
|
};
|
|
394
400
|
/**
|
|
395
401
|
* The web server's per-machine keys (src/shared/web-session.ts
|
|
@@ -464,6 +470,29 @@ export function configuredRuntimeCatalog(
|
|
|
464
470
|
);
|
|
465
471
|
}
|
|
466
472
|
|
|
473
|
+
/** The values `agents.remove_stopped_after` takes, in days (AG-14.1b). */
|
|
474
|
+
export const REMOVE_STOPPED_AFTER: Readonly<Record<string, number>> = {
|
|
475
|
+
never: 0,
|
|
476
|
+
"1d": 1,
|
|
477
|
+
"7d": 7,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* user.yml's `agents.remove_stopped_after`, in days (0: never, the
|
|
482
|
+
* default). Throws naming the key when it is none of never, 1d, 7d.
|
|
483
|
+
*/
|
|
484
|
+
export function configuredRemoveStoppedAfter(
|
|
485
|
+
read: () => UserConfig = readUserConfig,
|
|
486
|
+
): number {
|
|
487
|
+
const v = read().agents?.remove_stopped_after;
|
|
488
|
+
if (v === undefined || v === null) return 0;
|
|
489
|
+
if (typeof v === "string" && Object.hasOwn(REMOVE_STOPPED_AFTER, v))
|
|
490
|
+
return REMOVE_STOPPED_AFTER[v];
|
|
491
|
+
throw new Error(
|
|
492
|
+
`agents.remove_stopped_after (${path.basename(USER_CONFIG_PATH)}): must be never, 1d or 7d (got ${JSON.stringify(v)})`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
467
496
|
/**
|
|
468
497
|
* Set (a path) or clear (null) `agents.runtimes.<id>.bin` in user.yml,
|
|
469
498
|
* keeping everything else there. The caller checks the path first.
|
|
@@ -772,6 +801,8 @@ export const MCP_ACTIONS = [
|
|
|
772
801
|
"service_restart",
|
|
773
802
|
"workspace_sync",
|
|
774
803
|
"run_tests",
|
|
804
|
+
// AG-14.4: resume another agent of its workspace (or a granted one).
|
|
805
|
+
"agent_resume",
|
|
775
806
|
] as const;
|
|
776
807
|
export type McpAction = (typeof MCP_ACTIONS)[number];
|
|
777
808
|
|
|
@@ -833,9 +864,16 @@ export interface AgentsConfig {
|
|
|
833
864
|
/**
|
|
834
865
|
* Persona packs (AG-2a): directories of persona files, relative to the
|
|
835
866
|
* distribution root. Den's default pack is always read first; a pack's
|
|
836
|
-
* persona replaces one of the same id.
|
|
867
|
+
* persona replaces one of the same id. An entry may be a mapping,
|
|
868
|
+
* `{ path, hooks, instructions }`, to turn Den's guardrails on for that
|
|
869
|
+
* pack (agents/guardrails.ts; off for a distribution's pack by default).
|
|
870
|
+
*/
|
|
871
|
+
persona_packs?: (string | PersonaPackConfig)[];
|
|
872
|
+
/**
|
|
873
|
+
* Den's default pack's guardrails, `{ hooks, instructions }` (both on
|
|
874
|
+
* by default): false turns that one off for every persona of the pack.
|
|
837
875
|
*/
|
|
838
|
-
|
|
876
|
+
default_pack?: Partial<Guardrails>;
|
|
839
877
|
/**
|
|
840
878
|
* More runtime catalog entries (AG-0.5, agents/catalog.ts): a vendor CLI
|
|
841
879
|
* Den doesn't ship, with the same fields and checks as a built-in one.
|
|
@@ -849,6 +887,13 @@ export interface AgentsConfig {
|
|
|
849
887
|
[agentId: string]: unknown;
|
|
850
888
|
}
|
|
851
889
|
|
|
890
|
+
/** A `persona_packs` entry written as a mapping. */
|
|
891
|
+
export interface PersonaPackConfig {
|
|
892
|
+
path: string;
|
|
893
|
+
hooks?: boolean;
|
|
894
|
+
instructions?: boolean;
|
|
895
|
+
}
|
|
896
|
+
|
|
852
897
|
export const DEFAULT_AGENTS_MAX_CONCURRENT = 4;
|
|
853
898
|
|
|
854
899
|
/** The keys of `agents:` that are not an agent id. */
|
|
@@ -856,6 +901,7 @@ export const AGENTS_RESERVED_KEYS = [
|
|
|
856
901
|
"max_concurrent",
|
|
857
902
|
"persona_packs",
|
|
858
903
|
"runtime_catalog",
|
|
904
|
+
"default_pack",
|
|
859
905
|
];
|
|
860
906
|
|
|
861
907
|
/** One agent's own block, `agents.<id>`. */
|
|
@@ -869,6 +915,10 @@ export interface ResolvedAgentsConfig {
|
|
|
869
915
|
maxConcurrent: number;
|
|
870
916
|
/** As configured (relative to the distribution root). */
|
|
871
917
|
personaPacks: string[];
|
|
918
|
+
/** Each pack's guardrails as configured, by index of personaPacks. */
|
|
919
|
+
packGuardrails?: (Partial<Guardrails> | undefined)[];
|
|
920
|
+
/** agents.default_pack as configured. */
|
|
921
|
+
defaultPackGuardrails?: Partial<Guardrails>;
|
|
872
922
|
/** `agents.<id>` blocks, by agent id. */
|
|
873
923
|
agents: Record<string, AgentGrantsConfig>;
|
|
874
924
|
/** The distribution's runtime catalog entries (AG-0.5), checked. */
|
|
@@ -881,6 +931,36 @@ const AGENT_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
|
881
931
|
const isMapping = (v: unknown): v is Record<string, unknown> =>
|
|
882
932
|
v !== null && typeof v === "object" && !Array.isArray(v);
|
|
883
933
|
|
|
934
|
+
/**
|
|
935
|
+
* `{ hooks, instructions }` (agents.default_pack, or a persona_packs
|
|
936
|
+
* mapping with its `path`): each true or false; any other key throws.
|
|
937
|
+
*/
|
|
938
|
+
function guardrailsBlock(
|
|
939
|
+
v: unknown,
|
|
940
|
+
where: string,
|
|
941
|
+
withPath: boolean,
|
|
942
|
+
): Partial<Guardrails> {
|
|
943
|
+
if (v === undefined || v === null) return {};
|
|
944
|
+
if (!isMapping(v))
|
|
945
|
+
throw new Error(
|
|
946
|
+
`${where}: must be a mapping ({ hooks: true|false, instructions: true|false })`,
|
|
947
|
+
);
|
|
948
|
+
const out: Partial<Guardrails> = {};
|
|
949
|
+
for (const [k, x] of Object.entries(v)) {
|
|
950
|
+
if (withPath && k === "path") continue;
|
|
951
|
+
if (k !== "hooks" && k !== "instructions")
|
|
952
|
+
throw new Error(
|
|
953
|
+
`${where}.${k}: unknown key (takes ${withPath ? "path, " : ""}hooks, instructions)`,
|
|
954
|
+
);
|
|
955
|
+
if (typeof x !== "boolean")
|
|
956
|
+
throw new Error(
|
|
957
|
+
`${where}.${k}: must be true or false (got ${JSON.stringify(x)})`,
|
|
958
|
+
);
|
|
959
|
+
out[k] = x;
|
|
960
|
+
}
|
|
961
|
+
return out;
|
|
962
|
+
}
|
|
963
|
+
|
|
884
964
|
/**
|
|
885
965
|
* The effective agents config. Anything malformed throws, naming the key:
|
|
886
966
|
* a typo must not lift the cap, drop a pack or widen a grant.
|
|
@@ -894,6 +974,12 @@ export function readAgentsConfig(
|
|
|
894
974
|
const out: ResolvedAgentsConfig = {
|
|
895
975
|
maxConcurrent: DEFAULT_AGENTS_MAX_CONCURRENT,
|
|
896
976
|
personaPacks: [],
|
|
977
|
+
packGuardrails: [],
|
|
978
|
+
defaultPackGuardrails: guardrailsBlock(
|
|
979
|
+
raw.default_pack,
|
|
980
|
+
"agents.default_pack",
|
|
981
|
+
false,
|
|
982
|
+
),
|
|
897
983
|
agents: {},
|
|
898
984
|
runtimeCatalog: validateCatalog(
|
|
899
985
|
raw.runtime_catalog,
|
|
@@ -915,19 +1001,24 @@ export function readAgentsConfig(
|
|
|
915
1001
|
`agents.persona_packs: must be a list of paths (got ${JSON.stringify(packs)})`,
|
|
916
1002
|
);
|
|
917
1003
|
packs.forEach((p, i) => {
|
|
918
|
-
|
|
1004
|
+
const where = `agents.persona_packs[${i}]`;
|
|
1005
|
+
const dir = isMapping(p) ? p.path : p;
|
|
1006
|
+
if (typeof dir !== "string" || !dir.trim())
|
|
919
1007
|
throw new Error(
|
|
920
|
-
|
|
1008
|
+
`${where}: must be a non-empty path or { path, hooks, instructions } (got ${JSON.stringify(p)})`,
|
|
921
1009
|
);
|
|
1010
|
+
out.personaPacks.push(dir);
|
|
1011
|
+
out.packGuardrails!.push(
|
|
1012
|
+
isMapping(p) ? guardrailsBlock(p, where, true) : undefined,
|
|
1013
|
+
);
|
|
922
1014
|
});
|
|
923
|
-
out.personaPacks = packs as string[];
|
|
924
1015
|
}
|
|
925
1016
|
for (const [id, block] of Object.entries(raw)) {
|
|
926
1017
|
if (AGENTS_RESERVED_KEYS.includes(id)) continue;
|
|
927
1018
|
const where = `agents.${id}`;
|
|
928
1019
|
if (!AGENT_KEY_RE.test(id) || !isMapping(block))
|
|
929
1020
|
throw new Error(
|
|
930
|
-
`${where}: unknown key (agents: takes max_concurrent, persona_packs, runtime_catalog, ` +
|
|
1021
|
+
`${where}: unknown key (agents: takes max_concurrent, persona_packs, runtime_catalog, default_pack, ` +
|
|
931
1022
|
`or an agent id mapping to { grants: [...] })`,
|
|
932
1023
|
);
|
|
933
1024
|
for (const k of Object.keys(block))
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
* else). A DSN, in the env or a synced dev-metrics.config, is where to send
|
|
20
20
|
* once opted in; on its own it never sends.
|
|
21
21
|
*
|
|
22
|
+
* Each line also has `cwd` (den#5): where the command ran, relative to the
|
|
23
|
+
* distribution root ("." for the root itself), or "outside" for anywhere
|
|
24
|
+
* else, never an absolute or home path. `<cli> doctor` reads it to spot
|
|
25
|
+
* polling loops (polling.ts). Like every field, it is sent only when opted
|
|
26
|
+
* in; the envelope format is unchanged.
|
|
27
|
+
*
|
|
22
28
|
* Guarantees: never throws, never blocks the caller (the Sentry ship is
|
|
23
29
|
* fire-and-forget, bounded to ~2s). CI=true / DEV_METRICS_DISABLED=1 suppress
|
|
24
30
|
* shipping but still buffer the JSONL locally.
|
|
@@ -115,6 +121,29 @@ function commonFields(event: string, root: string): Record<string, string> {
|
|
|
115
121
|
};
|
|
116
122
|
}
|
|
117
123
|
|
|
124
|
+
/** The JSONL file, under the repo root. */
|
|
125
|
+
export function eventsFile(root: string = repoRoot()): string {
|
|
126
|
+
return path.join(root, ".dev-metrics", "events.jsonl");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `cwd` for a local line: the path from the root to cwd ("." for the root,
|
|
131
|
+
* "/"-separated), or "outside" when cwd is not under the root.
|
|
132
|
+
*/
|
|
133
|
+
export function cwdField(root: string, cwd: string = process.cwd()): string {
|
|
134
|
+
const real = (p: string): string => {
|
|
135
|
+
try {
|
|
136
|
+
return fs.realpathSync(p);
|
|
137
|
+
} catch {
|
|
138
|
+
return path.resolve(p);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const rel = path.relative(real(root), real(cwd));
|
|
142
|
+
if (rel === "") return ".";
|
|
143
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return "outside";
|
|
144
|
+
return rel.split(path.sep).join("/");
|
|
145
|
+
}
|
|
146
|
+
|
|
118
147
|
function isoNoMillis(): string {
|
|
119
148
|
return new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
120
149
|
}
|
|
@@ -334,14 +363,12 @@ export function record(
|
|
|
334
363
|
for (const [k, v] of Object.entries(fields)) {
|
|
335
364
|
obj[k] = v == null ? "" : String(v);
|
|
336
365
|
}
|
|
366
|
+
obj.cwd = cwdField(root);
|
|
337
367
|
|
|
338
368
|
try {
|
|
339
369
|
const dir = path.join(root, ".dev-metrics");
|
|
340
370
|
fs.mkdirSync(dir, { recursive: true });
|
|
341
|
-
fs.appendFileSync(
|
|
342
|
-
path.join(dir, "events.jsonl"),
|
|
343
|
-
JSON.stringify(obj) + "\n",
|
|
344
|
-
);
|
|
371
|
+
fs.appendFileSync(eventsFile(root), JSON.stringify(obj) + "\n");
|
|
345
372
|
} catch {
|
|
346
373
|
/* non-writable dir → fail-open */
|
|
347
374
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polling-loop warning (den#5) for `<cli> doctor`: one command, run from
|
|
3
|
+
* one place, more than POLL_LIMIT times in any POLL_WINDOW_MS window looks
|
|
4
|
+
* like an agent polling it. Local only: it reads the tail of the local
|
|
5
|
+
* .dev-metrics/events.jsonl and sends nothing anywhere.
|
|
6
|
+
*
|
|
7
|
+
* A line's place is its `cwd` (emit.ts). An older line has none, so it is
|
|
8
|
+
* keyed by its `repo` instead.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
|
|
12
|
+
/** More calls than this in one window is a polling loop. */
|
|
13
|
+
export const POLL_LIMIT = 600;
|
|
14
|
+
/** The window: an hour. */
|
|
15
|
+
export const POLL_WINDOW_MS = 60 * 60 * 1000;
|
|
16
|
+
/** At most this much of the file's end is read, however big it is. */
|
|
17
|
+
export const TAIL_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The file's last `max` bytes as whole lines: a line cut by the start of
|
|
21
|
+
* the slice is dropped. A missing or unreadable file is no lines.
|
|
22
|
+
*/
|
|
23
|
+
export function readTailLines(
|
|
24
|
+
file: string,
|
|
25
|
+
max: number = TAIL_BYTES,
|
|
26
|
+
): string[] {
|
|
27
|
+
let fd: number | undefined;
|
|
28
|
+
try {
|
|
29
|
+
fd = fs.openSync(file, "r");
|
|
30
|
+
const size = fs.fstatSync(fd).size;
|
|
31
|
+
const len = Math.min(size, max);
|
|
32
|
+
const buf = Buffer.alloc(len);
|
|
33
|
+
let got = 0;
|
|
34
|
+
while (got < len) {
|
|
35
|
+
const n = fs.readSync(fd, buf, got, len - got, size - len + got);
|
|
36
|
+
if (n <= 0) break;
|
|
37
|
+
got += n;
|
|
38
|
+
}
|
|
39
|
+
const lines = buf.subarray(0, got).toString("utf-8").split("\n");
|
|
40
|
+
if (len < size) lines.shift();
|
|
41
|
+
return lines.filter((l) => l.trim());
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
} finally {
|
|
45
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PollingLoop {
|
|
50
|
+
command: string;
|
|
51
|
+
/** Where: the line's cwd, or `repo <name>` for an older line. */
|
|
52
|
+
where: string;
|
|
53
|
+
/** Calls in the busiest window. */
|
|
54
|
+
calls: number;
|
|
55
|
+
/** The busiest window's first and last call (ISO, no millis). */
|
|
56
|
+
from: string;
|
|
57
|
+
to: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const iso = (ms: number): string =>
|
|
61
|
+
new Date(ms).toISOString().replace(/\.\d+Z$/, "Z");
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every command+place with more than `limit` calls in some `windowMs`
|
|
65
|
+
* window, with its busiest window. Lines that aren't JSON, or have no
|
|
66
|
+
* command or time, are skipped.
|
|
67
|
+
*/
|
|
68
|
+
export function findPollingLoops(
|
|
69
|
+
lines: string[],
|
|
70
|
+
limit: number = POLL_LIMIT,
|
|
71
|
+
windowMs: number = POLL_WINDOW_MS,
|
|
72
|
+
): PollingLoop[] {
|
|
73
|
+
const byKey = new Map<
|
|
74
|
+
string,
|
|
75
|
+
{ command: string; where: string; times: number[] }
|
|
76
|
+
>();
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
let o: Record<string, unknown>;
|
|
79
|
+
try {
|
|
80
|
+
o = JSON.parse(line) as Record<string, unknown>;
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!o || typeof o !== "object") continue;
|
|
85
|
+
const command = typeof o.command === "string" ? o.command : "";
|
|
86
|
+
const t = typeof o.ts === "string" ? Date.parse(o.ts) : NaN;
|
|
87
|
+
if (!command || Number.isNaN(t)) continue;
|
|
88
|
+
const where =
|
|
89
|
+
typeof o.cwd === "string" && o.cwd
|
|
90
|
+
? o.cwd
|
|
91
|
+
: `repo ${typeof o.repo === "string" && o.repo ? o.repo : "?"}`;
|
|
92
|
+
const key = `${command}\u0000${where}`;
|
|
93
|
+
let e = byKey.get(key);
|
|
94
|
+
if (!e) byKey.set(key, (e = { command, where, times: [] }));
|
|
95
|
+
e.times.push(t);
|
|
96
|
+
}
|
|
97
|
+
const out: PollingLoop[] = [];
|
|
98
|
+
for (const { command, where, times } of byKey.values()) {
|
|
99
|
+
if (times.length <= limit) continue;
|
|
100
|
+
times.sort((a, b) => a - b);
|
|
101
|
+
let best = 0;
|
|
102
|
+
let bi = 0;
|
|
103
|
+
let bj = 0;
|
|
104
|
+
for (let i = 0, j = 0; j < times.length; j++) {
|
|
105
|
+
while (times[j] - times[i] >= windowMs) i++;
|
|
106
|
+
if (j - i + 1 > best) {
|
|
107
|
+
best = j - i + 1;
|
|
108
|
+
bi = i;
|
|
109
|
+
bj = j;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (best > limit)
|
|
113
|
+
out.push({
|
|
114
|
+
command,
|
|
115
|
+
where,
|
|
116
|
+
calls: best,
|
|
117
|
+
from: iso(times[bi]),
|
|
118
|
+
to: iso(times[bj]),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return out.sort((a, b) => b.calls - a.calls);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One warning line, as doctor prints it. */
|
|
125
|
+
export function pollingWarning(p: PollingLoop): string {
|
|
126
|
+
return `looks like a polling loop: ${p.command} in ${p.where}, ${p.calls} calls in an hour (${p.from}–${p.to})`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** doctor's warnings for this events file (none when it is fine or absent). */
|
|
130
|
+
export function pollingWarnings(
|
|
131
|
+
file: string,
|
|
132
|
+
max: number = TAIL_BYTES,
|
|
133
|
+
): string[] {
|
|
134
|
+
return findPollingLoops(readTailLines(file, max)).map(pollingWarning);
|
|
135
|
+
}
|
package/src/shared/engine.ts
CHANGED
|
@@ -93,11 +93,23 @@ export function playwrightCommand(...args: string[]): EngineCommand {
|
|
|
93
93
|
* or with DEN_USE_DIST=1. Mirrors the web server's resolveServerEntrypoint.
|
|
94
94
|
*/
|
|
95
95
|
export function cliEntrypoint(engine: string = getEngineRoot()): EngineCommand {
|
|
96
|
-
|
|
96
|
+
return scriptEntrypoint("src/ui/cli/index.ts", engine);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How to run one of Den's TypeScript entry points (`src/….ts`, relative to
|
|
101
|
+
* the engine root) as a node process: cliEntrypoint's rules, for any
|
|
102
|
+
* script (an agent's hook, guardrails/stop-hook.ts).
|
|
103
|
+
*/
|
|
104
|
+
export function scriptEntrypoint(
|
|
105
|
+
rel: string,
|
|
106
|
+
engine: string = getEngineRoot(),
|
|
107
|
+
): EngineCommand {
|
|
108
|
+
const built = path.join(engine, "dist", rel.replace(/\.ts$/, ".js"));
|
|
97
109
|
if (process.env.DEN_USE_DIST === "1" && fs.existsSync(built))
|
|
98
110
|
return { command: process.execPath, args: [built] };
|
|
99
111
|
const loader = engineModuleFile("tsx/dist/loader.mjs", engine);
|
|
100
|
-
const src = path.join(engine,
|
|
112
|
+
const src = path.join(engine, rel);
|
|
101
113
|
if (loader && fs.existsSync(src))
|
|
102
114
|
return {
|
|
103
115
|
command: process.execPath,
|
|
@@ -58,6 +58,9 @@ export const KIND_TIERS: Readonly<Record<string, PluginTier>> = {
|
|
|
58
58
|
secrets: "server",
|
|
59
59
|
identity: "server",
|
|
60
60
|
"health-source": "server",
|
|
61
|
+
// den#6: reads a runtime's own transcripts on this machine, for tokens
|
|
62
|
+
// and outcomes per agent; never sends them anywhere.
|
|
63
|
+
usage: "server",
|
|
61
64
|
};
|
|
62
65
|
|
|
63
66
|
export interface PluginProvide {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Claude Code keeps its transcripts. No registration here, so the
|
|
3
|
+
* den-claude runtime can ask (conversationExists) without loading the
|
|
4
|
+
* usage sources (sources.ts registers one when it loads).
|
|
5
|
+
*/
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
|
|
9
|
+
/** Claude Code's transcripts: $CLAUDE_CONFIG_DIR/projects, else ~/.claude/projects. */
|
|
10
|
+
export function claudeProjectsDir(
|
|
11
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
12
|
+
): string {
|
|
13
|
+
const base =
|
|
14
|
+
env.CLAUDE_CONFIG_DIR || path.join(env.HOME || os.homedir(), ".claude");
|
|
15
|
+
return path.join(base, "projects");
|
|
16
|
+
}
|