@openviking/openclaw-plugin 2026.5.8

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.
@@ -0,0 +1,167 @@
1
+ import { pickMemoriesForInjection, postProcessMemories, summarizeInjectionMemories, toJsonLog, } from "./memory-ranking.js";
2
+ import { quickRecallPrecheck, withTimeout } from "./process-manager.js";
3
+ import { sanitizeUserTextForCapture } from "./text-utils.js";
4
+ const AUTO_RECALL_TIMEOUT_MS = 5_000;
5
+ const RECALL_QUERY_MAX_CHARS = 4_000;
6
+ export const AUTO_RECALL_SOURCE_MARKER = "Source: openviking-auto-recall";
7
+ export function prepareRecallQuery(rawText) {
8
+ const sanitized = sanitizeUserTextForCapture(rawText).trim();
9
+ const originalChars = sanitized.length;
10
+ if (!sanitized) {
11
+ return {
12
+ query: "",
13
+ truncated: false,
14
+ originalChars: 0,
15
+ finalChars: 0,
16
+ };
17
+ }
18
+ const query = sanitized.length > RECALL_QUERY_MAX_CHARS
19
+ ? sanitized.slice(0, RECALL_QUERY_MAX_CHARS).trim()
20
+ : sanitized;
21
+ return {
22
+ query,
23
+ truncated: sanitized.length > RECALL_QUERY_MAX_CHARS,
24
+ originalChars,
25
+ finalChars: query.length,
26
+ };
27
+ }
28
+ /** Estimate token count using chars/4 heuristic for diagnostics. */
29
+ export function estimateTokenCount(text) {
30
+ if (!text)
31
+ return 0;
32
+ return Math.ceil(text.length / 4);
33
+ }
34
+ async function resolveMemoryContent(item, readFn, options) {
35
+ let content;
36
+ if (options.recallPreferAbstract && item.abstract?.trim()) {
37
+ content = item.abstract.trim();
38
+ }
39
+ else if (item.level === 2) {
40
+ try {
41
+ const fullContent = await readFn(item.uri);
42
+ content =
43
+ fullContent && typeof fullContent === "string" && fullContent.trim()
44
+ ? fullContent.trim()
45
+ : (item.abstract?.trim() || item.uri);
46
+ }
47
+ catch {
48
+ content = item.abstract?.trim() || item.uri;
49
+ }
50
+ }
51
+ else {
52
+ content = item.abstract?.trim() || item.uri;
53
+ }
54
+ return content;
55
+ }
56
+ export async function buildMemoryLines(memories, readFn, options) {
57
+ const lines = [];
58
+ for (const item of memories) {
59
+ const content = await resolveMemoryContent(item, readFn, options);
60
+ lines.push(`- [${item.category ?? "memory"}] ${content}`);
61
+ }
62
+ return lines;
63
+ }
64
+ /**
65
+ * Build memory lines with a character budget constraint.
66
+ *
67
+ * Individual memories are never truncated. A memory that cannot fit within the
68
+ * remaining character budget is skipped so only complete memory entries are
69
+ * injected.
70
+ */
71
+ export async function buildMemoryLinesWithBudget(memories, readFn, options) {
72
+ const charBudget = options.recallMaxInjectedChars ?? options.recallTokenBudget ?? 0;
73
+ const lines = [];
74
+ let totalTokens = 0;
75
+ let totalChars = 0;
76
+ for (const item of memories) {
77
+ if (totalChars >= charBudget) {
78
+ break;
79
+ }
80
+ const content = await resolveMemoryContent(item, readFn, options);
81
+ const line = `- [${item.category ?? "memory"}] ${content}`;
82
+ const separatorChars = lines.length > 0 ? 1 : 0;
83
+ const projectedChars = totalChars + separatorChars + line.length;
84
+ if (projectedChars > charBudget) {
85
+ continue;
86
+ }
87
+ const lineTokens = estimateTokenCount(line);
88
+ lines.push(line);
89
+ totalTokens += lineTokens;
90
+ totalChars = projectedChars;
91
+ }
92
+ return { lines, estimatedTokens: totalTokens };
93
+ }
94
+ export function buildRecallContextBlock(memoryLines) {
95
+ return [
96
+ "<relevant-memories>",
97
+ AUTO_RECALL_SOURCE_MARKER,
98
+ "The following OpenViking memories may be relevant:",
99
+ ...memoryLines,
100
+ "</relevant-memories>",
101
+ ].join("\n");
102
+ }
103
+ export async function buildAutoRecallContext(params) {
104
+ const { cfg, client, agentId, queryText, logger, verbose } = params;
105
+ if (!cfg.autoRecall || queryText.length < 5) {
106
+ return { memoryCount: 0, estimatedTokens: 0 };
107
+ }
108
+ const precheck = await quickRecallPrecheck(cfg.baseUrl);
109
+ if (!precheck.ok) {
110
+ verbose?.(`openviking: skipping auto-recall because precheck failed (${precheck.reason})`);
111
+ return { memoryCount: 0, estimatedTokens: 0 };
112
+ }
113
+ return withTimeout((async () => {
114
+ const candidateLimit = Math.max(cfg.recallLimit * 4, 20);
115
+ const autoRecallPromises = [
116
+ client.find(queryText, {
117
+ targetUri: "viking://user/memories",
118
+ limit: candidateLimit,
119
+ scoreThreshold: 0,
120
+ }, agentId),
121
+ client.find(queryText, {
122
+ targetUri: "viking://agent/memories",
123
+ limit: candidateLimit,
124
+ scoreThreshold: 0,
125
+ }, agentId),
126
+ ];
127
+ if (cfg.recallResources) {
128
+ autoRecallPromises.push(client.find(queryText, {
129
+ targetUri: "viking://resources",
130
+ limit: candidateLimit,
131
+ scoreThreshold: 0,
132
+ }, agentId));
133
+ }
134
+ const autoRecallSettled = await Promise.allSettled(autoRecallPromises);
135
+ const allMemories = [];
136
+ for (const s of autoRecallSettled) {
137
+ if (s.status === "fulfilled") {
138
+ allMemories.push(...(s.value.memories ?? []), ...(s.value.resources ?? []));
139
+ }
140
+ else {
141
+ logger.warn?.(`openviking: auto-recall search failed: ${String(s.reason)}`);
142
+ }
143
+ }
144
+ const uniqueMemories = allMemories.filter((memory, index, self) => index === self.findIndex((m) => m.uri === memory.uri));
145
+ const leafOnly = uniqueMemories.filter((m) => !m.level || m.level === 2);
146
+ const processed = postProcessMemories(leafOnly, {
147
+ limit: candidateLimit,
148
+ scoreThreshold: cfg.recallScoreThreshold,
149
+ });
150
+ const memories = pickMemoriesForInjection(processed, cfg.recallLimit, queryText);
151
+ if (memories.length === 0) {
152
+ return { memoryCount: 0, estimatedTokens: 0 };
153
+ }
154
+ const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(memories, (uri) => client.read(uri, agentId), {
155
+ recallPreferAbstract: cfg.recallPreferAbstract,
156
+ recallMaxInjectedChars: cfg.recallMaxInjectedChars,
157
+ });
158
+ if (memoryLines.length === 0) {
159
+ verbose?.(`openviking: skipping auto-recall injection; no complete memories fit maxInjectedChars=${cfg.recallMaxInjectedChars}`);
160
+ return { memoryCount: 0, estimatedTokens: 0 };
161
+ }
162
+ const block = buildRecallContextBlock(memoryLines);
163
+ verbose?.(`openviking: injecting ${memoryLines.length} memories (${block.length} chars, ~${estimatedTokens} tokens, maxInjectedChars=${cfg.recallMaxInjectedChars})`);
164
+ verbose?.(`openviking: inject-detail ${toJsonLog({ count: memories.length, memories: summarizeInjectionMemories(memories) })}`);
165
+ return { block, memoryCount: memoryLines.length, estimatedTokens };
166
+ })(), AUTO_RECALL_TIMEOUT_MS, "openviking: auto-recall search timeout");
167
+ }
package/dist/client.js ADDED
@@ -0,0 +1,502 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { once } from "node:events";
3
+ import { createWriteStream } from "node:fs";
4
+ import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { basename, dirname, join, relative } from "node:path";
7
+ import { Zip, ZipDeflate } from "fflate";
8
+ const DEFAULT_WAIT_REQUEST_TIMEOUT_MS = 120_000;
9
+ export const DEFAULT_PHASE2_POLL_TIMEOUT_MS = 300_000;
10
+ const WAIT_REQUEST_TIMEOUT_BUFFER_MS = 5_000;
11
+ function sleep(ms) {
12
+ return new Promise((resolve) => setTimeout(resolve, ms));
13
+ }
14
+ const MEMORY_URI_PATTERNS = [
15
+ /^viking:\/\/user\/(?:[^/]+(?:\/agent\/[^/]+)?\/)?memories(?:\/|$)/,
16
+ /^viking:\/\/agent\/(?:[^/]+(?:\/user\/[^/]+)?\/)?memories(?:\/|$)/,
17
+ ];
18
+ const USER_STRUCTURE_DIRS = new Set(["memories", "profile.md", ".abstract.md", ".overview.md"]);
19
+ const AGENT_STRUCTURE_DIRS = new Set([
20
+ "memories",
21
+ "skills",
22
+ "instructions",
23
+ "workspaces",
24
+ ".abstract.md",
25
+ ".overview.md",
26
+ ]);
27
+ const REMOTE_RESOURCE_PREFIXES = ["http://", "https://", "git@", "ssh://", "git://"];
28
+ export function isMemoryUri(uri) {
29
+ return MEMORY_URI_PATTERNS.some((pattern) => pattern.test(uri));
30
+ }
31
+ function isRemoteResourceSource(source) {
32
+ return REMOTE_RESOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
33
+ }
34
+ function toBlobPart(value) {
35
+ return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
36
+ }
37
+ function resolveWaitRequestTimeoutMs(defaultTimeoutMs, waitTimeoutSeconds) {
38
+ const requestedMs = typeof waitTimeoutSeconds === "number" && Number.isFinite(waitTimeoutSeconds) && waitTimeoutSeconds > 0
39
+ ? Math.ceil(waitTimeoutSeconds * 1000) + WAIT_REQUEST_TIMEOUT_BUFFER_MS
40
+ : DEFAULT_WAIT_REQUEST_TIMEOUT_MS;
41
+ return Math.max(defaultTimeoutMs, requestedMs);
42
+ }
43
+ async function cleanupUploadTempPath(path) {
44
+ if (!path) {
45
+ return;
46
+ }
47
+ await rm(path, { force: true }).catch(() => undefined);
48
+ await rm(dirname(path), { recursive: true, force: true }).catch(() => undefined);
49
+ }
50
+ export class OpenVikingClient {
51
+ baseUrl;
52
+ apiKey;
53
+ defaultAgentId;
54
+ timeoutMs;
55
+ accountId;
56
+ userId;
57
+ routingDebugLog;
58
+ isolateUserScopeByAgent;
59
+ isolateAgentScopeByUser;
60
+ identityCache = new Map();
61
+ constructor(baseUrl, apiKey, defaultAgentId, timeoutMs,
62
+ /** When set, sent so ROOT keys or trusted deployments can select tenant identity. */
63
+ accountId = "", userId = "",
64
+ /** When set, logs routing for find + session writes (tenant headers + paths; never apiKey). */
65
+ routingDebugLog, isolateUserScopeByAgent = false, isolateAgentScopeByUser = true) {
66
+ this.baseUrl = baseUrl;
67
+ this.apiKey = apiKey;
68
+ this.defaultAgentId = defaultAgentId;
69
+ this.timeoutMs = timeoutMs;
70
+ this.accountId = accountId;
71
+ this.userId = userId;
72
+ this.routingDebugLog = routingDebugLog;
73
+ this.isolateUserScopeByAgent = isolateUserScopeByAgent;
74
+ this.isolateAgentScopeByUser = isolateAgentScopeByUser;
75
+ }
76
+ getDefaultAgentId() {
77
+ return this.defaultAgentId;
78
+ }
79
+ resolveEffectiveAgentId(agentId) {
80
+ const explicit = agentId?.trim();
81
+ if (explicit) {
82
+ return explicit;
83
+ }
84
+ const prefix = this.defaultAgentId.trim();
85
+ return prefix ? `${prefix}_main` : "main";
86
+ }
87
+ async getResolvedIdentity(agentId) {
88
+ return this.getRuntimeIdentity(agentId);
89
+ }
90
+ resolveTenantHeaders() {
91
+ const apiKey = this.apiKey.trim();
92
+ const accountId = this.accountId.trim();
93
+ const userId = this.userId.trim();
94
+ return {
95
+ ...(apiKey ? { apiKey } : {}),
96
+ ...(accountId ? { accountId } : {}),
97
+ ...(userId ? { userId } : {}),
98
+ };
99
+ }
100
+ async emitRoutingDebug(label, detail, agentId) {
101
+ if (!this.routingDebugLog) {
102
+ return;
103
+ }
104
+ const effectiveAgentId = this.resolveEffectiveAgentId(agentId);
105
+ const identity = await this.getRuntimeIdentity(agentId);
106
+ const tenantHeaders = this.resolveTenantHeaders();
107
+ this.routingDebugLog(`openviking: ${label} ` +
108
+ JSON.stringify({
109
+ ...detail,
110
+ X_OpenViking_Agent: effectiveAgentId,
111
+ X_OpenViking_Account: tenantHeaders.accountId ?? null,
112
+ X_OpenViking_User: tenantHeaders.userId ?? null,
113
+ resolved_user_id: identity.userId,
114
+ session_vfs_hint: detail.sessionId
115
+ ? `viking://session/${String(detail.sessionId)}`
116
+ : undefined,
117
+ }));
118
+ }
119
+ async request(path, init = {}, agentId, requestTimeoutMs) {
120
+ const effectiveAgentId = this.resolveEffectiveAgentId(agentId);
121
+ const controller = new AbortController();
122
+ const timer = setTimeout(() => controller.abort(), requestTimeoutMs ?? this.timeoutMs);
123
+ try {
124
+ const headers = new Headers(init.headers ?? {});
125
+ const tenantHeaders = this.resolveTenantHeaders();
126
+ if (tenantHeaders.apiKey) {
127
+ headers.set("X-API-Key", tenantHeaders.apiKey);
128
+ }
129
+ if (tenantHeaders.accountId) {
130
+ headers.set("X-OpenViking-Account", tenantHeaders.accountId);
131
+ }
132
+ if (tenantHeaders.userId) {
133
+ headers.set("X-OpenViking-User", tenantHeaders.userId);
134
+ }
135
+ if (effectiveAgentId) {
136
+ headers.set("X-OpenViking-Agent", effectiveAgentId);
137
+ }
138
+ if (init.body && !(init.body instanceof FormData) && !headers.has("Content-Type")) {
139
+ headers.set("Content-Type", "application/json");
140
+ }
141
+ const response = await fetch(`${this.baseUrl}${path}`, {
142
+ ...init,
143
+ headers,
144
+ signal: controller.signal,
145
+ });
146
+ const payload = (await response.json().catch(() => ({})));
147
+ if (!response.ok || payload.status === "error") {
148
+ const code = payload.error?.code ? ` [${payload.error.code}]` : "";
149
+ const message = payload.error?.message ?? `HTTP ${response.status}`;
150
+ throw new Error(`OpenViking request failed${code}: ${message}`);
151
+ }
152
+ return (payload.result ?? payload);
153
+ }
154
+ finally {
155
+ clearTimeout(timer);
156
+ }
157
+ }
158
+ async healthCheck() {
159
+ await this.request("/health");
160
+ }
161
+ async getRuntimeIdentity(agentId) {
162
+ const effectiveAgentId = this.resolveEffectiveAgentId(agentId);
163
+ const cached = this.identityCache.get(effectiveAgentId);
164
+ if (cached) {
165
+ return cached;
166
+ }
167
+ const fallback = { userId: "default", agentId: effectiveAgentId };
168
+ try {
169
+ const status = await this.request("/api/v1/system/status", {}, agentId);
170
+ const userId = typeof status.user === "string" && status.user.trim() ? status.user.trim() : "default";
171
+ const identity = { userId, agentId: effectiveAgentId };
172
+ this.identityCache.set(effectiveAgentId, identity);
173
+ return identity;
174
+ }
175
+ catch {
176
+ this.identityCache.set(effectiveAgentId, fallback);
177
+ return fallback;
178
+ }
179
+ }
180
+ async buildCanonicalRoot(scope, agentId) {
181
+ const identity = await this.getRuntimeIdentity(agentId);
182
+ if (scope === "user") {
183
+ const root = this.isolateUserScopeByAgent
184
+ ? `viking://user/${identity.userId}/agent/${identity.agentId}`
185
+ : `viking://user/${identity.userId}`;
186
+ return root;
187
+ }
188
+ const root = this.isolateAgentScopeByUser
189
+ ? `viking://agent/${identity.agentId}/user/${identity.userId}`
190
+ : `viking://agent/${identity.agentId}`;
191
+ return root;
192
+ }
193
+ async normalizeTargetUri(targetUri, agentId) {
194
+ const trimmed = targetUri.trim().replace(/\/+$/, "");
195
+ const match = trimmed.match(/^viking:\/\/(user|agent)(?:\/(.*))?$/);
196
+ if (!match) {
197
+ return trimmed;
198
+ }
199
+ const scope = match[1];
200
+ const rawRest = (match[2] ?? "").trim();
201
+ if (!rawRest) {
202
+ return trimmed;
203
+ }
204
+ const parts = rawRest.split("/").filter(Boolean);
205
+ if (parts.length === 0) {
206
+ return trimmed;
207
+ }
208
+ const reservedDirs = scope === "user" ? USER_STRUCTURE_DIRS : AGENT_STRUCTURE_DIRS;
209
+ if (!reservedDirs.has(parts[0])) {
210
+ return trimmed;
211
+ }
212
+ const root = await this.buildCanonicalRoot(scope, agentId);
213
+ return `${root}/${parts.join("/")}`;
214
+ }
215
+ async find(query, options, agentId) {
216
+ const normalizedTargetUri = await this.normalizeTargetUri(options.targetUri, agentId);
217
+ const body = {
218
+ query,
219
+ target_uri: normalizedTargetUri,
220
+ limit: options.limit,
221
+ score_threshold: options.scoreThreshold,
222
+ };
223
+ const effectiveAgentId = this.resolveEffectiveAgentId(agentId);
224
+ const identity = await this.getRuntimeIdentity(agentId);
225
+ const tenantHeaders = this.resolveTenantHeaders();
226
+ this.routingDebugLog?.(`openviking: find POST ${this.baseUrl}/api/v1/search/find ` +
227
+ JSON.stringify({
228
+ X_OpenViking_Agent: effectiveAgentId,
229
+ X_OpenViking_Account: tenantHeaders.accountId ?? null,
230
+ X_OpenViking_User: tenantHeaders.userId ?? null,
231
+ resolved_user_id: identity.userId,
232
+ target_uri: normalizedTargetUri,
233
+ target_uri_input: options.targetUri,
234
+ query: query.length > 4000
235
+ ? `${query.slice(0, 4000)}…(+${query.length - 4000} more chars)`
236
+ : query,
237
+ limit: body.limit,
238
+ score_threshold: body.score_threshold ?? null,
239
+ }));
240
+ return this.request("/api/v1/search/find", {
241
+ method: "POST",
242
+ body: JSON.stringify(body),
243
+ }, agentId);
244
+ }
245
+ async read(uri, agentId) {
246
+ return this.request(`/api/v1/content/read?uri=${encodeURIComponent(uri)}`, {}, agentId);
247
+ }
248
+ async uploadTempFile(filePath, agentId) {
249
+ const fileBytes = await readFile(filePath);
250
+ const form = new FormData();
251
+ form.append("file", new Blob([toBlobPart(fileBytes)], { type: "application/octet-stream" }), basename(filePath));
252
+ const result = await this.request("/api/v1/resources/temp_upload", { method: "POST", body: form }, agentId);
253
+ if (!result.temp_file_id) {
254
+ throw new Error("OpenViking temp upload did not return temp_file_id");
255
+ }
256
+ return result.temp_file_id;
257
+ }
258
+ async zipDirectoryForUpload(dirPath) {
259
+ const rootStats = await stat(dirPath);
260
+ if (!rootStats.isDirectory()) {
261
+ throw new Error(`Not a directory: ${dirPath}`);
262
+ }
263
+ const zipDir = await mkdtemp(join(tmpdir(), "openviking-openclaw-upload-"));
264
+ const zipPath = join(zipDir, `${basename(dirPath).replace(/[^a-zA-Z0-9._-]/g, "_")}-${randomUUID()}.zip`);
265
+ const output = createWriteStream(zipPath);
266
+ const outputClosed = once(output, "close");
267
+ const outputErrored = once(output, "error").then(([err]) => Promise.reject(err));
268
+ const zip = new Zip((err, chunk, final) => {
269
+ if (err) {
270
+ output.destroy(err);
271
+ return;
272
+ }
273
+ if (chunk?.length) {
274
+ output.write(Buffer.from(chunk));
275
+ }
276
+ if (final) {
277
+ output.end();
278
+ }
279
+ });
280
+ const walk = async (currentDir) => {
281
+ const entries = await readdir(currentDir, { withFileTypes: true });
282
+ for (const entry of entries) {
283
+ const fullPath = join(currentDir, entry.name);
284
+ if (entry.isDirectory()) {
285
+ await walk(fullPath);
286
+ continue;
287
+ }
288
+ if (!entry.isFile()) {
289
+ continue;
290
+ }
291
+ const relPath = relative(dirPath, fullPath).replace(/\\/g, "/");
292
+ if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) {
293
+ throw new Error(`Unsafe relative path while zipping: ${relPath}`);
294
+ }
295
+ const file = new ZipDeflate(relPath);
296
+ zip.add(file);
297
+ file.push(new Uint8Array(await readFile(fullPath)), true);
298
+ }
299
+ };
300
+ try {
301
+ await walk(dirPath);
302
+ zip.end();
303
+ await Promise.race([outputClosed, outputErrored]);
304
+ }
305
+ catch (err) {
306
+ zip.terminate();
307
+ output.destroy(err);
308
+ await cleanupUploadTempPath(zipPath);
309
+ throw err;
310
+ }
311
+ return zipPath;
312
+ }
313
+ async addResource(input, agentId) {
314
+ const pathOrUrl = input.pathOrUrl.trim();
315
+ if (!pathOrUrl) {
316
+ throw new Error("pathOrUrl is required");
317
+ }
318
+ if (input.to && input.parent) {
319
+ throw new Error("Cannot specify both 'to' and 'parent'.");
320
+ }
321
+ const body = {
322
+ to: input.to,
323
+ parent: input.parent,
324
+ reason: input.reason ?? "",
325
+ instruction: input.instruction ?? "",
326
+ wait: input.wait ?? false,
327
+ timeout: input.timeout,
328
+ strict: input.strict ?? false,
329
+ ignore_dirs: input.ignoreDirs,
330
+ include: input.include,
331
+ exclude: input.exclude,
332
+ };
333
+ if (typeof input.preserveStructure === "boolean") {
334
+ body.preserve_structure = input.preserveStructure;
335
+ }
336
+ let cleanupPath;
337
+ const requestTimeoutMs = input.wait ? resolveWaitRequestTimeoutMs(this.timeoutMs, input.timeout) : undefined;
338
+ try {
339
+ if (isRemoteResourceSource(pathOrUrl)) {
340
+ body.path = pathOrUrl;
341
+ }
342
+ else {
343
+ const localStats = await stat(pathOrUrl);
344
+ let uploadPath = pathOrUrl;
345
+ if (localStats.isDirectory()) {
346
+ uploadPath = await this.zipDirectoryForUpload(pathOrUrl);
347
+ cleanupPath = uploadPath;
348
+ body.source_name = basename(pathOrUrl);
349
+ }
350
+ else if (!localStats.isFile()) {
351
+ throw new Error(`Path is not a file or directory: ${pathOrUrl}`);
352
+ }
353
+ body.temp_file_id = await this.uploadTempFile(uploadPath, agentId);
354
+ }
355
+ return this.request("/api/v1/resources", { method: "POST", body: JSON.stringify(body) }, agentId, requestTimeoutMs);
356
+ }
357
+ finally {
358
+ await cleanupUploadTempPath(cleanupPath);
359
+ }
360
+ }
361
+ async addSkill(input, agentId) {
362
+ const hasPath = typeof input.path === "string" && input.path.trim().length > 0;
363
+ const hasData = input.data !== undefined && input.data !== null;
364
+ if (hasPath === hasData) {
365
+ throw new Error("Provide exactly one of 'path' or 'data' for skill import.");
366
+ }
367
+ const body = {
368
+ wait: input.wait ?? false,
369
+ timeout: input.timeout,
370
+ };
371
+ let cleanupPath;
372
+ const requestTimeoutMs = input.wait ? resolveWaitRequestTimeoutMs(this.timeoutMs, input.timeout) : undefined;
373
+ try {
374
+ if (hasPath) {
375
+ const skillPath = input.path.trim();
376
+ const localStats = await stat(skillPath);
377
+ let uploadPath = skillPath;
378
+ if (localStats.isDirectory()) {
379
+ uploadPath = await this.zipDirectoryForUpload(skillPath);
380
+ cleanupPath = uploadPath;
381
+ }
382
+ else if (!localStats.isFile()) {
383
+ throw new Error(`Path is not a file or directory: ${skillPath}`);
384
+ }
385
+ body.temp_file_id = await this.uploadTempFile(uploadPath, agentId);
386
+ }
387
+ else {
388
+ body.data = input.data;
389
+ }
390
+ return this.request("/api/v1/skills", { method: "POST", body: JSON.stringify(body) }, agentId, requestTimeoutMs);
391
+ }
392
+ finally {
393
+ await cleanupUploadTempPath(cleanupPath);
394
+ }
395
+ }
396
+ async addSessionMessage(sessionId, role, parts, agentId, createdAt, roleId) {
397
+ const body = { role, parts };
398
+ if (createdAt) {
399
+ body.created_at = createdAt;
400
+ }
401
+ if (roleId) {
402
+ body.role_id = roleId;
403
+ }
404
+ await this.emitRoutingDebug("session message POST (with parts)", {
405
+ path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
406
+ sessionId,
407
+ role,
408
+ role_id: roleId ?? null,
409
+ partCount: parts.length,
410
+ created_at: createdAt ?? null,
411
+ }, agentId);
412
+ await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`, {
413
+ method: "POST",
414
+ body: JSON.stringify(body),
415
+ }, agentId);
416
+ }
417
+ /** GET session — server auto-creates if absent; returns session meta including message stats and token usage. */
418
+ async getSession(sessionId, agentId) {
419
+ return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { method: "GET" }, agentId);
420
+ }
421
+ /**
422
+ * Commit a session: archive (Phase 1) and extract memories (Phase 2).
423
+ *
424
+ * wait=false (default): returns immediately after Phase 1 with task_id.
425
+ * wait=true: after Phase 1, polls GET /tasks/{task_id} until Phase 2
426
+ * completes (or times out), then returns the merged result.
427
+ */
428
+ async commitSession(sessionId, options) {
429
+ const keepRecentCount = options?.keepRecentCount != null && Number.isFinite(options.keepRecentCount)
430
+ ? Math.max(0, Math.floor(options.keepRecentCount))
431
+ : 0;
432
+ await this.emitRoutingDebug("session commit POST (archive + memory extraction)", {
433
+ path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`,
434
+ sessionId,
435
+ wait: options?.wait ?? false,
436
+ keepRecentCount,
437
+ }, options?.agentId);
438
+ const body = {};
439
+ if (keepRecentCount > 0) {
440
+ body.keep_recent_count = keepRecentCount;
441
+ }
442
+ const result = await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`, { method: "POST", body: JSON.stringify(body) }, options?.agentId);
443
+ if (!options?.wait || !result.task_id) {
444
+ return result;
445
+ }
446
+ // Client-side poll until Phase 2 finishes
447
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_PHASE2_POLL_TIMEOUT_MS);
448
+ const pollInterval = 500;
449
+ while (Date.now() < deadline) {
450
+ await sleep(pollInterval);
451
+ const task = await this.getTask(result.task_id, options.agentId).catch(() => null);
452
+ if (!task)
453
+ break;
454
+ if (task.status === "completed") {
455
+ const taskResult = (task.result ?? {});
456
+ const memoriesExtracted = (taskResult.memories_extracted ?? {});
457
+ result.status = "completed";
458
+ result.memories_extracted = memoriesExtracted;
459
+ return result;
460
+ }
461
+ if (task.status === "failed") {
462
+ result.status = "failed";
463
+ result.error = task.error;
464
+ return result;
465
+ }
466
+ }
467
+ result.status = "timeout";
468
+ return result;
469
+ }
470
+ /** Poll a background task by ID. */
471
+ async getTask(taskId, agentId) {
472
+ return this.request(`/api/v1/tasks/${encodeURIComponent(taskId)}`, { method: "GET" }, agentId);
473
+ }
474
+ async getSessionContext(sessionId, tokenBudget = 128_000, agentId) {
475
+ return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/context?token_budget=${tokenBudget}`, { method: "GET" }, agentId);
476
+ }
477
+ async getSessionArchive(sessionId, archiveId, agentId) {
478
+ return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/archives/${encodeURIComponent(archiveId)}`, { method: "GET" }, agentId);
479
+ }
480
+ async grepSessionArchives(sessionId, pattern, options = {}) {
481
+ const baseUri = `viking://session/${sessionId}/history`;
482
+ const uri = options.archiveId ? `${baseUri}/${options.archiveId}` : baseUri;
483
+ return this.request("/api/v1/search/grep", {
484
+ method: "POST",
485
+ body: JSON.stringify({
486
+ uri,
487
+ pattern,
488
+ case_insensitive: options.caseInsensitive ?? true,
489
+ ...(options.nodeLimit !== undefined ? { node_limit: options.nodeLimit } : {}),
490
+ ...(options.levelLimit !== undefined ? { level_limit: options.levelLimit } : {}),
491
+ }),
492
+ }, options.agentId);
493
+ }
494
+ async deleteSession(sessionId, agentId) {
495
+ await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" }, agentId);
496
+ }
497
+ async deleteUri(uri, agentId) {
498
+ await this.request(`/api/v1/fs?uri=${encodeURIComponent(uri)}&recursive=false`, {
499
+ method: "DELETE",
500
+ }, agentId);
501
+ }
502
+ }