crow-central-agency 0.27.0 → 0.27.2

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 (51) hide show
  1. package/README.md +1 -0
  2. package/dist/_shared/auto-approve/auto-approve-rule-set.js +3 -2
  3. package/dist/_shared/auto-approve/command-decomposition.js +47 -63
  4. package/dist/_shared/auto-approve/rule-strategies.js +10 -8
  5. package/dist/_shared/index.js +3 -2
  6. package/dist/_shared/schemas/graph.schema.js +8 -0
  7. package/dist/_shared/utils/mcp-utils.js +4 -0
  8. package/dist/bootstrap.js +1 -1
  9. package/dist/config/env.js +1 -0
  10. package/dist/feed/simply-feed-manager.js +1 -2
  11. package/dist/mcp/artifacts/write-artifact.js +3 -0
  12. package/dist/mcp/artifacts/write-circle-artifact.js +3 -0
  13. package/dist/mcp/crow-mcp-manager.js +2 -5
  14. package/dist/public/assets/{architectureDiagram-3BPJPVTR-B81-2nJq.js → architectureDiagram-3BPJPVTR-CClJU70x.js} +1 -1
  15. package/dist/public/assets/{chunk-727SXJPM-CA5zpRF4.js → chunk-727SXJPM-BAondkhN.js} +1 -1
  16. package/dist/public/assets/{chunk-AQP2D5EJ-BfWilfr3.js → chunk-AQP2D5EJ-kM5I-WKN.js} +1 -1
  17. package/dist/public/assets/{classDiagram-v2-Q7XG4LA2-hSZP-zK7.js → classDiagram-4FO5ZUOK-Dgr7qiRI.js} +1 -1
  18. package/dist/public/assets/{classDiagram-4FO5ZUOK-hSZP-zK7.js → classDiagram-v2-Q7XG4LA2-Dgr7qiRI.js} +1 -1
  19. package/dist/public/assets/{dagre-BM42HDAG-Du7tE18Y.js → dagre-BM42HDAG-DKTzSm29.js} +1 -1
  20. package/dist/public/assets/{diagram-2AECGRRQ-BtisML-9.js → diagram-2AECGRRQ-5PrRefQL.js} +1 -1
  21. package/dist/public/assets/{diagram-5GNKFQAL-4XFQLBWU.js → diagram-5GNKFQAL-DKTGe-Ql.js} +1 -1
  22. package/dist/public/assets/{diagram-LMA3HP47-CADs130y.js → diagram-LMA3HP47-Bb2RaPM2.js} +1 -1
  23. package/dist/public/assets/{diagram-OG6HWLK6-B0wqIPgk.js → diagram-OG6HWLK6-B-tad09d.js} +1 -1
  24. package/dist/public/assets/{erDiagram-TEJ5UH35-yQjnWqYr.js → erDiagram-TEJ5UH35-BL7n_6V6.js} +1 -1
  25. package/dist/public/assets/{flowDiagram-I6XJVG4X-DQyaPhc_.js → flowDiagram-I6XJVG4X-BLdaPh9i.js} +1 -1
  26. package/dist/public/assets/index-YTL1DK3j.css +1 -0
  27. package/dist/public/assets/index-uXjCHDqs.js +506 -0
  28. package/dist/public/assets/{infoDiagram-5YYISTIA-BDbevxAp.js → infoDiagram-5YYISTIA-CW9DCQBr.js} +1 -1
  29. package/dist/public/assets/{ishikawaDiagram-YF4QCWOH-Bsu77_Yo.js → ishikawaDiagram-YF4QCWOH-CwgrER31.js} +1 -1
  30. package/dist/public/assets/{kanban-definition-UN3LZRKU-f64whSnz.js → kanban-definition-UN3LZRKU-D1TuBRHr.js} +1 -1
  31. package/dist/public/assets/{mindmap-definition-RKZ34NQL-DfWnXOfI.js → mindmap-definition-RKZ34NQL-BtOi3_hU.js} +1 -1
  32. package/dist/public/assets/{pieDiagram-4H26LBE5-Bqg7-q_N.js → pieDiagram-4H26LBE5-DDo-JQZU.js} +1 -1
  33. package/dist/public/assets/{requirementDiagram-4Y6WPE33-cip1_k9k.js → requirementDiagram-4Y6WPE33-ghmfv1SX.js} +1 -1
  34. package/dist/public/assets/{sequenceDiagram-3UESZ5HK-CheQJr2g.js → sequenceDiagram-3UESZ5HK-D9BnXt7s.js} +1 -1
  35. package/dist/public/assets/{stateDiagram-AJRCARHV-an7e6llz.js → stateDiagram-AJRCARHV-BfaEy_5w.js} +1 -1
  36. package/dist/public/assets/{stateDiagram-v2-BHNVJYJU-BNydHOrt.js → stateDiagram-v2-BHNVJYJU-CTwOziWy.js} +1 -1
  37. package/dist/public/assets/{timeline-definition-PNZ67QCA-DoYJE76o.js → timeline-definition-PNZ67QCA-pAl_71QE.js} +1 -1
  38. package/dist/public/assets/{vennDiagram-CIIHVFJN-CtoH0FT3.js → vennDiagram-CIIHVFJN-DzbKt_lu.js} +1 -1
  39. package/dist/public/assets/{wardleyDiagram-YWT4CUSO-BgAGL7w8.js → wardleyDiagram-YWT4CUSO-CxEPL2m6.js} +1 -1
  40. package/dist/public/assets/{xychartDiagram-2RQKCTM6-B8gJqy0A.js → xychartDiagram-2RQKCTM6-B9a3Jq4J.js} +1 -1
  41. package/dist/public/index.html +2 -2
  42. package/dist/routes/fragment.routes.js +5 -0
  43. package/dist/routes/graph.routes.js +34 -6
  44. package/dist/runner/claude-code-agent-runner.js +1 -0
  45. package/dist/runner/github-copilot-agent-runner.js +44 -21
  46. package/dist/runner/github-copilot-permission.js +50 -0
  47. package/dist/services/relationship-manager.js +48 -6
  48. package/dist/utils/logger.js +2 -2
  49. package/package.json +1 -1
  50. package/dist/public/assets/index-BX-BOG4C.js +0 -506
  51. package/dist/public/assets/index-TfuJyPCv.css +0 -1
