@daniel156161/prism 0.2.81 → 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.
- package/dist/prism-extensions/integrations/ai-memory-errors.d.ts +13 -0
- package/dist/prism-extensions/integrations/ai-memory-errors.js +54 -0
- package/dist/prism-extensions/integrations/ai-memory-errors.js.map +1 -1
- package/dist/prism-extensions/integrations/ai-memory-http.d.ts +28 -0
- package/dist/prism-extensions/integrations/ai-memory-http.js +92 -0
- package/dist/prism-extensions/integrations/ai-memory-http.js.map +1 -0
- package/dist/prism-extensions/integrations/ai-memory-system.d.ts +2 -0
- package/dist/prism-extensions/integrations/ai-memory-system.js +49 -127
- package/dist/prism-extensions/integrations/ai-memory-system.js.map +1 -1
- package/dist/prism-extensions/integrations/ai-memory-write-preview.d.ts +36 -0
- package/dist/prism-extensions/integrations/ai-memory-write-preview.js +67 -0
- package/dist/prism-extensions/integrations/ai-memory-write-preview.js.map +1 -0
- package/dist/prism-extensions/ui/collapsed-text-rendering.d.ts +4 -2
- package/dist/prism-extensions/ui/collapsed-text-rendering.js +12 -7
- package/dist/prism-extensions/ui/collapsed-text-rendering.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +11 -6
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +0 -5
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-storage.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js +1 -3
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +37 -39
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js +16 -20
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/session-selector.js +0 -4
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +2376 -234
- package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js +1 -1
- package/package.json +3 -3
- package/src/prism-extensions/integrations/ai-memory-errors.ts +59 -0
- package/src/prism-extensions/integrations/ai-memory-http.ts +114 -0
- package/src/prism-extensions/integrations/ai-memory-system.ts +60 -135
- package/src/prism-extensions/integrations/ai-memory-write-preview.ts +83 -0
- package/src/prism-extensions/ui/collapsed-text-rendering.ts +14 -8
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/prism-session-db.js +0 -128
|
@@ -202,7 +202,7 @@ export class CombinedAutocompleteProvider {
|
|
|
202
202
|
prefix: atPrefix,
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
|
-
if (textBeforeCursor.startsWith("/")) {
|
|
205
|
+
if (!options.force && textBeforeCursor.startsWith("/")) {
|
|
206
206
|
const spaceIndex = textBeforeCursor.indexOf(" ");
|
|
207
207
|
if (spaceIndex === -1) {
|
|
208
208
|
const prefix = textBeforeCursor.slice(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daniel156161/prism",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.82",
|
|
4
4
|
"description": "Prism-branded wrapper around pi that stores config in ~/.prism",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@earendil-works/pi-ai": "^0.84.1",
|
|
29
29
|
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
30
|
-
"pi-mcp-adapter": "^2.21.
|
|
30
|
+
"pi-mcp-adapter": "^2.21.2",
|
|
31
31
|
"pkce-challenge": "^6.0.0",
|
|
32
32
|
"typebox": "^1.3.11"
|
|
33
33
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^26.2.0",
|
|
39
|
-
"tsx": "^4.23.
|
|
39
|
+
"tsx": "^4.23.12",
|
|
40
40
|
"typescript": "^7.0.2"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
@@ -14,6 +14,11 @@ export type AiMemoryRequestContext = {
|
|
|
14
14
|
filters?: Record<string, unknown>
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export type AiMemoryTransportContext = AiMemoryRequestContext & {
|
|
18
|
+
baseUrl: string
|
|
19
|
+
timeoutMs: number
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
function parseBody(text: string): any {
|
|
18
23
|
try {
|
|
19
24
|
return JSON.parse(text)
|
|
@@ -44,6 +49,60 @@ export function formatApiFailure(status: number, text: string, context: AiMemory
|
|
|
44
49
|
return parts.join(" | ")
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
function errorCode(error: unknown): string | undefined {
|
|
53
|
+
const candidates = [error as any, (error as any)?.cause]
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
const code = candidate?.code ?? candidate?.errno
|
|
56
|
+
if (typeof code === "string" && code.trim()) return code.trim()
|
|
57
|
+
}
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function errorMessage(error: unknown): string {
|
|
62
|
+
if (error instanceof Error) {
|
|
63
|
+
const cause = (error as any).cause
|
|
64
|
+
const causeMessage = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : ""
|
|
65
|
+
return causeMessage && causeMessage !== error.message ? `${error.message}: ${causeMessage}` : error.message
|
|
66
|
+
}
|
|
67
|
+
return String(error)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A request that never reached the API (timeout, refused connection, DNS) must
|
|
72
|
+
* still explain itself. `fetch` alone only reports "fetch failed" or "The
|
|
73
|
+
* operation was aborted due to timeout", which is useless while debugging why a
|
|
74
|
+
* memory write did not land.
|
|
75
|
+
*/
|
|
76
|
+
export function formatTransportFailure(error: unknown, context: AiMemoryTransportContext): string {
|
|
77
|
+
const name = (error as any)?.name
|
|
78
|
+
const code = errorCode(error)
|
|
79
|
+
const timedOut = name === "TimeoutError" || name === "AbortError"
|
|
80
|
+
const reason = timedOut
|
|
81
|
+
? `request timed out after ${context.timeoutMs}ms`
|
|
82
|
+
: `request failed before a response arrived: ${errorMessage(error)}`
|
|
83
|
+
|
|
84
|
+
const parts = [`AI Memory ${context.method} ${context.path} failed: ${reason}`]
|
|
85
|
+
if (code) parts.push(`code=${code}`)
|
|
86
|
+
parts.push(`base_url=${context.baseUrl}`)
|
|
87
|
+
if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
88
|
+
parts.push(`hint=Is the AI Memory System API running and reachable at ${context.baseUrl}?`)
|
|
89
|
+
} else if (timedOut) {
|
|
90
|
+
parts.push("hint=Raise PRISM_AI_MEMORY_TIMEOUT_MS or check the API load; nothing was confirmed as written.")
|
|
91
|
+
}
|
|
92
|
+
if (context.query) parts.push(`query=${JSON.stringify(context.query)}`)
|
|
93
|
+
if (context.filters && Object.keys(context.filters).length > 0) parts.push(`filters=${JSON.stringify(context.filters)}`)
|
|
94
|
+
return parts.join(" | ")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A 2xx response with a body that is not JSON, e.g. an HTML proxy page. */
|
|
98
|
+
export function formatInvalidJsonFailure(error: unknown, text: string, context: AiMemoryTransportContext): string {
|
|
99
|
+
const snippet = String(text ?? "").replace(/\s+/g, " ").trim().slice(0, 300)
|
|
100
|
+
const parts = [`AI Memory ${context.method} ${context.path} failed: response was not valid JSON: ${errorMessage(error)}`]
|
|
101
|
+
parts.push(`base_url=${context.baseUrl}`)
|
|
102
|
+
parts.push(`body=${snippet || "(empty)"}`)
|
|
103
|
+
return parts.join(" | ")
|
|
104
|
+
}
|
|
105
|
+
|
|
47
106
|
export function formatDegradedNotice(degraded: any): string {
|
|
48
107
|
const stages: string[] = Array.isArray(degraded?.stages) ? degraded.stages : []
|
|
49
108
|
if (stages.length === 0) return ""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport for the AI Memory System integration.
|
|
3
|
+
*
|
|
4
|
+
* Every request funnels through `requestJson` so that transport failures
|
|
5
|
+
* (timeout, refused connection, unreadable or non-JSON body) end up as
|
|
6
|
+
* descriptive errors instead of a bare "fetch failed" / "The operation was
|
|
7
|
+
* aborted". The tool call is still reported as an error to the model, but the
|
|
8
|
+
* message now explains *why* it failed and which route/base URL was involved.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { formatApiFailure, formatInvalidJsonFailure, formatTransportFailure, type AiMemoryRequestContext } from "./ai-memory-errors.js"
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BASE_URL = "http://127.0.0.1:8765"
|
|
14
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
|
|
15
|
+
const DEFAULT_STATUS_TIMEOUT_MS = 1_500
|
|
16
|
+
const DEFAULT_INJECT_TIMEOUT_MS = 1_200
|
|
17
|
+
const DEFAULT_INJECT_CACHE_TTL_MS = 60_000
|
|
18
|
+
|
|
19
|
+
export function aiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
20
|
+
return (env.PRISM_AI_MEMORY_BASE_URL || env.AI_MEMORY_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function positiveIntegerSetting(value: unknown, fallback: number): number {
|
|
24
|
+
const parsed = Number(value)
|
|
25
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
26
|
+
return Math.max(1, Math.trunc(parsed))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function aiMemoryRequestTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
30
|
+
return positiveIntegerSetting(env.PRISM_AI_MEMORY_TIMEOUT_MS ?? env.PI_AI_MEMORY_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function aiMemoryStatusTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
34
|
+
return positiveIntegerSetting(env.PRISM_AI_MEMORY_STATUS_TIMEOUT_MS ?? env.PI_AI_MEMORY_STATUS_TIMEOUT_MS, DEFAULT_STATUS_TIMEOUT_MS)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function aiMemoryInjectTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
38
|
+
return positiveIntegerSetting(env.PRISM_AI_MEMORY_INJECT_TIMEOUT_MS ?? env.PI_AI_MEMORY_INJECT_TIMEOUT_MS, DEFAULT_INJECT_TIMEOUT_MS)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function aiMemoryInjectCacheTtlMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
42
|
+
return positiveIntegerSetting(env.PRISM_AI_MEMORY_INJECT_CACHE_TTL_MS ?? env.PI_AI_MEMORY_INJECT_CACHE_TTL_MS, DEFAULT_INJECT_CACHE_TTL_MS)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type AiMemoryRequest = {
|
|
46
|
+
method: string
|
|
47
|
+
path: string
|
|
48
|
+
body?: unknown
|
|
49
|
+
timeoutMs?: number
|
|
50
|
+
query?: string
|
|
51
|
+
filters?: Record<string, unknown>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function requestContext(request: AiMemoryRequest): AiMemoryRequestContext {
|
|
55
|
+
const payload = request.body as any
|
|
56
|
+
return {
|
|
57
|
+
method: request.method,
|
|
58
|
+
path: request.path,
|
|
59
|
+
query: request.query ?? (typeof payload?.query === "string" ? payload.query : undefined),
|
|
60
|
+
filters: request.filters ?? (payload?.filters as Record<string, unknown> | undefined),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function requestJson<T>(request: AiMemoryRequest): Promise<T> {
|
|
65
|
+
const baseUrl = aiMemoryBaseUrl()
|
|
66
|
+
const timeoutMs = request.timeoutMs ?? aiMemoryRequestTimeoutMs()
|
|
67
|
+
const context = requestContext(request)
|
|
68
|
+
const transport = { ...context, baseUrl, timeoutMs }
|
|
69
|
+
|
|
70
|
+
let response: Response
|
|
71
|
+
let text: string
|
|
72
|
+
try {
|
|
73
|
+
response = await fetch(`${baseUrl}${request.path}`, {
|
|
74
|
+
method: request.method,
|
|
75
|
+
headers: {
|
|
76
|
+
accept: "application/json",
|
|
77
|
+
...(request.body === undefined ? {} : { "content-type": "application/json; charset=utf-8" }),
|
|
78
|
+
},
|
|
79
|
+
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
80
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
81
|
+
})
|
|
82
|
+
text = await response.text()
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(formatTransportFailure(error, transport))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!response.ok) throw new Error(formatApiFailure(response.status, text, context))
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(text) as T
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(formatInvalidJsonFailure(error, text, transport))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function postJson<T>(path: string, body: unknown, timeoutMs?: number): Promise<T> {
|
|
97
|
+
return requestJson<T>({ method: "POST", path, body, timeoutMs })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function putJson<T>(path: string, body: unknown, timeoutMs?: number): Promise<T> {
|
|
101
|
+
return requestJson<T>({ method: "PUT", path, body, timeoutMs })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function deleteJson<T>(path: string, body: unknown, timeoutMs?: number): Promise<T> {
|
|
105
|
+
return requestJson<T>({ method: "DELETE", path, body, timeoutMs })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function methodJson<T>(method: string, path: string, timeoutMs?: number, body?: unknown): Promise<T> {
|
|
109
|
+
return requestJson<T>({ method, path, body, timeoutMs })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getJson<T>(path: string, timeoutMs: number = aiMemoryStatusTimeoutMs()): Promise<T> {
|
|
113
|
+
return requestJson<T>({ method: "GET", path, timeoutMs })
|
|
114
|
+
}
|
|
@@ -2,26 +2,39 @@ type ExtensionAPI = any
|
|
|
2
2
|
|
|
3
3
|
import { Type } from "typebox"
|
|
4
4
|
import { readSettings } from "../core/shared-config.js"
|
|
5
|
-
import {
|
|
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"
|
|
6
19
|
import { renderCollapsibleTextResult } from "../ui/collapsed-text-rendering.js"
|
|
7
20
|
import { obsidianOpenUrl } from "./obsidian-memory.js"
|
|
8
21
|
import { ansiHyperlink, formatBracketedToolCall, renderSingleLineToolCall, type ThemeLike } from "../ui/tool-call-rendering.js"
|
|
9
22
|
|
|
10
|
-
const DEFAULT_BASE_URL = "http://127.0.0.1:8765"
|
|
11
23
|
const DEFAULT_LIMIT = 8
|
|
12
24
|
const DEFAULT_CONTEXT_LIMIT = 5
|
|
13
25
|
const DEFAULT_CONTEXT_SCORE_THRESHOLD = 0.15
|
|
14
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
|
|
15
|
-
const DEFAULT_STATUS_TIMEOUT_MS = 1_500
|
|
16
|
-
const DEFAULT_INJECT_TIMEOUT_MS = 1_200
|
|
17
|
-
const DEFAULT_INJECT_CACHE_TTL_MS = 60_000
|
|
18
26
|
const DEFAULT_ALWAYS_CONTEXT_CACHE_TTL_MS = 60_000
|
|
19
27
|
|
|
20
28
|
const injectCache = new Map<string, { expiresAt: number; results: any[] }>()
|
|
21
29
|
const alwaysContextCache = { expiresAt: 0, content: "" }
|
|
30
|
+
const reportedBackgroundFailures = new Set<string>()
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
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()
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
function clampLimit(value: unknown, fallback = DEFAULT_LIMIT): number {
|
|
@@ -70,28 +83,6 @@ function aiMemoryContextScoreThreshold(env: NodeJS.ProcessEnv = process.env): nu
|
|
|
70
83
|
return Number.isFinite(parsed) ? Math.max(0, parsed) : DEFAULT_CONTEXT_SCORE_THRESHOLD
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
function positiveIntegerSetting(value: unknown, fallback: number): number {
|
|
74
|
-
const parsed = Number(value)
|
|
75
|
-
if (!Number.isFinite(parsed)) return fallback
|
|
76
|
-
return Math.max(1, Math.trunc(parsed))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function aiMemoryRequestTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
80
|
-
return positiveIntegerSetting(env.PRISM_AI_MEMORY_TIMEOUT_MS ?? env.PI_AI_MEMORY_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function aiMemoryStatusTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
84
|
-
return positiveIntegerSetting(env.PRISM_AI_MEMORY_STATUS_TIMEOUT_MS ?? env.PI_AI_MEMORY_STATUS_TIMEOUT_MS, DEFAULT_STATUS_TIMEOUT_MS)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function aiMemoryInjectTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
88
|
-
return positiveIntegerSetting(env.PRISM_AI_MEMORY_INJECT_TIMEOUT_MS ?? env.PI_AI_MEMORY_INJECT_TIMEOUT_MS, DEFAULT_INJECT_TIMEOUT_MS)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function aiMemoryInjectCacheTtlMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
92
|
-
return positiveIntegerSetting(env.PRISM_AI_MEMORY_INJECT_CACHE_TTL_MS ?? env.PI_AI_MEMORY_INJECT_CACHE_TTL_MS, DEFAULT_INJECT_CACHE_TTL_MS)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
86
|
function cleanFilters(params: any): Record<string, string> {
|
|
96
87
|
const filters: Record<string, string> = {}
|
|
97
88
|
for (const key of ["source", "project", "type", "status", "tag", "path"]) {
|
|
@@ -101,82 +92,6 @@ function cleanFilters(params: any): Record<string, string> {
|
|
|
101
92
|
return filters
|
|
102
93
|
}
|
|
103
94
|
|
|
104
|
-
async function postJson<T>(path: string, body: unknown, timeoutMs = aiMemoryRequestTimeoutMs()): Promise<T> {
|
|
105
|
-
const baseUrl = aiMemoryBaseUrl()
|
|
106
|
-
const response = await fetch(`${baseUrl}${path}`, {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: { "content-type": "application/json; charset=utf-8", accept: "application/json" },
|
|
109
|
-
body: JSON.stringify(body),
|
|
110
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
111
|
-
})
|
|
112
|
-
const text = await response.text()
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
const payload = body as any
|
|
115
|
-
throw new Error(formatApiFailure(response.status, text, {
|
|
116
|
-
method: "POST",
|
|
117
|
-
path,
|
|
118
|
-
query: typeof payload?.query === "string" ? payload.query : undefined,
|
|
119
|
-
filters: payload?.filters,
|
|
120
|
-
}))
|
|
121
|
-
}
|
|
122
|
-
return JSON.parse(text) as T
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function putJson<T>(path: string, body: unknown, timeoutMs = aiMemoryRequestTimeoutMs()): Promise<T> {
|
|
126
|
-
const baseUrl = aiMemoryBaseUrl()
|
|
127
|
-
const response = await fetch(`${baseUrl}${path}`, {
|
|
128
|
-
method: "PUT",
|
|
129
|
-
headers: { "content-type": "application/json; charset=utf-8", accept: "application/json" },
|
|
130
|
-
body: JSON.stringify(body),
|
|
131
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
132
|
-
})
|
|
133
|
-
const text = await response.text()
|
|
134
|
-
if (!response.ok) throw new Error(formatApiFailure(response.status, text, { method: "PUT", path }))
|
|
135
|
-
return JSON.parse(text) as T
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function deleteJson<T>(path: string, body: unknown, timeoutMs = aiMemoryRequestTimeoutMs()): Promise<T> {
|
|
139
|
-
const baseUrl = aiMemoryBaseUrl()
|
|
140
|
-
const response = await fetch(`${baseUrl}${path}`, {
|
|
141
|
-
method: "DELETE",
|
|
142
|
-
headers: { "content-type": "application/json; charset=utf-8", accept: "application/json" },
|
|
143
|
-
body: JSON.stringify(body),
|
|
144
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
145
|
-
})
|
|
146
|
-
const text = await response.text()
|
|
147
|
-
if (!response.ok) throw new Error(formatApiFailure(response.status, text, { method: "DELETE", path }))
|
|
148
|
-
return JSON.parse(text) as T
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function methodJson<T>(
|
|
152
|
-
method: string,
|
|
153
|
-
path: string,
|
|
154
|
-
timeoutMs = aiMemoryRequestTimeoutMs(),
|
|
155
|
-
body?: unknown,
|
|
156
|
-
): Promise<T> {
|
|
157
|
-
const baseUrl = aiMemoryBaseUrl()
|
|
158
|
-
const response = await fetch(`${baseUrl}${path}`, {
|
|
159
|
-
method,
|
|
160
|
-
headers: { accept: "application/json", ...(body === undefined ? {} : { "content-type": "application/json; charset=utf-8" }) },
|
|
161
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
162
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
163
|
-
})
|
|
164
|
-
const text = await response.text()
|
|
165
|
-
if (!response.ok) throw new Error(formatApiFailure(response.status, text, { method, path }))
|
|
166
|
-
return JSON.parse(text) as T
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function getJson<T>(path: string, timeoutMs = aiMemoryStatusTimeoutMs()): Promise<T> {
|
|
170
|
-
const baseUrl = aiMemoryBaseUrl()
|
|
171
|
-
const response = await fetch(`${baseUrl}${path}`, {
|
|
172
|
-
headers: { accept: "application/json" },
|
|
173
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
174
|
-
})
|
|
175
|
-
const text = await response.text()
|
|
176
|
-
if (!response.ok) throw new Error(formatApiFailure(response.status, text, { method: "GET", path }))
|
|
177
|
-
return JSON.parse(text) as T
|
|
178
|
-
}
|
|
179
|
-
|
|
180
95
|
function formatSearchResults(results: any[]): string {
|
|
181
96
|
if (!Array.isArray(results) || results.length === 0) return "No AI Memory results found. Is the index built?"
|
|
182
97
|
return results.map((row, index) => {
|
|
@@ -299,13 +214,25 @@ export function formatAiMemoryToolCall(toolName: string, args: any, theme: Theme
|
|
|
299
214
|
const label = toolName.replace(/^ai_memory_/, "ai memory ").replace(/_/g, " ")
|
|
300
215
|
const rawValue = args?.query || args?.id || args?.path || args?.session || ""
|
|
301
216
|
const value = typeof rawValue === "string" && rawValue.trim() ? rawValue.trim() : "..."
|
|
302
|
-
const linksToVaultNote =
|
|
217
|
+
const linksToVaultNote = isAiMemoryWriteTool(toolName) || toolName === "ai_memory_vault_delete"
|
|
303
218
|
if (linksToVaultNote && value !== "...") {
|
|
304
219
|
return `${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("accent", ansiHyperlink(obsidianOpenUrl(value), value))}`
|
|
305
220
|
}
|
|
306
221
|
return formatBracketedToolCall(label, value, theme)
|
|
307
222
|
}
|
|
308
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
|
+
|
|
309
236
|
function formatVaultWriteResult(action: "Wrote" | "Edited", params: any, responsePath: unknown): string {
|
|
310
237
|
const path = String(responsePath ?? params.path ?? "")
|
|
311
238
|
const frontmatter = params.frontmatter === undefined ? "" : String(params.frontmatter).trim()
|
|
@@ -571,8 +498,8 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
|
|
|
571
498
|
renderCall(args: any, theme: ThemeLike, context: any) {
|
|
572
499
|
return renderSingleLineToolCall(formatAiMemoryToolCall("ai_memory_vault_write", args, theme), context)
|
|
573
500
|
},
|
|
574
|
-
renderResult(result: any, options: any, theme: ThemeLike) {
|
|
575
|
-
return
|
|
501
|
+
renderResult(result: any, options: any, theme: ThemeLike, context: any) {
|
|
502
|
+
return renderAiMemoryWriteResult("ai_memory_vault_write", result, options, theme, context)
|
|
576
503
|
},
|
|
577
504
|
})
|
|
578
505
|
|
|
@@ -599,8 +526,8 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
|
|
|
599
526
|
renderCall(args: any, theme: ThemeLike, context: any) {
|
|
600
527
|
return renderSingleLineToolCall(formatAiMemoryToolCall("ai_memory_vault_edit", args, theme), context)
|
|
601
528
|
},
|
|
602
|
-
renderResult(result: any, options: any, theme: ThemeLike) {
|
|
603
|
-
return
|
|
529
|
+
renderResult(result: any, options: any, theme: ThemeLike, context: any) {
|
|
530
|
+
return renderAiMemoryWriteResult("ai_memory_vault_edit", result, options, theme, context)
|
|
604
531
|
},
|
|
605
532
|
})
|
|
606
533
|
|
|
@@ -646,36 +573,34 @@ export default function aiMemorySystemExtension(pi: ExtensionAPI): void {
|
|
|
646
573
|
.catch(() => ctx.ui.setStatus?.("ai-memory", undefined))
|
|
647
574
|
})
|
|
648
575
|
|
|
649
|
-
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
|
+
|
|
650
579
|
// Always-loaded durable context (mapped from 00 Kontext) is injected as a
|
|
651
580
|
// system-prompt chunk so it remains in the cached prefix.
|
|
652
|
-
let alwaysBlock: string | undefined
|
|
653
581
|
try {
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const block = alwaysBlock.trim()
|
|
664
|
-
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)
|
|
665
591
|
}
|
|
666
592
|
|
|
667
|
-
// Query-specific candidates
|
|
668
|
-
if (!shouldInjectAiMemoryCandidates()) return
|
|
593
|
+
// Query-specific candidates (independent of the always-context block).
|
|
669
594
|
const query = String(event?.prompt ?? "").trim()
|
|
670
|
-
if (
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
// remains available and will surface API errors when the model calls it.
|
|
678
|
-
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
|
+
}
|
|
679
602
|
}
|
|
603
|
+
|
|
604
|
+
return patch.systemPrompt === undefined && patch.message === undefined ? undefined : patch
|
|
680
605
|
})
|
|
681
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
|
+
}
|
|
@@ -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
|
|
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 (!
|
|
45
|
-
|
|
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
|
-
|
|
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 {
|