swarmdo 1.29.0 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +66 -33
  2. package/README.md +6 -3
  3. package/package.json +1 -1
  4. package/v3/@swarmdo/cli/.claude/helpers/auto-memory-hook.mjs +65 -32
  5. package/v3/@swarmdo/cli/dist/src/affected/affected.d.ts +6 -1
  6. package/v3/@swarmdo/cli/dist/src/affected/affected.js +7 -2
  7. package/v3/@swarmdo/cli/dist/src/apply/apply.js +18 -7
  8. package/v3/@swarmdo/cli/dist/src/changelog/changelog.js +4 -1
  9. package/v3/@swarmdo/cli/dist/src/codegraph/codegraph.js +11 -1
  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 +1 -0
  14. package/v3/@swarmdo/cli/dist/src/commands/hooks.js +121 -1
  15. package/v3/@swarmdo/cli/dist/src/commands/hotspots.js +8 -2
  16. package/v3/@swarmdo/cli/dist/src/commands/index.js +6 -0
  17. package/v3/@swarmdo/cli/dist/src/commands/permissions.d.ts +14 -0
  18. package/v3/@swarmdo/cli/dist/src/commands/permissions.js +92 -0
  19. package/v3/@swarmdo/cli/dist/src/commands/usage.js +208 -3
  20. package/v3/@swarmdo/cli/dist/src/comms/mailbox.d.ts +88 -0
  21. package/v3/@swarmdo/cli/dist/src/comms/mailbox.js +138 -0
  22. package/v3/@swarmdo/cli/dist/src/comms/store.d.ts +21 -0
  23. package/v3/@swarmdo/cli/dist/src/comms/store.js +44 -0
  24. package/v3/@swarmdo/cli/dist/src/compact/compact.js +4 -1
  25. package/v3/@swarmdo/cli/dist/src/compact-snapshot/compact-snapshot.js +8 -3
  26. package/v3/@swarmdo/cli/dist/src/config-lint/lint.d.ts +3 -1
  27. package/v3/@swarmdo/cli/dist/src/config-lint/lint.js +11 -4
  28. package/v3/@swarmdo/cli/dist/src/env/env.js +40 -14
  29. package/v3/@swarmdo/cli/dist/src/hooks-recipe/recipe.js +20 -1
  30. package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.d.ts +3 -0
  31. package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.js +9 -0
  32. package/v3/@swarmdo/cli/dist/src/index.js +8 -0
  33. package/v3/@swarmdo/cli/dist/src/license/license.js +4 -1
  34. package/v3/@swarmdo/cli/dist/src/mcp-client.js +2 -0
  35. package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.d.ts +11 -0
  36. package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.js +93 -0
  37. package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +1 -0
  38. package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +1 -0
  39. package/v3/@swarmdo/cli/dist/src/mcp-tools/task-tools.js +4 -0
  40. package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.d.ts +2 -0
  41. package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.js +4 -2
  42. package/v3/@swarmdo/cli/dist/src/memory-inject/select.d.ts +82 -0
  43. package/v3/@swarmdo/cli/dist/src/memory-inject/select.js +164 -0
  44. package/v3/@swarmdo/cli/dist/src/memory-vault/import.js +11 -1
  45. package/v3/@swarmdo/cli/dist/src/pack/pack.js +6 -2
  46. package/v3/@swarmdo/cli/dist/src/permissions/audit.d.ts +50 -0
  47. package/v3/@swarmdo/cli/dist/src/permissions/audit.js +127 -0
  48. package/v3/@swarmdo/cli/dist/src/plugins/store/search.js +1 -1
  49. package/v3/@swarmdo/cli/dist/src/redact/redact.js +7 -2
  50. package/v3/@swarmdo/cli/dist/src/release/release.js +1 -1
  51. package/v3/@swarmdo/cli/dist/src/sbom/sbom.d.ts +2 -0
  52. package/v3/@swarmdo/cli/dist/src/sbom/sbom.js +5 -0
  53. package/v3/@swarmdo/cli/dist/src/swarmvector/semantic-router.js +8 -3
  54. package/v3/@swarmdo/cli/dist/src/testreport/testreport.js +9 -2
  55. package/v3/@swarmdo/cli/dist/src/transcript/export.js +8 -4
  56. package/v3/@swarmdo/cli/dist/src/update/checker.d.ts +1 -0
  57. package/v3/@swarmdo/cli/dist/src/update/checker.js +7 -2
  58. package/v3/@swarmdo/cli/dist/src/usage/limits.d.ts +77 -0
  59. package/v3/@swarmdo/cli/dist/src/usage/limits.js +134 -0
  60. package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.d.ts +29 -0
  61. package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.js +28 -0
  62. package/v3/@swarmdo/cli/dist/src/usage/reflect-html.d.ts +23 -0
  63. package/v3/@swarmdo/cli/dist/src/usage/reflect-html.js +95 -0
  64. package/v3/@swarmdo/cli/dist/src/usage/reflect.d.ts +131 -0
  65. package/v3/@swarmdo/cli/dist/src/usage/reflect.js +209 -0
  66. package/v3/@swarmdo/cli/dist/src/usage/spend-forecast.d.ts +28 -0
  67. package/v3/@swarmdo/cli/dist/src/usage/spend-forecast.js +33 -0
  68. package/v3/@swarmdo/cli/dist/src/usage/transcript-errors.d.ts +14 -0
  69. package/v3/@swarmdo/cli/dist/src/usage/transcript-errors.js +10 -0
  70. package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.d.ts +8 -1
  71. package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.js +21 -1
  72. package/v3/@swarmdo/cli/dist/src/util/csv.d.ts +14 -0
  73. package/v3/@swarmdo/cli/dist/src/util/csv.js +20 -0
  74. package/v3/@swarmdo/cli/dist/src/util/stdout.d.ts +23 -1
  75. package/v3/@swarmdo/cli/dist/src/util/stdout.js +34 -2
  76. package/v3/@swarmdo/cli/package.json +1 -1
