snow-flow 10.0.185 → 10.0.186-dev.682

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 (62) hide show
  1. package/bin/index.js.map +9 -9
  2. package/bin/worker.js.map +7 -7
  3. package/mcp/servicenow-unified.js +116 -116
  4. package/package.json +1 -1
  5. package/parsers-config.ts +2 -1
  6. package/src/bun/index.ts +10 -9
  7. package/src/cli/cmd/agent.ts +3 -3
  8. package/src/cli/cmd/auth.ts +46 -0
  9. package/src/cli/cmd/import.ts +2 -2
  10. package/src/cli/cmd/session.ts +9 -12
  11. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +2 -1
  12. package/src/cli/cmd/tui/component/prompt/index.tsx +19 -6
  13. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  14. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  15. package/src/cli/cmd/tui/routes/home.tsx +16 -2
  16. package/src/cli/cmd/tui/routes/session/index.tsx +122 -53
  17. package/src/cli/cmd/tui/routes/session/permission.tsx +9 -1
  18. package/src/cli/cmd/tui/routes/session/sidebar.tsx +9 -1
  19. package/src/cli/cmd/tui/thread.ts +4 -1
  20. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +1 -1
  21. package/src/cli/cmd/tui/util/clipboard.ts +3 -3
  22. package/src/cli/cmd/tui/worker.ts +6 -1
  23. package/src/config/config.ts +28 -0
  24. package/src/context/context-db.ts +437 -0
  25. package/src/format/formatter.ts +14 -5
  26. package/src/global/index.ts +3 -4
  27. package/src/mcp/index.ts +7 -2
  28. package/src/mcp/oauth-callback.ts +7 -15
  29. package/src/mcp/oauth-provider.ts +34 -3
  30. package/src/project/project.ts +8 -4
  31. package/src/provider/models.ts +1 -1
  32. package/src/provider/provider.ts +88 -9
  33. package/src/provider/transform.ts +7 -2
  34. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_capacity_plan.ts +20 -7
  35. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_retrospective.ts +6 -8
  36. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_sprint_manage.ts +46 -28
  37. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_team_manage.ts +53 -41
  38. package/src/servicenow/servicenow-mcp-unified/tools/agile/snow_agile_velocity_report.ts +8 -1
  39. package/src/servicenow/servicenow-mcp-unified/tools/automation/snow_schedule_script_job.ts +388 -243
  40. package/src/session/compaction.ts +126 -23
  41. package/src/session/message-v2.ts +33 -10
  42. package/src/session/processor.ts +29 -17
  43. package/src/session/prompt.ts +34 -6
  44. package/src/share/share-next.ts +2 -2
  45. package/src/shell/shell.ts +2 -1
  46. package/src/tool/edit.ts +15 -1
  47. package/src/tool/registry.ts +9 -1
  48. package/src/tool/truncation.ts +17 -0
  49. package/src/tool/websearch.ts +1 -1
  50. package/src/tool/websearch.txt +2 -2
  51. package/src/tool/write.ts +3 -4
  52. package/src/util/filesystem.ts +36 -7
  53. package/src/util/keybind.ts +1 -1
  54. package/src/util/log.ts +8 -5
  55. package/src/util/token.ts +28 -0
  56. package/test/cli/plugin-auth-picker.test.ts +120 -0
  57. package/test/fixture/fixture.ts +3 -0
  58. package/test/mcp/oauth-auto-connect.test.ts +197 -0
  59. package/test/project/project.test.ts +47 -0
  60. package/test/provider/provider.test.ts +2 -0
  61. package/test/provider/transform.test.ts +32 -0
  62. package/test/tool/edit.test.ts +679 -0
@@ -14,6 +14,7 @@ import { fn } from "@/util/fn"
14
14
  import { Agent } from "@/agent/agent"
15
15
  import { Plugin } from "@/plugin"
16
16
  import { Config } from "@/config/config"
17
+ import { ContextDB } from "@/context/context-db"
17
18
 
