@daniel156161/prism 0.2.92 → 0.2.96

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 (59) hide show
  1. package/README.md +0 -50
  2. package/dist/pi/pi-context-files.d.ts +6 -0
  3. package/dist/pi/pi-context-files.js +17 -0
  4. package/dist/pi/pi-context-files.js.map +1 -0
  5. package/dist/pi/pi-extensions.d.ts +0 -1
  6. package/dist/pi/pi-extensions.js +7 -22
  7. package/dist/pi/pi-extensions.js.map +1 -1
  8. package/dist/pi/pi-npm-extension-entry.d.ts +2 -0
  9. package/dist/pi/pi-npm-extension-entry.js +74 -0
  10. package/dist/pi/pi-npm-extension-entry.js.map +1 -0
  11. package/dist/pi/pi-package.js +4 -0
  12. package/dist/pi/pi-package.js.map +1 -1
  13. package/dist/pi/pi-patch-context-files.d.ts +12 -0
  14. package/dist/pi/pi-patch-context-files.js +46 -0
  15. package/dist/pi/pi-patch-context-files.js.map +1 -0
  16. package/dist/pi/pi-patch-interactive.js +5 -1
  17. package/dist/pi/pi-patch-interactive.js.map +1 -1
  18. package/dist/pi/plan-mode-tools.d.ts +13 -0
  19. package/dist/pi/plan-mode-tools.js +54 -0
  20. package/dist/pi/plan-mode-tools.js.map +1 -0
  21. package/dist/prism-extensions/core/mex-anchor.d.ts +9 -0
  22. package/dist/prism-extensions/core/mex-anchor.js +29 -0
  23. package/dist/prism-extensions/core/mex-anchor.js.map +1 -0
  24. package/dist/prism-extensions/core/mex-cli.d.ts +68 -0
  25. package/dist/prism-extensions/core/mex-cli.js +183 -0
  26. package/dist/prism-extensions/core/mex-cli.js.map +1 -0
  27. package/dist/prism-extensions/core/mex-graph-db.d.ts +26 -0
  28. package/dist/prism-extensions/core/mex-graph-db.js +69 -0
  29. package/dist/prism-extensions/core/mex-graph-db.js.map +1 -0
  30. package/dist/prism-extensions/integrations/ai-memory-system.js +27 -6
  31. package/dist/prism-extensions/integrations/ai-memory-system.js.map +1 -1
  32. package/dist/prism-extensions/integrations/mex-memory.d.ts +21 -0
  33. package/dist/prism-extensions/integrations/mex-memory.js +231 -0
  34. package/dist/prism-extensions/integrations/mex-memory.js.map +1 -0
  35. package/dist/prism-extensions/tools/builtin-tools.js +2 -0
  36. package/dist/prism-extensions/tools/builtin-tools.js.map +1 -1
  37. package/dist/prism-extensions/tools/mex-tools.d.ts +2 -0
  38. package/dist/prism-extensions/tools/mex-tools.js +78 -0
  39. package/dist/prism-extensions/tools/mex-tools.js.map +1 -0
  40. package/dist/prism-extensions/ui/toolbar.d.ts +2 -0
  41. package/dist/prism-extensions/ui/toolbar.js +10 -1
  42. package/dist/prism-extensions/ui/toolbar.js.map +1 -1
  43. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +15 -0
  44. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +6 -6
  45. package/package.json +5 -3
  46. package/src/prism-extensions/README.md +10 -1
  47. package/src/prism-extensions/core/mex-anchor.ts +29 -0
  48. package/src/prism-extensions/core/mex-cli.ts +217 -0
  49. package/src/prism-extensions/core/mex-graph-db.ts +76 -0
  50. package/src/prism-extensions/integrations/ai-memory-system.ts +30 -6
  51. package/src/prism-extensions/integrations/mex-memory.ts +264 -0
  52. package/src/prism-extensions/tools/builtin-tools.ts +2 -0
  53. package/src/prism-extensions/tools/mex-tools.ts +93 -0
  54. package/src/prism-extensions/ui/toolbar.ts +10 -1
  55. package/src/prism-extensions/commands/voice-command.ts +0 -49
  56. package/src/prism-extensions/core/voice-runtime.ts +0 -322
  57. package/src/prism-extensions/core/voicebox-client.ts +0 -205
  58. package/src/prism-extensions/core/voicebox-service.ts +0 -137
  59. package/src/prism-extensions/integrations/honcho-memory.ts +0 -299
