mono-pilot 0.2.9 → 0.2.12

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 (158) hide show
  1. package/README.md +270 -7
  2. package/dist/src/agents-paths.js +36 -0
  3. package/dist/src/brief/blocks.js +83 -0
  4. package/dist/src/brief/defaults.js +60 -0
  5. package/dist/src/brief/frontmatter.js +53 -0
  6. package/dist/src/brief/paths.js +10 -0
  7. package/dist/src/brief/reflection.js +27 -0
  8. package/dist/src/cli.js +62 -5
  9. package/dist/src/cluster/bus.js +102 -0
  10. package/dist/src/cluster/follower.js +137 -0
  11. package/dist/src/cluster/init.js +182 -0
  12. package/dist/src/cluster/leader.js +97 -0
  13. package/dist/src/cluster/log.js +49 -0
  14. package/dist/src/cluster/protocol.js +34 -0
  15. package/dist/src/cluster/services/bus.js +243 -0
  16. package/dist/src/cluster/services/embedding.js +12 -0
  17. package/dist/src/cluster/socket.js +86 -0
  18. package/dist/src/cluster/test-bus.js +175 -0
  19. package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
  20. package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
  21. package/dist/src/cluster_v2/connection.js +159 -0
  22. package/dist/src/cluster_v2/connection.test.js +55 -0
  23. package/dist/src/cluster_v2/events.js +102 -0
  24. package/dist/src/cluster_v2/index.js +2 -0
  25. package/dist/src/cluster_v2/observability.js +99 -0
  26. package/dist/src/cluster_v2/observability.test.js +46 -0
  27. package/dist/src/cluster_v2/rpc.js +389 -0
  28. package/dist/src/cluster_v2/rpc.test.js +110 -0
  29. package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
  30. package/dist/src/cluster_v2/runtime.js +531 -0
  31. package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
  32. package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
  33. package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
  34. package/dist/src/cluster_v2/services/bus.js +450 -0
  35. package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
  36. package/dist/src/cluster_v2/services/discord/collector.js +569 -0
  37. package/dist/src/cluster_v2/services/discord/index.js +1 -0
  38. package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
  39. package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
  40. package/dist/src/cluster_v2/services/embedding.js +66 -0
  41. package/dist/src/cluster_v2/services/registry-cache.js +107 -0
  42. package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
  43. package/dist/src/cluster_v2/services/registry.js +36 -0
  44. package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
  45. package/dist/src/cluster_v2/services/twitter/index.js +1 -0
  46. package/dist/src/config/digest.js +78 -0
  47. package/dist/src/config/discord.js +143 -0
  48. package/dist/src/config/image-gen.js +48 -0
  49. package/dist/src/config/mono-pilot.js +31 -0
  50. package/dist/src/config/twitter.js +100 -0
  51. package/dist/src/extensions/cluster.js +311 -0
  52. package/dist/src/extensions/commands/build-memory.js +76 -0
  53. package/dist/src/extensions/commands/digest/backfill.js +779 -0
  54. package/dist/src/extensions/commands/digest/index.js +1133 -0
  55. package/dist/src/extensions/commands/image-model.js +214 -0
  56. package/dist/src/extensions/game/bus-injection.js +47 -0
  57. package/dist/src/extensions/game/identity.js +83 -0
  58. package/dist/src/extensions/game/mailbox.js +61 -0
  59. package/dist/src/extensions/game/system-prompt.js +134 -0
  60. package/dist/src/extensions/game/tools.js +28 -0
  61. package/dist/src/extensions/lifecycle.js +337 -0
  62. package/dist/src/extensions/mode-runtime.js +26 -2
  63. package/dist/src/extensions/mono-game.js +66 -0
  64. package/dist/src/extensions/mono-pilot.js +100 -18
  65. package/dist/src/extensions/nvim.js +47 -0
  66. package/dist/src/extensions/session-hints.js +60 -35
  67. package/dist/src/extensions/sftp.js +897 -0
  68. package/dist/src/extensions/status.js +676 -0
  69. package/dist/src/extensions/system-events.js +478 -0
  70. package/dist/src/extensions/system-prompt.js +24 -14
  71. package/dist/src/extensions/user-message.js +94 -50
  72. package/dist/src/lsp/client.js +235 -0
  73. package/dist/src/lsp/index.js +165 -0
  74. package/dist/src/lsp/runtime.js +67 -0
  75. package/dist/src/lsp/server.js +242 -0
  76. package/dist/src/mcp/config.js +112 -0
  77. package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
  78. package/dist/src/mcp/servers.js +90 -0
  79. package/dist/src/memory/build-memory.js +103 -0
  80. package/dist/src/memory/config/defaults.js +55 -0
  81. package/dist/src/memory/config/loader.js +29 -0
  82. package/dist/src/memory/config/paths.js +9 -0
  83. package/dist/src/memory/config/resolve.js +90 -0
  84. package/dist/src/memory/config/types.js +1 -0
  85. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  86. package/dist/src/memory/embeddings/cache.js +47 -0
  87. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  88. package/dist/src/memory/embeddings/input-limits.js +48 -0
  89. package/dist/src/memory/embeddings/local.js +108 -0
  90. package/dist/src/memory/embeddings/types.js +1 -0
  91. package/dist/src/memory/index-manager.js +552 -0
  92. package/dist/src/memory/indexing/embeddings.js +67 -0
  93. package/dist/src/memory/indexing/files.js +180 -0
  94. package/dist/src/memory/indexing/index-file.js +105 -0
  95. package/dist/src/memory/log.js +38 -0
  96. package/dist/src/memory/paths.js +15 -0
  97. package/dist/src/memory/runtime/index.js +299 -0
  98. package/dist/src/memory/runtime/thread.js +116 -0
  99. package/dist/src/memory/search/fts.js +57 -0
  100. package/dist/src/memory/search/hybrid.js +50 -0
  101. package/dist/src/memory/search/text.js +30 -0
  102. package/dist/src/memory/search/vector.js +43 -0
  103. package/dist/src/memory/session/content-hash.js +7 -0
  104. package/dist/src/memory/session/entry.js +33 -0
  105. package/dist/src/memory/session/flush-policy.js +34 -0
  106. package/dist/src/memory/session/hook.js +191 -0
  107. package/dist/src/memory/session/paths.js +15 -0
  108. package/dist/src/memory/session/session-reader.js +88 -0
  109. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  110. package/dist/src/memory/session/transcript/entry.js +28 -0
  111. package/dist/src/memory/session/transcript/flush.js +56 -0
  112. package/dist/src/memory/session/transcript/paths.js +28 -0
  113. package/dist/src/memory/session/transcript/reader.js +112 -0
  114. package/dist/src/memory/session/transcript/state.js +31 -0
  115. package/dist/src/memory/store/schema.js +89 -0
  116. package/dist/src/memory/store/sqlite.js +89 -0
  117. package/dist/src/memory/types.js +1 -0
  118. package/dist/src/memory/warm.js +25 -0
  119. package/dist/src/rules/discovery.js +41 -0
  120. package/dist/{tools → src/tools}/README.md +29 -3
  121. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  122. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  123. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  124. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  125. package/dist/src/tools/ast-grep.js +357 -0
  126. package/dist/src/tools/brief-write.js +122 -0
  127. package/dist/src/tools/bus-send.js +100 -0
  128. package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
  129. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  130. package/dist/src/tools/codex-apply-patch.js +540 -0
  131. package/dist/{tools → src/tools}/delete.js +24 -0
  132. package/dist/src/tools/exit-plan-mode.js +83 -0
  133. package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
  134. package/dist/src/tools/generate-image.js +567 -0
  135. package/dist/{tools → src/tools}/glob.js +55 -1
  136. package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
  137. package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
  138. package/dist/src/tools/ls.js +48 -0
  139. package/dist/src/tools/lsp-diagnostics.js +67 -0
  140. package/dist/src/tools/lsp-symbols.js +54 -0
  141. package/dist/src/tools/mailbox.js +85 -0
  142. package/dist/src/tools/memory-get.js +90 -0
  143. package/dist/src/tools/memory-search.js +180 -0
  144. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  145. package/dist/{tools → src/tools}/read-file.js +8 -19
  146. package/dist/{tools → src/tools}/rg.js +10 -20
  147. package/dist/{tools → src/tools}/shell.js +19 -42
  148. package/dist/{tools → src/tools}/subagent.js +255 -6
  149. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  150. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  151. package/dist/{tools → src/tools}/web-search.js +29 -1
  152. package/package.json +21 -9
  153. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  154. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  155. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  156. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  157. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  158. /package/dist/{tools → src/tools}/subagent-description.md +0 -0