@@ -0,0 +1,280 @@
1
+ /**
2
+ * comms.ts — `swarmdo comms` — a cross-session agent mailbox (#44).
3
+ *
4
+ * swarmdo comms send -t <session> -m "<message>" leave a message for a session
5
+ * swarmdo comms inbox [-t <self>] [-u] list your messages (newest first)
6
+ * swarmdo comms read <id> show a full message + mark it read
7
+ *
8
+ * Any Claude Code session on the same repo shares `.swarmdo/comms/store.json`, so
9
+ * one session can message another by name ("multiplayer swarms"). Address `all`
10
+ * to broadcast. The message model, filtering, and retention live in the pure
11
+ * comms/mailbox.ts engine; this layer is fs persistence + presentation.
12
+ */
13
+ import { output } from '../output.js';
14
+ import { createMessage, addMessage, filterInbox, unreadCount, markRead, pruneMailbox, renderInboxContext, } from '../comms/mailbox.js';
15
+ import { loadMailbox, saveMailbox, resolveSelf, newMessageId, RETENTION } from '../comms/store.js';
16
+ /** Compact relative age, e.g. "3m", "2h", "5d", or "now". */
17
+ function relAge(iso, nowMs) {
18
+ const ms = nowMs - Date.parse(iso);
19
+ if (!Number.isFinite(ms) || ms < 0)
20
+ return '—';
21
+ const s = Math.floor(ms / 1000);
22
+ if (s < 60)
23
+ return 'now';
24
+ const m = Math.floor(s / 60);
25
+ if (m < 60)
26
+ return `${m}m`;
27
+ const h = Math.floor(m / 60);
28
+ if (h < 24)
29
+ return `${h}h`;
30
+ return `${Math.floor(h / 24)}d`;
31
+ }
32
+ const sendCommand = {
33
+ name: 'send',
34
+ description: 'Send a message to another session (address "all" to broadcast)',
35
+ options: [
36
+ { name: 'to', short: 't', description: 'Recipient session name (or "all")', type: 'string' },
37
+ { name: 'message', short: 'm', description: 'Message body', type: 'string' },
38
+ { name: 'subject', short: 's', description: 'Subject line', type: 'string' },
39
+ { name: 'from', short: 'f', description: 'Sender name (default: this session)', type: 'string' },
40
+ { name: 'json', description: 'Machine-readable output', type: 'boolean' },
41
+ ],
42
+ examples: [
43
+ { command: 'swarmdo comms send -t reviewer -m "auth PR is ready" -s "PR #42"', description: 'Message the "reviewer" session' },
44
+ { command: 'swarmdo comms send -t all -m "rebasing main in 5min"', description: 'Broadcast to every session' },
45
+ ],
46
+ action: async (ctx) => {
47
+ const cwd = ctx.cwd || process.cwd();
48
+ const to = (ctx.flags.to || ctx.args[0] || '').trim();
49
+ const body = (ctx.flags.message ?? ctx.args[1] ?? '').toString();
50
+ if (!to) {
51
+ output.printError('Recipient required. Use --to/-t <session> (or "all").');
52
+ return { success: false, exitCode: 1 };
53
+ }
54
+ if (!body.trim()) {
55
+ output.printError('Message body required. Use --message/-m "<text>".');
56
+ return { success: false, exitCode: 1 };
57
+ }
58
+ const from = resolveSelf(ctx.flags.from);
59
+ const msg = createMessage({
60
+ id: newMessageId(),
61
+ from,
62
+ to,
63
+ subject: ctx.flags.subject,
64
+ body,
65
+ createdAt: new Date().toISOString(),
66
+ });
67
+ let box = addMessage(loadMailbox(cwd), msg);
68
+ box = pruneMailbox(box, { ...RETENTION, nowMs: Date.now() });
69
+ saveMailbox(cwd, box);
70
+ if (ctx.flags.json === true) {
71
+ output.printJson({ sent: true, id: msg.id, to: msg.to, from: msg.from });
72
+ return { success: true, data: { id: msg.id } };
73
+ }
74
+ output.printSuccess(`Sent to ${output.highlight(msg.to)} — id ${output.dim(msg.id)}`);
75
+ return { success: true, data: { id: msg.id } };
76
+ },
77
+ };
78
+ const inboxCommand = {
79
+ name: 'inbox',
80
+ aliases: ['list'],
81
+ description: 'List messages addressed to you (newest first)',
82
+ options: [
83
+ { name: 'to', short: 't', description: 'Whose inbox (default: this session)', type: 'string' },
84
+ { name: 'from', short: 'f', description: 'Only messages from this sender', type: 'string' },
85
+ { name: 'unread', short: 'u', description: 'Only unread messages', type: 'boolean' },
86
+ { name: 'since', description: 'Only messages after this ISO timestamp', type: 'string' },
87
+ { name: 'mark-read', description: 'Mark all shown messages as read', type: 'boolean' },
88
+ { name: 'hook', description: 'Emit unread messages as UserPromptSubmit additionalContext JSON, then mark them read (for hook delivery)', type: 'boolean' },
89
+ { name: 'json', description: 'Machine-readable output', type: 'boolean' },
90
+ ],
91
+ examples: [
92
+ { command: 'swarmdo comms inbox', description: 'Your unread + read messages' },
93
+ { command: 'swarmdo comms inbox -u --mark-read', description: 'Show unread, then mark them read' },
94
+ ],
95
+ action: async (ctx) => {
96
+ const cwd = ctx.cwd || process.cwd();
97
+ const self = resolveSelf(ctx.flags.to);
98
+ // --hook: delivery path. Emit unread mail as UserPromptSubmit additionalContext
99
+ // and mark it read so it surfaces once, without polling. Hook-safe: NEVER
100
+ // errors (that would break the prompt) and stays silent when there's no mail.
101
+ if (ctx.flags.hook === true) {
102
+ try {
103
+ if (process.env.SWARMDO_COMMS_DISABLE === '1')
104
+ return { success: true };
105
+ const box = loadMailbox(cwd);
106
+ const unread = filterInbox(box, { to: self, unreadOnly: true });
107
+ if (unread.length > 0) {
108
+ const block = renderInboxContext(unread);
109
+ process.stdout.write(JSON.stringify({
110
+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: block },
111
+ }) + '\n');
112
+ const { box: next, marked } = markRead(box, unread.map((m) => m.id));
113
+ if (marked > 0)
114
+ saveMailbox(cwd, next);
115
+ }
116
+ }
117
+ catch {
118
+ /* never fail a hook */
119
+ }
120
+ return { success: true };
121
+ }
122
+ const box = loadMailbox(cwd);
123
+ const inbox = filterInbox(box, {
124
+ to: self,
125
+ from: ctx.flags.from,
126
+ unreadOnly: ctx.flags.unread === true,
127
+ since: ctx.flags.since,
128
+ });
129
+ const now = Date.now();
130
+ if (ctx.flags.json === true) {
131
+ output.printJson({ to: self, unread: unreadCount(box, self), messages: inbox });
132
+ }
133
+ else if (inbox.length === 0) {
134
+ output.printInfo(`No messages for ${output.highlight(self)}.`);
135
+ }
136
+ else {
137
+ output.writeln(output.bold(`Inbox: ${self}`) + output.dim(` (${unreadCount(box, self)} unread)`));
138
+ output.printTable({
139
+ columns: [
140
+ { key: 'flag', header: '', width: 2 },
141
+ { key: 'id', header: 'ID', width: 14 },
142
+ { key: 'from', header: 'From', width: 16 },
143
+ { key: 'subject', header: 'Subject', width: 34 },
144
+ { key: 'age', header: 'Age', width: 6, align: 'right' },
145
+ ],
146
+ data: inbox.map((m) => ({ flag: m.read ? ' ' : '●', id: m.id, from: m.from, subject: m.subject, age: relAge(m.createdAt, now) })),
147
+ });
148
+ output.writeln(output.dim('● = unread · read a message: swarmdo comms read <id>'));
149
+ }
150
+ if (ctx.flags['mark-read'] === true && inbox.length > 0) {
151
+ const { box: next, marked } = markRead(box, inbox.map((m) => m.id));
152
+ if (marked > 0)
153
+ saveMailbox(cwd, next);
154
+ if (ctx.flags.json !== true)
155
+ output.printInfo(`Marked ${marked} read.`);
156
+ }
157
+ return { success: true, data: { to: self, count: inbox.length } };
158
+ },
159
+ };
160
+ const readCommand = {
161
+ name: 'read',
162
+ description: 'Show a full message and mark it read',
163
+ options: [{ name: 'json', description: 'Machine-readable output', type: 'boolean' }],
164
+ examples: [{ command: 'swarmdo comms read msg_ab12cd34ef56', description: 'Open a message by id' }],
165
+ action: async (ctx) => {
166
+ const cwd = ctx.cwd || process.cwd();
167
+ const id = (ctx.args[0] || '').trim();
168
+ if (!id) {
169
+ output.printError('Message id required: swarmdo comms read <id>');
170
+ return { success: false, exitCode: 1 };
171
+ }
172
+ const box = loadMailbox(cwd);
173
+ const m = box.messages[id];
174
+ if (!m) {
175
+ output.printError(`No message with id ${id}.`);
176
+ return { success: false, exitCode: 1 };
177
+ }
178
+ const { box: next, marked } = markRead(box, [id]);
179
+ if (marked > 0)
180
+ saveMailbox(cwd, next);
181
+ if (ctx.flags.json === true) {
182
+ output.printJson({ ...m, read: true });
183
+ return { success: true, data: { id } };
184
+ }
185
+ output.writeln();
186
+ output.printList([
187
+ `${output.bold('From')}: ${m.from}`,
188
+ `${output.bold('To')}: ${m.to}`,
189
+ `${output.bold('Subject')}: ${m.subject}`,
190
+ `${output.bold('Sent')}: ${m.createdAt}`,
191
+ ]);
192
+ output.writeln();
193
+ output.writeln(m.body);
194
+ output.writeln();
195
+ return { success: true, data: { id } };
196
+ },
197
+ };
198
+ const watchCommand = {
199
+ name: 'watch',
200
+ description: 'Live-tail your inbox — print new messages as they arrive (Ctrl-C to stop)',
201
+ options: [
202
+ { name: 'to', short: 't', description: 'Whose inbox (default: this session)', type: 'string' },
203
+ { name: 'from', short: 'f', description: 'Only messages from this sender', type: 'string' },
204
+ { name: 'interval', short: 'i', description: 'Poll interval in seconds', type: 'number', default: 5 },
205
+ ],
206
+ examples: [
207
+ { command: 'swarmdo comms watch', description: 'Live-tail your inbox' },
208
+ { command: 'swarmdo comms watch -i 2', description: 'Poll every 2 seconds' },
209
+ ],
210
+ action: async (ctx) => {
211
+ const cwd = ctx.cwd || process.cwd();
212
+ const self = resolveSelf(ctx.flags.to);
213
+ const from = ctx.flags.from;
214
+ const intervalMs = Math.max(1, Number(ctx.flags.interval ?? 5)) * 1000;
215
+ // Only surface messages that arrive AFTER watch starts.
216
+ let cursor = new Date().toISOString();
217
+ output.printInfo(`Watching ${output.highlight(self)}'s inbox every ${intervalMs / 1000}s (Ctrl-C to stop)…`);
218
+ return await new Promise((resolve) => {
219
+ const tick = () => {
220
+ try {
221
+ const box = loadMailbox(cwd);
222
+ // filterInbox is newest-first; reverse to print in arrival order.
223
+ const fresh = filterInbox(box, { to: self, from, since: cursor }).reverse();
224
+ for (const m of fresh) {
225
+ output.writeln(`${output.dim(new Date(m.createdAt).toLocaleTimeString())} ${output.bold(m.from)} › ${m.subject}: ${m.body.replace(/\s+/g, ' ').trim()}`);
226
+ if (m.createdAt > cursor)
227
+ cursor = m.createdAt;
228
+ }
229
+ }
230
+ catch {
231
+ /* keep watching despite a transient read error */
232
+ }
233
+ };
234
+ const timer = setInterval(tick, intervalMs);
235
+ const stop = () => {
236
+ clearInterval(timer);
237
+ output.writeln();
238
+ output.printInfo('Stopped watching.');
239
+ resolve({ success: true });
240
+ };
241
+ process.once('SIGINT', stop);
242
+ process.once('SIGTERM', stop);
243
+ });
244
+ },
245
+ };
246
+ export const commsCommand = {
247
+ name: 'comms',
248
+ aliases: ['mailbox'],
249
+ description: 'Cross-session agent mailbox — message another session by name',
250
+ subcommands: [sendCommand, inboxCommand, readCommand, watchCommand],
251
+ options: [],
252
+ examples: [
253
+ { command: 'swarmdo comms send -t reviewer -m "PR ready"', description: 'Send a message' },
254
+ { command: 'swarmdo comms inbox -u', description: 'List your unread messages' },
255
+ { command: 'swarmdo comms read <id>', description: 'Open a message' },
256
+ ],
257
+ action: async (ctx) => {
258
+ const cwd = ctx.cwd || process.cwd();
259
+ const self = resolveSelf();
260
+ const box = loadMailbox(cwd);
261
+ const unread = unreadCount(box, self);
262
+ output.writeln();
263
+ output.writeln(output.bold('Cross-session agent mailbox'));
264
+ output.writeln();
265
+ output.writeln(`You are ${output.highlight(self)} — ${unread} unread message${unread === 1 ? '' : 's'}.`);
266
+ output.writeln();
267
+ output.writeln('Usage: swarmdo comms <send|inbox|read|watch> [options]');
268
+ output.printList([
269
+ `${output.highlight('send')} - Send a message (\`-t <session> -m "…"\`, \`-t all\` to broadcast)`,
270
+ `${output.highlight('inbox')} - List your messages (\`-u\` unread, \`--mark-read\`, \`--hook\`)`,
271
+ `${output.highlight('read')} - Show a full message by id and mark it read`,
272
+ `${output.highlight('watch')} - Live-tail your inbox for new messages`,
273
+ ]);
274
+ output.writeln();
275
+ output.writeln(output.dim('Sessions on the same repo share .swarmdo/comms/. Name yourself with $SWARMDO_SESSION.'));
276
+ return { success: true, data: { self, unread } };
277
+ },
278
+ };
279
+ export default commsCommand;
280
+ //# sourceMappingURL=comms.js.map
@@ -34,8 +34,12 @@ function optsFromFlags(ctx) {
34
34
  };
