@daniel156161/prism 0.2.78 → 0.2.82

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 (204) hide show
  1. package/dist/pi/pi-env.d.ts +1 -1
  2. package/dist/pi/pi-env.js +1 -1
  3. package/dist/pi/pi-patch-interactive.js +8 -5
  4. package/dist/pi/pi-patch-interactive.js.map +1 -1
  5. package/dist/prism-extensions/integrations/ai-memory-errors.d.ts +29 -0
  6. package/dist/prism-extensions/integrations/ai-memory-errors.js +106 -0
  7. package/dist/prism-extensions/integrations/ai-memory-errors.js.map +1 -0
  8. package/dist/prism-extensions/integrations/ai-memory-http.d.ts +28 -0
  9. package/dist/prism-extensions/integrations/ai-memory-http.js +92 -0
  10. package/dist/prism-extensions/integrations/ai-memory-http.js.map +1 -0
  11. package/dist/prism-extensions/integrations/ai-memory-system.d.ts +5 -0
  12. package/dist/prism-extensions/integrations/ai-memory-system.js +205 -123
  13. package/dist/prism-extensions/integrations/ai-memory-system.js.map +1 -1
  14. package/dist/prism-extensions/integrations/ai-memory-write-preview.d.ts +36 -0
  15. package/dist/prism-extensions/integrations/ai-memory-write-preview.js +67 -0
  16. package/dist/prism-extensions/integrations/ai-memory-write-preview.js.map +1 -0
  17. package/dist/prism-extensions/integrations/research-tools.js +3 -3
  18. package/dist/prism-extensions/integrations/research-tools.js.map +1 -1
  19. package/dist/prism-extensions/integrations/youtube.d.ts +2 -2
  20. package/dist/prism-extensions/integrations/youtube.js +6 -7
  21. package/dist/prism-extensions/integrations/youtube.js.map +1 -1
  22. package/dist/prism-extensions/ui/collapsed-text-rendering.d.ts +4 -2
  23. package/dist/prism-extensions/ui/collapsed-text-rendering.js +12 -7
  24. package/dist/prism-extensions/ui/collapsed-text-rendering.js.map +1 -1
  25. package/node_modules/@earendil-works/pi-agent-core/README.md +4 -4
  26. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  27. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +5 -1
  28. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  29. package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
  30. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +3 -0
  31. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
  32. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts +6 -0
  33. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts.map +1 -1
  34. package/node_modules/@earendil-works/pi-agent-core/dist/types.js.map +1 -1
  35. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  36. package/node_modules/@earendil-works/pi-ai/README.md +8 -1
  37. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.d.ts.map +1 -1
  38. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js +1 -0
  39. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js.map +1 -1
  40. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +2 -0
  41. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  42. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +2 -0
  43. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js.map +1 -1
  44. package/node_modules/@earendil-works/pi-ai/dist/providers/all.d.ts.map +1 -1
  45. package/node_modules/@earendil-works/pi-ai/dist/providers/all.js +2 -0
  46. package/node_modules/@earendil-works/pi-ai/dist/providers/all.js.map +1 -1
  47. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  48. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  49. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  50. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  51. package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan-individual.json +1 -0
  52. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  53. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-individual.d.ts +3 -0
  54. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-individual.d.ts.map +1 -0
  55. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-individual.js +15 -0
  56. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-individual.js.map +1 -0
  57. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-individual.models.d.ts +4 -0
  58. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-individual.models.d.ts.map +1 -0
  59. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-individual.models.js +6 -0
  60. package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-individual.models.js.map +1 -0
  61. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +1 -1
  62. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  63. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  64. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  65. package/node_modules/@earendil-works/pi-client/CHANGELOG.md +1 -1
  66. package/node_modules/@earendil-works/pi-client/package.json +2 -2
  67. package/node_modules/@earendil-works/pi-coding-agent/CHANGELOG.md +31 -2
  68. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  69. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js +3 -3
  70. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js.map +1 -1
  71. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/auth-check.d.ts +19 -0
  72. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/auth-check.d.ts.map +1 -0
  73. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/auth-check.js +50 -0
  74. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/auth-check.js.map +1 -0
  75. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/auth-command.d.ts +24 -0
  76. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/auth-command.d.ts.map +1 -0
  77. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/auth-command.js +102 -0
  78. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/auth-command.js.map +1 -0
  79. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/credential-print.d.ts +4 -14
  80. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/credential-print.d.ts.map +1 -1
  81. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/credential-print.js +38 -79
  82. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/credential-print.js.map +1 -1
  83. package/node_modules/@earendil-works/pi-coding-agent/dist/cli.js +1 -1
  84. package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +11 -6
  85. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.js +1 -1
  86. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +0 -5
  87. package/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-storage.d.ts +10 -0
  88. package/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  89. package/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-storage.js +79 -2
  90. package/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  91. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js +1 -3
  92. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
  93. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  94. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  95. package/node_modules/@earendil-works/pi-coding-agent/dist/core/keybindings.d.ts +8 -0
  96. package/node_modules/@earendil-works/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  97. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  98. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +1 -0
  99. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  100. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-runtime.d.ts +2 -0
  101. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-runtime.d.ts.map +1 -1
  102. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-runtime.js +3 -1
  103. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-runtime.js.map +1 -1
  104. package/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js +1 -1
  105. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +1 -1
  106. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +37 -39
  107. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js +16 -20
  108. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  109. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  110. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/bash.js +6 -4
  111. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  112. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.d.ts +4 -0
  113. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  114. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.js +11 -7
  115. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  116. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/find.d.ts +4 -0
  117. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/find.d.ts.map +1 -1
  118. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/find.js +5 -1
  119. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/find.js.map +1 -1
  120. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/grep.d.ts +4 -0
  121. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/grep.d.ts.map +1 -1
  122. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/grep.js +5 -1
  123. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/grep.js.map +1 -1
  124. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.d.ts +4 -0
  125. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.d.ts.map +1 -1
  126. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.js +5 -1
  127. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.js.map +1 -1
  128. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.d.ts +4 -0
  129. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  130. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +6 -2
  131. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  132. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.d.ts +4 -0
  133. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  134. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.js +6 -2
  135. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  136. package/node_modules/@earendil-works/pi-coding-agent/dist/main.d.ts.map +1 -1
  137. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +57 -21
  138. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js.map +1 -1
  139. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/session-selector.js +0 -4
  140. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  141. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  142. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +48 -233
  143. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  144. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  145. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js +11 -3
  146. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  147. package/node_modules/@earendil-works/pi-coding-agent/dist/server/create-harness.d.ts +26 -0
  148. package/node_modules/@earendil-works/pi-coding-agent/dist/server/create-harness.d.ts.map +1 -0
  149. package/node_modules/@earendil-works/pi-coding-agent/dist/server/create-harness.js +104 -0
  150. package/node_modules/@earendil-works/pi-coding-agent/dist/server/create-harness.js.map +1 -0
  151. package/node_modules/@earendil-works/pi-coding-agent/docs/extensions.md +3 -2
  152. package/node_modules/@earendil-works/pi-coding-agent/docs/keybindings.md +9 -5
  153. package/node_modules/@earendil-works/pi-coding-agent/docs/providers.md +8 -1
  154. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  155. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-anthropic/package.json +1 -1
  156. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  157. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/gondolin/package-lock.json +2 -2
  158. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/gondolin/package.json +1 -1
  159. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/sandbox/package-lock.json +2 -2
  160. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/sandbox/package.json +1 -1
  161. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/with-deps/package-lock.json +2 -2
  162. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/with-deps/package.json +1 -1
  163. package/node_modules/@earendil-works/pi-coding-agent/package.json +7 -7
  164. package/node_modules/@earendil-works/pi-protocol/package.json +1 -1
  165. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  166. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js +1 -1
  167. package/node_modules/@earendil-works/pi-tui/dist/keybindings.d.ts +10 -0
  168. package/node_modules/@earendil-works/pi-tui/dist/keybindings.d.ts.map +1 -1
  169. package/node_modules/@earendil-works/pi-tui/dist/keybindings.js +8 -0
  170. package/node_modules/@earendil-works/pi-tui/dist/keybindings.js.map +1 -1
  171. package/node_modules/@earendil-works/pi-tui/dist/latex.d.ts.map +1 -1
  172. package/node_modules/@earendil-works/pi-tui/dist/latex.js +188 -48
  173. package/node_modules/@earendil-works/pi-tui/dist/latex.js.map +1 -1
  174. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.d.ts +12 -0
  175. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.d.ts.map +1 -1
  176. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js +146 -12
  177. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js.map +1 -1
  178. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  179. package/node_modules/@types/node/README.md +1 -1
  180. package/node_modules/@types/node/child_process.d.ts +0 -1
  181. package/node_modules/@types/node/crypto.d.ts +6 -6
  182. package/node_modules/@types/node/dns/promises.d.ts +5 -5
  183. package/node_modules/@types/node/dns.d.ts +5 -5
  184. package/node_modules/@types/node/fs.d.ts +7 -0
  185. package/node_modules/@types/node/http.d.ts +30 -0
  186. package/node_modules/@types/node/http2.d.ts +4 -4
  187. package/node_modules/@types/node/os.d.ts +30 -30
  188. package/node_modules/@types/node/package.json +2 -2
  189. package/node_modules/@types/node/perf_hooks.d.ts +2 -1
  190. package/node_modules/@types/node/process.d.ts +42 -43
  191. package/node_modules/@types/node/quic.d.ts +1049 -84
  192. package/node_modules/@types/node/sqlite.d.ts +14 -1
  193. package/node_modules/@types/node/stream/iter.d.ts +3 -2
  194. package/node_modules/@types/node/test.d.ts +61 -0
  195. package/node_modules/@types/node/v8.d.ts +20 -23
  196. package/package.json +6 -6
  197. package/src/prism-extensions/integrations/ai-memory-errors.ts +112 -0
  198. package/src/prism-extensions/integrations/ai-memory-http.ts +114 -0
  199. package/src/prism-extensions/integrations/ai-memory-system.ts +221 -132
  200. package/src/prism-extensions/integrations/ai-memory-write-preview.ts +83 -0
  201. package/src/prism-extensions/integrations/research-tools.ts +3 -3
  202. package/src/prism-extensions/integrations/youtube.ts +6 -6
  203. package/src/prism-extensions/ui/collapsed-text-rendering.ts +14 -8
  204. package/node_modules/@earendil-works/pi-coding-agent/dist/core/prism-session-db.js +0 -128
