maestro-agent-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +24 -0
  3. package/README.md +133 -0
  4. package/dist/agents/contracts.d.ts +49 -0
  5. package/dist/agents/contracts.d.ts.map +1 -0
  6. package/dist/agents/contracts.js +2 -0
  7. package/dist/agents/contracts.js.map +1 -0
  8. package/dist/agents/rollout/shared.d.ts +24 -0
  9. package/dist/agents/rollout/shared.d.ts.map +1 -0
  10. package/dist/agents/rollout/shared.js +105 -0
  11. package/dist/agents/rollout/shared.js.map +1 -0
  12. package/dist/core/agent.d.ts +71 -0
  13. package/dist/core/agent.d.ts.map +1 -0
  14. package/dist/core/agent.js +22 -0
  15. package/dist/core/agent.js.map +1 -0
  16. package/dist/core/loop.d.ts +26 -0
  17. package/dist/core/loop.d.ts.map +1 -0
  18. package/dist/core/loop.js +317 -0
  19. package/dist/core/loop.js.map +1 -0
  20. package/dist/index.d.ts +49 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +53 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/mcp/client.d.ts +79 -0
  25. package/dist/mcp/client.d.ts.map +1 -0
  26. package/dist/mcp/client.js +176 -0
  27. package/dist/mcp/client.js.map +1 -0
  28. package/dist/mcp/pool-cache.d.ts +103 -0
  29. package/dist/mcp/pool-cache.d.ts.map +1 -0
  30. package/dist/mcp/pool-cache.js +249 -0
  31. package/dist/mcp/pool-cache.js.map +1 -0
  32. package/dist/mcp/pool.d.ts +65 -0
  33. package/dist/mcp/pool.d.ts.map +1 -0
  34. package/dist/mcp/pool.js +86 -0
  35. package/dist/mcp/pool.js.map +1 -0
  36. package/dist/media/file-events.d.ts +8 -0
  37. package/dist/media/file-events.d.ts.map +1 -0
  38. package/dist/media/file-events.js +15 -0
  39. package/dist/media/file-events.js.map +1 -0
  40. package/dist/memory/active-task-template.d.ts +34 -0
  41. package/dist/memory/active-task-template.d.ts.map +1 -0
  42. package/dist/memory/active-task-template.js +63 -0
  43. package/dist/memory/active-task-template.js.map +1 -0
  44. package/dist/memory/compressor.d.ts +87 -0
  45. package/dist/memory/compressor.d.ts.map +1 -0
  46. package/dist/memory/compressor.js +164 -0
  47. package/dist/memory/compressor.js.map +1 -0
  48. package/dist/memory/hash.d.ts +17 -0
  49. package/dist/memory/hash.d.ts.map +1 -0
  50. package/dist/memory/hash.js +20 -0
  51. package/dist/memory/hash.js.map +1 -0
  52. package/dist/memory/prune.d.ts +117 -0
  53. package/dist/memory/prune.d.ts.map +1 -0
  54. package/dist/memory/prune.js +416 -0
  55. package/dist/memory/prune.js.map +1 -0
  56. package/dist/memory/reminder.d.ts +57 -0
  57. package/dist/memory/reminder.d.ts.map +1 -0
  58. package/dist/memory/reminder.js +57 -0
  59. package/dist/memory/reminder.js.map +1 -0
  60. package/dist/memory/scrubber.d.ts +28 -0
  61. package/dist/memory/scrubber.d.ts.map +1 -0
  62. package/dist/memory/scrubber.js +147 -0
  63. package/dist/memory/scrubber.js.map +1 -0
  64. package/dist/memory/token-estimate.d.ts +10 -0
  65. package/dist/memory/token-estimate.d.ts.map +1 -0
  66. package/dist/memory/token-estimate.js +69 -0
  67. package/dist/memory/token-estimate.js.map +1 -0
  68. package/dist/platform/config.d.ts +12 -0
  69. package/dist/platform/config.d.ts.map +1 -0
  70. package/dist/platform/config.js +54 -0
  71. package/dist/platform/config.js.map +1 -0
  72. package/dist/platform/jsonl.d.ts +15 -0
  73. package/dist/platform/jsonl.d.ts.map +1 -0
  74. package/dist/platform/jsonl.js +80 -0
  75. package/dist/platform/jsonl.js.map +1 -0
  76. package/dist/platform/lifecycle.d.ts +22 -0
  77. package/dist/platform/lifecycle.d.ts.map +1 -0
  78. package/dist/platform/lifecycle.js +60 -0
  79. package/dist/platform/lifecycle.js.map +1 -0
  80. package/dist/platform/logger.d.ts +26 -0
  81. package/dist/platform/logger.d.ts.map +1 -0
  82. package/dist/platform/logger.js +41 -0
  83. package/dist/platform/logger.js.map +1 -0
  84. package/dist/platform/mcp-config.d.ts +15 -0
  85. package/dist/platform/mcp-config.d.ts.map +1 -0
  86. package/dist/platform/mcp-config.js +8 -0
  87. package/dist/platform/mcp-config.js.map +1 -0
  88. package/dist/provider.d.ts +81 -0
  89. package/dist/provider.d.ts.map +1 -0
  90. package/dist/provider.js +444 -0
  91. package/dist/provider.js.map +1 -0
  92. package/dist/providers/anthropic.d.ts +132 -0
  93. package/dist/providers/anthropic.d.ts.map +1 -0
  94. package/dist/providers/anthropic.js +518 -0
  95. package/dist/providers/anthropic.js.map +1 -0
  96. package/dist/providers/base.d.ts +140 -0
  97. package/dist/providers/base.d.ts.map +1 -0
  98. package/dist/providers/base.js +2 -0
  99. package/dist/providers/base.js.map +1 -0
  100. package/dist/providers/deepseek.d.ts +118 -0
  101. package/dist/providers/deepseek.d.ts.map +1 -0
  102. package/dist/providers/deepseek.js +467 -0
  103. package/dist/providers/deepseek.js.map +1 -0
  104. package/dist/registry.d.ts +3 -0
  105. package/dist/registry.d.ts.map +1 -0
  106. package/dist/registry.js +94 -0
  107. package/dist/registry.js.map +1 -0
  108. package/dist/session-store.d.ts +133 -0
  109. package/dist/session-store.d.ts.map +1 -0
  110. package/dist/session-store.js +277 -0
  111. package/dist/session-store.js.map +1 -0
  112. package/dist/skills/curator.d.ts +104 -0
  113. package/dist/skills/curator.d.ts.map +1 -0
  114. package/dist/skills/curator.js +162 -0
  115. package/dist/skills/curator.js.map +1 -0
  116. package/dist/skills/index-builder.d.ts +42 -0
  117. package/dist/skills/index-builder.d.ts.map +1 -0
  118. package/dist/skills/index-builder.js +94 -0
  119. package/dist/skills/index-builder.js.map +1 -0
  120. package/dist/skills/loader.d.ts +107 -0
  121. package/dist/skills/loader.d.ts.map +1 -0
  122. package/dist/skills/loader.js +286 -0
  123. package/dist/skills/loader.js.map +1 -0
  124. package/dist/skills/preprocess.d.ts +45 -0
  125. package/dist/skills/preprocess.d.ts.map +1 -0
  126. package/dist/skills/preprocess.js +126 -0
  127. package/dist/skills/preprocess.js.map +1 -0
  128. package/dist/skills/usage.d.ts +75 -0
  129. package/dist/skills/usage.d.ts.map +1 -0
  130. package/dist/skills/usage.js +147 -0
  131. package/dist/skills/usage.js.map +1 -0
  132. package/dist/state/todos.d.ts +95 -0
  133. package/dist/state/todos.d.ts.map +1 -0
  134. package/dist/state/todos.js +198 -0
  135. package/dist/state/todos.js.map +1 -0
  136. package/dist/storage/conversations.d.ts +28 -0
  137. package/dist/storage/conversations.d.ts.map +1 -0
  138. package/dist/storage/conversations.js +8 -0
  139. package/dist/storage/conversations.js.map +1 -0
  140. package/dist/sub-agent/runner.d.ts +78 -0
  141. package/dist/sub-agent/runner.d.ts.map +1 -0
  142. package/dist/sub-agent/runner.js +215 -0
  143. package/dist/sub-agent/runner.js.map +1 -0
  144. package/dist/tools/builtin/agent.d.ts +33 -0
  145. package/dist/tools/builtin/agent.d.ts.map +1 -0
  146. package/dist/tools/builtin/agent.js +76 -0
  147. package/dist/tools/builtin/agent.js.map +1 -0
  148. package/dist/tools/builtin/bash.d.ts +11 -0
  149. package/dist/tools/builtin/bash.d.ts.map +1 -0
  150. package/dist/tools/builtin/bash.js +91 -0
  151. package/dist/tools/builtin/bash.js.map +1 -0
  152. package/dist/tools/builtin/edit.d.ts +21 -0
  153. package/dist/tools/builtin/edit.d.ts.map +1 -0
  154. package/dist/tools/builtin/edit.js +238 -0
  155. package/dist/tools/builtin/edit.js.map +1 -0
  156. package/dist/tools/builtin/read.d.ts +17 -0
  157. package/dist/tools/builtin/read.d.ts.map +1 -0
  158. package/dist/tools/builtin/read.js +139 -0
  159. package/dist/tools/builtin/read.js.map +1 -0
  160. package/dist/tools/builtin/sandbox.d.ts +16 -0
  161. package/dist/tools/builtin/sandbox.d.ts.map +1 -0
  162. package/dist/tools/builtin/sandbox.js +58 -0
  163. package/dist/tools/builtin/sandbox.js.map +1 -0
  164. package/dist/tools/builtin/skill_view.d.ts +37 -0
  165. package/dist/tools/builtin/skill_view.d.ts.map +1 -0
  166. package/dist/tools/builtin/skill_view.js +82 -0
  167. package/dist/tools/builtin/skill_view.js.map +1 -0
  168. package/dist/tools/builtin/todo_write.d.ts +29 -0
  169. package/dist/tools/builtin/todo_write.d.ts.map +1 -0
  170. package/dist/tools/builtin/todo_write.js +96 -0
  171. package/dist/tools/builtin/todo_write.js.map +1 -0
  172. package/dist/tools/builtin/web_fetch.d.ts +10 -0
  173. package/dist/tools/builtin/web_fetch.d.ts.map +1 -0
  174. package/dist/tools/builtin/web_fetch.js +150 -0
  175. package/dist/tools/builtin/web_fetch.js.map +1 -0
  176. package/dist/tools/builtin/write.d.ts +35 -0
  177. package/dist/tools/builtin/write.d.ts.map +1 -0
  178. package/dist/tools/builtin/write.js +70 -0
  179. package/dist/tools/builtin/write.js.map +1 -0
  180. package/dist/tools/file-state.d.ts +99 -0
  181. package/dist/tools/file-state.d.ts.map +1 -0
  182. package/dist/tools/file-state.js +133 -0
  183. package/dist/tools/file-state.js.map +1 -0
  184. package/dist/tools/hooks/sandbox-fs.d.ts +25 -0
  185. package/dist/tools/hooks/sandbox-fs.d.ts.map +1 -0
  186. package/dist/tools/hooks/sandbox-fs.js +48 -0
  187. package/dist/tools/hooks/sandbox-fs.js.map +1 -0
  188. package/dist/tools/registry.d.ts +102 -0
  189. package/dist/tools/registry.d.ts.map +1 -0
  190. package/dist/tools/registry.js +93 -0
  191. package/dist/tools/registry.js.map +1 -0
  192. package/dist/types.d.ts +109 -0
  193. package/dist/types.d.ts.map +1 -0
  194. package/dist/types.js +20 -0
  195. package/dist/types.js.map +1 -0
  196. package/package.json +72 -0