35
35
  if (typeof ctx.flags['min-run'] === 'number')
36
36
  opts.minRun = ctx.flags['min-run'];
37
- else if (typeof ctx.flags['min-run'] === 'string')
38
- opts.minRun = parseInt(ctx.flags['min-run'], 10) || 3;
37
+ else if (typeof ctx.flags['min-run'] === 'string') {
38
+ // Explicit NaN check, NOT `|| 3` — the documented `--min-run 0` ("disables")
39
+ // is falsy and would otherwise be silently coerced back to the default 3.
40
+ const parsed = parseInt(ctx.flags['min-run'], 10);
41
+ opts.minRun = Number.isNaN(parsed) ? 3 : parsed;
42
+ }
39
43
  const win = ctx.flags.window;
40
44
  if (typeof win === 'string' && /^\d+:\d+$/.test(win)) {
41
45
  const [head, tail] = win.split(':').map((n) => parseInt(n, 10));
@@ -438,6 +438,7 @@ const lintCommand = {
438
438
  settingsFiles: [read('.claude/settings.json'), read('.claude/settings.local.json')],
439
439
  mcpConfig: read('.mcp.json'),
440
440
  commandsRoot: list('.claude/commands'),
441
+ sdoCommands: list('.claude/commands/sDo'),
441
442
  skills: list('.claude/skills'),
442
443
  });