@@ -0,0 +1,357 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import { keyHint } from "@mariozechner/pi-coding-agent";
5
+ import { Text } from "@mariozechner/pi-tui";
6
+ import { Type } from "@sinclair/typebox";
7
+ const DESCRIPTION = `Search code structure using ast-grep (AST-aware search).
8
+
9
+ - Supports two modes: "run" (inline pattern) and "scan" (rule-based)
10
+ - Read-only by design: rewrite/update flags are intentionally not exposed
11
+ - Default mode is "run"
12
+ - Use output_mode to return content, unique files, or match counts
13
+ - For Python and other indentation-sensitive languages, provide syntactically complete patterns`;
14
+ const astGrepSchema = Type.Object({
15
+ mode: Type.Optional(Type.Union([Type.Literal("run"), Type.Literal("scan")], {
16
+ description: 'Execution mode: "run" for inline pattern search, "scan" for rule-based scanning. Default: "run".',
17
+ })),
18
+ pattern: Type.Optional(Type.String({
19
+ description: 'AST pattern used by mode="run" (required for run mode).',
20
+ })),
21
+ path: Type.Optional(Type.String({
22
+ description: "File or directory to search in. Defaults to workspace root.",
23
+ })),
24
+ lang: Type.Optional(Type.String({
25
+ description: "Language for pattern parsing in run mode (e.g. ts, js, python, go).",
26
+ })),
27
+ selector: Type.Optional(Type.String({
28
+ description: "AST node kind selector for run mode (maps to --selector).",
29
+ })),
30
+ strictness: Type.Optional(Type.Union([
31
+ Type.Literal("cst"),
32
+ Type.Literal("smart"),
33
+ Type.Literal("ast"),
34
+ Type.Literal("relaxed"),
35
+ Type.Literal("signature"),
36
+ ], {
37
+ description: "Pattern strictness for run mode.",
38
+ })),
39
+ globs: Type.Optional(Type.Array(Type.String(), {
40
+ description: "Include/exclude globs (maps to repeated --globs; use !prefix to exclude).",
41
+ })),
42
+ rule: Type.Optional(Type.String({
43
+ description: "Rule file path for scan mode (maps to --rule).",
44
+ })),
45
+ inline_rules: Type.Optional(Type.String({
46
+ description: "Inline YAML rule text for scan mode (maps to --inline-rules).",
47
+ })),
48
+ filter: Type.Optional(Type.String({
49
+ description: "Rule id regex filter for scan mode (maps to --filter).",
50
+ })),
51
+ output_mode: Type.Optional(Type.Union([Type.Literal("content"), Type.Literal("files_with_matches"), Type.Literal("count")], {
52
+ description: 'Output mode: "content" shows matched snippets, "files_with_matches" returns unique file paths, "count" returns per-file counts. Default: "content".',
53
+ })),
54
+ head_limit: Type.Optional(Type.Number({
55
+ description: "Maximum entries returned after offset. Default: 50.",
56
+ minimum: 0,
57
+ })),
58
+ offset: Type.Optional(Type.Number({
59
+ description: "Skip first N entries (pagination). Default: 0.",
60
+ minimum: 0,
61
+ })),
62
+ });
63
+ function toNonNegativeInteger(value, fallback) {
64
+ if (value === undefined || !Number.isFinite(value) || Number.isNaN(value))
65
+ return fallback;
66
+ return Math.max(0, Math.floor(value));
67
+ }
68
+ function normalizeTextOutput(text) {
69
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
70
+ }
71
+ function compactWhitespace(text) {
72
+ const oneLine = normalizeTextOutput(text).replace(/\s+/g, " ").trim();
73
+ if (oneLine.length <= 220)
74
+ return oneLine;
75
+ return `${oneLine.slice(0, 219)}…`;
76
+ }
77
+ function resolveSearchPath(pathArg, workspaceCwd) {
78
+ if (!pathArg || pathArg.trim().length === 0)
79
+ return workspaceCwd;
80
+ const resolvedPath = isAbsolute(pathArg) ? pathArg : resolve(workspaceCwd, pathArg);
81
+ if (!existsSync(resolvedPath)) {
82
+ throw new Error(`Search path does not exist: ${resolvedPath}`);
83
+ }
84
+ return resolvedPath;
85
+ }
86
+ function resolveRulePath(pathArg, workspaceCwd) {
87
+ const resolvedPath = isAbsolute(pathArg) ? pathArg : resolve(workspaceCwd, pathArg);
88
+ if (!existsSync(resolvedPath)) {
89
+ throw new Error(`Rule file does not exist: ${resolvedPath}`);
90
+ }
91
+ return resolvedPath;
92
+ }
93
+ function buildRunArgs(input, searchPath) {
94
+ const pattern = input.pattern?.trim();
95
+ if (!pattern) {
96
+ throw new Error('pattern is required when mode="run".');
97
+ }
98
+ const args = ["run", "--pattern", pattern, "--json=stream"];
99
+ if (input.lang?.trim())
100
+ args.push("--lang", input.lang.trim());
101
+ if (input.selector?.trim())
102
+ args.push("--selector", input.selector.trim());
103
+ if (input.strictness)
104
+ args.push("--strictness", input.strictness);
105
+ for (const glob of input.globs ?? []) {
106
+ if (glob.trim().length === 0)
107
+ continue;
108
+ args.push("--globs", glob);
109
+ }
110
+ args.push(searchPath);
111
+ return args;
112
+ }
113
+ function buildScanArgs(input, searchPath, workspaceCwd) {
114
+ if (input.rule && input.inline_rules) {
115
+ throw new Error('scan mode accepts either "rule" or "inline_rules", not both.');
116
+ }
117
+ const args = ["scan", "--json=stream"];
118
+ if (input.rule?.trim()) {
119
+ args.push("--rule", resolveRulePath(input.rule.trim(), workspaceCwd));
120
+ }
121
+ if (input.inline_rules?.trim()) {
122
+ args.push("--inline-rules", input.inline_rules);
123
+ }
124
+ if (input.filter?.trim()) {
125
+ args.push("--filter", input.filter.trim());
126
+ }
127
+ for (const glob of input.globs ?? []) {
128
+ if (glob.trim().length === 0)
129
+ continue;
130
+ args.push("--globs", glob);
131
+ }
132
+ args.push(searchPath);
133
+ return args;
134
+ }
135
+ function parseJsonLine(line) {
136
+ try {
137
+ const parsed = JSON.parse(line);
138
+ if (typeof parsed === "object" && parsed !== null)
139
+ return parsed;
140
+ return undefined;
141
+ }
142
+ catch {
143
+ return undefined;
144
+ }
145
+ }
146
+ function getString(record, key) {
147
+ const value = record[key];
148
+ return typeof value === "string" ? value : undefined;
149
+ }
150
+ function parseMatches(stdout) {
151
+ const lines = normalizeTextOutput(stdout).split("\n");
152
+ const matches = [];
153
+ for (const rawLine of lines) {
154
+ if (rawLine.trim().length === 0)
155
+ continue;
156
+ const payload = parseJsonLine(rawLine);
157
+ if (!payload)
158
+ continue;
159
+ const file = getString(payload, "file");
160
+ if (!file)
161
+ continue;
162
+ const range = payload.range;
163
+ if (typeof range !== "object" || range === null)
164
+ continue;
165
+ const start = range.start;
166
+ if (typeof start !== "object" || start === null)
167
+ continue;
168
+ const lineRaw = start.line;
169
+ const colRaw = start.column;
170
+ const line = typeof lineRaw === "number" ? lineRaw + 1 : 1;
171
+ const column = typeof colRaw === "number" ? colRaw + 1 : 1;
172
+ const snippet = compactWhitespace(getString(payload, "lines") ?? getString(payload, "text") ?? "");
173
+ const ruleId = getString(payload, "ruleId");
174
+ matches.push({
175
+ file,
176
+ line,
177
+ column,
178
+ snippet,
179
+ ruleId,
180
+ });
181
+ }
182
+ return matches;
183
+ }
184
+ function uniqueFiles(matches) {
185
+ const seen = new Set();
186
+ const files = [];
187
+ for (const match of matches) {
188
+ if (seen.has(match.file))
189
+ continue;
190
+ seen.add(match.file);
191
+ files.push(match.file);
192
+ }
193
+ return files;
194
+ }
195
+ function fileCounts(matches) {
196
+ const counts = new Map();
197
+ for (const match of matches) {
198
+ counts.set(match.file, (counts.get(match.file) ?? 0) + 1);
199
+ }
200
+ return Array.from(counts.entries())
201
+ .sort((a, b) => a[0].localeCompare(b[0]))
202
+ .map(([file, count]) => `${file}:${count}`);
203
+ }
204
+ function applyPagination(entries, offset, headLimit) {
205
+ const start = Math.min(offset, entries.length);
206
+ const afterOffset = entries.slice(start);
207
+ const pagedEntries = afterOffset.slice(0, headLimit);
208
+ return {
209
+ pagedEntries,
210
+ totalAfterOffset: afterOffset.length,
211
+ headLimitTruncated: afterOffset.length > pagedEntries.length,
212
+ };
213
+ }
214
+ async function runAstGrep(args, cwd, signal) {
215
+ return await new Promise((resolvePromise, rejectPromise) => {
216
+ const child = spawn("ast-grep", args, {
217
+ cwd,
218
+ stdio: ["ignore", "pipe", "pipe"],
219
+ });
220
+ let stdout = "";
221
+ let stderr = "";
222
+ let settled = false;
223
+ const cleanupAbort = () => {
224
+ if (!signal)
225
+ return;
226
+ signal.removeEventListener("abort", onAbort);
227
+ };
228
+ const finalize = (result) => {
229
+ if (settled)
230
+ return;
231
+ settled = true;
232
+ cleanupAbort();
233
+ resolvePromise(result);
234
+ };
235
+ const fail = (error) => {
236
+ if (settled)
237
+ return;
238
+ settled = true;
239
+ cleanupAbort();
240
+ rejectPromise(error);
241
+ };
242
+ const onAbort = () => {
243
+ child.kill("SIGTERM");
244
+ fail(new Error("Operation aborted"));
245
+ };
246
+ if (signal) {
247
+ if (signal.aborted) {
248
+ onAbort();
249
+ return;
250
+ }
251
+ signal.addEventListener("abort", onAbort, { once: true });
252
+ }
253
+ child.stdout.on("data", (chunk) => {
254
+ stdout += chunk.toString("utf-8");
255
+ });
256
+ child.stderr.on("data", (chunk) => {
257
+ stderr += chunk.toString("utf-8");
258
+ });
259
+ child.on("error", (error) => {
260
+ if (error.code === "ENOENT") {
261
+ fail(new Error("ast-grep executable not found in PATH. Install ast-grep and try again."));
262
+ return;
263
+ }
264
+ fail(error);
265
+ });
266
+ child.on("close", (code) => {
267
+ finalize({ code, stdout, stderr });
268
+ });
269
+ });
270
+ }
271
+ function resolveOutputEntries(matches, outputMode) {
272
+ if (outputMode === "files_with_matches") {
273
+ return uniqueFiles(matches);
274
+ }
275
+ if (outputMode === "count") {
276
+ return fileCounts(matches);
277
+ }
278
+ return matches.map((match) => {
279
+ const rulePart = match.ruleId ? ` [rule=${match.ruleId}]` : "";
280
+ return `${match.file}:${match.line}:${match.column}:${rulePart} ${match.snippet}`.trim();
281
+ });
282
+ }
283
+ export default function astGrepExtension(pi) {
284
+ pi.registerTool({
285
+ name: "AstGrep",
286
+ label: "AstGrep",
287
+ description: DESCRIPTION,
288
+ parameters: astGrepSchema,
289
+ renderCall(args, theme) {
290
+ const input = args;
291
+ const mode = input.mode === "scan" ? "scan" : "run";
292
+ const mainArg = mode === "run"
293
+ ? (typeof input.pattern === "string" && input.pattern.trim().length > 0 ? input.pattern.trim() : "(missing pattern)")
294
+ : (typeof input.rule === "string" && input.rule.trim().length > 0 ? input.rule.trim() : "(scan)");
295
+ let text = theme.fg("toolTitle", theme.bold("AstGrep"));
296
+ text += ` ${theme.fg("toolOutput", `${mode} ${mainArg}`)}`;
297
+ return new Text(text, 0, 0);
298
+ },
299
+ renderResult(result, { expanded, isPartial }, theme) {
300
+ if (isPartial) {
301
+ return new Text(theme.fg("muted", "Searching AST..."), 0, 0);
302
+ }
303
+ const textBlock = result.content.find((entry) => entry.type === "text" && typeof entry.text === "string");
304
+ if (!textBlock) {
305
+ return new Text(theme.fg("error", "No output."), 0, 0);
306
+ }
307
+ const fullText = textBlock.text;
308
+ const lineCount = fullText.split("\n").filter((line) => line.trim().length > 0).length;
309
+ if (!expanded) {
310
+ const summary = `${lineCount} entries (click or ${keyHint("expandTools", "to expand")})`;
311
+ return new Text(theme.fg("muted", summary), 0, 0);
312
+ }
313
+ let text = fullText
314
+ .split("\n")
315
+ .map((line) => theme.fg("toolOutput", line))
316
+ .join("\n");
317
+ text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
318
+ return new Text(text, 0, 0);
319
+ },
320
+ async execute(_id, input, signal, _onUpdate, ctx) {
321
+ const mode = input.mode === "scan" ? "scan" : "run";
322
+ const outputMode = input.output_mode === "files_with_matches" || input.output_mode === "count" ? input.output_mode : "content";
323
+ const offset = toNonNegativeInteger(input.offset, 0);
324
+ const headLimit = toNonNegativeInteger(input.head_limit, 50);
325
+ const searchPath = resolveSearchPath(input.path, ctx.cwd);
326
+ const args = mode === "run" ? buildRunArgs(input, searchPath) : buildScanArgs(input, searchPath, ctx.cwd);
327
+ const result = await runAstGrep(args, ctx.cwd, signal);
328
+ const stderr = normalizeTextOutput(result.stderr).trim();
329
+ if (result.code !== 0 && result.code !== 1) {
330
+ throw new Error(stderr.length > 0 ? `ast-grep failed (${result.code}): ${stderr}` : `ast-grep failed (${result.code})`);
331
+ }
332
+ if (stderr.length > 0) {
333
+ throw new Error(`ast-grep stderr: ${stderr}`);
334
+ }
335
+ const matches = parseMatches(result.stdout);
336
+ const entries = resolveOutputEntries(matches, outputMode);
337
+ const { pagedEntries, totalAfterOffset, headLimitTruncated } = applyPagination(entries, offset, headLimit);
338
+ const text = pagedEntries.length > 0 ? pagedEntries.join("\n") : "No matches found.";
339
+ const details = {
340
+ mode,
341
+ output_mode: outputMode,
342
+ search_path: searchPath,
343
+ offset,
344
+ head_limit: headLimit,
345
+ returned_entries: pagedEntries.length,
346
+ total_entries_after_offset: totalAfterOffset,
347
+ head_limit_truncated: headLimitTruncated,
348
+ match_count: matches.length,
349
+ rewrite_enabled: false,
350
+ };
351
+ return {
352
+ content: [{ type: "text", text }],
353
+ details,
354
+ };
355
+ },
356
+ });
357
+ }
@@ -0,0 +1,122 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { keyHint } from "@mariozechner/pi-coding-agent";
4
+ import { Text } from "@mariozechner/pi-tui";
5
+ import { Type } from "@sinclair/typebox";
6
+ import { deriveAgentId } from "../agents-paths.js";
7
+ import { resolveBriefPath } from "../brief/paths.js";
8
+ import { countBodyLines, parseFrontmatter, serializeWithFrontmatter } from "../brief/frontmatter.js";
9
+ const DESCRIPTION = "Write content to a brief file in the current agent's brief directory " +
10
+ "(~/.mono-pilot/agents/<agent-id>/brief/). " +
11
+ "The brief is injected into your system prompt as the <brief> block — update it when you learn something worth remembering.\n" +
12
+ "Common paths: human/identity.md (who the user is), human/prefs/*.md (communication, coding style), " +
13
+ "project/overview.md, project/conventions.md, project/commands.md, project/gotchas.md, tasks/current.md.\n" +
14
+ "Preserves YAML frontmatter (description, limit). Validates body does not exceed the file's line limit. " +
15
+ "Creates the file with default frontmatter if it does not exist.";
16
+ const DEFAULT_LIMIT = 50;
17
+ const DEFAULT_DESCRIPTION = "Agent-created brief file.";
18
+ const briefWriteSchema = Type.Object({
19
+ path: Type.String({
20
+ description: 'Relative path within the brief directory (e.g. "human/prefs/communication.md", "project/gotchas.md")',
21
+ }),
22
+ content: Type.String({
23
+ description: "The body content to write (without frontmatter). Frontmatter is preserved automatically.",
24
+ }),
25
+ mode: Type.Optional(Type.Union([Type.Literal("overwrite"), Type.Literal("append")], {
26
+ description: 'Write mode: "overwrite" replaces body, "append" adds to end. Default: "overwrite"',
27
+ })),
28
+ });
29
+ export default function (pi) {
30
+ pi.registerTool({
31
+ name: "BriefWrite",
32
+ label: "BriefWrite",
33
+ description: DESCRIPTION,
34
+ parameters: briefWriteSchema,
35
+ renderCall(args, theme) {
36
+ const input = args;
37
+ const pathArg = typeof input.path === "string" && input.path.trim().length > 0
38
+ ? input.path
39
+ : "(missing path)";
40
+ const mode = input.mode ?? "overwrite";
41
+ let text = theme.fg("toolTitle", theme.bold("BriefWrite"));
42
+ text += ` ${theme.fg("toolOutput", `${pathArg} (${mode})`)}`;
43
+ return new Text(text, 0, 0);
44
+ },
45
+ renderResult(result, { expanded, isPartial }, theme) {
46
+ if (isPartial) {
47
+ return new Text(theme.fg("muted", "Writing..."), 0, 0);
48
+ }
49
+ const textBlock = result.content.find((entry) => entry.type === "text" && typeof entry.text === "string");
50
+ if (!textBlock) {
51
+ return new Text(theme.fg("error", "No text result returned."), 0, 0);
52
+ }
53
+ const statusLine = textBlock.text;
54
+ const details = result.details;
55
+ const body = typeof details?.body === "string" ? details.body : undefined;
56
+ if (!expanded) {
57
+ const summary = `${statusLine} (click or ${keyHint("expandTools", "to expand")})`;
58
+ return new Text(theme.fg("muted", summary), 0, 0);
59
+ }
60
+ let text = theme.fg("toolOutput", statusLine);
61
+ if (body) {
62
+ text += "\n" + body
63
+ .split("\n")
64
+ .map((line) => theme.fg("toolOutput", line))
65
+ .join("\n");
66
+ }
67
+ text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
68
+ return new Text(text, 0, 0);
69
+ },
70
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
71
+ const agentId = deriveAgentId(ctx.cwd);
72
+ const writeMode = params.mode ?? "overwrite";
73
+ const filePath = resolveBriefPath(params.path, agentId);
74
+ let frontmatter = { description: DEFAULT_DESCRIPTION, limit: DEFAULT_LIMIT };
75
+ let existingBody = "";
76
+ if (existsSync(filePath)) {
77
+ try {
78
+ const raw = readFileSync(filePath, "utf-8");
79
+ const parsed = parseFrontmatter(raw);
80
+ frontmatter = {
81
+ description: parsed.frontmatter.description ?? DEFAULT_DESCRIPTION,
82
+ limit: parsed.frontmatter.limit ?? DEFAULT_LIMIT,
83
+ };
84
+ existingBody = parsed.body;
85
+ }
86
+ catch {
87
+ // Fall through with defaults
88
+ }
89
+ }
90
+ const newBody = writeMode === "append"
91
+ ? (existingBody.trim() + "\n" + params.content).trim()
92
+ : params.content.trim();
93
+ const lineCount = countBodyLines(newBody);
94
+ if (lineCount > frontmatter.limit) {
95
+ return {
96
+ content: [{
97
+ type: "text",
98
+ text: `Rejected: content has ${lineCount} lines but file limit is ${frontmatter.limit}. ` +
99
+ `Condense the content or increase the limit in frontmatter.`,
100
+ }],
101
+ details: { status: "limit_exceeded", path: params.path, agentId, lineCount, limit: frontmatter.limit },
102
+ };
103
+ }
104
+ try {
105
+ const dir = dirname(filePath);
106
+ mkdirSync(dir, { recursive: true });
107
+ writeFileSync(filePath, serializeWithFrontmatter(frontmatter, newBody), "utf-8");
108
+ return {
109
+ content: [{ type: "text", text: `Written ${lineCount} lines to ${params.path}` }],
110
+ details: { status: "ok", path: params.path, agentId, lineCount, body: newBody },
111
+ };
112
+ }
113
+ catch (error) {
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ return {
116
+ content: [{ type: "text", text: `Error writing brief file: ${message}` }],
117
+ details: { status: "error", path: params.path, agentId, error: message },
118
+ };
119
+ }
120
+ },
121
+ });
122
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * bus_send tool — lets the agent send messages to other agents via the cluster bus.
3
+ */
4
+ import { keyHint } from "@mariozechner/pi-coding-agent";
5
+ import { Text } from "@mariozechner/pi-tui";
6
+ let activeBus = null;
7
+ let defaultBroadcastChannel;
8
+ export function setBusSendHandle(bus) {
9
+ activeBus = bus;
10
+ }
11
+ export function setBusSendDefaultChannel(channel) {
12
+ defaultBroadcastChannel = channel;
13
+ }
14
+ import { Type } from "@sinclair/typebox";
15
+ const busSendSchema = Type.Object({
16
+ to: Type.String({
17
+ description: "Target displayName/agentId or channel.",
18
+ }),
19
+ message: Type.String({ description: "The message text to send." }),
20
+ });
21
+ const busSendExtension = (pi) => {
22
+ pi.registerTool({
23
+ name: "BusSend",
24
+ label: "BusSend",
25
+ description: "Send a message to a displayName/agentId or broadcast to a channel via the cluster message bus.",
26
+ parameters: busSendSchema,
27
+ renderCall(args, theme) {
28
+ const input = args;
29
+ const target = input.to ? `→ ${input.to}` : `→ (missing)`;
30
+ return new Text(`${theme.fg("toolTitle", theme.bold("BusSend"))} ${theme.fg("toolOutput", target)}`, 0, 0);
31
+ },
32
+ renderResult(result, { expanded, isPartial }, theme) {
33
+ if (isPartial)
34
+ return new Text(theme.fg("muted", "Sending..."), 0, 0);
35
+ const text = result.content.find((e) => e.type === "text")?.text ?? "";
36
+ const details = result.details;
37
+ const color = text.startsWith("Send failed") ? "error" : "success";
38
+ if (!expanded) {
39
+ const summary = `${theme.fg(color, text)} ` +
40
+ `${theme.fg("muted", `(click or ${keyHint("expandTools", "to expand")})`)}`;
41
+ return new Text(summary, 0, 0);
42
+ }
43
+ const messageLine = details?.message
44
+ ? `${theme.fg("muted", "message:")} ${theme.fg("toolOutput", details.message)}`
45
+ : "";
46
+ let body = messageLine ? `${messageLine}\n${theme.fg(color, text)}` : theme.fg(color, text);
47
+ body += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
48
+ return new Text(body, 0, 0);
49
+ },
50
+ async execute(_toolCallId, params) {
51
+ const { to, message } = params;
52
+ if (!activeBus) {
53
+ return {
54
+ content: [{ type: "text", text: "Bus not connected." }],
55
+ details: { status: "not_connected", to, message },
56
+ };
57
+ }
58
+ try {
59
+ {
60
+ const isChannel = to.includes(":") || to === "public";
61
+ if (isChannel) {
62
+ const { seq, delivered } = await activeBus.broadcast({ text: message }, to);
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: `Broadcast to ${to} (seq=${seq}, ${delivered} recipients)`,
68
+ },
69
+ ],
70
+ details: { status: "broadcast", to, message },
71
+ };
72
+ }
73
+ const { agentId, displayName } = await activeBus.resolveTarget(to);
74
+ const privateChannel = `private:${agentId}`;
75
+ const { seq, delivered } = await activeBus.broadcast({ text: message }, privateChannel);
76
+ const label = displayName?.trim()
77
+ ? `${displayName} (${agentId})`
78
+ : agentId;
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: `Sent to ${label} via ${privateChannel} (seq=${seq}, ${delivered} recipients)`,
84
+ },
85
+ ],
86
+ details: { status: "sent", to, message },
87
+ };
88
+ }
89
+ }
90
+ catch (err) {
91
+ const msg = err.message;
92
+ return {
93
+ content: [{ type: "text", text: `Send failed: ${msg}` }],
94
+ details: { status: "error", to, message, error: msg },
95
+ };
96
+ }
97
+ },
98
+ });
99
+ };
100
+ export default busSendExtension;