package/README.md CHANGED
@@ -95,6 +95,7 @@ See `.env.example` for the full list, including:
95
95
  - `HOST` / `PORT` — server bind address (defaults: `localhost:3101`). Keep `HOST=localhost` and front the server with a secure tunnel for remote access rather than binding to `0.0.0.0`.
96
96
  - `CORS_ORIGINS` — only needed when the frontend is served from a different origin (e.g. during frontend dev). Single-box deployments can leave it unset.
97
97
  - `LOG_LEVEL` — log verbosity (defaults to `debug` in development, `info` otherwise).
98
+ - `LOG_PRETTY` — force human-readable, colorized logs (pino-pretty) even in production. Enabled automatically in development.
98
99
  - `CROW_SYSTEM_PATH` — directory for Crow's file-based storage. Defaults to `~/.crow`.
99
100
  - `CROW_SYSTEM_AGENT_NAME` — display name for the built-in Crow system agent (default: `Crow`).
100
101
  - `CROW_SYSTEM_AGENT_PROVIDER` — provider backing the built-in system agents: `claude` or `copilot` (default: `claude`). Forced to `claude` when `DISABLE_GITHUB_COPILOT` is set.
@@ -1,3 +1,4 @@
1
+ import { SUBCOMMAND_MATCH_MODE } from "./command-decomposition.js";
1
2
  import { parseRule } from "./rule-format.js";
2
3
  import { getRuleStrategy } from "./rule-strategy-registry.js";
3
4
  /**
@@ -23,8 +24,8 @@ export class AutoApproveRuleSet {
23
24
  }
24
25
  }
25
26
  }
26
- matches(toolName, input) {
27
- return getRuleStrategy(toolName).matches(toolName, input, this.parsed);
27
+ matches(toolName, input, mode = SUBCOMMAND_MATCH_MODE.ALL) {
28
+ return getRuleStrategy(toolName).matches(toolName, input, this.parsed, mode);
28
29
  }
29
30
  }
30
31
  //# sourceMappingURL=auto-approve-rule-set.js.map
@@ -1,6 +1,14 @@
1
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;
2
+ /** Number of non-flag tokens a derived prefix keeps before wildcarding the rest. */
3
+ export const DEFAULT_PREFIX_DEPTH = 3;
4
+ /**
5
+ * How a compound command aggregates per-subcommand matches. `ALL` (approve) requires every
6
+ * subcommand to match a specifier; `ANY` (deny) fires when a single subcommand matches.
7
+ */
8
+ export const SUBCOMMAND_MATCH_MODE = {
9
+ ALL: "all",
10
+ ANY: "any",
11
+ };
4
12
  /** Maximum number of rules derived from a single compound command, mirroring the SDK cap. */