443
444
  const failed = report.errors > 0 || (ctx.flags.strict === true && report.warnings > 0);
@@ -8,6 +8,8 @@ import { callMCPTool, MCPClientError } from '../mcp-client.js';
8
8
  import { storeCommand } from './transfer-store.js';
9
9
  import { existsSync, readFileSync, statSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
+ import { bridgeSearchEntries } from '../memory/memory-bridge.js';
12
+ import { selectInjectionMemories, mapSearchResultsToCandidates, extractPromptFromPayload, } from '../memory-inject/select.js';
11
13
  /**
12
14
  * #1686 — `?? 0` only defaults null/undefined; NaN slips through and
13
15
  * surfaces as `"NaN"` (or earlier crashed `.toFixed`) in the metrics
@@ -731,6 +733,122 @@ const routeCommand = {
731
733
  }
732
734
  }
733
735
  };
736
+ /**
737
+ * Read a Claude Code hook payload from stdin (JSON on UserPromptSubmit), with a
738
+ * short timeout so it never hangs when stdin isn't closed (a known Windows
739
+ * failure mode). Returns '' for a TTY or on any error — the caller then simply
740
+ * injects nothing.
741
+ */
742
+ async function readHookStdin(timeoutMs = 1500) {
743
+ if (process.stdin.isTTY)
744
+ return '';
745
+ return new Promise((resolve) => {
746
+ let data = '';
747
+ const finish = (v) => {
748
+ try {
749
+ process.stdin.pause();
750
+ }
751
+ catch { /* noop */ }
752
+ resolve(v);
753
+ };
754
+ const timer = setTimeout(() => finish(data), timeoutMs);
755
+ try {
756
+ process.stdin.setEncoding('utf8');
757
+ process.stdin.on('data', (c) => { data += c; });
758
+ process.stdin.on('end', () => { clearTimeout(timer); finish(data); });
759
+ process.stdin.on('error', () => { clearTimeout(timer); finish(data); });
760
+ process.stdin.resume();
761
+ }
762
+ catch {
763
+ clearTimeout(timer);
764
+ finish('');
765
+ }
766
+ });
767
+ }
768
+ const DEFAULT_INJECT_NAMESPACES = ['claude-memories', 'auto-memory', 'patterns'];
769
+ /**
770
+ * memory-inject — the #43 prompt-time semantic memory injection hook. Embeds the
771
+ * submitted prompt, vector-searches AgentDB across the memory namespaces, and
772
+ * emits the most relevant stored memories as UserPromptSubmit `additionalContext`
773
+ * under a strict token budget. Wired for the hook path: it NEVER errors out
774
+ * (that would break the user's prompt) and stays silent when nothing is relevant.
775
+ */
776
+ const memoryInjectCommand = {
777
+ name: 'memory-inject',
778
+ description: 'Inject relevant stored memories into the prompt (UserPromptSubmit hook)',
779
+ options: [
780
+ { name: 'prompt', short: 'p', description: 'Prompt text (else read the hook JSON from stdin)', type: 'string' },
781
+ { name: 'namespaces', description: 'Comma-separated namespaces to search', type: 'string', default: DEFAULT_INJECT_NAMESPACES.join(',') },
782
+ { name: 'budget', description: 'Max tokens of injected context', type: 'number', default: 800 },
783
+ { name: 'min-relevance', description: 'Minimum relevance score (0..1)', type: 'number', default: 0.35 },
784
+ { name: 'top-k', short: 'K', description: 'Max memories to inject', type: 'number', default: 5 },
785
+ { name: 'limit', description: 'Max search results to rank', type: 'number', default: 20 },
786
+ { name: 'preview', description: 'Print the block for humans instead of emitting hook JSON', type: 'boolean' },
787
+ { name: 'db', description: 'Path to the memory database', type: 'string' },
788
+ ],
789
+ examples: [
790
+ { command: 'swarmdo hooks memory-inject -p "refactor the auth module" --preview', description: 'Preview relevant memories for a prompt' },
791
+ { command: "echo '{\"prompt\":\"fix bug\"}' | swarmdo hooks memory-inject", description: 'Run as a UserPromptSubmit hook' },
792
+ ],
793
+ action: async (ctx) => {
794
+ const preview = Boolean(ctx.flags.preview);
795
+ // A hook must NEVER break the user's prompt: swallow every error and emit nothing.
796
+ try {
797
+ if (process.env.SWARMDO_MEMORY_INJECT_DISABLE === '1')
798
+ return { success: true };
799
+ let prompt = (ctx.flags.prompt || ctx.args.join(' ')).trim();
800
+ if (!prompt)
801
+ prompt = extractPromptFromPayload(await readHookStdin());
802
+ if (!prompt)
803
+ return { success: true }; // nothing to search on
804
+ const namespaces = String(ctx.flags.namespaces ?? DEFAULT_INJECT_NAMESPACES.join(','))
805
+ .split(',').map((s) => s.trim()).filter(Boolean);
806
+ const minRelevance = ctx.flags.minRelevance ?? 0.35;
807
+ const topK = ctx.flags.topK ?? 5;
808
+ const limit = Math.max(ctx.flags.limit ?? 20, topK);
809
+ const search = await bridgeSearchEntries({
810
+ query: prompt,
811
+ namespace: 'all',
812
+ limit,
813
+ threshold: minRelevance,
814
+ fullContent: true,
815
+ dbPath: ctx.flags.db,
816
+ });
817
+ const rows = (search && search.success ? search.results : []);
818
+ const candidates = mapSearchResultsToCandidates(rows, namespaces);
819
+ const result = selectInjectionMemories(candidates, {
820
+ budgetTokens: ctx.flags.budget ?? 800,
821
+ minRelevance,
822
+ topK,
823
+ });
824
+ if (preview) {
825
+ if (!result.block) {
826
+ output.printInfo('No relevant memories to inject.');
827
+ }
828
+ else {
829
+ output.writeln(result.block);
830
+ output.writeln();
831
+ output.printInfo(`Injected ${result.used.length} memor${result.used.length === 1 ? 'y' : 'ies'} ` +
832
+ `(~${result.tokensUsed} tokens${result.skipped ? `, ${result.skipped} skipped` : ''}).`);
833
+ }
834
+ return { success: true, data: { used: result.used.length, skipped: result.skipped, tokensUsed: result.tokensUsed } };
835
+ }
836
+ // Hook mode: emit additionalContext JSON ONLY when there's something to add,
837
+ // so an irrelevant prompt leaves Claude's context untouched.
838
+ if (result.block) {
839
+ process.stdout.write(JSON.stringify({
840
+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: result.block },
841
+ }) + '\n');
842
+ }
843
+ return { success: true };
844
+ }
845
+ catch (err) {
846
+ if (preview)
847
+ output.printError(`Memory injection failed: ${String(err)}`);
848
+ return { success: true }; // never fail the hook
849
+ }
850
+ },
851
+ };
734
852
  // Explain subcommand
