@geminixiang/mikan 0.2.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 (316) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/LICENSE +22 -0
  3. package/README.md +297 -0
  4. package/dist/adapter.d.ts +134 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +2 -0
  7. package/dist/adapter.js.map +1 -0
  8. package/dist/adapters/discord/bot.d.ts +63 -0
  9. package/dist/adapters/discord/bot.d.ts.map +1 -0
  10. package/dist/adapters/discord/bot.js +577 -0
  11. package/dist/adapters/discord/bot.js.map +1 -0
  12. package/dist/adapters/discord/context.d.ts +9 -0
  13. package/dist/adapters/discord/context.d.ts.map +1 -0
  14. package/dist/adapters/discord/context.js +245 -0
  15. package/dist/adapters/discord/context.js.map +1 -0
  16. package/dist/adapters/discord/index.d.ts +3 -0
  17. package/dist/adapters/discord/index.d.ts.map +1 -0
  18. package/dist/adapters/discord/index.js +3 -0
  19. package/dist/adapters/discord/index.js.map +1 -0
  20. package/dist/adapters/shared.d.ts +91 -0
  21. package/dist/adapters/shared.d.ts.map +1 -0
  22. package/dist/adapters/shared.js +191 -0
  23. package/dist/adapters/shared.js.map +1 -0
  24. package/dist/adapters/slack/bot.d.ts +139 -0
  25. package/dist/adapters/slack/bot.d.ts.map +1 -0
  26. package/dist/adapters/slack/bot.js +1272 -0
  27. package/dist/adapters/slack/bot.js.map +1 -0
  28. package/dist/adapters/slack/branch-manager.d.ts +28 -0
  29. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  30. package/dist/adapters/slack/branch-manager.js +117 -0
  31. package/dist/adapters/slack/branch-manager.js.map +1 -0
  32. package/dist/adapters/slack/context.d.ts +12 -0
  33. package/dist/adapters/slack/context.d.ts.map +1 -0
  34. package/dist/adapters/slack/context.js +327 -0
  35. package/dist/adapters/slack/context.js.map +1 -0
  36. package/dist/adapters/slack/index.d.ts +3 -0
  37. package/dist/adapters/slack/index.d.ts.map +1 -0
  38. package/dist/adapters/slack/index.js +3 -0
  39. package/dist/adapters/slack/index.js.map +1 -0
  40. package/dist/adapters/slack/session.d.ts +38 -0
  41. package/dist/adapters/slack/session.d.ts.map +1 -0
  42. package/dist/adapters/slack/session.js +66 -0
  43. package/dist/adapters/slack/session.js.map +1 -0
  44. package/dist/adapters/slack/tools/attach.d.ts +12 -0
  45. package/dist/adapters/slack/tools/attach.d.ts.map +1 -0
  46. package/dist/adapters/slack/tools/attach.js +40 -0
  47. package/dist/adapters/slack/tools/attach.js.map +1 -0
  48. package/dist/adapters/telegram/bot.d.ts +51 -0
  49. package/dist/adapters/telegram/bot.d.ts.map +1 -0
  50. package/dist/adapters/telegram/bot.js +430 -0
  51. package/dist/adapters/telegram/bot.js.map +1 -0
  52. package/dist/adapters/telegram/context.d.ts +9 -0
  53. package/dist/adapters/telegram/context.d.ts.map +1 -0
  54. package/dist/adapters/telegram/context.js +190 -0
  55. package/dist/adapters/telegram/context.js.map +1 -0
  56. package/dist/adapters/telegram/html.d.ts +3 -0
  57. package/dist/adapters/telegram/html.d.ts.map +1 -0
  58. package/dist/adapters/telegram/html.js +98 -0
  59. package/dist/adapters/telegram/html.js.map +1 -0
  60. package/dist/adapters/telegram/index.d.ts +3 -0
  61. package/dist/adapters/telegram/index.d.ts.map +1 -0
  62. package/dist/adapters/telegram/index.js +3 -0
  63. package/dist/adapters/telegram/index.js.map +1 -0
  64. package/dist/agent.d.ts +36 -0
  65. package/dist/agent.d.ts.map +1 -0
  66. package/dist/agent.js +1147 -0
  67. package/dist/agent.js.map +1 -0
  68. package/dist/commands/auto-reply.d.ts +5 -0
  69. package/dist/commands/auto-reply.d.ts.map +1 -0
  70. package/dist/commands/auto-reply.js +79 -0
  71. package/dist/commands/auto-reply.js.map +1 -0
  72. package/dist/commands/index.d.ts +5 -0
  73. package/dist/commands/index.d.ts.map +1 -0
  74. package/dist/commands/index.js +18 -0
  75. package/dist/commands/index.js.map +1 -0
  76. package/dist/commands/login.d.ts +5 -0
  77. package/dist/commands/login.d.ts.map +1 -0
  78. package/dist/commands/login.js +91 -0
  79. package/dist/commands/login.js.map +1 -0
  80. package/dist/commands/model.d.ts +14 -0
  81. package/dist/commands/model.d.ts.map +1 -0
  82. package/dist/commands/model.js +110 -0
  83. package/dist/commands/model.js.map +1 -0
  84. package/dist/commands/new.d.ts +5 -0
  85. package/dist/commands/new.d.ts.map +1 -0
  86. package/dist/commands/new.js +24 -0
  87. package/dist/commands/new.js.map +1 -0
  88. package/dist/commands/parse.d.ts +7 -0
  89. package/dist/commands/parse.d.ts.map +1 -0
  90. package/dist/commands/parse.js +17 -0
  91. package/dist/commands/parse.js.map +1 -0
  92. package/dist/commands/registry.d.ts +4 -0
  93. package/dist/commands/registry.d.ts.map +1 -0
  94. package/dist/commands/registry.js +9 -0
  95. package/dist/commands/registry.js.map +1 -0
  96. package/dist/commands/sandbox.d.ts +10 -0
  97. package/dist/commands/sandbox.d.ts.map +1 -0
  98. package/dist/commands/sandbox.js +83 -0
  99. package/dist/commands/sandbox.js.map +1 -0
  100. package/dist/commands/session-view.d.ts +5 -0
  101. package/dist/commands/session-view.d.ts.map +1 -0
  102. package/dist/commands/session-view.js +62 -0
  103. package/dist/commands/session-view.js.map +1 -0
  104. package/dist/commands/types.d.ts +41 -0
  105. package/dist/commands/types.d.ts.map +1 -0
  106. package/dist/commands/types.js +2 -0
  107. package/dist/commands/types.js.map +1 -0
  108. package/dist/commands/utils.d.ts +8 -0
  109. package/dist/commands/utils.d.ts.map +1 -0
  110. package/dist/commands/utils.js +14 -0
  111. package/dist/commands/utils.js.map +1 -0
  112. package/dist/config.d.ts +59 -0
  113. package/dist/config.d.ts.map +1 -0
  114. package/dist/config.js +370 -0
  115. package/dist/config.js.map +1 -0
  116. package/dist/context.d.ts +17 -0
  117. package/dist/context.d.ts.map +1 -0
  118. package/dist/context.js +24 -0
  119. package/dist/context.js.map +1 -0
  120. package/dist/conversation-history.d.ts +16 -0
  121. package/dist/conversation-history.d.ts.map +1 -0
  122. package/dist/conversation-history.js +144 -0
  123. package/dist/conversation-history.js.map +1 -0
  124. package/dist/download.d.ts +2 -0
  125. package/dist/download.d.ts.map +1 -0
  126. package/dist/download.js +89 -0
  127. package/dist/download.js.map +1 -0
  128. package/dist/env.d.ts +3 -0
  129. package/dist/env.d.ts.map +1 -0
  130. package/dist/env.js +12 -0
  131. package/dist/env.js.map +1 -0
  132. package/dist/events.d.ts +85 -0
  133. package/dist/events.d.ts.map +1 -0
  134. package/dist/events.js +483 -0
  135. package/dist/events.js.map +1 -0
  136. package/dist/execution-resolver.d.ts +25 -0
  137. package/dist/execution-resolver.d.ts.map +1 -0
  138. package/dist/execution-resolver.js +167 -0
  139. package/dist/execution-resolver.js.map +1 -0
  140. package/dist/file-guards.d.ts +9 -0
  141. package/dist/file-guards.d.ts.map +1 -0
  142. package/dist/file-guards.js +56 -0
  143. package/dist/file-guards.js.map +1 -0
  144. package/dist/fs-atomic.d.ts +10 -0
  145. package/dist/fs-atomic.d.ts.map +1 -0
  146. package/dist/fs-atomic.js +45 -0
  147. package/dist/fs-atomic.js.map +1 -0
  148. package/dist/index.d.ts +10 -0
  149. package/dist/index.d.ts.map +1 -0
  150. package/dist/index.js +7 -0
  151. package/dist/index.js.map +1 -0
  152. package/dist/instrument.d.ts +2 -0
  153. package/dist/instrument.d.ts.map +1 -0
  154. package/dist/instrument.js +10 -0
  155. package/dist/instrument.js.map +1 -0
  156. package/dist/log.d.ts +36 -0
  157. package/dist/log.d.ts.map +1 -0
  158. package/dist/log.js +206 -0
  159. package/dist/log.js.map +1 -0
  160. package/dist/login/index.d.ts +42 -0
  161. package/dist/login/index.d.ts.map +1 -0
  162. package/dist/login/index.js +239 -0
  163. package/dist/login/index.js.map +1 -0
  164. package/dist/login/portal.d.ts +19 -0
  165. package/dist/login/portal.d.ts.map +1 -0
  166. package/dist/login/portal.js +1544 -0
  167. package/dist/login/portal.js.map +1 -0
  168. package/dist/login/session.d.ts +26 -0
  169. package/dist/login/session.d.ts.map +1 -0
  170. package/dist/login/session.js +56 -0
  171. package/dist/login/session.js.map +1 -0
  172. package/dist/main.d.ts +3 -0
  173. package/dist/main.d.ts.map +1 -0
  174. package/dist/main.js +366 -0
  175. package/dist/main.js.map +1 -0
  176. package/dist/provisioner.d.ts +83 -0
  177. package/dist/provisioner.d.ts.map +1 -0
  178. package/dist/provisioner.js +500 -0
  179. package/dist/provisioner.js.map +1 -0
  180. package/dist/runtime/conversation-orchestrator.d.ts +40 -0
  181. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  182. package/dist/runtime/conversation-orchestrator.js +183 -0
  183. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  184. package/dist/runtime/index.d.ts +2 -0
  185. package/dist/runtime/index.d.ts.map +1 -0
  186. package/dist/runtime/index.js +2 -0
  187. package/dist/runtime/index.js.map +1 -0
  188. package/dist/runtime/session-runtime.d.ts +26 -0
  189. package/dist/runtime/session-runtime.d.ts.map +1 -0
  190. package/dist/runtime/session-runtime.js +221 -0
  191. package/dist/runtime/session-runtime.js.map +1 -0
  192. package/dist/sandbox/cloudflare.d.ts +15 -0
  193. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  194. package/dist/sandbox/cloudflare.js +138 -0
  195. package/dist/sandbox/cloudflare.js.map +1 -0
  196. package/dist/sandbox/container.d.ts +16 -0
  197. package/dist/sandbox/container.d.ts.map +1 -0
  198. package/dist/sandbox/container.js +138 -0
  199. package/dist/sandbox/container.js.map +1 -0
  200. package/dist/sandbox/errors.d.ts +6 -0
  201. package/dist/sandbox/errors.d.ts.map +1 -0
  202. package/dist/sandbox/errors.js +11 -0
  203. package/dist/sandbox/errors.js.map +1 -0
  204. package/dist/sandbox/firecracker.d.ts +17 -0
  205. package/dist/sandbox/firecracker.d.ts.map +1 -0
  206. package/dist/sandbox/firecracker.js +212 -0
  207. package/dist/sandbox/firecracker.js.map +1 -0
  208. package/dist/sandbox/host.d.ts +11 -0
  209. package/dist/sandbox/host.d.ts.map +1 -0
  210. package/dist/sandbox/host.js +89 -0
  211. package/dist/sandbox/host.js.map +1 -0
  212. package/dist/sandbox/image.d.ts +5 -0
  213. package/dist/sandbox/image.d.ts.map +1 -0
  214. package/dist/sandbox/image.js +30 -0
  215. package/dist/sandbox/image.js.map +1 -0
  216. package/dist/sandbox/index.d.ts +22 -0
  217. package/dist/sandbox/index.d.ts.map +1 -0
  218. package/dist/sandbox/index.js +54 -0
  219. package/dist/sandbox/index.js.map +1 -0
  220. package/dist/sandbox/path-context.d.ts +4 -0
  221. package/dist/sandbox/path-context.d.ts.map +1 -0
  222. package/dist/sandbox/path-context.js +20 -0
  223. package/dist/sandbox/path-context.js.map +1 -0
  224. package/dist/sandbox/types.d.ts +67 -0
  225. package/dist/sandbox/types.d.ts.map +1 -0
  226. package/dist/sandbox/types.js +2 -0
  227. package/dist/sandbox/types.js.map +1 -0
  228. package/dist/sandbox/utils.d.ts +4 -0
  229. package/dist/sandbox/utils.d.ts.map +1 -0
  230. package/dist/sandbox/utils.js +51 -0
  231. package/dist/sandbox/utils.js.map +1 -0
  232. package/dist/sentry.d.ts +50 -0
  233. package/dist/sentry.d.ts.map +1 -0
  234. package/dist/sentry.js +257 -0
  235. package/dist/sentry.js.map +1 -0
  236. package/dist/session-view/command.d.ts +5 -0
  237. package/dist/session-view/command.d.ts.map +1 -0
  238. package/dist/session-view/command.js +7 -0
  239. package/dist/session-view/command.js.map +1 -0
  240. package/dist/session-view/portal.d.ts +16 -0
  241. package/dist/session-view/portal.d.ts.map +1 -0
  242. package/dist/session-view/portal.js +1822 -0
  243. package/dist/session-view/portal.js.map +1 -0
  244. package/dist/session-view/service.d.ts +34 -0
  245. package/dist/session-view/service.d.ts.map +1 -0
  246. package/dist/session-view/service.js +434 -0
  247. package/dist/session-view/service.js.map +1 -0
  248. package/dist/session-view/store.d.ts +18 -0
  249. package/dist/session-view/store.d.ts.map +1 -0
  250. package/dist/session-view/store.js +36 -0
  251. package/dist/session-view/store.js.map +1 -0
  252. package/dist/sessions/metadata.d.ts +15 -0
  253. package/dist/sessions/metadata.d.ts.map +1 -0
  254. package/dist/sessions/metadata.js +11 -0
  255. package/dist/sessions/metadata.js.map +1 -0
  256. package/dist/sessions/policy.d.ts +13 -0
  257. package/dist/sessions/policy.d.ts.map +1 -0
  258. package/dist/sessions/policy.js +23 -0
  259. package/dist/sessions/policy.js.map +1 -0
  260. package/dist/sessions/store.d.ts +103 -0
  261. package/dist/sessions/store.d.ts.map +1 -0
  262. package/dist/sessions/store.js +349 -0
  263. package/dist/sessions/store.js.map +1 -0
  264. package/dist/store.d.ts +58 -0
  265. package/dist/store.d.ts.map +1 -0
  266. package/dist/store.js +152 -0
  267. package/dist/store.js.map +1 -0
  268. package/dist/tool-diagnostics.d.ts +2 -0
  269. package/dist/tool-diagnostics.d.ts.map +1 -0
  270. package/dist/tool-diagnostics.js +7 -0
  271. package/dist/tool-diagnostics.js.map +1 -0
  272. package/dist/tools/bash.d.ts +10 -0
  273. package/dist/tools/bash.d.ts.map +1 -0
  274. package/dist/tools/bash.js +80 -0
  275. package/dist/tools/bash.js.map +1 -0
  276. package/dist/tools/edit.d.ts +11 -0
  277. package/dist/tools/edit.d.ts.map +1 -0
  278. package/dist/tools/edit.js +133 -0
  279. package/dist/tools/edit.js.map +1 -0
  280. package/dist/tools/event.d.ts +62 -0
  281. package/dist/tools/event.d.ts.map +1 -0
  282. package/dist/tools/event.js +138 -0
  283. package/dist/tools/event.js.map +1 -0
  284. package/dist/tools/index.d.ts +14 -0
  285. package/dist/tools/index.d.ts.map +1 -0
  286. package/dist/tools/index.js +23 -0
  287. package/dist/tools/index.js.map +1 -0
  288. package/dist/tools/read.d.ts +11 -0
  289. package/dist/tools/read.d.ts.map +1 -0
  290. package/dist/tools/read.js +136 -0
  291. package/dist/tools/read.js.map +1 -0
  292. package/dist/tools/truncate.d.ts +57 -0
  293. package/dist/tools/truncate.d.ts.map +1 -0
  294. package/dist/tools/truncate.js +184 -0
  295. package/dist/tools/truncate.js.map +1 -0
  296. package/dist/tools/write.d.ts +10 -0
  297. package/dist/tools/write.d.ts.map +1 -0
  298. package/dist/tools/write.js +33 -0
  299. package/dist/tools/write.js.map +1 -0
  300. package/dist/trigger.d.ts +31 -0
  301. package/dist/trigger.d.ts.map +1 -0
  302. package/dist/trigger.js +98 -0
  303. package/dist/trigger.js.map +1 -0
  304. package/dist/ui-copy.d.ts +12 -0
  305. package/dist/ui-copy.d.ts.map +1 -0
  306. package/dist/ui-copy.js +36 -0
  307. package/dist/ui-copy.js.map +1 -0
  308. package/dist/vault-routing.d.ts +4 -0
  309. package/dist/vault-routing.d.ts.map +1 -0
  310. package/dist/vault-routing.js +16 -0
  311. package/dist/vault-routing.js.map +1 -0
  312. package/dist/vault.d.ts +72 -0
  313. package/dist/vault.d.ts.map +1 -0
  314. package/dist/vault.js +281 -0
  315. package/dist/vault.js.map +1 -0
  316. package/package.json +83 -0
