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.
Files changed (88) hide show
  1. package/README.md +11 -6
  2. package/package.json +1 -1
  3. package/v3/@swarmdo/cli/dist/src/affected/affected.d.ts +6 -1
  4. package/v3/@swarmdo/cli/dist/src/affected/affected.js +7 -2
  5. package/v3/@swarmdo/cli/dist/src/apply/apply.js +24 -8
  6. package/v3/@swarmdo/cli/dist/src/changelog/changelog.d.ts +21 -0
  7. package/v3/@swarmdo/cli/dist/src/changelog/changelog.js +47 -1
  8. package/v3/@swarmdo/cli/dist/src/codegraph/codegraph.js +11 -1
  9. package/v3/@swarmdo/cli/dist/src/commands/changelog.js +15 -4
  10. package/v3/@swarmdo/cli/dist/src/commands/comms.d.ts +16 -0
  11. package/v3/@swarmdo/cli/dist/src/commands/comms.js +280 -0
  12. package/v3/@swarmdo/cli/dist/src/commands/compact.js +6 -2
  13. package/v3/@swarmdo/cli/dist/src/commands/config.js +7 -1
  14. package/v3/@swarmdo/cli/dist/src/commands/coupling.d.ts +17 -0
  15. package/v3/@swarmdo/cli/dist/src/commands/coupling.js +85 -0
  16. package/v3/@swarmdo/cli/dist/src/commands/hooks.js +176 -1
  17. package/v3/@swarmdo/cli/dist/src/commands/hotspots.js +10 -3
  18. package/v3/@swarmdo/cli/dist/src/commands/index.js +12 -3
  19. package/v3/@swarmdo/cli/dist/src/commands/permissions.d.ts +14 -0
  20. package/v3/@swarmdo/cli/dist/src/commands/permissions.js +92 -0
  21. package/v3/@swarmdo/cli/dist/src/commands/redact.js +11 -0
  22. package/v3/@swarmdo/cli/dist/src/commands/release.js +27 -5
  23. package/v3/@swarmdo/cli/dist/src/commands/task.js +57 -2
  24. package/v3/@swarmdo/cli/dist/src/commands/usage.js +34 -1
  25. package/v3/@swarmdo/cli/dist/src/comms/mailbox.d.ts +88 -0
  26. package/v3/@swarmdo/cli/dist/src/comms/mailbox.js +138 -0
  27. package/v3/@swarmdo/cli/dist/src/comms/store.d.ts +21 -0
  28. package/v3/@swarmdo/cli/dist/src/comms/store.js +44 -0
  29. package/v3/@swarmdo/cli/dist/src/compact/compact.js +4 -1
  30. package/v3/@swarmdo/cli/dist/src/compact-snapshot/compact-snapshot.js +8 -3
  31. package/v3/@swarmdo/cli/dist/src/config-lint/agents-lint.d.ts +30 -0
  32. package/v3/@swarmdo/cli/dist/src/config-lint/agents-lint.js +87 -0
  33. package/v3/@swarmdo/cli/dist/src/config-lint/lint.d.ts +6 -1
  34. package/v3/@swarmdo/cli/dist/src/config-lint/lint.js +14 -4
  35. package/v3/@swarmdo/cli/dist/src/coupling/coupling.d.ts +56 -0
  36. package/v3/@swarmdo/cli/dist/src/coupling/coupling.js +86 -0
  37. package/v3/@swarmdo/cli/dist/src/env/env.js +40 -14
  38. package/v3/@swarmdo/cli/dist/src/hooks-recipe/command-guard.d.ts +46 -0
  39. package/v3/@swarmdo/cli/dist/src/hooks-recipe/command-guard.js +120 -0
  40. package/v3/@swarmdo/cli/dist/src/hooks-recipe/recipe.js +28 -1
  41. package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.d.ts +3 -0
  42. package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.js +9 -0
  43. package/v3/@swarmdo/cli/dist/src/index.js +8 -0
  44. package/v3/@swarmdo/cli/dist/src/license/license.js +4 -1
  45. package/v3/@swarmdo/cli/dist/src/mcp-client.js +4 -0
  46. package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.d.ts +11 -0
  47. package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.js +93 -0
  48. package/v3/@swarmdo/cli/dist/src/mcp-tools/coupling-tools.d.ts +14 -0
  49. package/v3/@swarmdo/cli/dist/src/mcp-tools/coupling-tools.js +56 -0
  50. package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +2 -0
  51. package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +2 -0
  52. package/v3/@swarmdo/cli/dist/src/mcp-tools/profiles.js +1 -0
  53. package/v3/@swarmdo/cli/dist/src/mcp-tools/task-deps.d.ts +35 -0
  54. package/v3/@swarmdo/cli/dist/src/mcp-tools/task-deps.js +87 -0
  55. package/v3/@swarmdo/cli/dist/src/mcp-tools/task-tools.js +4 -0
  56. package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.d.ts +2 -0
  57. package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.js +4 -2
  58. package/v3/@swarmdo/cli/dist/src/memory-inject/select.d.ts +82 -0
  59. package/v3/@swarmdo/cli/dist/src/memory-inject/select.js +164 -0
  60. package/v3/@swarmdo/cli/dist/src/memory-vault/import.js +11 -1
  61. package/v3/@swarmdo/cli/dist/src/pack/pack.js +6 -2
  62. package/v3/@swarmdo/cli/dist/src/permissions/audit.d.ts +50 -0
  63. package/v3/@swarmdo/cli/dist/src/permissions/audit.js +127 -0
  64. package/v3/@swarmdo/cli/dist/src/plugins/store/search.js +1 -1
  65. package/v3/@swarmdo/cli/dist/src/redact/redact.js +22 -5
  66. package/v3/@swarmdo/cli/dist/src/redact/sarif.d.ts +28 -0
  67. package/v3/@swarmdo/cli/dist/src/redact/sarif.js +53 -0
  68. package/v3/@swarmdo/cli/dist/src/release/release.d.ts +19 -0
  69. package/v3/@swarmdo/cli/dist/src/release/release.js +29 -1
  70. package/v3/@swarmdo/cli/dist/src/sbom/sbom.d.ts +2 -0
  71. package/v3/@swarmdo/cli/dist/src/sbom/sbom.js +5 -0
  72. package/v3/@swarmdo/cli/dist/src/swarmvector/semantic-router.js +8 -3
  73. package/v3/@swarmdo/cli/dist/src/testreport/testreport.js +9 -2
  74. package/v3/@swarmdo/cli/dist/src/transcript/export.js +8 -4
  75. package/v3/@swarmdo/cli/dist/src/update/checker.d.ts +1 -0
  76. package/v3/@swarmdo/cli/dist/src/update/checker.js +7 -2
  77. package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.d.ts +29 -0
  78. package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.js +28 -0
  79. package/v3/@swarmdo/cli/dist/src/usage/reflect-html.js +1 -0
  80. package/v3/@swarmdo/cli/dist/src/usage/reflect.d.ts +35 -1
  81. package/v3/@swarmdo/cli/dist/src/usage/reflect.js +58 -4
  82. package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.d.ts +8 -1
  83. package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.js +21 -1
  84. package/v3/@swarmdo/cli/dist/src/util/since.d.ts +13 -0
  85. package/v3/@swarmdo/cli/dist/src/util/since.js +21 -0
  86. package/v3/@swarmdo/cli/dist/src/util/stdout.d.ts +23 -1
  87. package/v3/@swarmdo/cli/dist/src/util/stdout.js +34 -2
  88. package/v3/@swarmdo/cli/package.json +1 -1