@@ -0,0 +1,29 @@
1
+ import type { TodoStore } from "../../state/todos.js";
2
+ import type { ToolHandler } from "../../tools/registry.js";
3
+ /**
4
+ * `todo_write` builtin — TodoWrite-style task tracking for maestro.
5
+ *
6
+ * One tool, snapshot-replace semantics: the model passes the COMPLETE list
7
+ * it wants in place, and the store reconciles (upsert by id, drop entries
8
+ * absent from the snapshot, auto-assign ids on new items).
9
+ *
10
+ * Claude Code parity:
11
+ * - No separate `todo_list`. Read-side surfaces via the per-turn system
12
+ * reminder, which renders the current list each turn — no extra
13
+ * round-trip + no decision surface for "which tool do I call?".
14
+ * - 1-in-progress invariant. The LAST `in_progress` in the incoming
15
+ * snapshot wins; earlier in_progress entries are flipped to `pending`
16
+ * and the demoted id is reported back so the model knows.
17
+ *
18
+ * Side-effecting: every call mutates the store + persists to disk.
19
+ * `parallelSafe: false` is the right default — running two `todo_write`
20
+ * calls in parallel would race the snapshot.
21
+ */
22
+ export interface TodoWriteToolOptions {
23
+ /** Required. The per-session store. Built by maestroProvider via
24
+ * `getTodoStore(sessionId)` so the same instance is shared across
25
+ * every tool call in this turn. */
26
+ store: TodoStore;
27
+ }
28
+ export declare function createTodoWriteTool(opts: TodoWriteToolOptions): ToolHandler;
29
+ //# sourceMappingURL=todo_write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todo_write.d.ts","sourceRoot":"","sources":["../../../src/tools/builtin/todo_write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,eAAe,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,oBAAoB;IACnC;;wCAEoC;IACpC,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,GAAG,WAAW,CA+E3E"}
@@ -0,0 +1,96 @@
1
+ export function createTodoWriteTool(opts) {
2
+ const { store } = opts;
3
+ return {
4
+ parallelSafe: false,
5
+ schema: {
6
+ name: "todo_write",
7
+ description: "Write the current task list. Pass the COMPLETE snapshot of what you want the list " +
8
+ "to look like — entries with an existing `id` are updated, new entries (id omitted) " +
9
+ "get an auto-assigned id (task-N), and entries absent from the snapshot are dropped. " +
10
+ "At most one entry may have status `in_progress`; if you mark more than one, the LAST " +
11
+ "in_progress wins and the others are flipped to `pending`. The current list is " +
12
+ "surfaced to you in the system reminder each turn — call this whenever a multi-step " +
13
+ "plan starts, mid-flight when a step finishes, or to add/drop items.",
14
+ input_schema: {
15
+ type: "object",
16
+ properties: {
17
+ todos: {
18
+ type: "array",
19
+ description: "Complete snapshot of the task list. Each item: " +
20
+ "{id?: string, content: string, status: 'pending'|'in_progress'|'completed', " +
21
+ "activeForm?: string}. Include EVERY task you want to keep — omitting one drops it.",
22
+ },
23
+ },
24
+ required: ["todos"],
25
+ },
26
+ },
27
+ async execute(input) {
28
+ const raw = input.todos;
29
+ if (!Array.isArray(raw)) {
30
+ return JSON.stringify({
31
+ error: "todo_write: 'todos' must be an array",
32
+ });
33
+ }
34
+ const incoming = [];
35
+ for (let i = 0; i < raw.length; i++) {
36
+ const item = raw[i];
37
+ if (!item || typeof item !== "object") {
38
+ return JSON.stringify({
39
+ error: `todo_write: todos[${i}] must be an object`,
40
+ });
41
+ }
42
+ const obj = item;
43
+ const content = obj.content;
44
+ const status = obj.status;
45
+ if (typeof content !== "string" || content.length === 0) {
46
+ return JSON.stringify({
47
+ error: `todo_write: todos[${i}].content must be a non-empty string`,
48
+ });
49
+ }
50
+ if (status !== "pending" && status !== "in_progress" && status !== "completed") {
51
+ return JSON.stringify({
52
+ error: `todo_write: todos[${i}].status must be 'pending', 'in_progress', or 'completed'`,
53
+ });
54
+ }
55
+ const upsert = { content, status };
56
+ if (typeof obj.id === "string" && obj.id.length > 0)
57
+ upsert.id = obj.id;
58
+ if (typeof obj.activeForm === "string" && obj.activeForm.length > 0) {
59
+ upsert.activeForm = obj.activeForm;
60
+ }
61
+ incoming.push(upsert);
62
+ }
63
+ const result = store.upsert(incoming);
64
+ const summary = summarize(result.todos);
65
+ const parts = [
66
+ `Task list updated (${result.todos.length} item${result.todos.length === 1 ? "" : "s"}).`,
67
+ summary,
68
+ ];
69
+ if (result.demotedId) {
70
+ parts.push(`Note: more than one item was marked in_progress; '${result.demotedId}' was flipped to pending so only the last in_progress entry stays active.`);
71
+ }
72
+ return parts.join("\n\n");
73
+ },
74
+ };
75
+ }
76
+ /**
77
+ * Render a compact ascii summary of the list. Used in the tool result so
78
+ * the model immediately sees what landed (mirrors how Edit returns a
79
+ * preview after a successful write).
80
+ *
81
+ * Format:
82
+ * [✓] task-1 Done thing here
83
+ * [→] task-2 In-flight thing
84
+ * [ ] task-3 Pending thing
85
+ */
86
+ function summarize(todos) {
87
+ if (todos.length === 0)
88
+ return "(list is empty)";
89
+ return todos
90
+ .map((t) => {
91
+ const mark = t.status === "completed" ? "✓" : t.status === "in_progress" ? "→" : " ";
92
+ return `[${mark}] ${t.id} ${t.content}`;
93
+ })
94
+ .join("\n");
95
+ }
96
+ //# sourceMappingURL=todo_write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todo_write.js","sourceRoot":"","sources":["../../../src/tools/builtin/todo_write.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { ToolHandler } from "../../tools/registry.js";
2
+ export declare const webFetchTool: ToolHandler;
3
+ /** Strip HTML to plain text using cheerio. Removes <script>/<style>/<head>/
4
+ * <noscript> blocks entirely; then extracts remaining text with full entity
5
+ * decoding and comment removal built into cheerio's .text(). Collapses
6
+ * whitespace and preserves paragraph breaks — clean prose, no markup noise. */
7
+ export declare function htmlToText(html: string): string;
8
+ export declare const __FETCH_TIMEOUT_MS = 30000;
9
+ export declare const __MAX_RESPONSE_BYTES: number;
10
+ //# sourceMappingURL=web_fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web_fetch.d.ts","sourceRoot":"","sources":["../../../src/tools/builtin/web_fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA6BpD,eAAO,MAAM,YAAY,EAAE,WAuG1B,CAAC;AAWF;;;gFAGgF;AAChF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAa/C;AAGD,eAAO,MAAM,kBAAkB,QAAmB,CAAC;AACnD,eAAO,MAAM,oBAAoB,QAAqB,CAAC"}
@@ -0,0 +1,150 @@
1
+ import * as cheerio from "cheerio";
2
+ /**
3
+ * WebFetch builtin — claude SDK `WebFetch` tool parity for maestro.
4
+ *
5
+ * Claude SDK's WebFetch fetches the URL, converts the body to markdown, then
6
+ * spins up a sub-model with the user's `prompt` to extract / summarize the
7
+ * result. We don't have a free sub-model channel here, so the maestro variant
8
+ * returns the raw text body (stripped of HTML tags) and ignores the prompt —
9
+ * the parent model can apply its own reasoning to the body, which is the
10
+ * same end state at one less hop.
11
+ *
12
+ * Bounds:
13
+ * - URL must start with http:// or https://.
14
+ * - 30s timeout via AbortController.
15
+ * - 1MB response cap (`MAX_RESPONSE_BYTES`). Anything larger is truncated.
16
+ * - Only `text/*` and `application/json` content-types are decoded; binary
17
+ * types return a stub message.
18
+ * - HTML is parsed with cheerio for robust text extraction (script/style/
19
+ * head elements removed, entities decoded, comments ignored).
20
+ *
21
+ * Returns the formatted preamble + body on success, or `JSON.stringify({error})`
22
+ * on every failure mode.
23
+ */
24
+ const FETCH_TIMEOUT_MS = 30_000;
25
+ const MAX_RESPONSE_BYTES = 1 * 1024 * 1024; // 1MB
26
+ export const webFetchTool = {
27
+ // HTTP GET against an external URL with no local side effects — multiple
28
+ // fetches in the same turn can run in parallel.
29
+ parallelSafe: true,
30
+ schema: {
31
+ name: "WebFetch",
32
+ description: "Fetch a URL and return its text content. Supports text/* and application/json " +
33
+ "content types only — binary responses (PDF, images) are not decoded. " +
34
+ "30s timeout, 1MB response cap. The optional 'prompt' field is accepted " +
35
+ "for claude SDK compatibility but ignored — return value is the raw body.",
36
+ input_schema: {
37
+ type: "object",
38
+ properties: {
39
+ url: {
40
+ type: "string",
41
+ description: "Absolute http(s) URL to fetch.",
42
+ },
43
+ prompt: {
44
+ type: "string",
45
+ description: "Ignored — accepted for claude SDK compatibility.",
46
+ },
47
+ },
48
+ required: ["url"],
49
+ },
50
+ },
51
+ async execute(input) {
52
+ const url = typeof input.url === "string" ? input.url.trim() : "";
53
+ if (!url) {
54
+ return JSON.stringify({ error: "WebFetch: missing 'url' argument" });
55
+ }
56
+ if (!/^https?:\/\//i.test(url)) {
57
+ return JSON.stringify({
58
+ error: `WebFetch: url must start with http:// or https://, got '${url}'`,
59
+ });
60
+ }
61
+ const controller = new AbortController();
62
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
63
+ let response;
64
+ try {
65
+ response = await fetch(url, { signal: controller.signal });
66
+ }
67
+ catch (e) {
68
+ clearTimeout(timer);
69
+ const err = e;
70
+ if (err.name === "AbortError") {
71
+ return JSON.stringify({
72
+ error: `WebFetch: timeout after ${FETCH_TIMEOUT_MS}ms`,
73
+ url,
74
+ });
75
+ }
76
+ return JSON.stringify({
77
+ error: `WebFetch: fetch failed: ${err.message ?? String(e)}`,
78
+ url,
79
+ });
80
+ }
81
+ clearTimeout(timer);
82
+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
83
+ if (!response.ok) {
84
+ const text = await safeText(response);
85
+ return JSON.stringify({
86
+ error: `WebFetch: HTTP ${response.status} ${response.statusText}`,
87
+ url,
88
+ contentType,
89
+ body: text.slice(0, 500),
90
+ });
91
+ }
92
+ // Only decode text-ish responses. application/json is a text content type
93
+ // that browsers serve without `text/` prefix — special-case it.
94
+ const isTextLike = contentType.startsWith("text/") || contentType.includes("json");
95
+ if (!isTextLike) {
96
+ return JSON.stringify({
97
+ error: `WebFetch: non-text content-type '${contentType}'. Download with bash + Read.`,
98
+ url,
99
+ contentType,
100
+ });
101
+ }
102
+ let body = await safeText(response);
103
+ let truncated = false;
104
+ if (body.length > MAX_RESPONSE_BYTES) {
105
+ body = body.slice(0, MAX_RESPONSE_BYTES);
106
+ truncated = true;
107
+ }
108
+ // HTML → text via cheerio: parse the DOM, remove script/style/head/no-
109
+ // script, then extract text. cheerio handles entity decoding and ignores
110
+ // comments; the output is clean prose with paragraph breaks preserved.
111
+ if (contentType.includes("html")) {
112
+ body = htmlToText(body);
113
+ }
114
+ const parts = [];
115
+ parts.push(`URL: ${url}`);
116
+ parts.push(`Content-Type: ${contentType}`);
117
+ parts.push(`Length: ${body.length}${truncated ? " (truncated at 1MB)" : ""}`);
118
+ parts.push("");
119
+ parts.push(body);
120
+ return parts.join("\n");
121
+ },
122
+ };
123
+ /** Best-effort text decode — never throws. */
124
+ async function safeText(response) {
125
+ try {
126
+ return await response.text();
127
+ }
128
+ catch {
129
+ return "";
130
+ }
131
+ }
132
+ /** Strip HTML to plain text using cheerio. Removes <script>/<style>/<head>/
133
+ * <noscript> blocks entirely; then extracts remaining text with full entity
134
+ * decoding and comment removal built into cheerio's .text(). Collapses
135
+ * whitespace and preserves paragraph breaks — clean prose, no markup noise. */
136
+ export function htmlToText(html) {
137
+ const $ = cheerio.load(html);
138
+ // Remove script/style/head/noscript blocks — pure noise for the model.
139
+ $("script, style, head, noscript").remove();
140
+ // cheerio's .text() decodes all HTML entities and ignores comments.
141
+ let out = $.text();
142
+ // Collapse runs of whitespace to a single space, keep paragraph breaks.
143
+ out = out.replace(/[ \t]+/g, " ");
144
+ out = out.replace(/\n\s*\n+/g, "\n\n");
145
+ return out.trim();
146
+ }
147
+ // Internal exports for tests.
148
+ export const __FETCH_TIMEOUT_MS = FETCH_TIMEOUT_MS;
149
+ export const __MAX_RESPONSE_BYTES = MAX_RESPONSE_BYTES;
150
+ //# sourceMappingURL=web_fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web_fetch.js","sourceRoot":"","sources":["../../../src/tools/builtin/web_fetch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { FileStateTracker } from "../../tools/file-state.js";
2
+ import type { ToolHandler } from "../../tools/registry.js";
3
+ /**
4
+ * NOTE: filesystem sandboxing now lives in the `sandbox-fs` PreToolUse hook.
5
+ * Standalone callers (tests, scripts) without the registry get NO sandbox.
6
+ */
7
+ /**
8
+ * Write builtin — claude SDK `Write` tool parity for maestro.
9
+ *
10
+ * Mirrors the upstream `Write` tool's name + schema so the model's pretrained
11
+ * instinct calls our handler with the right shape. Overwrite-on-collision is
12
+ * the documented claude SDK behavior — no merge, no append. Callers that need
13
+ * additive writes should use the bash tool with `>>` for now (Phase 5 may add
14
+ * a separate `Edit` builtin that handles the read-modify-write cycle).
15
+ *
16
+ * Path safety: absolute path is enforced. We DO NOT sandbox the write under
17
+ * the user's workspace dir here — Clawgram's permission system + the agent
18
+ * loop's abort plumbing are the authoritative guardrails. A stricter check
19
+ * can land later without breaking the schema (the model only sees the public
20
+ * fields).
21
+ */
22
+ export interface WriteToolOptions {
23
+ /**
24
+ * Per-session file-state tracker. When provided, Write enforces Read-
25
+ * before-Write on existing files (same invariant as Edit — the model must
26
+ * see line-numbered content before overwriting). Write-on-create is always
27
+ * allowed. After Write, the tracker forgets the recorded state so the next
28
+ * Edit on the path must Re-read.
29
+ */
30
+ tracker?: FileStateTracker;
31
+ }
32
+ export declare function createWriteTool(opts?: WriteToolOptions): ToolHandler;
33
+ /** Backwards-compatible singleton (no tracker). */
34
+ export declare const writeTool: ToolHandler;
35
+ //# sourceMappingURL=write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../../src/tools/builtin/write.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;GAGG;AAEH;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED,wBAAgB,eAAe,CAAC,IAAI,GAAE,gBAAqB,GAAG,WAAW,CAkExE;AAED,mDAAmD;AACnD,eAAO,MAAM,SAAS,EAAE,WAA+B,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, isAbsolute } from "node:path";
3
+ export function createWriteTool(opts = {}) {
4
+ const { tracker } = opts;
5
+ return {
6
+ schema: {
7
+ name: "Write",
8
+ description: "Write content to a file (overwrite). Parent directories are created " +
9
+ "automatically. file_path must be absolute. Use this for new files or " +
10
+ "complete rewrites; for additive output use the bash tool with `>>`.",
11
+ input_schema: {
12
+ type: "object",
13
+ properties: {
14
+ file_path: {
15
+ type: "string",
16
+ description: "Absolute path to the file to write. Parent dirs auto-created.",
17
+ },
18
+ content: {
19
+ type: "string",
20
+ description: "Full file content. Overwrites any existing file.",
21
+ },
22
+ },
23
+ required: ["file_path", "content"],
24
+ },
25
+ },
26
+ async execute(input) {
27
+ const filePath = typeof input.file_path === "string" ? input.file_path : "";
28
+ if (!filePath) {
29
+ return JSON.stringify({ error: "Write: missing 'file_path' argument" });
30
+ }
31
+ if (!isAbsolute(filePath)) {
32
+ return JSON.stringify({
33
+ error: `Write: file_path must be absolute, got '${filePath}'`,
34
+ });
35
+ }
36
+ // gate: Read-before-Write — when the file already exists the model
37
+ // must have read it first (line-numbered view) to avoid blind overwrites.
38
+ if (tracker) {
39
+ const gateErr = tracker.checkBeforeMutate(filePath, "Write");
40
+ if (gateErr)
41
+ return JSON.stringify({ error: gateErr });
42
+ }
43
+ // `content` is allowed to be the empty string (legit way to truncate a
44
+ // file), so we accept any string but reject non-strings outright.
45
+ if (typeof input.content !== "string") {
46
+ return JSON.stringify({
47
+ error: `Write: 'content' must be a string, got ${typeof input.content}`,
48
+ });
49
+ }
50
+ const content = input.content;
51
+ try {
52
+ mkdirSync(dirname(filePath), { recursive: true });
53
+ writeFileSync(filePath, content, "utf-8");
54
+ }
55
+ catch (e) {
56
+ return JSON.stringify({
57
+ error: `Write: failed: ${e instanceof Error ? e.message : String(e)}`,
58
+ });
59
+ }
60
+ // Forget any prior Read state — the file just changed under us. Next
61
+ // Edit must Re-read to refresh the line-numbered view.
62
+ tracker?.forget(filePath);
63
+ const bytes = Buffer.byteLength(content, "utf-8");
64
+ return `File written: ${filePath} (${bytes} bytes)`;
65
+ },
66
+ };
67
+ }
68
+ /** Backwards-compatible singleton (no tracker). */
69
+ export const writeTool = createWriteTool();
70
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../../../src/tools/builtin/write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAoChD,MAAM,UAAU,eAAe,CAAC,OAAyB,EAAE;IACzD,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACzB,OAAO;QACL,MAAM,EAAE;YACN,IAAI,EAAE,OAAO;YACb,WAAW,EACT,sEAAsE;gBACtE,uEAAuE;gBACvE,qEAAqE;YACvE,YAAY,EAAE;gBACZ,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,SAAS,EAAE;wBACT,IAAI,EAAE,QAAQ;wBACd,WAAW,EAAE,+DAA+D;qBAC7E;oBACD,OAAO,EAAE;wBACP,IAAI,EAAE,QAAQ;wBACd,WAAW,EAAE,kDAAkD;qBAChE;iBACF;gBACD,QAAQ,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC;aACnC;SACF;QACD,KAAK,CAAC,OAAO,CAAC,KAAK;YACjB,MAAM,QAAQ,GAAG,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5E,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAC,CAAC;YAC1E,CAAC;YACD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC1B,OAAO,IAAI,CAAC,SAAS,CAAC;oBACpB,KAAK,EAAE,2CAA2C,QAAQ,GAAG;iBAC9D,CAAC,CAAC;YACL,CAAC;YACD,mEAAmE;YACnE,0EAA0E;YAC1E,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,OAAO,GAAG,OAAO,CAAC,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAC7D,IAAI,OAAO;oBAAE,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;YACzD,CAAC;YACD,uEAAuE;YACvE,kEAAkE;YAClE,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;gBACtC,OAAO,IAAI,CAAC,SAAS,CAAC;oBACpB,KAAK,EAAE,0CAA0C,OAAO,KAAK,CAAC,OAAO,EAAE;iBACxE,CAAC,CAAC;YACL,CAAC;YACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;YAE9B,IAAI,CAAC;gBACH,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAClD,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;YAC5C,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,OAAO,IAAI,CAAC,SAAS,CAAC;oBACpB,KAAK,EAAE,kBAAkB,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;iBACtE,CAAC,CAAC;YACL,CAAC;YAED,qEAAqE;YACrE,uDAAuD;YACvD,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;YAE1B,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAClD,OAAO,iBAAiB,QAAQ,KAAK,KAAK,SAAS,CAAC;QACtD,CAAC;KACF,CAAC;AACJ,CAAC;AAED,mDAAmD;AACnD,MAAM,CAAC,MAAM,SAAS,GAAgB,eAAe,EAAE,CAAC"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Per-session file-state tracker for Read-before-Edit enforcement.
3
+ *
4
+ * Claude Code's Edit refuses to touch a path that hasn't been Read in the
5
+ * same session. The invariant cuts two failure modes the model otherwise
6
+ * runs into:
7
+ *
8
+ * 1. Blind edits — the model guesses an `old_string` it never saw and
9
+ * either fails the match or replaces the wrong region.
10
+ * 2. Race-on-edit — a path was Read N turns ago, an external process (or
11
+ * another session) mutated it since, and the model's stale `old_string`
12
+ * lands but corrupts the actual current contents.
13
+ *
14
+ * Maestro lacked this gate; this module is the smallest implementation that
15
+ * closes both. Each session gets its own tracker (keyed by maestro sessionId
16
+ * in `sessions` below) so two users / two topics don't share file state.
17
+ *
18
+ * Recorded fields per path:
19
+ * - mtime — mtimeMs at the time of Read. Drift detection compares this
20
+ * against the live stat at Edit/Write time.
21
+ * - size — secondary drift signal. mtime can stay the same when content
22
+ * is rewritten within the same millisecond (rare but real on
23
+ * fast filesystems); size is a cheap belt-and-suspenders.
24
+ * - readAt — wall clock of the Read. Diagnostic only — surfaced in the
25
+ * "stale" error message so users see how old the Read was.
26
+ *
27
+ * Cleanup contract: `deleteSession(sessionId)` is called from
28
+ * `deleteMaestroSession` and (per-session) inside `cleanupStaleMaestroSessions`
29
+ * so the tracker map can't outlive its sessions. Without that hook, a long-
30
+ * running bot accumulates one map entry per forgotten session forever.
31
+ */
32
+ export interface FileStateEntry {
33
+ /** mtimeMs at the time of Read. */
34
+ mtime: number;
35
+ /** size in bytes at the time of Read. */
36
+ size: number;
37
+ /** Date.now() when the Read happened. Diagnostic only. */
38
+ readAt: number;
39
+ }
40
+ /**
41
+ * One tracker per session — `Map<absPath, FileStateEntry>`.
42
+ *
43
+ * Wrapped (instead of a raw Map) so the public surface is explicit: callers
44
+ * only record and check, they don't iterate or mutate entries directly.
45
+ */
46
+ export declare class FileStateTracker {
47
+ private readonly entries;
48
+ /**
49
+ * Record a successful Read of `absPath`. Called from Read's execute()
50
+ * after the file has been stat'd and the contents handed back to the
51
+ * model. A subsequent Read of the same path overwrites the entry — the
52
+ * latest Read is what Edit's drift check compares against.
53
+ */
54
+ recordRead(absPath: string, mtime: number, size: number): void;
55
+ /**
56
+ * Forget the recorded state for `absPath`. Called when Edit / Write
57
+ * mutates the file so the next Edit must Re-read first (its prior cached
58
+ * stat is now stale by definition). Also called when a Write creates a
59
+ * file — the next Edit on the same path must Read it back to confirm
60
+ * the model's mental model matches what landed on disk.
61
+ */
62
+ forget(absPath: string): void;
63
+ /**
64
+ * Check whether `absPath` may be Edit/Write'd. Returns null on allow,
65
+ * or a string error on deny — callers stringify into `{error}` so the
66
+ * model sees the same shape as sandbox / arg-validation rejections.
67
+ *
68
+ * Three reject paths:
69
+ * - "no_read": never Read this session. Forces the model to Read first.
70
+ * - "stale_mtime": mtime drifted vs recorded — file changed externally.
71
+ * - "stale_size": same as above but size changed (covers same-ms writes).
72
+ *
73
+ * Allow path: live stat matches recorded mtime AND size.
74
+ *
75
+ * One subtlety: if the path doesn't exist, we return null (allow). Write
76
+ * legitimately creates files, and Edit's "must exist" check runs after
77
+ * this gate inside the tool — so a missing file in Edit will reject with
78
+ * "file does not exist, use Write to create". Read-before-Mutate is about
79
+ * content drift, not existence.
80
+ */
81
+ checkBeforeMutate(absPath: string, tool: "Edit" | "Write"): string | null;
82
+ /** Used by tests + diagnostics. Not part of the per-turn contract. */
83
+ has(absPath: string): boolean;
84
+ /** Total number of tracked paths. Diagnostic. */
85
+ size(): number;
86
+ }
87
+ /** Get or create the tracker for a session. */
88
+ export declare function getFileStateTracker(sessionId: string): FileStateTracker;
89
+ /**
90
+ * Drop a session's tracker. Called from `deleteMaestroSession` and from
91
+ * `cleanupStaleMaestroSessions` after the on-disk JSONL is unlinked.
92
+ * ENOENT-equivalent: deleting a session that was never tracked is a no-op.
93
+ */
94
+ export declare function dropFileStateTracker(sessionId: string): void;
95
+ /** Test-only. Drops every tracker. */
96
+ export declare function __resetAllTrackers(): void;
97
+ /** Test-only. Snapshot of how many sessions are currently tracked. */
98
+ export declare function __trackerCount(): number;
99
+ //# sourceMappingURL=file-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-state.d.ts","sourceRoot":"","sources":["../../src/tools/file-state.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAE7D;;;;;OAKG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAI9D;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI7B;;;;;;;;;;;;;;;;;OAiBG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI;IAiDzE,sEAAsE;IACtE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAI7B,iDAAiD;IACjD,IAAI,IAAI,MAAM;CAGf;AAiBD,+CAA+C;AAC/C,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAOvE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED,sCAAsC;AACtC,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,sEAAsE;AACtE,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}