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.
- package/dist/_shared/auto-approve/auto-approve-rule-set.js +30 -0
- package/dist/_shared/auto-approve/auto-approve-rule.types.js +2 -0
- package/dist/_shared/auto-approve/command-decomposition.js +238 -0
- package/dist/_shared/auto-approve/rule-format.js +79 -0
- package/dist/_shared/auto-approve/rule-strategies.js +72 -0
- package/dist/_shared/auto-approve/rule-strategy-registry.js +7 -0
- package/dist/_shared/index.js +5 -0
- package/dist/bootstrap.js +2 -0
- package/dist/feed/feed-search-index.js +211 -0
- package/dist/feed/feed-search-index.types.js +2 -0
- package/dist/feed/simply-feed-manager.js +92 -58
- package/dist/mcp/agents/invoke-agent.js +10 -7
- package/dist/mcp/tasks/get-task-result.js +4 -3
- package/dist/mcp/tasks/get-task.js +1 -1
- package/dist/mcp/tasks/tasks-mcp-server-utils.js +25 -26
- package/dist/mcp/tasks/tasks-mcp-server.js +1 -1
- package/dist/public/assets/{architectureDiagram-3BPJPVTR-Bt4tuNX8.js → architectureDiagram-3BPJPVTR-9PO2Zi5j.js} +1 -1
- package/dist/public/assets/{chunk-727SXJPM-BjjXmWC5.js → chunk-727SXJPM-BmaCn-6B.js} +1 -1
- package/dist/public/assets/{chunk-AQP2D5EJ-DjJ_-SOd.js → chunk-AQP2D5EJ-Cca-zKhj.js} +1 -1
- package/dist/public/assets/{classDiagram-v2-Q7XG4LA2-DVPqtO0x.js → classDiagram-4FO5ZUOK-B_zdrMg0.js} +1 -1
- package/dist/public/assets/{classDiagram-4FO5ZUOK-DVPqtO0x.js → classDiagram-v2-Q7XG4LA2-B_zdrMg0.js} +1 -1
- package/dist/public/assets/{dagre-BM42HDAG-CZBf8H9_.js → dagre-BM42HDAG-CRiqaSzz.js} +1 -1
- package/dist/public/assets/{diagram-2AECGRRQ-C_U6a4go.js → diagram-2AECGRRQ-BRpLjqbq.js} +1 -1
- package/dist/public/assets/{diagram-5GNKFQAL-Bk-3tHvA.js → diagram-5GNKFQAL-Bc9v-jIT.js} +1 -1
- package/dist/public/assets/{diagram-LMA3HP47-JxZtWEor.js → diagram-LMA3HP47-BML6fr2x.js} +1 -1
- package/dist/public/assets/{diagram-OG6HWLK6-BJ1F3Sxu.js → diagram-OG6HWLK6-Czbcayu-.js} +1 -1
- package/dist/public/assets/{erDiagram-TEJ5UH35-BJBHxTf8.js → erDiagram-TEJ5UH35-DWxHYHOI.js} +1 -1
- package/dist/public/assets/{flowDiagram-I6XJVG4X-DJ3nm5z7.js → flowDiagram-I6XJVG4X-ZS40gCP9.js} +1 -1
- package/dist/public/assets/index-Bf-eiYs6.js +506 -0
- package/dist/public/assets/index-DOeMoLVc.css +1 -0
- package/dist/public/assets/{infoDiagram-5YYISTIA-Cc1k7k1c.js → infoDiagram-5YYISTIA-cREQjSEN.js} +1 -1
- package/dist/public/assets/{ishikawaDiagram-YF4QCWOH-DrDraHBD.js → ishikawaDiagram-YF4QCWOH-BGHPisrV.js} +1 -1
- package/dist/public/assets/{kanban-definition-UN3LZRKU-CNUMfAVC.js → kanban-definition-UN3LZRKU-C-2qMHn6.js} +1 -1
- package/dist/public/assets/{mindmap-definition-RKZ34NQL-DOeL-Iu4.js → mindmap-definition-RKZ34NQL-gLns4iF7.js} +1 -1
- package/dist/public/assets/{pieDiagram-4H26LBE5-DCP9wuZK.js → pieDiagram-4H26LBE5-Da06yraI.js} +1 -1
- package/dist/public/assets/{requirementDiagram-4Y6WPE33-7UzZpq-C.js → requirementDiagram-4Y6WPE33-BsSvQc8s.js} +1 -1
- package/dist/public/assets/{sequenceDiagram-3UESZ5HK-D5alEu9E.js → sequenceDiagram-3UESZ5HK-CxdEFAWz.js} +1 -1
- package/dist/public/assets/{stateDiagram-AJRCARHV-EkSeb-RA.js → stateDiagram-AJRCARHV-itM-qY8o.js} +1 -1
- package/dist/public/assets/{stateDiagram-v2-BHNVJYJU-DmpUDd8s.js → stateDiagram-v2-BHNVJYJU-CSRZkocn.js} +1 -1
- package/dist/public/assets/{timeline-definition-PNZ67QCA-B4oLmMT1.js → timeline-definition-PNZ67QCA-BUxAMoDy.js} +1 -1
- package/dist/public/assets/{vennDiagram-CIIHVFJN-DgiGQ7rE.js → vennDiagram-CIIHVFJN-CtO4Fn4j.js} +1 -1
- package/dist/public/assets/{wardleyDiagram-YWT4CUSO-dDWLWZoc.js → wardleyDiagram-YWT4CUSO-BewTtRo4.js} +1 -1
- package/dist/public/assets/{xychartDiagram-2RQKCTM6-TOELm0t1.js → xychartDiagram-2RQKCTM6-DjHvRmZM.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/routines/inter-agent-task-routine.js +8 -11
- package/dist/runner/claude-code-agent-runner.js +18 -10
- package/dist/runner/github-copilot-agent-runner.js +21 -23
- package/dist/services/agent-registry.js +15 -5
- package/dist/services/runtime/agent-runtime-manager.js +2 -2
- package/package.json +5 -2
- package/dist/public/assets/index-BJmLehUe.js +0 -505
- 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,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
|
package/dist/_shared/index.js
CHANGED
|
@@ -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
|