@@ -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;
@@ -83,10 +90,37 @@ export declare function nextDay(iso: string): string;
83
90
  * Pure. Used to derive a `--period 1m|3m|…` window start from today.
84
91
  */
85
92
  export declare function monthsBefore(iso: string, n: number): string;
86
- /** Inclusive whole-day span between two ISO dates (from <= to). Pure. */
93
+ /**
94
+ * Inclusive whole-day span between two ISO dates (from <= to). Pure.
95
+ *
96
+ * Counts on the UTC timeline (`Date.UTC`), NOT local midnights: a local
97
+ * `new Date('…T00:00:00')` is DST-sensitive, so a range crossing a spring-forward
98
+ * day (only 23h long) would floor-divide to one day short — e.g. under
99
+ * TZ=America/New_York a full March would read 30. UTC calendar midnights are
100
+ * always an exact 86.4M ms apart, so this is correct in every process timezone.
101
+ */
87
102
  export declare function spanDays(from: string, to: string): number;
88
103
  /** Longest run of consecutive calendar days present in the set. Pure. */
89
104
  export declare function longestStreakOf(days: string[]): number;
105
+ export interface SpikeDay {
106
+ day: string;
107
+ costUsd: number;
108
+ /** costUsd / median active-day cost */
109
+ ratioToMedian: number;
110
+ }
111
+ /**
112
+ * Days whose cost is an outlier vs the median active-day cost — a heads-up for
113
+ * runaway sessions. Median (not mean) so the spikes don't inflate the baseline.
114
+ * Needs ≥3 active days for a meaningful median; a floor keeps trivially-cheap
115
+ * days from tripping the ratio. Sorted by cost desc. Pure.
116
+ */
117
+ export declare function detectSpikeDays(days: Array<{
118
+ day: string;
119
+ costUsd: number;
120
+ }>, opts?: {
121
+ minRatio?: number;
122
+ minCostUsd?: number;
123
+ }): SpikeDay[];
90
124
  /** Render a numeric series as a Unicode block sparkline (one char per bucket).
91
125
  * Zero buckets are blank; the rest scale ▁..█ against the max. Pure — used for
92
126
  * 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) {
@@ -38,11 +40,21 @@ export function monthsBefore(iso, n) {
38
40
  const dd = String(dt.getDate()).padStart(2, '0');
39
41
  return `${yy}-${mm}-${dd}`;
40
42
  }
41
- /** Inclusive whole-day span between two ISO dates (from <= to). Pure. */
43
+ /**
44
+ * Inclusive whole-day span between two ISO dates (from <= to). Pure.
45
+ *
46
+ * Counts on the UTC timeline (`Date.UTC`), NOT local midnights: a local
47
+ * `new Date('…T00:00:00')` is DST-sensitive, so a range crossing a spring-forward
48
+ * day (only 23h long) would floor-divide to one day short — e.g. under
49
+ * TZ=America/New_York a full March would read 30. UTC calendar midnights are
50
+ * always an exact 86.4M ms apart, so this is correct in every process timezone.
51
+ */
42
52
  export function spanDays(from, to) {
43
- const a = new Date(from + 'T00:00:00');
44
- const b = new Date(to + 'T00:00:00');
45
- return Math.floor((b.getTime() - a.getTime()) / 86_400_000) + 1;
53
+ const [ay, am, ad] = from.split('-').map(Number);
54
+ const [by, bm, bd] = to.split('-').map(Number);
55
+ const a = Date.UTC(ay, am - 1, ad);
56
+ const b = Date.UTC(by, bm - 1, bd);
57
+ return Math.round((b - a) / 86_400_000) + 1;
46
58
  }