@@ -0,0 +1,138 @@
1
+ import { mkdir, stat, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import * as log from "../log.js";
5
+ const eventSchema = Type.Object({
6
+ label: Type.String({
7
+ description: "Brief description of the event you're scheduling (shown to user)",
8
+ }),
9
+ type: Type.Union([Type.Literal("immediate"), Type.Literal("one-shot"), Type.Literal("periodic")]),
10
+ text: Type.String({
11
+ description: "A self-contained task for the future run. Include the necessary context, tone, and constraints in the text itself because events do not inherit normal conversation history.",
12
+ }),
13
+ at: Type.Optional(Type.String({
14
+ description: "ISO 8601 timestamp with offset, required for one-shot events",
15
+ })),
16
+ schedule: Type.Optional(Type.String({
17
+ description: "Cron schedule, required for periodic events",
18
+ })),
19
+ timezone: Type.Optional(Type.String({
20
+ description: "IANA timezone, required for periodic events",
21
+ })),
22
+ filenamePrefix: Type.Optional(Type.String({
23
+ description: "Optional filename prefix for the event file",
24
+ })),
25
+ });
26
+ export class HostEventStore {
27
+ constructor(eventsDir) {
28
+ this.eventsDir = eventsDir;
29
+ }
30
+ static fromWorkspaceDir(workspaceDir) {
31
+ return new HostEventStore(join(workspaceDir, "events"));
32
+ }
33
+ async write(filename, payload) {
34
+ await mkdir(this.eventsDir, { recursive: true });
35
+ const filePath = join(this.eventsDir, filename);
36
+ await writeFile(filePath, JSON.stringify(payload) + "\n", "utf-8");
37
+ const fileStat = await stat(filePath);
38
+ return { path: filePath, size: fileStat.size };
39
+ }
40
+ }
41
+ export function createEventTool(eventStore) {
42
+ let eventContext = null;
43
+ const tool = {
44
+ name: "event",
45
+ label: "event",
46
+ description: "Schedule an immediate, one-shot, or periodic event for the current conversation. Write text as a self-contained task with any needed context, tone, or constraints because events do not inherit normal conversation history. This automatically writes to the correct events directory and fills the current platform, conversation, conversation kind, and requester userId.",
47
+ parameters: eventSchema,
48
+ execute: async (_toolCallId, params, signal) => {
49
+ if (signal?.aborted) {
50
+ throw new Error("Operation aborted");
51
+ }
52
+ if (!eventContext) {
53
+ throw new Error("Event context not configured");
54
+ }
55
+ const payload = buildEventPayload(params, eventContext);
56
+ const prefix = sanitizeFileSegment(params.filenamePrefix || payload.type || "event");
57
+ const filename = `${prefix}-${Date.now()}.json`;
58
+ log.logInfo(`Writing event file via control plane store: ${filename} (type=${payload.type}, platform=${payload.platform}, conversation=${payload.conversationId})`);
59
+ try {
60
+ const result = await eventStore.write(filename, payload);
61
+ log.logInfo(`Wrote event file via control plane store: ${result.path} (${result.size} bytes)`);
62
+ }
63
+ catch (err) {
64
+ log.logWarning(`Failed to write event file via control plane store: ${filename}`, String(err));
65
+ throw err;
66
+ }
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: payload.type === "periodic"
72
+ ? `Scheduled periodic event ${filename} for ${payload.platform}/${payload.conversationId} (${payload.schedule} ${payload.timezone})`
73
+ : payload.type === "one-shot"
74
+ ? `Scheduled one-shot event ${filename} for ${payload.platform}/${payload.conversationId} at ${payload.at}`
75
+ : `Queued immediate event ${filename} for ${payload.platform}/${payload.conversationId}`,
76
+ },
77
+ ],
78
+ details: undefined,
79
+ };
80
+ },
81
+ };
82
+ return {
83
+ tool,
84
+ setEventContext: (context) => {
85
+ eventContext = context;
86
+ },
87
+ };
88
+ }
89
+ function buildEventPayload(params, context) {
90
+ const base = {
91
+ platform: context.platform,
92
+ conversationId: context.conversationId,
93
+ conversationKind: context.conversationKind,
94
+ userId: context.userId,
95
+ text: params.text,
96
+ };
97
+ if (params.type === "immediate") {
98
+ return {
99
+ ...base,
100
+ type: "immediate",
101
+ };
102
+ }
103
+ if (params.type === "one-shot") {
104
+ if (!params.at) {
105
+ throw new Error("`at` is required for one-shot events");
106
+ }
107
+ const atTime = new Date(params.at).getTime();
108
+ if (Number.isNaN(atTime)) {
109
+ throw new Error("`at` must be a valid ISO 8601 timestamp with UTC offset");
110
+ }
111
+ if (atTime <= Date.now()) {
112
+ throw new Error(`\`at\` must be in the future; got ${params.at} (now=${new Date().toISOString()}). Check the timezone offset.`);
113
+ }
114
+ // No sessionKey or threadTs: reminders should fire as top-level messages, not buried in old threads
115
+ return { ...base, type: "one-shot", at: params.at };
116
+ }
117
+ if (!params.schedule) {
118
+ throw new Error("`schedule` is required for periodic events");
119
+ }
120
+ if (!params.timezone) {
121
+ throw new Error("`timezone` is required for periodic events");
122
+ }
123
+ return {
124
+ ...base,
125
+ type: "periodic",
126
+ schedule: params.schedule,
127
+ timezone: params.timezone,
128
+ };
129
+ }
130
+ function sanitizeFileSegment(value) {
131
+ const sanitized = value
132
+ .trim()
133
+ .toLowerCase()
134
+ .replace(/[^a-z0-9._-]+/g, "-")
135
+ .replace(/^-+|-+$/g, "");
136
+ return sanitized || "event";
137
+ }
138
+ //# sourceMappingURL=event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event.js","sourceRoot":"","sources":["../../src/tools/event.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, stat, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport * as log from \"../log.js\";\n\nconst eventSchema = Type.Object({\n label: Type.String({\n description: \"Brief description of the event you're scheduling (shown to user)\",\n }),\n type: Type.Union([Type.Literal(\"immediate\"), Type.Literal(\"one-shot\"), Type.Literal(\"periodic\")]),\n text: Type.String({\n description:\n \"A self-contained task for the future run. Include the necessary context, tone, and constraints in the text itself because events do not inherit normal conversation history.\",\n }),\n at: Type.Optional(\n Type.String({\n description: \"ISO 8601 timestamp with offset, required for one-shot events\",\n }),\n ),\n schedule: Type.Optional(\n Type.String({\n description: \"Cron schedule, required for periodic events\",\n }),\n ),\n timezone: Type.Optional(\n Type.String({\n description: \"IANA timezone, required for periodic events\",\n }),\n ),\n filenamePrefix: Type.Optional(\n Type.String({\n description: \"Optional filename prefix for the event file\",\n }),\n ),\n});\n\ninterface EventToolContext {\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n}\n\ntype EventToolParams = {\n label: string;\n type: \"immediate\" | \"one-shot\" | \"periodic\";\n text: string;\n at?: string;\n schedule?: string;\n timezone?: string;\n filenamePrefix?: string;\n};\n\nexport type EventPayload =\n | {\n type: \"immediate\";\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n text: string;\n }\n | {\n type: \"one-shot\";\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n text: string;\n at: string;\n }\n | {\n type: \"periodic\";\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n text: string;\n schedule: string;\n timezone: string;\n };\n\nexport interface EventStore {\n write(filename: string, payload: EventPayload): Promise<{ path: string; size: number }>;\n}\n\nexport class HostEventStore implements EventStore {\n constructor(private readonly eventsDir: string) {}\n\n static fromWorkspaceDir(workspaceDir: string): HostEventStore {\n return new HostEventStore(join(workspaceDir, \"events\"));\n }\n\n async write(filename: string, payload: EventPayload): Promise<{ path: string; size: number }> {\n await mkdir(this.eventsDir, { recursive: true });\n const filePath = join(this.eventsDir, filename);\n await writeFile(filePath, JSON.stringify(payload) + \"\\n\", \"utf-8\");\n const fileStat = await stat(filePath);\n return { path: filePath, size: fileStat.size };\n }\n}\n\nexport function createEventTool(eventStore: EventStore): {\n tool: AgentTool<typeof eventSchema>;\n setEventContext: (context: EventToolContext) => void;\n} {\n let eventContext: EventToolContext | null = null;\n\n const tool: AgentTool<typeof eventSchema> = {\n name: \"event\",\n label: \"event\",\n description:\n \"Schedule an immediate, one-shot, or periodic event for the current conversation. Write text as a self-contained task with any needed context, tone, or constraints because events do not inherit normal conversation history. This automatically writes to the correct events directory and fills the current platform, conversation, conversation kind, and requester userId.\",\n parameters: eventSchema,\n execute: async (_toolCallId: string, params: EventToolParams, signal?: AbortSignal) => {\n if (signal?.aborted) {\n throw new Error(\"Operation aborted\");\n }\n\n if (!eventContext) {\n throw new Error(\"Event context not configured\");\n }\n\n const payload = buildEventPayload(params, eventContext);\n const prefix = sanitizeFileSegment(params.filenamePrefix || payload.type || \"event\");\n const filename = `${prefix}-${Date.now()}.json`;\n\n log.logInfo(\n `Writing event file via control plane store: ${filename} (type=${payload.type}, platform=${payload.platform}, conversation=${payload.conversationId})`,\n );\n\n try {\n const result = await eventStore.write(filename, payload);\n log.logInfo(\n `Wrote event file via control plane store: ${result.path} (${result.size} bytes)`,\n );\n } catch (err) {\n log.logWarning(\n `Failed to write event file via control plane store: ${filename}`,\n String(err),\n );\n throw err;\n }\n\n return {\n content: [\n {\n type: \"text\",\n text:\n payload.type === \"periodic\"\n ? `Scheduled periodic event ${filename} for ${payload.platform}/${payload.conversationId} (${payload.schedule} ${payload.timezone})`\n : payload.type === \"one-shot\"\n ? `Scheduled one-shot event ${filename} for ${payload.platform}/${payload.conversationId} at ${payload.at}`\n : `Queued immediate event ${filename} for ${payload.platform}/${payload.conversationId}`,\n },\n ],\n details: undefined,\n };\n },\n };\n\n return {\n tool,\n setEventContext: (context: EventToolContext) => {\n eventContext = context;\n },\n };\n}\n\nfunction buildEventPayload(params: EventToolParams, context: EventToolContext): EventPayload {\n const base = {\n platform: context.platform,\n conversationId: context.conversationId,\n conversationKind: context.conversationKind,\n userId: context.userId,\n text: params.text,\n };\n\n if (params.type === \"immediate\") {\n return {\n ...base,\n type: \"immediate\",\n };\n }\n\n if (params.type === \"one-shot\") {\n if (!params.at) {\n throw new Error(\"`at` is required for one-shot events\");\n }\n\n const atTime = new Date(params.at).getTime();\n if (Number.isNaN(atTime)) {\n throw new Error(\"`at` must be a valid ISO 8601 timestamp with UTC offset\");\n }\n if (atTime <= Date.now()) {\n throw new Error(\n `\\`at\\` must be in the future; got ${params.at} (now=${new Date().toISOString()}). Check the timezone offset.`,\n );\n }\n\n // No sessionKey or threadTs: reminders should fire as top-level messages, not buried in old threads\n return { ...base, type: \"one-shot\", at: params.at };\n }\n\n if (!params.schedule) {\n throw new Error(\"`schedule` is required for periodic events\");\n }\n if (!params.timezone) {\n throw new Error(\"`timezone` is required for periodic events\");\n }\n return {\n ...base,\n type: \"periodic\",\n schedule: params.schedule,\n timezone: params.timezone,\n };\n}\n\nfunction sanitizeFileSegment(value: string): string {\n const sanitized = value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return sanitized || \"event\";\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import type { TSchema } from "@sinclair/typebox";
3
+ import type { Executor } from "../sandbox/index.js";
4
+ export declare function createMikanTools(executor: Executor, workspaceDir: string): {
5
+ tools: AgentTool<TSchema>[];
6
+ setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;
7
+ setEventContext: (context: {
8
+ platform: string;
9
+ conversationId: string;
10
+ conversationKind: "direct" | "shared";
11
+ userId: string;
12
+ }) => void;
13
+ };
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAOpD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,MAAM,GACnB;IACD,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5B,iBAAiB,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IACrF,eAAe,EAAE,CAAC,OAAO,EAAE;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,QAAQ,GAAG,QAAQ,CAAC;QACtC,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,IAAI,CAAC;CACZ,CAiBA","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport type { TSchema } from \"@sinclair/typebox\";\nimport { createAttachTool } from \"../adapters/slack/tools/attach.js\";\nimport type { Executor } from \"../sandbox/index.js\";\nimport { createBashTool } from \"./bash.js\";\nimport { createEditTool } from \"./edit.js\";\nimport { createEventTool, HostEventStore } from \"./event.js\";\nimport { createReadTool } from \"./read.js\";\nimport { createWriteTool } from \"./write.js\";\n\nexport function createMikanTools(\n executor: Executor,\n workspaceDir: string,\n): {\n tools: AgentTool<TSchema>[];\n setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;\n setEventContext: (context: {\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n }) => void;\n} {\n const { tool: attachTool, setUploadFunction } = createAttachTool();\n const { tool: eventTool, setEventContext } = createEventTool(\n HostEventStore.fromWorkspaceDir(workspaceDir),\n );\n return {\n tools: [\n createReadTool(executor),\n createBashTool(executor),\n createEditTool(executor),\n createWriteTool(executor),\n eventTool,\n attachTool,\n ],\n setUploadFunction,\n setEventContext,\n };\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import { createAttachTool } from "../adapters/slack/tools/attach.js";
2
+ import { createBashTool } from "./bash.js";
3
+ import { createEditTool } from "./edit.js";
4
+ import { createEventTool, HostEventStore } from "./event.js";
5
+ import { createReadTool } from "./read.js";
6
+ import { createWriteTool } from "./write.js";
7
+ export function createMikanTools(executor, workspaceDir) {
8
+ const { tool: attachTool, setUploadFunction } = createAttachTool();
9
+ const { tool: eventTool, setEventContext } = createEventTool(HostEventStore.fromWorkspaceDir(workspaceDir));
10
+ return {
11
+ tools: [
12
+ createReadTool(executor),
13
+ createBashTool(executor),
14
+ createEditTool(executor),
15
+ createWriteTool(executor),
16
+ eventTool,
17
+ attachTool,
18
+ ],
19
+ setUploadFunction,
20
+ setEventContext,
21
+ };
22
+ }
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAErE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,UAAU,gBAAgB,CAC9B,QAAkB,EAClB,YAAoB;IAWpB,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,CAC1D,cAAc,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAC9C,CAAC;IACF,OAAO;QACL,KAAK,EAAE;YACL,cAAc,CAAC,QAAQ,CAAC;YACxB,cAAc,CAAC,QAAQ,CAAC;YACxB,cAAc,CAAC,QAAQ,CAAC;YACxB,eAAe,CAAC,QAAQ,CAAC;YACzB,SAAS;YACT,UAAU;SACX;QACD,iBAAiB;QACjB,eAAe;KAChB,CAAC;AACJ,CAAC","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport type { TSchema } from \"@sinclair/typebox\";\nimport { createAttachTool } from \"../adapters/slack/tools/attach.js\";\nimport type { Executor } from \"../sandbox/index.js\";\nimport { createBashTool } from \"./bash.js\";\nimport { createEditTool } from \"./edit.js\";\nimport { createEventTool, HostEventStore } from \"./event.js\";\nimport { createReadTool } from \"./read.js\";\nimport { createWriteTool } from \"./write.js\";\n\nexport function createMikanTools(\n executor: Executor,\n workspaceDir: string,\n): {\n tools: AgentTool<TSchema>[];\n setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;\n setEventContext: (context: {\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n }) => void;\n} {\n const { tool: attachTool, setUploadFunction } = createAttachTool();\n const { tool: eventTool, setEventContext } = createEventTool(\n HostEventStore.fromWorkspaceDir(workspaceDir),\n );\n return {\n tools: [\n createReadTool(executor),\n createBashTool(executor),\n createEditTool(executor),\n createWriteTool(executor),\n eventTool,\n attachTool,\n ],\n setUploadFunction,\n setEventContext,\n };\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import type { Executor } from "../sandbox/index.js";
3
+ declare const readSchema: import("@sinclair/typebox").TObject<{
4
+ label: import("@sinclair/typebox").TString;
5
+ path: import("@sinclair/typebox").TString;
6
+ offset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
7
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
8
+ }>;
9
+ export declare function createReadTool(executor: Executor): AgentTool<typeof readSchema>;
10
+ export {};
11
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAI/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AA4BpD,QAAA,MAAM,UAAU;;;;;EASd,CAAC;AAMH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CA0H/E","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport type { ImageContent, TextContent } from \"@earendil-works/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { extname } from \"path\";\nimport type { Executor } from \"../sandbox/index.js\";\nimport {\n DEFAULT_MAX_BYTES,\n DEFAULT_MAX_LINES,\n formatSize,\n type TruncationResult,\n truncateHead,\n} from \"./truncate.js\";\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".png\": \"image/png\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n const ext = extname(filePath).toLowerCase();\n return IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n label: Type.String({\n description: \"Brief description of what you're reading and why (shown to user)\",\n }),\n path: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n offset: Type.Optional(\n Type.Number({ description: \"Line number to start reading from (1-indexed)\" }),\n ),\n limit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\ninterface ReadToolDetails {\n truncation?: TruncationResult;\n}\n\nexport function createReadTool(executor: Executor): AgentTool<typeof readSchema> {\n return {\n name: \"read\",\n label: \"read\",\n description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n parameters: readSchema,\n execute: async (\n _toolCallId: string,\n { path, offset, limit }: { label: string; path: string; offset?: number; limit?: number },\n signal?: AbortSignal,\n ): Promise<{\n content: (TextContent | ImageContent)[];\n details: ReadToolDetails | undefined;\n }> => {\n const mimeType = isImageFile(path);\n\n if (mimeType) {\n // Read as image (binary) - use base64\n const result = await executor.exec(`base64 < ${shellEscape(path)}`, { signal });\n if (result.code !== 0) {\n throw new Error(result.stderr || `Failed to read file: ${path}`);\n }\n const base64 = result.stdout.replace(/\\s/g, \"\"); // Remove whitespace from base64\n\n return {\n content: [\n { type: \"text\", text: `Read image file [${mimeType}]` },\n { type: \"image\", data: base64, mimeType },\n ],\n details: undefined,\n };\n }\n\n // Get total line count first\n const countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });\n if (countResult.code !== 0) {\n throw new Error(countResult.stderr || `Failed to read file: ${path}`);\n }\n const totalFileLines = Number.parseInt(countResult.stdout.trim(), 10) + 1; // wc -l counts newlines, not lines\n\n // Apply offset if specified (1-indexed)\n const startLine = offset ? Math.max(1, offset) : 1;\n const startLineDisplay = startLine;\n\n // Check if offset is out of bounds\n if (startLine > totalFileLines) {\n throw new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);\n }\n\n // Read content with offset\n let cmd: string;\n if (startLine === 1) {\n cmd = `cat ${shellEscape(path)}`;\n } else {\n cmd = `tail -n +${startLine} ${shellEscape(path)}`;\n }\n\n const result = await executor.exec(cmd, { signal });\n if (result.code !== 0) {\n throw new Error(result.stderr || `Failed to read file: ${path}`);\n }\n\n let selectedContent = result.stdout;\n let userLimitedLines: number | undefined;\n\n // Apply user limit if specified\n if (limit !== undefined) {\n const lines = selectedContent.split(\"\\n\");\n const endLine = Math.min(limit, lines.length);\n selectedContent = lines.slice(0, endLine).join(\"\\n\");\n userLimitedLines = endLine;\n }\n\n // Apply truncation (respects both line and byte limits)\n const truncation = truncateHead(selectedContent);\n\n let outputText: string;\n let details: ReadToolDetails | undefined;\n\n if (truncation.firstLineExceedsLimit) {\n // First line at offset exceeds 50KB - tell model to use bash\n const firstLineSize = formatSize(\n Buffer.byteLength(selectedContent.split(\"\\n\")[0], \"utf-8\"),\n );\n outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n details = { truncation };\n } else if (truncation.truncated) {\n // Truncation occurred - build actionable notice\n const endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n const nextOffset = endLineDisplay + 1;\n\n outputText = truncation.content;\n\n if (truncation.truncatedBy === \"lines\") {\n outputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n } else {\n outputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n }\n details = { truncation };\n } else if (userLimitedLines !== undefined) {\n // User specified limit, check if there's more content\n const linesFromStart = startLine - 1 + userLimitedLines;\n if (linesFromStart < totalFileLines) {\n const remaining = totalFileLines - linesFromStart;\n const nextOffset = startLine + userLimitedLines;\n\n outputText = truncation.content;\n outputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n } else {\n outputText = truncation.content;\n }\n } else {\n // No truncation, no user limit exceeded\n outputText = truncation.content;\n }\n\n return {\n content: [{ type: \"text\", text: outputText }],\n details,\n };\n },\n };\n}\n\nfunction shellEscape(s: string): string {\n return `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
@@ -0,0 +1,136 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { extname } from "path";
3
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, } from "./truncate.js";
4
+ /**
5
+ * Map of file extensions to MIME types for common image formats
6
+ */
7
+ const IMAGE_MIME_TYPES = {
8
+ ".jpg": "image/jpeg",
9
+ ".jpeg": "image/jpeg",
10
+ ".png": "image/png",
11
+ ".gif": "image/gif",
12
+ ".webp": "image/webp",
13
+ };
14
+ /**
15
+ * Check if a file is an image based on its extension
16
+ */
17
+ function isImageFile(filePath) {
18
+ const ext = extname(filePath).toLowerCase();
19
+ return IMAGE_MIME_TYPES[ext] || null;
20
+ }
21
+ const readSchema = Type.Object({
22
+ label: Type.String({
23
+ description: "Brief description of what you're reading and why (shown to user)",
24
+ }),
25
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
26
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
27
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
28
+ });
29
+ export function createReadTool(executor) {
30
+ return {
31
+ name: "read",
32
+ label: "read",
33
+ description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,
34
+ parameters: readSchema,
35
+ execute: async (_toolCallId, { path, offset, limit }, signal) => {
36
+ const mimeType = isImageFile(path);
37
+ if (mimeType) {
38
+ // Read as image (binary) - use base64
39
+ const result = await executor.exec(`base64 < ${shellEscape(path)}`, { signal });
40
+ if (result.code !== 0) {
41
+ throw new Error(result.stderr || `Failed to read file: ${path}`);
42
+ }
43
+ const base64 = result.stdout.replace(/\s/g, ""); // Remove whitespace from base64
44
+ return {
45
+ content: [
46
+ { type: "text", text: `Read image file [${mimeType}]` },
47
+ { type: "image", data: base64, mimeType },
48
+ ],
49
+ details: undefined,
50
+ };
51
+ }
52
+ // Get total line count first
53
+ const countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });
54
+ if (countResult.code !== 0) {
55
+ throw new Error(countResult.stderr || `Failed to read file: ${path}`);
56
+ }
57
+ const totalFileLines = Number.parseInt(countResult.stdout.trim(), 10) + 1; // wc -l counts newlines, not lines
58
+ // Apply offset if specified (1-indexed)
59
+ const startLine = offset ? Math.max(1, offset) : 1;
60
+ const startLineDisplay = startLine;
61
+ // Check if offset is out of bounds
62
+ if (startLine > totalFileLines) {
63
+ throw new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);
64
+ }
65
+ // Read content with offset
66
+ let cmd;
67
+ if (startLine === 1) {
68
+ cmd = `cat ${shellEscape(path)}`;
69
+ }
70
+ else {
71
+ cmd = `tail -n +${startLine} ${shellEscape(path)}`;
72
+ }
73
+ const result = await executor.exec(cmd, { signal });
74
+ if (result.code !== 0) {
75
+ throw new Error(result.stderr || `Failed to read file: ${path}`);
76
+ }
77
+ let selectedContent = result.stdout;
78
+ let userLimitedLines;
79
+ // Apply user limit if specified
80
+ if (limit !== undefined) {
81
+ const lines = selectedContent.split("\n");
82
+ const endLine = Math.min(limit, lines.length);
83
+ selectedContent = lines.slice(0, endLine).join("\n");
84
+ userLimitedLines = endLine;
85
+ }
86
+ // Apply truncation (respects both line and byte limits)
87
+ const truncation = truncateHead(selectedContent);
88
+ let outputText;
89
+ let details;
90
+ if (truncation.firstLineExceedsLimit) {
91
+ // First line at offset exceeds 50KB - tell model to use bash
92
+ const firstLineSize = formatSize(Buffer.byteLength(selectedContent.split("\n")[0], "utf-8"));
93
+ outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
94
+ details = { truncation };
95
+ }
96
+ else if (truncation.truncated) {
97
+ // Truncation occurred - build actionable notice
98
+ const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
99
+ const nextOffset = endLineDisplay + 1;
100
+ outputText = truncation.content;
101
+ if (truncation.truncatedBy === "lines") {
102
+ outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
103
+ }
104
+ else {
105
+ outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;
106
+ }
107
+ details = { truncation };
108
+ }
109
+ else if (userLimitedLines !== undefined) {
110
+ // User specified limit, check if there's more content
111
+ const linesFromStart = startLine - 1 + userLimitedLines;
112
+ if (linesFromStart < totalFileLines) {
113
+ const remaining = totalFileLines - linesFromStart;
114
+ const nextOffset = startLine + userLimitedLines;
115
+ outputText = truncation.content;
116
+ outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
117
+ }
118
+ else {
119
+ outputText = truncation.content;
120
+ }
121
+ }
122
+ else {
123
+ // No truncation, no user limit exceeded
124
+ outputText = truncation.content;
125
+ }
126
+ return {
127
+ content: [{ type: "text", text: outputText }],
128
+ details,
129
+ };
130
+ },
131
+ };
132
+ }
133
+ function shellEscape(s) {
134
+ return `'${s.replace(/'/g, "'\\''")}'`;
135
+ }
136
+ //# sourceMappingURL=read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@earendil-works/pi-agent-core\";\nimport type { ImageContent, TextContent } from \"@earendil-works/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { extname } from \"path\";\nimport type { Executor } from \"../sandbox/index.js\";\nimport {\n DEFAULT_MAX_BYTES,\n DEFAULT_MAX_LINES,\n formatSize,\n type TruncationResult,\n truncateHead,\n} from \"./truncate.js\";\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".png\": \"image/png\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n const ext = extname(filePath).toLowerCase();\n return IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n label: Type.String({\n description: \"Brief description of what you're reading and why (shown to user)\",\n }),\n path: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n offset: Type.Optional(\n Type.Number({ description: \"Line number to start reading from (1-indexed)\" }),\n ),\n limit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\ninterface ReadToolDetails {\n truncation?: TruncationResult;\n}\n\nexport function createReadTool(executor: Executor): AgentTool<typeof readSchema> {\n return {\n name: \"read\",\n label: \"read\",\n description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n parameters: readSchema,\n execute: async (\n _toolCallId: string,\n { path, offset, limit }: { label: string; path: string; offset?: number; limit?: number },\n signal?: AbortSignal,\n ): Promise<{\n content: (TextContent | ImageContent)[];\n details: ReadToolDetails | undefined;\n }> => {\n const mimeType = isImageFile(path);\n\n if (mimeType) {\n // Read as image (binary) - use base64\n const result = await executor.exec(`base64 < ${shellEscape(path)}`, { signal });\n if (result.code !== 0) {\n throw new Error(result.stderr || `Failed to read file: ${path}`);\n }\n const base64 = result.stdout.replace(/\\s/g, \"\"); // Remove whitespace from base64\n\n return {\n content: [\n { type: \"text\", text: `Read image file [${mimeType}]` },\n { type: \"image\", data: base64, mimeType },\n ],\n details: undefined,\n };\n }\n\n // Get total line count first\n const countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });\n if (countResult.code !== 0) {\n throw new Error(countResult.stderr || `Failed to read file: ${path}`);\n }\n const totalFileLines = Number.parseInt(countResult.stdout.trim(), 10) + 1; // wc -l counts newlines, not lines\n\n // Apply offset if specified (1-indexed)\n const startLine = offset ? Math.max(1, offset) : 1;\n const startLineDisplay = startLine;\n\n // Check if offset is out of bounds\n if (startLine > totalFileLines) {\n throw new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);\n }\n\n // Read content with offset\n let cmd: string;\n if (startLine === 1) {\n cmd = `cat ${shellEscape(path)}`;\n } else {\n cmd = `tail -n +${startLine} ${shellEscape(path)}`;\n }\n\n const result = await executor.exec(cmd, { signal });\n if (result.code !== 0) {\n throw new Error(result.stderr || `Failed to read file: ${path}`);\n }\n\n let selectedContent = result.stdout;\n let userLimitedLines: number | undefined;\n\n // Apply user limit if specified\n if (limit !== undefined) {\n const lines = selectedContent.split(\"\\n\");\n const endLine = Math.min(limit, lines.length);\n selectedContent = lines.slice(0, endLine).join(\"\\n\");\n userLimitedLines = endLine;\n }\n\n // Apply truncation (respects both line and byte limits)\n const truncation = truncateHead(selectedContent);\n\n let outputText: string;\n let details: ReadToolDetails | undefined;\n\n if (truncation.firstLineExceedsLimit) {\n // First line at offset exceeds 50KB - tell model to use bash\n const firstLineSize = formatSize(\n Buffer.byteLength(selectedContent.split(\"\\n\")[0], \"utf-8\"),\n );\n outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n details = { truncation };\n } else if (truncation.truncated) {\n // Truncation occurred - build actionable notice\n const endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n const nextOffset = endLineDisplay + 1;\n\n outputText = truncation.content;\n\n if (truncation.truncatedBy === \"lines\") {\n outputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n } else {\n outputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n }\n details = { truncation };\n } else if (userLimitedLines !== undefined) {\n // User specified limit, check if there's more content\n const linesFromStart = startLine - 1 + userLimitedLines;\n if (linesFromStart < totalFileLines) {\n const remaining = totalFileLines - linesFromStart;\n const nextOffset = startLine + userLimitedLines;\n\n outputText = truncation.content;\n outputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n } else {\n outputText = truncation.content;\n }\n } else {\n // No truncation, no user limit exceeded\n outputText = truncation.content;\n }\n\n return {\n content: [{ type: \"text\", text: outputText }],\n details,\n };\n },\n };\n}\n\nfunction shellEscape(s: string): string {\n return `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared truncation utilities for tool outputs.
3
+ *
4
+ * Truncation is based on two independent limits - whichever is hit first wins:
5
+ * - Line limit (default: 2000 lines)
6
+ * - Byte limit (default: 50KB)
7
+ *
8
+ * Never returns partial lines (except bash tail truncation edge case).
9
+ */
10
+ export declare const DEFAULT_MAX_LINES = 2000;
11
+ export declare const DEFAULT_MAX_BYTES: number;
12
+ export interface TruncationResult {
13
+ /** The truncated content */
14
+ content: string;
15
+ /** Whether truncation occurred */
16
+ truncated: boolean;
17
+ /** Which limit was hit: "lines", "bytes", or null if not truncated */
18
+ truncatedBy: "lines" | "bytes" | null;
19
+ /** Total number of lines in the original content */
20
+ totalLines: number;
21
+ /** Total number of bytes in the original content */
22
+ totalBytes: number;
23
+ /** Number of complete lines in the truncated output */
24
+ outputLines: number;
25
+ /** Number of bytes in the truncated output */
26
+ outputBytes: number;
27
+ /** Whether the last line was partially truncated (only for tail truncation edge case) */
28
+ lastLinePartial: boolean;
29
+ /** Whether the first line exceeded the byte limit (for head truncation) */
30
+ firstLineExceedsLimit: boolean;
31
+ }
32
+ export interface TruncationOptions {
33
+ /** Maximum number of lines (default: 2000) */
34
+ maxLines?: number;
35
+ /** Maximum number of bytes (default: 50KB) */
36
+ maxBytes?: number;
37
+ }
38
+ /**
39
+ * Format bytes as human-readable size.
40
+ */
41
+ export declare function formatSize(bytes: number): string;
42
+ /**
43
+ * Truncate content from the head (keep first N lines/bytes).
44
+ * Suitable for file reads where you want to see the beginning.
45
+ *
46
+ * Never returns partial lines. If first line exceeds byte limit,
47
+ * returns empty content with firstLineExceedsLimit=true.
48
+ */
49
+ export declare function truncateHead(content: string, options?: TruncationOptions): TruncationResult;
50
+ /**
51
+ * Truncate content from the tail (keep last N lines/bytes).
52
+ * Suitable for bash output where you want to see the end (errors, final results).
53
+ *
54
+ * May return partial first line if the last line of original content exceeds byte limit.
55
+ */
56
+ export declare function truncateTail(content: string, options?: TruncationOptions): TruncationResult;
57
+ //# sourceMappingURL=truncate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/tools/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAC/B,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA4E/F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAqE/F","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\n\nexport interface TruncationResult {\n /** The truncated content */\n content: string;\n /** Whether truncation occurred */\n truncated: boolean;\n /** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n truncatedBy: \"lines\" | \"bytes\" | null;\n /** Total number of lines in the original content */\n totalLines: number;\n /** Total number of bytes in the original content */\n totalBytes: number;\n /** Number of complete lines in the truncated output */\n outputLines: number;\n /** Number of bytes in the truncated output */\n outputBytes: number;\n /** Whether the last line was partially truncated (only for tail truncation edge case) */\n lastLinePartial: boolean;\n /** Whether the first line exceeded the byte limit (for head truncation) */\n firstLineExceedsLimit: boolean;\n}\n\nexport interface TruncationOptions {\n /** Maximum number of lines (default: 2000) */\n maxLines?: number;\n /** Maximum number of bytes (default: 50KB) */\n maxBytes?: number;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n if (bytes < 1024) {\n return `${bytes}B`;\n } else if (bytes < 1024 * 1024) {\n return `${(bytes / 1024).toFixed(1)}KB`;\n } else {\n return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n }\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n const totalBytes = Buffer.byteLength(content, \"utf-8\");\n const lines = content.split(\"\\n\");\n const totalLines = lines.length;\n\n // Check if no truncation needed\n if (totalLines <= maxLines && totalBytes <= maxBytes) {\n return {\n content,\n truncated: false,\n truncatedBy: null,\n totalLines,\n totalBytes,\n outputLines: totalLines,\n outputBytes: totalBytes,\n lastLinePartial: false,\n firstLineExceedsLimit: false,\n };\n }\n\n // Check if first line alone exceeds byte limit\n const firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n if (firstLineBytes > maxBytes) {\n return {\n content: \"\",\n truncated: true,\n truncatedBy: \"bytes\",\n totalLines,\n totalBytes,\n outputLines: 0,\n outputBytes: 0,\n lastLinePartial: false,\n firstLineExceedsLimit: true,\n };\n }\n\n // Collect complete lines that fit\n const outputLinesArr: string[] = [];\n let outputBytesCount = 0;\n let truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n for (let i = 0; i < lines.length && i < maxLines; i++) {\n const line = lines[i];\n const lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0); // +1 for newline\n\n if (outputBytesCount + lineBytes > maxBytes) {\n truncatedBy = \"bytes\";\n break;\n }\n\n outputLinesArr.push(line);\n outputBytesCount += lineBytes;\n }\n\n // If we exited due to line limit\n if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n truncatedBy = \"lines\";\n }\n\n const outputContent = outputLinesArr.join(\"\\n\");\n const finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n return {\n content: outputContent,\n truncated: true,\n truncatedBy,\n totalLines,\n totalBytes,\n outputLines: outputLinesArr.length,\n outputBytes: finalOutputBytes,\n lastLinePartial: false,\n firstLineExceedsLimit: false,\n };\n}\n\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {\n const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n const totalBytes = Buffer.byteLength(content, \"utf-8\");\n const lines = content.split(\"\\n\");\n const totalLines = lines.length;\n\n // Check if no truncation needed\n if (totalLines <= maxLines && totalBytes <= maxBytes) {\n return {\n content,\n truncated: false,\n truncatedBy: null,\n totalLines,\n totalBytes,\n outputLines: totalLines,\n outputBytes: totalBytes,\n lastLinePartial: false,\n firstLineExceedsLimit: false,\n };\n }\n\n // Work backwards from the end\n const outputLinesArr: string[] = [];\n let outputBytesCount = 0;\n let truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n let lastLinePartial = false;\n\n for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n const line = lines[i];\n const lineBytes = Buffer.byteLength(line, \"utf-8\") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n if (outputBytesCount + lineBytes > maxBytes) {\n truncatedBy = \"bytes\";\n // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n // take the end of the line (partial)\n if (outputLinesArr.length === 0) {\n const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n outputLinesArr.unshift(truncatedLine);\n outputBytesCount = Buffer.byteLength(truncatedLine, \"utf-8\");\n lastLinePartial = true;\n }\n break;\n }\n\n outputLinesArr.unshift(line);\n outputBytesCount += lineBytes;\n }\n\n // If we exited due to line limit\n if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n truncatedBy = \"lines\";\n }\n\n const outputContent = outputLinesArr.join(\"\\n\");\n const finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n return {\n content: outputContent,\n truncated: true,\n truncatedBy,\n totalLines,\n totalBytes,\n outputLines: outputLinesArr.length,\n outputBytes: finalOutputBytes,\n lastLinePartial,\n firstLineExceedsLimit: false,\n };\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n const buf = Buffer.from(str, \"utf-8\");\n if (buf.length <= maxBytes) {\n return str;\n }\n\n // Start from the end, skip maxBytes back\n let start = buf.length - maxBytes;\n\n // Find a valid UTF-8 boundary (start of a character)\n while (start < buf.length && (buf[start] & 0xc0) === 0x80) {\n start++;\n }\n\n return buf.slice(start).toString(\"utf-8\");\n}\n"]}