@@ -2,24 +2,39 @@ type ExtensionAPI = any
2
2
 
3
3
  import { Type } from "typebox"
4
4
  import { readSettings } from "../core/shared-config.js"
5
+ import { formatDegradedNotice } from "./ai-memory-errors.js"
6
+ import {
7
+ aiMemoryBaseUrl,
8
+ aiMemoryInjectCacheTtlMs,
9
+ aiMemoryInjectTimeoutMs,
10
+ aiMemoryRequestTimeoutMs,
11
+ aiMemoryStatusTimeoutMs,
12
+ deleteJson,
13
+ getJson,
14
+ methodJson,
15
+ postJson,
16
+ putJson,
17
+ } from "./ai-memory-http.js"
18
+ import { isAiMemoryWriteTool, renderAiMemoryWriteResult } from "./ai-memory-write-preview.js"
5
19
  import { renderCollapsibleTextResult } from "../ui/collapsed-text-rendering.js"
6
- import { formatBracketedToolCall, renderSingleLineToolCall, type ThemeLike } from "../ui/tool-call-rendering.js"
20
+ import { obsidianOpenUrl } from "./obsidian-memory.js"
21
+ import { ansiHyperlink, formatBracketedToolCall, renderSingleLineToolCall, type ThemeLike } from "../ui/tool-call-rendering.js"
7
22
 
8
- const DEFAULT_BASE_URL = "http://127.0.0.1:8765"
9
23
  const DEFAULT_LIMIT = 8
10
24
  const DEFAULT_CONTEXT_LIMIT = 5
11
25
  const DEFAULT_CONTEXT_SCORE_THRESHOLD = 0.15