18
19
  export namespace SessionCompaction {
19
20
  const log = Log.create({ service: "session.compaction" })
@@ -121,8 +122,31 @@ export namespace SessionCompaction {
121
122
  sessionID: string
122
123
  abort: AbortSignal
123
124
  auto: boolean
125
+ overflow?: boolean
124
126
  }) {
125
127
  const userMessage = input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User
128
+
129
+ let messages = input.messages
130
+ let replay: MessageV2.WithParts | undefined
131
+ if (input.overflow) {
132
+ const idx = input.messages.findIndex((m) => m.info.id === input.parentID)
133
+ for (let i = idx - 1; i >= 0; i--) {
134
+ const msg = input.messages[i]
135
+ if (msg.info.role === "user" && !msg.parts.some((p) => p.type === "compaction")) {
136
+ replay = msg
137
+ messages = input.messages.slice(0, i)
138
+ break
139
+ }
140
+ }
141
+ const hasContent = replay && messages.some(
142
+ (m) => m.info.role === "user" && !m.parts.some((p) => p.type === "compaction"),
143
+ )
144
+ if (!hasContent) {
145
+ replay = undefined
146
+ messages = input.messages
147
+ }
148
+ }
149
+
126
150
  const agent = await Agent.get("compaction")
127
151
  const model = agent.model
128
152
  ? await Provider.getModel(agent.model.providerID, agent.model.modelID)
@@ -175,7 +199,7 @@ export namespace SessionCompaction {
175
199
  tools: {},
176
200
  system: [],
177
201
  messages: [
178
- ...MessageV2.toModelMessages(input.messages, model),
202
+ ...MessageV2.toModelMessages(messages, model, { stripMedia: true }),
179
203
  {
180
204
  role: "user",
181
205
  content: [
@@ -189,32 +213,82 @@ export namespace SessionCompaction {
189
213
  model,
190
214
  })
191
215
 
216
+ if (result === "compact") {
217
+ processor.message.error = new MessageV2.ContextOverflowError({
218
+ message: replay
219
+ ? "Conversation history too large to compact - exceeds model context limit"
220
+ : "Session too large to compact - context exceeds model limit even after stripping media",
221
+ }).toObject()
222
+ processor.message.finish = "error"
223
+ await Session.updateMessage(processor.message)
224
+ return "stop"
225
+ }
226
+
192
227
  if (result === "continue" && input.auto) {
193
- const continueMsg = await Session.updateMessage({
194
- id: Identifier.ascending("message"),
195
- role: "user",
196
- sessionID: input.sessionID,
197
- time: {
198
- created: Date.now(),
199
- },
200
- agent: userMessage.agent,
201
- model: userMessage.model,
202
- })
203
- await Session.updatePart({
204
- id: Identifier.ascending("part"),
205
- messageID: continueMsg.id,
206
- sessionID: input.sessionID,
207
- type: "text",
208
- synthetic: true,
209
- text: "Continue if you have next steps",
210
- time: {
211
- start: Date.now(),
212
- end: Date.now(),
213
- },
214
- })
228
+ if (replay) {
229
+ const original = replay.info as MessageV2.User
230
+ const replayMsg = await Session.updateMessage({
231
+ id: Identifier.ascending("message"),
232
+ role: "user",
233
+ sessionID: input.sessionID,
234
+ time: { created: Date.now() },
235
+ agent: original.agent,
236
+ model: original.model,
237
+ tools: original.tools,
238
+ system: original.system,
239
+ variant: original.variant,
240
+ })
241
+ for (const part of replay.parts) {
242
+ if (part.type === "compaction") continue
243
+ const replayPart =
244
+ part.type === "file" && MessageV2.isMedia(part.mime)
245
+ ? { type: "text" as const, text: `[Attached ${part.mime}: ${part.filename ?? "file"}]` }
246
+ : part
247
+ await Session.updatePart({
248
+ ...replayPart,
249
+ id: Identifier.ascending("part"),
250
+ messageID: replayMsg.id,
251
+ sessionID: input.sessionID,
252
+ })
253
+ }
254
+ } else {
255
+ const continueMsg = await Session.updateMessage({
256
+ id: Identifier.ascending("message"),
257
+ role: "user",
258
+ sessionID: input.sessionID,
259
+ time: { created: Date.now() },
260
+ agent: userMessage.agent,
261
+ model: userMessage.model,
262
+ })
263
+ const text =
264
+ (input.overflow
265
+ ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n"
266
+ : "") + "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed."
267
+ await Session.updatePart({
268
+ id: Identifier.ascending("part"),
269
+ messageID: continueMsg.id,
270
+ sessionID: input.sessionID,
271
+ type: "text",
272
+ synthetic: true,
273
+ text,
274
+ time: {
275
+ start: Date.now(),
276
+ end: Date.now(),
277
+ },
278
+ })
279
+ }
215
280
  }
216
281
  if (processor.message.error) return "stop"
217
282
  Bus.publish(Event.Compacted, { sessionID: input.sessionID })
283
+
284
+ // Capture Instance context before fire-and-forget (AsyncLocalStorage won't
285
+ // be available once the promise escapes the current execution context).
286
+ const projectID = Instance.project.id
287
+ const directory = Instance.directory
288
+ persistSummary(input.sessionID, msg.id, projectID, directory).catch((e) => {
289
+ log.warn("failed to persist compaction summary", { error: e })
290
+ })
291
+
218
292
  return "continue"
219
293
  }
220
294
 
@@ -227,6 +301,7 @@ export namespace SessionCompaction {
227
301
  modelID: z.string(),
228
302
  }),
229
303
  auto: z.boolean(),
304
+ overflow: z.boolean().optional(),
230
305
  }),
