crow-central-agency 0.25.13 → 0.25.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 (49) hide show
  1. package/dist/_shared/schemas/artifact.schema.js +1 -0
  2. package/dist/bootstrap.js +1 -0
  3. package/dist/core/error/app-error.types.js +1 -0
  4. package/dist/mcp/artifacts/artifacts-mcp-server-utils.js +70 -1
  5. package/dist/mcp/artifacts/artifacts-mcp-server.js +14 -0
  6. package/dist/mcp/artifacts/delete-circle-artifact.js +1 -1
  7. package/dist/mcp/artifacts/edit-artifact.js +66 -0
  8. package/dist/mcp/artifacts/edit-circle-artifact.js +70 -0
  9. package/dist/mcp/artifacts/find-content-in-artifact.js +37 -0
  10. package/dist/mcp/artifacts/find-content-in-circle-artifact.js +41 -0
  11. package/dist/mcp/artifacts/list-artifacts.js +16 -5
  12. package/dist/mcp/artifacts/list-circle-artifacts.js +15 -4
  13. package/dist/mcp/artifacts/read-artifact.js +3 -4
  14. package/dist/mcp/artifacts/read-circle-artifact.js +2 -3
  15. package/dist/mcp/artifacts/write-artifact.js +9 -4
  16. package/dist/mcp/artifacts/write-circle-artifact.js +11 -6
  17. package/dist/mcp/tool-utils.js +1 -1
  18. package/dist/public/assets/{architectureDiagram-3BPJPVTR-DbWcWOBY.js → architectureDiagram-3BPJPVTR-BfGAPq2z.js} +1 -1
  19. package/dist/public/assets/{chunk-727SXJPM-ClhRzWEk.js → chunk-727SXJPM-DuZqS4MG.js} +1 -1
  20. package/dist/public/assets/{chunk-AQP2D5EJ-CYZG32-Q.js → chunk-AQP2D5EJ-Cn4xG-Ry.js} +1 -1
  21. package/dist/public/assets/{classDiagram-v2-Q7XG4LA2-D3t5sS78.js → classDiagram-4FO5ZUOK-BUl9xlUL.js} +1 -1
  22. package/dist/public/assets/{classDiagram-4FO5ZUOK-urSTQh2x.js → classDiagram-v2-Q7XG4LA2-D1CQ-gDz.js} +1 -1
  23. package/dist/public/assets/{diagram-2AECGRRQ-CsvYtH42.js → diagram-2AECGRRQ-CX8foJPq.js} +1 -1
  24. package/dist/public/assets/{diagram-5GNKFQAL-DKBa5qvt.js → diagram-5GNKFQAL-DA78CQem.js} +1 -1
  25. package/dist/public/assets/{diagram-LMA3HP47-Dn8u-OFi.js → diagram-LMA3HP47-xHMjy8aE.js} +1 -1
  26. package/dist/public/assets/{diagram-OG6HWLK6-vMG-8YUk.js → diagram-OG6HWLK6-DlRO3Ayl.js} +1 -1
  27. package/dist/public/assets/{erDiagram-TEJ5UH35-BQ9XtJBn.js → erDiagram-TEJ5UH35-CZqcN6px.js} +1 -1
  28. package/dist/public/assets/{flowDiagram-I6XJVG4X-CTSa0pWx.js → flowDiagram-I6XJVG4X-DlOZbuGG.js} +1 -1
  29. package/dist/public/assets/{index-CoWuzbp6.js → index-DmdHg7Be.js} +4 -4
  30. package/dist/public/assets/{infoDiagram-5YYISTIA-D9OLId3y.js → infoDiagram-5YYISTIA-BW8XbP1C.js} +1 -1
  31. package/dist/public/assets/{ishikawaDiagram-YF4QCWOH-DgrXdDFX.js → ishikawaDiagram-YF4QCWOH-BA-_CtvR.js} +1 -1
  32. package/dist/public/assets/{kanban-definition-UN3LZRKU-BHDPdLIR.js → kanban-definition-UN3LZRKU-C8TVdEvL.js} +1 -1
  33. package/dist/public/assets/{mindmap-definition-RKZ34NQL-BWzeH6a0.js → mindmap-definition-RKZ34NQL-DZkPdbNS.js} +1 -1
  34. package/dist/public/assets/{pieDiagram-4H26LBE5-B-Yr1GRG.js → pieDiagram-4H26LBE5-BBx1uSsh.js} +1 -1
  35. package/dist/public/assets/{requirementDiagram-4Y6WPE33-C0ukX7lT.js → requirementDiagram-4Y6WPE33-C35Fc9Mo.js} +1 -1
  36. package/dist/public/assets/{sequenceDiagram-3UESZ5HK-B9Zy5t1a.js → sequenceDiagram-3UESZ5HK-DO2aCVNa.js} +1 -1
  37. package/dist/public/assets/{stateDiagram-AJRCARHV-C5Spt8v4.js → stateDiagram-AJRCARHV-Dtf4GcS-.js} +1 -1
  38. package/dist/public/assets/{stateDiagram-v2-BHNVJYJU-BirPm6MH.js → stateDiagram-v2-BHNVJYJU-BWXbQBcB.js} +1 -1
  39. package/dist/public/assets/{timeline-definition-PNZ67QCA--qBnNF5F.js → timeline-definition-PNZ67QCA-ar2DXF9S.js} +1 -1
  40. package/dist/public/assets/{vennDiagram-CIIHVFJN-IE4AXUoa.js → vennDiagram-CIIHVFJN-B7tt3Tsv.js} +1 -1
  41. package/dist/public/assets/{wardleyDiagram-YWT4CUSO-DHgyly1b.js → wardleyDiagram-YWT4CUSO-CdG42xCG.js} +1 -1
  42. package/dist/public/assets/{xychartDiagram-2RQKCTM6-uC6o17p1.js → xychartDiagram-2RQKCTM6-CMAXWBmj.js} +1 -1
  43. package/dist/public/index.html +1 -1
  44. package/dist/routes/artifact.routes.js +2 -4
  45. package/dist/server/error-handler.js +1 -0
  46. package/dist/services/artifact/artifact-manager.js +119 -9
  47. package/dist/services/artifact/artifact-tags.js +9 -0
  48. package/dist/services/runtime/agent-runtime-manager.js +10 -4
  49. package/package.json +3 -3
