@openviking/opencode-plugin 0.1.6 → 0.2.0
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 +50 -49
- package/INSTALL.md +50 -49
- package/README.md +40 -123
- package/index.mjs +20 -12
- package/lib/config.mjs +272 -0
- package/lib/mcp-config.mjs +21 -0
- package/lib/memory-recall.mjs +21 -172
- package/lib/memory-session.mjs +301 -478
- package/lib/repo-context.mjs +5 -5
- package/lib/session-inject.mjs +71 -0
- package/lib/shared/capture-utils.mjs +500 -0
- package/lib/shared/credentials.mjs +207 -0
- package/lib/shared/debug-log.mjs +58 -0
- package/lib/shared/mcp-proxy-core.mjs +451 -0
- package/lib/shared/pending-queue.mjs +439 -0
- package/lib/shared/profile-inject.mjs +294 -0
- package/lib/shared/recall-core.mjs +282 -0
- package/lib/shared/session-model.mjs +57 -0
- package/lib/shared/setup-wizard.mjs +111 -0
- package/lib/shared/uri-guard.mjs +56 -0
- package/lib/utils.mjs +33 -116
- package/lib/viking-uri-guard.mjs +9 -24
- package/package.json +8 -7
- package/scripts/setup.mjs +8 -0
- package/servers/mcp-proxy.mjs +67 -0
- package/servers/mcp-proxy.test.mjs +100 -0
- package/lib/code-tools.mjs +0 -134
- package/lib/memadd-local.mjs +0 -118
- package/lib/memory-tools.mjs +0 -410
package/lib/code-tools.mjs
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin"
|
|
2
|
-
import { effectivePeerId, log, makeRequest, unwrapResponse, validateVikingUri } from "./utils.mjs"
|
|
3
|
-
|
|
4
|
-
const z = tool.schema
|
|
5
|
-
|
|
6
|
-
const CODE_TOOL_ENDPOINTS = {
|
|
7
|
-
search: "/api/v1/code/search",
|
|
8
|
-
outline: "/api/v1/code/outline",
|
|
9
|
-
expand: "/api/v1/code/expand",
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function codeToolRequestOptions(kind, { uri, query, symbol, actorPeerId, abortSignal }) {
|
|
13
|
-
const body = { uri }
|
|
14
|
-
if (kind === "search") body.query = query
|
|
15
|
-
if (kind === "expand") body.symbol = symbol
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
method: "POST",
|
|
19
|
-
endpoint: CODE_TOOL_ENDPOINTS[kind],
|
|
20
|
-
body,
|
|
21
|
-
abortSignal,
|
|
22
|
-
actorPeerId,
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function createCodeTools({ config }) {
|
|
27
|
-
const actorPeerId = effectivePeerId(config)
|
|
28
|
-
return {
|
|
29
|
-
codesearch: tool({
|
|
30
|
-
description:
|
|
31
|
-
"Search AST-supported symbol names (classes, functions, methods) by substring across a confirmed viking:// code repository or source subtree. " +
|
|
32
|
-
"Use only after you have evidence that the uri contains supported source files. " +
|
|
33
|
-
"Use when you do not know which file contains a symbol. " +
|
|
34
|
-
"Do not use for general memory search, documentation-only resources, plain text notes, chat/session history, or local filesystem paths. " +
|
|
35
|
-
"Returns structured results: symbol name, class context, file URI, line range. " +
|
|
36
|
-
"Typical workflow: verify code repo with ls/glob/add_resource output, then codesearch, codeoutline, codeexpand.",
|
|
37
|
-
args: {
|
|
38
|
-
query: z.string().describe("Symbol name substring to search for (case-insensitive)."),
|
|
39
|
-
uri: z
|
|
40
|
-
.string()
|
|
41
|
-
.describe(
|
|
42
|
-
"Viking URI for a confirmed ingested code repository or source subtree. Do not pass a local path or an unverified viking:// directory.",
|
|
43
|
-
),
|
|
44
|
-
},
|
|
45
|
-
async execute(args, context) {
|
|
46
|
-
const validationError = validateVikingUri(args.uri, "codesearch")
|
|
47
|
-
if (validationError) return validationError
|
|
48
|
-
try {
|
|
49
|
-
const response = await makeRequest(config, {
|
|
50
|
-
...codeToolRequestOptions("search", {
|
|
51
|
-
uri: args.uri,
|
|
52
|
-
query: args.query,
|
|
53
|
-
actorPeerId,
|
|
54
|
-
abortSignal: context.abort,
|
|
55
|
-
}),
|
|
56
|
-
})
|
|
57
|
-
return unwrapResponse(response)
|
|
58
|
-
} catch (error) {
|
|
59
|
-
log("ERROR", "codesearch", "Search failed", { error: error?.message, args })
|
|
60
|
-
return `Error: ${error.message}`
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
}),
|
|
64
|
-
|
|
65
|
-
codeoutline: tool({
|
|
66
|
-
description:
|
|
67
|
-
"Show the symbol structure of a confirmed viking:// source file: classes, functions, methods, and line spans. " +
|
|
68
|
-
"Use only for source files inside an ingested code repository, after you know the exact viking:// file URI. " +
|
|
69
|
-
"Do not use on directories, documentation-only files, plain text notes, or files that are not supported source code. " +
|
|
70
|
-
"Use read instead when you need the full file content.",
|
|
71
|
-
args: {
|
|
72
|
-
uri: z
|
|
73
|
-
.string()
|
|
74
|
-
.describe(
|
|
75
|
-
"Viking URI of a confirmed supported source file, e.g. viking://resources/myproject/src/main.py.",
|
|
76
|
-
),
|
|
77
|
-
},
|
|
78
|
-
async execute(args, context) {
|
|
79
|
-
const validationError = validateVikingUri(args.uri, "codeoutline")
|
|
80
|
-
if (validationError) return validationError
|
|
81
|
-
try {
|
|
82
|
-
const response = await makeRequest(config, {
|
|
83
|
-
...codeToolRequestOptions("outline", {
|
|
84
|
-
uri: args.uri,
|
|
85
|
-
actorPeerId,
|
|
86
|
-
abortSignal: context.abort,
|
|
87
|
-
}),
|
|
88
|
-
})
|
|
89
|
-
return unwrapResponse(response)
|
|
90
|
-
} catch (error) {
|
|
91
|
-
log("ERROR", "codeoutline", "Outline failed", { error: error?.message, uri: args.uri })
|
|
92
|
-
return `Error: ${error.message}`
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
}),
|
|
96
|
-
|
|
97
|
-
codeexpand: tool({
|
|
98
|
-
description:
|
|
99
|
-
"Return the full source of one named symbol from a confirmed viking:// source file. " +
|
|
100
|
-
"Use only after codeoutline or other evidence shows the symbol exists in that file. " +
|
|
101
|
-
"Do not use for broad exploration, non-code files, documentation, chat/session history, or unverified viking:// resources. " +
|
|
102
|
-
"Accepts 'bar' (top-level function or class) or 'Foo.bar' (method inside class Foo). " +
|
|
103
|
-
"For multiple symbols from the same file, read may be more efficient.",
|
|
104
|
-
args: {
|
|
105
|
-
uri: z
|
|
106
|
-
.string()
|
|
107
|
-
.describe("Viking URI of the confirmed supported source file containing the symbol."),
|
|
108
|
-
symbol: z
|
|
109
|
-
.string()
|
|
110
|
-
.describe(
|
|
111
|
-
"Symbol name: 'foo' for top-level, 'Foo.bar' for a method inside class Foo.",
|
|
112
|
-
),
|
|
113
|
-
},
|
|
114
|
-
async execute(args, context) {
|
|
115
|
-
const validationError = validateVikingUri(args.uri, "codeexpand")
|
|
116
|
-
if (validationError) return validationError
|
|
117
|
-
try {
|
|
118
|
-
const response = await makeRequest(config, {
|
|
119
|
-
...codeToolRequestOptions("expand", {
|
|
120
|
-
uri: args.uri,
|
|
121
|
-
symbol: args.symbol,
|
|
122
|
-
actorPeerId,
|
|
123
|
-
abortSignal: context.abort,
|
|
124
|
-
}),
|
|
125
|
-
})
|
|
126
|
-
return unwrapResponse(response)
|
|
127
|
-
} catch (error) {
|
|
128
|
-
log("ERROR", "codeexpand", "Expand failed", { error: error?.message, args })
|
|
129
|
-
return `Error: ${error.message}`
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
}),
|
|
133
|
-
}
|
|
134
|
-
}
|
package/lib/memadd-local.mjs
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import fs from "fs"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import { fileURLToPath } from "url"
|
|
4
|
-
import {
|
|
5
|
-
ensureRemoteUrl,
|
|
6
|
-
makeMultipartRequest,
|
|
7
|
-
makeRequest,
|
|
8
|
-
unwrapResponse,
|
|
9
|
-
} from "./utils.mjs"
|
|
10
|
-
|
|
11
|
-
export const MEMADD_LOCAL_FILE_ONLY_ERROR = "Error: memadd local upload currently supports files only."
|
|
12
|
-
|
|
13
|
-
const ADD_RESOURCE_KEYS = [
|
|
14
|
-
"to",
|
|
15
|
-
"parent",
|
|
16
|
-
"reason",
|
|
17
|
-
"instruction",
|
|
18
|
-
"wait",
|
|
19
|
-
"timeout",
|
|
20
|
-
"watch_interval",
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
export function resolveMemaddSource(inputPath, projectDirectory = process.cwd()) {
|
|
24
|
-
if (ensureRemoteUrl(inputPath)) {
|
|
25
|
-
return { kind: "remote", path: inputPath }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let filePath
|
|
29
|
-
try {
|
|
30
|
-
filePath = resolveLocalPath(inputPath, projectDirectory)
|
|
31
|
-
} catch (error) {
|
|
32
|
-
return { kind: "error", error: `Error: ${error.message}` }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let stat
|
|
36
|
-
try {
|
|
37
|
-
stat = fs.statSync(filePath)
|
|
38
|
-
} catch (error) {
|
|
39
|
-
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
|
|
40
|
-
return { kind: "error", error: `Error: Local file not found: ${filePath}` }
|
|
41
|
-
}
|
|
42
|
-
return { kind: "error", error: `Error: Unable to access local file: ${filePath}: ${error.message}` }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!stat.isFile()) {
|
|
46
|
-
return { kind: "error", error: MEMADD_LOCAL_FILE_ONLY_ERROR }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return { kind: "local", path: filePath, filename: path.basename(filePath) }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function resolveLocalPath(inputPath, projectDirectory = process.cwd()) {
|
|
53
|
-
if (typeof inputPath !== "string" || inputPath.trim() === "") {
|
|
54
|
-
throw new Error("memadd path is required.")
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let localPath = inputPath
|
|
58
|
-
if (isFileUrl(inputPath)) {
|
|
59
|
-
localPath = fileURLToPath(inputPath)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (path.isAbsolute(localPath)) return path.normalize(localPath)
|
|
63
|
-
return path.resolve(projectDirectory || process.cwd(), localPath)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function buildAddResourceBody(args, source, tempFileId) {
|
|
67
|
-
const body = source.kind === "remote" ? { path: source.path } : { temp_file_id: tempFileId }
|
|
68
|
-
for (const key of ADD_RESOURCE_KEYS) {
|
|
69
|
-
if (args[key] !== undefined) body[key] = args[key]
|
|
70
|
-
}
|
|
71
|
-
return body
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function uploadLocalResource(config, source, abortSignal, actorPeerId) {
|
|
75
|
-
const bytes = await fs.promises.readFile(source.path)
|
|
76
|
-
const form = new FormData()
|
|
77
|
-
form.append("file", new Blob([bytes], { type: "application/octet-stream" }), source.filename)
|
|
78
|
-
|
|
79
|
-
const uploadResponse = await makeMultipartRequest(config, {
|
|
80
|
-
method: "POST",
|
|
81
|
-
endpoint: "/api/v1/resources/temp_upload",
|
|
82
|
-
body: form,
|
|
83
|
-
abortSignal,
|
|
84
|
-
actorPeerId,
|
|
85
|
-
})
|
|
86
|
-
const tempFileId = unwrapResponse(uploadResponse)?.temp_file_id
|
|
87
|
-
if (!tempFileId) {
|
|
88
|
-
throw new Error("OpenViking temp upload did not return temp_file_id")
|
|
89
|
-
}
|
|
90
|
-
return tempFileId
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function addMemaddResource(config, args, projectDirectory, abortSignal, actorPeerId) {
|
|
94
|
-
const source = resolveMemaddSource(args.path, projectDirectory)
|
|
95
|
-
if (source.kind === "error") return { error: source.error }
|
|
96
|
-
|
|
97
|
-
const tempFileId = source.kind === "local"
|
|
98
|
-
? await uploadLocalResource(config, source, abortSignal, actorPeerId)
|
|
99
|
-
: undefined
|
|
100
|
-
const body = buildAddResourceBody(args, source, tempFileId)
|
|
101
|
-
const addResponse = await makeRequest(config, {
|
|
102
|
-
method: "POST",
|
|
103
|
-
endpoint: "/api/v1/resources",
|
|
104
|
-
body,
|
|
105
|
-
abortSignal,
|
|
106
|
-
timeoutMs: args.wait ? Math.max(config.timeoutMs, (args.timeout ?? 300) * 1000) : config.timeoutMs,
|
|
107
|
-
actorPeerId,
|
|
108
|
-
})
|
|
109
|
-
return { addResponse, source }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function isFileUrl(value) {
|
|
113
|
-
try {
|
|
114
|
-
return new URL(value).protocol === "file:"
|
|
115
|
-
} catch {
|
|
116
|
-
return false
|
|
117
|
-
}
|
|
118
|
-
}
|
package/lib/memory-tools.mjs
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
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
|
-
}
|