@@ -1,205 +0,0 @@
1
- import { readSettings } from "./shared-config.js"
2
-
3
- export interface VoiceboxConfig {
4
- baseUrl: string
5
- profile?: string
6
- language: string
7
- engine?: string
8
- personality?: boolean
9
- clientId: string
10
- timeoutMs: number
11
- generationTimeoutMs: number
12
- }
13
-
14
- export interface VoiceboxGenerationStatus {
15
- id: string
16
- status: string
17
- duration?: number
18
- error?: string | null
19
- source?: string
20
- }
21
-
22
- function nonEmpty(value: unknown): string | undefined {
23
- return typeof value === "string" && value.trim() ? value.trim() : undefined
24
- }
25
-
26
- function boolSetting(value: unknown): boolean | undefined {
27
- if (typeof value === "boolean") return value
28
- if (typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim())) return true
29
- if (typeof value === "string" && /^(0|false|no|off)$/i.test(value.trim())) return false
30
- return undefined
31
- }
32
-
33
- function numberSetting(value: unknown): number | undefined {
34
- if (typeof value === "number" && Number.isFinite(value)) return value
35
- if (typeof value === "string" && value.trim()) {
36
- const parsed = Number(value)
37
- if (Number.isFinite(parsed)) return parsed
38
- }
39
- return undefined
40
- }
41
-
42
- function getVoiceboxSettings(env: NodeJS.ProcessEnv): any {
43
- const settings = readSettings(undefined, env)
44
- return settings?.voicebox ?? settings?.voice?.voicebox ?? {}
45
- }
46
-
47
- function getVoiceSettings(env: NodeJS.ProcessEnv): any {
48
- return readSettings(undefined, env)?.voice ?? {}
49
- }
50
-
51
- export function normalizeVoiceboxBaseUrl(value: string): string {
52
- return value.replace(/\/+$/, "")
53
- }
54
-
55
- export function resolveVoiceboxConfig(env: NodeJS.ProcessEnv = process.env): VoiceboxConfig {
56
- const voicebox = getVoiceboxSettings(env)
57
- return {
58
- baseUrl: normalizeVoiceboxBaseUrl(nonEmpty(env.PRISM_VOICEBOX_URL) ?? nonEmpty(voicebox.url) ?? nonEmpty(voicebox.baseUrl) ?? "http://127.0.0.1:17493"),
59
- profile: nonEmpty(env.PRISM_VOICEBOX_PROFILE) ?? nonEmpty(voicebox.profile),
60
- language: nonEmpty(env.PRISM_VOICEBOX_LANGUAGE) ?? nonEmpty(voicebox.language) ?? "en",
61
- engine: nonEmpty(env.PRISM_VOICEBOX_ENGINE) ?? nonEmpty(voicebox.engine),
62
- personality: boolSetting(env.PRISM_VOICEBOX_PERSONALITY) ?? boolSetting(voicebox.personality) ?? false,
63
- clientId: nonEmpty(env.PRISM_VOICEBOX_CLIENT_ID) ?? nonEmpty(voicebox.clientId) ?? "prism",
64
- timeoutMs: Math.max(1_000, numberSetting(env.PRISM_VOICEBOX_TIMEOUT_MS) ?? numberSetting(voicebox.timeoutMs) ?? 30_000),
65
- generationTimeoutMs: Math.max(1_000, numberSetting(env.PRISM_VOICEBOX_GENERATION_TIMEOUT_MS) ?? numberSetting(voicebox.generationTimeoutMs) ?? 180_000),
66
- }
67
- }
68
-
69
- export function resolveVoiceBackend(env: NodeJS.ProcessEnv = process.env): string | undefined {
70
- return nonEmpty(env.PRISM_VOICE_BACKEND) ?? nonEmpty(getVoiceSettings(env).backend)
71
- }
72
-
73
- export function isVoiceboxBackend(env: NodeJS.ProcessEnv = process.env): boolean {
74
- return /^(voicebox|auto)$/i.test(resolveVoiceBackend(env) ?? "") || !!nonEmpty(env.PRISM_VOICEBOX_URL) || !!nonEmpty(getVoiceboxSettings(env).url) || !!nonEmpty(getVoiceboxSettings(env).baseUrl)
75
- }
76
-
77
- async function fetchJson(url: string, init: RequestInit, timeoutMs: number): Promise<any> {
78
- const response = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) })
79
- const text = await response.text()
80
- const parsed = parseJsonOrText(text)
81
- if (!response.ok) {
82
- const detail = typeof parsed?.detail === "string" ? parsed.detail : typeof parsed === "string" ? parsed : `HTTP ${response.status}`
83
- throw new Error(detail)
84
- }
85
- return parsed
86
- }
87
-
88
- async function fetchBuffer(url: string, init: RequestInit, timeoutMs: number): Promise<Buffer> {
89
- const response = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) })
90
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
91
- return Buffer.from(await response.arrayBuffer())
92
- }
93
-
94
- async function fetchStream(url: string, init: RequestInit, timeoutMs: number, onChunk: (chunk: Uint8Array) => void | Promise<void>): Promise<void> {
95
- const response = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) })
96
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
97
- if (!response.body) return
98
- const reader = response.body.getReader()
99
- try {
100
- while (true) {
101
- const { done, value } = await reader.read()
102
- if (done) break
103
- if (value?.byteLength) await onChunk(value)
104
- }
105
- } finally {
106
- reader.releaseLock()
107
- }
108
- }
109
-
110
- function parseJsonOrText(text: string): any {
111
- try { return text ? JSON.parse(text) : undefined } catch { return text }
112
- }
113
-
114
- function parseVoiceboxStatusEvent(text: string): VoiceboxGenerationStatus | undefined {
115
- for (const line of text.split(/\r?\n/)) {
116
- if (!line.startsWith("data:")) continue
117
- const parsed = parseJsonOrText(line.replace(/^data:\s*/, ""))
118
- if (parsed && typeof parsed === "object" && typeof parsed.status === "string") return parsed
119
- }
120
- const parsed = parseJsonOrText(text)
121
- if (parsed && typeof parsed === "object" && typeof parsed.status === "string") return parsed
122
- return undefined
123
- }
124
-
125
- export async function checkVoiceboxHealth(config = resolveVoiceboxConfig()): Promise<boolean> {
126
- try {
127
- await fetchJson(`${config.baseUrl}/health`, { method: "GET" }, Math.min(config.timeoutMs, 5_000))
128
- return true
129
- } catch {
130
- return false
131
- }
132
- }
133
-
134
- export async function speakWithVoicebox(text: string, config = resolveVoiceboxConfig()): Promise<any> {
135
- return fetchJson(`${config.baseUrl}/speak`, {
136
- method: "POST",
137
- headers: {
138
- "Content-Type": "application/json",
139
- "X-Voicebox-Client-Id": config.clientId,
140
- },
141
- body: JSON.stringify({
142
- text,
143
- profile: config.profile,
144
- language: config.language,
145
- engine: config.engine,
146
- personality: config.personality,
147
- }),
148
- }, config.timeoutMs)
149
- }
150
-
151
- function isAbortLikeError(error: unknown): boolean {
152
- return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")
153
- }
154
-
155
- export async function waitForVoiceboxGeneration(id: string, config = resolveVoiceboxConfig()): Promise<VoiceboxGenerationStatus> {
156
- const deadline = Date.now() + config.generationTimeoutMs
157
- let lastStatus: VoiceboxGenerationStatus | undefined
158
- while (Date.now() < deadline) {
159
- try {
160
- const response = await fetch(`${config.baseUrl}/generate/${encodeURIComponent(id)}/status`, { method: "GET", signal: AbortSignal.timeout(Math.min(config.timeoutMs, 10_000)) })
161
- const text = await response.text()
162
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
163
- lastStatus = parseVoiceboxStatusEvent(text) ?? lastStatus
164
- if (lastStatus?.status === "completed") return lastStatus
165
- if (lastStatus?.status === "failed") throw new Error(lastStatus.error || "Voicebox generation failed")
166
- } catch (error) {
167
- if (!isAbortLikeError(error)) throw error
168
- // Voicebox can block status responses while lazily loading a TTS model.
169
- // Treat individual poll timeouts as transient; the overall generation
170
- // timeout below remains the hard limit.
171
- }
172
- await new Promise((resolve) => setTimeout(resolve, 500))
173
- }
174
- throw new Error(`Voicebox generation timed out${lastStatus?.status ? ` (${lastStatus.status})` : ""}`)
175
- }
176
-
177
- export async function downloadVoiceboxAudio(id: string, config = resolveVoiceboxConfig()): Promise<Buffer> {
178
- return fetchBuffer(`${config.baseUrl}/audio/${encodeURIComponent(id)}`, { method: "GET" }, config.timeoutMs)
179
- }
180
-
181
- export async function synthesizeSpeechWithVoicebox(text: string, config = resolveVoiceboxConfig()): Promise<Buffer> {
182
- const generation = await speakWithVoicebox(text, config)
183
- const id = String(generation?.id ?? "")
184
- if (!id) throw new Error("Voicebox did not return a generation id")
185
- await waitForVoiceboxGeneration(id, config)
186
- return downloadVoiceboxAudio(id, config)
187
- }
188
-
189
- export async function streamSpeechWithVoicebox(text: string, onChunk: (chunk: Uint8Array) => void | Promise<void>, config = resolveVoiceboxConfig()): Promise<void> {
190
- const generation = await speakWithVoicebox(text, config)
191
- const id = String(generation?.id ?? "")
192
- if (!id) throw new Error("Voicebox did not return a generation id")
193
- await waitForVoiceboxGeneration(id, config)
194
- await fetchStream(`${config.baseUrl}/audio/${encodeURIComponent(id)}`, { method: "GET" }, config.timeoutMs, onChunk)
195
- }
196
-
197
- export async function transcribeWithVoicebox(audio: Blob | Buffer | Uint8Array, options: { fileName?: string; language?: string; model?: string } = {}, config = resolveVoiceboxConfig()): Promise<string> {
198
- const form = new FormData()
199
- const file = audio instanceof Blob ? audio : new Blob([new Uint8Array(audio)])
200
- form.append("file", file, options.fileName ?? "audio.wav")
201
- if (options.language) form.append("language", options.language)
202
- if (options.model) form.append("model", options.model)
203
- const result = await fetchJson(`${config.baseUrl}/transcribe`, { method: "POST", body: form }, config.timeoutMs)
204
- return String(result?.text ?? "")
205
- }
@@ -1,137 +0,0 @@
1
- import * as fs from "node:fs"
2
- import * as os from "node:os"
3
- import * as path from "node:path"
4
- import { spawnSync, type SpawnSyncReturns } from "node:child_process"
5
- import { readSettings } from "./shared-config.js"
6
- import { checkVoiceboxHealth, resolveVoiceboxConfig } from "./voicebox-client.js"
7
-
8
- const VOICEBOX_REPO_URL = "https://github.com/jamiepine/voicebox.git"
9
-
10
- export interface VoiceboxServiceConfig {
11
- autoInstall: boolean
12
- autoStart: boolean
13
- autoStop: boolean
14
- installDir: string
15
- installCommand: string
16
- startCommand: string
17
- stopCommand: string
18
- startupTimeoutMs: number
19
- installTimeoutMs: number
20
- stopTimeoutMs: number
21
- }
22
-
23
- function nonEmpty(value: unknown): string | undefined {
24
- return typeof value === "string" && value.trim() ? value.trim() : undefined
25
- }
26
-
27
- function boolSetting(value: unknown): boolean | undefined {
28
- if (typeof value === "boolean") return value
29
- if (typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim())) return true
30
- if (typeof value === "string" && /^(0|false|no|off)$/i.test(value.trim())) return false
31
- return undefined
32
- }
33
-
34
- function numberSetting(value: unknown): number | undefined {
35
- if (typeof value === "number" && Number.isFinite(value)) return value
36
- if (typeof value === "string" && value.trim()) {
37
- const parsed = Number(value)
38
- if (Number.isFinite(parsed)) return parsed
39
- }
40
- return undefined
41
- }
42
-
43
- function quote(value: string): string {
44
- return JSON.stringify(value)
45
- }
46
-
47
- function defaultInstallDir(env: NodeJS.ProcessEnv): string {
48
- return nonEmpty(env.PRISM_VOICEBOX_DIR) ?? path.join(os.homedir(), ".local", "share", "prism", "voicebox")
49
- }
50
-
51
- function defaultInstallCommand(installDir: string): string {
52
- return `mkdir -p ${quote(path.dirname(installDir))} && git clone ${quote(VOICEBOX_REPO_URL)} ${quote(installDir)}`
53
- }
54
-
55
- function defaultStartCommand(installDir: string): string {
56
- return `if command -v prism-voicebox >/dev/null 2>&1; then prism-voicebox start; else cd ${quote(installDir)} && docker compose up -d --build; fi`
57
- }
58
-
59
- function defaultStopCommand(installDir: string): string {
60
- return `if command -v prism-voicebox >/dev/null 2>&1; then prism-voicebox stop; else cd ${quote(installDir)} && docker compose down; fi`
61
- }
62
-
63
- export function resolveVoiceboxServiceConfig(env: NodeJS.ProcessEnv = process.env): VoiceboxServiceConfig {
64
- const settings = readSettings(undefined, env)?.voicebox ?? {}
65
- const installDir = nonEmpty(env.PRISM_VOICEBOX_DIR) ?? nonEmpty(settings.installDir) ?? defaultInstallDir(env)
66
- return {
67
- autoInstall: boolSetting(env.PRISM_VOICEBOX_AUTO_INSTALL) ?? boolSetting(settings.autoInstall) ?? true,
68
- autoStart: boolSetting(env.PRISM_VOICEBOX_AUTO_START) ?? boolSetting(settings.autoStart) ?? true,
69
- autoStop: boolSetting(env.PRISM_VOICEBOX_AUTO_STOP) ?? boolSetting(settings.autoStop) ?? true,
70
- installDir,
71
- installCommand: nonEmpty(env.PRISM_VOICEBOX_INSTALL_COMMAND) ?? nonEmpty(settings.installCommand) ?? defaultInstallCommand(installDir),
72
- startCommand: nonEmpty(env.PRISM_VOICEBOX_START_COMMAND) ?? nonEmpty(settings.startCommand) ?? defaultStartCommand(installDir),
73
- stopCommand: nonEmpty(env.PRISM_VOICEBOX_STOP_COMMAND) ?? nonEmpty(settings.stopCommand) ?? defaultStopCommand(installDir),
74
- startupTimeoutMs: Math.max(1_000, numberSetting(env.PRISM_VOICEBOX_STARTUP_TIMEOUT_MS) ?? numberSetting(settings.startupTimeoutMs) ?? 120_000),
75
- installTimeoutMs: Math.max(1_000, numberSetting(env.PRISM_VOICEBOX_INSTALL_TIMEOUT_MS) ?? numberSetting(settings.installTimeoutMs) ?? 300_000),
76
- stopTimeoutMs: Math.max(1_000, numberSetting(env.PRISM_VOICEBOX_STOP_TIMEOUT_MS) ?? numberSetting(settings.stopTimeoutMs) ?? 60_000),
77
- }
78
- }
79
-
80
- async function sleep(ms: number): Promise<void> {
81
- await new Promise((resolve) => setTimeout(resolve, ms))
82
- }
83
-
84
- function runShell(command: string, timeoutMs: number, env: NodeJS.ProcessEnv, spawnSyncProcess: typeof spawnSync): boolean {
85
- const result: SpawnSyncReturns<Buffer> = spawnSyncProcess("sh", ["-lc", command], { env, stdio: "ignore", timeout: timeoutMs })
86
- return !result.error && (result.status === 0 || result.status === null)
87
- }
88
-
89
- export function isVoiceboxServiceInstalled(env: NodeJS.ProcessEnv = process.env): boolean {
90
- const service = resolveVoiceboxServiceConfig(env)
91
- return fs.existsSync(path.join(service.installDir, "docker-compose.yml")) || fs.existsSync(path.join(service.installDir, "compose.yml"))
92
- }
93
-
94
- export async function installVoiceboxService(env: NodeJS.ProcessEnv = process.env, spawnSyncProcess: typeof spawnSync = spawnSync): Promise<boolean> {
95
- const service = resolveVoiceboxServiceConfig(env)
96
- if (isVoiceboxServiceInstalled(env)) return true
97
- if (!service.autoInstall) return false
98
- return runShell(service.installCommand, service.installTimeoutMs, env, spawnSyncProcess) && isVoiceboxServiceInstalled(env)
99
- }
100
-
101
- export async function startVoiceboxService(env: NodeJS.ProcessEnv = process.env, spawnSyncProcess: typeof spawnSync = spawnSync): Promise<boolean> {
102
- const service = resolveVoiceboxServiceConfig(env)
103
- if (!service.autoStart) return false
104
- if (!isVoiceboxServiceInstalled(env) && !(await installVoiceboxService(env, spawnSyncProcess))) return false
105
- return runShell(service.startCommand, service.startupTimeoutMs, env, spawnSyncProcess)
106
- }
107
-
108
- export async function stopVoiceboxService(env: NodeJS.ProcessEnv = process.env, spawnSyncProcess: typeof spawnSync = spawnSync): Promise<boolean> {
109
- const service = resolveVoiceboxServiceConfig(env)
110
- if (!service.autoStop) return false
111
- if (!isVoiceboxServiceInstalled(env)) return false
112
- return runShell(service.stopCommand, service.stopTimeoutMs, env, spawnSyncProcess)
113
- }
114
-
115
- export async function ensureVoiceboxServiceRunning(env: NodeJS.ProcessEnv = process.env, spawnSyncProcess: typeof spawnSync = spawnSync, onStatus?: (message: string, level?: string) => void): Promise<boolean> {
116
- const config = resolveVoiceboxConfig(env)
117
- if (await checkVoiceboxHealth(config)) return true
118
- const service = resolveVoiceboxServiceConfig(env)
119
-
120
- if (!isVoiceboxServiceInstalled(env)) {
121
- if (!service.autoInstall) return false
122
- onStatus?.(`Voicebox is not installed. Installing to ${service.installDir}…`, "info")
123
- if (!(await installVoiceboxService(env, spawnSyncProcess))) return false
124
- }
125
-
126
- if (!service.autoStart) return false
127
- onStatus?.("Voicebox is not running. Starting local Voicebox container…", "info")
128
- const started = await startVoiceboxService(env, spawnSyncProcess)
129
- if (!started) return false
130
-
131
- const deadline = Date.now() + service.startupTimeoutMs
132
- while (Date.now() < deadline) {
133
- if (await checkVoiceboxHealth(config)) return true
134
- await sleep(1_000)
135
- }
136
- return false
137
- }
@@ -1,299 +0,0 @@
1
- import { readSettings, getPrismSettingsPath } from "../core/shared-config.js"
2
- import { schema } from "../core/vault.js"
3
- import { renderSingleLineToolCall, type ThemeLike } from "../ui/tool-call-rendering.js"
4
-
5
- export type ExtensionAPI = any
6
-
7
- export type HonchoConfig = {
8
- baseUrl?: string
9
- apiKey?: string
10
- workspace: string
11
- userPeer: string
12
- assistantPeer: string
13
- sessionId?: string
14
- autoCapture: boolean
15
- maxCaptureChars: number
16
- }
17
-
18
- type HonchoMessage = {
19
- role?: string
20
- content?: unknown
21
- customType?: string
22
- stopReason?: string
23
- metadata?: Record<string, unknown>
24
- }
25
-
26
- const DEFAULT_WORKSPACE = "prism"
27
- const DEFAULT_USER_PEER = "user"
28
- const DEFAULT_ASSISTANT_PEER = "prism"
29
- const DEFAULT_MAX_CAPTURE_CHARS = 12_000
30
-
31
- const INJECTED_CONTEXT_TYPES = new Set([
32
- "honcho-memory-context",
33
- "obsidian-memory-context",
34
- "logseq-memory-context",
35
- "rp-memory-context",
36
- ])
37
-
38
- function nonEmptyString(value: unknown): string | undefined {
39
- return typeof value === "string" && value.trim() ? value.trim() : undefined
40
- }
41
-
42
- function boolFromConfig(value: unknown, fallback: boolean): boolean {
43
- if (value === undefined || value === null) return fallback
44
- if (typeof value === "boolean") return value
45
- const normalized = String(value).trim().toLowerCase()
46
- if (["0", "false", "no", "off"].includes(normalized)) return false
47
- if (["1", "true", "yes", "on"].includes(normalized)) return true
48
- return fallback
49
- }
50
-
51
- export function sanitizeHonchoId(value: string): string {
52
- const cleaned = value.trim().replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "")
53
- return cleaned || "default"
54
- }
55
-
56
- function honchoSettingsBlock(settings: any): any {
57
- return settings?.honchoMemory ?? settings?.honcho ?? settings?.integrations?.honcho ?? settings?.memory?.honcho ?? {}
58
- }
59
-
60
- export function readHonchoSettings(env: NodeJS.ProcessEnv = process.env): any {
61
- return honchoSettingsBlock(readSettings(getPrismSettingsPath(env)))
62
- }
63
-
64
- export function resolveHonchoConfig(env: NodeJS.ProcessEnv = process.env, settings: any = readHonchoSettings(env)): HonchoConfig {
65
- const sessionId = nonEmptyString(env.PRISM_HONCHO_SESSION_ID) ?? nonEmptyString(settings?.sessionId) ?? nonEmptyString(settings?.session)
66
- const maxCaptureChars = nonEmptyString(env.PRISM_HONCHO_MAX_CAPTURE_CHARS) ?? settings?.maxCaptureChars
67
- return {
68
- baseUrl: nonEmptyString(env.PRISM_HONCHO_BASE_URL) ?? nonEmptyString(env.HONCHO_BASE_URL) ?? nonEmptyString(settings?.baseUrl) ?? nonEmptyString(settings?.baseURL) ?? nonEmptyString(settings?.url),
69
- apiKey: nonEmptyString(env.PRISM_HONCHO_API_KEY) ?? nonEmptyString(env.HONCHO_API_KEY) ?? nonEmptyString(settings?.apiKey) ?? nonEmptyString(settings?.token) ?? nonEmptyString(settings?.jwt),
70
- workspace: sanitizeHonchoId(nonEmptyString(env.PRISM_HONCHO_WORKSPACE) ?? nonEmptyString(env.HONCHO_WORKSPACE) ?? nonEmptyString(settings?.workspace) ?? DEFAULT_WORKSPACE),
71
- userPeer: sanitizeHonchoId(nonEmptyString(env.PRISM_HONCHO_USER_PEER) ?? nonEmptyString(settings?.userPeer) ?? nonEmptyString(settings?.user) ?? DEFAULT_USER_PEER),
72
- assistantPeer: sanitizeHonchoId(nonEmptyString(env.PRISM_HONCHO_ASSISTANT_PEER) ?? nonEmptyString(settings?.assistantPeer) ?? nonEmptyString(settings?.assistant) ?? DEFAULT_ASSISTANT_PEER),
73
- sessionId: sessionId ? sanitizeHonchoId(sessionId) : undefined,
74
- autoCapture: boolFromConfig(env.PRISM_HONCHO_AUTO_CAPTURE ?? settings?.autoCapture, true),
75
- maxCaptureChars: Math.max(100, Math.min(Number(maxCaptureChars ?? DEFAULT_MAX_CAPTURE_CHARS) || DEFAULT_MAX_CAPTURE_CHARS, 25_000)),
76
- }
77
- }
78
-
79
- export function isHonchoConfigured(env: NodeJS.ProcessEnv = process.env): boolean {
80
- return !!resolveHonchoConfig(env).baseUrl
81
- }
82
-
83
- function textFromContent(content: unknown): string {
84
- if (typeof content === "string") return content
85
- if (Array.isArray(content)) {
86
- return content.map((part) => {
87
- if (typeof part === "string") return part
88
- if (part && typeof part === "object" && typeof (part as any).text === "string") return (part as any).text
89
- return ""
90
- }).filter(Boolean).join("\n")
91
- }
92
- return ""
93
- }
94
-
95
- export function shouldCaptureHonchoMessage(message: HonchoMessage): boolean {
96
- const role = message?.role
97
- if (role !== "user" && role !== "assistant") return false
98
- if (message.customType && INJECTED_CONTEXT_TYPES.has(message.customType)) return false
99
- if (role === "assistant" && (message.stopReason === "error" || message.stopReason === "aborted")) return false
100
- const text = textFromContent(message.content).trim()
101
- if (!text) return false
102
- const lower = text.slice(0, 200).toLowerCase()
103
- if (lower.includes("prompt-relevant memories from the user's obsidian vault")) return false
104
- if (lower.includes("always-loaded context from 00 kontext")) return false
105
- if (lower.includes("prompt-relevant pages from the user's logseq vault")) return false
106
- if (lower.includes("honcho memory context")) return false
107
- return true
108
- }
109
-
110
- export function honchoSessionIdFromContext(ctx: any, fallback = "prism-session"): string {
111
- const explicit = nonEmptyString(ctx?.sessionId) ?? nonEmptyString(ctx?.session?.id) ?? nonEmptyString(ctx?.sessionManager?.sessionId)
112
- if (explicit) return sanitizeHonchoId(explicit)
113
- const file = nonEmptyString(ctx?.sessionManager?.getSessionFile?.())
114
- if (file) return sanitizeHonchoId(file.split(/[\\/]/).pop()?.replace(/\.[^.]+$/, "") ?? file)
115
- return sanitizeHonchoId(fallback)
116
- }
117
-
118
- class HonchoHttpClient {
119
- readonly baseUrl: string
120
- readonly apiKey?: string
121
-
122
- constructor(config: HonchoConfig) {
123
- if (!config.baseUrl) throw new Error("Honcho base URL is not configured")
124
- this.baseUrl = config.baseUrl.replace(/\/+$/, "")
125
- this.apiKey = config.apiKey
126
- }
127
-
128
- private headers(): Record<string, string> {
129
- const headers: Record<string, string> = { "content-type": "application/json" }
130
- if (this.apiKey) headers.authorization = `Bearer ${this.apiKey}`
131
- return headers
132
- }
133
-
134
- private async request(path: string, init: RequestInit = {}): Promise<any> {
135
- const response = await fetch(`${this.baseUrl}${path}`, { ...init, headers: { ...this.headers(), ...(init.headers as any ?? {}) } })
136
- const text = await response.text()
137
- const body = text ? safeJsonParse(text) : undefined
138
- if (!response.ok) {
139
- const detail = typeof body?.detail === "string" ? body.detail : typeof body?.error?.message === "string" ? body.error.message : text
140
- throw new Error(`Honcho ${response.status}: ${detail || response.statusText}`)
141
- }
142
- return body
143
- }
144
-
145
- async health(): Promise<any> { return this.request("/health", { method: "GET" }) }
146
- async ensureWorkspace(workspace: string): Promise<void> { await this.request("/v3/workspaces", { method: "POST", body: JSON.stringify({ id: workspace }) }) }
147
- async ensurePeer(workspace: string, peer: string): Promise<void> { await this.request(`/v3/workspaces/${encodeURIComponent(workspace)}/peers`, { method: "POST", body: JSON.stringify({ id: peer }) }) }
148
- async ensureSession(workspace: string, session: string, peers: string[]): Promise<void> {
149
- const peerMap = Object.fromEntries(peers.map((peer) => [peer, {}]))
150
- await this.request(`/v3/workspaces/${encodeURIComponent(workspace)}/sessions`, { method: "POST", body: JSON.stringify({ id: session, peers: peerMap }) })
151
- }
152
- async addMessages(workspace: string, session: string, messages: Array<{ peer_id: string; content: string; metadata?: Record<string, unknown> }>): Promise<any> {
153
- return this.request(`/v3/workspaces/${encodeURIComponent(workspace)}/sessions/${encodeURIComponent(session)}/messages`, { method: "POST", body: JSON.stringify({ messages }) })
154
- }
155
- async chat(workspace: string, peer: string, query: string, sessionId?: string, reasoningLevel = "low"): Promise<string> {
156
- const body: Record<string, unknown> = { query, reasoning_level: reasoningLevel, stream: false }
157
- if (sessionId) body.session_id = sessionId
158
- const result = await this.request(`/v3/workspaces/${encodeURIComponent(workspace)}/peers/${encodeURIComponent(peer)}/chat`, { method: "POST", body: JSON.stringify(body) })
159
- return String(result?.content ?? "")
160
- }
161
- async representation(workspace: string, peer: string, options: Record<string, unknown> = {}): Promise<string> {
162
- const result = await this.request(`/v3/workspaces/${encodeURIComponent(workspace)}/peers/${encodeURIComponent(peer)}/representation`, { method: "POST", body: JSON.stringify(options) })
163
- return String(result?.representation ?? "")
164
- }
165
- async context(workspace: string, session: string, tokens = 2000, summary = true): Promise<any> {
166
- const params = new URLSearchParams({ tokens: String(tokens), summary: String(summary) })
167
- return this.request(`/v3/workspaces/${encodeURIComponent(workspace)}/sessions/${encodeURIComponent(session)}/context?${params}`, { method: "GET" })
168
- }
169
- async queueStatus(workspace: string): Promise<any> { return this.request(`/v3/workspaces/${encodeURIComponent(workspace)}/queue/status`, { method: "GET" }) }
170
- }
171
-
172
- function safeJsonParse(text: string): any {
173
- try { return JSON.parse(text) } catch { return undefined }
174
- }
175
-
176
- async function ensureBase(client: HonchoHttpClient, config: HonchoConfig, sessionId?: string): Promise<void> {
177
- await client.ensureWorkspace(config.workspace)
178
- await client.ensurePeer(config.workspace, config.userPeer)
179
- await client.ensurePeer(config.workspace, config.assistantPeer)
180
- if (sessionId) await client.ensureSession(config.workspace, sessionId, [config.userPeer, config.assistantPeer])
181
- }
182
-
183
- export function buildCapturedHonchoMessages(messages: HonchoMessage[], config: HonchoConfig): Array<{ peer_id: string; content: string; metadata: Record<string, unknown> }> {
184
- return messages.filter(shouldCaptureHonchoMessage).map((message) => {
185
- const role = message.role === "assistant" ? "assistant" : "user"
186
- const peer_id = role === "assistant" ? config.assistantPeer : config.userPeer
187
- const raw = textFromContent(message.content).trim()
188
- const content = raw.length > config.maxCaptureChars ? `${raw.slice(0, config.maxCaptureChars)}\n\n[truncated by Prism Honcho auto-capture]` : raw
189
- return { peer_id, content, metadata: { source: "prism", role, auto_capture: true } }
190
- })
191
- }
192
-
193
- function formatToolCall(action: string, value: unknown, theme: ThemeLike): string {
194
- const label = typeof value === "string" && value.trim() ? value.trim() : "..."
195
- return `${theme.fg("toolTitle", theme.bold(`honcho ${action}`))} ${theme.fg("accent", label)}`
196
- }
197
-
198
- export default async function honchoMemoryExtension(pi: ExtensionAPI): Promise<void> {
199
- const config = resolveHonchoConfig()
200
- if (!config.baseUrl) return
201
-
202
- const client = new HonchoHttpClient(config)
203
-
204
- pi.on?.("session_start", async (_event: any, ctx: any) => {
205
- const status = config.apiKey ? `Honcho: ${config.workspace}` : "Honcho: missing API key"
206
- ctx.ui?.setStatus?.("honcho-memory", status)
207
- })
208
-
209
- pi.on?.("agent_end", async (event: any, ctx: any) => {
210
- if (!config.autoCapture || !config.apiKey) return
211
- const sessionId = config.sessionId ?? honchoSessionIdFromContext(ctx)
212
- const messages = buildCapturedHonchoMessages(event?.messages ?? [], config)
213
- if (messages.length === 0) return
214
- try {
215
- await ensureBase(client, config, sessionId)
216
- await client.addMessages(config.workspace, sessionId, messages)
217
- ctx?.ui?.setStatus?.("honcho-memory", `Honcho: captured ${messages.length}`)
218
- } catch (error) {
219
- ctx?.ui?.notify?.(`Honcho auto-capture failed: ${error instanceof Error ? error.message : String(error)}`, "warning")
220
- }
221
- })
222
-
223
- pi.registerTool({
224
- name: "honcho_status",
225
- label: "Honcho Status",
226
- description: "Check Honcho memory integration status, auth, workspace, queue and API health.",
227
- promptSnippet: "Check Honcho memory integration status",
228
- parameters: schema({}),
229
- async execute() {
230
- const health = await client.health()
231
- const queue = config.apiKey ? await client.queueStatus(config.workspace).catch((error) => ({ error: error instanceof Error ? error.message : String(error) })) : { error: "Missing PRISM_HONCHO_API_KEY" }
232
- return { content: [{ type: "text", text: [`Honcho: ${config.baseUrl}`, `Workspace: ${config.workspace}`, `Auth: ${config.apiKey ? "configured" : "missing API key"}`, `Auto-capture: ${config.autoCapture ? "on" : "off"}`, `Health: ${JSON.stringify(health)}`, `Queue: ${JSON.stringify(queue)}`].join("\n") }], details: { config: { ...config, apiKey: config.apiKey ? "<configured>" : undefined }, health, queue } }
233
- },
234
- renderCall(_args: any, theme: ThemeLike, context: any) { return renderSingleLineToolCall(formatToolCall("status", config.workspace, theme), context) },
235
- } as any)
236
-
237
- pi.registerTool({
238
- name: "honcho_remember",
239
- label: "Honcho Remember",
240
- description: "Store a memory/message in Honcho for a peer/session. Uses Honcho automatic reasoning later.",
241
- promptSnippet: "Store conversational memory in Honcho [show peer/session and content summary]",
242
- promptGuidelines: [
243
- "Use honcho_remember for conversational facts/preferences that should be available to future Prism runs.",
244
- "Do not store secrets, passwords, API keys, or sensitive personal data unless the user explicitly requests it.",
245
- "Use Obsidian for curated durable source-of-truth notes; use Honcho for automatic conversational memory.",
246
- ],
247
- parameters: schema({ text: { type: "string", description: "Memory text to store" }, peer: { type: "string", description: "Peer ID, default Daniel/user peer" }, session: { type: "string", description: "Honcho session ID, default current Prism session" } }, ["text"]),
248
- async execute(_id: string, params: any, _signal: any, _onUpdate: any, ctx: any) {
249
- const sessionId = sanitizeHonchoId(params.session || config.sessionId || honchoSessionIdFromContext(ctx))
250
- const peer = sanitizeHonchoId(params.peer || config.userPeer)
251
- await ensureBase(client, config, sessionId)
252
- if (peer !== config.userPeer && peer !== config.assistantPeer) await client.ensurePeer(config.workspace, peer)
253
- await client.addMessages(config.workspace, sessionId, [{ peer_id: peer, content: String(params.text ?? ""), metadata: { source: "prism", manual: true } }])
254
- return { content: [{ type: "text", text: `Stored in Honcho.\nWorkspace: ${config.workspace}\nSession: ${sessionId}\nPeer: ${peer}` }], details: { workspace: config.workspace, session: sessionId, peer } }
255
- },
256
- renderCall(args: any, theme: ThemeLike, context: any) { return renderSingleLineToolCall(formatToolCall("remember", args?.peer ?? config.userPeer, theme), context) },
257
- } as any)
258
-
259
- pi.registerTool({
260
- name: "honcho_recall",
261
- label: "Honcho Recall",
262
- description: "Ask Honcho a natural-language question about a peer's remembered context.",
263
- promptSnippet: "Recall conversational memory from Honcho [show query]",
264
- parameters: schema({ query: { type: "string", description: "Question to ask Honcho" }, peer: { type: "string", description: "Peer ID, default Daniel/user peer" }, session: { type: "string", description: "Optional session scope" }, reasoningLevel: { type: "string", description: "minimal, low, medium, high, max. Default low" } }, ["query"]),
265
- async execute(_id: string, params: any) {
266
- const peer = sanitizeHonchoId(params.peer || config.userPeer)
267
- const sessionId = params.session ? sanitizeHonchoId(params.session) : undefined
268
- const answer = await client.chat(config.workspace, peer, String(params.query), sessionId, params.reasoningLevel || "low")
269
- return { content: [{ type: "text", text: answer || "Honcho returned no answer." }], details: { workspace: config.workspace, peer, session: sessionId, query: params.query } }
270
- },
271
- renderCall(args: any, theme: ThemeLike, context: any) { return renderSingleLineToolCall(formatToolCall("recall", args?.query, theme), context) },
272
- } as any)
273
-
274
- pi.registerTool({
275
- name: "honcho_context",
276
- label: "Honcho Context",
277
- description: "Read Honcho session context with recent messages and optional summary.",
278
- promptSnippet: "Read Honcho session context [show session]",
279
- parameters: schema({ session: { type: "string", description: "Honcho session ID, default current Prism session" }, tokens: { type: "number", description: "Token budget, default 2000" }, summary: { type: "boolean", description: "Include summary if available, default true" } }),
280
- async execute(_id: string, params: any, _signal: any, _onUpdate: any, ctx: any) {
281
- const sessionId = sanitizeHonchoId(params.session || config.sessionId || honchoSessionIdFromContext(ctx))
282
- const result = await client.context(config.workspace, sessionId, params.tokens ?? 2000, params.summary ?? true)
283
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: { workspace: config.workspace, session: sessionId } }
284
- },
285
- renderCall(args: any, theme: ThemeLike, context: any) { return renderSingleLineToolCall(formatToolCall("context", args?.session ?? "current", theme), context) },
286
- } as any)
287
-
288
- pi.registerCommand("honcho", {
289
- description: "Show Honcho memory integration status",
290
- handler: async (_args: string, ctx: any) => {
291
- try {
292
- const health = await client.health()
293
- ctx.ui.notify(`Honcho: ${config.baseUrl}\nWorkspace: ${config.workspace}\nAuth: ${config.apiKey ? "configured" : "missing API key"}\nAuto-capture: ${config.autoCapture ? "on" : "off"}\nHealth: ${JSON.stringify(health)}`, config.apiKey ? "info" : "warning")
294
- } catch (error) {
295
- ctx.ui.notify(`Honcho unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning")
296
- }
297
- },
298
- })
299
- }