5
13
  export const MAX_DERIVED_RULES = 5;
6
14
  /**
@@ -16,29 +24,7 @@ export const SINGLE_CHAR_SEPARATORS = [";", "|", "&", "\n", "\r"];
16
24
  export const PROCESS_WRAPPERS = new Set(["timeout", "time", "nice", "nohup", "stdbuf"]);
17
25
  const WRAPPERS_WITH_NUMERIC_ARG = new Set(["timeout", "nice"]);
18
26
  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"]);
27
+ const OPTION_PREFIX = "-";
42
28
  const SINGLE_QUOTE = "'";
43
29
  const DOUBLE_QUOTE = '"';
44
30
  const REDIRECT_CHAR = ">";
@@ -109,7 +95,7 @@ function stripWrappers(tokens) {
109
95
  if (head === XARGS_WRAPPER) {
110
96
  // Only bare `xargs` (no flags) is transparent; flagged xargs stays and simply won't match.
111
97
  const next = remaining[1];
112
- if (next !== undefined && next.startsWith("-")) {
98
+ if (next !== undefined && next.startsWith(OPTION_PREFIX)) {
113
99
  break;
114
100
  }
115
101
  remaining = remaining.slice(1);
@@ -119,7 +105,7 @@ function stripWrappers(tokens) {
119
105
  break;
120
106
  }
121
107
  remaining = remaining.slice(1);
122
- while (remaining.length > 0 && remaining[0].startsWith("-")) {
108
+ while (remaining.length > 0 && remaining[0].startsWith(OPTION_PREFIX)) {
123
109
  remaining = remaining.slice(1);
124
110
  }
125
111
  if (WRAPPERS_WITH_NUMERIC_ARG.has(head) && remaining.length > 0 && /^[0-9]/.test(remaining[0])) {
@@ -174,26 +160,31 @@ export function splitSubcommands(command) {
174
160
  }
175
161
  return subcommands;
176
162
  }
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);
163
+ function derivePrefixTokens(tokens, depth) {
164
+ const prefix = [];
165
+ let counted = 0;
166
+ let index = 0;
167
+ while (index < tokens.length && counted < depth) {
168
+ const token = tokens[index];
169
+ if (token.startsWith(OPTION_PREFIX)) {
170
+ prefix.push(token);
171
+ index += 1;
172
+ if (index < tokens.length && !tokens[index].startsWith(OPTION_PREFIX)) {
173
+ prefix.push(tokens[index]);
174
+ index += 1;
175
+ }
176
+ continue;
177
+ }
178
+ prefix.push(token);
179
+ counted += 1;
180
+ index += 1;
191
181
  }
192
- return READ_ONLY_BUILTINS.has(head);
182
+ return counted === 0 ? undefined : prefix;
193
183
  }
194
184
  /**
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}.
185
+ * Capture side: derive one flag-aware prefix specifier per subcommand, deduped and capped at
186
+ * {@link MAX_DERIVED_RULES}. A subcommand with no non-flag token (e.g. a lone `--flag`) contributes
187
+ * nothing. User config is the only auto-approve authority — there is no read-only skip.
197
188
  */
