@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
package/lib/utils.mjs
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { homedir } from "os"
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_CONFIG = {
|
|
6
|
+
endpoint: "http://localhost:1933",
|
|
7
|
+
apiKey: "",
|
|
8
|
+
account: "",
|
|
9
|
+
user: "",
|
|
10
|
+
peerId: "",
|
|
11
|
+
enabled: true,
|
|
12
|
+
timeoutMs: 30000,
|
|
13
|
+
runtime: {
|
|
14
|
+
dataDir: "",
|
|
15
|
+
},
|
|
16
|
+
repoContext: {
|
|
17
|
+
enabled: true,
|
|
18
|
+
cacheTtlMs: 60000,
|
|
19
|
+
},
|
|
20
|
+
autoRecall: {
|
|
21
|
+
enabled: true,
|
|
22
|
+
limit: 6,
|
|
23
|
+
scoreThreshold: 0.15,
|
|
24
|
+
maxContentChars: 500,
|
|
25
|
+
preferAbstract: true,
|
|
26
|
+
tokenBudget: 2000,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let logFilePath = null
|
|
31
|
+
|
|
32
|
+
function cloneDefaultConfig() {
|
|
33
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mergeConfig(fileConfig = {}) {
|
|
37
|
+
const config = cloneDefaultConfig()
|
|
38
|
+
for (const key of ["endpoint", "apiKey", "account", "user", "peerId", "enabled", "timeoutMs"]) {
|
|
39
|
+
if (fileConfig[key] !== undefined) config[key] = fileConfig[key]
|
|
40
|
+
}
|
|
41
|
+
config.runtime = {
|
|
42
|
+
...DEFAULT_CONFIG.runtime,
|
|
43
|
+
dataDir: fileConfig.runtime?.dataDir ?? DEFAULT_CONFIG.runtime.dataDir,
|
|
44
|
+
}
|
|
45
|
+
config.repoContext = { ...DEFAULT_CONFIG.repoContext, ...(fileConfig.repoContext ?? {}) }
|
|
46
|
+
config.autoRecall = { ...DEFAULT_CONFIG.autoRecall, ...(fileConfig.autoRecall ?? {}) }
|
|
47
|
+
|
|
48
|
+
if (process.env.OPENVIKING_API_KEY) {
|
|
49
|
+
config.apiKey = process.env.OPENVIKING_API_KEY
|
|
50
|
+
}
|
|
51
|
+
if (process.env.OPENVIKING_ACCOUNT) {
|
|
52
|
+
config.account = process.env.OPENVIKING_ACCOUNT
|
|
53
|
+
}
|
|
54
|
+
if (process.env.OPENVIKING_USER) {
|
|
55
|
+
config.user = process.env.OPENVIKING_USER
|
|
56
|
+
}
|
|
57
|
+
if (process.env.OPENVIKING_PEER_ID) {
|
|
58
|
+
config.peerId = process.env.OPENVIKING_PEER_ID
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
config.timeoutMs = normalizeNumber(config.timeoutMs, DEFAULT_CONFIG.timeoutMs, 1000, 300000)
|
|
62
|
+
config.repoContext.cacheTtlMs = normalizeNumber(
|
|
63
|
+
config.repoContext.cacheTtlMs,
|
|
64
|
+
DEFAULT_CONFIG.repoContext.cacheTtlMs,
|
|
65
|
+
1000,
|
|
66
|
+
60 * 60 * 1000,
|
|
67
|
+
)
|
|
68
|
+
clampRecallConfig(config.autoRecall)
|
|
69
|
+
return config
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeNumber(value, fallback, min, max) {
|
|
73
|
+
const next = Number(value)
|
|
74
|
+
if (!Number.isFinite(next)) return fallback
|
|
75
|
+
return Math.max(min, Math.min(max, next))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function clampRecallConfig(recall) {
|
|
79
|
+
recall.limit = Math.max(1, Math.min(50, Math.round(Number(recall.limit) || 6)))
|
|
80
|
+
recall.scoreThreshold = Math.max(0, Math.min(1, Number(recall.scoreThreshold) || 0))
|
|
81
|
+
recall.maxContentChars = Math.max(100, Math.min(5000, Math.round(Number(recall.maxContentChars) || 500)))
|
|
82
|
+
recall.tokenBudget = Math.max(100, Math.min(10000, Math.round(Number(recall.tokenBudget) || 2000)))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function loadConfig(pluginRoot, projectDirectory) {
|
|
86
|
+
for (const configPath of getConfigPaths(pluginRoot, projectDirectory)) {
|
|
87
|
+
try {
|
|
88
|
+
if (fs.existsSync(configPath)) {
|
|
89
|
+
const fileConfig = JSON.parse(fs.readFileSync(configPath, "utf8"))
|
|
90
|
+
return mergeConfig(fileConfig)
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn(`Failed to load OpenViking config from ${configPath}:`, error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return mergeConfig()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getConfigPaths(pluginRoot, projectDirectory) {
|
|
100
|
+
const paths = []
|
|
101
|
+
if (process.env.OPENVIKING_PLUGIN_CONFIG) paths.push(expandHome(process.env.OPENVIKING_PLUGIN_CONFIG))
|
|
102
|
+
if (projectDirectory) paths.push(path.join(projectDirectory, ".opencode", "openviking-config.json"))
|
|
103
|
+
paths.push(path.join(homedir(), ".config", "opencode", "openviking-config.json"))
|
|
104
|
+
paths.push(path.join(pluginRoot, "openviking-config.json"))
|
|
105
|
+
return paths
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveDataDir(pluginRoot, config) {
|
|
109
|
+
const configured = config.runtime?.dataDir
|
|
110
|
+
if (configured) return expandHome(configured)
|
|
111
|
+
return path.join(homedir(), ".config", "opencode", "openviking")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function expandHome(value) {
|
|
115
|
+
if (!value || typeof value !== "string") return value
|
|
116
|
+
if (value === "~") return homedir()
|
|
117
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(homedir(), value.slice(2))
|
|
118
|
+
return value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function initLogger(dataDir) {
|
|
122
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
123
|
+
logFilePath = path.join(dataDir, "openviking-memory.log")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function safeStringify(value) {
|
|
127
|
+
if (value === null || value === undefined) return value
|
|
128
|
+
if (typeof value !== "object") return value
|
|
129
|
+
if (Array.isArray(value)) return value.map((item) => safeStringify(item))
|
|
130
|
+
|
|
131
|
+
const result = {}
|
|
132
|
+
for (const key of Object.keys(value)) {
|
|
133
|
+
const item = value[key]
|
|
134
|
+
if (typeof item === "function") {
|
|
135
|
+
result[key] = "[Function]"
|
|
136
|
+
} else if (typeof item === "object" && item !== null) {
|
|
137
|
+
try {
|
|
138
|
+
result[key] = safeStringify(item)
|
|
139
|
+
} catch {
|
|
140
|
+
result[key] = "[Circular or Non-serializable]"
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
result[key] = item
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function log(level, toolName, message, data) {
|
|
150
|
+
const normalizedLevel = String(level || "INFO").toUpperCase()
|
|
151
|
+
const entry = {
|
|
152
|
+
timestamp: new Date().toISOString(),
|
|
153
|
+
level: normalizedLevel,
|
|
154
|
+
tool: toolName,
|
|
155
|
+
message,
|
|
156
|
+
...(data ? { data: safeStringify(data) } : {}),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!logFilePath) {
|
|
160
|
+
if (normalizedLevel === "ERROR") console.error(message, data ?? "")
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
fs.appendFileSync(logFilePath, `${JSON.stringify(entry)}\n`, "utf8")
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error("Failed to write OpenViking plugin log:", error)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function makeToast(client) {
|
|
172
|
+
return (message, variant = "warning") =>
|
|
173
|
+
client?.tui?.showToast?.({
|
|
174
|
+
body: { title: "OpenViking", message, variant, duration: 8000 },
|
|
175
|
+
}).catch(() => {})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function normalizeEndpoint(endpoint) {
|
|
179
|
+
return endpoint.replace(/\/+$/, "")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function effectivePeerId(config) {
|
|
183
|
+
return String(config.peerId || "").trim() || null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function makeRequest(config, options) {
|
|
187
|
+
const url = `${normalizeEndpoint(config.endpoint)}${options.endpoint}`
|
|
188
|
+
const headers = makeAuthHeaders(
|
|
189
|
+
config,
|
|
190
|
+
{ "Content-Type": "application/json", ...(options.headers ?? {}) },
|
|
191
|
+
options.actorPeerId,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const controller = new AbortController()
|
|
195
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? config.timeoutMs)
|
|
196
|
+
let onAbort = null
|
|
197
|
+
|
|
198
|
+
if (options.abortSignal) {
|
|
199
|
+
if (options.abortSignal.aborted) controller.abort()
|
|
200
|
+
onAbort = () => controller.abort()
|
|
201
|
+
options.abortSignal.addEventListener("abort", onAbort, { once: true })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const response = await fetch(url, {
|
|
206
|
+
method: options.method,
|
|
207
|
+
headers,
|
|
208
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
209
|
+
signal: controller.signal,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const text = await response.text()
|
|
213
|
+
const payload = text ? parseJsonOrText(text) : {}
|
|
214
|
+
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
const rawError = typeof payload === "object" ? payload.error ?? payload.message : payload
|
|
217
|
+
const errorMessage = typeof rawError === "string" ? rawError : JSON.stringify(rawError)
|
|
218
|
+
if (response.status === 401 || response.status === 403) {
|
|
219
|
+
throw new Error("Authentication failed. Please check apiKey/account/user in openviking-config.json or OPENVIKING_* environment variables.")
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`Request failed (${response.status}): ${errorMessage}`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return payload
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error?.name === "AbortError") {
|
|
227
|
+
throw new Error(`Request timeout after ${options.timeoutMs ?? config.timeoutMs}ms`)
|
|
228
|
+
}
|
|
229
|
+
if (error?.message?.includes("fetch failed") || error?.code === "ECONNREFUSED") {
|
|
230
|
+
throw new Error(`OpenViking service unavailable at ${config.endpoint}. Start it with: openviking-server --config ~/.openviking/ov.conf`)
|
|
231
|
+
}
|
|
232
|
+
throw error
|
|
233
|
+
} finally {
|
|
234
|
+
clearTimeout(timeout)
|
|
235
|
+
if (options.abortSignal && onAbort) {
|
|
236
|
+
options.abortSignal.removeEventListener("abort", onAbort)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function makeMultipartRequest(config, options) {
|
|
242
|
+
const url = `${normalizeEndpoint(config.endpoint)}${options.endpoint}`
|
|
243
|
+
const headers = makeAuthHeaders(config, options.headers ?? {}, options.actorPeerId)
|
|
244
|
+
|
|
245
|
+
const controller = new AbortController()
|
|
246
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? config.timeoutMs)
|
|
247
|
+
let onAbort = null
|
|
248
|
+
|
|
249
|
+
if (options.abortSignal) {
|
|
250
|
+
if (options.abortSignal.aborted) controller.abort()
|
|
251
|
+
onAbort = () => controller.abort()
|
|
252
|
+
options.abortSignal.addEventListener("abort", onAbort, { once: true })
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const response = await fetch(url, {
|
|
257
|
+
method: options.method,
|
|
258
|
+
headers,
|
|
259
|
+
body: options.body,
|
|
260
|
+
signal: controller.signal,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const text = await response.text()
|
|
264
|
+
const payload = text ? parseJsonOrText(text) : {}
|
|
265
|
+
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
const rawError = typeof payload === "object" ? payload.error ?? payload.message : payload
|
|
268
|
+
const errorMessage = typeof rawError === "string" ? rawError : JSON.stringify(rawError)
|
|
269
|
+
if (response.status === 401 || response.status === 403) {
|
|
270
|
+
throw new Error("Authentication failed. Please check apiKey/account/user in openviking-config.json or OPENVIKING_* environment variables.")
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`Request failed (${response.status}): ${errorMessage}`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return payload
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error?.name === "AbortError") {
|
|
278
|
+
throw new Error(`Request timeout after ${options.timeoutMs ?? config.timeoutMs}ms`)
|
|
279
|
+
}
|
|
280
|
+
if (error?.message?.includes("fetch failed") || error?.code === "ECONNREFUSED") {
|
|
281
|
+
throw new Error(`OpenViking service unavailable at ${config.endpoint}. Start it with: openviking-server --config ~/.openviking/ov.conf`)
|
|
282
|
+
}
|
|
283
|
+
throw error
|
|
284
|
+
} finally {
|
|
285
|
+
clearTimeout(timeout)
|
|
286
|
+
if (options.abortSignal && onAbort) {
|
|
287
|
+
options.abortSignal.removeEventListener("abort", onAbort)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function makeAuthHeaders(config, headers = {}, actorPeerId = "") {
|
|
293
|
+
const result = { ...headers }
|
|
294
|
+
if (config.apiKey) result["X-API-Key"] = config.apiKey
|
|
295
|
+
if (config.account) result["X-OpenViking-Account"] = config.account
|
|
296
|
+
if (config.user) result["X-OpenViking-User"] = config.user
|
|
297
|
+
const peerId = String(actorPeerId || "").trim()
|
|
298
|
+
if (peerId) result["X-OpenViking-Actor-Peer"] = peerId
|
|
299
|
+
return result
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function parseJsonOrText(text) {
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(text)
|
|
305
|
+
} catch {
|
|
306
|
+
return text
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function getResponseErrorMessage(error) {
|
|
311
|
+
if (!error) return "Unknown OpenViking error"
|
|
312
|
+
if (typeof error === "string") return error
|
|
313
|
+
return error.message || error.code || "Unknown OpenViking error"
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function unwrapResponse(response) {
|
|
317
|
+
if (!response || typeof response !== "object") {
|
|
318
|
+
throw new Error("OpenViking returned an invalid response")
|
|
319
|
+
}
|
|
320
|
+
if (response.status && response.status !== "ok") {
|
|
321
|
+
throw new Error(getResponseErrorMessage(response.error))
|
|
322
|
+
}
|
|
323
|
+
return response.result
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function validateVikingUri(uri, toolName = "tool") {
|
|
327
|
+
if (typeof uri !== "string" || !uri.startsWith("viking://")) {
|
|
328
|
+
log("ERROR", toolName, "Invalid Viking URI", { uri })
|
|
329
|
+
return 'Error: Invalid URI format. Must start with "viking://".'
|
|
330
|
+
}
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function ensureRemoteUrl(value) {
|
|
335
|
+
try {
|
|
336
|
+
const url = new URL(value)
|
|
337
|
+
return url.protocol === "http:" || url.protocol === "https:"
|
|
338
|
+
} catch {
|
|
339
|
+
return false
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { log } from "./utils.mjs"
|
|
2
|
+
|
|
3
|
+
const FILESYSTEM_TOOL_HINTS = {
|
|
4
|
+
read: {
|
|
5
|
+
replacement: "memread",
|
|
6
|
+
example: (uri) => `memread(uri="${uri}", level="auto")`,
|
|
7
|
+
},
|
|
8
|
+
glob: {
|
|
9
|
+
replacement: "membrowse",
|
|
10
|
+
example: (uri) => `membrowse(uri="${uri}", view="list", recursive=true)`,
|
|
11
|
+
},
|
|
12
|
+
grep: {
|
|
13
|
+
replacement: "memsearch",
|
|
14
|
+
example: (uri, args) => `memsearch(query="${String(args.pattern ?? "").replaceAll('"', '\\"')}", target_uri="${uri}")`,
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createVikingUriGuard() {
|
|
19
|
+
return async (input, output) => {
|
|
20
|
+
const toolName = normalizeToolName(input?.tool ?? input?.name)
|
|
21
|
+
const hint = FILESYSTEM_TOOL_HINTS[toolName]
|
|
22
|
+
if (!hint) return
|
|
23
|
+
|
|
24
|
+
const args = output?.args ?? input?.args ?? {}
|
|
25
|
+
const uri = findVikingUri(args)
|
|
26
|
+
if (!uri) return
|
|
27
|
+
|
|
28
|
+
log("INFO", "viking-uri-guard", "Blocked filesystem tool for viking URI", {
|
|
29
|
+
tool: toolName,
|
|
30
|
+
uri,
|
|
31
|
+
})
|
|
32
|
+
throw new Error(
|
|
33
|
+
[
|
|
34
|
+
"viking:// URIs are OpenViking virtual paths, not local filesystem paths.",
|
|
35
|
+
`Use ${hint.replacement} instead.`,
|
|
36
|
+
`Example: ${hint.example(uri, args)}`,
|
|
37
|
+
].join("\n"),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function findVikingUri(args = {}) {
|
|
43
|
+
for (const key of ["filePath", "path", "uri"]) {
|
|
44
|
+
const value = args[key]
|
|
45
|
+
if (typeof value === "string" && value.startsWith("viking://")) return value
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeToolName(value) {
|
|
51
|
+
return String(value || "").trim().toLowerCase()
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openviking/opencode-plugin",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"author": "tanyouqing",
|
|
5
|
+
"description": "Unified OpenCode plugin for OpenViking repository retrieval and long-term memory",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.mjs",
|
|
13
|
+
"lib/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"INSTALL.md",
|
|
16
|
+
"INSTALL-ZH.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"check": "node --check index.mjs && node --check lib/runtime.mjs && node --check lib/repo-context.mjs && node --check lib/memory-session.mjs && node --check lib/memadd-local.mjs && node --check lib/memory-tools.mjs && node --check lib/code-tools.mjs && node --check lib/memory-recall.mjs && node --check lib/viking-uri-guard.mjs && node --check lib/utils.mjs && node --check tests/*.test.mjs",
|
|
20
|
+
"test": "node --test tests/*.test.mjs"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@opencode-ai/plugin": "^1.14.37"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/volcengine/OpenViking.git",
|
|
31
|
+
"directory": "examples/opencode-plugin"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"opencode",
|
|
35
|
+
"opencode-plugin",
|
|
36
|
+
"openviking",
|
|
37
|
+
"memory",
|
|
38
|
+
"rag",
|
|
39
|
+
"code-search"
|
|
40
|
+
],
|
|
41
|
+
"license": "Apache-2.0"
|
|
42
|
+
}
|