47
59
  /** Longest run of consecutive calendar days present in the set. Pure. */
48
60
  export function longestStreakOf(days) {
@@ -58,6 +70,27 @@ export function longestStreakOf(days) {
58
70
  }
59
71
  return best;
60
72
  }
73
+ /**
74
+ * Days whose cost is an outlier vs the median active-day cost — a heads-up for
75
+ * runaway sessions. Median (not mean) so the spikes don't inflate the baseline.
76
+ * Needs ≥3 active days for a meaningful median; a floor keeps trivially-cheap
77
+ * days from tripping the ratio. Sorted by cost desc. Pure.
78
+ */
79
+ export function detectSpikeDays(days, opts = {}) {
80
+ const minRatio = opts.minRatio ?? 2.5;
81
+ const minCostUsd = opts.minCostUsd ?? 0.5;
82
+ const costs = days.map((d) => d.costUsd).filter((c) => c > 0).sort((a, b) => a - b);
83
+ if (costs.length < 3)
84
+ return [];
85
+ const mid = costs.length >> 1;
86
+ const median = costs.length % 2 ? costs[mid] : (costs[mid - 1] + costs[mid]) / 2;
87
+ if (median <= 0)
88
+ return [];
89
+ return days
90
+ .filter((d) => d.costUsd >= minCostUsd && d.costUsd >= minRatio * median)
91
+ .map((d) => ({ day: d.day, costUsd: d.costUsd, ratioToMedian: d.costUsd / median }))
92
+ .sort((a, b) => b.costUsd - a.costUsd);
93
+ }
61
94
  const within = (key, from, to) => key >= from && key <= to;
