@openviking/opencode-plugin 0.1.5
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/INSTALL-ZH.md +225 -0
- package/INSTALL.md +220 -0
- package/README.md +261 -0
- package/index.mjs +84 -0
- package/lib/code-tools.mjs +134 -0
- package/lib/memadd-local.mjs +118 -0
- package/lib/memory-recall.mjs +214 -0
- package/lib/memory-session.mjs +651 -0
- package/lib/memory-tools.mjs +410 -0
- package/lib/repo-context.mjs +68 -0
- package/lib/runtime.mjs +33 -0
- package/lib/utils.mjs +341 -0
- package/lib/viking-uri-guard.mjs +52 -0
- package/package.json +42 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { addMemaddResource } from "./memadd-local.mjs"
|
|
3
|
+
import {
|
|
4
|
+
log,
|
|
5
|
+
effectivePeerId,
|
|
6
|
+
makeRequest,
|
|
7
|
+
unwrapResponse,
|
|
8
|
+
validateVikingUri,
|
|
9
|
+
} from "./utils.mjs"
|
|
10
|
+
|
|
11
|
+
const z = tool.schema
|
|
12
|
+
|
|
13
|
+
export function createMemoryTools({ config, sessionManager, projectDirectory }) {
|
|
14
|
+
const actorPeerId = effectivePeerId(config)
|
|
15
|
+
return {
|
|
16
|
+
memsearch: tool({
|
|
17
|
+
description:
|
|
18
|
+
"Search OpenViking memories, indexed repositories, and skills. Use this for semantic or conceptual questions. Narrow `target_uri` whenever possible, for example viking://resources/project/ or viking://user/memories/.",
|
|
19
|
+
args: {
|
|
20
|
+
query: z.string().describe("Natural language query, question, or task description."),
|
|
21
|
+
target_uri: z.string().optional().describe("Optional Viking URI scope, e.g. viking://resources/ or viking://user/memories/."),
|
|
22
|
+
mode: z.enum(["auto", "fast", "deep"]).optional().describe("auto chooses based on query complexity; fast uses /find; deep uses /search with session context when available."),
|
|
23
|
+
session_id: z.string().optional().describe("Optional explicit OpenViking session ID for context-aware search."),
|
|
24
|
+
limit: z.number().optional().describe("Maximum number of results. Defaults to 10."),
|
|
25
|
+
score_threshold: z.number().optional().describe("Optional minimum score threshold."),
|
|
26
|
+
},
|
|
27
|
+
async execute(args, context) {
|
|
28
|
+
try {
|
|
29
|
+
let sessionId = args.session_id
|
|
30
|
+
if (!sessionId && context.sessionID) {
|
|
31
|
+
sessionId = sessionManager.getMappedSessionId(context.sessionID)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const mode = resolveSearchMode(args.mode, args.query, sessionId)
|
|
35
|
+
const body = {
|
|
36
|
+
query: args.query,
|
|
37
|
+
limit: args.limit ?? 10,
|
|
38
|
+
}
|
|
39
|
+
if (args.target_uri) body.target_uri = args.target_uri
|
|
40
|
+
if (args.score_threshold !== undefined) body.score_threshold = args.score_threshold
|
|
41
|
+
if (mode === "deep" && sessionId) body.session_id = sessionId
|
|
42
|
+
const response = await makeRequest(config, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
endpoint: mode === "deep" ? "/api/v1/search/search" : "/api/v1/search/find",
|
|
45
|
+
body,
|
|
46
|
+
abortSignal: context.abort,
|
|
47
|
+
actorPeerId,
|
|
48
|
+
})
|
|
49
|
+
return formatSearchResults(unwrapResponse(response), args.query, { mode })
|
|
50
|
+
} catch (error) {
|
|
51
|
+
log("ERROR", "memsearch", "Search failed", { error: error?.message, args })
|
|
52
|
+
return `Error: ${error.message}`
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
memread: tool({
|
|
58
|
+
description:
|
|
59
|
+
"Read a specific viking:// URI. Use after memsearch, membrowse, memgrep, or memglob returns a URI. `auto` chooses overview for directories and read for files.",
|
|
60
|
+
args: {
|
|
61
|
+
uri: z.string().describe("Complete Viking URI to read."),
|
|
62
|
+
level: z.enum(["auto", "abstract", "overview", "read"]).optional().describe("Read level. Defaults to auto."),
|
|
63
|
+
},
|
|
64
|
+
async execute(args, context) {
|
|
65
|
+
const validationError = validateVikingUri(args.uri, "memread")
|
|
66
|
+
if (validationError) return validationError
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
let level = args.level ?? "auto"
|
|
70
|
+
if (level === "auto") {
|
|
71
|
+
level = await resolveReadLevel(config, args.uri, context.abort, actorPeerId)
|
|
72
|
+
}
|
|
73
|
+
const response = await makeRequest(config, {
|
|
74
|
+
method: "GET",
|
|
75
|
+
endpoint: `/api/v1/content/${level}?uri=${encodeURIComponent(args.uri)}`,
|
|
76
|
+
abortSignal: context.abort,
|
|
77
|
+
actorPeerId,
|
|
78
|
+
})
|
|
79
|
+
const content = unwrapResponse(response)
|
|
80
|
+
return typeof content === "string" ? content : JSON.stringify(content, null, 2)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
log("ERROR", "memread", "Read failed", { error: error?.message, uri: args.uri })
|
|
83
|
+
return `Error: ${error.message}`
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
membrowse: tool({
|
|
89
|
+
description:
|
|
90
|
+
"Browse OpenViking filesystem structure. Use list/tree/stat to discover exact URIs before reading. Scope to the narrowest useful viking:// path.",
|
|
91
|
+
args: {
|
|
92
|
+
uri: z.string().describe("Viking URI to inspect, e.g. viking://resources/ or viking://user/memories/."),
|
|
93
|
+
view: z.enum(["list", "tree", "stat"]).optional().describe("Browse view. Defaults to list."),
|
|
94
|
+
recursive: z.boolean().optional().describe("For list view only, recursively list descendants."),
|
|
95
|
+
simple: z.boolean().optional().describe("For list view only, return simpler URI-oriented output."),
|
|
96
|
+
},
|
|
97
|
+
async execute(args, context) {
|
|
98
|
+
const validationError = validateVikingUri(args.uri, "membrowse")
|
|
99
|
+
if (validationError) return validationError
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const view = args.view ?? "list"
|
|
103
|
+
const encodedUri = encodeURIComponent(args.uri)
|
|
104
|
+
let endpoint
|
|
105
|
+
if (view === "stat") {
|
|
106
|
+
endpoint = `/api/v1/fs/stat?uri=${encodedUri}`
|
|
107
|
+
} else if (view === "tree") {
|
|
108
|
+
endpoint = `/api/v1/fs/tree?uri=${encodedUri}`
|
|
109
|
+
} else {
|
|
110
|
+
endpoint = `/api/v1/fs/ls?uri=${encodedUri}&recursive=${args.recursive ? "true" : "false"}&simple=${args.simple ? "true" : "false"}`
|
|
111
|
+
}
|
|
112
|
+
const response = await makeRequest(config, {
|
|
113
|
+
method: "GET",
|
|
114
|
+
endpoint,
|
|
115
|
+
abortSignal: context.abort,
|
|
116
|
+
actorPeerId,
|
|
117
|
+
})
|
|
118
|
+
return JSON.stringify({ view, result: unwrapResponse(response) }, null, 2)
|
|
119
|
+
} catch (error) {
|
|
120
|
+
log("ERROR", "membrowse", "Browse failed", { error: error?.message, uri: args.uri })
|
|
121
|
+
return `Error: ${error.message}`
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
|
|
126
|
+
memcommit: tool({
|
|
127
|
+
description:
|
|
128
|
+
"Commit the current OpenCode session to OpenViking and extract persistent memories. Use for immediate memory extraction before ending a conversation or after important preferences/decisions are discussed.",
|
|
129
|
+
args: {
|
|
130
|
+
session_id: z.string().optional().describe("Optional explicit OpenViking session ID. Omit to use the current OpenCode session mapping."),
|
|
131
|
+
},
|
|
132
|
+
async execute(args, context) {
|
|
133
|
+
let sessionId = args.session_id
|
|
134
|
+
if (!sessionId && context.sessionID) {
|
|
135
|
+
sessionId = sessionManager.getMappedSessionId(context.sessionID)
|
|
136
|
+
}
|
|
137
|
+
if (!sessionId) {
|
|
138
|
+
return "Error: No OpenViking session is associated with the current OpenCode session. Start or resume a normal OpenCode session first, or pass session_id."
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const result = await sessionManager.commitSession(sessionId, context.sessionID, context.abort)
|
|
143
|
+
return formatCommitResult(sessionId, result)
|
|
144
|
+
} catch (error) {
|
|
145
|
+
log("ERROR", "memcommit", "Commit failed", { error: error?.message, session_id: sessionId })
|
|
146
|
+
return `Error: ${error.message}`
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
memgrep: tool({
|
|
152
|
+
description:
|
|
153
|
+
"Search exact text or regex-like patterns in OpenViking content. Use this for symbols, function names, classes, error strings, or known keywords. Narrow `uri` to the smallest relevant repository or directory.",
|
|
154
|
+
args: {
|
|
155
|
+
pattern: z.string().describe("Pattern or exact keyword to search for."),
|
|
156
|
+
uri: z.string().optional().describe("Starting Viking URI. Defaults to viking://resources/."),
|
|
157
|
+
case_insensitive: z.boolean().optional().describe("Whether search should ignore case."),
|
|
158
|
+
exclude_uri: z.string().optional().describe("Optional URI prefix to exclude from matches."),
|
|
159
|
+
level_limit: z.number().optional().describe("Optional maximum traversal depth."),
|
|
160
|
+
},
|
|
161
|
+
async execute(args, context) {
|
|
162
|
+
const uri = args.uri ?? "viking://resources/"
|
|
163
|
+
const validationError = validateVikingUri(uri, "memgrep")
|
|
164
|
+
if (validationError) return validationError
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const body = { uri, pattern: args.pattern }
|
|
168
|
+
if (args.case_insensitive !== undefined) body.case_insensitive = args.case_insensitive
|
|
169
|
+
if (args.exclude_uri) body.exclude_uri = args.exclude_uri
|
|
170
|
+
if (args.level_limit !== undefined) body.level_limit = args.level_limit
|
|
171
|
+
const response = await makeRequest(config, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
endpoint: "/api/v1/search/grep",
|
|
174
|
+
body,
|
|
175
|
+
abortSignal: context.abort,
|
|
176
|
+
actorPeerId,
|
|
177
|
+
})
|
|
178
|
+
return JSON.stringify(unwrapResponse(response), null, 2)
|
|
179
|
+
} catch (error) {
|
|
180
|
+
log("ERROR", "memgrep", "Grep failed", { error: error?.message, args })
|
|
181
|
+
return `Error: ${error.message}`
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
}),
|
|
185
|
+
|
|
186
|
+
memglob: tool({
|
|
187
|
+
description:
|
|
188
|
+
"List files by glob pattern in OpenViking. Use this to enumerate candidate files before memread. Narrow `uri` to the smallest relevant repository or directory.",
|
|
189
|
+
args: {
|
|
190
|
+
pattern: z.string().describe("Glob pattern, e.g. **/*.py or **/test_*.ts."),
|
|
191
|
+
uri: z.string().optional().describe("Starting Viking URI. Defaults to viking://resources/."),
|
|
192
|
+
node_limit: z.number().optional().describe("Optional maximum number of matches."),
|
|
193
|
+
},
|
|
194
|
+
async execute(args, context) {
|
|
195
|
+
const uri = args.uri ?? "viking://resources/"
|
|
196
|
+
const validationError = validateVikingUri(uri, "memglob")
|
|
197
|
+
if (validationError) return validationError
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const body = { uri, pattern: args.pattern }
|
|
201
|
+
if (args.node_limit !== undefined) body.node_limit = args.node_limit
|
|
202
|
+
const response = await makeRequest(config, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
endpoint: "/api/v1/search/glob",
|
|
205
|
+
body,
|
|
206
|
+
abortSignal: context.abort,
|
|
207
|
+
actorPeerId,
|
|
208
|
+
})
|
|
209
|
+
return JSON.stringify(unwrapResponse(response), null, 2)
|
|
210
|
+
} catch (error) {
|
|
211
|
+
log("ERROR", "memglob", "Glob failed", { error: error?.message, args })
|
|
212
|
+
return `Error: ${error.message}`
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
|
|
217
|
+
memadd: tool({
|
|
218
|
+
description:
|
|
219
|
+
"Add a remote URL or local file resource to OpenViking under viking://resources/. Local files are uploaded through OpenViking temp upload before indexing. After adding, this returns observer queue status so indexing progress is visible.",
|
|
220
|
+
args: {
|
|
221
|
+
path: z.string().describe("Remote http(s) URL, local file path, or file:// URL to add. Relative local paths are resolved from the OpenCode project directory."),
|
|
222
|
+
to: z.string().optional().describe("Exact target URI under viking://resources/. Cannot be used with parent."),
|
|
223
|
+
parent: z.string().optional().describe("Parent URI under viking://resources/. Cannot be used with to."),
|
|
224
|
+
reason: z.string().optional().describe("Reason for adding this resource."),
|
|
225
|
+
instruction: z.string().optional().describe("Optional processing instruction."),
|
|
226
|
+
wait: z.boolean().optional().describe("Whether OpenViking should wait for semantic processing."),
|
|
227
|
+
timeout: z.number().optional().describe("Timeout seconds when wait=true."),
|
|
228
|
+
watch_interval: z.number().optional().describe("Minutes between scheduled refreshes. Requires to."),
|
|
229
|
+
},
|
|
230
|
+
async execute(args, context) {
|
|
231
|
+
if (args.to && args.parent) return "Error: Use either `to` or `parent`, not both."
|
|
232
|
+
if (args.to && !args.to.startsWith("viking://resources")) return "Error: `to` must be under viking://resources/."
|
|
233
|
+
if (args.parent && !args.parent.startsWith("viking://resources")) return "Error: `parent` must be under viking://resources/."
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const result = await addMemaddResource(config, args, projectDirectory, context.abort, actorPeerId)
|
|
237
|
+
if (result.error) return result.error
|
|
238
|
+
const queue = await getQueueStatus(config, context.abort)
|
|
239
|
+
return JSON.stringify({ add_resource: unwrapResponse(result.addResponse), queue }, null, 2)
|
|
240
|
+
} catch (error) {
|
|
241
|
+
log("ERROR", "memadd", "Add resource failed", { error: error?.message, args })
|
|
242
|
+
return `Error: ${error.message}`
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
|
|
247
|
+
memwrite: tool({
|
|
248
|
+
description:
|
|
249
|
+
"Write text content to a viking:// file through OpenViking content/write. Use create for new durable notes or resources, append to extend an existing file, and replace only when the user explicitly wants to overwrite existing content.",
|
|
250
|
+
args: {
|
|
251
|
+
uri: z.string().describe("Complete viking:// file URI to write, e.g. viking://user/memories/project-notes.md or viking://resources/docs/api.md."),
|
|
252
|
+
content: z.string().describe("Text content to write."),
|
|
253
|
+
mode: z.enum(["create", "append", "replace"]).optional().describe("Write mode. Defaults to create to avoid accidental overwrite."),
|
|
254
|
+
wait: z.boolean().optional().describe("Whether OpenViking should wait for semantic/vector refresh."),
|
|
255
|
+
timeout: z.number().optional().describe("Timeout seconds when wait=true."),
|
|
256
|
+
},
|
|
257
|
+
async execute(args, context) {
|
|
258
|
+
const validationError = validateVikingUri(args.uri, "memwrite")
|
|
259
|
+
if (validationError) return validationError
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const body = {
|
|
263
|
+
uri: args.uri,
|
|
264
|
+
content: args.content,
|
|
265
|
+
mode: args.mode ?? "create",
|
|
266
|
+
wait: args.wait ?? false,
|
|
267
|
+
}
|
|
268
|
+
if (args.timeout !== undefined) body.timeout = args.timeout
|
|
269
|
+
const response = await makeRequest(config, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
endpoint: "/api/v1/content/write",
|
|
272
|
+
body,
|
|
273
|
+
abortSignal: context.abort,
|
|
274
|
+
actorPeerId,
|
|
275
|
+
})
|
|
276
|
+
return JSON.stringify({ write: unwrapResponse(response) }, null, 2)
|
|
277
|
+
} catch (error) {
|
|
278
|
+
log("ERROR", "memwrite", "Write failed", { error: error?.message, args: { ...args, content: `[${args.content?.length ?? 0} chars]` } })
|
|
279
|
+
return `Error: ${error.message}`
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
}),
|
|
283
|
+
|
|
284
|
+
memremove: tool({
|
|
285
|
+
description:
|
|
286
|
+
"Remove a viking:// resource. The user must explicitly confirm deletion before this tool is called. Set confirm=true, otherwise deletion is refused.",
|
|
287
|
+
args: {
|
|
288
|
+
uri: z.string().describe("Viking URI to remove."),
|
|
289
|
+
recursive: z.boolean().optional().describe("Recursively remove a directory."),
|
|
290
|
+
confirm: z.boolean().describe("Must be true after explicit user confirmation."),
|
|
291
|
+
},
|
|
292
|
+
async execute(args, context) {
|
|
293
|
+
if (!args.confirm) {
|
|
294
|
+
return "Error: Refusing to delete. Ask the user for explicit confirmation, then call memremove with confirm=true."
|
|
295
|
+
}
|
|
296
|
+
const validationError = validateVikingUri(args.uri, "memremove")
|
|
297
|
+
if (validationError) return validationError
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const response = await makeRequest(config, {
|
|
301
|
+
method: "DELETE",
|
|
302
|
+
endpoint: `/api/v1/fs?uri=${encodeURIComponent(args.uri)}&recursive=${args.recursive ? "true" : "false"}`,
|
|
303
|
+
abortSignal: context.abort,
|
|
304
|
+
actorPeerId,
|
|
305
|
+
})
|
|
306
|
+
return JSON.stringify(unwrapResponse(response), null, 2)
|
|
307
|
+
} catch (error) {
|
|
308
|
+
log("ERROR", "memremove", "Remove failed", { error: error?.message, args })
|
|
309
|
+
return `Error: ${error.message}`
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
}),
|
|
313
|
+
|
|
314
|
+
memqueue: tool({
|
|
315
|
+
description: "Return OpenViking observer queue status for embedding and semantic processing after resource indexing operations.",
|
|
316
|
+
args: {},
|
|
317
|
+
async execute(_args, context) {
|
|
318
|
+
try {
|
|
319
|
+
const queue = await getQueueStatus(config, context.abort)
|
|
320
|
+
return JSON.stringify(queue, null, 2)
|
|
321
|
+
} catch (error) {
|
|
322
|
+
log("ERROR", "memqueue", "Queue status failed", { error: error?.message })
|
|
323
|
+
return `Error: ${error.message}`
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function resolveReadLevel(config, uri, abortSignal, actorPeerId) {
|
|
331
|
+
try {
|
|
332
|
+
const statResponse = await makeRequest(config, {
|
|
333
|
+
method: "GET",
|
|
334
|
+
endpoint: `/api/v1/fs/stat?uri=${encodeURIComponent(uri)}`,
|
|
335
|
+
abortSignal,
|
|
336
|
+
actorPeerId,
|
|
337
|
+
})
|
|
338
|
+
return unwrapResponse(statResponse)?.isDir ? "overview" : "read"
|
|
339
|
+
} catch {
|
|
340
|
+
return "read"
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveSearchMode(requestedMode, query, sessionId) {
|
|
345
|
+
if (requestedMode === "fast" || requestedMode === "deep") return requestedMode
|
|
346
|
+
if (sessionId) return "deep"
|
|
347
|
+
const normalized = query.trim()
|
|
348
|
+
const wordCount = normalized ? normalized.split(/\s+/).length : 0
|
|
349
|
+
return normalized.includes("?") || normalized.length >= 80 || wordCount >= 8 ? "deep" : "fast"
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function formatSearchResults(result, query, extra) {
|
|
353
|
+
const memories = result?.memories ?? []
|
|
354
|
+
const resources = result?.resources ?? []
|
|
355
|
+
const skills = result?.skills ?? []
|
|
356
|
+
const allResults = [...memories, ...resources, ...skills]
|
|
357
|
+
if (allResults.length === 0) {
|
|
358
|
+
return "No results found matching the query."
|
|
359
|
+
}
|
|
360
|
+
return JSON.stringify(
|
|
361
|
+
{
|
|
362
|
+
total: result?.total ?? allResults.length,
|
|
363
|
+
memories,
|
|
364
|
+
resources,
|
|
365
|
+
skills,
|
|
366
|
+
query_plan: result?.query_plan,
|
|
367
|
+
query,
|
|
368
|
+
...extra,
|
|
369
|
+
},
|
|
370
|
+
null,
|
|
371
|
+
2,
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function formatCommitResult(sessionId, result) {
|
|
376
|
+
const task = result.task
|
|
377
|
+
const payload = task?.result ?? result.result ?? {}
|
|
378
|
+
const memoriesExtracted = totalMemoriesExtracted(payload.memories_extracted)
|
|
379
|
+
return JSON.stringify(
|
|
380
|
+
{
|
|
381
|
+
message: result.status === "accepted" ? "Commit is still processing in the background" : `Memory extraction complete: ${memoriesExtracted} memories extracted`,
|
|
382
|
+
session_id: payload.session_id ?? sessionId,
|
|
383
|
+
status: result.status,
|
|
384
|
+
memories_extracted: memoriesExtracted,
|
|
385
|
+
archived: payload.archived ?? false,
|
|
386
|
+
task_id: task?.task_id ?? result.task_id,
|
|
387
|
+
},
|
|
388
|
+
null,
|
|
389
|
+
2,
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function totalMemoriesExtracted(memories) {
|
|
394
|
+
if (typeof memories === "number") return memories
|
|
395
|
+
if (!memories || typeof memories !== "object") return 0
|
|
396
|
+
return Object.entries(memories).reduce((sum, [key, value]) => {
|
|
397
|
+
if (key === "total") return sum
|
|
398
|
+
return sum + (typeof value === "number" ? value : 0)
|
|
399
|
+
}, 0)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function getQueueStatus(config, abortSignal) {
|
|
403
|
+
const response = await makeRequest(config, {
|
|
404
|
+
method: "GET",
|
|
405
|
+
endpoint: "/api/v1/observer/queue",
|
|
406
|
+
abortSignal,
|
|
407
|
+
timeoutMs: 5000,
|
|
408
|
+
})
|
|
409
|
+
return unwrapResponse(response)
|
|
410
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { effectivePeerId, log, makeRequest, unwrapResponse } from "./utils.mjs"
|
|
2
|
+
|
|
3
|
+
export function createRepoContext({ config }) {
|
|
4
|
+
let cachedRepos = null
|
|
5
|
+
let lastFetchTime = 0
|
|
6
|
+
|
|
7
|
+
async function refreshRepos({ force = false } = {}) {
|
|
8
|
+
if (!config.repoContext?.enabled) return null
|
|
9
|
+
|
|
10
|
+
const now = Date.now()
|
|
11
|
+
const ttl = config.repoContext?.cacheTtlMs ?? 60000
|
|
12
|
+
if (!force && cachedRepos !== null && now - lastFetchTime < ttl) {
|
|
13
|
+
return cachedRepos
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const response = await makeRequest(config, {
|
|
18
|
+
method: "GET",
|
|
19
|
+
endpoint: `/api/v1/fs/ls?uri=${encodeURIComponent("viking://resources/")}&recursive=false&simple=false`,
|
|
20
|
+
timeoutMs: 8000,
|
|
21
|
+
actorPeerId: effectivePeerId(config),
|
|
22
|
+
})
|
|
23
|
+
const result = unwrapResponse(response)
|
|
24
|
+
const items = Array.isArray(result) ? result : []
|
|
25
|
+
const repos = items
|
|
26
|
+
.filter((item) => item?.uri?.startsWith("viking://resources/") && item.uri !== "viking://resources/")
|
|
27
|
+
.map(formatRepoLine)
|
|
28
|
+
|
|
29
|
+
cachedRepos = repos.length > 0 ? repos.join("\n") : ""
|
|
30
|
+
lastFetchTime = now
|
|
31
|
+
log("INFO", "repo-context", "Repo context refreshed", { count: repos.length })
|
|
32
|
+
return cachedRepos
|
|
33
|
+
} catch (error) {
|
|
34
|
+
log("WARN", "repo-context", "Failed to refresh indexed repositories", { error: error?.message })
|
|
35
|
+
return cachedRepos
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getRepoSystemPrompt() {
|
|
40
|
+
if (!config.repoContext?.enabled || !cachedRepos) return null
|
|
41
|
+
return [
|
|
42
|
+
"## OpenViking - Indexed Code Repositories",
|
|
43
|
+
"",
|
|
44
|
+
"The following external repositories are indexed in OpenViking and searchable through tools.",
|
|
45
|
+
"When the user asks about these projects or their internals, use the OpenViking tools before answering.",
|
|
46
|
+
"",
|
|
47
|
+
"Tool guidance:",
|
|
48
|
+
"- Use `memsearch` for semantic or conceptual repository questions.",
|
|
49
|
+
"- Use `memgrep` for exact symbols, error strings, class names, function names, and regex-like searches.",
|
|
50
|
+
"- Use `memglob` to enumerate files by pattern.",
|
|
51
|
+
"- Use `membrowse` to inspect directory structure and `memread` to read specific URIs.",
|
|
52
|
+
"- Use `memadd`, `memremove`, and `memqueue` for repository resource management when explicitly requested.",
|
|
53
|
+
"",
|
|
54
|
+
cachedRepos,
|
|
55
|
+
].join("\n")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
refreshRepos,
|
|
60
|
+
getRepoSystemPrompt,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatRepoLine(item) {
|
|
65
|
+
const name = item.uri.replace("viking://resources/", "").replace(/\/$/, "") || "resources"
|
|
66
|
+
const abstract = item.abstract || item.overview
|
|
67
|
+
return abstract ? `- **${name}** (${item.uri})\n ${abstract}` : `- **${name}** (${item.uri})`
|
|
68
|
+
}
|
package/lib/runtime.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { log, makeToast, normalizeEndpoint } from "./utils.mjs"
|
|
2
|
+
|
|
3
|
+
export async function checkServiceHealth(config, timeoutMs = 3000) {
|
|
4
|
+
const controller = new AbortController()
|
|
5
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
6
|
+
try {
|
|
7
|
+
const response = await fetch(`${normalizeEndpoint(config.endpoint)}/health`, {
|
|
8
|
+
method: "GET",
|
|
9
|
+
signal: controller.signal,
|
|
10
|
+
})
|
|
11
|
+
return response.ok
|
|
12
|
+
} catch (error) {
|
|
13
|
+
log("WARN", "health", "OpenViking health check failed", {
|
|
14
|
+
endpoint: config.endpoint,
|
|
15
|
+
error: error?.message,
|
|
16
|
+
})
|
|
17
|
+
return false
|
|
18
|
+
} finally {
|
|
19
|
+
clearTimeout(timeout)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function initializeRuntime(config, client) {
|
|
24
|
+
const toast = makeToast(client)
|
|
25
|
+
|
|
26
|
+
if (await checkServiceHealth(config)) {
|
|
27
|
+
log("INFO", "runtime", "OpenViking service is healthy", { endpoint: config.endpoint })
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await toast(`OpenViking service is not reachable at ${config.endpoint}. Start openviking-server before using memory tools.`, "warning")
|
|
32
|
+
return false
|
|
33
|
+
}
|