crow-central-agency 0.26.13 → 0.26.14

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 (52) hide show
  1. package/dist/_shared/auto-approve/auto-approve-rule-set.js +30 -0
  2. package/dist/_shared/auto-approve/auto-approve-rule.types.js +2 -0
  3. package/dist/_shared/auto-approve/command-decomposition.js +238 -0
  4. package/dist/_shared/auto-approve/rule-format.js +79 -0
  5. package/dist/_shared/auto-approve/rule-strategies.js +72 -0
  6. package/dist/_shared/auto-approve/rule-strategy-registry.js +7 -0
  7. package/dist/_shared/index.js +5 -0
  8. package/dist/bootstrap.js +2 -0
  9. package/dist/feed/feed-search-index.js +211 -0
  10. package/dist/feed/feed-search-index.types.js +2 -0
  11. package/dist/feed/simply-feed-manager.js +92 -58
  12. package/dist/mcp/agents/invoke-agent.js +10 -7
  13. package/dist/mcp/tasks/get-task-result.js +4 -3
  14. package/dist/mcp/tasks/get-task.js +1 -1
  15. package/dist/mcp/tasks/tasks-mcp-server-utils.js +25 -26
  16. package/dist/mcp/tasks/tasks-mcp-server.js +1 -1
  17. package/dist/public/assets/{architectureDiagram-3BPJPVTR-Bt4tuNX8.js → architectureDiagram-3BPJPVTR-9PO2Zi5j.js} +1 -1
  18. package/dist/public/assets/{chunk-727SXJPM-BjjXmWC5.js → chunk-727SXJPM-BmaCn-6B.js} +1 -1
  19. package/dist/public/assets/{chunk-AQP2D5EJ-DjJ_-SOd.js → chunk-AQP2D5EJ-Cca-zKhj.js} +1 -1
  20. package/dist/public/assets/{classDiagram-v2-Q7XG4LA2-DVPqtO0x.js → classDiagram-4FO5ZUOK-B_zdrMg0.js} +1 -1
  21. package/dist/public/assets/{classDiagram-4FO5ZUOK-DVPqtO0x.js → classDiagram-v2-Q7XG4LA2-B_zdrMg0.js} +1 -1
  22. package/dist/public/assets/{dagre-BM42HDAG-CZBf8H9_.js → dagre-BM42HDAG-CRiqaSzz.js} +1 -1
  23. package/dist/public/assets/{diagram-2AECGRRQ-C_U6a4go.js → diagram-2AECGRRQ-BRpLjqbq.js} +1 -1
  24. package/dist/public/assets/{diagram-5GNKFQAL-Bk-3tHvA.js → diagram-5GNKFQAL-Bc9v-jIT.js} +1 -1
  25. package/dist/public/assets/{diagram-LMA3HP47-JxZtWEor.js → diagram-LMA3HP47-BML6fr2x.js} +1 -1
  26. package/dist/public/assets/{diagram-OG6HWLK6-BJ1F3Sxu.js → diagram-OG6HWLK6-Czbcayu-.js} +1 -1
  27. package/dist/public/assets/{erDiagram-TEJ5UH35-BJBHxTf8.js → erDiagram-TEJ5UH35-DWxHYHOI.js} +1 -1
  28. package/dist/public/assets/{flowDiagram-I6XJVG4X-DJ3nm5z7.js → flowDiagram-I6XJVG4X-ZS40gCP9.js} +1 -1
  29. package/dist/public/assets/index-Bf-eiYs6.js +506 -0
  30. package/dist/public/assets/index-DOeMoLVc.css +1 -0
  31. package/dist/public/assets/{infoDiagram-5YYISTIA-Cc1k7k1c.js → infoDiagram-5YYISTIA-cREQjSEN.js} +1 -1
  32. package/dist/public/assets/{ishikawaDiagram-YF4QCWOH-DrDraHBD.js → ishikawaDiagram-YF4QCWOH-BGHPisrV.js} +1 -1
  33. package/dist/public/assets/{kanban-definition-UN3LZRKU-CNUMfAVC.js → kanban-definition-UN3LZRKU-C-2qMHn6.js} +1 -1
  34. package/dist/public/assets/{mindmap-definition-RKZ34NQL-DOeL-Iu4.js → mindmap-definition-RKZ34NQL-gLns4iF7.js} +1 -1
  35. package/dist/public/assets/{pieDiagram-4H26LBE5-DCP9wuZK.js → pieDiagram-4H26LBE5-Da06yraI.js} +1 -1
  36. package/dist/public/assets/{requirementDiagram-4Y6WPE33-7UzZpq-C.js → requirementDiagram-4Y6WPE33-BsSvQc8s.js} +1 -1
  37. package/dist/public/assets/{sequenceDiagram-3UESZ5HK-D5alEu9E.js → sequenceDiagram-3UESZ5HK-CxdEFAWz.js} +1 -1
  38. package/dist/public/assets/{stateDiagram-AJRCARHV-EkSeb-RA.js → stateDiagram-AJRCARHV-itM-qY8o.js} +1 -1
  39. package/dist/public/assets/{stateDiagram-v2-BHNVJYJU-DmpUDd8s.js → stateDiagram-v2-BHNVJYJU-CSRZkocn.js} +1 -1
  40. package/dist/public/assets/{timeline-definition-PNZ67QCA-B4oLmMT1.js → timeline-definition-PNZ67QCA-BUxAMoDy.js} +1 -1
  41. package/dist/public/assets/{vennDiagram-CIIHVFJN-DgiGQ7rE.js → vennDiagram-CIIHVFJN-CtO4Fn4j.js} +1 -1
  42. package/dist/public/assets/{wardleyDiagram-YWT4CUSO-dDWLWZoc.js → wardleyDiagram-YWT4CUSO-BewTtRo4.js} +1 -1
  43. package/dist/public/assets/{xychartDiagram-2RQKCTM6-TOELm0t1.js → xychartDiagram-2RQKCTM6-DjHvRmZM.js} +1 -1
  44. package/dist/public/index.html +2 -2
  45. package/dist/routines/inter-agent-task-routine.js +8 -11
  46. package/dist/runner/claude-code-agent-runner.js +18 -10
  47. package/dist/runner/github-copilot-agent-runner.js +21 -23
  48. package/dist/services/agent-registry.js +15 -5
  49. package/dist/services/runtime/agent-runtime-manager.js +2 -2
  50. package/package.json +5 -2
  51. package/dist/public/assets/index-BJmLehUe.js +0 -505
  52. package/dist/public/assets/index-BqakLGSS.css +0 -1