735
853
  const explainCommand = {
736
854
  name: 'explain',
@@ -4447,7 +4565,7 @@ const notifyCommand = {
4447
4565
  const recipeCommand = {
4448
4566
  name: 'recipe',
4449
4567
  aliases: ['recipes'],
4450
- description: 'Install ready-made Claude Code hook recipes (desktop notify) into settings — dry-run unless --apply',
4568
+ description: 'Install ready-made Claude Code hook recipes (desktop notify, memory injection) into settings — dry-run unless --apply',
4451
4569
  options: [
4452
4570
  { name: 'apply', type: 'boolean', description: 'write the change (default: preview only)', default: false },
4453
4571
  { name: 'shared', type: 'boolean', description: 'target .claude/settings.json (shared/committed) instead of settings.local.json', default: false },
@@ -4553,6 +4671,7 @@ export const hooksCommand = {
4553
4671
  sessionEndCommand,
4554
4672
  sessionRestoreCommand,
4555
4673
  routeCommand,
4674
+ memoryInjectCommand,
4556
4675
  explainCommand,
4557
4676
  pretrainCommand,
4558
4677
  buildAgentsCommand,
@@ -4610,6 +4729,7 @@ export const hooksCommand = {
4610
4729
  `${output.highlight('session-end')} - End current session and persist state`,
4611
4730
  `${output.highlight('session-restore')} - Restore a previous session`,
4612
4731
  `${output.highlight('route')} - Route tasks to optimal agents`,
4732
+ `${output.highlight('memory-inject')} - Inject relevant memories into the prompt (UserPromptSubmit)`,
4613
4733
  `${output.highlight('explain')} - Explain routing decisions`,
4614
4734
  `${output.highlight('pretrain')} - Bootstrap intelligence from repository`,
4615
4735
  `${output.highlight('build-agents')} - Generate optimized agent configs`,
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { execFileSync } from 'node:child_process';
15
15
  import { output } from '../output.js';
16
- import { parseGitLog, computeHotspots, formatHotspots } from '../hotspots/hotspots.js';
16
+ import { parseGitLog, computeHotspots, formatHotspots, hotspotsToCsv } from '../hotspots/hotspots.js';
17
17
  const SORT_KEYS = ['risk', 'churn', 'commits', 'authors'];
18
18
  /** Read a numeric flag that the parser may deliver as a number OR a string. */
19
19
  function numFlag(v, def) {
@@ -33,6 +33,7 @@ async function run(ctx) {
33
33
  }
34
34
  // Global --format (text|json|table); text and table both render the table.
35
35
  const asJson = ctx.flags.format === 'json';
36
+ const asCsv = ctx.flags.csv === true; // dedicated flag (global --format has no csv choice)
36
37
  // Capture history: SOH-delimited header + numstat, no merges. `%aN` (not
37
38
  // `%an`) resolves author names through `.mailmap`, so name/email variants of
38
39
  // the same person fold into one identity — otherwise the author-spread factor
@@ -50,7 +51,10 @@ async function run(ctx) {
50
51
  }
51
52
  const now = Date.now();
52
53
  const spots = computeHotspots(parseGitLog(raw), now, { by, top: top > 0 ? top : undefined, minCommits });
53
- if (asJson) {
54
+ if (asCsv) {
55
+ process.stdout.write(hotspotsToCsv(spots) + '\n');
56
+ }
57
+ else if (asJson) {
54
58
  process.stdout.write(JSON.stringify({ generated: new Date(now).toISOString(), by, count: spots.length, hotspots: spots }, null, 2) + '\n');
55
59
  }
56
60
  else {
@@ -72,10 +76,12 @@ export const hotspotsCommand = {
72
76
  { name: 'top', description: 'keep only the top N files (default 20; 0 = all)', type: 'string' },
73
77
  { name: 'min-commits', description: 'drop files with fewer than N commits (default 1)', type: 'string' },
74
78
  { name: 'by', description: `sort key: ${SORT_KEYS.join('|')} (default risk)`, type: 'string' },
79
+ { name: 'csv', description: 'export the ranking as CSV (for spreadsheets)', type: 'boolean' },
75
80
  ],
76
81
  examples: [
77
82
  { command: 'swarmdo hotspots src --since 90d', description: 'Risk hotspots under src/ in the last 90 days' },
78
83
  { command: 'swarmdo hotspots --by churn --top 10 --format json', description: 'Top-10 by churn as JSON' },
84
+ { command: 'swarmdo hotspots --csv > hotspots.csv', description: 'Export the risk ranking to CSV' },
79
85
  ],
80
86
  action: run,
81
87
  };
@@ -100,6 +100,12 @@ const commandLoaders = {
100
100
  // Isolated git worktrees for parallel work (claude-squad/vibe-kanban demand)
101
101
  worktree: () => import('./worktree.js'),
102
102
  wt: () => import('./worktree.js'), // alias via loader key
103
+ // Cross-session agent mailbox (multiplayer swarms — dorsa_rohani demand)
104
+ comms: () => import('./comms.js'),
105
+ mailbox: () => import('./comms.js'), // alias via loader key
106
+ // Static audit of Claude Code permission rules (conflicts / dead / over-broad)
107
+ permissions: () => import('./permissions.js'),
108
+ perms: () => import('./permissions.js'), // alias via loader key
103
109
  // Export Claude Code sessions to Markdown (conversation-exporter demand)
104
110
  transcript: () => import('./transcript.js'),
105
111
  tx: () => import('./transcript.js'), // alias via loader key
@@ -0,0 +1,14 @@
1
+ /**
2
+ * permissions.ts — `swarmdo permissions` (alias `perms`) — audit Claude Code
3
+ * permission rules for conflicts, dead/shadowed rules, and over-broad grants.
4
+ *
5
+ * Complements `config lint` (which validates the hooks block): this reads the
6
+ * `permissions.allow` / `deny` / `ask` arrays from `.claude/settings.json` +
7
+ * `settings.local.json` (+ `~/.claude/settings.json` with `--global`), merges
8
+ * them into the effective ruleset Claude Code enforces, and runs the pure
9
+ * permissions/audit.ts analyzer. Read-only — never edits settings.
10
+ */
11
+ import type { Command } from '../types.js';
12
+ export declare const permissionsCommand: Command;
13
+ export default permissionsCommand;
14
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * permissions.ts — `swarmdo permissions` (alias `perms`) — audit Claude Code
3
+ * permission rules for conflicts, dead/shadowed rules, and over-broad grants.
4
+ *
5
+ * Complements `config lint` (which validates the hooks block): this reads the
6
+ * `permissions.allow` / `deny` / `ask` arrays from `.claude/settings.json` +
7
+ * `settings.local.json` (+ `~/.claude/settings.json` with `--global`), merges
8
+ * them into the effective ruleset Claude Code enforces, and runs the pure
9
+ * permissions/audit.ts analyzer. Read-only — never edits settings.
10
+ */
11
+ import { existsSync, readFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+ import { output } from '../output.js';
15
+ import { auditPermissions } from '../permissions/audit.js';
16
+ const strArray = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : []);
17
+ /** Extract permissions.allow/deny/ask from a settings file, or null if none. */
18
+ function readPerms(file) {
19
+ try {
20
+ if (!existsSync(file))
21
+ return null;
22
+ const obj = JSON.parse(readFileSync(file, 'utf8'));
23
+ const perm = obj?.permissions;
24
+ if (!perm || typeof perm !== 'object')
25
+ return null;
26
+ const sets = { allow: strArray(perm.allow), deny: strArray(perm.deny), ask: strArray(perm.ask) };
27
+ return sets.allow.length + sets.deny.length + sets.ask.length > 0 ? sets : null;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ export const permissionsCommand = {
34
+ name: 'permissions',
35
+ aliases: ['perms'],
36
+ description: 'Audit Claude Code permission rules (allow/deny/ask) for conflicts, dead/shadowed rules, and over-broad grants',
37
+ options: [
38
+ { name: 'global', description: 'Also include ~/.claude/settings.json', type: 'boolean' },
39
+ { name: 'strict', description: 'Exit 1 if any error-severity finding (CI gate)', type: 'boolean' },
40
+ { name: 'json', description: 'Machine-readable output', type: 'boolean' },
41
+ ],
42
+ examples: [
43
+ { command: 'swarmdo permissions', description: "Audit this project's permission rules" },
44
+ { command: 'swarmdo permissions --global --strict', description: 'Include user-global settings; fail on conflicts' },
45
+ ],
46
+ action: async (ctx) => {
47
+ const cwd = ctx.cwd || process.cwd();
48
+ const files = [join(cwd, '.claude', 'settings.json'), join(cwd, '.claude', 'settings.local.json')];
49
+ if (ctx.flags.global === true)
50
+ files.push(join(homedir(), '.claude', 'settings.json'));
51
+ const used = [];
52
+ const merged = { allow: [], deny: [], ask: [] };
53
+ for (const file of files) {
54
+ const p = readPerms(file);
55
+ if (!p)
56
+ continue;
57
+ used.push(file);
58
+ merged.allow.push(...(p.allow ?? []));
59
+ merged.deny.push(...(p.deny ?? []));
60
+ merged.ask.push(...(p.ask ?? []));
61
+ }
62
+ const totalRules = merged.allow.length + merged.deny.length + merged.ask.length;
63
+ if (used.length === 0) {
64
+ if (ctx.flags.json === true)
65
+ output.printJson({ files: [], rules: 0, findings: [] });
66
+ else
67
+ output.printInfo('No permission rules found in .claude/settings*.json.');
68
+ return { success: true };
69
+ }
70
+ const findings = auditPermissions(merged);
71
+ const errors = findings.filter((f) => f.severity === 'error').length;
72
+ const warns = findings.filter((f) => f.severity === 'warn').length;
73
+ if (ctx.flags.json === true) {
74
+ output.printJson({ files: used, rules: totalRules, findings });
75
+ }
76
+ else if (findings.length === 0) {
77
+ output.printSuccess(`permissions audit: ${totalRules} rule(s) across ${used.length} file(s) — no issues`);
78
+ }
79
+ else {
80
+ const icon = (s) => (s === 'error' ? '✖' : s === 'warn' ? '⚠' : '·');
81
+ output.writeln(output.bold('Permission audit') + output.dim(` (${totalRules} rules, ${used.length} file${used.length === 1 ? '' : 's'})`));
82
+ for (const x of findings)
83
+ output.writeln(` ${icon(x.severity)} ${output.dim(`[${x.rule}]`)} ${x.message}`);
84
+ output.writeln('');
85
+ output.writeln(`${errors} error${errors === 1 ? '' : 's'}, ${warns} warning${warns === 1 ? '' : 's'}, ${findings.length - errors - warns} info`);
86
+ }
87
+ const failed = ctx.flags.strict === true && errors > 0;
88
+ return { success: !failed, exitCode: failed ? 1 : 0, data: { rules: totalRules, findings } };
89
+ },
90
+ };
91
+ export default permissionsCommand;
92
+ //# sourceMappingURL=permissions.js.map