@@ -43,6 +43,7 @@ export const ArtifactMetadataSchema = z.object({
43
43
  entityId: z.string(),
44
44
  entityType: EntityTypeSchema,
45
45
  size: z.number(),
46
+ tags: z.array(z.string()).optional(),
46
47
  createdTimestamp: z.number(),
47
48
  updatedTimestamp: z.number(),
48
49
  createdBy: AgentTaskSourceSchema,
package/dist/bootstrap.js CHANGED
@@ -147,6 +147,7 @@ export async function bootstrap(options) {
147
147
  isConfigurable: googleContactsMcpDefinition.isConfigurable,
148
148
  displayName: googleContactsMcpDefinition.displayName,
149
149
  });
150
+ await runtimeManager.startRecovery();
150
151
  // Start scheduler
151
152
  crowScheduler.start();
152
153
  // Create Fastify server
@@ -18,6 +18,7 @@ export const APP_ERROR_CODES = {
18
18
  INVALID_STATE_TRANSITION: "invalid_state_transition",
19
19
  PATH_TRAVERSAL: "path_traversal",
20
20
  INVALID_FILENAME: "invalid_filename",
21
+ CONFLICT: "conflict",
21
22
  MCP_CONFIG_NOT_FOUND: "mcp_config_not_found",
22
23
  MCP_ERROR: "mcp_error",
23
24
  SDK_ERROR: "sdk_error",
@@ -2,8 +2,50 @@ import path from "node:path";
2
2
  import { ARTIFACT_CONTENT_TYPE, ARTIFACT_TYPE } from "../../_shared/index.js";
3
3
  import { formatLocalDateTime } from "../../utils/date-utils.js";
4
4
  import { processTextContent, textToolResult } from "../tool-utils.js";
5
+ import { last } from "es-toolkit";
5
6
  export const ARTIFACT_TYPE_VALUES = Object.values(ARTIFACT_TYPE);
6
7
  export const ARTIFACT_CONTENT_TYPE_VALUES = Object.values(ARTIFACT_CONTENT_TYPE);
8
+ export const EDIT_ARTIFACT_MODE = {
9
+ INSERT: "insert",
10
+ REPLACE: "replace",
11
+ };
12
+ export const EDIT_ARTIFACT_MODE_VALUES = Object.values(EDIT_ARTIFACT_MODE);
13
+ /**
14
+ * Apply an insert/replace line edit. Lines are 1-based; endLine is inclusive (used for replace only).
15
+ * Throws when line numbers are out of range or endLine is missing/invalid for replace.
16
+ */
17
+ export function applyLineEdit(existingContent, content, mode, startLine, endLine) {
18
+ const existingLines = existingContent.split("\n");
19
+ const totalLines = existingLines.length;
20
+ if (startLine < 1) {
21
+ throw new Error("startLine must be a positive integer starting from 1.");
22
+ }
23
+ // allow adding a line after last line
24
+ if (startLine > totalLines + 1) {
25
+ throw new Error(`Starting line (${startLine}) exceeds the total number of lines (${totalLines}).`);
26
+ }
27
+ if (mode === EDIT_ARTIFACT_MODE.REPLACE) {
28
+ if (endLine === undefined) {
29
+ throw new Error("endLine is required for 'replace' mode.");
30
+ }
31
+ if (endLine > totalLines) {
32
+ throw new Error(`Ending line (${endLine}) exceeds the total number of lines (${totalLines}).`);
33
+ }
34
+ if (endLine < startLine) {
35
+ throw new Error(`Ending line (${endLine}) must be greater than or equal to starting line (${startLine}).`);
36
+ }
37
+ }
38
+ const preContent = existingLines.slice(0, startLine - 1);
39
+ const effectiveEnd = mode === EDIT_ARTIFACT_MODE.REPLACE && endLine !== undefined ? endLine : startLine - 1;
40
+ const postContent = existingLines.slice(effectiveEnd);
41
+ const newContent = content.split("\n");
42
+ const lastLine = last(newContent);
43
+ if (lastLine !== undefined && !lastLine) {
44
+ newContent.splice(-1, 1);
45
+ }
46
+ const updatedContent = preContent.concat(newContent).concat(postContent).join("\n");
47
+ return updatedContent;
48
+ }
7
49
  /** Image extensions that Claude can process natively via base64 */
8
50
  const SUPPORTED_IMAGE_MIME = {
9
51
  ".jpg": "image/jpeg",
@@ -18,8 +60,11 @@ const PDF_MIME = "application/pdf";
18
60
  export function buildReadArtifactResult(content, metadata, userTimezone, lineOptions) {
19
61
  const header = [
20
62
  `--- METADATA ---`,
21
- `[Type: ${metadata.type} | Content: ${metadata.contentType} | Modified: ${formatLocalDateTime(new Date(metadata.updatedTimestamp), userTimezone)}]`,
63
+ `[Type: ${metadata.type} | Content: ${metadata.contentType} | Modified: ${formatLocalDateTime(new Date(metadata.updatedTimestamp), userTimezone)} | Version: ${metadata.updatedTimestamp}]`,
22
64
  ];
65
+ if (metadata.tags?.length) {
66
+ header.push(`[Tags: ${metadata.tags.join(", ")}]`);
67
+ }
23
68
  if (typeof content === "string" || metadata.contentType === ARTIFACT_CONTENT_TYPE.TEXT) {
24
69
  const rawText = typeof content === "string" ? content : content.toString("utf-8");
25
70
  const processed = processTextContent(rawText, lineOptions);
@@ -55,4 +100,28 @@ export function buildReadArtifactResult(content, metadata, userTimezone, lineOpt
55
100
  `[Binary artifact: ${metadata.contentType} content (${metadata.size} bytes). This binary format is not supported for interpretation.]`,
56
101
  ]);
57
102
  }
103
+ /** Build the MCP text result for a find-content search. Dedupes by line; honors an optional limit on lines. */
104
+ export function buildFindContentResult(filename, query, result, limit) {
105
+ if (!result.found) {
106
+ return textToolResult([`No matches found for "${query}" in ${filename}.`]);
107
+ }
108
+ const seenLines = new Set();
109
+ const shownLines = [];
110
+ for (const match of result.matches) {
111
+ if (seenLines.has(match.lineNumber)) {
112
+ continue;
113
+ }
114
+ seenLines.add(match.lineNumber);
115
+ if (limit === undefined || shownLines.length < limit) {
116
+ shownLines.push(`[L${match.lineNumber}] ${match.lineContent}`);
117
+ }
118
+ }
119
+ const truncated = limit !== undefined && seenLines.size > limit;
120
+ const lines = [
121
+ `Found ${seenLines.size} matching line(s) for "${query}" in ${filename}${truncated ? ` (showing first ${limit})` : ""}:`,
122
+ "Lines are prefixed with [LNNN] markers. These markers are NOT part of the line content.",
123
+ ...shownLines,
124
+ ];
125
+ return textToolResult(lines);
126
+ }
58
127
  //# sourceMappingURL=artifacts-mcp-server-utils.js.map
@@ -1,28 +1,39 @@
1
1
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { getWriteArtifactToolConfig } from "./write-artifact.js";
3
+ import { getEditArtifactToolConfig } from "./edit-artifact.js";
3
4
  import { getReadArtifactToolConfig } from "./read-artifact.js";
4
5
  import { getListArtifactsToolConfig } from "./list-artifacts.js";
5
6
  import { getDeleteArtifactToolConfig } from "./delete-artifact.js";
7
+ import { getFindContentInArtifactToolConfig } from "./find-content-in-artifact.js";
6
8
  import { getWriteCircleArtifactToolConfig } from "./write-circle-artifact.js";
9
+ import { getEditCircleArtifactToolConfig } from "./edit-circle-artifact.js";
7
10
  import { getReadCircleArtifactToolConfig } from "./read-circle-artifact.js";
8
11
  import { getListCircleArtifactsToolConfig } from "./list-circle-artifacts.js";
9
12
  import { getDeleteCircleArtifactToolConfig } from "./delete-circle-artifact.js";
13
+ import { getFindContentInCircleArtifactToolConfig } from "./find-content-in-circle-artifact.js";
10
14
  export const ARTIFACTS_MCP_SERVER_NAME = "crow-artifacts";
11
15
  export function createArtifactsMcpServer(agentId, artifactManager, registry, circleManager, sensorManager) {
12
16
  const writeArtifact = getWriteArtifactToolConfig(agentId, artifactManager, sensorManager);
17
+ const editArtifact = getEditArtifactToolConfig(agentId, artifactManager, sensorManager);
13
18
  const readArtifact = getReadArtifactToolConfig(agentId, artifactManager, registry, circleManager, sensorManager);
14
19
  const listArtifacts = getListArtifactsToolConfig(agentId, artifactManager, registry, circleManager, sensorManager);
15
20
  const deleteArtifact = getDeleteArtifactToolConfig(agentId, artifactManager);
21
+ const findContentInArtifact = getFindContentInArtifactToolConfig(agentId, artifactManager);
16
22
  const writeCircleArtifact = getWriteCircleArtifactToolConfig(agentId, artifactManager, sensorManager);
23
+ const editCircleArtifact = getEditCircleArtifactToolConfig(agentId, artifactManager, sensorManager);
17
24
  const readCircleArtifact = getReadCircleArtifactToolConfig(agentId, artifactManager, sensorManager);
18
25
  const listCircleArtifacts = getListCircleArtifactsToolConfig(agentId, artifactManager, sensorManager);
19
26
  const deleteCircleArtifact = getDeleteCircleArtifactToolConfig(agentId, artifactManager);
27
+ const findContentInCircleArtifact = getFindContentInCircleArtifactToolConfig(agentId, artifactManager);
20
28
  return createSdkMcpServer({
21
29
  name: ARTIFACTS_MCP_SERVER_NAME,
22
30
  tools: [
23
31
  tool(writeArtifact.name, writeArtifact.description, writeArtifact.inputSchema, writeArtifact.handler, {
24
32
  annotations: writeArtifact.annotations,
25
33
  }),
34
+ tool(editArtifact.name, editArtifact.description, editArtifact.inputSchema, editArtifact.handler, {
35
+ annotations: editArtifact.annotations,
36
+ }),
26
37
  tool(readArtifact.name, readArtifact.description, readArtifact.inputSchema, readArtifact.handler, {
27
38
  annotations: readArtifact.annotations,
28
39
  }),
@@ -32,10 +43,13 @@ export function createArtifactsMcpServer(agentId, artifactManager, registry, cir
32
43
  tool(deleteArtifact.name, deleteArtifact.description, deleteArtifact.inputSchema, deleteArtifact.handler, {
33
44
  annotations: deleteArtifact.annotations,
34
45
  }),
46
+ tool(findContentInArtifact.name, findContentInArtifact.description, findContentInArtifact.inputSchema, findContentInArtifact.handler, { annotations: findContentInArtifact.annotations }),
35
47
  tool(writeCircleArtifact.name, writeCircleArtifact.description, writeCircleArtifact.inputSchema, writeCircleArtifact.handler, { annotations: writeCircleArtifact.annotations }),
48
+ tool(editCircleArtifact.name, editCircleArtifact.description, editCircleArtifact.inputSchema, editCircleArtifact.handler, { annotations: editCircleArtifact.annotations }),
36
49
  tool(readCircleArtifact.name, readCircleArtifact.description, readCircleArtifact.inputSchema, readCircleArtifact.handler, { annotations: readCircleArtifact.annotations }),
37
50
  tool(listCircleArtifacts.name, listCircleArtifacts.description, listCircleArtifacts.inputSchema, listCircleArtifacts.handler, { annotations: listCircleArtifacts.annotations }),
38
51
  tool(deleteCircleArtifact.name, deleteCircleArtifact.description, deleteCircleArtifact.inputSchema, deleteCircleArtifact.handler, { annotations: deleteCircleArtifact.annotations }),
52
+ tool(findContentInCircleArtifact.name, findContentInCircleArtifact.description, findContentInCircleArtifact.inputSchema, findContentInCircleArtifact.handler, { annotations: findContentInCircleArtifact.annotations }),
39
53
  ],
40
54
  });
41
55
  }
@@ -9,7 +9,7 @@ export function getDeleteCircleArtifactToolConfig(agentId, artifactManager) {
9
9
  };
10
10
  const handler = async ({ circle_id, filename }) => {
11
11
  if (!artifactManager.isDirectCircleMember(circle_id, agentId)) {
12
- return textToolResult(["Error: you are not a direct member of this circle"], true);
12
+ return textToolResult(["You are not a direct member of this circle."], true);
13
13
  }
14
14
  try {
15
15
  const metadata = await artifactManager.getCircleArtifactMetadata(circle_id, filename);
@@ -0,0 +1,66 @@
1
+ import { z } from "zod";
2
+ import { ARTIFACT_CONTENT_TYPE } from "../../_shared/index.js";
3
+ import { getErrorToolResult, textToolResult } from "../tool-utils.js";
4
+ import { formatLocalDateTime } from "../../utils/date-utils.js";
5
+ import { applyLineEdit, EDIT_ARTIFACT_MODE, EDIT_ARTIFACT_MODE_VALUES } from "./artifacts-mcp-server-utils.js";
6
+ export const EDIT_ARTIFACT_TOOL_NAME = "edit_artifact";
7
+ export function getEditArtifactToolConfig(agentId, artifactManager, sensorManager) {
8
+ const inputSchema = {
9
+ filename: z.string().describe("Name of an existing TEXT artifact to edit."),
10
+ mode: z
11
+ .enum(EDIT_ARTIFACT_MODE_VALUES)
12
+ .describe("Edit mode. 'insert': insert new lines before startLine. 'replace': replace lines startLine..endLine inclusive. Both require a TEXT artifact."),
13
+ content: z.string().describe("The new text to insert or use as replacement."),
14
+ startLine: z
15
+ .number()
16
+ .min(1)
17
+ .describe("1-based line number. For 'insert', new lines are placed before this line. For 'replace', this is the first line replaced."),
18
+ endLine: z
19
+ .number()
20
+ .min(1)
21
+ .optional()
22
+ .describe("1-based inclusive last line to replace. Required for 'replace' mode. Ignored for 'insert'."),
23
+ version: z
24
+ .number()
25
+ .describe("The Version value from your most recent read of this artifact (the [Version: ...] token)."),
26
+ addTags: z.array(z.string()).optional().describe("Optional tags to add to the artifact."),
27
+ removeTags: z.array(z.string()).optional().describe("Optional tags to remove from the artifact."),
28
+ };
29
+ const handler = async ({ filename: rawFilename, content, mode, startLine, endLine, version, addTags, removeTags, }) => {
30
+ const filename = rawFilename.trim();
31
+ try {
32
+ if (mode === EDIT_ARTIFACT_MODE.REPLACE && endLine === undefined) {
33
+ throw new Error("endLine is required for 'replace' mode.");
34
+ }
35
+ const { content: existingContent, metadata } = await artifactManager.readArtifact(agentId, filename);
36
+ if (metadata.contentType !== ARTIFACT_CONTENT_TYPE.TEXT || typeof existingContent !== "string") {
37
+ throw new Error(`edit_artifact only supports TEXT artifacts (${metadata.filename} is ${metadata.contentType}). Use write_artifact to replace the file.`);
38
+ }
39
+ const nextContent = applyLineEdit(existingContent, content, mode, startLine, endLine);
40
+ const updated = await artifactManager.updateArtifact(agentId, filename, {
41
+ content: nextContent,
42
+ addTags,
43
+ removeTags,
44
+ expectedUpdatedTimestamp: version,
45
+ });
46
+ const userTimezone = await sensorManager.getUserTimezone();
47
+ const editNote = mode === EDIT_ARTIFACT_MODE.REPLACE
48
+ ? `replaced lines ${startLine}-${endLine}`
49
+ : `inserted at line ${startLine}`;
50
+ return textToolResult([
51
+ `Artifact edited: ${updated.filename} (${editNote}, size: ${updated.size} bytes, modified: ${formatLocalDateTime(new Date(updated.updatedTimestamp), userTimezone)}). Re-read the artifact before the next edit; line numbers and Version are now stale.`,
52
+ ]);
53
+ }
54
+ catch (error) {
55
+ return getErrorToolResult(error, "Failed to edit artifact.");
56
+ }
57
+ };
58
+ const config = {
59
+ name: EDIT_ARTIFACT_TOOL_NAME,
60
+ description: "Surgically modify a TEXT artifact you own by inserting or replacing a range of lines. Use write_artifact to replace the full file or to write non-TEXT content.",
61
+ inputSchema,
62
+ handler,
63
+ };
64
+ return config;
65
+ }
66
+ //# sourceMappingURL=edit-artifact.js.map
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { ARTIFACT_CONTENT_TYPE } from "../../_shared/index.js";
3
+ import { getErrorToolResult, textToolResult } from "../tool-utils.js";
4
+ import { formatLocalDateTime } from "../../utils/date-utils.js";
5
+ import { applyLineEdit, EDIT_ARTIFACT_MODE, EDIT_ARTIFACT_MODE_VALUES } from "./artifacts-mcp-server-utils.js";
6
+ export const EDIT_CIRCLE_ARTIFACT_TOOL_NAME = "edit_circle_artifact";
7
+ export function getEditCircleArtifactToolConfig(agentId, artifactManager, sensorManager) {
8
+ const inputSchema = {
9
+ circle_id: z.string().describe("The circle ID that owns the artifact."),
10
+ filename: z.string().describe("Name of an existing TEXT circle artifact to edit."),
11
+ mode: z
12
+ .enum(EDIT_ARTIFACT_MODE_VALUES)
13
+ .describe("Edit mode. 'insert': insert new lines before startLine. 'replace': replace lines startLine..endLine inclusive. Both require a TEXT artifact."),
14
+ content: z.string().describe("The new text to insert or use as replacement."),
15
+ startLine: z
16
+ .number()
17
+ .min(1)
18
+ .describe("1-based line number. For 'insert', new lines are placed before this line. For 'replace', this is the first line replaced."),
19
+ endLine: z
20
+ .number()
21
+ .min(1)
22
+ .optional()
23
+ .describe("1-based inclusive last line to replace. Required for 'replace' mode. Ignored for 'insert'."),
24
+ version: z
25
+ .number()
26
+ .describe("The Version value from your most recent read of this artifact (the [Version: ...] token)."),
27
+ addTags: z.array(z.string()).optional().describe("Optional tags to add to the artifact."),
28
+ removeTags: z.array(z.string()).optional().describe("Optional tags to remove from the artifact."),
29
+ };
30
+ const handler = async ({ circle_id, filename: rawFilename, content, mode, startLine, endLine, version, addTags, removeTags, }) => {
31
+ if (!artifactManager.isDirectCircleMember(circle_id, agentId)) {
32
+ return textToolResult(["You are not a direct member of this circle."], true);
33
+ }
34
+ const filename = rawFilename.trim();
35
+ try {
36
+ if (mode === EDIT_ARTIFACT_MODE.REPLACE && endLine === undefined) {
37
+ throw new Error("endLine is required for 'replace' mode.");
38
+ }
39
+ const { content: existingContent, metadata } = await artifactManager.readCircleArtifact(circle_id, filename);
40
+ if (metadata.contentType !== ARTIFACT_CONTENT_TYPE.TEXT || typeof existingContent !== "string") {
41
+ throw new Error(`edit_circle_artifact only supports TEXT artifacts (${metadata.filename} is ${metadata.contentType}). Use write_circle_artifact to replace the file.`);
42
+ }
43
+ const nextContent = applyLineEdit(existingContent, content, mode, startLine, endLine);
44
+ const updated = await artifactManager.updateCircleArtifact(circle_id, filename, {
45
+ content: nextContent,
46
+ addTags,
47
+ removeTags,
48
+ expectedUpdatedTimestamp: version,
49
+ });
50
+ const userTimezone = await sensorManager.getUserTimezone();
51
+ const editNote = mode === EDIT_ARTIFACT_MODE.REPLACE
52
+ ? `replaced lines ${startLine}-${endLine}`
53
+ : `inserted at line ${startLine}`;
54
+ return textToolResult([
55
+ `Circle artifact edited: ${updated.filename} (circle: ${circle_id}, ${editNote}, size: ${updated.size} bytes, modified: ${formatLocalDateTime(new Date(updated.updatedTimestamp), userTimezone)}). Re-read the artifact before the next edit; line numbers and Version are now stale.`,
56
+ ]);
57
+ }
58
+ catch (error) {
59
+ return getErrorToolResult(error, "Failed to edit circle artifact.");
60
+ }
61
+ };
62
+ const config = {
63
+ name: EDIT_CIRCLE_ARTIFACT_TOOL_NAME,
64
+ description: "Surgically modify a TEXT circle artifact by inserting or replacing a range of lines. Only direct members of the circle can edit. Use write_circle_artifact to replace the full file or to write non-TEXT content.",
65
+ inputSchema,
66
+ handler,
67
+ };
68
+ return config;
69
+ }
70
+ //# sourceMappingURL=edit-circle-artifact.js.map
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { getErrorToolResult, textToolResult } from "../tool-utils.js";
3
+ import { buildFindContentResult } from "./artifacts-mcp-server-utils.js";
4
+ export const FIND_CONTENT_IN_ARTIFACT_TOOL_NAME = "find_content_in_artifact";
5
+ export function getFindContentInArtifactToolConfig(agentId, artifactManager) {
6
+ const inputSchema = {
7
+ filename: z.string().describe("Name of an existing TEXT artifact to search."),
8
+ query: z.string().min(1).describe("Substring to search for. Case-insensitive."),
9
+ startLine: z
10
+ .number()
11
+ .min(1)
12
+ .optional()
13
+ .describe("Optional. 1-based line number to start searching from. Defaults to 1."),
14
+ limit: z.number().min(1).optional().describe("Optional. Maximum number of matches to return."),
15
+ };
16
+ const handler = async ({ filename: rawFilename, query, startLine, limit }) => {
17
+ const filename = rawFilename.trim();
18
+ try {
19
+ if (!query) {
20
+ return textToolResult(["Search query must not be empty."], true);
21
+ }
22
+ const result = await artifactManager.findArtifactContent(agentId, filename, query, startLine);
23
+ return buildFindContentResult(filename, query, result, limit);
24
+ }
25
+ catch (error) {
26
+ return getErrorToolResult(error, "Failed to search artifact content.");
27
+ }
28
+ };
29
+ const config = {
30
+ name: FIND_CONTENT_IN_ARTIFACT_TOOL_NAME,
31
+ description: "Search a TEXT artifact you own for a substring and return matching lines with their 1-based line numbers. Case-insensitive. Use startLine to skip ahead and limit to cap the number of matches.",
32
+ inputSchema,
33
+ handler,
34
+ };
35
+ return config;
36
+ }
37
+ //# sourceMappingURL=find-content-in-artifact.js.map
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { getErrorToolResult, textToolResult } from "../tool-utils.js";
3
+ import { buildFindContentResult } from "./artifacts-mcp-server-utils.js";
4
+ export const FIND_CONTENT_IN_CIRCLE_ARTIFACT_TOOL_NAME = "find_content_in_circle_artifact";
5
+ export function getFindContentInCircleArtifactToolConfig(agentId, artifactManager) {
6
+ const inputSchema = {
7
+ circle_id: z.string().describe("The circle ID that owns the artifact."),
8
+ filename: z.string().describe("Name of an existing TEXT circle artifact to search."),
9
+ query: z.string().min(1).describe("Substring to search for. Case-insensitive."),
10
+ startLine: z
11
+ .number()
12
+ .min(1)
13
+ .optional()
14
+ .describe("Optional. 1-based line number to start searching from. Defaults to 1."),
15
+ limit: z.number().min(1).optional().describe("Optional. Maximum number of matches to return."),
16
+ };
17
+ const handler = async ({ circle_id, filename: rawFilename, query, startLine, limit, }) => {
18
+ if (!artifactManager.isDirectCircleMember(circle_id, agentId)) {
19
+ return textToolResult(["You are not a direct member of this circle."], true);
20
+ }
21
+ const filename = rawFilename.trim();
22
+ try {
23
+ if (!query) {
24
+ return textToolResult(["Search query must not be empty."], true);
25
+ }
26
+ const result = await artifactManager.findCircleArtifactContent(circle_id, filename, query, startLine);
27
+ return buildFindContentResult(filename, query, result, limit);
28
+ }
29
+ catch (error) {
30
+ return getErrorToolResult(error, "Failed to search circle artifact content.");
31
+ }
32
+ };
33
+ const config = {
34
+ name: FIND_CONTENT_IN_CIRCLE_ARTIFACT_TOOL_NAME,
35
+ description: "Search a TEXT circle artifact for a substring and return matching lines with their 1-based line numbers. Only direct members of the circle can search. Case-insensitive. Use startLine to skip ahead and limit to cap the number of matches.",
36
+ inputSchema,
37
+ handler,
38
+ };
39
+ return config;
40
+ }
41
+ //# sourceMappingURL=find-content-in-circle-artifact.js.map
@@ -11,29 +11,40 @@ export function getListArtifactsToolConfig(agentId, artifactManager, registry, c
11
11
  .enum(ARTIFACT_TYPE_VALUES)
12
12
  .optional()
13
13
  .describe(`Filter by artifact type. Values: ${ARTIFACT_TYPE_VALUES.join(", ")}`),
14
+ tags: z
15
+ .array(z.string())
16
+ .optional()
17
+ .describe("Filter by tags. Only artifacts that have every specified tag are returned."),
14
18
  limit: z.number().optional().describe("Number of artifacts to return per page."),
15
19
  skip: z.number().optional().describe("Number of artifacts to skip for pagination."),
16
20
  };
17
- const handler = async ({ agent_id, type, limit, skip }) => {
21
+ const handler = async ({ agent_id, type, tags, limit, skip }) => {
18
22
  const targetId = agent_id ?? agentId;
19
23
  if (agent_id) {
20
24
  try {
21
25
  registry.getAgent(agent_id);
22
26
  }
23
27
  catch {
24
- return textToolResult(["Error: agent not found"], true);
28
+ return textToolResult(["Target agent not found."], true);
25
29
  }
26
30
  if (!circleManager.isAgentVisible(agentId, targetId)) {
27
- return textToolResult(["Error: agent not visible to you"], true);
31
+ return textToolResult(["Target agent is not visible to you."], true);
28
32
  }
29
33
  }
30
34
  try {
31
35
  const [artifacts, userTimezone] = await Promise.all([
32
- artifactManager.listArtifacts(targetId, { type }),
36
+ artifactManager.listArtifacts(targetId, { type, tags }),
33
37
  sensorManager.getUserTimezone(),
34
38
  ]);
35
39
  if (artifacts.length === 0) {
36
- const suffix = type ? ` with type ${type}` : "";
40
+ const filterParts = [];
41
+ if (type) {
42
+ filterParts.push(`type ${type}`);
43
+ }
44
+ if (tags?.length) {
45
+ filterParts.push(`tags [${tags.join(", ")}]`);
46
+ }
47
+ const suffix = filterParts.length ? ` matching ${filterParts.join(" and ")}` : "";
37
48
  return textToolResult([`No artifacts found for agent ${targetId}${suffix}.`]);
38
49
  }
39
50
  const pagination = applyPagination(artifacts, limit || DEFAULT_ARTIFACTS_LIMIT, skip);
@@ -11,20 +11,31 @@ export function getListCircleArtifactsToolConfig(agentId, artifactManager, senso
11
11
  .enum(ARTIFACT_TYPE_VALUES)
12
12
  .optional()
13
13
  .describe(`Filter by artifact type. Values: ${ARTIFACT_TYPE_VALUES.join(", ")}`),
14
+ tags: z
15
+ .array(z.string())
16
+ .optional()
17
+ .describe("Filter by tags. Only artifacts that have every specified tag are returned."),
14
18
  limit: z.number().optional().describe("Number of artifacts to return per page."),
15
19
  skip: z.number().optional().describe("Number of artifacts to skip for pagination."),
16
20
  };
17
- const handler = async ({ circle_id, type, limit, skip }) => {
21
+ const handler = async ({ circle_id, type, tags, limit, skip }) => {
18
22
  if (!artifactManager.isDirectCircleMember(circle_id, agentId)) {
19
- return textToolResult(["Error: you are not a direct member of this circle"], true);
23
+ return textToolResult(["You are not a direct member of this circle."], true);
20
24
  }
21
25
  try {
22
26
  const [artifacts, userTimezone] = await Promise.all([
23
- artifactManager.listCircleArtifacts(circle_id, { type }),
27
+ artifactManager.listCircleArtifacts(circle_id, { type, tags }),
24
28
  sensorManager.getUserTimezone(),
25
29
  ]);
26
30
  if (artifacts.length === 0) {
27
- const suffix = type ? ` with type ${type}` : "";
31
+ const filterParts = [];
32
+ if (type) {
33
+ filterParts.push(`type ${type}`);
34
+ }
35
+ if (tags?.length) {
36
+ filterParts.push(`tags [${tags.join(", ")}]`);
37
+ }
38
+ const suffix = filterParts.length ? ` matching ${filterParts.join(" and ")}` : "";
28
39
  return textToolResult([`No artifacts found for circle ${circle_id}${suffix}.`]);
29
40
  }
30
41
  const pagination = applyPagination(artifacts, limit || DEFAULT_CIRCLE_ARTIFACTS_LIMIT, skip);
@@ -23,15 +23,14 @@ export function getReadArtifactToolConfig(agentId, artifactManager, registry, ci
23
23
  registry.getAgent(agent_id);
24
24
  }
25
25
  catch {
26
- return textToolResult(["Error: agent not found"], true);
26
+ return textToolResult(["Target agent not found."], true);
27
27
  }
28
28
  if (!circleManager.isAgentVisible(agentId, agent_id)) {
29
- return textToolResult(["Error: agent not visible to you"], true);
29
+ return textToolResult(["Target agent is not visible to you."], true);
30
30
  }
31
31
  try {
32
- const [content, metadata, userTimezone] = await Promise.all([
32
+ const [{ content, metadata }, userTimezone] = await Promise.all([
33
33
  artifactManager.readArtifact(agent_id, filename, { useAdapter: true }),
34
- artifactManager.getArtifactMetadata(agent_id, filename),
35
34
  sensorManager.getUserTimezone(),
36
35
  ]);
37
36
  return buildReadArtifactResult(content, metadata, userTimezone, { showLineNumber, startLine, limit });
@@ -20,12 +20,11 @@ export function getReadCircleArtifactToolConfig(agentId, artifactManager, sensor
20
20
  };
21
21
  const handler = async ({ circle_id, filename, showLineNumber, startLine, limit, }) => {
22
22
  if (!artifactManager.isDirectCircleMember(circle_id, agentId)) {
23
- return textToolResult(["Error: you are not a direct member of this circle"], true);
23
+ return textToolResult(["You are not a direct member of this circle."], true);
24
24
  }
25
25
  try {
26
- const [content, metadata, userTimezone] = await Promise.all([
26
+ const [{ content, metadata }, userTimezone] = await Promise.all([
27
27
  artifactManager.readCircleArtifact(circle_id, filename, { useAdapter: true }),
28
- artifactManager.getCircleArtifactMetadata(circle_id, filename),
29
28
  sensorManager.getUserTimezone(),
30
29
  ]);
31
30
  return buildReadArtifactResult(content, metadata, userTimezone, { showLineNumber, startLine, limit });
@@ -6,7 +6,7 @@ import { ARTIFACT_CONTENT_TYPE_VALUES, ARTIFACT_TYPE_VALUES } from "./artifacts-
6
6
  export const WRITE_ARTIFACT_TOOL_NAME = "write_artifact";
7
7
  export function getWriteArtifactToolConfig(agentId, artifactManager, sensorManager) {
8
8
  const inputSchema = {
9
- filename: z.string().describe("Name of the file to create or overwrite, e.g. 'report.md' or 'data.json'"),
9
+ filename: z.string().describe("Name of the file to write, e.g. 'report.md' or 'data.json'."),
10
10
  content: z
11
11
  .string()
12
12
  .describe("The content to write. For TEXT content type, provide the raw text. For binary content types (IMAGE, AUDIO, BINARY), provide base64-encoded data."),
@@ -17,9 +17,13 @@ export function getWriteArtifactToolConfig(agentId, artifactManager, sensorManag
17
17
  content_type: z
18
18
  .enum(ARTIFACT_CONTENT_TYPE_VALUES)
19
19
  .optional()
20
- .describe(`Content type annotation. Values: ${ARTIFACT_CONTENT_TYPE_VALUES.join(", ")}. Defaults to ${ARTIFACT_CONTENT_TYPE.TEXT}`),
20
+ .describe(`Content type annotation. Values: ${ARTIFACT_CONTENT_TYPE_VALUES.join(", ")}. Defaults to ${ARTIFACT_CONTENT_TYPE.TEXT}.`),
21
+ tags: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("Tags to attach to the artifact. Fully replaces existing tags; omit to leave the artifact untagged."),
21
25
  };
22
- const handler = async ({ filename: rawFilename, content, type, content_type }) => {
26
+ const handler = async ({ filename: rawFilename, content, type, content_type, tags, }) => {
23
27
  const filename = rawFilename.trim();
24
28
  try {
25
29
  const isBinary = content_type && content_type !== ARTIFACT_CONTENT_TYPE.TEXT;
@@ -27,6 +31,7 @@ export function getWriteArtifactToolConfig(agentId, artifactManager, sensorManag
27
31
  const metadata = await artifactManager.writeArtifact(agentId, filename, artifactContent, {
28
32
  type,
29
33
  contentType: content_type,
34
+ tags,
30
35
  createdBy: { sourceType: AGENT_TASK_SOURCE_TYPE.AGENT, agentId },
31
36
  });
32
37
  const userTimezone = await sensorManager.getUserTimezone();
@@ -43,7 +48,7 @@ export function getWriteArtifactToolConfig(agentId, artifactManager, sensorManag
43
48
  };
44
49
  const config = {
45
50
  name: WRITE_ARTIFACT_TOOL_NAME,
46
- description: "Save a file to your own artifacts folder. Other agents can read your artifacts to collaborate. You can only write to your own folder.",
51
+ description: "Save a file to your own artifacts folder, creating it or replacing the existing file at that name. Other agents can read your artifacts to collaborate. Use edit_artifact for surgical line-level changes to a TEXT artifact.",
47
52
  inputSchema,
48
53
  handler,
49
54
  };
@@ -6,8 +6,8 @@ import { ARTIFACT_CONTENT_TYPE_VALUES, ARTIFACT_TYPE_VALUES } from "./artifacts-
6
6
  export const WRITE_CIRCLE_ARTIFACT_TOOL_NAME = "write_circle_artifact";
7
7
  export function getWriteCircleArtifactToolConfig(agentId, artifactManager, sensorManager) {
8
8
  const inputSchema = {
9
- circle_id: z.string().describe("The circle ID to write the artifact to"),
10
- filename: z.string().describe("Name of the file to create or overwrite"),
9
+ circle_id: z.string().describe("The circle ID to write the artifact to."),
10
+ filename: z.string().describe("Name of the file to write."),
11
11
  content: z
12
12
  .string()
13
13
  .describe("The content to write. For TEXT content type, provide the raw text. For binary content types (IMAGE, AUDIO, BINARY), provide base64-encoded data."),
@@ -18,11 +18,15 @@ export function getWriteCircleArtifactToolConfig(agentId, artifactManager, senso
18
18
  content_type: z
19
19
  .enum(ARTIFACT_CONTENT_TYPE_VALUES)
20
20
  .optional()
21
- .describe(`Content type annotation. Values: ${ARTIFACT_CONTENT_TYPE_VALUES.join(", ")}. Defaults to ${ARTIFACT_CONTENT_TYPE.TEXT}`),
21
+ .describe(`Content type annotation. Values: ${ARTIFACT_CONTENT_TYPE_VALUES.join(", ")}. Defaults to ${ARTIFACT_CONTENT_TYPE.TEXT}.`),
22
+ tags: z
23
+ .array(z.string())
24
+ .optional()
25
+ .describe("Tags to attach to the artifact. Fully replaces existing tags; omit to leave the artifact untagged."),
22
26
  };
23
- const handler = async ({ circle_id, filename: rawFilename, content, type, content_type, }) => {
27
+ const handler = async ({ circle_id, filename: rawFilename, content, type, content_type, tags, }) => {
24
28
  if (!artifactManager.isDirectCircleMember(circle_id, agentId)) {
25
- return textToolResult(["Error: you are not a direct member of this circle"], true);
29
+ return textToolResult(["You are not a direct member of this circle."], true);
26
30
  }
27
31
  const filename = rawFilename.trim();
28
32
  try {
@@ -31,6 +35,7 @@ export function getWriteCircleArtifactToolConfig(agentId, artifactManager, senso
31
35
  const metadata = await artifactManager.writeCircleArtifact(circle_id, filename, artifactContent, {
32
36
  type,
33
37
  contentType: content_type,
38
+ tags,
34
39
  createdBy: { sourceType: AGENT_TASK_SOURCE_TYPE.AGENT, agentId },
35
40
  });
36
41
  const userTimezone = await sensorManager.getUserTimezone();
@@ -47,7 +52,7 @@ export function getWriteCircleArtifactToolConfig(agentId, artifactManager, senso
47
52
  };
48
53
  const config = {
49
54
  name: WRITE_CIRCLE_ARTIFACT_TOOL_NAME,
50
- description: "Save a file to a circle's shared artifacts folder. Only direct members of the circle can read and write circle artifacts.",
55
+ description: "Save a file to a circle's shared artifacts folder, creating it or replacing the existing file at that name. Only direct members of the circle can read and write circle artifacts. Use edit_circle_artifact for surgical line-level changes to a TEXT artifact.",
51
56
  inputSchema,
52
57
  handler,
53
58
  };
@@ -76,7 +76,7 @@ export function processTextContent(text, options) {
76
76
  const clampedEnd = Math.min(options.limit !== undefined ? start + options.limit : totalLines, totalLines);
77
77
  const sliced = allLines.slice(start, clampedEnd);
78
78
  if (options.showLineNumber) {
79
- headerParts.push("Lines are prefixed with [LNNN] markers. These markers are NOT part of the note content.");
79
+ headerParts.push("Lines are prefixed with [LNNN] markers. These markers are NOT part of the line content.");
80
80
  }
81
81
  const hasRange = options.startLine !== undefined || options.limit !== undefined;
82
82
  headerParts.push(`--- CONTENT${hasRange ? ` (lines ${start + 1} - ${clampedEnd})` : ""} ---`);