swarmdo 1.30.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/package.json +1 -1
- package/v3/@swarmdo/cli/dist/src/affected/affected.d.ts +6 -1
- package/v3/@swarmdo/cli/dist/src/affected/affected.js +7 -2
- package/v3/@swarmdo/cli/dist/src/apply/apply.js +18 -7
- package/v3/@swarmdo/cli/dist/src/changelog/changelog.js +4 -1
- package/v3/@swarmdo/cli/dist/src/codegraph/codegraph.js +11 -1
- package/v3/@swarmdo/cli/dist/src/commands/comms.d.ts +16 -0
- package/v3/@swarmdo/cli/dist/src/commands/comms.js +280 -0
- package/v3/@swarmdo/cli/dist/src/commands/compact.js +6 -2
- package/v3/@swarmdo/cli/dist/src/commands/config.js +1 -0
- package/v3/@swarmdo/cli/dist/src/commands/hooks.js +121 -1
- package/v3/@swarmdo/cli/dist/src/commands/hotspots.js +8 -2
- package/v3/@swarmdo/cli/dist/src/commands/index.js +6 -0
- package/v3/@swarmdo/cli/dist/src/commands/permissions.d.ts +14 -0
- package/v3/@swarmdo/cli/dist/src/commands/permissions.js +92 -0
- package/v3/@swarmdo/cli/dist/src/commands/usage.js +34 -1
- package/v3/@swarmdo/cli/dist/src/comms/mailbox.d.ts +88 -0
- package/v3/@swarmdo/cli/dist/src/comms/mailbox.js +138 -0
- package/v3/@swarmdo/cli/dist/src/comms/store.d.ts +21 -0
- package/v3/@swarmdo/cli/dist/src/comms/store.js +44 -0
- package/v3/@swarmdo/cli/dist/src/compact/compact.js +4 -1
- package/v3/@swarmdo/cli/dist/src/compact-snapshot/compact-snapshot.js +8 -3
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.d.ts +3 -1
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.js +11 -4
- package/v3/@swarmdo/cli/dist/src/env/env.js +40 -14
- package/v3/@swarmdo/cli/dist/src/hooks-recipe/recipe.js +20 -1
- package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.d.ts +3 -0
- package/v3/@swarmdo/cli/dist/src/hotspots/hotspots.js +9 -0
- package/v3/@swarmdo/cli/dist/src/index.js +8 -0
- package/v3/@swarmdo/cli/dist/src/license/license.js +4 -1
- package/v3/@swarmdo/cli/dist/src/mcp-client.js +2 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.d.ts +11 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.js +93 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/task-tools.js +4 -0
- package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.d.ts +2 -0
- package/v3/@swarmdo/cli/dist/src/memory/memory-bridge.js +4 -2
- package/v3/@swarmdo/cli/dist/src/memory-inject/select.d.ts +82 -0
- package/v3/@swarmdo/cli/dist/src/memory-inject/select.js +164 -0
- package/v3/@swarmdo/cli/dist/src/memory-vault/import.js +11 -1
- package/v3/@swarmdo/cli/dist/src/pack/pack.js +6 -2
- package/v3/@swarmdo/cli/dist/src/permissions/audit.d.ts +50 -0
- package/v3/@swarmdo/cli/dist/src/permissions/audit.js +127 -0
- package/v3/@swarmdo/cli/dist/src/plugins/store/search.js +1 -1
- package/v3/@swarmdo/cli/dist/src/redact/redact.js +7 -2
- package/v3/@swarmdo/cli/dist/src/release/release.js +1 -1
- package/v3/@swarmdo/cli/dist/src/sbom/sbom.d.ts +2 -0
- package/v3/@swarmdo/cli/dist/src/sbom/sbom.js +5 -0
- package/v3/@swarmdo/cli/dist/src/swarmvector/semantic-router.js +8 -3
- package/v3/@swarmdo/cli/dist/src/testreport/testreport.js +9 -2
- package/v3/@swarmdo/cli/dist/src/transcript/export.js +8 -4
- package/v3/@swarmdo/cli/dist/src/update/checker.d.ts +1 -0
- package/v3/@swarmdo/cli/dist/src/update/checker.js +7 -2
- package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.d.ts +29 -0
- package/v3/@swarmdo/cli/dist/src/usage/model-efficiency.js +28 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect-html.js +1 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect.d.ts +26 -0
- package/v3/@swarmdo/cli/dist/src/usage/reflect.js +44 -0
- package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.d.ts +8 -1
- package/v3/@swarmdo/cli/dist/src/usage/transcript-usage.js +21 -1
- package/v3/@swarmdo/cli/dist/src/util/stdout.d.ts +23 -1
- package/v3/@swarmdo/cli/dist/src/util/stdout.js +34 -2
- package/v3/@swarmdo/cli/package.json +1 -1
|
@@ -36,6 +36,26 @@ const VITE_BUILTINS = new Set(['MODE', 'BASE_URL', 'PROD', 'DEV', 'SSR']);
|
|
|
36
36
|
export function extractEnvRefs(source, file) {
|
|
37
37
|
const out = [];
|
|
38
38
|
const lines = source.split('\n');
|
|
39
|
+
// Map a byte offset in `source` to its 1-based line, for the whole-source
|
|
40
|
+
// destructure scan below (which can span lines). lineStart[i] = the offset
|
|
41
|
+
// where line i+1 begins; binary-search for the last start <= offset.
|
|
42
|
+
const lineStart = [0];
|
|
43
|
+
for (let k = 0; k < source.length; k++)
|
|
44
|
+
if (source[k] === '\n')
|
|
45
|
+
lineStart.push(k + 1);
|
|
46
|
+
const lineAt = (index) => {
|
|
47
|
+
let lo = 0, hi = lineStart.length - 1;
|
|
48
|
+
while (lo < hi) {
|
|
49
|
+
const mid = (lo + hi + 1) >> 1;
|
|
50
|
+
if (lineStart[mid] <= index)
|
|
51
|
+
lo = mid;
|
|
52
|
+
else
|
|
53
|
+
hi = mid - 1;
|
|
54
|
+
}
|
|
55
|
+
return lo + 1;
|
|
56
|
+
};
|
|
57
|
+
// Member-style reads (process.env.X, os.getenv('X'), …) are inherently
|
|
58
|
+
// single-line — scan line by line so each ref carries its own line number.
|
|
39
59
|
for (let i = 0; i < lines.length; i++) {
|
|
40
60
|
const line = lines[i];
|
|
41
61
|
if (!line.includes('env') && !line.includes('getenv'))
|
|
@@ -50,21 +70,27 @@ export function extractEnvRefs(source, file) {
|
|
|
50
70
|
out.push({ key, file, line: i + 1 });
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
}
|
|
74
|
+
// Destructuring reads: `const { PORT, DB_URL } = process.env`. Scanned over the
|
|
75
|
+
// WHOLE source, not per line — a Prettier-wrapped list spans lines:
|
|
76
|
+
// const {\n PORT,\n DB_URL,\n} = process.env
|
|
77
|
+
// The regex's `\s*`/`[^}]+` already match newlines, so running it per line was
|
|
78
|
+
// exactly what hid multi-line destructures (a false-clean `env --ci`). The
|
|
79
|
+
// `(?![.[])` guard keeps it to the bare object, not `= process.env.FOO`; each
|
|
80
|
+
// property key maps back to its own line via its offset.
|
|
81
|
+
DESTRUCTURE_RE.lastIndex = 0;
|
|
82
|
+
let dm;
|
|
83
|
+
while ((dm = DESTRUCTURE_RE.exec(source)) !== null) {
|
|
84
|
+
const bodyStart = dm.index + dm[0].indexOf(dm[1]);
|
|
85
|
+
let partOffset = 0;
|
|
86
|
+
for (const part of dm[1].split(',')) {
|
|
87
|
+
// The env-var name is the property key — left of a rename `:` or default `=`.
|
|
88
|
+
const key = part.trim().split(/[:=]/)[0].trim();
|
|
89
|
+
if (/^[A-Za-z_$][\w$]*$/.test(key) && !VITE_BUILTINS.has(key)) { // skip `...rest`, empties, builtins
|
|
90
|
+
const kOff = part.indexOf(key);
|
|
91
|
+
out.push({ key, file, line: lineAt(bodyStart + partOffset + (kOff < 0 ? 0 : kOff)) });
|
|
67
92
|
}
|
|
93
|
+
partOffset += part.length + 1; // + the comma consumed by split
|
|
68
94
|
}
|
|
69
95
|
}
|
|
70
96
|
return out;
|
|
@@ -23,6 +23,20 @@ export const RECIPES = [
|
|
|
23
23
|
title: 'Desktop ping when Claude needs input',
|
|
24
24
|
description: 'Fires a desktop notification when Claude is waiting for input or a permission decision (Notification hook).',
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
name: 'memory-inject',
|
|
28
|
+
event: 'UserPromptSubmit',
|
|
29
|
+
command: 'swarmdo hooks memory-inject',
|
|
30
|
+
title: 'Inject relevant memories into each prompt',
|
|
31
|
+
description: 'On every prompt, semantically searches your stored memories (claude-memories/auto-memory/patterns) and injects the most relevant ones as context under a token budget (UserPromptSubmit hook). Powered by `swarmdo hooks memory-inject`. Opt-in: adds a small per-prompt search.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'comms-inbox',
|
|
35
|
+
event: 'UserPromptSubmit',
|
|
36
|
+
command: 'swarmdo comms inbox --hook',
|
|
37
|
+
title: 'Surface new cross-session messages each prompt',
|
|
38
|
+
description: 'On every prompt, injects any unread messages other sessions sent to this one (multiplayer swarms) as context, then marks them read — so mail surfaces without polling (UserPromptSubmit hook). Powered by `swarmdo comms inbox --hook`. Opt-in.',
|
|
39
|
+
},
|
|
26
40
|
];
|
|
27
41
|
export function findRecipe(name) {
|
|
28
42
|
const n = (name ?? '').trim().toLowerCase();
|
|
@@ -37,7 +51,12 @@ export function hasRecipe(settings, r) {
|
|
|
37
51
|
const events = hooks?.[r.event];
|
|
38
52
|
if (!Array.isArray(events))
|
|
39
53
|
return false;
|
|
40
|
-
|
|
54
|
+
// Match on matcher too: the same command under a DIFFERENT matcher is a
|
|
55
|
+
// distinct binding, so a matcher-scoped recipe isn't "already installed" just
|
|
56
|
+
// because its command appears elsewhere (matcher-less recipes compare
|
|
57
|
+
// undefined === undefined, unchanged).
|
|
58
|
+
return events.some((e) => e && typeof e === 'object' && e.matcher === r.matcher &&
|
|
59
|
+
Array.isArray(e.hooks) &&
|
|
41
60
|
e.hooks.some((h) => h?.command === r.command));
|
|
42
61
|
}
|
|
43
62
|
/** Additively merge a recipe into a settings object. Pure: returns a new object,
|
|
@@ -64,6 +64,9 @@ export declare function resolveRenamePath(raw: string): string;
|
|
|
64
64
|
export declare function parseGitLog(raw: string): Commit[];
|
|
65
65
|
/** Aggregate commits into per-file hotspots, scored and ranked. Pure. */
|
|
66
66
|
export declare function computeHotspots(commits: Commit[], now: number, opts?: HotspotOptions): FileHotspot[];
|
|
67
|
+
/** Export the ranking as CSV for spreadsheets / tech-debt review. Epoch dates
|
|
68
|
+
* render as ISO `YYYY-MM-DD` (UTC). Pure. */
|
|
69
|
+
export declare function hotspotsToCsv(spots: FileHotspot[]): string;
|
|
67
70
|
/** Human-readable table. Pure. */
|
|
68
71
|
export declare function formatHotspots(spots: FileHotspot[], now: number): string;
|
|
69
72
|
//# sourceMappingURL=hotspots.d.ts.map
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* <SOH>%H<US>%an<US>%aI ← one header line per commit
|
|
16
16
|
* <added>\t<deleted>\t<path> ← one numstat line per file ('-' for binary)
|
|
17
17
|
*/
|
|
18
|
+
import { toCsv } from '../util/csv.js';
|
|
18
19
|
const SOH = '\x01'; // start-of-commit marker (git --format=format:%x01...)
|
|
19
20
|
const US = '\x1f'; // field separator (%x1f)
|
|
20
21
|
/**
|
|
@@ -134,6 +135,14 @@ export function computeHotspots(commits, now, opts = {}) {
|
|
|
134
135
|
out = out.slice(0, opts.top);
|
|
135
136
|
return out;
|
|
136
137
|
}
|
|
138
|
+
/** Export the ranking as CSV for spreadsheets / tech-debt review. Epoch dates
|
|
139
|
+
* render as ISO `YYYY-MM-DD` (UTC). Pure. */
|
|
140
|
+
export function hotspotsToCsv(spots) {
|
|
141
|
+
const isoDay = (ms) => (ms > 0 ? new Date(ms).toISOString().slice(0, 10) : '');
|
|
142
|
+
const headers = ['path', 'risk', 'commits', 'churn', 'authors', 'firstTouched', 'lastTouched'];
|
|
143
|
+
const rows = spots.map((s) => [s.path, s.risk, s.commits, s.churn, s.authors, isoDay(s.firstTouched), isoDay(s.lastTouched)]);
|
|
144
|
+
return toCsv(headers, rows);
|
|
145
|
+
}
|
|
137
146
|
/** Human-readable table. Pure. */
|
|
138
147
|
export function formatHotspots(spots, now) {
|
|
139
148
|
if (spots.length === 0)
|
|
@@ -20,6 +20,7 @@ import { commands, getCommandsByCategory, getCommand, getCommandAsync, getComman
|
|
|
20
20
|
import { suggestCommand } from './suggest.js';
|
|
21
21
|
import { runStartupUpdateCheck } from './update/index.js';
|
|
22
22
|
import { migrateStorageDir } from './storage-migration.js';
|
|
23
|
+
import { guardStreamEpipe } from './util/stdout.js';
|
|
23
24
|
// Read version from package.json at runtime
|
|
24
25
|
function getPackageVersion() {
|
|
25
26
|
try {
|
|
@@ -68,6 +69,13 @@ export class CLI {
|
|
|
68
69
|
*/
|
|
69
70
|
async run(args = process.argv.slice(2)) {
|
|
70
71
|
try {
|
|
72
|
+
// #78 — Arm the EPIPE guard on stdout/stderr before ANY write happens, so
|
|
73
|
+
// `swarmdo <cmd> | head` (reader closes the pipe early) terminates cleanly
|
|
74
|
+
// instead of crashing with an unhandled 'error' event + raw stack trace.
|
|
75
|
+
// Covers every write path (output.writeln, console, writeStdout), not just
|
|
76
|
+
// writeStdout callers. Idempotent per stream.
|
|
77
|
+
guardStreamEpipe(process.stdout);
|
|
78
|
+
guardStreamEpipe(process.stderr);
|
|
71
79
|
// One-time storage migration: rename a legacy `.claude-flow/` data dir
|
|
72
80
|
// to `.swarmdo/` so users upgrading across the clean-break rename keep
|
|
73
81
|
// their agent registry, swarm state, task store, and sessions. Cheap
|
|
@@ -39,7 +39,10 @@ export function classifyLicense(pkg) {
|
|
|
39
39
|
if (Array.isArray(arr) && arr.length) {
|
|
40
40
|
const types = arr
|
|
41
41
|
.map((e) => (typeof e === 'string' ? e : e?.type))
|
|
42
|
-
.filter((t) => typeof t === 'string' && !!t.trim())
|
|
42
|
+
.filter((t) => typeof t === 'string' && !!t.trim())
|
|
43
|
+
// Normalize each entry like the singular `license`/`{type}` branches above,
|
|
44
|
+
// so npm sentinels (`SEE LICENSE IN …`, `UNLICENSED`) become UNKNOWN here too.
|
|
45
|
+
.map(normalizeNpmLicense);
|
|
43
46
|
if (types.length)
|
|
44
47
|
return types.join(' OR ');
|
|
45
48
|
}
|
|
@@ -15,6 +15,7 @@ import { memoryTools } from './mcp-tools/memory-tools.js';
|
|
|
15
15
|
import { configTools } from './mcp-tools/config-tools.js';
|
|
16
16
|
import { hooksTools } from './mcp-tools/hooks-tools.js';
|
|
17
17
|
import { taskTools } from './mcp-tools/task-tools.js';
|
|
18
|
+
import { commsTools } from './mcp-tools/comms-tools.js';
|
|
18
19
|
import { sessionTools } from './mcp-tools/session-tools.js';
|
|
19
20
|
import { hiveMindTools } from './mcp-tools/hive-mind-tools.js';
|
|
20
21
|
import { workflowTools } from './mcp-tools/workflow-tools.js';
|
|
@@ -107,6 +108,7 @@ const TOOL_GROUPS = {
|
|
|
107
108
|
config: () => configTools,
|
|
108
109
|
hooks: () => hooksTools,
|
|
109
110
|
task: () => taskTools,
|
|
111
|
+
comms: () => commsTools,
|
|
110
112
|
session: () => sessionTools,
|
|
111
113
|
'hive-mind': () => hiveMindTools,
|
|
112
114
|
workflow: () => workflowTools,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comms MCP Tools — cross-session agent mailbox (#44).
|
|
3
|
+
*
|
|
4
|
+
* Thin wrappers over the pure comms/mailbox.ts engine + comms/store.ts fs
|
|
5
|
+
* persistence, so an in-Claude session can message another session (or read its
|
|
6
|
+
* own inbox) without shelling out. Same `.swarmdo/comms/store.json` the
|
|
7
|
+
* `swarmdo comms` CLI uses.
|
|
8
|
+
*/
|
|
9
|
+
import { type MCPTool } from './types.js';
|
|
10
|
+
export declare const commsTools: MCPTool[];
|
|
11
|
+
//# sourceMappingURL=comms-tools.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comms MCP Tools — cross-session agent mailbox (#44).
|
|
3
|
+
*
|
|
4
|
+
* Thin wrappers over the pure comms/mailbox.ts engine + comms/store.ts fs
|
|
5
|
+
* persistence, so an in-Claude session can message another session (or read its
|
|
6
|
+
* own inbox) without shelling out. Same `.swarmdo/comms/store.json` the
|
|
7
|
+
* `swarmdo comms` CLI uses.
|
|
8
|
+
*/
|
|
9
|
+
import { getProjectCwd } from './types.js';
|
|
10
|
+
import { createMessage, addMessage, filterInbox, markRead, pruneMailbox } from '../comms/mailbox.js';
|
|
11
|
+
import { loadMailbox, saveMailbox, resolveSelf, newMessageId, RETENTION } from '../comms/store.js';
|
|
12
|
+
export const commsTools = [
|
|
13
|
+
{
|
|
14
|
+
name: 'comms_send',
|
|
15
|
+
description: 'Send a message to another Claude Code session working on the same repo (cross-session mailbox). Address "all" to broadcast to every session. Use for multi-session/"multiplayer" coordination; delivery is via a shared file, not the network.',
|
|
16
|
+
category: 'comms',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
to: { type: 'string', description: 'Recipient session name, or "all" to broadcast' },
|
|
21
|
+
message: { type: 'string', description: 'Message body' },
|
|
22
|
+
subject: { type: 'string', description: 'Optional subject line' },
|
|
23
|
+
from: { type: 'string', description: 'Sender name (default: this session — $SWARMDO_SESSION or hostname)' },
|
|
24
|
+
},
|
|
25
|
+
required: ['to', 'message'],
|
|
26
|
+
},
|
|
27
|
+
handler: async (input) => {
|
|
28
|
+
const to = String(input.to ?? '').trim();
|
|
29
|
+
const body = String(input.message ?? '');
|
|
30
|
+
if (!to)
|
|
31
|
+
return { success: false, error: 'recipient (to) is required' };
|
|
32
|
+
if (!body.trim())
|
|
33
|
+
return { success: false, error: 'message is required' };
|
|
34
|
+
const cwd = getProjectCwd();
|
|
35
|
+
const from = resolveSelf(input.from);
|
|
36
|
+
const msg = createMessage({
|
|
37
|
+
id: newMessageId(),
|
|
38
|
+
from,
|
|
39
|
+
to,
|
|
40
|
+
subject: input.subject,
|
|
41
|
+
body,
|
|
42
|
+
createdAt: new Date().toISOString(),
|
|
43
|
+
});
|
|
44
|
+
let box = addMessage(loadMailbox(cwd), msg);
|
|
45
|
+
box = pruneMailbox(box, { ...RETENTION, nowMs: Date.now() });
|
|
46
|
+
saveMailbox(cwd, box);
|
|
47
|
+
return { success: true, id: msg.id, to: msg.to, from: msg.from, subject: msg.subject };
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'comms_inbox',
|
|
52
|
+
description: 'List messages other sessions have sent to this session (cross-session mailbox), newest first. Includes "all" broadcasts. Set markRead to acknowledge them.',
|
|
53
|
+
category: 'comms',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
to: { type: 'string', description: 'Whose inbox (default: this session)' },
|
|
58
|
+
unreadOnly: { type: 'boolean', description: 'Only unread messages' },
|
|
59
|
+
from: { type: 'string', description: 'Only messages from this sender' },
|
|
60
|
+
since: { type: 'string', description: 'Only messages after this ISO-8601 timestamp' },
|
|
61
|
+
markRead: { type: 'boolean', description: 'Mark the returned messages as read' },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
handler: async (input) => {
|
|
65
|
+
const cwd = getProjectCwd();
|
|
66
|
+
const self = resolveSelf(input.to);
|
|
67
|
+
let box = loadMailbox(cwd);
|
|
68
|
+
const messages = filterInbox(box, {
|
|
69
|
+
to: self,
|
|
70
|
+
unreadOnly: input.unreadOnly === true,
|
|
71
|
+
from: input.from,
|
|
72
|
+
since: input.since,
|
|
73
|
+
});
|
|
74
|
+
let marked = 0;
|
|
75
|
+
if (input.markRead === true && messages.length > 0) {
|
|
76
|
+
const res = markRead(box, messages.map((m) => m.id));
|
|
77
|
+
if (res.marked > 0)
|
|
78
|
+
saveMailbox(cwd, res.box);
|
|
79
|
+
box = res.box;
|
|
80
|
+
marked = res.marked;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
to: self,
|
|
85
|
+
count: messages.length,
|
|
86
|
+
unread: filterInbox(box, { to: self, unreadOnly: true }).length,
|
|
87
|
+
marked,
|
|
88
|
+
messages,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
//# sourceMappingURL=comms-tools.js.map
|
|
@@ -10,6 +10,7 @@ export { memoryTools } from './memory-tools.js';
|
|
|
10
10
|
export { configTools } from './config-tools.js';
|
|
11
11
|
export { hooksTools } from './hooks-tools.js';
|
|
12
12
|
export { taskTools } from './task-tools.js';
|
|
13
|
+
export { commsTools } from './comms-tools.js';
|
|
13
14
|
export { sessionTools } from './session-tools.js';
|
|
14
15
|
export { hiveMindTools } from './hive-mind-tools.js';
|
|
15
16
|
export { workflowTools } from './workflow-tools.js';
|
|
@@ -9,6 +9,7 @@ export { memoryTools } from './memory-tools.js';
|
|
|
9
9
|
export { configTools } from './config-tools.js';
|
|
10
10
|
export { hooksTools } from './hooks-tools.js';
|
|
11
11
|
export { taskTools } from './task-tools.js';
|
|
12
|
+
export { commsTools } from './comms-tools.js';
|
|
12
13
|
export { sessionTools } from './session-tools.js';
|
|
13
14
|
export { hiveMindTools } from './hive-mind-tools.js';
|
|
14
15
|
export { workflowTools } from './workflow-tools.js';
|
|
@@ -485,6 +485,10 @@ export const taskTools = [
|
|
|
485
485
|
progress: 0,
|
|
486
486
|
assignedTo: [...original.assignedTo],
|
|
487
487
|
tags: [...original.tags, 'retry-of:' + taskId],
|
|
488
|
+
// Carry the dependency gate — a retried task must stay blocked until its
|
|
489
|
+
// prerequisites complete, exactly like the original. Dropping it let the
|
|
490
|
+
// dispatcher run the retry against unfinished/failed deps (a half-built state).
|
|
491
|
+
...(original.dependsOn?.length ? { dependsOn: [...original.dependsOn] } : {}),
|
|
488
492
|
createdAt: new Date().toISOString(),
|
|
489
493
|
startedAt: null,
|
|
490
494
|
completedAt: null,
|
|
@@ -54,6 +54,8 @@ export declare function bridgeSearchEntries(options: {
|
|
|
54
54
|
limit?: number;
|
|
55
55
|
threshold?: number;
|
|
56
56
|
dbPath?: string;
|
|
57
|
+
/** Return full entry content instead of the 60-char preview (for memory injection). */
|
|
58
|
+
fullContent?: boolean;
|
|
57
59
|
}): Promise<{
|
|
58
60
|
success: boolean;
|
|
59
61
|
results: {
|
|
@@ -714,7 +714,7 @@ export async function bridgeSearchEntries(options) {
|
|
|
714
714
|
if (!ctx)
|
|
715
715
|
return null;
|
|
716
716
|
try {
|
|
717
|
-
const { query: queryStr, namespace, limit = 10, threshold = 0.3 } = options;
|
|
717
|
+
const { query: queryStr, namespace, limit = 10, threshold = 0.3, fullContent = false } = options;
|
|
718
718
|
const effectiveNamespace = namespace || 'all';
|
|
719
719
|
const startTime = Date.now();
|
|
720
720
|
// Generate query embedding
|
|
@@ -784,7 +784,9 @@ export async function bridgeSearchEntries(options) {
|
|
|
784
784
|
results.push({
|
|
785
785
|
id: String(row.id).substring(0, 12),
|
|
786
786
|
key: row.key || String(row.id).substring(0, 15),
|
|
787
|
-
content:
|
|
787
|
+
content: fullContent
|
|
788
|
+
? (row.content || '')
|
|
789
|
+
: (row.content || '').substring(0, 60) + ((row.content || '').length > 60 ? '...' : ''),
|
|
788
790
|
score,
|
|
789
791
|
namespace: row.namespace || 'default',
|
|
790
792
|
provenance,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-inject/select.ts — pure selection + formatting for prompt-time
|
|
3
|
+
* semantic memory injection (#43).
|
|
4
|
+
*
|
|
5
|
+
* A UserPromptSubmit hook embeds the current prompt, vector-searches AgentDB
|
|
6
|
+
* across the memory namespaces, and injects the most relevant stored memories
|
|
7
|
+
* into Claude's context under a strict token budget — recall at the moment of
|
|
8
|
+
* need instead of a manual `memory search` or a bulk session-start dump.
|
|
9
|
+
*
|
|
10
|
+
* This module is the DIFFERENTIATING logic and is deliberately pure (no I/O, no
|
|
11
|
+
* embeddings): given already-scored candidates it decides WHICH memories to
|
|
12
|
+
* inject (relevance threshold, cross-namespace dedup, token budget, top-k) and
|
|
13
|
+
* formats the context block. The command layer (`hooks memory-inject`) supplies
|
|
14
|
+
* the real embedding + vector search and emits the hook JSON.
|
|
15
|
+
*/
|
|
16
|
+
/** One candidate memory returned by the vector search. */
|
|
17
|
+
export interface InjectionCandidate {
|
|
18
|
+
key: string;
|
|
19
|
+
namespace: string;
|
|
20
|
+
content: string;
|
|
21
|
+
/** Relevance (higher = more relevant), from the vector search. */
|
|
22
|
+
score: number;
|
|
23
|
+
provenance?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface InjectionOptions {
|
|
26
|
+
/** Max tokens the whole injected block may occupy (estimate). Default 800. */
|
|
27
|
+
budgetTokens?: number;
|
|
28
|
+
/** Drop candidates scoring below this. Default 0.35. */
|
|
29
|
+
minRelevance?: number;
|
|
30
|
+
/** Never inject more than this many memories. Default 5. */
|
|
31
|
+
topK?: number;
|
|
32
|
+
/** Chars-per-token estimate for the budget. Default 4. */
|
|
33
|
+
charsPerToken?: number;
|
|
34
|
+
/** Trim each memory's content to this many chars before formatting. Default 400. */
|
|
35
|
+
maxItemChars?: number;
|
|
36
|
+
/** Heading for the injected block. */
|
|
37
|
+
header?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface InjectionResult {
|
|
40
|
+
/** The formatted context block, or '' when nothing qualifies. */
|
|
41
|
+
block: string;
|
|
42
|
+
/** Candidates actually included (content already trimmed), in injection order. */
|
|
43
|
+
used: InjectionCandidate[];
|
|
44
|
+
/** Qualifying, unique candidates left out by the token budget or top-k cap. */
|
|
45
|
+
skipped: number;
|
|
46
|
+
/** Estimated tokens the rendered block occupies. */
|
|
47
|
+
tokensUsed: number;
|
|
48
|
+
}
|
|
49
|
+
/** Rough token estimate: ceil(chars / charsPerToken). */
|
|
50
|
+
export declare function estimateTokens(text: string, charsPerToken?: number): number;
|
|
51
|
+
/** Collapse whitespace and cap length at a word boundary, adding an ellipsis. */
|
|
52
|
+
export declare function trimContent(content: string, maxChars: number): string;
|
|
53
|
+
/**
|
|
54
|
+
* Select and format the memories to inject.
|
|
55
|
+
*
|
|
56
|
+
* Pipeline: drop empty/below-threshold → sort by score desc (shorter content
|
|
57
|
+
* wins ties so the budget packs more) → dedup by normalized content → greedily
|
|
58
|
+
* fill the token budget up to top-k, trimming each item. Returns block: '' when
|
|
59
|
+
* nothing qualifies (caller injects nothing — no context noise).
|
|
60
|
+
*/
|
|
61
|
+
export declare function selectInjectionMemories(candidates: InjectionCandidate[], options?: InjectionOptions): InjectionResult;
|
|
62
|
+
/**
|
|
63
|
+
* Extract the user's prompt from a Claude Code UserPromptSubmit hook payload.
|
|
64
|
+
* Claude Code pipes JSON like `{"prompt":"…","session_id":"…"}` on stdin. Falls
|
|
65
|
+
* back to `.userPrompt`/`.message`, then to the raw text when it isn't JSON
|
|
66
|
+
* (a caller passing a plain string). Returns '' when nothing usable is found.
|
|
67
|
+
*/
|
|
68
|
+
export declare function extractPromptFromPayload(raw: string): string;
|
|
69
|
+
/** A vector-search row as returned by bridgeSearchEntries. */
|
|
70
|
+
export interface SearchRow {
|
|
71
|
+
key: string;
|
|
72
|
+
content: string;
|
|
73
|
+
score: number;
|
|
74
|
+
namespace: string;
|
|
75
|
+
provenance?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Map raw search rows to injection candidates, dropping empty content and
|
|
79
|
+
* (when `allowed` is given) rows outside the requested namespace set.
|
|
80
|
+
*/
|
|
81
|
+
export declare function mapSearchResultsToCandidates(results: SearchRow[], allowed?: string[] | Set<string>): InjectionCandidate[];
|
|
82
|
+
//# sourceMappingURL=select.d.ts.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-inject/select.ts — pure selection + formatting for prompt-time
|
|
3
|
+
* semantic memory injection (#43).
|
|
4
|
+
*
|
|
5
|
+
* A UserPromptSubmit hook embeds the current prompt, vector-searches AgentDB
|
|
6
|
+
* across the memory namespaces, and injects the most relevant stored memories
|
|
7
|
+
* into Claude's context under a strict token budget — recall at the moment of
|
|
8
|
+
* need instead of a manual `memory search` or a bulk session-start dump.
|
|
9
|
+
*
|
|
10
|
+
* This module is the DIFFERENTIATING logic and is deliberately pure (no I/O, no
|
|
11
|
+
* embeddings): given already-scored candidates it decides WHICH memories to
|
|
12
|
+
* inject (relevance threshold, cross-namespace dedup, token budget, top-k) and
|
|
13
|
+
* formats the context block. The command layer (`hooks memory-inject`) supplies
|
|
14
|
+
* the real embedding + vector search and emits the hook JSON.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULTS = {
|
|
17
|
+
budgetTokens: 800,
|
|
18
|
+
minRelevance: 0.35,
|
|
19
|
+
topK: 5,
|
|
20
|
+
charsPerToken: 4,
|
|
21
|
+
maxItemChars: 400,
|
|
22
|
+
header: '## 🧠 Relevant memories (swarmdo)',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Intro line placed under the header. States the provenance + a verify caveat —
|
|
26
|
+
* injected memories reflect what was true WHEN WRITTEN, so file/flag names may
|
|
27
|
+
* be stale (matches the auto-memory guidance).
|
|
28
|
+
*/
|
|
29
|
+
const INTRO = 'Retrieved from stored memory by semantic search — may be relevant to this request. ' +
|
|
30
|
+
'They reflect what was true when written; verify file/flag/function names before relying on them.';
|
|
31
|
+
/** Rough token estimate: ceil(chars / charsPerToken). */
|
|
32
|
+
export function estimateTokens(text, charsPerToken = DEFAULTS.charsPerToken) {
|
|
33
|
+
const cpt = charsPerToken > 0 ? charsPerToken : DEFAULTS.charsPerToken;
|
|
34
|
+
return Math.ceil(text.length / cpt);
|
|
35
|
+
}
|
|
36
|
+
/** Collapse whitespace and cap length at a word boundary, adding an ellipsis. */
|
|
37
|
+
export function trimContent(content, maxChars) {
|
|
38
|
+
const collapsed = content.replace(/\s+/g, ' ').trim();
|
|
39
|
+
if (maxChars <= 0 || collapsed.length <= maxChars)
|
|
40
|
+
return collapsed;
|
|
41
|
+
const cut = collapsed.slice(0, maxChars);
|
|
42
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
43
|
+
const body = lastSpace > maxChars * 0.6 ? cut.slice(0, lastSpace) : cut;
|
|
44
|
+
return body.trimEnd() + '…';
|
|
45
|
+
}
|
|
46
|
+
/** Normalized dedup key — the same memory bridged into >1 namespace collapses. */
|
|
47
|
+
function dedupKey(c) {
|
|
48
|
+
return c.content.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
/** Format one memory as a source-attributed bullet. Score shown as a percent. */
|
|
51
|
+
function formatItem(c) {
|
|
52
|
+
const pct = Math.round(Math.max(0, Math.min(1, c.score)) * 100);
|
|
53
|
+
return `- **[${c.namespace}/${c.key}]** (${pct}%) ${c.content}`;
|
|
54
|
+
}
|
|
55
|
+
function renderBlock(used, header) {
|
|
56
|
+
const lines = [header, INTRO, '', ...used.map(formatItem)];
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Select and format the memories to inject.
|
|
61
|
+
*
|
|
62
|
+
* Pipeline: drop empty/below-threshold → sort by score desc (shorter content
|
|
63
|
+
* wins ties so the budget packs more) → dedup by normalized content → greedily
|
|
64
|
+
* fill the token budget up to top-k, trimming each item. Returns block: '' when
|
|
65
|
+
* nothing qualifies (caller injects nothing — no context noise).
|
|
66
|
+
*/
|
|
67
|
+
export function selectInjectionMemories(candidates, options = {}) {
|
|
68
|
+
const opts = {
|
|
69
|
+
budgetTokens: options.budgetTokens ?? DEFAULTS.budgetTokens,
|
|
70
|
+
minRelevance: options.minRelevance ?? DEFAULTS.minRelevance,
|
|
71
|
+
topK: options.topK ?? DEFAULTS.topK,
|
|
72
|
+
charsPerToken: options.charsPerToken ?? DEFAULTS.charsPerToken,
|
|
73
|
+
maxItemChars: options.maxItemChars ?? DEFAULTS.maxItemChars,
|
|
74
|
+
header: options.header ?? DEFAULTS.header,
|
|
75
|
+
};
|
|
76
|
+
const budget = Math.max(0, opts.budgetTokens);
|
|
77
|
+
const topK = Math.max(0, Math.floor(opts.topK));
|
|
78
|
+
const qualifying = (candidates || []).filter((c) => c &&
|
|
79
|
+
typeof c.content === 'string' &&
|
|
80
|
+
c.content.trim().length > 0 &&
|
|
81
|
+
typeof c.score === 'number' &&
|
|
82
|
+
c.score >= opts.minRelevance);
|
|
83
|
+
const sorted = [...qualifying].sort((a, b) => b.score - a.score || a.content.length - b.content.length || a.key.localeCompare(b.key));
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const unique = [];
|
|
86
|
+
for (const c of sorted) {
|
|
87
|
+
const k = dedupKey(c);
|
|
88
|
+
if (seen.has(k))
|
|
89
|
+
continue;
|
|
90
|
+
seen.add(k);
|
|
91
|
+
unique.push(c);
|
|
92
|
+
}
|
|
93
|
+
// Fixed overhead of the header + intro line; every bullet must fit alongside it.
|
|
94
|
+
const overhead = estimateTokens(`${opts.header}\n${INTRO}\n`, opts.charsPerToken);
|
|
95
|
+
const used = [];
|
|
96
|
+
let lineTokens = 0;
|
|
97
|
+
let skipped = 0;
|
|
98
|
+
for (const c of unique) {
|
|
99
|
+
if (used.length >= topK) {
|
|
100
|
+
skipped++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const trimmed = { ...c, content: trimContent(c.content, opts.maxItemChars) };
|
|
104
|
+
const cost = estimateTokens('\n' + formatItem(trimmed), opts.charsPerToken);
|
|
105
|
+
if (overhead + lineTokens + cost > budget) {
|
|
106
|
+
skipped++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
used.push(trimmed);
|
|
110
|
+
lineTokens += cost;
|
|
111
|
+
}
|
|
112
|
+
if (used.length === 0) {
|
|
113
|
+
return { block: '', used: [], skipped, tokensUsed: 0 };
|
|
114
|
+
}
|
|
115
|
+
const block = renderBlock(used, opts.header);
|
|
116
|
+
return { block, used, skipped, tokensUsed: estimateTokens(block, opts.charsPerToken) };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extract the user's prompt from a Claude Code UserPromptSubmit hook payload.
|
|
120
|
+
* Claude Code pipes JSON like `{"prompt":"…","session_id":"…"}` on stdin. Falls
|
|
121
|
+
* back to `.userPrompt`/`.message`, then to the raw text when it isn't JSON
|
|
122
|
+
* (a caller passing a plain string). Returns '' when nothing usable is found.
|
|
123
|
+
*/
|
|
124
|
+
export function extractPromptFromPayload(raw) {
|
|
125
|
+
const text = (raw || '').trim();
|
|
126
|
+
if (!text)
|
|
127
|
+
return '';
|
|
128
|
+
try {
|
|
129
|
+
const obj = JSON.parse(text);
|
|
130
|
+
if (obj && typeof obj === 'object') {
|
|
131
|
+
const p = obj.prompt ?? obj.userPrompt ?? obj.message ?? obj.text;
|
|
132
|
+
return typeof p === 'string' ? p.trim() : '';
|
|
133
|
+
}
|
|
134
|
+
// JSON that parsed to a bare string/number → use its string form.
|
|
135
|
+
return typeof obj === 'string' ? obj.trim() : '';
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Not JSON — treat the raw text as the prompt.
|
|
139
|
+
return text;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Map raw search rows to injection candidates, dropping empty content and
|
|
144
|
+
* (when `allowed` is given) rows outside the requested namespace set.
|
|
145
|
+
*/
|
|
146
|
+
export function mapSearchResultsToCandidates(results, allowed) {
|
|
147
|
+
const allow = allowed ? (allowed instanceof Set ? allowed : new Set(allowed)) : null;
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const r of results || []) {
|
|
150
|
+
if (!r || typeof r.content !== 'string' || r.content.trim().length === 0)
|
|
151
|
+
continue;
|
|
152
|
+
if (allow && !allow.has(r.namespace))
|
|
153
|
+
continue;
|
|
154
|
+
out.push({
|
|
155
|
+
key: r.key,
|
|
156
|
+
namespace: r.namespace,
|
|
157
|
+
content: r.content,
|
|
158
|
+
score: typeof r.score === 'number' ? r.score : 0,
|
|
159
|
+
provenance: r.provenance,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=select.js.map
|
|
@@ -27,12 +27,22 @@ export function unfenceBody(body) {
|
|
|
27
27
|
const m = /^```json\n([\s\S]*)\n```$/.exec(t);
|
|
28
28
|
if (!m)
|
|
29
29
|
return body;
|
|
30
|
+
let parsed;
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
+
parsed = JSON.parse(m[1]);
|
|
32
33
|
}
|
|
33
34
|
catch {
|
|
34
35
|
return body;
|
|
35
36
|
}
|
|
37
|
+
// Only unfence a block that renderBody itself produced (it always pretty-prints
|
|
38
|
+
// with 2-space indent). A raw stored value that merely HAPPENS to already be a
|
|
39
|
+
// ```json fence — renderBody left it untouched because it isn't a bare
|
|
40
|
+
// object/array — must round-trip byte-for-byte; minifying it would silently
|
|
41
|
+
// rewrite the user's data on every export→import even with zero edits.
|
|
42
|
+
const reFenced = '```json\n' + JSON.stringify(parsed, null, 2) + '\n```';
|
|
43
|
+
if (reFenced !== t)
|
|
44
|
+
return body;
|
|
45
|
+
return JSON.stringify(parsed);
|
|
36
46
|
}
|
|
37
47
|
/** Parse one note. Returns null unless it carries our frontmatter stamp. */
|
|
38
48
|
export function parseNote(content) {
|
|
@@ -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
|
});
|