62
95
  const SPARK_BLOCKS = ' ▁▂▃▄▅▆▇█'; // index 0 = empty (zero), 1..8 = ▁..█ scaled to max
63
96
  /** Render a numeric series as a Unicode block sparkline (one char per bucket).
@@ -145,6 +178,25 @@ export function computeReflection(dayRows, modelRows, from, to, opts = {}, proje
145
178
  // Cost-ranked shares over the same window, for both models and projects.
146
179
  const topModels = rankShares(modelRows, from, to, totals.costUsd, topN);
147
180
  const topProjects = rankShares(projectRows, from, to, totals.costUsd, topN);
181
+ // $ saved by prompt caching this period: fold windowed per-model token counts,
182
+ // then price each via the (injected) resolver. Unpriced models contribute 0.
183
+ const resolvePrice = opts.resolvePrice ?? resolveTranscriptPrice;
184
+ const cacheTokens = new Map();
185
+ for (const r of modelRows) {
186
+ if (!within(r.day, from, to))
187
+ continue;
188
+ const s = cacheTokens.get(r.key) ?? { inputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 };
189
+ s.inputTokens += r.totals.inputTokens;
190
+ s.cacheWriteTokens += r.totals.cacheWriteTokens;
191
+ s.cacheReadTokens += r.totals.cacheReadTokens;
192
+ cacheTokens.set(r.key, s);
193
+ }
194
+ let cacheSavingsUsd = 0;
195
+ for (const [model, tok] of cacheTokens) {
196
+ const sv = modelCacheSavings(tok, resolvePrice(model));
197
+ if (sv !== null)
198
+ cacheSavingsUsd += sv;
199
+ }
148
200
  const inputSide = totals.inputTokens + totals.cacheWriteTokens + totals.cacheReadTokens;
149
201
  const relChange = firstHalfCost > 0 ? (secondHalfCost - firstHalfCost) / firstHalfCost : (secondHalfCost > 0 ? 1 : 0);
150
202
  const direction = Math.abs(relChange) < flat ? 'flat' : relChange > 0 ? 'up' : 'down';
@@ -152,6 +204,7 @@ export function computeReflection(dayRows, modelRows, from, to, opts = {}, proje
152
204
  period: { from, to, spanDays: spanDays(from, to) },
153
205
  totals,
154
206
  busiestDay,
207
+ spikeDays: detectSpikeDays(days.map((r) => ({ day: r.key, costUsd: r.totals.costUsd }))),
155
208
  topModels,
156
209
  topProjects,
157
210
  peakHour: peakHourOf(hourHistogram),
@@ -160,6 +213,7 @@ export function computeReflection(dayRows, modelRows, from, to, opts = {}, proje
160
213
  cacheReadPct: inputSide > 0 ? totals.cacheReadTokens / inputSide : 0,
161
214
  avgCostPerActiveDay: totals.activeDays > 0 ? totals.costUsd / totals.activeDays : 0,
162
215
  trend: { firstHalfCost, secondHalfCost, direction },
216
+ cacheSavingsUsd,
163
217
  };
164
218
  }
165
219
  //# 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 {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * since.ts — normalize compact age windows for `git log --since`.
3
+ *
4
+ * git's approxidate parser does NOT understand the bare compact form `90d`
5
+ * (it silently matches nothing — `git log --since=90d` returns zero commits),
6
+ * yet `hotspots`/`coupling` document `--since 90d`. This maps the compact forms
7
+ * (`90d`, `6mo`, `2w`, `1y`, `3h`) to the spelled-out `"90 days ago"` git DOES
8
+ * understand. Anything already spelled out (`"3 months ago"`), an ISO date, or
9
+ * an unrecognized token passes through unchanged. Pure + unit-tested.
10
+ */
11
+ /** Convert `90d`→`90 days ago`, `6mo`→`6 months ago`, etc. Passthrough otherwise. */
12
+ export declare function normalizeSince(since: string): string;
13
+ //# sourceMappingURL=since.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * since.ts — normalize compact age windows for `git log --since`.
3
+ *
4
+ * git's approxidate parser does NOT understand the bare compact form `90d`
5
+ * (it silently matches nothing — `git log --since=90d` returns zero commits),
6
+ * yet `hotspots`/`coupling` document `--since 90d`. This maps the compact forms
7
+ * (`90d`, `6mo`, `2w`, `1y`, `3h`) to the spelled-out `"90 days ago"` git DOES
8
+ * understand. Anything already spelled out (`"3 months ago"`), an ISO date, or
9
+ * an unrecognized token passes through unchanged. Pure + unit-tested.
10
+ */
11
+ const COMPACT = /^\s*(\d+)\s*(d|days?|w|wks?|weeks?|mo|mons?|months?|y|yrs?|years?|h|hrs?|hours?)\s*$/i;
12
+ /** Convert `90d`→`90 days ago`, `6mo`→`6 months ago`, etc. Passthrough otherwise. */
13
+ export function normalizeSince(since) {
14
+ const m = COMPACT.exec(since);
15
+ if (!m)
16
+ return since;
17
+ const c0 = m[2][0].toLowerCase(); // the allowed units are uniquely keyed by first letter
18
+ const unit = c0 === 'd' ? 'days' : c0 === 'w' ? 'weeks' : c0 === 'y' ? 'years' : c0 === 'h' ? 'hours' : 'months';
19
+ return `${m[1]} ${unit} ago`;
20
+ }
21
+ //# sourceMappingURL=since.js.map
@@ -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 writeStdout(data: string): Promise<void>;
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
@@ -7,10 +7,42 @@
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
+ const guarded = new WeakSet();
22
+ /**
23
+ * Install (once per stream) a persistent `'error'` handler that swallows EPIPE —
24
+ * a closed reader is normal termination for a Unix producer, not a crash — and
25
+ * rethrows everything else so genuine stream errors still surface. Exported for
26
+ * the CLI entry point to arm stdout/stderr before any write happens.
10
27
  */
11
- export function writeStdout(data) {
28
+ export function guardStreamEpipe(stream) {
29
+ if (guarded.has(stream))
30
+ return;
31
+ guarded.add(stream);
32
+ stream.on('error', (err) => {
33
+ if (err.code !== 'EPIPE')
34
+ throw err;
35
+ });
36
+ }
37
+ export function writeStdout(data, stream = process.stdout) {
38
+ guardStreamEpipe(stream);
12
39
  return new Promise((resolve, reject) => {
13
- process.stdout.write(data, (err) => (err ? reject(err) : resolve()));
40
+ stream.write(data, (err) => {
41
+ if (err && err.code !== 'EPIPE')
42
+ reject(err);
43
+ else
44
+ resolve();
45
+ });
14
46
  });
15
47
  }
16
48
  //# sourceMappingURL=stdout.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmdo/cli",
3
- "version": "1.30.0",
3
+ "version": "1.45.0",
4
4
  "type": "module",
5
5
  "description": "Swarmdo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",