12
- const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
13
- const DEFAULT_STATUS_TIMEOUT_MS = 1_500
14
- const DEFAULT_INJECT_TIMEOUT_MS = 1_200
15
- const DEFAULT_INJECT_CACHE_TTL_MS = 60_000
16
26
  const DEFAULT_ALWAYS_CONTEXT_CACHE_TTL_MS = 60_000
17
27
 
18
28
  const injectCache = new Map<string, { expiresAt: number; results: any[] }>()
19
29
  const alwaysContextCache = { expiresAt: 0, content: "" }
20
-
21
- function aiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
22
- return (env.PRISM_AI_MEMORY_BASE_URL || env.AI_MEMORY_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "")
30
+ const reportedBackgroundFailures = new Set<string>()
31
+
32
+ /** Test hook: drop injection caches and one-shot failure notices. */
33
+ export function resetAiMemoryCaches(): void {
34
+ injectCache.clear()
35
+ alwaysContextCache.expiresAt = 0
36
+ alwaysContextCache.content = ""
37
+ reportedBackgroundFailures.clear()
23
38
  }
24
39
 
25
40
  function clampLimit(value: unknown, fallback = DEFAULT_LIMIT): number {
@@ -68,28 +83,6 @@ function aiMemoryContextScoreThreshold(env: NodeJS.ProcessEnv = process.env): nu
68
83
  return Number.isFinite(parsed) ? Math.max(0, parsed) : DEFAULT_CONTEXT_SCORE_THRESHOLD
69
84
  }
70
85
 
