swarmdo 1.30.0 → 1.45.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 +11 -6
- 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 +24 -8
- package/v3/@swarmdo/cli/dist/src/changelog/changelog.d.ts +21 -0
- package/v3/@swarmdo/cli/dist/src/changelog/changelog.js +47 -1
- package/v3/@swarmdo/cli/dist/src/codegraph/codegraph.js +11 -1
- package/v3/@swarmdo/cli/dist/src/commands/changelog.js +15 -4
- 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 +7 -1
- package/v3/@swarmdo/cli/dist/src/commands/coupling.d.ts +17 -0
- package/v3/@swarmdo/cli/dist/src/commands/coupling.js +85 -0
- package/v3/@swarmdo/cli/dist/src/commands/hooks.js +176 -1
- package/v3/@swarmdo/cli/dist/src/commands/hotspots.js +10 -3
- package/v3/@swarmdo/cli/dist/src/commands/index.js +12 -3
- 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/redact.js +11 -0
- package/v3/@swarmdo/cli/dist/src/commands/release.js +27 -5
- package/v3/@swarmdo/cli/dist/src/commands/task.js +57 -2
- 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/agents-lint.d.ts +30 -0
- package/v3/@swarmdo/cli/dist/src/config-lint/agents-lint.js +87 -0
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.d.ts +6 -1
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.js +14 -4
- package/v3/@swarmdo/cli/dist/src/coupling/coupling.d.ts +56 -0
- package/v3/@swarmdo/cli/dist/src/coupling/coupling.js +86 -0
- package/v3/@swarmdo/cli/dist/src/env/env.js +40 -14
- package/v3/@swarmdo/cli/dist/src/hooks-recipe/command-guard.d.ts +46 -0
- package/v3/@swarmdo/cli/dist/src/hooks-recipe/command-guard.js +120 -0
- package/v3/@swarmdo/cli/dist/src/hooks-recipe/recipe.js +28 -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 +4 -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/coupling-tools.d.ts +14 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/coupling-tools.js +56 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +2 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +2 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/profiles.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/task-deps.d.ts +35 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/task-deps.js +87 -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 +22 -5
- package/v3/@swarmdo/cli/dist/src/redact/sarif.d.ts +28 -0
- package/v3/@swarmdo/cli/dist/src/redact/sarif.js +53 -0
- package/v3/@swarmdo/cli/dist/src/release/release.d.ts +19 -0
- package/v3/@swarmdo/cli/dist/src/release/release.js +29 -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 +35 -1
- package/v3/@swarmdo/cli/dist/src/usage/reflect.js +58 -4
- 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/since.d.ts +13 -0
- package/v3/@swarmdo/cli/dist/src/util/since.js +21 -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
|
@@ -193,9 +193,13 @@ export function makeIgnoreMatcher(patterns) {
|
|
|
193
193
|
const dirOnly = body.endsWith('/');
|
|
194
194
|
if (dirOnly)
|
|
195
195
|
body = body.slice(0, -1);
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
196
|
+
const leadingSlash = body.startsWith('/');
|
|
197
|
+
if (leadingSlash)
|
|
198
198
|
body = body.slice(1);
|
|
199
|
+
// gitignore(5): a slash ANYWHERE (not just leading) anchors the pattern to
|
|
200
|
+
// the root — `src/fixtures` matches `src/fixtures` but NOT `a/src/fixtures`.
|
|
201
|
+
// Only a slash-free pattern (`dir`, `*.ext`) matches at any depth.
|
|
202
|
+
const anchored = leadingSlash || body.includes('/');
|
|
199
203
|
const re = new RegExp('^' + globToRegExp(body) + (dirOnly ? '(/|$)' : '($|/)'));
|
|
200
204
|
return { negate, anchored, re };
|
|
201
205
|
});
|
|
@@ -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';
|
|
@@ -44,20 +44,37 @@ export const RULES = [
|
|
|
44
44
|
* Keyword set mirrors gitleaks' reference `generic-api-key` rule (adds bare
|
|
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
|
-
* anchors it, so `keyboard=`/`monkey_val=` don't match
|
|
48
|
-
*
|
|
47
|
+
* anchors it, so `keyboard=`/`monkey_val=` don't match. A `(?<![A-Za-z])` guard
|
|
48
|
+
* requires the keyword NOT be glued to a preceding letter, so a common word that
|
|
49
|
+
* merely ENDS in a keyword — `monkey=`/`donkey=`/`whiskey=`/`turkey=` (end in
|
|
50
|
+
* `key`), `compass=`/`bypass=` (end in `pass`) — is no longer flagged as a
|
|
51
|
+
* secret, while a `_`/`-`-separated name (`PRIVATE_KEY`, `x-api-key`) still
|
|
52
|
+
* matches (`_`/`-` aren't letters). The entropy + length gates keep low-entropy
|
|
53
|
+
* values (paths, short flags) out. The value class also stops at the URL/query
|
|
54
|
+
* delimiters `&`, `?`, `#` (absent from base64/base64url/hex token alphabets):
|
|
55
|
+
* otherwise a query-string secret like `client_secret=…&api_key=…` over-captures
|
|
56
|
+
* past the `&` into the next secret, and that inflated span — overlapping a range
|
|
57
|
+
* the high-confidence RULES pass already claimed — gets dropped entirely, leaving
|
|
58
|
+
* the first secret unredacted.
|
|
49
59
|
*/
|
|
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"'
|
|
60
|
+
const ASSIGNMENT_RE = /(?<![A-Za-z])(?: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
61
|
/** Shannon entropy in bits/char. Pure; used by the assignment fallback. */
|
|
52
62
|
export function shannonEntropy(s) {
|
|
53
63
|
if (!s)
|
|
54
64
|
return 0;
|
|
65
|
+
// Iterate AND normalize by code points, not UTF-16 code units. `for..of` /
|
|
66
|
+
// spread yield one symbol per code point, but `s.length` counts an astral
|
|
67
|
+
// char (emoji, CJK Ext-B, math alphanumerics) as 2 units — so dividing the
|
|
68
|
+
// per-symbol counts by s.length made the probabilities sum to <1 and
|
|
69
|
+
// under-counted entropy up to ~2×, letting a high-entropy secret with astral
|
|
70
|
+
// chars fall under the threshold and pass through UN-redacted (#95).
|
|
71
|
+
const cps = [...s];
|
|
55
72
|
const freq = new Map();
|
|
56
|
-
for (const ch of
|
|
73
|
+
for (const ch of cps)
|
|
57
74
|
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
58
75
|
let bits = 0;
|
|
59
76
|
for (const n of freq.values()) {
|
|
60
|
-
const p = n /
|
|
77
|
+
const p = n / cps.length;
|
|
61
78
|
bits -= p * Math.log2(p);
|
|
62
79
|
}
|
|
63
80
|
return bits;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sarif.ts — render redact secret-scan findings as a SARIF 2.1.0 document.
|
|
3
|
+
*
|
|
4
|
+
* SARIF (Static Analysis Results Interchange Format) is a ratified OASIS
|
|
5
|
+
* standard that GitHub code-scanning ingests via `github/codeql-action/
|
|
6
|
+
* upload-sarif`. Emit it from `redact --scan` in CI and leaked-secret findings
|
|
7
|
+
* surface as alerts in the Security tab + PR annotations instead of dying in
|
|
8
|
+
* the build log — the same thing gitleaks/trufflehog/ggshield do with their
|
|
9
|
+
* `--report-format sarif`.
|
|
10
|
+
*
|
|
11
|
+
* Pure + deterministic: findings in, JSON string out. No fs/network.
|
|
12
|
+
*/
|
|
13
|
+
import type { Finding } from './redact.js';
|
|
14
|
+
export interface SarifOptions {
|
|
15
|
+
/** SARIF tool.driver.name (default 'swarmdo-redact') */
|
|
16
|
+
toolName?: string;
|
|
17
|
+
/** SARIF tool.driver.version — omitted from the document when not given */
|
|
18
|
+
toolVersion?: string;
|
|
19
|
+
/**
|
|
20
|
+
* URI for the artifact each finding lives in. `redact --scan` reads a STREAM
|
|
21
|
+
* (stdin / wrapped output), so there is no committed file by default; pass
|
|
22
|
+
* this (e.g. from `--source <path>`) to anchor the alerts onto a repo path.
|
|
23
|
+
*/
|
|
24
|
+
artifactUri?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Render `findings` as a SARIF 2.1.0 document string. Pure + deterministic. */
|
|
27
|
+
export declare function toSarif(findings: Finding[], opts?: SarifOptions): string;
|
|
28
|
+
//# sourceMappingURL=sarif.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sarif.ts — render redact secret-scan findings as a SARIF 2.1.0 document.
|
|
3
|
+
*
|
|
4
|
+
* SARIF (Static Analysis Results Interchange Format) is a ratified OASIS
|
|
5
|
+
* standard that GitHub code-scanning ingests via `github/codeql-action/
|
|
6
|
+
* upload-sarif`. Emit it from `redact --scan` in CI and leaked-secret findings
|
|
7
|
+
* surface as alerts in the Security tab + PR annotations instead of dying in
|
|
8
|
+
* the build log — the same thing gitleaks/trufflehog/ggshield do with their
|
|
9
|
+
* `--report-format sarif`.
|
|
10
|
+
*
|
|
11
|
+
* Pure + deterministic: findings in, JSON string out. No fs/network.
|
|
12
|
+
*/
|
|
13
|
+
const SARIF_SCHEMA = 'https://json.schemastore.org/sarif-2.1.0.json';
|
|
14
|
+
const INFO_URI = 'the upstream project (see NOTICE)';
|
|
15
|
+
/** Render `findings` as a SARIF 2.1.0 document string. Pure + deterministic. */
|
|
16
|
+
export function toSarif(findings, opts = {}) {
|
|
17
|
+
const toolName = opts.toolName ?? 'swarmdo-redact';
|
|
18
|
+
// One rule descriptor per distinct ruleId, in first-seen order. Findings
|
|
19
|
+
// carry their own description, so the entropy fallback (not in RULES) is
|
|
20
|
+
// covered too — every result.ruleId is guaranteed to appear in rules[].
|
|
21
|
+
const ruleIndex = new Map();
|
|
22
|
+
const rules = [];
|
|
23
|
+
for (const f of findings) {
|
|
24
|
+
if (ruleIndex.has(f.ruleId))
|
|
25
|
+
continue;
|
|
26
|
+
ruleIndex.set(f.ruleId, rules.length);
|
|
27
|
+
rules.push({ id: f.ruleId, name: f.ruleId, shortDescription: { text: f.description } });
|
|
28
|
+
}
|
|
29
|
+
const results = findings.map((f) => {
|
|
30
|
+
const physicalLocation = {
|
|
31
|
+
region: { startLine: f.line, startColumn: f.column },
|
|
32
|
+
};
|
|
33
|
+
if (opts.artifactUri)
|
|
34
|
+
physicalLocation.artifactLocation = { uri: opts.artifactUri };
|
|
35
|
+
return {
|
|
36
|
+
ruleId: f.ruleId,
|
|
37
|
+
ruleIndex: ruleIndex.get(f.ruleId),
|
|
38
|
+
level: 'error',
|
|
39
|
+
message: { text: `Possible secret: ${f.description}` },
|
|
40
|
+
locations: [{ physicalLocation }],
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const driver = { name: toolName, informationUri: INFO_URI, rules };
|
|
44
|
+
if (opts.toolVersion)
|
|
45
|
+
driver.version = opts.toolVersion;
|
|
46
|
+
const doc = {
|
|
47
|
+
$schema: SARIF_SCHEMA,
|
|
48
|
+
version: '2.1.0',
|
|
49
|
+
runs: [{ tool: { driver }, results }],
|
|
50
|
+
};
|
|
51
|
+
return JSON.stringify(doc, null, 2);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=sarif.js.map
|
|
@@ -69,4 +69,23 @@ export declare const DOC_FILES: string[];
|
|
|
69
69
|
export declare function planRelease(input: ReleaseInput): ReleasePlan;
|
|
70
70
|
/** One-line human rendering of a step (for the dry-run plan). */
|
|
71
71
|
export declare function renderStep(s: ReleaseStep): string;
|
|
72
|
+
export interface SiteIdentity {
|
|
73
|
+
name: string;
|
|
74
|
+
email: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the committer identity for the throwaway swarmdo.com clone. That
|
|
78
|
+
* clone inherits no identity, and a machine may have only a *local* (per-repo)
|
|
79
|
+
* git user — so `git commit` there dies with "Author identity unknown" unless
|
|
80
|
+
* we inject one. Prefer the operator's configured identity; fall back to the
|
|
81
|
+
* Swarmdo bot so the site deploy never fails on a missing global git config.
|
|
82
|
+
* `readConfig` is injected (real: `git -C <root> config <key>`) so this stays
|
|
83
|
+
* pure and unit-testable.
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveSiteIdentity(readConfig: (key: string) => string | undefined): SiteIdentity;
|
|
86
|
+
/**
|
|
87
|
+
* `git commit` argv for the site sync — injects the identity via `-c` so the
|
|
88
|
+
* commit succeeds without a global git user, and keeps the bot co-author trailer.
|
|
89
|
+
*/
|
|
90
|
+
export declare function siteCommitArgs(version: string, id: SiteIdentity): string[];
|
|
72
91
|
//# sourceMappingURL=release.d.ts.map
|
|
@@ -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) {
|
|
@@ -83,4 +83,32 @@ export function renderStep(s) {
|
|
|
83
83
|
case 'deploy-site': return `deploy website → SwarmDo/swarmdo.com (copy working copy, push, curl-verify swarmdo.com serves ${s.version})`;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the committer identity for the throwaway swarmdo.com clone. That
|
|
88
|
+
* clone inherits no identity, and a machine may have only a *local* (per-repo)
|
|
89
|
+
* git user — so `git commit` there dies with "Author identity unknown" unless
|
|
90
|
+
* we inject one. Prefer the operator's configured identity; fall back to the
|
|
91
|
+
* Swarmdo bot so the site deploy never fails on a missing global git config.
|
|
92
|
+
* `readConfig` is injected (real: `git -C <root> config <key>`) so this stays
|
|
93
|
+
* pure and unit-testable.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveSiteIdentity(readConfig) {
|
|
96
|
+
const name = (readConfig('user.name') || '').trim();
|
|
97
|
+
const email = (readConfig('user.email') || '').trim();
|
|
98
|
+
return {
|
|
99
|
+
name: name || 'Swarmdo',
|
|
100
|
+
email: email || 'maintainers@swarmdo.com',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `git commit` argv for the site sync — injects the identity via `-c` so the
|
|
105
|
+
* commit succeeds without a global git user, and keeps the bot co-author trailer.
|
|
106
|
+
*/
|
|
107
|
+
export function siteCommitArgs(version, id) {
|
|
108
|
+
return [
|
|
109
|
+
'-c', `user.name=${id.name}`,
|
|
110
|
+
'-c', `user.email=${id.email}`,
|
|
111
|
+
'commit', '-m', `release: sync site for v${version}\n\nCo-Authored-By: Swarmdo <maintainers@swarmdo.com>`,
|
|
112
|
+
];
|
|
113
|
+
}
|
|
86
114
|
//# sourceMappingURL=release.js.map
|
|
@@ -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)) : ''}
|