@openviking/opencode-plugin 2026.9.10 → 2026.9.21-1
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 +42 -28
- package/INSTALL.md +41 -27
- package/README.md +47 -33
- package/index.mjs +5 -9
- package/lib/config.mjs +30 -307
- package/lib/memory-recall.mjs +2 -2
- package/lib/memory-session.mjs +6 -5
- package/lib/repo-context.mjs +2 -3
- package/lib/runtime.mjs +12 -19
- package/lib/shared/capture-utils.mjs +77 -1
- package/lib/shared/config-schema.mjs +359 -0
- package/lib/shared/credentials.mjs +238 -102
- package/lib/shared/mcp-proxy-config.mjs +154 -2
- package/lib/shared/mcp-proxy-core.mjs +23 -15
- package/lib/shared/ov-http.mjs +112 -0
- package/lib/shared/plugin-config.mjs +300 -0
- package/lib/shared/recall-core.mjs +66 -13
- package/lib/shared/uri-guard.mjs +173 -1
- package/lib/shared/workspace-config.mjs +433 -0
- package/lib/shared/workspace-peer.mjs +63 -0
- package/lib/shared/workspace-registry.mjs +87 -0
- package/lib/utils.mjs +34 -177
- package/lib/viking-uri-guard.mjs +23 -8
- package/package.json +4 -3
- package/servers/mcp-proxy.mjs +7 -27
- package/servers/mcp-proxy.test.mjs +0 -102
package/lib/config.mjs
CHANGED
|
@@ -1,84 +1,20 @@
|
|
|
1
|
-
import fs from "fs"
|
|
2
1
|
import path from "path"
|
|
3
2
|
import { homedir } from "os"
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
recallPeerScope: "all",
|
|
20
|
-
// Server-side query expansion costs a model call before retrieval starts, so
|
|
21
|
-
// it has to be switchable from the client that pays the latency.
|
|
22
|
-
recallQueryExpansion: "auto",
|
|
23
|
-
enabled: true,
|
|
24
|
-
mcp: {
|
|
25
|
-
enabled: true,
|
|
26
|
-
},
|
|
27
|
-
timeoutMs: 30000,
|
|
28
|
-
runtime: {
|
|
29
|
-
dataDir: "",
|
|
30
|
-
},
|
|
31
|
-
repoContext: {
|
|
32
|
-
enabled: true,
|
|
33
|
-
cacheTtlMs: 60000,
|
|
34
|
-
},
|
|
35
|
-
autoRecall: {
|
|
36
|
-
enabled: true,
|
|
37
|
-
limit: 10,
|
|
38
|
-
scoreThreshold: 0.35,
|
|
39
|
-
maxContentChars: 500,
|
|
40
|
-
preferAbstract: true,
|
|
41
|
-
tokenBudget: 2000,
|
|
42
|
-
minQueryLength: 3,
|
|
43
|
-
},
|
|
44
|
-
autoCapture: true,
|
|
45
|
-
captureMode: "semantic",
|
|
46
|
-
captureMaxLength: 24000,
|
|
47
|
-
captureAssistantTurns: true,
|
|
48
|
-
captureToolMaxChars: 1000000,
|
|
49
|
-
commitTokenThreshold: 20000,
|
|
50
|
-
commitKeepRecentCount: 10,
|
|
51
|
-
profileTokenBudget: 10000,
|
|
52
|
-
resumeContextBudget: 32000,
|
|
53
|
-
noAutoInject: false,
|
|
54
|
-
bypassSession: false,
|
|
55
|
-
bypassSessionPatterns: [],
|
|
56
|
-
debug: false,
|
|
57
|
-
debugLogPath: path.join(homedir(), ".openviking", "logs", "opencode-plugin.log"),
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function cloneDefaultConfig() {
|
|
61
|
-
return JSON.parse(JSON.stringify(DEFAULT_CONFIG))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizeNumber(value, fallback, min, max) {
|
|
65
|
-
const next = Number(value)
|
|
66
|
-
if (!Number.isFinite(next)) return fallback
|
|
67
|
-
return Math.max(min, Math.min(max, next))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function envBool(name) {
|
|
71
|
-
const value = process.env[name]
|
|
72
|
-
if (value == null || value === "") return undefined
|
|
73
|
-
const lower = String(value).trim().toLowerCase()
|
|
74
|
-
if (lower === "0" || lower === "false" || lower === "no" || lower === "off") return false
|
|
75
|
-
if (lower === "1" || lower === "true" || lower === "yes" || lower === "on") return true
|
|
76
|
-
return undefined
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function str(value, fallback = "") {
|
|
80
|
-
return typeof value === "string" && value.trim() ? value.trim() : fallback
|
|
81
|
-
}
|
|
3
|
+
import { buildPluginConfig } from "./shared/plugin-config.mjs"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for the opencode plugin.
|
|
7
|
+
*
|
|
8
|
+
* This plugin used to keep its own `openviking-config.json`, looked up along
|
|
9
|
+
* four candidate paths. Every knob now comes from the same place as every other
|
|
10
|
+
* harness — `ovcli.conf`'s `plugin` section, overridden by `plugin.opencode`,
|
|
11
|
+
* by the workspace file, and by the environment — so `ov config switch` moves
|
|
12
|
+
* behaviour along with credentials instead of only half of it.
|
|
13
|
+
*
|
|
14
|
+
* What stays here is the three knobs this plugin's own code reads as sections
|
|
15
|
+
* rather than as flat keys.
|
|
16
|
+
*/
|
|
17
|
+
const MANIFEST_URL = new URL("../package.json", import.meta.url)
|
|
82
18
|
|
|
83
19
|
function expandHome(value) {
|
|
84
20
|
if (!value || typeof value !== "string") return value
|
|
@@ -87,236 +23,23 @@ function expandHome(value) {
|
|
|
87
23
|
return value
|
|
88
24
|
}
|
|
89
25
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
return { path: "", data: {} }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function getConfigPaths(pluginRoot, projectDirectory) {
|
|
103
|
-
const paths = []
|
|
104
|
-
if (process.env.OPENVIKING_PLUGIN_CONFIG) paths.push(expandHome(process.env.OPENVIKING_PLUGIN_CONFIG))
|
|
105
|
-
if (projectDirectory) paths.push(path.join(projectDirectory, ".opencode", "openviking-config.json"))
|
|
106
|
-
paths.push(path.join(homedir(), ".config", "opencode", "openviking-config.json"))
|
|
107
|
-
paths.push(path.join(pluginRoot, "openviking-config.json"))
|
|
108
|
-
return paths
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function applyLegacyConnection(config, fileConfig) {
|
|
112
|
-
const hasLegacyCredentials = ["endpoint", "apiKey", "account", "user", "peerId"]
|
|
113
|
-
.some((key) => fileConfig[key] !== undefined && fileConfig[key] !== "")
|
|
114
|
-
if (!hasLegacyCredentials) return false
|
|
115
|
-
|
|
116
|
-
if (fileConfig.endpoint !== undefined) config.endpoint = fileConfig.endpoint
|
|
117
|
-
if (fileConfig.apiKey !== undefined) config.apiKey = fileConfig.apiKey
|
|
118
|
-
if (fileConfig.account !== undefined) config.account = fileConfig.account
|
|
119
|
-
if (fileConfig.user !== undefined) config.user = fileConfig.user
|
|
120
|
-
if (fileConfig.peerId !== undefined) config.peerId = fileConfig.peerId
|
|
121
|
-
return true
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function applyBehaviorConfig(config, fileConfig = {}) {
|
|
125
|
-
if (fileConfig.enabled !== undefined) config.enabled = fileConfig.enabled !== false
|
|
126
|
-
const mcp = fileConfig.mcp && typeof fileConfig.mcp === "object" ? fileConfig.mcp : {}
|
|
127
|
-
config.mcp = {
|
|
128
|
-
...DEFAULT_CONFIG.mcp,
|
|
129
|
-
...mcp,
|
|
130
|
-
enabled: mcp.enabled !== false,
|
|
131
|
-
}
|
|
132
|
-
if (fileConfig.timeoutMs !== undefined) config.timeoutMs = fileConfig.timeoutMs
|
|
133
|
-
config.runtime = {
|
|
134
|
-
...DEFAULT_CONFIG.runtime,
|
|
135
|
-
...(fileConfig.runtime ?? {}),
|
|
136
|
-
}
|
|
137
|
-
config.repoContext = {
|
|
138
|
-
...DEFAULT_CONFIG.repoContext,
|
|
139
|
-
...(fileConfig.repoContext ?? {}),
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const autoRecall = fileConfig.autoRecall ?? {}
|
|
143
|
-
config.recallLimitConfigured = autoRecall.limit !== undefined ||
|
|
144
|
-
fileConfig.recallLimit !== undefined
|
|
145
|
-
config.autoRecall = {
|
|
146
|
-
...DEFAULT_CONFIG.autoRecall,
|
|
147
|
-
...autoRecall,
|
|
148
|
-
enabled: autoRecall.enabled !== false,
|
|
149
|
-
limit: autoRecall.limit ?? fileConfig.recallLimit ?? DEFAULT_CONFIG.autoRecall.limit,
|
|
150
|
-
scoreThreshold: autoRecall.scoreThreshold ?? fileConfig.scoreThreshold ?? DEFAULT_CONFIG.autoRecall.scoreThreshold,
|
|
151
|
-
maxContentChars: autoRecall.maxContentChars ?? fileConfig.recallMaxContentChars ?? DEFAULT_CONFIG.autoRecall.maxContentChars,
|
|
152
|
-
preferAbstract: autoRecall.preferAbstract ?? fileConfig.recallPreferAbstract ?? DEFAULT_CONFIG.autoRecall.preferAbstract,
|
|
153
|
-
tokenBudget: autoRecall.tokenBudget ?? fileConfig.recallTokenBudget ?? DEFAULT_CONFIG.autoRecall.tokenBudget,
|
|
154
|
-
minQueryLength: autoRecall.minQueryLength ?? fileConfig.minQueryLength ?? DEFAULT_CONFIG.autoRecall.minQueryLength,
|
|
155
|
-
}
|
|
26
|
+
export function loadConfig(pluginRoot, projectDirectory, { env = process.env } = {}) {
|
|
27
|
+
const config = buildPluginConfig("opencode", {
|
|
28
|
+
cwd: projectDirectory,
|
|
29
|
+
env,
|
|
30
|
+
manifestUrl: MANIFEST_URL,
|
|
31
|
+
logFile: "opencode-plugin.log",
|
|
32
|
+
deriveEffectivePeer: true,
|
|
33
|
+
})
|
|
156
34
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"commitKeepRecentCount",
|
|
165
|
-
"profileTokenBudget",
|
|
166
|
-
"resumeContextBudget",
|
|
167
|
-
"noAutoInject",
|
|
168
|
-
"bypassSession",
|
|
169
|
-
"bypassSessionPatterns",
|
|
170
|
-
"debug",
|
|
171
|
-
"debugLogPath",
|
|
172
|
-
"workspacePeer",
|
|
173
|
-
"recallPeerScope",
|
|
174
|
-
"recallQueryExpansion",
|
|
175
|
-
]) {
|
|
176
|
-
if (fileConfig[key] !== undefined) config[key] = fileConfig[key]
|
|
35
|
+
return {
|
|
36
|
+
...config,
|
|
37
|
+
// The MCP registration, the runtime data directory, and the repo context
|
|
38
|
+
// cache.
|
|
39
|
+
mcp: { enabled: config.mcpEnabled },
|
|
40
|
+
runtime: { dataDir: config.dataDir },
|
|
41
|
+
repoContext: { enabled: config.repoContext, cacheTtlMs: config.repoContextCacheTtlMs },
|
|
177
42
|
}
|
|
178
|
-
config.recallQueryExpansionConfigured = fileConfig.recallQueryExpansion !== undefined
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function applyEnv(config) {
|
|
182
|
-
if (process.env.OPENVIKING_TIMEOUT_MS) config.timeoutMs = process.env.OPENVIKING_TIMEOUT_MS
|
|
183
|
-
if (process.env.OPENVIKING_AUTO_RECALL !== undefined) {
|
|
184
|
-
config.autoRecall.enabled = envBool("OPENVIKING_AUTO_RECALL") ?? config.autoRecall.enabled
|
|
185
|
-
}
|
|
186
|
-
if (process.env.OPENVIKING_RECALL_LIMIT) {
|
|
187
|
-
config.autoRecall.limit = process.env.OPENVIKING_RECALL_LIMIT
|
|
188
|
-
config.recallLimitConfigured = true
|
|
189
|
-
}
|
|
190
|
-
if (process.env.OPENVIKING_SCORE_THRESHOLD) config.autoRecall.scoreThreshold = process.env.OPENVIKING_SCORE_THRESHOLD
|
|
191
|
-
if (process.env.OPENVIKING_RECALL_MAX_CONTENT_CHARS) {
|
|
192
|
-
config.autoRecall.maxContentChars = process.env.OPENVIKING_RECALL_MAX_CONTENT_CHARS
|
|
193
|
-
}
|
|
194
|
-
if (process.env.OPENVIKING_RECALL_TOKEN_BUDGET) config.autoRecall.tokenBudget = process.env.OPENVIKING_RECALL_TOKEN_BUDGET
|
|
195
|
-
if (process.env.OPENVIKING_RECALL_PREFER_ABSTRACT !== undefined) {
|
|
196
|
-
config.autoRecall.preferAbstract = envBool("OPENVIKING_RECALL_PREFER_ABSTRACT") ?? config.autoRecall.preferAbstract
|
|
197
|
-
}
|
|
198
|
-
if (process.env.OPENVIKING_RECALL_PEER_SCOPE) config.recallPeerScope = process.env.OPENVIKING_RECALL_PEER_SCOPE
|
|
199
|
-
if (process.env.OPENVIKING_RECALL_QUERY_EXPANSION) {
|
|
200
|
-
config.recallQueryExpansion = process.env.OPENVIKING_RECALL_QUERY_EXPANSION
|
|
201
|
-
config.recallQueryExpansionConfigured = true
|
|
202
|
-
}
|
|
203
|
-
if (process.env.OPENVIKING_WORKSPACE_PEER !== undefined) {
|
|
204
|
-
config.workspacePeer = envBool("OPENVIKING_WORKSPACE_PEER") ?? config.workspacePeer
|
|
205
|
-
}
|
|
206
|
-
if (process.env.OPENVIKING_MIN_QUERY_LENGTH) config.autoRecall.minQueryLength = process.env.OPENVIKING_MIN_QUERY_LENGTH
|
|
207
|
-
if (process.env.OPENVIKING_AUTO_CAPTURE !== undefined) {
|
|
208
|
-
config.autoCapture = envBool("OPENVIKING_AUTO_CAPTURE") ?? config.autoCapture
|
|
209
|
-
}
|
|
210
|
-
if (process.env.OPENVIKING_CAPTURE_MODE) config.captureMode = process.env.OPENVIKING_CAPTURE_MODE
|
|
211
|
-
if (process.env.OPENVIKING_CAPTURE_MAX_LENGTH) config.captureMaxLength = process.env.OPENVIKING_CAPTURE_MAX_LENGTH
|
|
212
|
-
if (process.env.OPENVIKING_CAPTURE_ASSISTANT_TURNS !== undefined) {
|
|
213
|
-
config.captureAssistantTurns = envBool("OPENVIKING_CAPTURE_ASSISTANT_TURNS") ?? config.captureAssistantTurns
|
|
214
|
-
}
|
|
215
|
-
if (process.env.OPENVIKING_CAPTURE_TOOL_MAX_CHARS) {
|
|
216
|
-
config.captureToolMaxChars = process.env.OPENVIKING_CAPTURE_TOOL_MAX_CHARS
|
|
217
|
-
}
|
|
218
|
-
if (process.env.OPENVIKING_COMMIT_TOKEN_THRESHOLD) {
|
|
219
|
-
config.commitTokenThreshold = process.env.OPENVIKING_COMMIT_TOKEN_THRESHOLD
|
|
220
|
-
}
|
|
221
|
-
if (process.env.OPENVIKING_COMMIT_KEEP_RECENT_COUNT) {
|
|
222
|
-
config.commitKeepRecentCount = process.env.OPENVIKING_COMMIT_KEEP_RECENT_COUNT
|
|
223
|
-
}
|
|
224
|
-
if (process.env.OPENVIKING_PROFILE_TOKEN_BUDGET) config.profileTokenBudget = process.env.OPENVIKING_PROFILE_TOKEN_BUDGET
|
|
225
|
-
if (process.env.OPENVIKING_RESUME_CONTEXT_BUDGET) config.resumeContextBudget = process.env.OPENVIKING_RESUME_CONTEXT_BUDGET
|
|
226
|
-
if (process.env.OPENVIKING_NO_AUTO_INJECT !== undefined) {
|
|
227
|
-
config.noAutoInject = envBool("OPENVIKING_NO_AUTO_INJECT") ?? config.noAutoInject
|
|
228
|
-
}
|
|
229
|
-
if (process.env.OPENVIKING_BYPASS_SESSION !== undefined) {
|
|
230
|
-
config.bypassSession = envBool("OPENVIKING_BYPASS_SESSION") ?? config.bypassSession
|
|
231
|
-
}
|
|
232
|
-
if (process.env.OPENVIKING_BYPASS_SESSION_PATTERNS) {
|
|
233
|
-
config.bypassSessionPatterns = process.env.OPENVIKING_BYPASS_SESSION_PATTERNS
|
|
234
|
-
.split(",")
|
|
235
|
-
.map((item) => item.trim())
|
|
236
|
-
.filter(Boolean)
|
|
237
|
-
}
|
|
238
|
-
if (process.env.OPENVIKING_DEBUG !== undefined) config.debug = envBool("OPENVIKING_DEBUG") ?? config.debug
|
|
239
|
-
if (process.env.OPENVIKING_DEBUG_LOG) config.debugLogPath = process.env.OPENVIKING_DEBUG_LOG
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function normalizeConfig(config) {
|
|
243
|
-
config.endpoint = str(config.endpoint, DEFAULT_CONFIG.endpoint).replace(/\/+$/, "")
|
|
244
|
-
config.baseUrl = config.endpoint
|
|
245
|
-
config.accountId = config.account
|
|
246
|
-
config.userId = config.user
|
|
247
|
-
config.userAgent = USER_AGENT
|
|
248
|
-
config.harness = "opencode"
|
|
249
|
-
config.timeoutMs = normalizeNumber(config.timeoutMs, DEFAULT_CONFIG.timeoutMs, 1000, 300000)
|
|
250
|
-
config.repoContext.cacheTtlMs = normalizeNumber(
|
|
251
|
-
config.repoContext.cacheTtlMs,
|
|
252
|
-
DEFAULT_CONFIG.repoContext.cacheTtlMs,
|
|
253
|
-
1000,
|
|
254
|
-
60 * 60 * 1000,
|
|
255
|
-
)
|
|
256
|
-
config.autoRecall.limit = Math.max(1, Math.min(50, Math.round(Number(config.autoRecall.limit) || 10)))
|
|
257
|
-
config.autoRecall.scoreThreshold = Math.max(0, Math.min(1, Number(config.autoRecall.scoreThreshold) || 0))
|
|
258
|
-
config.autoRecall.maxContentChars = Math.max(100, Math.min(5000, Math.round(Number(config.autoRecall.maxContentChars) || 500)))
|
|
259
|
-
config.autoRecall.tokenBudget = Math.max(200, Math.min(50000, Math.round(Number(config.autoRecall.tokenBudget) || 2000)))
|
|
260
|
-
config.autoRecall.minQueryLength = Math.max(1, Math.min(64, Math.round(Number(config.autoRecall.minQueryLength) || 3)))
|
|
261
|
-
config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic"
|
|
262
|
-
config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all"
|
|
263
|
-
config.recallQueryExpansion = config.recallQueryExpansion === "off" ? "off" : "auto"
|
|
264
|
-
config.captureMaxLength = Math.max(200, Math.min(100000, Math.round(Number(config.captureMaxLength) || 24000)))
|
|
265
|
-
config.captureToolMaxChars = Math.max(200, Math.min(1000000, Math.round(Number(config.captureToolMaxChars) || 1000000)))
|
|
266
|
-
config.commitTokenThreshold = Math.max(1000, Math.round(Number(config.commitTokenThreshold) || 20000))
|
|
267
|
-
const rawCommitKeepRecentCount = config.commitKeepRecentCount
|
|
268
|
-
const commitKeepRecentCount = rawCommitKeepRecentCount == null ||
|
|
269
|
-
(typeof rawCommitKeepRecentCount === "string" && rawCommitKeepRecentCount.trim() === "")
|
|
270
|
-
? Number.NaN
|
|
271
|
-
: Number(rawCommitKeepRecentCount)
|
|
272
|
-
config.commitKeepRecentCount = Number.isFinite(commitKeepRecentCount)
|
|
273
|
-
? Math.max(0, Math.round(commitKeepRecentCount))
|
|
274
|
-
: DEFAULT_CONFIG.commitKeepRecentCount
|
|
275
|
-
config.profileTokenBudget = Math.max(500, Math.round(Number(config.profileTokenBudget) || 10000))
|
|
276
|
-
config.resumeContextBudget = Math.max(1024, Math.round(Number(config.resumeContextBudget) || 32000))
|
|
277
|
-
if (!Array.isArray(config.bypassSessionPatterns)) config.bypassSessionPatterns = []
|
|
278
|
-
|
|
279
|
-
config.recallLimit = config.autoRecall.limit
|
|
280
|
-
config.scoreThreshold = config.autoRecall.scoreThreshold
|
|
281
|
-
config.recallMaxContentChars = config.autoRecall.maxContentChars
|
|
282
|
-
config.recallPreferAbstract = config.autoRecall.preferAbstract !== false
|
|
283
|
-
config.recallTokenBudget = config.autoRecall.tokenBudget
|
|
284
|
-
config.minQueryLength = config.autoRecall.minQueryLength
|
|
285
|
-
return config
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export function loadConfig(pluginRoot, projectDirectory) {
|
|
289
|
-
const config = cloneDefaultConfig()
|
|
290
|
-
const { path: configPath, data: fileConfig } = readConfigFile(pluginRoot, projectDirectory)
|
|
291
|
-
applyBehaviorConfig(config, fileConfig)
|
|
292
|
-
|
|
293
|
-
const creds = resolveOpenVikingCredentials()
|
|
294
|
-
config.endpoint = creds.baseUrl
|
|
295
|
-
config.apiKey = creds.apiKey
|
|
296
|
-
config.account = creds.account
|
|
297
|
-
config.user = creds.user
|
|
298
|
-
// Shared credentials only carry a peer when ovcli.conf sets actor_peer_id
|
|
299
|
-
// (or OPENVIKING_PEER_ID is exported); without one the project config's
|
|
300
|
-
// peerId still applies so authenticated setups keep writing peer-scoped
|
|
301
|
-
// data instead of silently dropping into the shared user tree (#4487).
|
|
302
|
-
config.peerId = creds.peerId || str(fileConfig.peerId, "")
|
|
303
|
-
config.mcpUrl = creds.mcpUrl
|
|
304
|
-
config.credentialSource = creds.credentialSource
|
|
305
|
-
config.credentialPath = creds.cliPath || creds.ovPath || ""
|
|
306
|
-
|
|
307
|
-
const mayUseLegacyCredentials = creds.credentialSource !== "env" &&
|
|
308
|
-
creds.credentialSource !== "ovcli" &&
|
|
309
|
-
!creds.apiKey &&
|
|
310
|
-
!creds.account &&
|
|
311
|
-
!creds.user &&
|
|
312
|
-
!creds.peerId
|
|
313
|
-
const legacyUsed = mayUseLegacyCredentials ? applyLegacyConnection(config, fileConfig) : false
|
|
314
|
-
applyEnv(config)
|
|
315
|
-
config.configPath = configPath
|
|
316
|
-
config.legacyCredentialsUsed = legacyUsed && creds.credentialSource !== "env"
|
|
317
|
-
const normalized = normalizeConfig(config)
|
|
318
|
-
normalized.effectivePeer = resolveEffectivePeerId({ cfg: normalized, cwd: projectDirectory })
|
|
319
|
-
return normalized
|
|
320
43
|
}
|
|
321
44
|
|
|
322
45
|
export function resolveDataDir(pluginRoot, config) {
|
package/lib/memory-recall.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { buildRecallBlock } from "./shared/recall-core.mjs"
|
|
1
|
+
import { buildRecallBlock, isRecallEnabled } from "./shared/recall-core.mjs"
|
|
2
2
|
import { isBypassed } from "./shared/session-model.mjs"
|
|
3
3
|
import { effectivePeerId, fetchJSON, log } from "./utils.mjs"
|
|
4
4
|
|
|
5
5
|
export function createMemoryRecall({ config, sessionManager }) {
|
|
6
6
|
async function injectRelevantMemories(input, output) {
|
|
7
|
-
if (!config
|
|
7
|
+
if (!isRecallEnabled(config)) return
|
|
8
8
|
const query = extractCurrentUserText(output.parts ?? [])
|
|
9
9
|
if (!query) return
|
|
10
10
|
if (query.length < config.minQueryLength) return
|
package/lib/memory-session.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from "path"
|
|
|
3
3
|
import {
|
|
4
4
|
extractPartsFromPayload,
|
|
5
5
|
extractTextFromPayload,
|
|
6
|
+
isCaptureEnabled,
|
|
6
7
|
shouldCaptureText,
|
|
7
8
|
} from "./shared/capture-utils.mjs"
|
|
8
9
|
import {
|
|
@@ -46,7 +47,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
async function init() {
|
|
49
|
-
if (config
|
|
50
|
+
if (isCaptureEnabled(config)) await migrateLegacySessionMap()
|
|
50
51
|
await loadState()
|
|
51
52
|
const health = await fetchJSON(config, "/health", {}, { timeoutMs: 5000 })
|
|
52
53
|
if (health.ok) {
|
|
@@ -157,9 +158,9 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
157
158
|
await handleSessionCompacted(event)
|
|
158
159
|
} else if (event.type === "session.idle") {
|
|
159
160
|
await handleSessionIdle(event)
|
|
160
|
-
} else if (event.type === "message.updated" && config
|
|
161
|
+
} else if (event.type === "message.updated" && isCaptureEnabled(config)) {
|
|
161
162
|
await handleMessageUpdated(event)
|
|
162
|
-
} else if (event.type === "message.part.updated" && config
|
|
163
|
+
} else if (event.type === "message.part.updated" && isCaptureEnabled(config)) {
|
|
163
164
|
await handleMessagePartUpdated(event)
|
|
164
165
|
}
|
|
165
166
|
}
|
|
@@ -286,7 +287,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
286
287
|
if (!state) return false
|
|
287
288
|
|
|
288
289
|
const added = await flushPendingMessages(opencodeSessionId, state)
|
|
289
|
-
if (commit && config
|
|
290
|
+
if (commit && isCaptureEnabled(config)) {
|
|
290
291
|
await commitOvSession(state.ovSessionId, { force: true, reason })
|
|
291
292
|
} else if (added > 0) {
|
|
292
293
|
await maybeCommitByThreshold(state)
|
|
@@ -388,7 +389,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
388
389
|
}
|
|
389
390
|
|
|
390
391
|
async function flushPendingMessages(opencodeSessionId, state) {
|
|
391
|
-
if (!config
|
|
392
|
+
if (!isCaptureEnabled(config)) return 0
|
|
392
393
|
const toSend = []
|
|
393
394
|
for (const [messageId, message] of state.messages.entries()) {
|
|
394
395
|
if (message.captured) continue
|
package/lib/repo-context.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { effectivePeerId, log, makeRequest
|
|
1
|
+
import { effectivePeerId, log, makeRequest } from "./utils.mjs"
|
|
2
2
|
|
|
3
3
|
export function createRepoContext({ config }) {
|
|
4
4
|
let cachedRepos = null
|
|
@@ -14,13 +14,12 @@ export function createRepoContext({ config }) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
|
-
const
|
|
17
|
+
const result = await makeRequest(config, {
|
|
18
18
|
method: "GET",
|
|
19
19
|
endpoint: `/api/v1/fs/ls?uri=${encodeURIComponent("viking://resources/")}&recursive=false&simple=false`,
|
|
20
20
|
timeoutMs: 8000,
|
|
21
21
|
actorPeerId: effectivePeerId(config),
|
|
22
22
|
})
|
|
23
|
-
const result = unwrapResponse(response)
|
|
24
23
|
const items = Array.isArray(result) ? result : []
|
|
25
24
|
const repos = items
|
|
26
25
|
.filter((item) => item?.uri?.startsWith("viking://resources/") && item.uri !== "viking://resources/")
|
package/lib/runtime.mjs
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
import { log, makeToast
|
|
1
|
+
import { fetchJSON, getResponseErrorMessage, log, makeToast } from "./utils.mjs"
|
|
2
2
|
|
|
3
|
+
// The probe used to send a User-Agent and nothing else, so a server that wants
|
|
4
|
+
// a key reported itself as down and the plugin disabled itself on startup.
|
|
3
5
|
export async function checkServiceHealth(config, timeoutMs = 3000) {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} catch (error) {
|
|
14
|
-
log("WARN", "health", "OpenViking health check failed", {
|
|
15
|
-
endpoint: config.endpoint,
|
|
16
|
-
error: error?.message,
|
|
17
|
-
})
|
|
18
|
-
return false
|
|
19
|
-
} finally {
|
|
20
|
-
clearTimeout(timeout)
|
|
21
|
-
}
|
|
6
|
+
const res = await fetchJSON(config, "/health", { method: "GET" }, { timeoutMs })
|
|
7
|
+
if (res.ok) return true
|
|
8
|
+
|
|
9
|
+
log("WARN", "health", "OpenViking health check failed", {
|
|
10
|
+
endpoint: config.endpoint,
|
|
11
|
+
status: res.status,
|
|
12
|
+
error: getResponseErrorMessage(res.error),
|
|
13
|
+
})
|
|
14
|
+
return false
|
|
22
15
|
}
|
|
23
16
|
|
|
24
17
|
export async function initializeRuntime(config, client) {
|
|
@@ -266,7 +266,13 @@ export function extractTextFromPayload(payload, options = {}) {
|
|
|
266
266
|
return chunks.join("\n\n");
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Map every tool call id in a transcript to the name of the tool it invoked.
|
|
271
|
+
*
|
|
272
|
+
* A result block names the call by id only, so the name has to come from the
|
|
273
|
+
* call that preceded it. Pass the map as `toolNameById` to the extractors.
|
|
274
|
+
*/
|
|
275
|
+
export function collectToolNamesByIdFromEntries(entries) {
|
|
270
276
|
const map = {};
|
|
271
277
|
for (const entry of entries || []) {
|
|
272
278
|
const payload = entry?.payload && typeof entry.payload === "object" ? entry.payload : entry;
|
|
@@ -433,6 +439,24 @@ export function extractCaptureTurns(rolloutEntries, cfg = {}) {
|
|
|
433
439
|
return turns;
|
|
434
440
|
}
|
|
435
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Index of the last turn that came from a human prompt, or -1.
|
|
444
|
+
*
|
|
445
|
+
* `role === "user"` alone is not enough: normalizeCaptureRole() maps tool
|
|
446
|
+
* results onto the user role too, and those carry `tool` parts rather than
|
|
447
|
+
* `text` parts. Used by the post-compact shrink path to find where the current
|
|
448
|
+
* interaction starts.
|
|
449
|
+
*/
|
|
450
|
+
export function findLastHumanTurnIndex(turns) {
|
|
451
|
+
const list = Array.isArray(turns) ? turns : [];
|
|
452
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
453
|
+
const turn = list[i];
|
|
454
|
+
if (turn?.role !== "user") continue;
|
|
455
|
+
if (turn.parts?.some((part) => part?.type === "text")) return i;
|
|
456
|
+
}
|
|
457
|
+
return -1;
|
|
458
|
+
}
|
|
459
|
+
|
|
436
460
|
export function normalizeCaptureRole(role) {
|
|
437
461
|
const value = normalizeType(role);
|
|
438
462
|
if (value === "user") return "user";
|
|
@@ -511,12 +535,24 @@ function stripInjectedDigestBlocks(text) {
|
|
|
511
535
|
return out.join("\n");
|
|
512
536
|
}
|
|
513
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Drop everything in a turn that the conversation did not put there.
|
|
540
|
+
*
|
|
541
|
+
* Recall injects a context block into the prompt, and the host adds notes of
|
|
542
|
+
* its own; captured back unchanged, this turn's injection becomes next turn's
|
|
543
|
+
* memory and the loop feeds on itself. Formatting the conversation did author
|
|
544
|
+
* — newlines, code fences — survives.
|
|
545
|
+
*/
|
|
514
546
|
export function sanitizeCapturedText(text) {
|
|
515
547
|
let value = String(text || "");
|
|
516
548
|
value = value
|
|
517
549
|
.replace(/\u0000/g, "")
|
|
518
550
|
.replace(/<openviking-context\b[^>]*>[\s\S]*?<\/openviking-context>/gi, " ")
|
|
519
551
|
.replace(/<relevant-memor(?:y|ies)\b[^>]*>[\s\S]*?<\/relevant-memor(?:y|ies)>/gi, " ")
|
|
552
|
+
// Claude Code wraps its own out-of-band notes to the model in these two
|
|
553
|
+
// shapes. They are the host talking to itself, not the conversation.
|
|
554
|
+
.replace(/<system-reminder\b[^>]*>[\s\S]*?<\/system-reminder>/gi, " ")
|
|
555
|
+
.replace(/^[ \t]*\[Subagent Context\][^\n]*$/gim, " ")
|
|
520
556
|
.replace(/^\s*Sender\s*\([^)]+\)\s*```[\s\S]*?```\s*/gim, " ")
|
|
521
557
|
.replace(/^\s*Conversation (?:metadata|info):\s*```[\s\S]*?```\s*/gim, " ")
|
|
522
558
|
.replace(/^\s*\[?\d{4}-\d{2}-\d{2}[T ][^\]\n]{3,80}\]?\s*/gm, "")
|
|
@@ -540,6 +576,23 @@ function isPunctuationOnly(text) {
|
|
|
540
576
|
return !/[a-z0-9\u3400-\u9fff]/i.test(text);
|
|
541
577
|
}
|
|
542
578
|
|
|
579
|
+
/**
|
|
580
|
+
* Is capture on for this config?
|
|
581
|
+
*
|
|
582
|
+
* The switch has four spellings in the wild: a boolean `autoCapture`, opencode's
|
|
583
|
+
* `{ enabled }` object, dsh and pi's `syncTurns`, and the global `enabled`
|
|
584
|
+
* that turns the whole plugin off. Reading it here rather than in each loader
|
|
585
|
+
* is what keeps it from drifting a fifth time — and any spelling that says off
|
|
586
|
+
* wins, so a config that disables capture under an older name still disables it.
|
|
587
|
+
*/
|
|
588
|
+
export function isCaptureEnabled(cfg = {}) {
|
|
589
|
+
for (const value of [cfg.enabled, cfg.autoCapture, cfg.capture, cfg.syncTurns]) {
|
|
590
|
+
if (value === false) return false;
|
|
591
|
+
if (value && typeof value === "object" && !Array.isArray(value) && value.enabled === false) return false;
|
|
592
|
+
}
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
543
596
|
export function shouldCaptureText(text, role, cfg = {}, { filters = true } = {}) {
|
|
544
597
|
const maxLength = cfg.captureMaxLength || 24000;
|
|
545
598
|
const sanitized = sanitizeCapturedText(text);
|
|
@@ -576,3 +629,26 @@ export function shouldCaptureText(text, role, cfg = {}, { filters = true } = {})
|
|
|
576
629
|
|
|
577
630
|
return { shouldCapture: true, reason: "ok", text: capped };
|
|
578
631
|
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Apply the capture filter to a list of `{ role, content }` turns.
|
|
635
|
+
*
|
|
636
|
+
* The harnesses that compose `agent-hook-runtime` used to send whatever their
|
|
637
|
+
* transcript parser produced: an acknowledgement, a slash command, a stray
|
|
638
|
+
* `ok`, or a turn far past `captureMaxLength` all reached the extractor
|
|
639
|
+
* verbatim. This is the same decision every other harness makes, in one place,
|
|
640
|
+
* so a thin harness gets it by calling rather than by reimplementing it.
|
|
641
|
+
*
|
|
642
|
+
* Returns the surviving turns with `content` replaced by the sanitized and
|
|
643
|
+
* capped text, and the dropped ones with the reason, for the debug log.
|
|
644
|
+
*/
|
|
645
|
+
export function filterCaptureTurns(turns, cfg = {}) {
|
|
646
|
+
const kept = [];
|
|
647
|
+
const dropped = [];
|
|
648
|
+
for (const turn of Array.isArray(turns) ? turns : []) {
|
|
649
|
+
const decision = shouldCaptureText(turn?.content, turn?.role, cfg);
|
|
650
|
+
if (decision.shouldCapture) kept.push({ ...turn, content: decision.text });
|
|
651
|
+
else dropped.push({ role: turn?.role, reason: decision.reason });
|
|
652
|
+
}
|
|
653
|
+
return { kept, dropped };
|
|
654
|
+
}
|