71
- function positiveIntegerSetting(value: unknown, fallback: number): number {
72
- const parsed = Number(value)
73
- if (!Number.isFinite(parsed)) return fallback
74
- return Math.max(1, Math.trunc(parsed))
75
- }
76
-
77
- function aiMemoryRequestTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
78
- return positiveIntegerSetting(env.PRISM_AI_MEMORY_TIMEOUT_MS ?? env.PI_AI_MEMORY_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS)
79
- }
80
-
81
- function aiMemoryStatusTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
82
- return positiveIntegerSetting(env.PRISM_AI_MEMORY_STATUS_TIMEOUT_MS ?? env.PI_AI_MEMORY_STATUS_TIMEOUT_MS, DEFAULT_STATUS_TIMEOUT_MS)
83
- }
84
-
85
- function aiMemoryInjectTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
86
- return positiveIntegerSetting(env.PRISM_AI_MEMORY_INJECT_TIMEOUT_MS ?? env.PI_AI_MEMORY_INJECT_TIMEOUT_MS, DEFAULT_INJECT_TIMEOUT_MS)
87
- }
88
-
89
- function aiMemoryInjectCacheTtlMs(env: NodeJS.ProcessEnv = process.env): number {
90
- return positiveIntegerSetting(env.PRISM_AI_MEMORY_INJECT_CACHE_TTL_MS ?? env.PI_AI_MEMORY_INJECT_CACHE_TTL_MS, DEFAULT_INJECT_CACHE_TTL_MS)
91
- }
92
-
93
86
  function cleanFilters(params: any): Record<string, string> {
94
87
  const filters: Record<string, string> = {}
95
88
  for (const key of ["source", "project", "type", "status", "tag", "path"]) {
@@ -99,74 +92,6 @@ function cleanFilters(params: any): Record<string, string> {
99
92
  return filters
100
93
  }
101
94
 
102
- async function postJson<T>(path: string, body: unknown, timeoutMs = aiMemoryRequestTimeoutMs()): Promise<T> {
103
- const baseUrl = aiMemoryBaseUrl()
104
- const response = await fetch(`${baseUrl}${path}`, {
105
- method: "POST",
106
- headers: { "content-type": "application/json; charset=utf-8", accept: "application/json" },
107
- body: JSON.stringify(body),
108
- signal: AbortSignal.timeout(timeoutMs),
109
- })
110
- const text = await response.text()
111
- if (!response.ok) throw new Error(`AI Memory request failed: HTTP ${response.status} ${text.slice(0, 500)}`)
112
- return JSON.parse(text) as T
113
- }
114
-
115
- async function putJson<T>(path: string, body: unknown, timeoutMs = aiMemoryRequestTimeoutMs()): Promise<T> {
116
- const baseUrl = aiMemoryBaseUrl()
117
- const response = await fetch(`${baseUrl}${path}`, {
118
- method: "PUT",
119
- headers: { "content-type": "application/json; charset=utf-8", accept: "application/json" },
120
- body: JSON.stringify(body),
121
- signal: AbortSignal.timeout(timeoutMs),
122
- })
123
- const text = await response.text()
124
- if (!response.ok) throw new Error(`AI Memory request failed: HTTP ${response.status} ${text.slice(0, 500)}`)
125
- return JSON.parse(text) as T
126
- }
127
-
128
- async function deleteJson<T>(path: string, body: unknown, timeoutMs = aiMemoryRequestTimeoutMs()): Promise<T> {
129
- const baseUrl = aiMemoryBaseUrl()
130
- const response = await fetch(`${baseUrl}${path}`, {
131
- method: "DELETE",
132
- headers: { "content-type": "application/json; charset=utf-8", accept: "application/json" },
133
- body: JSON.stringify(body),
134
- signal: AbortSignal.timeout(timeoutMs),
135
- })
136
- const text = await response.text()
137
- if (!response.ok) throw new Error(`AI Memory request failed: HTTP ${response.status} ${text.slice(0, 500)}`)
138
- return JSON.parse(text) as T
139
- }
140
-
141
- async function methodJson<T>(
142
- method: string,
143
- path: string,
144
- timeoutMs = aiMemoryRequestTimeoutMs(),
145
- body?: unknown,
146
- ): Promise<T> {
147
- const baseUrl = aiMemoryBaseUrl()
148
- const response = await fetch(`${baseUrl}${path}`, {
149
- method,
150
- headers: { accept: "application/json", ...(body === undefined ? {} : { "content-type": "application/json; charset=utf-8" }) },
151
- body: body === undefined ? undefined : JSON.stringify(body),
152
- signal: AbortSignal.timeout(timeoutMs),
153
- })
154
- const text = await response.text()
155
- if (!response.ok) throw new Error(`AI Memory request failed: HTTP ${response.status} ${text.slice(0, 500)}`)
156
- return JSON.parse(text) as T
157
- }
158
-
159
- async function getJson<T>(path: string, timeoutMs = aiMemoryStatusTimeoutMs()): Promise<T> {
160
- const baseUrl = aiMemoryBaseUrl()
161
- const response = await fetch(`${baseUrl}${path}`, {
162
- headers: { accept: "application/json" },
163
- signal: AbortSignal.timeout(timeoutMs),
164
- })
165
- const text = await response.text()
166
- if (!response.ok) throw new Error(`AI Memory request failed: HTTP ${response.status} ${text.slice(0, 500)}`)
167
- return JSON.parse(text) as T
168
- }
169
-
170
95
  function formatSearchResults(results: any[]): string {
171
96
  if (!Array.isArray(results) || results.length === 0) return "No AI Memory results found. Is the index built?"
172
97
  return results.map((row, index) => {
@@ -176,11 +101,19 @@ function formatSearchResults(results: any[]): string {
176
101
  const lexical = Number(row.lexical_score ?? 0).toFixed(4)
177
102
  const graph = Number(row.graph_score ?? 0).toFixed(1)
178
103
  const fact = Number(row.fact_score ?? 0).toFixed(1)
104
+ const feedback = Number(row.feedback_score ?? 0).toFixed(2)
105
+ const stale = Number(row.stale_score ?? 0).toFixed(2)
179
106
  const excerpt = String(row.text ?? "").replace(/\s+/g, " ").trim().slice(0, 700)
180
- return `${index + 1}. score=${score} vector=${vector} keyword=${keyword} lexical=${lexical} graph=${graph} fact=${fact}\nsource=${row.source}:${row.path} > ${row.heading}\n${excerpt}`
107
+ return `${index + 1}. score=${score} vector=${vector} keyword=${keyword} lexical=${lexical} graph=${graph} fact=${fact} feedback=${feedback} stale=${stale}\nsource=${row.source}:${row.path} > ${row.heading}\n${excerpt}`
181
108
  }).join("\n\n")
182
109
  }
183
110
 
111
+ export function formatSearchResponse(data: any): string {
112
+ const results = Array.isArray(data?.results) ? data.results : []
113
+ const notices = [formatDegradedNotice(data?.degraded), String(data?.hint ?? "").trim()].filter(Boolean)
114
+ return [...notices, formatSearchResults(results)].join("\n\n")
115
+ }
116
+
184
117
  function formatReadResult(data: any): string {
185
118
  const source = String(data?.source ?? "unknown")
186
119
  const path = String(data?.path ?? "unknown")
@@ -277,9 +210,37 @@ function renderAiMemoryResult(result: any, options: { expanded?: boolean; isPart
277
210
  })
278
211
  }
279
212
 
280
- function formatAiMemoryToolCall(toolName: string, args: any, theme: ThemeLike): string {
213
+ export function formatAiMemoryToolCall(toolName: string, args: any, theme: ThemeLike): string {
281
214
  const label = toolName.replace(/^ai_memory_/, "ai memory ").replace(/_/g, " ")
282
- return formatBracketedToolCall(label, args?.query || args?.id || args?.path || args?.session || "", theme)
215
+ const rawValue = args?.query || args?.id || args?.path || args?.session || ""
216
+ const value = typeof rawValue === "string" && rawValue.trim() ? rawValue.trim() : "..."
217
+ const linksToVaultNote = isAiMemoryWriteTool(toolName) || toolName === "ai_memory_vault_delete"
218
+ if (linksToVaultNote && value !== "...") {
219
+ return `${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("accent", ansiHyperlink(obsidianOpenUrl(value), value))}`
220
+ }
221
+ return formatBracketedToolCall(label, value, theme)
222
+ }
223
+
224
+ /**
225
+ * Surface background (non-tool) failures once per distinct reason. The dedupe key
226
+ * ignores the per-turn query so a broken API does not warn on every prompt.
227
+ */
228
+ function reportBackgroundFailure(ctx: any, label: string, error: unknown): void {
229
+ const message = `AI Memory ${label} failed: ${error instanceof Error ? error.message : String(error)}`
230
+ const key = `${label}|${message.split(" | query=")[0]}`
231
+ if (reportedBackgroundFailures.has(key)) return
232
+ reportedBackgroundFailures.add(key)
233
+ ctx?.ui?.notify?.(message, "warning")
234
+ }
235
+
236
+ function formatVaultWriteResult(action: "Wrote" | "Edited", params: any, responsePath: unknown): string {
237
+ const path = String(responsePath ?? params.path ?? "")
238
+ const frontmatter = params.frontmatter === undefined ? "" : String(params.frontmatter).trim()
239
+ const heading = action === "Edited" ? `# ${String(params.heading ?? "").trim()}` : ""
240
+ const body = String(params.body ?? "")
241
+ const writtenContent = [frontmatter, heading, body].filter(Boolean).join("\n\n") || "(empty)"
242
+ const status = action === "Wrote" ? `Wrote: ${path}\n\n` : ""
243
+ return `${status}Write content:\n${writtenContent}`
283
244
  }
284
245
 
285
246
  export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
@@ -305,12 +266,12 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
305
266
  path: Type.Optional(Type.String({ description: "Path substring filter" })),
306
267
  }),
307
268
  async execute(_id: string, params: any) {
308
- const data = await postJson<{ results: any[] }>("/search", {
269
+ const data = await postJson<{ results: any[]; degraded?: any; hint?: string }>("/search", {
309
270
  query: String(params.query ?? ""),
310
271
  limit: clampLimit(params.limit),
311
272
  filters: cleanFilters(params),
312
273
  })
313
- return { content: [{ type: "text", text: formatSearchResults(data.results) }], details: { count: data.results?.length ?? 0, results: data.results } }
274
+ return { content: [{ type: "text", text: formatSearchResponse(data) }], details: { count: data.results?.length ?? 0, results: data.results, degraded: data.degraded, hint: data.hint } }
314
275
  },
315
276
  renderCall(args: any, theme: ThemeLike, context: any) {
316
277
  return renderSingleLineToolCall(formatAiMemoryToolCall("ai_memory_search", args, theme), context)
@@ -369,6 +330,130 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
369
330
  },
370
331
  })
371
332
 
333
+ pi.registerTool({
334
+ name: "ai_memory_rate_result",
335
+ label: "AI Memory Rate Result",
336
+ description: "Rate one retrieved memory result so future ranking learns from it.",
337
+ promptSnippet: "Use ai_memory_rate_result when a retrieved memory was clearly helpful, irrelevant, outdated, or wrong.",
338
+ promptGuidelines: [
339
+ "Rate the path exactly as returned by ai_memory_search.",
340
+ "Use 'outdated' or 'wrong' when the user corrects information that came from memory.",
341
+ "This changes ranking only; it never writes or deletes a note.",
342
+ ],
343
+ parameters: Type.Object({
344
+ path: Type.String({ description: "Path of the rated result, e.g. 02 Projekte/Memory.md" }),
345
+ rating: Type.String({ description: "helpful, irrelevant, outdated, or wrong" }),
346
+ query: Type.Optional(Type.String({ description: "Search query that produced the result" })),
347
+ reason: Type.Optional(Type.String({ description: "Short reason in German" })),
348
+ source: Type.Optional(Type.String({ description: "obsidian or prism_session" })),
349
+ chunk_id: Type.Optional(Type.String({ description: "Exact chunk id if known" })),
350
+ session_id: Type.Optional(Type.String({ description: "Originating session id" })),
351
+ }),
352
+ async execute(_id: string, params: any) {
353
+ const data = await postJson<any>("/retrieval-feedback", {
354
+ path: String(params.path ?? ""),
355
+ rating: String(params.rating ?? ""),
356
+ query: String(params.query ?? ""),
357
+ reason: String(params.reason ?? ""),
358
+ source: String(params.source ?? ""),
359
+ chunk_id: String(params.chunk_id ?? ""),
360
+ session_id: String(params.session_id ?? ""),
361
+ })
362
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], details: data }
363
+ },
364
+ renderCall(args: any, theme: ThemeLike, context: any) {
365
+ return renderSingleLineToolCall(formatAiMemoryToolCall("ai_memory_rate_result", args, theme), context)
366
+ },
367
+ })
368
+
369
+ pi.registerTool({
370
+ name: "ai_memory_session_review",
371
+ label: "AI Memory Session Review",
372
+ description: "Review a Prism session for outcome evidence and human-approved durable lesson proposals.",
373
+ promptSnippet: "Review a completed session and return pending lessons; this does not approve them.",
374
+ parameters: Type.Object({
375
+ session: Type.Optional(Type.String({ description: "Session path/id or latest" })),
376
+ force: Type.Optional(Type.Boolean({ description: "Review an unchanged session again" })),
377
+ }),
378
+ async execute(_id: string, params: any) {
379
+ const data = await postJson<any>("/session-review", {
380
+ session: typeof params.session === "string" ? params.session : "latest",
381
+ force: params.force === true,
382
+ })
383
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], details: data }
384
+ },
385
+ renderCall(args: any, theme: ThemeLike, context: any) {
386
+ return renderSingleLineToolCall(formatAiMemoryToolCall("ai_memory_session_review", args, theme), context)
387
+ },
388
+ })
389
+
390
+ pi.registerTool({
391
+ name: "ai_memory_propose_lesson",
392
+ label: "AI Memory Propose Lesson",
393
+ description: "Create a pending durable lesson proposal. User approval is still required before writing memory.",
394
+ promptSnippet: "Propose a lesson from explicit user feedback without promoting it automatically.",
395
+ parameters: Type.Object({
396
+ category: Type.String({ description: "correction, decision, preference, workflow, or quality" }),
397
+ title: Type.String({ description: "Short German title" }),
398
+ rule: Type.String({ description: "Proposed durable rule in German" }),
399
+ rationale: Type.Optional(Type.String({ description: "Why the rule matters" })),
400
+ evidence: Type.String({ description: "Exact user feedback supporting the proposal" }),
401
+ session_id: Type.Optional(Type.String({ description: "Originating session id" })),
402
+ confidence: Type.Optional(Type.Number({ description: "Confidence from 0 to 1" })),
403
+ }),
404
+ async execute(_id: string, params: any) {
405
+ const data = await postJson<any>("/lessons", {
406
+ category: String(params.category ?? ""),
407
+ title: String(params.title ?? ""),
408
+ rule: String(params.rule ?? ""),
409
+ rationale: String(params.rationale ?? ""),
410
+ evidence: String(params.evidence ?? ""),
411
+ session_id: String(params.session_id ?? "manual"),
412
+ confidence: params.confidence === undefined ? 0.8 : Number(params.confidence),
413
+ })
414
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], details: data }
415
+ },
416
+ renderCall(args: any, theme: ThemeLike, context: any) {
417
+ return renderSingleLineToolCall(formatAiMemoryToolCall("ai_memory_propose_lesson", args, theme), context)
418
+ },
419
+ })
420
+
421
+ pi.registerTool({
422
+ name: "ai_memory_feedback",
423
+ label: "AI Memory Feedback",
424
+ description: "List pending lessons or approve/reject one proposal. Approval writes durable memory.",
425
+ promptSnippet: "Use only after explicit user approval to approve a proposed durable lesson.",
426
+ promptGuidelines: ["Never approve a lesson without explicit user approval in the current conversation."],
427
+ parameters: Type.Object({
428
+ action: Type.Optional(Type.String({ description: "list (default), approve, or reject" })),
429
+ proposal_id: Type.Optional(Type.String({ description: "Required for approve/reject" })),
430
+ status: Type.Optional(Type.String({ description: "List filter: pending, approved, or rejected" })),
431
+ session_id: Type.Optional(Type.String({ description: "Optional session filter for listing" })),
432
+ }),
433
+ async execute(_id: string, params: any) {
434
+ const action = String(params.action ?? "list")
435
+ let data: any
436
+ if (action === "list") {
437
+ const query = new URLSearchParams()
438
+ if (params.status) query.set("status", String(params.status))
439
+ if (params.session_id) query.set("session_id", String(params.session_id))
440
+ data = await getJson<any>(`/lessons${query.size ? `?${query}` : ""}`, aiMemoryRequestTimeoutMs())
441
+ } else {
442
+ if (!["approve", "reject"].includes(action) || !params.proposal_id) {
443
+ throw new Error("action must be list, approve, or reject; proposal_id is required for decisions")
444
+ }
445
+ data = await methodJson<any>("DECISION", "/lessons", aiMemoryRequestTimeoutMs(), {
446
+ proposal_id: String(params.proposal_id),
447
+ decision: action,
448
+ })
449
+ }
450
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], details: data }
451
+ },
452
+ renderCall(args: any, theme: ThemeLike, context: any) {
453
+ return renderSingleLineToolCall(formatAiMemoryToolCall("ai_memory_feedback", args, theme), context)
454
+ },
455
+ })
456
+
372
457
  // ── File-backed vault ops (write/edit/list/delete) ──
