@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.
- package/README.md +0 -50
- package/dist/pi/pi-context-files.d.ts +6 -0
- package/dist/pi/pi-context-files.js +17 -0
- package/dist/pi/pi-context-files.js.map +1 -0
- package/dist/pi/pi-extensions.d.ts +0 -1
- package/dist/pi/pi-extensions.js +7 -22
- package/dist/pi/pi-extensions.js.map +1 -1
- package/dist/pi/pi-npm-extension-entry.d.ts +2 -0
- package/dist/pi/pi-npm-extension-entry.js +74 -0
- package/dist/pi/pi-npm-extension-entry.js.map +1 -0
- package/dist/pi/pi-package.js +4 -0
- package/dist/pi/pi-package.js.map +1 -1
- package/dist/pi/pi-patch-context-files.d.ts +12 -0
- package/dist/pi/pi-patch-context-files.js +46 -0
- package/dist/pi/pi-patch-context-files.js.map +1 -0
- package/dist/pi/pi-patch-interactive.js +5 -1
- package/dist/pi/pi-patch-interactive.js.map +1 -1
- package/dist/pi/plan-mode-tools.d.ts +13 -0
- package/dist/pi/plan-mode-tools.js +54 -0
- package/dist/pi/plan-mode-tools.js.map +1 -0
- package/dist/prism-extensions/core/mex-anchor.d.ts +9 -0
- package/dist/prism-extensions/core/mex-anchor.js +29 -0
- package/dist/prism-extensions/core/mex-anchor.js.map +1 -0
- package/dist/prism-extensions/core/mex-cli.d.ts +68 -0
- package/dist/prism-extensions/core/mex-cli.js +183 -0
- package/dist/prism-extensions/core/mex-cli.js.map +1 -0
- package/dist/prism-extensions/core/mex-graph-db.d.ts +26 -0
- package/dist/prism-extensions/core/mex-graph-db.js +69 -0
- package/dist/prism-extensions/core/mex-graph-db.js.map +1 -0
- package/dist/prism-extensions/integrations/ai-memory-system.js +27 -6
- package/dist/prism-extensions/integrations/ai-memory-system.js.map +1 -1
- package/dist/prism-extensions/integrations/mex-memory.d.ts +21 -0
- package/dist/prism-extensions/integrations/mex-memory.js +231 -0
- package/dist/prism-extensions/integrations/mex-memory.js.map +1 -0
- package/dist/prism-extensions/tools/builtin-tools.js +2 -0
- package/dist/prism-extensions/tools/builtin-tools.js.map +1 -1
- package/dist/prism-extensions/tools/mex-tools.d.ts +2 -0
- package/dist/prism-extensions/tools/mex-tools.js +78 -0
- package/dist/prism-extensions/tools/mex-tools.js.map +1 -0
- package/dist/prism-extensions/ui/toolbar.d.ts +2 -0
- package/dist/prism-extensions/ui/toolbar.js +10 -1
- package/dist/prism-extensions/ui/toolbar.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +15 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +6 -6
- package/package.json +5 -3
- package/src/prism-extensions/README.md +10 -1
- package/src/prism-extensions/core/mex-anchor.ts +29 -0
- package/src/prism-extensions/core/mex-cli.ts +217 -0
- package/src/prism-extensions/core/mex-graph-db.ts +76 -0
- package/src/prism-extensions/integrations/ai-memory-system.ts +30 -6
- package/src/prism-extensions/integrations/mex-memory.ts +264 -0
- package/src/prism-extensions/tools/builtin-tools.ts +2 -0
- package/src/prism-extensions/tools/mex-tools.ts +93 -0
- package/src/prism-extensions/ui/toolbar.ts +10 -1
- package/src/prism-extensions/commands/voice-command.ts +0 -49
- package/src/prism-extensions/core/voice-runtime.ts +0 -322
- package/src/prism-extensions/core/voicebox-client.ts +0 -205
- package/src/prism-extensions/core/voicebox-service.ts +0 -137
- 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
|
-
}
|