231
306
  async (input) => {
232
307
  const msg = await Session.updateMessage({
@@ -245,7 +320,35 @@ export namespace SessionCompaction {
245
320
  sessionID: msg.sessionID,
246
321
  type: "compaction",
247
322
  auto: input.auto,
323
+ overflow: input.overflow,
248
324
  })
249
325
  },
250
326
  )
327
+
328
+ async function persistSummary(sessionID: string, messageID: string, projectID: string, directory: string) {
329
+ const config = await Config.get()
330
+ if (config.contextdb?.persist_summaries === false) return
331
+
332
+ const db = await ContextDB.get()
333
+ if (!db) return
334
+
335
+ const msgs = await Session.messages({ sessionID })
336
+ const compactionMsg = msgs.find((m) => m.info.id === messageID)
337
+ if (!compactionMsg) return
338
+
339
+ const summaryText = compactionMsg.parts
340
+ .filter((p) => p.type === "text")
341
+ .map((p) => (p as MessageV2.TextPart).text)
342
+ .join("\n")
343
+
344
+ if (!summaryText.trim()) return
345
+
346
+ ContextDB.storeSummary({
347
+ sessionID,
348
+ projectID,
349
+ directory,
350
+ summaryText,
351
+ filesMentioned: ContextDB.extractFileReferences(summaryText),
352
+ })
353
+ }
251
354
  }
@@ -16,6 +16,10 @@ import type { Provider } from "@/provider/provider"
16
16
  import { Instance } from "@/project/instance"
17
17
 
18
18
  export namespace MessageV2 {
19
+ export function isMedia(mime: string) {
20
+ return mime.startsWith("image/") || mime === "application/pdf"
21
+ }
22
+
19
23
  export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
20
24
  export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
21
25
  export const AuthError = NamedError.create(
@@ -37,6 +41,10 @@ export namespace MessageV2 {
37
41
  }),
38
42
  )
39
43
  export type APIError = z.infer<typeof APIError.Schema>
44
+ export const ContextOverflowError = NamedError.create(
45
+ "ContextOverflowError",
46
+ z.object({ message: z.string(), responseBody: z.string().optional() }),
47
+ )
40
48
 