373
458
 
374
459
  pi.registerTool({
@@ -396,7 +481,7 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
396
481
  name: "ai_memory_vault_write",
397
482
  label: "AI Memory Vault Write",
398
483
  description: "Create/overwrite a Markdown file in the configured Obsidian vault.",
399
- promptSnippet: "Write vault file [path + body/frontmatter]",
484
+ promptSnippet: "Write vault file [show path and written body/frontmatter]",
400
485
  parameters: Type.Object({
401
486
  path: Type.String({ description: "Vault-relative path, e.g. 02 Projekte/Test.md" }),
402
487
  frontmatter: Type.Optional(Type.String({ description: "Optional YAML frontmatter including leading/trailing --- lines" })),
@@ -408,18 +493,21 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
408
493
  frontmatter: params.frontmatter !== undefined ? String(params.frontmatter) : null,
409
494
  body: String(params.body ?? ""),
410
495
  })
411
- return { content: [{ type: "text", text: `Wrote: ${data.path ?? params.path}` }], details: data }
496
+ return { content: [{ type: "text", text: formatVaultWriteResult("Wrote", params, data.path) }], details: data }
412
497
  },
413
498
  renderCall(args: any, theme: ThemeLike, context: any) {
414
499
  return renderSingleLineToolCall(formatAiMemoryToolCall("ai_memory_vault_write", args, theme), context)
415
500
  },
501
+ renderResult(result: any, options: any, theme: ThemeLike, context: any) {
502
+ return renderAiMemoryWriteResult("ai_memory_vault_write", result, options, theme, context)
503
+ },
416
504
  })