198
189
  export function deriveRules(command, depth = DEFAULT_PREFIX_DEPTH) {
199
190
  const specifiers = [];
@@ -201,15 +192,11 @@ export function deriveRules(command, depth = DEFAULT_PREFIX_DEPTH) {
201
192
  if (specifiers.length >= MAX_DERIVED_RULES) {
202
193
  break;
203
194
  }
204
- if (isReadOnlyCommand(subcommand)) {
205
- continue;
206
- }
207
- const tokens = tokenize(subcommand);
208
- if (tokens.length === 0) {
195
+ const prefixTokens = derivePrefixTokens(tokenize(subcommand), depth);
196
+ if (prefixTokens === undefined) {
209
197
  continue;
210
198
  }
211
- const prefix = tokens.slice(0, depth).join(" ");
212
- const specifier = `${prefix}${WORD_BOUNDARY_SUFFIX}`;
199
+ const specifier = `${prefixTokens.join(" ")}${WORD_BOUNDARY_SUFFIX}`;
213
200
  if (!specifiers.includes(specifier)) {
214
201
  specifiers.push(specifier);
215
202
  }
@@ -217,22 +204,19 @@ export function deriveRules(command, depth = DEFAULT_PREFIX_DEPTH) {
217
204
  return specifiers;
218
205
  }
219
206
  /**
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`.
207
+ * Match side: fails closed. Each raw subcommand is matched against the specifiers via literal-prefix
208
+ * `matchesSpecifier` (no flag logic on this side). `ALL` (approve) requires every subcommand to
209
+ * match; `ANY` (deny) fires when a single subcommand matches. Empty/unparseable input yields `false`
210
+ * in both modes.
222
211
  */
223
- export function matchesRules(command, specifiers) {
212
+ export function matchesRules(command, specifiers, mode = SUBCOMMAND_MATCH_MODE.ALL) {
224
213
  const subcommands = splitSubcommands(command);
225
214
  if (subcommands.length === 0) {
226
215
  return false;
227
216
  }
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;
217
+ const subcommandMatches = (subcommand) => specifiers.some((specifier) => matchesSpecifier(subcommand, specifier));
218
+ return mode === SUBCOMMAND_MATCH_MODE.ANY
219
+ ? subcommands.some(subcommandMatches)
220
+ : subcommands.every(subcommandMatches);
237
221
  }
238
222
  //# sourceMappingURL=command-decomposition.js.map
@@ -12,20 +12,22 @@ const COMMAND_TOOL_BASE_NAMES = new Set([
12
12
  CLAUDE_CODE_TOOL.POWER_SHELL.toLowerCase(),
13
13
  ]);
14
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.
15
+ * Whole-tool matching: case-insensitive exact tool-name match, plus a trailing `*` (including a bare
16
+ * `*` and MCP prefixes like `mcp__crow-artifacts__*`) as a prefix match. Case-insensitivity aligns
17
+ * with the command strategy (which lowercases the tool name) so a `Bash` rule covers Copilot's
18
+ * `bash`, for both allow and deny.
18
19
  */
19
20
  function wholeToolMatches(toolName, rules) {
21
+ const targetTool = toolName.toLowerCase();
20
22
  for (const rule of rules) {
21
23
  if (rule.specifier !== undefined) {
22
24
  continue;
23
25
  }
24
- const pattern = rule.tool;
25
- if (pattern === toolName) {
26
+ const pattern = rule.tool.toLowerCase();
27
+ if (pattern === targetTool) {
26
28
  return true;
27
29
  }
28
- if (pattern.endsWith(GLOB_STAR) && toolName.startsWith(pattern.slice(0, -GLOB_STAR.length))) {
30
+ if (pattern.endsWith(GLOB_STAR) && targetTool.startsWith(pattern.slice(0, -GLOB_STAR.length))) {
29
31
  return true;
30
32
  }
31
33
  }
@@ -51,7 +53,7 @@ export const commandRuleStrategy = {
51
53
  }
52
54
  return deriveCommandRules(command).map((specifier) => formatRule({ tool: toolName, specifier }));
53
55
  },
54
- matches: (toolName, input, rules) => {
56
+ matches: (toolName, input, rules, mode) => {
55
57
  if (wholeToolMatches(toolName, rules)) {
56
58
  return true;
57
59
  }
@@ -66,7 +68,7 @@ export const commandRuleStrategy = {
66
68
  specifiers.push(rule.specifier);
67
69
  }
68
70
  }
69
- return matchesCommandRules(command, specifiers);
71
+ return matchesCommandRules(command, specifiers, mode);
70
72
  },
71
73
  };
72
74
  //# sourceMappingURL=rule-strategies.js.map
@@ -24,11 +24,12 @@ export { CreateFragmentAssociationInputSchema, FRAGMENT_KIND, FRAGMENT_FIRST_LEV
24
24
  export { REFLECTION_AGENT_REF, REFLECTION_TEMP_PREFIX, REFLECTION_OP, ReflectionOpSchema, ReflectionPlanSchema, } from "./schemas/fragment-reflection.schema.js";
25
25
  export { MESSAGE_SOURCE_TYPE, MessageSourceSchema, } from "./schemas/message-source.schema.js";
26
26
  export { AGENT_COMMAND, AgentCommandSchema } from "./schemas/agent-command.schema.js";
27
- export { GraphNodeSchema, GraphEdgeSchema, GraphDataSchema, } from "./schemas/graph.schema.js";
27
+ export { GraphNodePositionSchema, GraphNodeSchema, GraphEdgeSchema, GraphDataSchema, } from "./schemas/graph.schema.js";
28
28
  export { ConnectorConnectionSchema, ConnectorInfoSchema, ConnectConnectorResponseSchema, } from "./schemas/connector.schema.js";
29
29
  export { parseRule, parseRules, formatRule, matchesSpecifier, GLOB_STAR, WORD_BOUNDARY_SUFFIX, PREFIX_STAR_SUFFIX, } from "./auto-approve/rule-format.js";
30
- export { splitSubcommands, deriveRules, matchesRules, isReadOnlyCommand, DEFAULT_PREFIX_DEPTH, MAX_DERIVED_RULES, READ_ONLY_BUILTINS, PROCESS_WRAPPERS, } from "./auto-approve/command-decomposition.js";
30
+ export { splitSubcommands, deriveRules, matchesRules, DEFAULT_PREFIX_DEPTH, MAX_DERIVED_RULES, PROCESS_WRAPPERS, SUBCOMMAND_MATCH_MODE, } from "./auto-approve/command-decomposition.js";
31
31
  export { defaultRuleStrategy, commandRuleStrategy } from "./auto-approve/rule-strategies.js";
32
32
  export { getRuleStrategy } from "./auto-approve/rule-strategy-registry.js";
33
33
  export { AutoApproveRuleSet } from "./auto-approve/auto-approve-rule-set.js";
34
+ export { normalizeMcpName } from "./utils/mcp-utils.js";
34
35
  //# sourceMappingURL=index.js.map
@@ -2,6 +2,12 @@ import { z } from "zod";
2
2
  import { EntityTypeSchema, RelationshipTypeSchema } from "./agent-circle.schema.js";
3
3
  import { AGENT_STATUS } from "./agent-runtime-state.schema.js";
4
4
  import { FragmentKindSchema } from "./fragment.schema.js";
5
+ /** A user-authored layout position for a graph node; `id` is the node's entity id */
6
+ export const GraphNodePositionSchema = z.object({
7
+ id: z.string(),
8
+ x: z.number(),
9
+ y: z.number(),
10
+ });
5
11
  /** A node in the relationship graph — represents an agent, a circle, or a fragment */
6
12
  export const GraphNodeSchema = z.object({
7
13
  id: z.string(),
@@ -14,6 +20,8 @@ export const GraphNodeSchema = z.object({
14
20
  status: z
15
21
  .enum([AGENT_STATUS.IDLE, AGENT_STATUS.ACTIVATING, AGENT_STATUS.STREAMING, AGENT_STATUS.COMPACTING])
16
22
  .optional(),
23
+ x: z.number().optional(),
24
+ y: z.number().optional(),
17
25
  });
18
26
  /** An edge in the relationship graph — represents a membership relationship */
19
27
  export const GraphEdgeSchema = z.object({
@@ -0,0 +1,4 @@
1
+ export function normalizeMcpName(name) {
2
+ return name.toLowerCase().replaceAll(" ", "_");
3
+ }
4
+ //# sourceMappingURL=mcp-utils.js.map
package/dist/bootstrap.js CHANGED
@@ -181,7 +181,7 @@ export async function bootstrap(options) {
181
181
  await registerSensorRoutes(server, sensorManager);
182
182
  await registerCircleRoutes(server, circleManager, registry);
183
183
  await registerFragmentRoutes(server, fragmentManager, registry);
184
- await registerGraphRoutes(server, circleManager, registry, runtimeManager, fragmentManager);
184
+ await registerGraphRoutes(server, circleManager, registry, runtimeManager, fragmentManager, relationshipManager);
185
185
  await registerFeedRoutes(server, feedManager);
186
186
  await registerSystemSettingsRoutes(server, systemSettingsManager);
187
187
  await registerConnectorsRoutes(server, connectorManager);
@@ -48,6 +48,7 @@ export const env = {
48
48
  NODE_ENV: nodeEnv,
49
49
  IS_DEV: isDev,
50
50
  LOG_LEVEL: getOptional("LOG_LEVEL") ?? (isDev ? "debug" : "info"),
51
+ LOG_PRETTY: getBoolean("LOG_PRETTY"),
51
52
  HOST: host,
52
53
  PORT: port,
53
54
  CORS_ORIGINS: corsOrigins.split(",").map((origin) => origin.trim()),
@@ -446,8 +446,7 @@ export class SimplyFeedManager extends EventBus {
446
446
  const feeds = await this.getFeeds();
447
447
  for (const feed of feeds) {
448
448
  try {
449
- const items = await this.refreshFeed(feed.id, false);
450
- log.info({ feedId: feed.id, feedName: feed.title, newItemsCount: items.length }, "Refreshed feed");
449
+ await this.refreshFeed(feed.id, false);
451
450
  }
452
451
  catch (error) {
453
452
  log.error({ feedId: feed.id, feedName: feed.title, error }, "Failed to refresh feed");
@@ -27,6 +27,9 @@ export function getWriteArtifactToolConfig(agentId, artifactManager, sensorManag
27
27
  const filename = rawFilename.trim();
28
28
  try {
29
29
  const isBinary = content_type && content_type !== ARTIFACT_CONTENT_TYPE.TEXT;
30
+ if (!isBinary && !content.trim()) {
31
+ throw new Error("Artifact content must not be empty. Provide content, or use delete_artifact if you meant to remove it.");
32
+ }
30
33
  const artifactContent = isBinary ? Buffer.from(content, "base64") : content;
31
34
  const metadata = await artifactManager.writeArtifact(agentId, filename, artifactContent, {
32
35
  type,
@@ -31,6 +31,9 @@ export function getWriteCircleArtifactToolConfig(agentId, artifactManager, senso
31
31
  const filename = rawFilename.trim();
32
32
  try {
33
33
  const isBinary = content_type && content_type !== ARTIFACT_CONTENT_TYPE.TEXT;
34
+ if (!isBinary && !content.trim()) {
35
+ throw new Error("Artifact content must not be empty. Provide content, or use delete_circle_artifact if you meant to remove it.");
36
+ }
34
37
  const artifactContent = isBinary ? Buffer.from(content, "base64") : content;
35
38
  const metadata = await artifactManager.writeCircleArtifact(circle_id, filename, artifactContent, {
36
39
  type,
@@ -1,4 +1,4 @@
1
- import { McpServerConfigSchema, CreateMcpConfigInputSchema, UpdateMcpConfigInputSchema, MCP_CONFIG_TYPE, CROW_SYSTEM_AGENT_ID, AGENT_TYPE, } from "../_shared/index.js";
1
+ import { McpServerConfigSchema, CreateMcpConfigInputSchema, UpdateMcpConfigInputSchema, MCP_CONFIG_TYPE, CROW_SYSTEM_AGENT_ID, AGENT_TYPE, normalizeMcpName, } from "../_shared/index.js";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { AppError } from "../core/error/app-error.js";
4
4
  import { APP_ERROR_CODES } from "../core/error/app-error.types.js";
@@ -79,7 +79,7 @@ export class CrowMcpManager {
79
79
  return (isCrowSystemAgent(agentId) && config.enableForCrow) || configuredMcpIds.has(config.id);
80
80
  });
81
81
  for (const config of userMcpConfigs) {
82
- const name = this.normalizeMcpName(config.name);
82
+ const name = normalizeMcpName(config.name);
83
83
  if (serverConfigMap.has(name)) {
84
84
  log.warn({ configId: config.id, name }, "User MCP config name collides with an internal server, skipping");
85
85
  continue;
@@ -194,9 +194,6 @@ export class CrowMcpManager {
194
194
  await this.store.delete(MCP_CONFIG_STORE_TABLE, configId);
195
195
  log.info({ configId, name: existing.name }, "MCP config deleted");
196
196
  }
197
- normalizeMcpName(name) {
198
- return name.toLowerCase().replaceAll(" ", "_");
199
- }
200
197
  /** Map a persisted user MCP config to a Crow MCP transport. */
201
198
  toTransport(config) {
202
199
  if (config.type === MCP_CONFIG_TYPE.STDIO) {