41
49
  const PartBase = z.object({
42
50
  id: z.string(),
@@ -161,6 +169,7 @@ export namespace MessageV2 {
161
169
  export const CompactionPart = PartBase.extend({
162
170
  type: z.literal("compaction"),
163
171
  auto: z.boolean(),
172
+ overflow: z.boolean().optional(),
164
173
  }).meta({
165
174
  ref: "CompactionPart",
166
175
  })
@@ -362,6 +371,7 @@ export namespace MessageV2 {
362
371
  OutputLengthError.Schema,
363
372
  AbortedError.Schema,
364
373
  APIError.Schema,
374
+ ContextOverflowError.Schema,
365
375
  ])
366
376
  .optional(),
367
377
  parentID: z.string(),
@@ -529,7 +539,11 @@ export namespace MessageV2 {
529
539
  return result
530
540
  }
531
541
 
532
- export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
542
+ export function toModelMessages(
543
+ input: WithParts[],
544
+ model: Provider.Model,
545
+ options?: { stripMedia?: boolean },
546
+ ): ModelMessage[] {
533
547
  const result: UIMessage[] = []
534
548
  const toolNames = new Set<string>()
535
549
 
@@ -583,13 +597,21 @@ export namespace MessageV2 {
583
597
  text: part.text,
584
598
  })
585
599
  // text/plain and directory files are converted into text parts, ignore them
586
- if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory")
587
- userMessage.parts.push({
588
- type: "file",
589
- url: part.url,
590
- mediaType: part.mime,
591
- filename: part.filename,
592
- })
600
+ if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory") {
601
+ if (options?.stripMedia && isMedia(part.mime)) {
602
+ userMessage.parts.push({
603
+ type: "text",
604
+ text: `[Attached ${part.mime}: ${part.filename ?? "file"}]`,
605
+ })
606
+ } else {
607
+ userMessage.parts.push({
608
+ type: "file",
609
+ url: part.url,
610
+ mediaType: part.mime,
611
+ filename: part.filename,
612
+ })
613
+ }
614
+ }
593
615
 
594
616
  if (part.type === "compaction") {
595
617
  userMessage.parts.push({
@@ -638,7 +660,7 @@ export namespace MessageV2 {
638
660
  toolNames.add(part.tool)
639
661
  if (part.state.status === "completed") {
640
662
  const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output
641
- const attachments = part.state.time.compacted ? [] : (part.state.attachments ?? [])
663
+ const attachments = part.state.time.compacted || options?.stripMedia ? [] : (part.state.attachments ?? [])
642
664
  const output =
643
665
  attachments.length > 0
644
666
  ? {
@@ -746,7 +768,8 @@ export namespace MessageV2 {
746
768
  msg.parts.some((part) => part.type === "compaction")
747
769
  )
748
770
  break
749
- if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish) completed.add(msg.info.parentID)
771
+ if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish && !msg.info.error)
772
+ completed.add(msg.info.parentID)
750
773
  }
751
774
  result.reverse()
752
775
  return result
@@ -271,7 +271,10 @@ export namespace SessionProcessor {
271
271
  sessionID: input.sessionID,
272
272
  messageID: input.assistantMessage.parentID,
273
273
  })
274
- if (await SessionCompaction.isNearOverflow({ tokens: usage.tokens, model: input.model })) {
274
+ if (
275
+ !input.assistantMessage.summary &&
276
+ (await SessionCompaction.isOverflow({ tokens: usage.tokens, model: input.model }))
277
+ ) {
275
278
  needsCompaction = true
276
279
  }
277
280
  break
@@ -342,24 +345,33 @@ export namespace SessionProcessor {
342
345
  stack: JSON.stringify(e.stack),
343
346
  })
344
347
  const error = MessageV2.fromError(e, { providerID: input.model.providerID })
345
- const retry = SessionRetry.retryable(error)
346
- if (retry !== undefined) {
347
- attempt++
348
- const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined)
349
- SessionStatus.set(input.sessionID, {
350
- type: "retry",
351
- attempt,
352
- message: retry,
353
- next: Date.now() + delay,
348
+ if (MessageV2.ContextOverflowError.isInstance(error)) {
349
+ needsCompaction = true
350
+ Bus.publish(Session.Event.Error, {
351
+ sessionID: input.sessionID,
352
+ error,
353
+ })
354
+ } else {
355
+ const retry = SessionRetry.retryable(error)
356
+ if (retry !== undefined) {
357
+ attempt++
358
+ const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined)
359
+ SessionStatus.set(input.sessionID, {
360
+ type: "retry",
361
+ attempt,
362
+ message: retry,
363
+ next: Date.now() + delay,
364
+ })
365
+ await SessionRetry.sleep(delay, input.abort).catch(() => {})
366
+ continue
367
+ }
368
+ input.assistantMessage.error = error
369
+ Bus.publish(Session.Event.Error, {
370
+ sessionID: input.assistantMessage.sessionID,
371
+ error: input.assistantMessage.error,
354
372
  })
355
- await SessionRetry.sleep(delay, input.abort).catch(() => {})
356
- continue
373
+ SessionStatus.set(input.sessionID, { type: "idle" })
357
374
  }
358
- input.assistantMessage.error = error
359
- Bus.publish(Session.Event.Error, {
360
- sessionID: input.assistantMessage.sessionID,
361
- error: input.assistantMessage.error,
362
- })
363
375
  }
364
376
  if (snapshot) {
365
377
  const patch = await Snapshot.patch(snapshot)
@@ -26,7 +26,6 @@ import { ToolRegistry } from "../tool/registry"
26
26
  import { MCP } from "../mcp"
27
27
  import { LSP } from "../lsp"
28
28
  import { ReadTool } from "../tool/read"
29
- import { ListTool } from "../tool/ls"
30
29
  import { FileTime } from "../file/time"
31
30
  import { Flag } from "../flag/flag"
32
31
  import { ulid } from "ulid"
@@ -48,6 +47,7 @@ import { iife } from "@/util/iife"
48
47
  import { Shell } from "@/shell/shell"
49
48
  import { Truncate } from "@/tool/truncation"
50
49
  import { UsageReporter, ActivityReporter, AnonymousTelemetry } from "@/usage"
50
+ import { ContextDB } from "@/context/context-db"
51
51
 
52
52
  // @ts-ignore
53
53
  globalThis.AI_SDK_LOG_WARNINGS = false
@@ -277,6 +277,7 @@ export namespace SessionPrompt {
277
277
  using _ = defer(() => cancel(sessionID))
278
278
 
279
279
  let step = 0
280
+ let l2RetrievedForUser: string | undefined
280
281
  const session = await Session.get(sessionID)
281
282
  while (true) {
282
283
  SessionStatus.set(sessionID, { type: "busy" })
@@ -501,6 +502,7 @@ export namespace SessionPrompt {
501
502
  abort,
502
503
  sessionID,
503
504
  auto: task.auto,
505
+ overflow: task.overflow,
504
506
  })
505
507
  if (result === "stop") break
506
508
  continue
@@ -610,12 +612,32 @@ export namespace SessionPrompt {
610
612
 
611
613
  await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages })
612
614
 
615
+ // L2: cross-session context retrieval via ContextDB.
616
+ // Retrieve once per user turn (tracked by lastUser.id) to avoid
617
+ // redundant FTS queries during multi-step tool-call loops.
618
+ const needsL2 = lastUser.id !== l2RetrievedForUser
619
+ let l2Context: string[] = []
620
+ if (needsL2) {
621
+ const userQuery =
622
+ sessionMessages
623
+ .findLast((m) => m.info.role === "user")
624
+ ?.parts.filter((p) => p.type === "text" && !("synthetic" in p && (p as any).synthetic))
625
+ .map((p) => (p as MessageV2.TextPart).text)
626
+ .join(" ") ?? ""
627
+ l2Context = await ContextDB.retrieveContext({
628
+ sessionID,
629
+ projectID: Instance.project.id,
630
+ userQuery,
631
+ })
632
+ l2RetrievedForUser = lastUser.id
633
+ }
634
+
613
635
  const result = await processor.process({
614
636
  user: lastUser,
615
637
  agent,
616
638
  abort,
617
639
  sessionID,
618
- system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())],
640
+ system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system()), ...l2Context],
619
641
  messages: [
620
642
  ...MessageV2.toModelMessages(sessionMessages, model),
621
643
  ...(isLastStep
@@ -637,6 +659,7 @@ export namespace SessionPrompt {
637
659
  agent: lastUser.agent,
638
660
  model: lastUser.model,
639
661
  auto: true,
662
+ overflow: !processor.message.finish,
640
663
  })
641
664
  }
642
665
  continue
@@ -832,7 +855,12 @@ export namespace SessionPrompt {
832
855
  title: "",
833
856
  metadata,
834
857
  output: truncated.content,
835
- attachments,
858
+ attachments: attachments.map((attachment) => ({
859
+ ...attachment,
860
+ id: Identifier.ascending("part"),
861
+ sessionID: ctx.sessionID,
862
+ messageID: input.processor.message.id,
863
+ })),
836
864
  content: result.content, // directly return content to preserve ordering when outputting to model
837
865
  }
838
866
  }
@@ -1086,7 +1114,7 @@ export namespace SessionPrompt {
1086
1114
  }
1087
1115
 
1088
1116
  if (part.mime === "application/x-directory") {
1089
- const args = { path: filepath }
1117
+ const args = { filePath: filepath }
1090
1118
  const listCtx: Tool.Context = {
1091
1119
  sessionID: input.sessionID,
1092
1120
  abort: new AbortController().signal,
@@ -1097,7 +1125,7 @@ export namespace SessionPrompt {
1097
1125
  metadata: async () => {},
1098
1126
  ask: async () => {},
1099
1127
  }
1100
- const result = await ListTool.init().then((t) => t.execute(args, listCtx))
1128
+ const result = await ReadTool.init().then((t) => t.execute(args, listCtx))
1101
1129
  return [
1102
1130
  {
1103
1131
  id: Identifier.ascending("part"),
@@ -1105,7 +1133,7 @@ export namespace SessionPrompt {
1105
1133
  sessionID: input.sessionID,
1106
1134
  type: "text",
1107
1135
  synthetic: true,
1108
- text: `Called the list tool with the following input: ${JSON.stringify(args)}`,
1136
+ text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
1109
1137
  },
1110
1138
  {
1111
1139
  id: Identifier.ascending("part"),
@@ -31,7 +31,7 @@ export namespace ShareNext {
31
31
  await sync(evt.properties.info.sessionID, [
32
32
  {
33
33
  type: "message",
34
- data: evt.properties.info,
34
+ data: evt.properties.info as any,
35
35
  },
36
36
  ])
37
37
  if (evt.properties.info.role === "user") {
@@ -184,7 +184,7 @@ export namespace ShareNext {
184
184
  },
185
185
  ...messages.map((x) => ({
186
186
  type: "message" as const,
187
- data: x.info,
187
+ data: x.info as any,
188
188
  })),
189
189
  ...messages.flatMap((x) => x.parts.map((y) => ({ type: "part" as const, data: y }))),
190
190
  {
@@ -1,5 +1,6 @@
1
1
  import { Flag } from "@/flag/flag"
2
2
  import { lazy } from "@/util/lazy"
3
+ import { statSync } from "fs"
3
4
  import path from "path"
4
5
  import { spawn, type ChildProcess } from "child_process"
5
6
 
@@ -43,7 +44,7 @@ export namespace Shell {
43
44
  // git.exe is typically at: C:\Program Files\Git\cmd\git.exe
44
45
  // bash.exe is at: C:\Program Files\Git\bin\bash.exe
45
46
  const bash = path.join(git, "..", "..", "bin", "bash.exe")
46
- if (Bun.file(bash).size) return bash
47
+ try { if (statSync(bash).size) return bash } catch {}
47
48
  }
48
49
  return process.env.COMSPEC || "cmd.exe"
49
50
  }
package/src/tool/edit.ts CHANGED
@@ -24,6 +24,15 @@ function normalizeLineEndings(text: string): string {
24
24
  return text.replaceAll("\r\n", "\n")
25
25
  }
26
26
 
27
+ function detectLineEnding(text: string): "\n" | "\r\n" {
28
+ return text.includes("\r\n") ? "\r\n" : "\n"
29
+ }
30
+
31
+ function convertToLineEnding(text: string, ending: "\n" | "\r\n"): string {
32
+ if (ending === "\n") return text
33
+ return text.replaceAll("\n", "\r\n")
34
+ }
35
+
27
36
  export const EditTool = Tool.define("edit", {
28
37
  description: DESCRIPTION,
29
38
  parameters: z.object({
@@ -79,7 +88,12 @@ export const EditTool = Tool.define("edit", {
79
88
  if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
80
89
  await FileTime.assert(ctx.sessionID, filePath)
81
90
  contentOld = await file.text()
82
- contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
91
+
92
+ const ending = detectLineEnding(contentOld)
93
+ const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending)
94
+ const next = convertToLineEnding(normalizeLineEndings(params.newString), ending)
95
+
96
+ contentNew = replace(contentOld, old, next, params.replaceAll)
83
97
 
84
98
  diff = trimDiff(
85
99
  createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
@@ -157,9 +157,17 @@ export namespace ToolRegistry {
157
157
  })
158
158
  .map(async (t) => {
159
159
  using _ = log.time(t.id)
160
+ const tool = await t.init({ agent })
161
+ const output = {
162
+ description: tool.description,
163
+ parameters: tool.parameters,
164
+ }
165
+ await Plugin.trigger("tool.definition", { toolID: t.id }, output)
160
166
  return {
161
167
  id: t.id,
162
- ...(await t.init({ agent })),
168
+ ...tool,
169
+ description: output.description,
170
+ parameters: output.parameters,
163
171
  }
164
172
  }),
165
173
  )
@@ -5,6 +5,7 @@ import { Identifier } from "../id/id"
5
5
  import { PermissionNext } from "../permission/next"
6
6
  import type { Agent } from "../agent/agent"
7
7
  import { Scheduler } from "../scheduler"
8
+ import { ContextDB } from "@/context/context-db"
8
9
 
9
10
  export namespace Truncate {
10
11
  export const MAX_LINES = 2000
@@ -106,4 +107,20 @@ export namespace Truncate {
106
107
 
107
108
  return { content: message, truncated: true, outputPath: filepath }
108
109
  }
110
+
111
+ export function indexOutput(input: { sessionID: string; toolName: string; toolInput: string; summary: string; outputPath?: string }) {
112
+ const hash = new Bun.CryptoHasher("sha256").update(input.toolName + ":" + input.toolInput).digest("hex")
113
+ ContextDB.get()
114
+ .then((db) => {
115
+ if (!db) return
116
+ ContextDB.storeToolOutput({
117
+ sessionID: input.sessionID,
118
+ toolName: input.toolName,
119
+ inputHash: hash,
120
+ outputSummary: input.summary.slice(0, 2000),
121
+ outputPath: input.outputPath,
122
+ })
123
+ })
124
+ .catch(() => {})
125
+ }
109
126
  }
@@ -39,7 +39,7 @@ interface McpSearchResponse {
39
39
  export const WebSearchTool = Tool.define("websearch", async () => {
40
40
  return {
41
41
  get description() {
42
- return DESCRIPTION.replace("{{date}}", new Date().toISOString().slice(0, 10))
42
+ return DESCRIPTION.replace("{{year}}", new Date().getFullYear().toString())
43
43
  },
44
44
  parameters: z.object({
45
45
  query: z.string().describe("Websearch query"),
@@ -10,5 +10,5 @@ Usage notes:
10
10
  - Configurable context length for optimal LLM integration
11
11
  - Domain filtering and advanced search options available
12
12
 
13
- Today's date is {{date}}. You MUST use this year when searching for recent information or current events
14
- - Example: If today is 2025-07-15 and the user asks for "latest AI news", search for "AI news 2025", NOT "AI news 2024"
13
+ The current year is {{year}}. You MUST use this year when searching for recent information or current events
14
+ - Example: If the current year is 2026 and the user asks for "latest AI news", search for "AI news 2026", NOT "AI news 2025"
package/src/tool/write.ts CHANGED
@@ -26,9 +26,8 @@ export const WriteTool = Tool.define("write", {
26
26
  const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
27
27
  await assertExternalDirectory(ctx, filepath)
28
28
 
29
- const file = Bun.file(filepath)
30
- const exists = await file.exists()
31
- const contentOld = exists ? await file.text() : ""
29
+ const exists = await Filesystem.exists(filepath)
30
+ const contentOld = exists ? await Filesystem.readText(filepath) : ""
32
31
  if (exists) await FileTime.assert(ctx.sessionID, filepath)
33
32
 
34
33
  const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
@@ -42,7 +41,7 @@ export const WriteTool = Tool.define("write", {
42
41
  },
43
42
  })
44
43
 
45
- await Bun.write(filepath, params.content)
44
+ await Filesystem.write(filepath, params.content)
46
45
  await Bus.publish(File.Event.Edited, {
47
46
  file: filepath,
48
47
  })