417
505
 
418
506
  pi.registerTool({
419
507
  name: "ai_memory_vault_edit",
420
508
  label: "AI Memory Vault Edit",
421
509
  description: "Replace the content block under a given heading in a Markdown file (or append if missing).",
422
- promptSnippet: "Edit vault file section by heading",
510
+ promptSnippet: "Edit vault file section [show path, heading, and written body/frontmatter]",
423
511
  parameters: Type.Object({
424
512
  path: Type.String({ description: "Vault-relative path" }),
425
513
  heading: Type.String({ description: "Heading text without leading #, e.g. 'Update 2026-01-01 — Thema'" }),
@@ -433,11 +521,14 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
433
521
  frontmatter: params.frontmatter !== undefined ? String(params.frontmatter) : null,
434
522
  body: String(params.body ?? ""),
435
523
  })
436
- return { content: [{ type: "text", text: `Edited: ${data.path ?? params.path}` }], details: data }
524
+ return { content: [{ type: "text", text: formatVaultWriteResult("Edited", params, data.path) }], details: data }
437
525
  },
438
526
  renderCall(args: any, theme: ThemeLike, context: any) {
439
527
  return renderSingleLineToolCall(formatAiMemoryToolCall("ai_memory_vault_edit", args, theme), context)
440
528
  },
529
+ renderResult(result: any, options: any, theme: ThemeLike, context: any) {
530
+ return renderAiMemoryWriteResult("ai_memory_vault_edit", result, options, theme, context)
531
+ },
441
532
  })
442
533
 
443
534
  pi.registerTool({
@@ -482,36 +573,34 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
482
573
  .catch(() => ctx.ui.setStatus?.("ai-memory", undefined))
483
574
  })
484
575
 
