swarmdo 1.30.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/README.md +6 -3
- package/package.json +1 -1
- 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 +34 -1
- 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/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.js +1 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect.d.ts +26 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect.js +44 -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/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,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
|
|
@@ -81,6 +81,7 @@ ${stat('Avg / active day', usd(r.avgCostPerActiveDay))}
|
|
|
81
81
|
${stat('Longest streak', `${r.longestStreak} day${r.longestStreak === 1 ? '' : 's'}`)}
|
|
82
82
|
${stat('Peak hour', peak)}
|
|
83
83
|
${stat('Cache read', pct(r.cacheReadPct))}
|
|
84
|
+
${r.cacheSavingsUsd > 0 ? stat('Cache saved', usd(r.cacheSavingsUsd)) : ''}
|
|
84
85
|
${stat('Busiest day', busiest)}
|
|
85
86
|
${stat('Trend', `${arrow} ${pct(r.trend.firstHalfCost > 0 ? (r.trend.secondHalfCost - r.trend.firstHalfCost) / r.trend.firstHalfCost : 0)}`)}
|
|
86
87
|
${opts.delegation && opts.delegation.toolCalls > 0 ? stat('Delegation', pct(opts.delegation.ratio)) : ''}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* rows the parser already produces. See #47.
|
|
15
15
|
*/
|
|
16
16
|
import type { DayRow, ModelRow } from './diff.js';
|
|
17
|
+
import { type TranscriptModelPrice } from './claude-pricing.js';
|
|
17
18
|
export interface ModelShare {
|
|
18
19
|
model: string;
|
|
19
20
|
costUsd: number;
|
|
@@ -45,6 +46,8 @@ export interface Reflection {
|
|
|
45
46
|
costUsd: number;
|
|
46
47
|
totalTokens: number;
|
|
47
48
|
} | null;
|
|
49
|
+
/** active days whose cost is an outlier vs the median (possible runaways) */
|
|
50
|
+
spikeDays: SpikeDay[];
|
|
48
51
|
/** models ranked by cost desc (capped to opts.topModels) */
|
|
49
52
|
topModels: ModelShare[];
|
|
50
53
|
/** projects ranked by cost desc (ModelShare.model holds the project path) */
|
|
@@ -68,12 +71,16 @@ export interface Reflection {
|
|
|
68
71
|
secondHalfCost: number;
|
|
69
72
|
direction: 'up' | 'down' | 'flat';
|
|
70
73
|
};
|
|
74
|
+
/** $ prompt-caching saved this period (vs paying full input rate); 0 if no priced models */
|
|
75
|
+
cacheSavingsUsd: number;
|
|
71
76
|
}
|
|
72
77
|
export interface ReflectOptions {
|
|
73
78
|
/** how many models to keep in topModels (default 5) */
|
|
74
79
|
topModels?: number;
|
|
75
80
|
/** relative change below which the trend reads 'flat' (default 0.05 = 5%) */
|
|
76
81
|
flatThreshold?: number;
|
|
82
|
+
/** price resolver for cache-savings (injected for tests; default = the real table) */
|
|
83
|
+
resolvePrice?: (model: string) => TranscriptModelPrice | undefined;
|
|
77
84
|
}
|
|
78
85
|
/** The calendar day after an ISO date (handles month/year rollover). Pure. */
|
|
79
86
|
export declare function nextDay(iso: string): string;
|
|
@@ -87,6 +94,25 @@ export declare function monthsBefore(iso: string, n: number): string;
|
|
|
87
94
|
export declare function spanDays(from: string, to: string): number;
|
|
88
95
|
/** Longest run of consecutive calendar days present in the set. Pure. */
|
|
89
96
|
export declare function longestStreakOf(days: string[]): number;
|
|
97
|
+
export interface SpikeDay {
|
|
98
|
+
day: string;
|
|
99
|
+
costUsd: number;
|
|
100
|
+
/** costUsd / median active-day cost */
|
|
101
|
+
ratioToMedian: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Days whose cost is an outlier vs the median active-day cost — a heads-up for
|
|
105
|
+
* runaway sessions. Median (not mean) so the spikes don't inflate the baseline.
|
|
106
|
+
* Needs ≥3 active days for a meaningful median; a floor keeps trivially-cheap
|
|
107
|
+
* days from tripping the ratio. Sorted by cost desc. Pure.
|
|
108
|
+
*/
|
|
109
|
+
export declare function detectSpikeDays(days: Array<{
|
|
110
|
+
day: string;
|
|
111
|
+
costUsd: number;
|
|
112
|
+
}>, opts?: {
|
|
113
|
+
minRatio?: number;
|
|
114
|
+
minCostUsd?: number;
|
|
115
|
+
}): SpikeDay[];
|
|
90
116
|
/** Render a numeric series as a Unicode block sparkline (one char per bucket).
|
|
91
117
|
* Zero buckets are blank; the rest scale ▁..█ against the max. Pure — used for
|
|
92
118
|
* the terminal cost-by-hour view (the HTML dashboard draws the same data). */
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* without touching disk. The command layer in ../commands/usage.ts feeds it the
|
|
14
14
|
* rows the parser already produces. See #47.
|
|
15
15
|
*/
|
|
16
|
+
import { modelCacheSavings } from './cache-stats.js';
|
|
17
|
+
import { resolveTranscriptPrice } from './claude-pricing.js';
|
|
16
18
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
17
19
|
/** The calendar day after an ISO date (handles month/year rollover). Pure. */
|
|
18
20
|
export function nextDay(iso) {
|
|
@@ -58,6 +60,27 @@ export function longestStreakOf(days) {
|
|
|
58
60
|
}
|
|
59
61
|
return best;
|
|
60
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Days whose cost is an outlier vs the median active-day cost — a heads-up for
|
|
65
|
+
* runaway sessions. Median (not mean) so the spikes don't inflate the baseline.
|
|
66
|
+
* Needs ≥3 active days for a meaningful median; a floor keeps trivially-cheap
|
|
67
|
+
* days from tripping the ratio. Sorted by cost desc. Pure.
|
|
68
|
+
*/
|
|
69
|
+
export function detectSpikeDays(days, opts = {}) {
|
|
70
|
+
const minRatio = opts.minRatio ?? 2.5;
|
|
71
|
+
const minCostUsd = opts.minCostUsd ?? 0.5;
|
|
72
|
+
const costs = days.map((d) => d.costUsd).filter((c) => c > 0).sort((a, b) => a - b);
|
|
73
|
+
if (costs.length < 3)
|
|
74
|
+
return [];
|
|
75
|
+
const mid = costs.length >> 1;
|
|
76
|
+
const median = costs.length % 2 ? costs[mid] : (costs[mid - 1] + costs[mid]) / 2;
|
|
77
|
+
if (median <= 0)
|
|
78
|
+
return [];
|
|
79
|
+
return days
|
|
80
|
+
.filter((d) => d.costUsd >= minCostUsd && d.costUsd >= minRatio * median)
|
|
81
|
+
.map((d) => ({ day: d.day, costUsd: d.costUsd, ratioToMedian: d.costUsd / median }))
|
|
82
|
+
.sort((a, b) => b.costUsd - a.costUsd);
|
|
83
|
+
}
|
|
61
84
|
const within = (key, from, to) => key >= from && key <= to;
|
|
62
85
|
const SPARK_BLOCKS = ' ▁▂▃▄▅▆▇█'; // index 0 = empty (zero), 1..8 = ▁..█ scaled to max
|
|
63
86
|
/** Render a numeric series as a Unicode block sparkline (one char per bucket).
|
|
@@ -145,6 +168,25 @@ export function computeReflection(dayRows, modelRows, from, to, opts = {}, proje
|
|
|
145
168
|
// Cost-ranked shares over the same window, for both models and projects.
|
|
146
169
|
const topModels = rankShares(modelRows, from, to, totals.costUsd, topN);
|
|
147
170
|
const topProjects = rankShares(projectRows, from, to, totals.costUsd, topN);
|
|
171
|
+
// $ saved by prompt caching this period: fold windowed per-model token counts,
|
|
172
|
+
// then price each via the (injected) resolver. Unpriced models contribute 0.
|
|
173
|
+
const resolvePrice = opts.resolvePrice ?? resolveTranscriptPrice;
|
|
174
|
+
const cacheTokens = new Map();
|
|
175
|
+
for (const r of modelRows) {
|
|
176
|
+
if (!within(r.day, from, to))
|
|
177
|
+
continue;
|
|
178
|
+
const s = cacheTokens.get(r.key) ?? { inputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 };
|
|
179
|
+
s.inputTokens += r.totals.inputTokens;
|
|
180
|
+
s.cacheWriteTokens += r.totals.cacheWriteTokens;
|
|
181
|
+
s.cacheReadTokens += r.totals.cacheReadTokens;
|
|
182
|
+
cacheTokens.set(r.key, s);
|
|
183
|
+
}
|
|
184
|
+
let cacheSavingsUsd = 0;
|
|
185
|
+
for (const [model, tok] of cacheTokens) {
|
|
186
|
+
const sv = modelCacheSavings(tok, resolvePrice(model));
|
|
187
|
+
if (sv !== null)
|
|
188
|
+
cacheSavingsUsd += sv;
|
|
189
|
+
}
|
|
148
190
|
const inputSide = totals.inputTokens + totals.cacheWriteTokens + totals.cacheReadTokens;
|
|
149
191
|
const relChange = firstHalfCost > 0 ? (secondHalfCost - firstHalfCost) / firstHalfCost : (secondHalfCost > 0 ? 1 : 0);
|
|
150
192
|
const direction = Math.abs(relChange) < flat ? 'flat' : relChange > 0 ? 'up' : 'down';
|
|
@@ -152,6 +194,7 @@ export function computeReflection(dayRows, modelRows, from, to, opts = {}, proje
|
|
|
152
194
|
period: { from, to, spanDays: spanDays(from, to) },
|
|
153
195
|
totals,
|
|
154
196
|
busiestDay,
|
|
197
|
+
spikeDays: detectSpikeDays(days.map((r) => ({ day: r.key, costUsd: r.totals.costUsd }))),
|
|
155
198
|
topModels,
|
|
156
199
|
topProjects,
|
|
157
200
|
peakHour: peakHourOf(hourHistogram),
|
|
@@ -160,6 +203,7 @@ export function computeReflection(dayRows, modelRows, from, to, opts = {}, proje
|
|
|
160
203
|
cacheReadPct: inputSide > 0 ? totals.cacheReadTokens / inputSide : 0,
|
|
161
204
|
avgCostPerActiveDay: totals.activeDays > 0 ? totals.costUsd / totals.activeDays : 0,
|
|
162
205
|
trend: { firstHalfCost, secondHalfCost, direction },
|
|
206
|
+
cacheSavingsUsd,
|
|
163
207
|
};
|
|
164
208
|
}
|
|
165
209
|
//# sourceMappingURL=reflect.js.map
|
|
@@ -77,7 +77,14 @@ export declare function normalizeDateBound(raw: string | undefined): string | un
|
|
|
77
77
|
* report must not fail because one session file is mid-write.
|
|
78
78
|
*/
|
|
79
79
|
export declare function collectUsage(opts?: CollectOptions): UsageCollection;
|
|
80
|
-
export type UsageDimension = 'day' | 'month' | 'model' | 'project' | 'session';
|
|
80
|
+
export type UsageDimension = 'day' | 'week' | 'month' | 'model' | 'project' | 'session';
|
|
81
|
+
/**
|
|
82
|
+
* ISO-8601 week key for a `YYYY-MM-DD` date, e.g. '2026-W28'. The week belongs to
|
|
83
|
+
* the year of its Thursday, so a late-December / early-January week can carry the
|
|
84
|
+
* neighbouring ISO year (e.g. 2025-12-31 → 2026-W01, 2027-01-01 → 2026-W53).
|
|
85
|
+
* Keys sort chronologically as strings. Pure.
|
|
86
|
+
*/
|
|
87
|
+
export declare function isoWeekKey(iso: string): string;
|
|
81
88
|
/**
|
|
82
89
|
* Group events along one dimension. Time dimensions sort chronologically;
|
|
83
90
|
* the rest sort by descending cost (what a "top spenders" view wants).
|
|
@@ -209,8 +209,28 @@ function toUsageEvent(line, project, seen, unpriced) {
|
|
|
209
209
|
costSource,
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* ISO-8601 week key for a `YYYY-MM-DD` date, e.g. '2026-W28'. The week belongs to
|
|
214
|
+
* the year of its Thursday, so a late-December / early-January week can carry the
|
|
215
|
+
* neighbouring ISO year (e.g. 2025-12-31 → 2026-W01, 2027-01-01 → 2026-W53).
|
|
216
|
+
* Keys sort chronologically as strings. Pure.
|
|
217
|
+
*/
|
|
218
|
+
export function isoWeekKey(iso) {
|
|
219
|
+
const [y, m, d] = iso.split('-').map(Number);
|
|
220
|
+
const date = new Date(Date.UTC(y, m - 1, d));
|
|
221
|
+
const dayNum = (date.getUTCDay() + 6) % 7; // Mon=0 … Sun=6
|
|
222
|
+
date.setUTCDate(date.getUTCDate() - dayNum + 3); // Thursday of this week
|
|
223
|
+
const isoYear = date.getUTCFullYear();
|
|
224
|
+
const jan4 = new Date(Date.UTC(isoYear, 0, 4)); // Jan 4 is always in week 1
|
|
225
|
+
const jan4Day = (jan4.getUTCDay() + 6) % 7;
|
|
226
|
+
const week1Monday = new Date(jan4);
|
|
227
|
+
week1Monday.setUTCDate(jan4.getUTCDate() - jan4Day);
|
|
228
|
+
const week = 1 + Math.round((date.getTime() - week1Monday.getTime()) / (7 * 86_400_000));
|
|
229
|
+
return `${isoYear}-W${String(week).padStart(2, '0')}`;
|
|
230
|
+
}
|
|
212
231
|
const DIMENSION_KEY = {
|
|
213
232
|
day: (e) => e.dateKey,
|
|
233
|
+
week: (e) => isoWeekKey(e.dateKey),
|
|
214
234
|
month: (e) => e.monthKey,
|
|
215
235
|
model: (e) => e.model,
|
|
216
236
|
project: (e) => e.project,
|
|
@@ -253,7 +273,7 @@ export function aggregateUsage(events, dimension) {
|
|
|
253
273
|
addEvent(t, e);
|
|
254
274
|
}
|
|
255
275
|
const rows = Array.from(groups.entries()).map(([key, totals]) => ({ key, totals }));
|
|
256
|
-
if (dimension === 'day' || dimension === 'month') {
|
|
276
|
+
if (dimension === 'day' || dimension === 'week' || dimension === 'month') {
|
|
257
277
|
rows.sort((a, b) => a.key.localeCompare(b.key));
|
|
258
278
|
}
|
|
259
279
|
else {
|
|
@@ -7,6 +7,28 @@
|
|
|
7
7
|
* (`swarmdo sbom | …`, `pack`, `compact`, `redact`) truncates at the ~64 KiB
|
|
8
8
|
* pipe buffer. Awaiting the write callback guarantees the data reached the OS
|
|
9
9
|
* before we hand control back. For a TTY (synchronous) this is a no-op await.
|
|
10
|
+
*
|
|
11
|
+
* Broken-pipe (EPIPE) handling: when the reader closes the pipe early (`… |
|
|
12
|
+
* head`), Node emits an `'error'` event on the stdout Socket — often
|
|
13
|
+
* ASYNCHRONOUSLY, after our write callback has already fired — and an unhandled
|
|
14
|
+
* `'error'` event crashes the process with a raw stack trace. A per-write
|
|
15
|
+
* listener therefore doesn't cover it (it's gone by the time the async error
|
|
16
|
+
* lands). So we install ONE persistent EPIPE guard per stream that swallows the
|
|
17
|
+
* broken pipe (normal producer termination) while still rethrowing any other
|
|
18
|
+
* stream error, and additionally resolve the write promise on an EPIPE callback.
|
|
19
|
+
* The `stream` param is injectable so this is unit-testable without a subprocess.
|
|
20
|
+
*/
|
|
21
|
+
/** The subset of a writable stream writeStdout needs (process.stdout satisfies it). */
|
|
22
|
+
export interface WritableLike {
|
|
23
|
+
write(data: string, cb: (err?: Error | null) => void): boolean;
|
|
24
|
+
on(event: 'error', listener: (err: Error) => void): unknown;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Install (once per stream) a persistent `'error'` handler that swallows EPIPE —
|
|
28
|
+
* a closed reader is normal termination for a Unix producer, not a crash — and
|
|
29
|
+
* rethrows everything else so genuine stream errors still surface. Exported for
|
|
30
|
+
* the CLI entry point to arm stdout/stderr before any write happens.
|
|
10
31
|
*/
|
|
11
|
-
export declare function
|
|
32
|
+
export declare function guardStreamEpipe(stream: WritableLike): void;
|
|
33
|
+
export declare function writeStdout(data: string, stream?: WritableLike): Promise<void>;
|
|
12
34
|
//# sourceMappingURL=stdout.d.ts.map
|