swarmdo 1.29.0 → 1.37.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/.claude/helpers/auto-memory-hook.mjs +66 -33
- package/README.md +6 -3
- package/package.json +1 -1
- package/v3/@swarmdo/cli/.claude/helpers/auto-memory-hook.mjs +65 -32
- package/v3/@swarmdo/cli/dist/src/affected/affected.d.ts +6 -1
- package/v3/@swarmdo/cli/dist/src/affected/affected.js +7 -2
- package/v3/@swarmdo/cli/dist/src/apply/apply.js +18 -7
- package/v3/@swarmdo/cli/dist/src/changelog/changelog.js +4 -1
- package/v3/@swarmdo/cli/dist/src/codegraph/codegraph.js +11 -1
- package/v3/@swarmdo/cli/dist/src/commands/comms.d.ts +16 -0
- package/v3/@swarmdo/cli/dist/src/commands/comms.js +280 -0
- package/v3/@swarmdo/cli/dist/src/commands/compact.js +6 -2
- package/v3/@swarmdo/cli/dist/src/commands/config.js +1 -0
- package/v3/@swarmdo/cli/dist/src/commands/hooks.js +121 -1
- package/v3/@swarmdo/cli/dist/src/commands/hotspots.js +8 -2
- package/v3/@swarmdo/cli/dist/src/commands/index.js +6 -0
- package/v3/@swarmdo/cli/dist/src/commands/permissions.d.ts +14 -0
- package/v3/@swarmdo/cli/dist/src/commands/permissions.js +92 -0
- package/v3/@swarmdo/cli/dist/src/commands/usage.js +208 -3
- package/v3/@swarmdo/cli/dist/src/comms/mailbox.d.ts +88 -0
- package/v3/@swarmdo/cli/dist/src/comms/mailbox.js +138 -0
- package/v3/@swarmdo/cli/dist/src/comms/store.d.ts +21 -0
- package/v3/@swarmdo/cli/dist/src/comms/store.js +44 -0
- package/v3/@swarmdo/cli/dist/src/compact/compact.js +4 -1
- package/v3/@swarmdo/cli/dist/src/compact-snapshot/compact-snapshot.js +8 -3
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.d.ts +3 -1
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.js +11 -4
- package/v3/@swarmdo/cli/dist/src/env/env.js +40 -14
- package/v3/@swarmdo/cli/dist/src/hooks-recipe/recipe.js +20 -1
- package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.d.ts +3 -0
- package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.js +9 -0
- package/v3/@swarmdo/cli/dist/src/index.js +8 -0
- package/v3/@swarmdo/cli/dist/src/license/license.js +4 -1
- package/v3/@swarmdo/cli/dist/src/mcp-client.js +2 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.d.ts +11 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.js +93 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/task-tools.js +4 -0
- package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.d.ts +2 -0
- package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.js +4 -2
- package/v3/@swarmdo/cli/dist/src/memory-inject/select.d.ts +82 -0
- package/v3/@swarmdo/cli/dist/src/memory-inject/select.js +164 -0
- package/v3/@swarmdo/cli/dist/src/memory-vault/import.js +11 -1
- package/v3/@swarmdo/cli/dist/src/pack/pack.js +6 -2
- package/v3/@swarmdo/cli/dist/src/permissions/audit.d.ts +50 -0
- package/v3/@swarmdo/cli/dist/src/permissions/audit.js +127 -0
- package/v3/@swarmdo/cli/dist/src/plugins/store/search.js +1 -1
- package/v3/@swarmdo/cli/dist/src/redact/redact.js +7 -2
- package/v3/@swarmdo/cli/dist/src/release/release.js +1 -1
- package/v3/@swarmdo/cli/dist/src/sbom/sbom.d.ts +2 -0
- package/v3/@swarmdo/cli/dist/src/sbom/sbom.js +5 -0
- package/v3/@swarmdo/cli/dist/src/swarmvector/semantic-router.js +8 -3
- package/v3/@swarmdo/cli/dist/src/testreport/testreport.js +9 -2
- package/v3/@swarmdo/cli/dist/src/transcript/export.js +8 -4
- package/v3/@swarmdo/cli/dist/src/update/checker.d.ts +1 -0
- package/v3/@swarmdo/cli/dist/src/update/checker.js +7 -2
- package/v3/@swarmdo/cli/dist/src/usage/limits.d.ts +77 -0
- package/v3/@swarmdo/cli/dist/src/usage/limits.js +134 -0
- package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.d.ts +29 -0
- package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.js +28 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect-html.d.ts +23 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect-html.js +95 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect.d.ts +131 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect.js +209 -0
- package/v3/@swarmdo/cli/dist/src/usage/spend-forecast.d.ts +28 -0
- package/v3/@swarmdo/cli/dist/src/usage/spend-forecast.js +33 -0
- package/v3/@swarmdo/cli/dist/src/usage/transcript-errors.d.ts +14 -0
- package/v3/@swarmdo/cli/dist/src/usage/transcript-errors.js +10 -0
- package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.d.ts +8 -1
- package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.js +21 -1
- package/v3/@swarmdo/cli/dist/src/util/csv.d.ts +14 -0
- package/v3/@swarmdo/cli/dist/src/util/csv.js +20 -0
- package/v3/@swarmdo/cli/dist/src/util/stdout.d.ts +23 -1
- package/v3/@swarmdo/cli/dist/src/util/stdout.js +34 -2
- package/v3/@swarmdo/cli/package.json +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* permissions/audit.ts — static analyzer for Claude Code permission rules (#80).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code reads `permissions.allow` / `deny` / `ask` arrays from
|
|
5
|
+
* `.claude/settings.json` (+ `settings.local.json` + `~/.claude/settings.json`),
|
|
6
|
+
* each entry a `Tool` or `Tool(specifier)` rule, with precedence deny > ask >
|
|
7
|
+
* allow. Users hand-edit these and get them wrong: a rule in both allow and deny
|
|
8
|
+
* (silently dead), a specific allow already covered by a broader one, or an
|
|
9
|
+
* over-broad `Bash(*)` that re-grants what deny was fencing off.
|
|
10
|
+
*
|
|
11
|
+
* This is the PURE rule layer — parse + set logic, no fs/network — so the whole
|
|
12
|
+
* analysis is unit-testable; the command reads the settings files.
|
|
13
|
+
*/
|
|
14
|
+
export type PermSeverity = 'error' | 'warn' | 'info';
|
|
15
|
+
export interface PermFinding {
|
|
16
|
+
severity: PermSeverity;
|
|
17
|
+
/** kebab-case finding type */
|
|
18
|
+
rule: string;
|
|
19
|
+
message: string;
|
|
20
|
+
/** the rule string(s) the finding is about */
|
|
21
|
+
subjects: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface ParsedRule {
|
|
24
|
+
tool: string;
|
|
25
|
+
/** undefined = a bare tool rule (grants the whole tool) */
|
|
26
|
+
specifier?: string;
|
|
27
|
+
}
|
|
28
|
+
export type ParseResult = ParsedRule | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
export interface PermissionSets {
|
|
32
|
+
allow?: string[];
|
|
33
|
+
deny?: string[];
|
|
34
|
+
ask?: string[];
|
|
35
|
+
}
|
|
36
|
+
/** Parse one `Tool` or `Tool(specifier)` rule. */
|
|
37
|
+
export declare function parseRule(raw: string): ParseResult;
|
|
38
|
+
/**
|
|
39
|
+
* Does `broad` grant everything `specific` grants (same tool)? A bare tool or a
|
|
40
|
+
* `*` specifier covers anything on that tool; otherwise the broad specifier is
|
|
41
|
+
* glob-matched against the specific one (exact equality included).
|
|
42
|
+
*/
|
|
43
|
+
export declare function covers(broad: ParsedRule, specific: ParsedRule): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Analyze a merged permission ruleset. Reports: allow↔deny conflicts (dead allow
|
|
46
|
+
* rules), allow rules shadowed by a broader allow, over-broad whole-tool grants,
|
|
47
|
+
* exact duplicates, and malformed rules. Order: errors, then warns, then infos.
|
|
48
|
+
*/
|
|
49
|
+
export declare function auditPermissions(perms: PermissionSets): PermFinding[];
|
|
50
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* permissions/audit.ts — static analyzer for Claude Code permission rules (#80).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code reads `permissions.allow` / `deny` / `ask` arrays from
|
|
5
|
+
* `.claude/settings.json` (+ `settings.local.json` + `~/.claude/settings.json`),
|
|
6
|
+
* each entry a `Tool` or `Tool(specifier)` rule, with precedence deny > ask >
|
|
7
|
+
* allow. Users hand-edit these and get them wrong: a rule in both allow and deny
|
|
8
|
+
* (silently dead), a specific allow already covered by a broader one, or an
|
|
9
|
+
* over-broad `Bash(*)` that re-grants what deny was fencing off.
|
|
10
|
+
*
|
|
11
|
+
* This is the PURE rule layer — parse + set logic, no fs/network — so the whole
|
|
12
|
+
* analysis is unit-testable; the command reads the settings files.
|
|
13
|
+
*/
|
|
14
|
+
/** Parse one `Tool` or `Tool(specifier)` rule. */
|
|
15
|
+
export function parseRule(raw) {
|
|
16
|
+
const s = (raw ?? '').trim();
|
|
17
|
+
if (!s)
|
|
18
|
+
return { error: 'empty rule' };
|
|
19
|
+
const open = s.indexOf('(');
|
|
20
|
+
if (open === -1) {
|
|
21
|
+
// Bare tool, e.g. `Bash` or `WebFetch`.
|
|
22
|
+
if (!/^[A-Za-z_][\w-]*$/.test(s))
|
|
23
|
+
return { error: 'invalid tool name' };
|
|
24
|
+
return { tool: s };
|
|
25
|
+
}
|
|
26
|
+
if (!s.endsWith(')'))
|
|
27
|
+
return { error: 'unbalanced parentheses' };
|
|
28
|
+
const tool = s.slice(0, open);
|
|
29
|
+
if (!tool)
|
|
30
|
+
return { error: 'missing tool name' };
|
|
31
|
+
if (!/^[A-Za-z_][\w-]*$/.test(tool))
|
|
32
|
+
return { error: 'invalid tool name' };
|
|
33
|
+
const specifier = s.slice(open + 1, s.length - 1);
|
|
34
|
+
if (specifier.trim() === '')
|
|
35
|
+
return { error: 'empty specifier' };
|
|
36
|
+
return { tool, specifier };
|
|
37
|
+
}
|
|
38
|
+
function isParsed(r) {
|
|
39
|
+
return r.tool !== undefined;
|
|
40
|
+
}
|
|
41
|
+
function escapeRegex(s) {
|
|
42
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
+
}
|
|
44
|
+
/** Turn a specifier glob (`*` = any run) into an anchored matcher. */
|
|
45
|
+
function specifierMatches(broadSpec, target) {
|
|
46
|
+
const re = new RegExp('^' + broadSpec.split('*').map(escapeRegex).join('.*') + '$');
|
|
47
|
+
return re.test(target);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Does `broad` grant everything `specific` grants (same tool)? A bare tool or a
|
|
51
|
+
* `*` specifier covers anything on that tool; otherwise the broad specifier is
|
|
52
|
+
* glob-matched against the specific one (exact equality included).
|
|
53
|
+
*/
|
|
54
|
+
export function covers(broad, specific) {
|
|
55
|
+
if (broad.tool !== specific.tool)
|
|
56
|
+
return false;
|
|
57
|
+
if (broad.specifier === undefined)
|
|
58
|
+
return true; // bare tool grants all
|
|
59
|
+
if (broad.specifier === '*')
|
|
60
|
+
return true;
|
|
61
|
+
if (specific.specifier === undefined)
|
|
62
|
+
return false; // broad is scoped, specific is the whole tool
|
|
63
|
+
return specifierMatches(broad.specifier, specific.specifier);
|
|
64
|
+
}
|
|
65
|
+
/** True for a rule that grants an entire tool (`Bash` or `Bash(*)`). */
|
|
66
|
+
function isWholeTool(r) {
|
|
67
|
+
return r.specifier === undefined || r.specifier === '*';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Analyze a merged permission ruleset. Reports: allow↔deny conflicts (dead allow
|
|
71
|
+
* rules), allow rules shadowed by a broader allow, over-broad whole-tool grants,
|
|
72
|
+
* exact duplicates, and malformed rules. Order: errors, then warns, then infos.
|
|
73
|
+
*/
|
|
74
|
+
export function auditPermissions(perms) {
|
|
75
|
+
const findings = [];
|
|
76
|
+
const parsed = { allow: [], deny: [], ask: [] };
|
|
77
|
+
for (const list of ['allow', 'deny', 'ask']) {
|
|
78
|
+
for (const raw of perms[list] ?? []) {
|
|
79
|
+
const r = parseRule(raw);
|
|
80
|
+
if (!isParsed(r)) {
|
|
81
|
+
findings.push({ severity: 'error', rule: 'malformed-rule', message: `${list} rule "${raw}" is malformed: ${r.error}`, subjects: [raw] });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
parsed[list].push({ raw, parsed: r });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// conflict: an allow rule that some deny rule covers is dead (deny wins).
|
|
89
|
+
for (const a of parsed.allow) {
|
|
90
|
+
const killer = parsed.deny.find((d) => covers(d.parsed, a.parsed));
|
|
91
|
+
if (killer) {
|
|
92
|
+
findings.push({ severity: 'error', rule: 'conflict', message: `allow "${a.raw}" is dead — deny "${killer.raw}" overrides it (deny wins)`, subjects: [a.raw, killer.raw] });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// over-broad: an allow rule granting a whole tool defeats scoped denies.
|
|
96
|
+
for (const a of parsed.allow) {
|
|
97
|
+
if (isWholeTool(a.parsed)) {
|
|
98
|
+
findings.push({ severity: 'warn', rule: 'over-broad', message: `allow "${a.raw}" grants every ${a.parsed.tool} call — narrow it or a scoped deny can't fence it off`, subjects: [a.raw] });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// exact duplicates within a list.
|
|
102
|
+
for (const list of ['allow', 'deny', 'ask']) {
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
const dupped = new Set();
|
|
105
|
+
for (const e of parsed[list]) {
|
|
106
|
+
if (seen.has(e.raw))
|
|
107
|
+
dupped.add(e.raw);
|
|
108
|
+
else
|
|
109
|
+
seen.add(e.raw);
|
|
110
|
+
}
|
|
111
|
+
for (const raw of dupped) {
|
|
112
|
+
findings.push({ severity: 'info', rule: 'duplicate', message: `${list} rule "${raw}" is listed more than once`, subjects: [raw] });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// shadowed-allow: a scoped allow rule already covered by a broader, different allow.
|
|
116
|
+
for (const a of parsed.allow) {
|
|
117
|
+
if (isWholeTool(a.parsed))
|
|
118
|
+
continue; // whole-tool grants are flagged as over-broad, not shadowed
|
|
119
|
+
const broader = parsed.allow.find((b) => b.raw !== a.raw && covers(b.parsed, a.parsed));
|
|
120
|
+
if (broader) {
|
|
121
|
+
findings.push({ severity: 'info', rule: 'shadowed-allow', message: `allow "${a.raw}" is redundant — already covered by "${broader.raw}"`, subjects: [a.raw, broader.raw] });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const order = { error: 0, warn: 1, info: 2 };
|
|
125
|
+
return findings.sort((x, y) => order[x.severity] - order[y.severity]);
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -60,7 +60,7 @@ export function searchPlugins(registry, options = {}) {
|
|
|
60
60
|
}
|
|
61
61
|
// Security audit filter
|
|
62
62
|
if (options.hasSecurityAudit !== undefined) {
|
|
63
|
-
plugins = plugins.filter(p => options.hasSecurityAudit ? p.securityAudit !== undefined :
|
|
63
|
+
plugins = plugins.filter(p => options.hasSecurityAudit ? p.securityAudit !== undefined : p.securityAudit === undefined);
|
|
64
64
|
}
|
|
65
65
|
// Sort
|
|
66
66
|
const sortBy = options.sortBy || 'downloads';
|
|
@@ -45,9 +45,14 @@ export const RULES = [
|
|
|
45
45
|
* `key`, `credential`, `creds` so `PRIVATE_KEY=`/`ENCRYPTION_KEY=`/`DB_CREDENTIAL=`
|
|
46
46
|
* secrets reach the entropy check). The `[:=]` immediately after the keyword
|
|
47
47
|
* anchors it, so `keyboard=`/`monkey_val=` don't match; the entropy + length
|
|
48
|
-
* gates keep low-entropy values (paths, short flags) from being flagged.
|
|
48
|
+
* gates keep low-entropy values (paths, short flags) from being flagged. The
|
|
49
|
+
* value class also stops at the URL/query delimiters `&`, `?`, `#` (absent from
|
|
50
|
+
* base64/base64url/hex token alphabets): otherwise a query-string secret like
|
|
51
|
+
* `client_secret=…&api_key=…` over-captures past the `&` into the next secret,
|
|
52
|
+
* and that inflated span — overlapping a range the high-confidence RULES pass
|
|
53
|
+
* already claimed — gets dropped entirely, leaving the first secret unredacted.
|
|
49
54
|
*/
|
|
50
|
-
const ASSIGNMENT_RE = /(?:pass(?:word|phrase|wd)?|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|auth[_-]?token|client[_-]?secret|credential|creds|key)["']?\s*[:=]\s*["']?([^\s"'
|
|
55
|
+
const ASSIGNMENT_RE = /(?:pass(?:word|phrase|wd)?|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|auth[_-]?token|client[_-]?secret|credential|creds|key)["']?\s*[:=]\s*["']?([^\s"'`,;&?#]{8,})/gi;
|
|
51
56
|
/** Shannon entropy in bits/char. Pure; used by the assignment fallback. */
|
|
52
57
|
export function shannonEntropy(s) {
|
|
53
58
|
if (!s)
|
|
@@ -49,7 +49,7 @@ export function planRelease(input) {
|
|
|
49
49
|
for (const f of TRIO_FILES)
|
|
50
50
|
steps.push({ kind: 'bump-json', file: f, version: next });
|
|
51
51
|
steps.push({ kind: 'bump-lock', file: LOCK_FILE, version: next });
|
|
52
|
-
steps.push({ kind: 'sync-docs', version: next, files: DOC_FILES });
|
|
52
|
+
steps.push({ kind: 'sync-docs', version: next, files: [...DOC_FILES] }); // copy — a pure plan must not alias the module constant
|
|
53
53
|
steps.push({ kind: 'exec', title: 'commit release', cmd: 'git', args: ['commit', '-am', `release: ${tag}`], cwd: root });
|
|
54
54
|
steps.push({ kind: 'exec', title: 'push main', cmd: 'git', args: ['push', 'origin', 'main'], cwd: root });
|
|
55
55
|
if (!input.skipPublish) {
|
|
@@ -58,6 +58,11 @@ export function componentsFromNpmLock(lock, opts = {}) {
|
|
|
58
58
|
continue; // the root project entry
|
|
59
59
|
if (!key.includes('node_modules/'))
|
|
60
60
|
continue;
|
|
61
|
+
// Skip workspace symlinks (`link:true`, no version) — they're the project's
|
|
62
|
+
// own local packages, not third-party components; emitting them fabricates a
|
|
63
|
+
// phantom `@name@0.0.0` purl that pollutes vuln scanners / regulators.
|
|
64
|
+
if (entry.link)
|
|
65
|
+
continue;
|
|
61
66
|
if (opts.productionOnly && entry.dev)
|
|
62
67
|
continue;
|
|
63
68
|
const name = nameFromKey(key);
|
|
@@ -31,8 +31,13 @@ export class SemanticRouter {
|
|
|
31
31
|
throw new Error(`Embedding must be Float32Array of length ${this.dimension}`);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
// Normalize
|
|
35
|
-
|
|
34
|
+
// Normalize ONLY for cosine (cosine = dot product of unit vectors). The
|
|
35
|
+
// dotProduct and euclidean metrics are magnitude-sensitive, so they must
|
|
36
|
+
// keep the raw vectors — normalizing them unconditionally silently turned
|
|
37
|
+
// both into cosine and could flip the ranking.
|
|
38
|
+
const normalizedEmbeddings = this.metric === 'cosine'
|
|
39
|
+
? embeddings.map(emb => this.normalize(emb))
|
|
40
|
+
: embeddings.map(emb => Float32Array.from(emb));
|
|
36
41
|
this.intents.set(name, {
|
|
37
42
|
name,
|
|
38
43
|
embeddings: normalizedEmbeddings,
|
|
@@ -47,7 +52,7 @@ export class SemanticRouter {
|
|
|
47
52
|
if (!(embedding instanceof Float32Array) || embedding.length !== this.dimension) {
|
|
48
53
|
throw new Error(`Embedding must be Float32Array of length ${this.dimension}`);
|
|
49
54
|
}
|
|
50
|
-
const normalizedQuery = this.normalize(embedding);
|
|
55
|
+
const normalizedQuery = this.metric === 'cosine' ? this.normalize(embedding) : embedding;
|
|
51
56
|
const scores = [];
|
|
52
57
|
// Calculate best score for each intent
|
|
53
58
|
for (const [intentName, intent] of this.intents) {
|
|
@@ -97,8 +97,15 @@ export function parseJUnit(xml) {
|
|
|
97
97
|
const suite = attrs.classname ?? attrs.suite ?? '';
|
|
98
98
|
if (attrs.time)
|
|
99
99
|
durationMs += Math.round(parseFloat(attrs.time) * 1000) || 0;
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// Strip captured stdout/stderr before scanning for failure/skip markers: their
|
|
101
|
+
// text (pytest/jest-junit/gotestsum embed logs, often in CDATA) can contain
|
|
102
|
+
// literal <failure…>/<error…>-looking content that must NOT be read as a real
|
|
103
|
+
// failure element on an otherwise-passing test.
|
|
104
|
+
const scanBody = body
|
|
105
|
+
.replace(/<system-(out|err)\b[^>]*>[\s\S]*?<\/system-\1>/g, '')
|
|
106
|
+
.replace(/<system-(out|err)\b[^>]*\/>/g, '');
|
|
107
|
+
const fail = scanBody.match(/<(failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/\1>)/);
|
|
108
|
+
const isSkipped = /<skipped\b[^>]*\/?>/.test(scanBody);
|
|
102
109
|
if (fail) {
|
|
103
110
|
failed++;
|
|
104
111
|
const fAttrs = parseAttrs(fail[2]);
|
|
@@ -141,6 +141,7 @@ export function summarizeFile(fs_stat) {
|
|
|
141
141
|
let firstPrompt = '';
|
|
142
142
|
try {
|
|
143
143
|
const content = fs.readFileSync(fs_stat.file, 'utf8');
|
|
144
|
+
const lines = [];
|
|
144
145
|
for (const raw of content.split('\n')) {
|
|
145
146
|
if (!raw.trim())
|
|
146
147
|
continue;
|
|
@@ -151,17 +152,20 @@ export function summarizeFile(fs_stat) {
|
|
|
151
152
|
catch {
|
|
152
153
|
continue;
|
|
153
154
|
}
|
|
155
|
+
lines.push(line);
|
|
154
156
|
if (line.type && !RENDERABLE.has(line.type))
|
|
155
157
|
continue;
|
|
156
|
-
|
|
157
|
-
if (role === 'user' || role === 'assistant')
|
|
158
|
-
turns++;
|
|
159
|
-
if (!firstPrompt && role === 'user') {
|
|
158
|
+
if (!firstPrompt && line.message?.role === 'user') {
|
|
160
159
|
const txt = cleanUserText(contentToText(line.message?.content) || (typeof line.message?.content === 'string' ? line.message.content : ''));
|
|
161
160
|
if (txt)
|
|
162
161
|
firstPrompt = txt.replace(/\s+/g, ' ').slice(0, 100);
|
|
163
162
|
}
|
|
164
163
|
}
|
|
164
|
+
// Count turns EXACTLY as exportSession does — render, then count role
|
|
165
|
+
// headings — so `transcript list` and `transcript export` can never disagree
|
|
166
|
+
// (#70). A pure tool-result carrier line renders no heading, so it's not a
|
|
167
|
+
// turn; the old raw per-line count inflated `list` for any session with tools.
|
|
168
|
+
turns = countRenderedTurns(renderTranscriptMarkdown(lines));
|
|
165
169
|
}
|
|
166
170
|
catch { /* unreadable */ }
|
|
167
171
|
return { sessionId: sessionIdFromFile(fs_stat.file), file: fs_stat.file, project: fs_stat.project, turns, firstPrompt, mtimeMs: fs_stat.mtimeMs, sizeBytes: fs_stat.sizeBytes };
|
|
@@ -23,6 +23,7 @@ export interface UpdateConfig {
|
|
|
23
23
|
exclude: string[];
|
|
24
24
|
}
|
|
25
25
|
declare const DEFAULT_CONFIG: UpdateConfig;
|
|
26
|
+
export declare function getUpdateType(current: string, latest: string): 'major' | 'minor' | 'patch' | 'none';
|
|
26
27
|
export declare function getInstalledVersion(packageName: string): string | null;
|
|
27
28
|
export declare function checkForUpdates(config?: UpdateConfig): Promise<{
|
|
28
29
|
results: UpdateCheckResult[];
|
|
@@ -45,11 +45,16 @@ async function fetchPackageInfo(packageName) {
|
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
function getUpdateType(current, latest) {
|
|
48
|
+
export function getUpdateType(current, latest) {
|
|
49
49
|
if (!semver.valid(current) || !semver.valid(latest)) {
|
|
50
50
|
return 'none';
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
// Only a strictly-NEWER latest is an update. Without this guard the
|
|
53
|
+
// per-field comparisons below misclassify a same-or-OLDER latest: e.g.
|
|
54
|
+
// current 1.30.2 vs latest 1.29.5 has patch 5 > 2, so it would report
|
|
55
|
+
// 'patch' and auto-update could DOWNGRADE. `gt` also subsumes the equality
|
|
56
|
+
// case (eq → not gt → 'none'). (#79)
|
|
57
|
+
if (!semver.gt(latest, current)) {
|
|
53
58
|
return 'none';
|
|
54
59
|
}
|
|
55
60
|
if (semver.major(latest) > semver.major(current)) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* limits.ts — forecast when a Claude Code usage window will hit its cap.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code passes an official `rate_limits` payload to statusline scripts:
|
|
5
|
+
* for the rolling 5-hour and 7-day windows it reports `used_percentage` and
|
|
6
|
+
* `resets_at`. This is the projection layer on top — at the current burn rate,
|
|
7
|
+
* when will the window hit 100%, and does that land before the window resets?
|
|
8
|
+
* ("at this pace you hit the weekly cap Thu 14:00, 6h before it resets").
|
|
9
|
+
*
|
|
10
|
+
* Pure + deterministic: it takes a normalized window state + an injected `now`
|
|
11
|
+
* (the statusline shim maps the raw payload to WindowState — see #46), so the
|
|
12
|
+
* projection math is unit-tested with zero clock or wire-format coupling.
|
|
13
|
+
*/
|
|
14
|
+
export type LimitStatus = 'ok' | 'warn' | 'over';
|
|
15
|
+
/** Rolling-window durations Claude Code reports (5-hour + 7-day caps). */
|
|
16
|
+
export declare const FIVE_HOUR_MS: number;
|
|
17
|
+
export declare const SEVEN_DAY_MS: number;
|
|
18
|
+
export interface NamedWindow {
|
|
19
|
+
/** short label, '5h' | '7d' */
|
|
20
|
+
label: string;
|
|
21
|
+
state: WindowState;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Map Claude Code's statusline `rate_limits` payload to normalized windows.
|
|
25
|
+
* Tolerant of shape drift: accepts the payload bare or wrapped in `rate_limits`,
|
|
26
|
+
* snake_case or camelCase keys, and epoch-seconds / epoch-ms / ISO `resets_at`.
|
|
27
|
+
* Windows missing usage or a reset time are dropped. Pure.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseRateLimits(payload: unknown): NamedWindow[];
|
|
30
|
+
export interface WindowState {
|
|
31
|
+
/** percent of the window's quota consumed, 0..100 */
|
|
32
|
+
usedPercentage: number;
|
|
33
|
+
/** window duration in ms (5h = 18_000_000, 7d = 604_800_000) */
|
|
34
|
+
windowMs: number;
|
|
35
|
+
/** epoch ms when the window resets to 0% */
|
|
36
|
+
resetsAtMs: number;
|
|
37
|
+
}
|
|
38
|
+
export interface LimitForecast {
|
|
39
|
+
usedPercentage: number;
|
|
40
|
+
resetsAtMs: number;
|
|
41
|
+
/** ms from now until the window resets (never negative) */
|
|
42
|
+
msToReset: number;
|
|
43
|
+
/** projected epoch ms when usage reaches 100% at the current burn rate, or
|
|
44
|
+
* null when it can't be projected (no burn yet, already at/over 100%, or the
|
|
45
|
+
* window hasn't started per the clock) */
|
|
46
|
+
exhaustionMs: number | null;
|
|
47
|
+
/** true when exhaustion is projected at or before the window resets */
|
|
48
|
+
willExhaust: boolean;
|
|
49
|
+
status: LimitStatus;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Forecast a single window. Burn rate is `usedPercentage / elapsed`, where
|
|
53
|
+
* elapsed = now − windowStart and windowStart = resetsAt − windowMs. Pure.
|
|
54
|
+
*/
|
|
55
|
+
export declare function forecastWindow(w: WindowState, nowMs: number, opts?: {
|
|
56
|
+
warnPct?: number;
|
|
57
|
+
}): LimitForecast;
|
|
58
|
+
/** Worst status across windows (over > warn > ok). Pure. */
|
|
59
|
+
export declare function worstStatus(forecasts: LimitForecast[]): LimitStatus;
|
|
60
|
+
/**
|
|
61
|
+
* The binding window — the one that constrains you first. A window projected to
|
|
62
|
+
* exhaust before reset outranks one that won't; among those that will, the
|
|
63
|
+
* earlier exhaustion wins; otherwise the higher used_percentage. null for [].
|
|
64
|
+
*/
|
|
65
|
+
export declare function bindingWindow(forecasts: LimitForecast[]): LimitForecast | null;
|
|
66
|
+
/** Compact human duration for a ms span, e.g. 9000000 → "2h30m", 90000 → "1m". */
|
|
67
|
+
export declare function humanizeMs(ms: number): string;
|
|
68
|
+
/** Ultra-compact one-line indicator for a statusline, e.g. "5h 72%⚠ · 7d 12%".
|
|
69
|
+
* `!` marks an over-cap window, `⚠` an at-risk one. Pure. */
|
|
70
|
+
export declare function formatLimitSegment(forecasts: Array<{
|
|
71
|
+
label: string;
|
|
72
|
+
f: LimitForecast;
|
|
73
|
+
}>): string;
|
|
74
|
+
/** One-line summary for a window, e.g. "5h window: 42% used, resets in 2h14m —
|
|
75
|
+
* on pace to hit the cap in ~1h20m (before reset)". `label` names the window. */
|
|
76
|
+
export declare function formatForecast(label: string, f: LimitForecast, nowMs: number): string;
|
|
77
|
+
//# sourceMappingURL=limits.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* limits.ts — forecast when a Claude Code usage window will hit its cap.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code passes an official `rate_limits` payload to statusline scripts:
|
|
5
|
+
* for the rolling 5-hour and 7-day windows it reports `used_percentage` and
|
|
6
|
+
* `resets_at`. This is the projection layer on top — at the current burn rate,
|
|
7
|
+
* when will the window hit 100%, and does that land before the window resets?
|
|
8
|
+
* ("at this pace you hit the weekly cap Thu 14:00, 6h before it resets").
|
|
9
|
+
*
|
|
10
|
+
* Pure + deterministic: it takes a normalized window state + an injected `now`
|
|
11
|
+
* (the statusline shim maps the raw payload to WindowState — see #46), so the
|
|
12
|
+
* projection math is unit-tested with zero clock or wire-format coupling.
|
|
13
|
+
*/
|
|
14
|
+
/** Rolling-window durations Claude Code reports (5-hour + 7-day caps). */
|
|
15
|
+
export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
|
|
16
|
+
export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
/** Coerce an epoch-seconds, epoch-ms, or ISO-string timestamp to epoch ms. */
|
|
18
|
+
function toEpochMs(v) {
|
|
19
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
20
|
+
return v < 1e12 ? v * 1000 : v; // secs vs ms
|
|
21
|
+
if (typeof v === 'string') {
|
|
22
|
+
const t = Date.parse(v);
|
|
23
|
+
return Number.isNaN(t) ? null : t;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function pickWindow(obj, windowMs, label) {
|
|
28
|
+
if (!obj || typeof obj !== 'object')
|
|
29
|
+
return null;
|
|
30
|
+
const o = obj;
|
|
31
|
+
const used = (o.used_percentage ?? o.usedPercentage);
|
|
32
|
+
const resetsAtMs = toEpochMs(o.resets_at ?? o.resetsAt);
|
|
33
|
+
if (typeof used !== 'number' || !Number.isFinite(used) || resetsAtMs === null)
|
|
34
|
+
return null;
|
|
35
|
+
return { label, state: { usedPercentage: Math.max(0, Math.min(100, used)), windowMs, resetsAtMs } };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Map Claude Code's statusline `rate_limits` payload to normalized windows.
|
|
39
|
+
* Tolerant of shape drift: accepts the payload bare or wrapped in `rate_limits`,
|
|
40
|
+
* snake_case or camelCase keys, and epoch-seconds / epoch-ms / ISO `resets_at`.
|
|
41
|
+
* Windows missing usage or a reset time are dropped. Pure.
|
|
42
|
+
*/
|
|
43
|
+
export function parseRateLimits(payload) {
|
|
44
|
+
const p = payload && typeof payload === 'object' ? payload : {};
|
|
45
|
+
const rl = (p.rate_limits ?? p.rateLimits ?? p);
|
|
46
|
+
const out = [];
|
|
47
|
+
const w5 = pickWindow(rl.five_hour ?? rl.fiveHour, FIVE_HOUR_MS, '5h');
|
|
48
|
+
const w7 = pickWindow(rl.seven_day ?? rl.sevenDay, SEVEN_DAY_MS, '7d');
|
|
49
|
+
if (w5)
|
|
50
|
+
out.push(w5);
|
|
51
|
+
if (w7)
|
|
52
|
+
out.push(w7);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Forecast a single window. Burn rate is `usedPercentage / elapsed`, where
|
|
57
|
+
* elapsed = now − windowStart and windowStart = resetsAt − windowMs. Pure.
|
|
58
|
+
*/
|
|
59
|
+
export function forecastWindow(w, nowMs, opts = {}) {
|
|
60
|
+
const warnPct = opts.warnPct ?? 80;
|
|
61
|
+
const used = w.usedPercentage;
|
|
62
|
+
const msToReset = Math.max(0, w.resetsAtMs - nowMs);
|
|
63
|
+
const windowStartMs = w.resetsAtMs - w.windowMs;
|
|
64
|
+
const elapsed = nowMs - windowStartMs;
|
|
65
|
+
let exhaustionMs = null;
|
|
66
|
+
if (used >= 100) {
|
|
67
|
+
// already exhausted — mark exhaustion as "now"
|
|
68
|
+
exhaustionMs = nowMs;
|
|
69
|
+
}
|
|
70
|
+
else if (used > 0 && elapsed > 0) {
|
|
71
|
+
const msTo100 = ((100 - used) * elapsed) / used; // (remaining%) / (used%/elapsed)
|
|
72
|
+
exhaustionMs = nowMs + msTo100;
|
|
73
|
+
}
|
|
74
|
+
const willExhaust = exhaustionMs !== null && exhaustionMs <= w.resetsAtMs;
|
|
75
|
+
const status = used >= 100 ? 'over' : (willExhaust || used >= warnPct) ? 'warn' : 'ok';
|
|
76
|
+
return { usedPercentage: used, resetsAtMs: w.resetsAtMs, msToReset, exhaustionMs, willExhaust, status };
|
|
77
|
+
}
|
|
78
|
+
/** Worst status across windows (over > warn > ok). Pure. */
|
|
79
|
+
export function worstStatus(forecasts) {
|
|
80
|
+
const rank = { ok: 0, warn: 1, over: 2 };
|
|
81
|
+
return forecasts.reduce((acc, f) => (rank[f.status] > rank[acc] ? f.status : acc), 'ok');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The binding window — the one that constrains you first. A window projected to
|
|
85
|
+
* exhaust before reset outranks one that won't; among those that will, the
|
|
86
|
+
* earlier exhaustion wins; otherwise the higher used_percentage. null for [].
|
|
87
|
+
*/
|
|
88
|
+
export function bindingWindow(forecasts) {
|
|
89
|
+
if (forecasts.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return [...forecasts].sort((a, b) => {
|
|
92
|
+
if (a.willExhaust !== b.willExhaust)
|
|
93
|
+
return a.willExhaust ? -1 : 1;
|
|
94
|
+
if (a.willExhaust && b.willExhaust)
|
|
95
|
+
return a.exhaustionMs - b.exhaustionMs;
|
|
96
|
+
return b.usedPercentage - a.usedPercentage;
|
|
97
|
+
})[0];
|
|
98
|
+
}
|
|
99
|
+
/** Compact human duration for a ms span, e.g. 9000000 → "2h30m", 90000 → "1m". */
|
|
100
|
+
export function humanizeMs(ms) {
|
|
101
|
+
if (ms <= 0)
|
|
102
|
+
return '0m';
|
|
103
|
+
const totalMin = Math.floor(ms / 60_000);
|
|
104
|
+
const d = Math.floor(totalMin / 1440);
|
|
105
|
+
const h = Math.floor((totalMin % 1440) / 60);
|
|
106
|
+
const m = totalMin % 60;
|
|
107
|
+
if (d > 0)
|
|
108
|
+
return `${d}d${h > 0 ? `${h}h` : ''}`;
|
|
109
|
+
if (h > 0)
|
|
110
|
+
return `${h}h${m > 0 ? `${m}m` : ''}`;
|
|
111
|
+
return `${m}m`;
|
|
112
|
+
}
|
|
113
|
+
/** Ultra-compact one-line indicator for a statusline, e.g. "5h 72%⚠ · 7d 12%".
|
|
114
|
+
* `!` marks an over-cap window, `⚠` an at-risk one. Pure. */
|
|
115
|
+
export function formatLimitSegment(forecasts) {
|
|
116
|
+
if (forecasts.length === 0)
|
|
117
|
+
return 'limits: n/a';
|
|
118
|
+
const mark = (s) => (s === 'over' ? '!' : s === 'warn' ? '⚠' : '');
|
|
119
|
+
return forecasts.map(({ label, f }) => `${label} ${Math.round(f.usedPercentage)}%${mark(f.status)}`).join(' · ');
|
|
120
|
+
}
|
|
121
|
+
/** One-line summary for a window, e.g. "5h window: 42% used, resets in 2h14m —
|
|
122
|
+
* on pace to hit the cap in ~1h20m (before reset)". `label` names the window. */
|
|
123
|
+
export function formatForecast(label, f, nowMs) {
|
|
124
|
+
const head = `${label}: ${Math.round(f.usedPercentage)}% used, resets in ${humanizeMs(f.msToReset)}`;
|
|
125
|
+
if (f.status === 'over')
|
|
126
|
+
return `${head} — CAP REACHED`;
|
|
127
|
+
if (f.willExhaust && f.exhaustionMs !== null) {
|
|
128
|
+
const inMs = Math.max(0, f.exhaustionMs - nowMs);
|
|
129
|
+
const before = humanizeMs(f.resetsAtMs - f.exhaustionMs);
|
|
130
|
+
return `${head} — on pace to hit the cap in ~${humanizeMs(inMs)} (${before} before reset)`;
|
|
131
|
+
}
|
|
132
|
+
return `${head} — on pace to stay under the cap`;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-efficiency.ts — rank models by their EFFECTIVE cost per unit of useful
|
|
3
|
+
* output in your actual usage.
|
|
4
|
+
*
|
|
5
|
+
* A model's list output price is fixed, but what you really pay per output token
|
|
6
|
+
* also carries the input + cache cost of every request amortized over the output
|
|
7
|
+
* it produced. A model driven with huge contexts can effectively cost far more
|
|
8
|
+
* per output token than its sticker price — this surfaces that, per model, from
|
|
9
|
+
* the same aggregation the `usage models` view already computes.
|
|
10
|
+
*
|
|
11
|
+
* Pure: it folds already-summed per-model totals, no transcripts, no clock.
|
|
12
|
+
*/
|
|
13
|
+
import type { UsageTotals } from './transcript-usage.js';
|
|
14
|
+
export interface ModelEfficiency {
|
|
15
|
+
model: string;
|
|
16
|
+
costUsd: number;
|
|
17
|
+
outputTokens: number;
|
|
18
|
+
/** effective $ per 1,000,000 output tokens (cost / output × 1e6) */
|
|
19
|
+
costPerMOutput: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Effective cost per 1M output tokens per model, cheapest first. Models with no
|
|
23
|
+
* cost or no output are dropped (no meaningful ratio). Pure.
|
|
24
|
+
*/
|
|
25
|
+
export declare function computeModelEfficiency(models: Array<{
|
|
26
|
+
key: string;
|
|
27
|
+
totals: UsageTotals;
|
|
28
|
+
}>): ModelEfficiency[];
|
|
29
|
+
//# sourceMappingURL=model-efficiency.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-efficiency.ts — rank models by their EFFECTIVE cost per unit of useful
|
|
3
|
+
* output in your actual usage.
|
|
4
|
+
*
|
|
5
|
+
* A model's list output price is fixed, but what you really pay per output token
|
|
6
|
+
* also carries the input + cache cost of every request amortized over the output
|
|
7
|
+
* it produced. A model driven with huge contexts can effectively cost far more
|
|
8
|
+
* per output token than its sticker price — this surfaces that, per model, from
|
|
9
|
+
* the same aggregation the `usage models` view already computes.
|
|
10
|
+
*
|
|
11
|
+
* Pure: it folds already-summed per-model totals, no transcripts, no clock.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Effective cost per 1M output tokens per model, cheapest first. Models with no
|
|
15
|
+
* cost or no output are dropped (no meaningful ratio). Pure.
|
|
16
|
+
*/
|
|
17
|
+
export function computeModelEfficiency(models) {
|
|
18
|
+
return models
|
|
19
|
+
.filter((m) => m.totals.costUsd > 0 && m.totals.outputTokens > 0)
|
|
20
|
+
.map((m) => ({
|
|
21
|
+
model: m.key,
|
|
22
|
+
costUsd: m.totals.costUsd,
|
|
23
|
+
outputTokens: m.totals.outputTokens,
|
|
24
|
+
costPerMOutput: (m.totals.costUsd / m.totals.outputTokens) * 1_000_000,
|
|
25
|
+
}))
|
|
26
|
+
.sort((a, b) => a.costPerMOutput - b.costPerMOutput || (a.model < b.model ? -1 : 1));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=model-efficiency.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reflect-html.ts — render a Reflection (see reflect.ts) into a self-contained,
|
|
3
|
+
* shareable HTML dashboard for `swarmdo usage reflect --html`.
|
|
4
|
+
*
|
|
5
|
+
* Pure + deterministic: it takes an already-computed Reflection and returns a
|
|
6
|
+
* complete HTML document string with all CSS inlined (no external requests, no
|
|
7
|
+
* clock — any timestamp is passed in), so it unit-tests without a browser or a
|
|
8
|
+
* network. Every user-controlled string (model ids, project paths) is HTML-
|
|
9
|
+
* escaped. See #47.
|
|
10
|
+
*/
|
|
11
|
+
import type { Reflection } from './reflect.js';
|
|
12
|
+
/** HTML-escape a string for safe interpolation into text or attributes. */
|
|
13
|
+
export declare function escapeHtml(s: string): string;
|
|
14
|
+
/** Render the retrospective to a complete self-contained HTML document. Pure. */
|
|
15
|
+
export declare function renderReflectionHtml(r: Reflection, opts?: {
|
|
16
|
+
generatedAt?: string;
|
|
17
|
+
delegation?: {
|
|
18
|
+
taskCalls: number;
|
|
19
|
+
toolCalls: number;
|
|
20
|
+
ratio: number;
|
|
21
|
+
};
|
|
22
|
+
}): string;
|
|
23
|
+
//# sourceMappingURL=reflect-html.d.ts.map
|