485
- pi.on?.("before_agent_start", async (event: any) => {
576
+ pi.on?.("before_agent_start", async (event: any, ctx: any) => {
577
+ const patch: { systemPrompt?: string; message?: any } = {}
578
+
486
579
  // Always-loaded durable context (mapped from 00 Kontext) is injected as a
487
580
  // system-prompt chunk so it remains in the cached prefix.
488
- let alwaysBlock: string | undefined
489
581
  try {
490
- const alwaysContent = await loadAlwaysContext()
491
- const alwaysMessage = buildAlwaysContextMessage(alwaysContent)
492
- alwaysBlock = alwaysMessage?.content
493
- } catch {
494
- // Ignore — always-context is best-effort.
495
- }
496
-
497
- const prev = String(event?.systemPrompt ?? "")
498
- if (alwaysBlock && alwaysBlock.trim()) {
499
- const block = alwaysBlock.trim()
500
- return { systemPrompt: `${prev}${prev ? "\n\n" : ""}${block}` }
582
+ const alwaysBlock = buildAlwaysContextMessage(await loadAlwaysContext())?.content?.trim()
583
+ if (alwaysBlock) {
584
+ const prev = String(event?.systemPrompt ?? "")
585
+ patch.systemPrompt = `${prev}${prev ? "\n\n" : ""}${alwaysBlock}`
586
+ }
587
+ } catch (error) {
588
+ // Best-effort, but never silent: a broken API would otherwise drop durable
589
+ // context without anyone noticing.
590
+ reportBackgroundFailure(ctx, "always-context injection", error)
501
591
  }
502
592
 
503
- // Query-specific candidates
504
- if (!shouldInjectAiMemoryCandidates()) return
593
+ // Query-specific candidates (independent of the always-context block).
505
594
  const query = String(event?.prompt ?? "").trim()
506
- if (!query) return
507
- try {
508
- const results = await loadAiMemoryInjectResults(query)
509
- const message = buildAiMemoryContextMessage(results)
510
- return message ? { message } : undefined
511
- } catch {
512
- // Candidate injection is opportunistic. The explicit ai_memory_search tool
513
- // remains available and will surface API errors when the model calls it.
514
- return undefined
595
+ if (shouldInjectAiMemoryCandidates() && query) {
596
+ try {
597
+ const message = buildAiMemoryContextMessage(await loadAiMemoryInjectResults(query))
598
+ if (message) patch.message = message
599
+ } catch (error) {
600
+ reportBackgroundFailure(ctx, "candidate injection", error)
601
+ }
515
602
  }
603
+
604
+ return patch.systemPrompt === undefined && patch.message === undefined ? undefined : patch
516
605
  })
517
606
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Live preview and rendering of memory writes.
3
+ *
4
+ * Vault writes used to be invisible until the tool finished: the call row only
5
+ * showed the path and the result slot said "Searching AI Memory...". This module
6
+ * formats the *partially streamed* arguments so the note content is readable
7
+ * while the model is still producing it, and keeps the row expandable with
8
+ * Ctrl+O / Strg+O (`app.tools.expand`) in both states.
9
+ */
10
+
11
+ import { renderCollapsibleText, renderCollapsibleTextResult } from "../ui/collapsed-text-rendering.js"
12
+ import { truncateToWidth, type ThemeLike } from "../ui/tool-call-rendering.js"
13
+
14
+ /** Collapsed height while arguments stream in; the newest lines stay visible. */
15
+ export const WRITE_STREAM_COLLAPSED_LINES = 16
16
+ /** Collapsed height of a finished write result. */
17
+ export const WRITE_RESULT_COLLAPSED_LINES = 24
18
+
19
+ const WRITE_PREVIEW_TOOLS = new Set(["ai_memory_vault_write", "ai_memory_vault_edit"])
20
+
21
+ export function isAiMemoryWriteTool(toolName: string): boolean {
22
+ return WRITE_PREVIEW_TOOLS.has(toolName)
23
+ }
24
+
25
+ function text(value: unknown): string {
26
+ return value === undefined || value === null ? "" : String(value)
27
+ }
28
+
29
+ export function formatAiMemoryWriteTarget(toolName: string, args: any): string {
30
+ const path = text(args?.path).trim() || "(path pending)"
31
+ const heading = text(args?.heading).trim()
32
+ return toolName === "ai_memory_vault_edit" && heading ? `${path} > # ${heading}` : path
33
+ }
34
+
35
+ /**
36
+ * @param args partially streamed tool arguments (may miss fields entirely)
37
+ * @param streaming true while arguments are still arriving
38
+ */
39
+ export function buildAiMemoryWritePreview(toolName: string, args: any, streaming = true): { header: string; content: string } {
40
+ const label = toolName === "ai_memory_vault_edit" ? "Editing" : "Writing"
41
+ const frontmatter = text(args?.frontmatter).trim()
42
+ const heading = text(args?.heading).trim()
43
+ const headingLine = toolName === "ai_memory_vault_edit" && heading ? `# ${heading}` : ""
44
+ const body = text(args?.body)
45
+ const written = [frontmatter, headingLine, body.trimEnd()].filter(Boolean).join("\n\n")
46
+
47
+ const header = `${label}: ${formatAiMemoryWriteTarget(toolName, args)}${streaming ? " … streaming" : ""}`
48
+ const content = written || (streaming ? "(waiting for content…)" : "(empty)")
49
+ return { header, content }
50
+ }
51
+
52
+ export function formatAiMemoryWritePreview(toolName: string, args: any, streaming = true): string {
53
+ const { header, content } = buildAiMemoryWritePreview(toolName, args, streaming)
54
+ return `${header}\n\n${content}`
55
+ }
56
+
57
+ /**
58
+ * Result slot for vault writes: streams the note content while the model writes,
59
+ * shows the confirmed content afterwards. Both states honour `expanded`, so
60
+ * Ctrl+O / Strg+O toggles between the collapsed excerpt and the full text.
61
+ */
62
+ export function renderAiMemoryWriteResult(
63
+ toolName: string,
64
+ result: any,
65
+ options: { expanded?: boolean; isPartial?: boolean } = {},
66
+ theme: ThemeLike,
67
+ context?: any,
68
+ ): { render(width: number): string[] } {
69
+ if (!options.isPartial) {
70
+ return renderCollapsibleTextResult(result, options, theme, {
71
+ partialLabel: "Writing AI Memory...",
72
+ emptyLabel: "No AI Memory write result",
73
+ maxLines: WRITE_RESULT_COLLAPSED_LINES,
74
+ })
75
+ }
76
+
77
+ const { header, content } = buildAiMemoryWritePreview(toolName, context?.args ?? {}, context?.argsComplete !== true)
78
+ // Keep the target pinned above the streamed tail so it never scrolls away.
79
+ const body = renderCollapsibleText(content, options, theme, { maxLines: WRITE_STREAM_COLLAPSED_LINES, keep: "tail" })
80
+ return {
81
+ render: (width: number) => [theme.fg("accent", truncateToWidth(header, Math.max(10, width - 2))), ...body.render(width)],
82
+ }
83
+ }
@@ -332,7 +332,7 @@ export async function webSearch(query: string, limit = 10): Promise<string> {
332
332
  export async function webFetch(url: string, maxChars = 20_000): Promise<string> {
333
333
  const safeUrl = validateHttpUrl(url)
334
334
  const safeMaxChars = clampLimit(maxChars, 20_000, 1_000, 100_000)
335
- if (isYouTubeUrl(safeUrl)) return fetchYouTubeContent(safeUrl, safeMaxChars)
335
+ if (isYouTubeUrl(safeUrl)) return fetchYouTubeContent(safeUrl)
336
336
  const response = await fetch(safeUrl, { headers: { "User-Agent": "Mozilla/5.0 (compatible; prism-research/1.0)" }, signal: AbortSignal.timeout(15_000) })
337
337
  const text = await response.text()
338
338
  if (!response.ok) throw new Error(`Fetch failed for ${safeUrl}: HTTP ${response.status}`)
@@ -392,8 +392,8 @@ export default function researchToolsExtension(pi: ExtensionAPI): void {
392
392
  pi.registerTool({
393
393
  name: "web_fetch",
394
394
  label: "web fetch",
395
- description: "Fetch readable text from a web page for citation-backed research. For YouTube URLs, extracts structured video metadata (title, channel, description, duration, views, publish date) and transcript when available.",
396
- promptSnippet: "Fetch readable text from a web page. For YouTube URLs, extracts structured video metadata (title, channel, description, duration, views, publish date) and transcript when available.",
395
+ description: "Fetch readable text from a web page for citation-backed research. For YouTube URLs, extracts structured video metadata and the complete transcript without applying maxChars truncation.",
396
+ promptSnippet: "Fetch readable text from a web page. For YouTube URLs, extracts structured video metadata and the complete transcript without truncation.",
397
397
  parameters: Type.Object({ url: Type.String(), maxChars: Type.Optional(Type.Number()) }),
398
398
  async execute(_id: string, params: any) {
399
399
  return { content: [{ type: "text", text: await webFetch(params.url, params.maxChars || 20_000) }], details: { url: params.url } }
@@ -215,7 +215,7 @@ export function formatDuration(seconds: number): string {
215
215
  // Format the final YouTube result
216
216
  // ---------------------------------------------------------------------------
217
217
 
218
- export function formatYouTubeResult(meta: YouTubeMetadata, transcript: string, maxChars: number): string {
218
+ export function formatYouTubeResult(meta: YouTubeMetadata, transcript: string, _maxChars?: number): string {
219
219
  const lines: string[] = []
220
220
 
221
221
  lines.push(`# ${meta.title}`)
@@ -240,16 +240,16 @@ export function formatYouTubeResult(meta: YouTubeMetadata, transcript: string, m
240
240
  lines.push(transcript)
241
241
  }
242
242
 
243
- let result = lines.join("\n")
244
- if (result.length > maxChars) result = `${result.slice(0, maxChars - 20)}\n\n[truncated]`
245
- return result
243
+ // YouTube transcripts must stay complete. The generic web_fetch maxChars
244
+ // limit is intentionally ignored here so long videos are not cut off.
245
+ return lines.join("\n")
246
246
  }
247
247
 
248
248
  // ---------------------------------------------------------------------------
249
249
  // Main entry point for web_fetch YouTube handling
250
250
  // ---------------------------------------------------------------------------
251
251
 
252
- export async function fetchYouTubeContent(url: string, maxChars = 20_000): Promise<string> {
252
+ export async function fetchYouTubeContent(url: string, _maxChars?: number): Promise<string> {
253
253
  const videoId = extractVideoId(url)
254
254
  if (!videoId) throw new Error(`Could not extract video ID from URL: ${url}`)
255
255
 
@@ -288,5 +288,5 @@ export async function fetchYouTubeContent(url: string, maxChars = 20_000): Promi
288
288
  transcript = await fetchTranscriptYtDlp(videoId, "en")
289
289
  }
290
290
 
291
- return formatYouTubeResult(meta, transcript, maxChars)
291
+ return formatYouTubeResult(meta, transcript)
292
292
  }
@@ -9,6 +9,8 @@ export type CollapsibleTextRenderConfig = {
9
9
  partialLabel: string
10
10
  emptyLabel: string
11
11
  maxLines: number
12
+ /** "head" keeps the first lines (default), "tail" keeps the newest lines. */
13
+ keep?: "head" | "tail"
12
14
  lines?: (text: string) => string[]
13
15
  strip?: (text: string) => string
14
16
  }
@@ -31,26 +33,30 @@ export function renderCollapsibleText(
31
33
  text: string,
32
34
  options: CollapsibleTextRenderOptions = {},
33
35
  theme: ThemeLike,
34
- config: Pick<CollapsibleTextRenderConfig, "maxLines" | "lines" | "strip">,
36
+ config: Pick<CollapsibleTextRenderConfig, "maxLines" | "keep" | "lines" | "strip">,
35
37
  ): { render(width: number): string[] } {
36
38
  const body = config.strip ? config.strip(text) : text
37
39
  const lines = config.lines ? config.lines(body) : body.split(/\r?\n/)
38
- const shown = options.expanded ? lines : lines.slice(0, config.maxLines)
40
+ const keepTail = config.keep === "tail"
41
+ const shown = options.expanded ? lines : keepTail ? lines.slice(-config.maxLines) : lines.slice(0, config.maxLines)
42
+ const hidden = lines.length - shown.length
39
43
 
40
44
  return {
41
45
  render: (width: number) => {
42
46
  const maxWidth = Math.max(10, width - 2)
47
+ const hint = hidden > 0 && !options.expanded
48
+ ? theme.fg("muted", truncateToWidth(collapseHint(hidden, keepTail ? "above" : "below"), maxWidth))
49
+ : undefined
43
50
  const rendered = shown.map((line) => theme.fg("dim", truncateToWidth(line, maxWidth)))
44
- if (!options.expanded && lines.length > shown.length) {
45
- rendered.push(theme.fg("muted", truncateToWidth(collapseHint(lines.length - shown.length), maxWidth)))
46
- }
47
- return rendered
51
+ if (!hint) return rendered
52
+ return keepTail ? [hint, ...rendered] : [...rendered, hint]
48
53
  },
49
54
  }
50
55
  }
51
56
 
52
- export function collapseHint(hiddenLineCount: number): string {
53
- return `... ${hiddenLineCount} more lines (Ctrl+O / Strg+O to expand)`
57
+ export function collapseHint(hiddenLineCount: number, position: "below" | "above" = "below"): string {
58
+ const what = position === "above" ? "earlier lines" : "more lines"
59
+ return `... ${hiddenLineCount} ${what} (Ctrl+O / Strg+O to expand)`
54
60
  }
55
61
 
56
62
  export function stripLeadingPathHeader(text: string, relPath: string): string {