@@ -0,0 +1,30 @@
1
+ import { parseRule } from "./rule-format.js";
2
+ import { getRuleStrategy } from "./rule-strategy-registry.js";
3
+ /**
4
+ * A mutable set of auto-approve rule strings that parses each rule once and caches the result, so
5
+ * permission checks match against the pre-parsed rules instead of re-parsing the whole list per call.
6
+ * Raw strings are deduped; malformed rules are dropped on add (same as `parseRules`).
7
+ */
8
+ export class AutoApproveRuleSet {
9
+ raw = new Set();
10
+ parsed = [];
11
+ constructor(initialRules = []) {
12
+ this.add(initialRules);
13
+ }
14
+ add(rules) {
15
+ for (const rule of rules) {
16
+ if (this.raw.has(rule)) {
17
+ continue;
18
+ }
19
+ this.raw.add(rule);
20
+ const parsedRule = parseRule(rule);
21
+ if (parsedRule !== undefined) {
22
+ this.parsed.push(parsedRule);
23
+ }
24
+ }
25
+ }
26
+ matches(toolName, input) {
27
+ return getRuleStrategy(toolName).matches(toolName, input, this.parsed);
28
+ }
29
+ }
30
+ //# sourceMappingURL=auto-approve-rule-set.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=auto-approve-rule.types.js.map
@@ -0,0 +1,238 @@
1
+ import { matchesSpecifier, WORD_BOUNDARY_SUFFIX } from "./rule-format.js";
2
+ /** Prefix depth for derived rules (first N tokens of a subcommand). */
3
+ export const DEFAULT_PREFIX_DEPTH = 2;
4
+ /** Maximum number of rules derived from a single compound command, mirroring the SDK cap. */
5
+ export const MAX_DERIVED_RULES = 5;
6
+ /**
7
+ * Shell separators that decompose a compound command. Two-character separators are matched before
8
+ * the single-character ones so `&&`/`||`/`|&` win over `&`/`|`.
9
+ */
10
+ export const TWO_CHAR_SEPARATORS = ["&&", "||", "|&"];
11
+ export const SINGLE_CHAR_SEPARATORS = [";", "|", "&", "\n", "\r"];
12
+ /**
13
+ * Process wrappers stripped from the front of a subcommand before matching. `timeout`/`nice` also
14
+ * consume a leading numeric argument (duration / priority).
15
+ */
16
+ export const PROCESS_WRAPPERS = new Set(["timeout", "time", "nice", "nohup", "stdbuf"]);
17
+ const WRAPPERS_WITH_NUMERIC_ARG = new Set(["timeout", "nice"]);
18
+ const XARGS_WRAPPER = "xargs";
19
+ /**
20
+ * Read-only shell builtins that run with no prompt. `cd` and `git` are handled separately
21
+ * (in-cwd `cd` only; read-only `git` subcommands only).
22
+ */
23
+ export const READ_ONLY_BUILTINS = new Set([
24
+ "ls",
25
+ "cat",
26
+ "echo",
27
+ "pwd",
28
+ "head",
29
+ "tail",
30
+ "grep",
31
+ "find",
32
+ "wc",
33
+ "which",
34
+ "diff",
35
+ "stat",
36
+ "du",
37
+ ]);
38
+ const CD_COMMAND = "cd";
39
+ const IN_CWD_CD_ARGS = new Set([".", "./"]);
40
+ const GIT_COMMAND = "git";
41
+ const READ_ONLY_GIT_SUBCOMMANDS = new Set(["status", "log", "diff", "show"]);
42
+ const SINGLE_QUOTE = "'";
43
+ const DOUBLE_QUOTE = '"';
44
+ const REDIRECT_CHAR = ">";
45
+ /** Split a command string into whitespace-delimited tokens, respecting single/double quotes. */
46
+ function tokenize(command) {
47
+ const tokens = [];
48
+ let current = "";
49
+ let hasToken = false;
50
+ let quote;
51
+ for (const char of command) {
52
+ if (quote !== undefined) {
53
+ if (char === quote) {
54
+ quote = undefined;
55
+ }
56
+ else {
57
+ current += char;
58
+ }
59
+ continue;
60
+ }
61
+ if (char === SINGLE_QUOTE || char === DOUBLE_QUOTE) {
62
+ quote = char;
63
+ hasToken = true;
64
+ continue;
65
+ }
66
+ if (char === " " || char === "\t") {
67
+ if (hasToken) {
68
+ tokens.push(current);
69
+ current = "";
70
+ hasToken = false;
71
+ }
72
+ continue;
73
+ }
74
+ current += char;
75
+ hasToken = true;
76
+ }
77
+ if (hasToken) {
78
+ tokens.push(current);
79
+ }
80
+ return tokens;
81
+ }
82
+ /** Return the shell separator starting at `index`, or `undefined` if none. */
83
+ function separatorAt(command, index) {
84
+ const twoChar = command.slice(index, index + 2);
85
+ for (const separator of TWO_CHAR_SEPARATORS) {
86
+ if (twoChar === separator) {
87
+ return separator;
88
+ }
89
+ }
90
+ const char = command[index];
91
+ if (char === "&") {
92
+ // Guard shell redirects (`2>&1`, `&>file`) from being read as a background separator.
93
+ if (command[index - 1] === REDIRECT_CHAR || command[index + 1] === REDIRECT_CHAR) {
94
+ return undefined;
95
+ }
96
+ }
97
+ for (const separator of SINGLE_CHAR_SEPARATORS) {
98
+ if (char === separator) {
99
+ return separator;
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+ /** Strip leading process wrappers (and their option/numeric args) from a token list. */
105
+ function stripWrappers(tokens) {
106
+ let remaining = tokens;
107
+ while (remaining.length > 0) {
108
+ const head = remaining[0];
109
+ if (head === XARGS_WRAPPER) {
110
+ // Only bare `xargs` (no flags) is transparent; flagged xargs stays and simply won't match.
111
+ const next = remaining[1];
112
+ if (next !== undefined && next.startsWith("-")) {
113
+ break;
114
+ }
115
+ remaining = remaining.slice(1);
116
+ continue;
117
+ }
118
+ if (!PROCESS_WRAPPERS.has(head)) {
119
+ break;
120
+ }
121
+ remaining = remaining.slice(1);
122
+ while (remaining.length > 0 && remaining[0].startsWith("-")) {
123
+ remaining = remaining.slice(1);
124
+ }
125
+ if (WRAPPERS_WITH_NUMERIC_ARG.has(head) && remaining.length > 0 && /^[0-9]/.test(remaining[0])) {
126
+ remaining = remaining.slice(1);
127
+ }
128
+ }
129
+ return remaining;
130
+ }
131
+ /**
132
+ * Split a compound command into its normalized subcommands. Splits on shell separators outside
133
+ * quotes, strips process wrappers, and drops empty segments. The returned strings are re-joined
134
+ * from parsed tokens (quotes removed, whitespace collapsed) for stable matching.
135
+ */
136
+ export function splitSubcommands(command) {
137
+ const rawSegments = [];
138
+ let current = "";
139
+ let quote;
140
+ let index = 0;
141
+ while (index < command.length) {
142
+ const char = command[index];
143
+ if (quote !== undefined) {
144
+ current += char;
145
+ if (char === quote) {
146
+ quote = undefined;
147
+ }
148
+ index += 1;
149
+ continue;
150
+ }
151
+ if (char === SINGLE_QUOTE || char === DOUBLE_QUOTE) {
152
+ quote = char;
153
+ current += char;
154
+ index += 1;
155
+ continue;
156
+ }
157
+ const separator = separatorAt(command, index);
158
+ if (separator !== undefined) {
159
+ rawSegments.push(current);
160
+ current = "";
161
+ index += separator.length;
162
+ continue;
163
+ }
164
+ current += char;
165
+ index += 1;
166
+ }
167
+ rawSegments.push(current);
168
+ const subcommands = [];
169
+ for (const segment of rawSegments) {
170
+ const tokens = stripWrappers(tokenize(segment));
171
+ if (tokens.length > 0) {
172
+ subcommands.push(tokens.join(" "));
173
+ }
174
+ }
175
+ return subcommands;
176
+ }
177
+ /** Whether a normalized subcommand is a read-only builtin, in-cwd `cd`, or read-only `git`. */
178
+ export function isReadOnlyCommand(subcommand) {
179
+ const tokens = tokenize(subcommand);
180
+ const head = tokens[0];
181
+ if (head === undefined) {
182
+ return false;
183
+ }
184
+ if (head === CD_COMMAND) {
185
+ const target = tokens[1];
186
+ return target === undefined || IN_CWD_CD_ARGS.has(target);
187
+ }
188
+ if (head === GIT_COMMAND) {
189
+ const gitSubcommand = tokens[1];
190
+ return gitSubcommand !== undefined && READ_ONLY_GIT_SUBCOMMANDS.has(gitSubcommand);
191
+ }
192
+ return READ_ONLY_BUILTINS.has(head);
193
+ }
194
+ /**
195
+ * Capture side: derive one prefix-`depth` specifier per non-read-only subcommand (in-cwd `cd` and
196
+ * read-only builtins contribute nothing), deduped and capped at {@link MAX_DERIVED_RULES}.
197
+ */
198
+ export function deriveRules(command, depth = DEFAULT_PREFIX_DEPTH) {
199
+ const specifiers = [];
200
+ for (const subcommand of splitSubcommands(command)) {
201
+ if (specifiers.length >= MAX_DERIVED_RULES) {
202
+ break;
203
+ }
204
+ if (isReadOnlyCommand(subcommand)) {
205
+ continue;
206
+ }
207
+ const tokens = tokenize(subcommand);
208
+ if (tokens.length === 0) {
209
+ continue;
210
+ }
211
+ const prefix = tokens.slice(0, depth).join(" ");
212
+ const specifier = `${prefix}${WORD_BOUNDARY_SUFFIX}`;
213
+ if (!specifiers.includes(specifier)) {
214
+ specifiers.push(specifier);
215
+ }
216
+ }
217
+ return specifiers;
218
+ }
219
+ /**
220
+ * Match side: fails closed. Every subcommand must be a read-only builtin or match one of the
221
+ * specifiers. Empty/unparseable input yields `false`.
222
+ */
223
+ export function matchesRules(command, specifiers) {
224
+ const subcommands = splitSubcommands(command);
225
+ if (subcommands.length === 0) {
226
+ return false;
227
+ }
228
+ for (const subcommand of subcommands) {
229
+ if (isReadOnlyCommand(subcommand)) {
230
+ continue;
231
+ }
232
+ if (!specifiers.some((specifier) => matchesSpecifier(subcommand, specifier))) {
233
+ return false;
234
+ }
235
+ }
236
+ return true;
237
+ }
238
+ //# sourceMappingURL=command-decomposition.js.map
@@ -0,0 +1,79 @@
1
+ /** Glob wildcard used in rule specifiers. */
2
+ export const GLOB_STAR = "*";
3
+ /**
4
+ * A space before a trailing `*` enforces a word boundary: `ls *` matches `ls` and `ls -la`,
5
+ * but never `lsof`.
6
+ */
7
+ export const WORD_BOUNDARY_SUFFIX = " *";
8
+ /** Claude's `:*` prefix suffix, treated as equivalent to a trailing ` *`. */
9
+ export const PREFIX_STAR_SUFFIX = ":*";
10
+ const RULE_PATTERN = /^([^()]+)(?:\((.*)\))?$/;
11
+ const REGEXP_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/g;
12
+ /**
13
+ * Parse a rule string into its tool + optional specifier. Fails closed: any malformed rule
14
+ * (unbalanced parentheses, empty tool, stray `(` in the tool name) yields `undefined`.
15
+ */
16
+ export function parseRule(rule) {
17
+ const trimmed = rule.trim();
18
+ if (trimmed.length === 0) {
19
+ return undefined;
20
+ }
21
+ const match = RULE_PATTERN.exec(trimmed);
22
+ if (!match) {
23
+ return undefined;
24
+ }
25
+ const tool = match[1];
26
+ const specifier = match[2]?.trim();
27
+ if (tool.length === 0) {
28
+ return undefined;
29
+ }
30
+ return specifier === undefined ? { tool } : { tool, specifier };
31
+ }
32
+ /** Parse a list of rule strings, discarding any that fail to parse. */
33
+ export function parseRules(rules) {
34
+ const parsed = [];
35
+ for (const rule of rules) {
36
+ const result = parseRule(rule);
37
+ if (result !== undefined) {
38
+ parsed.push(result);
39
+ }
40
+ }
41
+ return parsed;
42
+ }
43
+ /** Format a parsed rule back into its canonical string form. */
44
+ export function formatRule(rule) {
45
+ return rule.specifier === undefined ? rule.tool : `${rule.tool}(${rule.specifier})`;
46
+ }
47
+ /** Normalize Claude's `:*` prefix suffix into the equivalent word-boundary ` *` form. */
48
+ function normalizeSpecifier(specifier) {
49
+ if (specifier.endsWith(PREFIX_STAR_SUFFIX)) {
50
+ return `${specifier.slice(0, -PREFIX_STAR_SUFFIX.length)}${WORD_BOUNDARY_SUFFIX}`;
51
+ }
52
+ return specifier;
53
+ }
54
+ function globToRegExp(pattern) {
55
+ const source = pattern
56
+ .split(GLOB_STAR)
57
+ .map((segment) => segment.replace(REGEXP_SPECIAL_CHARS, "\\$&"))
58
+ .join(".*");
59
+ return new RegExp(`^${source}$`);
60
+ }
61
+ /**
62
+ * Whether a single command/value matches a rule specifier under glob semantics. A specifier
63
+ * ending in a word-boundary ` *` (or `:*`) matches the exact prefix or the prefix followed by a
64
+ * space; any other `*` is a general wildcard. No wildcard means an exact match.
65
+ */
66
+ export function matchesSpecifier(value, specifier) {
67
+ const normalized = normalizeSpecifier(specifier);
68
+ if (normalized.endsWith(WORD_BOUNDARY_SUFFIX)) {
69
+ const base = normalized.slice(0, -WORD_BOUNDARY_SUFFIX.length);
70
+ if (!base.includes(GLOB_STAR)) {
71
+ return value === base || value.startsWith(`${base} `);
72
+ }
73
+ }
74
+ if (normalized.includes(GLOB_STAR)) {
75
+ return globToRegExp(normalized).test(value);
76
+ }
77
+ return value === normalized;
78
+ }
79
+ //# sourceMappingURL=rule-format.js.map
@@ -0,0 +1,72 @@
1
+ import { CLAUDE_CODE_TOOL } from "../schemas/agent.schema.js";
2
+ import { deriveRules as deriveCommandRules, matchesRules as matchesCommandRules } from "./command-decomposition.js";
3
+ import { formatRule, GLOB_STAR } from "./rule-format.js";
4
+ /** Input field carrying the shell command for Bash/PowerShell tools. */
5
+ const COMMAND_INPUT_KEY = "command";
6
+ /**
7
+ * Command-scoped tools, compared case-insensitively to cover Claude (`Bash`, `PowerShell`) and
8
+ * GitHub Copilot (`bash`, `powershell`).
9
+ */
10
+ const COMMAND_TOOL_BASE_NAMES = new Set([
11
+ CLAUDE_CODE_TOOL.BASH.toLowerCase(),
12
+ CLAUDE_CODE_TOOL.POWER_SHELL.toLowerCase(),
13
+ ]);
14
+ /**
15
+ * Whole-tool matching, preserving the legacy `isToolAutoApproved` behavior: exact tool-name match,
16
+ * plus a trailing `*` (including a bare `*` and MCP prefixes like `mcp__crow-artifacts__*`) as a
17
+ * prefix match.
18
+ */
19
+ function wholeToolMatches(toolName, rules) {
20
+ for (const rule of rules) {
21
+ if (rule.specifier !== undefined) {
22
+ continue;
23
+ }
24
+ const pattern = rule.tool;
25
+ if (pattern === toolName) {
26
+ return true;
27
+ }
28
+ if (pattern.endsWith(GLOB_STAR) && toolName.startsWith(pattern.slice(0, -GLOB_STAR.length))) {
29
+ return true;
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ function extractCommand(input) {
35
+ const command = input[COMMAND_INPUT_KEY];
36
+ return typeof command === "string" ? command : undefined;
37
+ }
38
+ /** Default strategy: today's whole-tool behavior, used for any tool without a specialization. */
39
+ export const defaultRuleStrategy = {
40
+ appliesTo: () => true,
41
+ deriveRules: (toolName) => [toolName],
42
+ matches: (toolName, _input, rules) => wholeToolMatches(toolName, rules),
43
+ };
44
+ /** Command strategy: Bash/PowerShell decomposition on top of the whole-tool behavior. */
45
+ export const commandRuleStrategy = {
46
+ appliesTo: (toolName) => COMMAND_TOOL_BASE_NAMES.has(toolName.toLowerCase()),
47
+ deriveRules: (toolName, input) => {
48
+ const command = extractCommand(input);
49
+ if (command === undefined) {
50
+ return [];
51
+ }
52
+ return deriveCommandRules(command).map((specifier) => formatRule({ tool: toolName, specifier }));
53
+ },
54
+ matches: (toolName, input, rules) => {
55
+ if (wholeToolMatches(toolName, rules)) {
56
+ return true;
57
+ }
58
+ const command = extractCommand(input);
59
+ if (command === undefined) {
60
+ return false;
61
+ }
62
+ const targetTool = toolName.toLowerCase();
63
+ const specifiers = [];
64
+ for (const rule of rules) {
65
+ if (rule.specifier !== undefined && rule.tool.toLowerCase() === targetTool) {
66
+ specifiers.push(rule.specifier);
67
+ }
68
+ }
69
+ return matchesCommandRules(command, specifiers);
70
+ },
71
+ };
72
+ //# sourceMappingURL=rule-strategies.js.map
@@ -0,0 +1,7 @@
1
+ import { commandRuleStrategy, defaultRuleStrategy } from "./rule-strategies.js";
2
+ const REGISTERED_STRATEGIES = [commandRuleStrategy];
3
+ /** Resolve the auto-approve strategy for a tool, falling back to the default whole-tool strategy. */
4
+ export function getRuleStrategy(toolName) {
5
+ return REGISTERED_STRATEGIES.find((strategy) => strategy.appliesTo(toolName)) ?? defaultRuleStrategy;
6
+ }
7
+ //# sourceMappingURL=rule-strategy-registry.js.map
@@ -24,4 +24,9 @@ export { MESSAGE_SOURCE_TYPE, MessageSourceSchema, } from "./schemas/message-sou
24
24
  export { AGENT_COMMAND, AgentCommandSchema } from "./schemas/agent-command.schema.js";
25
25
  export { GraphNodeSchema, GraphEdgeSchema, GraphDataSchema, } from "./schemas/graph.schema.js";
26
26
  export { ConnectorConnectionSchema, ConnectorInfoSchema, ConnectConnectorResponseSchema, } from "./schemas/connector.schema.js";
27
+ export { parseRule, parseRules, formatRule, matchesSpecifier, GLOB_STAR, WORD_BOUNDARY_SUFFIX, PREFIX_STAR_SUFFIX, } from "./auto-approve/rule-format.js";
28
+ export { splitSubcommands, deriveRules, matchesRules, isReadOnlyCommand, DEFAULT_PREFIX_DEPTH, MAX_DERIVED_RULES, READ_ONLY_BUILTINS, PROCESS_WRAPPERS, } from "./auto-approve/command-decomposition.js";
29
+ export { defaultRuleStrategy, commandRuleStrategy } from "./auto-approve/rule-strategies.js";
30
+ export { getRuleStrategy } from "./auto-approve/rule-strategy-registry.js";
31
+ export { AutoApproveRuleSet } from "./auto-approve/auto-approve-rule-set.js";
27
32
  //# sourceMappingURL=index.js.map
package/dist/bootstrap.js CHANGED
@@ -91,6 +91,7 @@ export async function bootstrap(options) {
91
91
  const taskManager = new AgentTaskManager(storeProvider, broadcaster, circleManager);
92
92
  await taskManager.initialize();
93
93
  const feedManager = new SimplyFeedManager(storeProvider, folderFileProvider, crowScheduler);
94
+ await feedManager.initialize();
94
95
  const artifactManager = new ArtifactManager(storeProvider, registry, circleManager);
95
96
  await artifactManager.initialize();
96
97
  const documentSearchService = new DocumentSearchService(artifactManager, taskManager, registry, circleManager);
@@ -175,6 +176,7 @@ export async function bootstrap(options) {
175
176
  const shutdown = async () => {
176
177
  logger.info("Shutting down...");
177
178
  crowScheduler.stop();
179
+ await feedManager.dispose();
178
180
  await discordBotManager.destroy();
179
181
  await server.close();
180
182
  await copilotClientManager.dispose();
@@ -0,0 +1,211 @@
1
+ import MiniSearch, {} from "minisearch";
2
+ import { logger } from "../utils/logger.js";
3
+ const log = logger.child({ context: "feed-search-index" });
4
+ export const SEARCH_INDEX_STORE_TABLE = "feeds/searchIndex";
5
+ export const SEARCH_INDEX_KEY = "global";
6
+ const INDEX_ID_FIELD = "id";
7
+ const INDEX_FIELDS = ["title", "subtitle", "description", "summary", "topics", "categories", "author"];
8
+ const INDEX_STORE_FIELDS = ["feedId"];
9
+ /**
10
+ * Lexical-primary options for the raw user query. Prefix matching plus modest
11
+ * fuzziness tolerate typos and partial words; title-first boosts keep exact
12
+ * headline hits above body matches. IDF (BM25) ensures rare terms — such as a
13
+ * person's name — rank at the top automatically.
14
+ */
15
+ const RAW_SEARCH_OPTIONS = {
16
+ prefix: true,
17
+ fuzzy: 0.2,
18
+ boost: { title: 3, author: 2, summary: 1.5, topics: 1.2, description: 1, subtitle: 1, categories: 1 },
19
+ };
20
+ /**
21
+ * Options for the LLM-expanded terms. Restricted to semantic fields and run
22
+ * without prefix/fuzzy so expansion only adds recall; its contribution is
23
+ * further scaled down by EXPANSION_SCORE_WEIGHT so it can never displace a
24
+ * literal match.
25
+ */
26
+ const EXPANSION_SEARCH_OPTIONS = {
27
+ fields: ["topics", "summary"],
28
+ prefix: false,
29
+ fuzzy: false,
30
+ };
31
+ const EXPANSION_SCORE_WEIGHT = 0.15;
32
+ /** Persist at most once per quiet window after the first pending change. */
33
+ const PERSIST_DEBOUNCE_MS = 5000;
34
+ function createMiniSearchOptions() {
35
+ return {
36
+ idField: INDEX_ID_FIELD,
37
+ fields: INDEX_FIELDS,
38
+ storeFields: INDEX_STORE_FIELDS,
39
+ };
40
+ }
41
+ /**
42
+ * A single global MiniSearch index over all feeds' items. A global (rather than
43
+ * per-feed) index is required so IDF is corpus-relative across feeds, giving
44
+ * correct cross-feed ranking. Only `id` and `feedId` are stored; the full
45
+ * FeedItem is hydrated by id after ranking. The index is persisted to the
46
+ * object store on a debounce and flushed on shutdown, never per item.
47
+ */
48
+ export class FeedSearchIndex {
49
+ indexStore;
50
+ miniSearch;
51
+ fingerprint;
52
+ dirty = false;
53
+ persistTimer;
54
+ constructor(indexStore) {
55
+ this.indexStore = indexStore;
56
+ this.miniSearch = new MiniSearch(createMiniSearchOptions());
57
+ this.fingerprint = JSON.stringify(createMiniSearchOptions());
58
+ }
59
+ /**
60
+ * Load the persisted index, or rebuild from all items when absent or when the
61
+ * options fingerprint no longer matches. The rebuild is persisted so the next
62
+ * boot loads cleanly.
63
+ */
64
+ async initialize(loadAllItems) {
65
+ const persisted = await this.loadPersisted();
66
+ if (persisted && persisted.optionsFingerprint === this.fingerprint) {
67
+ try {
68
+ this.miniSearch = await MiniSearch.loadJSONAsync(persisted.serializedIndex, createMiniSearchOptions());
69
+ log.info({ documentCount: this.miniSearch.documentCount }, "Loaded persisted feed search index");
70
+ return;
71
+ }
72
+ catch (error) {
73
+ log.warn({ error }, "Failed to load persisted feed search index; rebuilding");
74
+ }
75
+ }
76
+ await this.rebuild(loadAllItems);
77
+ }
78
+ /** Add new items or replace existing ones (same id) in the index. */
79
+ addOrReplace(items) {
80
+ if (items.length === 0) {
81
+ return;
82
+ }
83
+ for (const item of items) {
84
+ const document = this.toDocument(item);
85
+ if (this.miniSearch.has(document.id)) {
86
+ this.miniSearch.replace(document);
87
+ }
88
+ else {
89
+ this.miniSearch.add(document);
90
+ }
91
+ }
92
+ this.schedulePersist();
93
+ }
94
+ /** Remove items from the index by id (pruned or deleted items). */
95
+ discard(itemIds) {
96
+ const presentIds = itemIds.filter((id) => this.miniSearch.has(id));
97
+ if (presentIds.length === 0) {
98
+ return;
99
+ }
100
+ this.miniSearch.discardAll(presentIds);
101
+ this.schedulePersist();
102
+ }
103
+ /**
104
+ * Rank items for the raw query blended with low-weight expansion terms.
105
+ * Results are scored on a single blended score (raw match + weighted
106
+ * expansion), optionally filtered to the given feeds while IDF stays global.
107
+ */
108
+ search(rawQuery, expansionTerms, feedIds) {
109
+ const feedIdSet = feedIds ? new Set(feedIds) : undefined;
110
+ const filter = feedIdSet ? (result) => feedIdSet.has(result.feedId) : undefined;
111
+ const scores = new Map();
112
+ const rawResults = this.miniSearch.search(rawQuery, { ...RAW_SEARCH_OPTIONS, filter });
113
+ for (const result of rawResults) {
114
+ scores.set(result.id, { id: result.id, feedId: result.feedId, score: result.score });
115
+ }
116
+ if (expansionTerms.length > 0) {
117
+ const expansionResults = this.miniSearch.search(expansionTerms.join(" "), {
118
+ ...EXPANSION_SEARCH_OPTIONS,
119
+ filter,
120
+ });
121
+ for (const result of expansionResults) {
122
+ const weighted = result.score * EXPANSION_SCORE_WEIGHT;
123
+ const existing = scores.get(result.id);
124
+ if (existing) {
125
+ existing.score += weighted;
126
+ }
127
+ else {
128
+ scores.set(result.id, { id: result.id, feedId: result.feedId, score: weighted });
129
+ }
130
+ }
131
+ }
132
+ return Array.from(scores.values()).sort((first, second) => second.score - first.score);
133
+ }
134
+ /** Persist any pending changes immediately and cancel the debounce timer. */
135
+ async flush() {
136
+ if (this.persistTimer) {
137
+ clearTimeout(this.persistTimer);
138
+ this.persistTimer = undefined;
139
+ }
140
+ if (this.dirty) {
141
+ await this.persist();
142
+ }
143
+ }
144
+ async rebuild(loadAllItems) {
145
+ this.miniSearch = new MiniSearch(createMiniSearchOptions());
146
+ const items = await loadAllItems();
147
+ await this.miniSearch.addAllAsync(items.map((item) => this.toDocument(item)));
148
+ this.dirty = true;
149
+ await this.persist();
150
+ log.info({ documentCount: this.miniSearch.documentCount }, "Rebuilt feed search index");
151
+ }
152
+ schedulePersist() {
153
+ this.dirty = true;
154
+ if (this.persistTimer) {
155
+ return;
156
+ }
157
+ this.persistTimer = setTimeout(() => {
158
+ this.persistTimer = undefined;
159
+ void this.persist();
160
+ }, PERSIST_DEBOUNCE_MS);
161
+ this.persistTimer.unref?.();
162
+ }
163
+ async persist() {
164
+ let payload;
165
+ try {
166
+ payload = {
167
+ optionsFingerprint: this.fingerprint,
168
+ serializedIndex: JSON.stringify(this.miniSearch),
169
+ };
170
+ }
171
+ catch (error) {
172
+ log.error({ error }, "Failed to serialize feed search index");
173
+ return;
174
+ }
175
+ // Clear the dirty flag synchronously after serialization so changes that
176
+ // arrive while the store write is in flight re-mark the index dirty and are
177
+ // not lost by a concurrent flush or the next persist.
178
+ this.dirty = false;
179
+ try {
180
+ await this.indexStore.set(SEARCH_INDEX_STORE_TABLE, SEARCH_INDEX_KEY, payload);
181
+ }
182
+ catch (error) {
183
+ this.dirty = true;
184
+ log.error({ error }, "Failed to persist feed search index");
185
+ }
186
+ }
187
+ async loadPersisted() {
188
+ try {
189
+ const entry = await this.indexStore.get(SEARCH_INDEX_STORE_TABLE, SEARCH_INDEX_KEY);
190
+ return entry?.value;
191
+ }
192
+ catch (error) {
193
+ log.warn({ error }, "Failed to read persisted feed search index");
194
+ return undefined;
195
+ }
196
+ }
197
+ toDocument(item) {
198
+ return {
199
+ id: item.id,
200
+ feedId: item.feedId,
201
+ title: item.title,
202
+ subtitle: item.subtitle,
203
+ description: item.description,
204
+ summary: item.summary ?? "",
205
+ topics: item.topics ?? [],
206
+ categories: item.categories ?? [],
207
+ author: item.author ?? "",
208
+ };
209
+ }
210
+ }
211
+ //# sourceMappingURL=feed-search-index.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=feed-search-index.types.js.map