@context-use/openclaw-memory 0.1.0-beta.32 → 0.1.0-beta.523
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/dist/bootstrap.js +50 -26
- package/dist/index.js +949 -97
- package/dist/remove-worker.js +1299 -0
- package/dist/setup-prompt.js +7 -3
- package/dist/setup-runtime.js +16 -0
- package/dist/setup.js +663 -175
- package/openclaw.plugin.json +3 -1
- package/package.json +1 -1
package/dist/setup.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "@context-use/openclaw-memory",
|
|
5
|
-
version: "0.1.0-beta.
|
|
5
|
+
version: "0.1.0-beta.523",
|
|
6
6
|
description: "Context Use as OpenClaw's sole durable personal memory",
|
|
7
7
|
type: "module",
|
|
8
8
|
license: "MIT",
|
|
@@ -83,16 +83,21 @@ ${openclawConnectCommand(serverUrl)}
|
|
|
83
83
|
|
|
84
84
|
Follow the plugin's instructions: give me the authorization URL and ask me to return the final localhost redirect URL. When I send it back, finish connecting. Let the plugin manage configuration and gateway refresh.
|
|
85
85
|
|
|
86
|
-
After authorization,
|
|
86
|
+
After authorization, tell me to start a new session with /new so the gateway can finish applying the memory settings. Verify openclaw context-use status in that new session before confirming that memory is active.`;
|
|
87
|
+
}
|
|
88
|
+
var OPENCLAW_REMOVAL_PROMPT = `Remove the Context Use memory plugin using ${OPENCLAW_INSTALL_COMMAND} remove in this OpenClaw profile. This also works if authorization was never completed or the plugin is disabled. Let the plugin restore its configuration changes, clear its credentials and refresh the gateway. Preserve my remote memories and conversation history. Removal runs separately: end this turn once it is queued, then check ${OPENCLAW_INSTALL_COMMAND} status in a new turn. Do not poll from the requesting turn, schedule retries, stop the gateway, or reinstall to finish removal. Verify that a fresh conversation uses the restored memory provider.`;
|
|
89
|
+
function openclawReconnectPrompt(serverUrl) {
|
|
90
|
+
return `Reauthorize my Context Use memory plugin for ${serverUrl}. Run ${OPENCLAW_INSTALL_COMMAND} reconnect '${serverUrl.replaceAll("'", "'\\''")}' in the same OpenClaw profile. Give me the new authorization URL and ask me to return the final localhost redirect URL. Pass that URL to ${OPENCLAW_INSTALL_COMMAND} authorize through standard input. Let the plugin manage configuration and gateway refresh. After authorization, tell me to start a new session with /new, then verify status in that new session before confirming that memory is active.`;
|
|
87
91
|
}
|
|
88
|
-
var OPENCLAW_REMOVAL_PROMPT = `Remove the Context Use memory plugin using ${OPENCLAW_INSTALL_COMMAND} remove in this OpenClaw profile. This also works if authorization was never completed or the plugin is disabled. Let the plugin restore its configuration changes, clear its credentials and refresh the gateway. Preserve my remote memories and conversation history. Verify that a fresh conversation uses the restored memory provider.`;
|
|
89
92
|
|
|
90
93
|
// src/setup.ts
|
|
91
94
|
import { text } from "node:stream/consumers";
|
|
92
95
|
|
|
93
96
|
// src/connection.ts
|
|
94
|
-
import {
|
|
95
|
-
import {
|
|
97
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
98
|
+
import { existsSync } from "node:fs";
|
|
99
|
+
import { rm as rm4 } from "node:fs/promises";
|
|
100
|
+
import { join as join5 } from "node:path";
|
|
96
101
|
import { auth } from "@modelcontextprotocol/client";
|
|
97
102
|
import {
|
|
98
103
|
mutateConfigFile,
|
|
@@ -117,7 +122,10 @@ var OPENCLAW_VERSION_RANGE = package_default.peerDependencies.openclaw;
|
|
|
117
122
|
var CALLBACK_URL = "http://127.0.0.1:49187/context-use/callback";
|
|
118
123
|
var REQUEST_TIMEOUT_MS = 30000;
|
|
119
124
|
var AUTHORIZATION_TIMEOUT_MS = 600000;
|
|
125
|
+
var AUTHORIZATION_SCOPE = "mcp offline_access";
|
|
120
126
|
var toolName = (name) => `context_use_${name}`;
|
|
127
|
+
var FINISH_LEARNING_TOOL = toolName("finish_learning");
|
|
128
|
+
var SAVE_ATTACHMENT_TOOL = toolName("save_learning_attachment");
|
|
121
129
|
var PluginConfigSchema = z.strictObject({
|
|
122
130
|
agentId: z.string().regex(/^[a-z0-9][a-z0-9_-]*$/),
|
|
123
131
|
serverUrl: z.url()
|
|
@@ -164,13 +172,27 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
164
172
|
|
|
165
173
|
// src/state.ts
|
|
166
174
|
import { readFileSync } from "node:fs";
|
|
167
|
-
import { mkdir, readFile
|
|
168
|
-
import { join } from "node:path";
|
|
175
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
176
|
+
import { dirname, join } from "node:path";
|
|
169
177
|
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
170
178
|
import { lock } from "proper-lockfile";
|
|
171
179
|
import { z as z2 } from "zod";
|
|
180
|
+
|
|
181
|
+
// src/private-files.ts
|
|
182
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
172
183
|
var PRIVATE_DIRECTORY_MODE = 448;
|
|
173
184
|
var PRIVATE_FILE_MODE = 384;
|
|
185
|
+
async function writePrivateFile(input) {
|
|
186
|
+
const temporary = `${input.path}.tmp`;
|
|
187
|
+
try {
|
|
188
|
+
await writeFile(temporary, input.data, { mode: PRIVATE_FILE_MODE });
|
|
189
|
+
await rename(temporary, input.path);
|
|
190
|
+
} finally {
|
|
191
|
+
await rm(temporary, { force: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/state.ts
|
|
174
196
|
var LOCK_STALE_MS = 120000;
|
|
175
197
|
var LOCK_RETRIES = 300;
|
|
176
198
|
var LOCK_RETRY_MS = 100;
|
|
@@ -180,6 +202,7 @@ var ChangeSchema = z2.object({
|
|
|
180
202
|
applied: z2.unknown()
|
|
181
203
|
});
|
|
182
204
|
var ConnectionStateSchema = z2.object({
|
|
205
|
+
learningId: z2.string().uuid().optional(),
|
|
183
206
|
config: PluginConfigSchema,
|
|
184
207
|
changes: z2.array(ChangeSchema),
|
|
185
208
|
tools: z2.array(z2.object({
|
|
@@ -209,18 +232,16 @@ async function readState(directory) {
|
|
|
209
232
|
}
|
|
210
233
|
}
|
|
211
234
|
async function writeState(input) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
} finally {
|
|
218
|
-
await rm(temporary, { force: true });
|
|
219
|
-
}
|
|
235
|
+
await mkdir(input.directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
236
|
+
await writePrivateFile({
|
|
237
|
+
path: join(input.directory, "connection.json"),
|
|
238
|
+
data: JSON.stringify(input.state)
|
|
239
|
+
});
|
|
220
240
|
}
|
|
221
241
|
async function withConnection(input) {
|
|
222
|
-
await mkdir(input.directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
242
|
+
await mkdir(dirname(input.directory), { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
223
243
|
const release = await lock(input.directory, {
|
|
244
|
+
realpath: false,
|
|
224
245
|
stale: LOCK_STALE_MS,
|
|
225
246
|
retries: { retries: LOCK_RETRIES, minTimeout: LOCK_RETRY_MS, maxTimeout: LOCK_RETRY_MS }
|
|
226
247
|
});
|
|
@@ -254,7 +275,7 @@ function oauthProvider(input) {
|
|
|
254
275
|
grant_types: ["authorization_code", "refresh_token"],
|
|
255
276
|
response_types: ["code"],
|
|
256
277
|
token_endpoint_auth_method: "none",
|
|
257
|
-
scope:
|
|
278
|
+
scope: AUTHORIZATION_SCOPE
|
|
258
279
|
},
|
|
259
280
|
state: () => randomBytes(STATE_BYTES).toString("hex"),
|
|
260
281
|
clientInformation: () => oauth.client,
|
|
@@ -386,28 +407,29 @@ async function discoverTools(client) {
|
|
|
386
407
|
}
|
|
387
408
|
return tools.filter((tool) => MCP_TOOL_NAMES.some((name) => name === tool.name));
|
|
388
409
|
}
|
|
389
|
-
async function
|
|
410
|
+
async function withMemoryClient(input) {
|
|
390
411
|
return await withConnection({
|
|
391
412
|
directory: input.directory,
|
|
392
413
|
run: async () => {
|
|
393
414
|
input.signal?.throwIfAborted();
|
|
394
415
|
const state = await readState(input.directory);
|
|
395
|
-
if (!state || state.config.serverUrl !== input.serverUrl || state.config.agentId !== input.agentId || !state.oauth.tokens) {
|
|
416
|
+
if (!state || state.learningId !== input.connectionId || state.config.serverUrl !== input.serverUrl || state.config.agentId !== input.agentId || !state.oauth.tokens) {
|
|
396
417
|
throw new ConnectionError("Context Use is disconnected or requires authorization.");
|
|
397
418
|
}
|
|
398
419
|
return await withClient({
|
|
399
420
|
directory: input.directory,
|
|
400
421
|
state,
|
|
401
|
-
run:
|
|
402
|
-
return await client.callTool({ name: input.name, arguments: input.arguments }, {
|
|
403
|
-
timeout: REQUEST_TIMEOUT_MS,
|
|
404
|
-
signal: input.signal
|
|
405
|
-
});
|
|
406
|
-
}
|
|
422
|
+
run: input.run
|
|
407
423
|
});
|
|
408
424
|
}
|
|
409
425
|
});
|
|
410
426
|
}
|
|
427
|
+
async function callMemoryTool(input) {
|
|
428
|
+
return await withMemoryClient({
|
|
429
|
+
...input,
|
|
430
|
+
run: (client) => client.callTool({ name: input.name, arguments: input.arguments }, { timeout: REQUEST_TIMEOUT_MS, signal: input.signal })
|
|
431
|
+
});
|
|
432
|
+
}
|
|
411
433
|
|
|
412
434
|
// src/configuration.ts
|
|
413
435
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -430,6 +452,12 @@ Do not save transient chatter, generic knowledge, unsupported inference, secrets
|
|
|
430
452
|
authorization links, redirect URLs or authorization codes. Respect requests not to retain facts.
|
|
431
453
|
If nothing merits preservation, make no write. If an operation fails or its outcome is uncertain,
|
|
432
454
|
do not claim success or blindly replay a create. Keep the final reply focused on the user's task.`;
|
|
455
|
+
var CONVERSATION_LEARNING_GUIDANCE = `When background learning is enabled, Context Use queues conversation evidence for background learning after turns and before resets
|
|
456
|
+
and compactions. The Gateway curates this evidence without holding up your reply. Leave routine
|
|
457
|
+
learning to that worker. If the user explicitly asks you to remember, correct or forget something,
|
|
458
|
+
handle it now through Context Use tools: read the curation guide, search and read existing knowledge,
|
|
459
|
+
then make the smallest supported change. Respect retention preferences. Do not claim information
|
|
460
|
+
has been saved until the required writes succeed; queued background work is not a completed save.`;
|
|
433
461
|
var MEMORY_GUIDANCE = `Context Use is your sole durable personal memory. Read and write
|
|
434
462
|
through its context_use_ tools whenever useful, including background work. Use it across your
|
|
435
463
|
connected conversations. If Context Use is unavailable, report the limitation when relevant;
|
|
@@ -510,29 +538,45 @@ function prepareConfiguration(input) {
|
|
|
510
538
|
assertPersonalConfiguration(input);
|
|
511
539
|
const grants = ["agents", "entries", input.state.config.agentId, "tools", "alsoAllow"];
|
|
512
540
|
const allow = ["agents", "entries", input.state.config.agentId, "tools", "allow"];
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
const
|
|
541
|
+
const previousGrant = input.state.changes.find((change) => isDeepStrictEqual(change.path, grants) || isDeepStrictEqual(change.path, allow));
|
|
542
|
+
const grantPath = previousGrant?.path ?? (readPath({ config, path: allow }) === undefined ? grants : allow);
|
|
543
|
+
const alsoAllow = previousGrant?.applied ?? readPath({ config, path: grantPath });
|
|
544
|
+
const pluginAllow = input.state.changes.find((change) => isDeepStrictEqual(change.path, ["plugins", "allow"]))?.applied ?? config.plugins?.allow;
|
|
516
545
|
const plan = configurationPlan(input.state);
|
|
517
546
|
plan.push({
|
|
518
547
|
path: grantPath,
|
|
519
548
|
value: [...new Set([...Array.isArray(alsoAllow) ? alsoAllow : [], PLUGIN_ID])]
|
|
520
549
|
});
|
|
521
|
-
if (pluginAllow) {
|
|
550
|
+
if (Array.isArray(pluginAllow)) {
|
|
522
551
|
plan.push({
|
|
523
552
|
path: ["plugins", "allow"],
|
|
524
553
|
value: [...new Set([...pluginAllow, PLUGIN_ID, "active-memory"])]
|
|
525
554
|
});
|
|
526
555
|
}
|
|
527
|
-
|
|
556
|
+
const previousConnection = input.state.changes.find((change) => isDeepStrictEqual(change.path, ["plugins", "entries", PLUGIN_ID, "config"]));
|
|
557
|
+
const reconfiguring = Boolean(previousConnection && isDeepStrictEqual(readPath({ config, path: previousConnection.path }), previousConnection.applied));
|
|
558
|
+
applyPlan({ ...input, plan, reconfiguring });
|
|
559
|
+
}
|
|
560
|
+
function mergeListChange(input) {
|
|
561
|
+
const removed = input.from.filter((value) => !input.to.includes(value));
|
|
562
|
+
const added = input.to.filter((value) => !input.from.includes(value));
|
|
563
|
+
return [...new Set([...input.current.filter((value) => !removed.includes(value)), ...added])];
|
|
528
564
|
}
|
|
529
565
|
function applyPlan(input) {
|
|
530
566
|
const config = input.config;
|
|
531
567
|
for (const { path, value } of input.plan) {
|
|
532
568
|
const before = readPath({ config, path });
|
|
533
569
|
const existing = input.state.changes.find((change) => isDeepStrictEqual(change.path, path));
|
|
570
|
+
if (existing && isDeepStrictEqual(value, existing.applied) && (input.reconfiguring || !isDeepStrictEqual(before, existing.before))) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
let updated = value;
|
|
534
574
|
if (existing && !isDeepStrictEqual(before, existing.applied) && !isDeepStrictEqual(before, existing.before)) {
|
|
535
|
-
|
|
575
|
+
if (Array.isArray(before) && Array.isArray(existing.applied) && Array.isArray(value)) {
|
|
576
|
+
updated = mergeListChange({ current: before, from: existing.applied, to: value });
|
|
577
|
+
} else {
|
|
578
|
+
throw new ConnectionError(`Configuration changed since setup: ${path.join(".")}. Disconnect before reconfiguring.`);
|
|
579
|
+
}
|
|
536
580
|
}
|
|
537
581
|
if (!existing) {
|
|
538
582
|
input.state.changes.push({
|
|
@@ -543,7 +587,7 @@ function applyPlan(input) {
|
|
|
543
587
|
} else {
|
|
544
588
|
existing.applied = structuredClone(value);
|
|
545
589
|
}
|
|
546
|
-
writePath({ config, path, value });
|
|
590
|
+
writePath({ config, path, value: updated });
|
|
547
591
|
}
|
|
548
592
|
}
|
|
549
593
|
function restoreConfiguration(input) {
|
|
@@ -557,12 +601,15 @@ function restoreConfiguration(input) {
|
|
|
557
601
|
const current = readPath({ config: input.config, path: change.path });
|
|
558
602
|
if (isDeepStrictEqual(current, change.applied)) {
|
|
559
603
|
writePath({ config: input.config, path: change.path, value: change.before });
|
|
560
|
-
} else if (Array.isArray(current) && Array.isArray(change.applied) && (change.before === undefined || Array.isArray(change.before)) && ["allow", "alsoAllow"].includes(change.path.at(-1) ?? "")) {
|
|
561
|
-
const added = change.applied.filter((value) => !change.before?.includes(value));
|
|
604
|
+
} else if (Array.isArray(current) && Array.isArray(change.applied) && (change.before === undefined || Array.isArray(change.before)) && ["allow", "alsoAllow", "toolsAllow"].includes(change.path.at(-1) ?? "")) {
|
|
562
605
|
writePath({
|
|
563
606
|
config: input.config,
|
|
564
607
|
path: change.path,
|
|
565
|
-
value:
|
|
608
|
+
value: mergeListChange({
|
|
609
|
+
current,
|
|
610
|
+
from: change.applied,
|
|
611
|
+
to: change.before ?? []
|
|
612
|
+
})
|
|
566
613
|
});
|
|
567
614
|
} else if (!isDeepStrictEqual(current, change.before)) {
|
|
568
615
|
preserved.push(change.path.join("."));
|
|
@@ -586,24 +633,449 @@ function assertPersonalConfiguration(input) {
|
|
|
586
633
|
}
|
|
587
634
|
}
|
|
588
635
|
|
|
636
|
+
// src/learning-store.ts
|
|
637
|
+
import { randomUUID } from "node:crypto";
|
|
638
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
639
|
+
import { join as join2 } from "node:path";
|
|
640
|
+
import { DatabaseSync } from "node:sqlite";
|
|
641
|
+
var BATCH_CHARS = 48000;
|
|
642
|
+
var LEARNING_SESSION_PREFIX = "subagent:context-use-learning:";
|
|
643
|
+
var LEARNING_SESSION_PATTERN = new RegExp(`^agent:[^:]+:${LEARNING_SESSION_PREFIX}`);
|
|
644
|
+
function isLearningSession(sessionKey) {
|
|
645
|
+
return LEARNING_SESSION_PATTERN.test(sessionKey ?? "");
|
|
646
|
+
}
|
|
647
|
+
var attachmentDirectory = (connectionId) => `learning-${encodeURIComponent(connectionId)}-attachments`;
|
|
648
|
+
var DREAM_INTERVAL_MS = 21600000;
|
|
649
|
+
var learningDatabase = (connectionId) => `learning-${encodeURIComponent(connectionId)}.sqlite`;
|
|
650
|
+
|
|
651
|
+
class LearningStore {
|
|
652
|
+
db;
|
|
653
|
+
constructor(input) {
|
|
654
|
+
mkdirSync(input.directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
655
|
+
const path = join2(input.directory, learningDatabase(input.connectionId));
|
|
656
|
+
this.db = new DatabaseSync(path);
|
|
657
|
+
chmodSync(path, PRIVATE_FILE_MODE);
|
|
658
|
+
this.db.exec(`
|
|
659
|
+
PRAGMA busy_timeout = 1000;
|
|
660
|
+
PRAGMA secure_delete = ON;
|
|
661
|
+
CREATE TABLE IF NOT EXISTS owner (config TEXT NOT NULL);
|
|
662
|
+
CREATE TABLE IF NOT EXISTS evidence (
|
|
663
|
+
id INTEGER PRIMARY KEY, source TEXT NOT NULL, fingerprint TEXT NOT NULL,
|
|
664
|
+
text TEXT, capturedAt INTEGER NOT NULL, UNIQUE(source, fingerprint)
|
|
665
|
+
);
|
|
666
|
+
CREATE TABLE IF NOT EXISTS job (
|
|
667
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
|
|
668
|
+
id TEXT NOT NULL, kind TEXT NOT NULL, sessionKey TEXT NOT NULL,
|
|
669
|
+
source TEXT NOT NULL, through INTEGER NOT NULL, evidence TEXT NOT NULL,
|
|
670
|
+
runId TEXT, acknowledged INTEGER NOT NULL DEFAULT 0, startedAt INTEGER NOT NULL DEFAULT 0
|
|
671
|
+
);
|
|
672
|
+
CREATE TABLE IF NOT EXISTS retries (source TEXT PRIMARY KEY, retryAt INTEGER NOT NULL);
|
|
673
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
674
|
+
source TEXT NOT NULL, key TEXT NOT NULL, evidenceId INTEGER NOT NULL,
|
|
675
|
+
data TEXT, asset TEXT, uploadName TEXT, PRIMARY KEY (source, key)
|
|
676
|
+
);
|
|
677
|
+
CREATE TABLE IF NOT EXISTS dreaming (
|
|
678
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton = 1), through INTEGER NOT NULL,
|
|
679
|
+
completedAt INTEGER NOT NULL
|
|
680
|
+
);
|
|
681
|
+
`);
|
|
682
|
+
const owner = JSON.stringify({ ...input.config, connectionId: input.connectionId });
|
|
683
|
+
this.db.prepare("INSERT INTO owner SELECT ? WHERE NOT EXISTS (SELECT 1 FROM owner)").run(owner);
|
|
684
|
+
if (this.db.prepare("SELECT config FROM owner").get().config !== owner) {
|
|
685
|
+
this.db.close();
|
|
686
|
+
throw new Error("Context Use learning queue belongs to a different connection.");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
capture(input) {
|
|
690
|
+
this.transaction(() => {
|
|
691
|
+
const insert = this.db.prepare("INSERT OR IGNORE INTO evidence(source, fingerprint, text, capturedAt) VALUES (?, ?, ?, ?)");
|
|
692
|
+
for (const item of input.evidence) {
|
|
693
|
+
insert.run(input.source, item.key, item.text, input.now);
|
|
694
|
+
if (item.attachment) {
|
|
695
|
+
this.db.prepare(`INSERT OR IGNORE INTO attachments(source, key, evidenceId, data)
|
|
696
|
+
SELECT source, fingerprint, id, ? FROM evidence WHERE source = ? AND fingerprint = ?`).run(JSON.stringify(item.attachment), input.source, item.key);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
current() {
|
|
702
|
+
return this.db.prepare("SELECT * FROM job").get();
|
|
703
|
+
}
|
|
704
|
+
next(input) {
|
|
705
|
+
return this.transaction(() => {
|
|
706
|
+
const existing = this.current();
|
|
707
|
+
if (existing) {
|
|
708
|
+
return existing;
|
|
709
|
+
}
|
|
710
|
+
const first = this.db.prepare("SELECT e.source FROM evidence e LEFT JOIN retries r ON r.source = e.source WHERE e.text IS NOT NULL AND (r.retryAt IS NULL OR r.retryAt <= ?) ORDER BY e.id LIMIT 1").get(input.now);
|
|
711
|
+
let { through, evidence } = this.batch(first?.source);
|
|
712
|
+
if (!first) {
|
|
713
|
+
through = this.dreamThrough(input.now);
|
|
714
|
+
if (!through) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const id = randomUUID();
|
|
719
|
+
this.db.prepare("INSERT INTO job(singleton, id, kind, sessionKey, source, through, evidence) VALUES (1, ?, ?, ?, ?, ?, ?)").run(id, first ? "learn" : "dream", `agent:${input.agentId}:${LEARNING_SESSION_PREFIX}${id}`, first?.source ?? "", through, evidence);
|
|
720
|
+
return this.current();
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
batch(source) {
|
|
724
|
+
const rows = source ? this.db.prepare("SELECT id, text, capturedAt FROM evidence WHERE source = ? AND text IS NOT NULL ORDER BY id LIMIT 100").all(source) : [];
|
|
725
|
+
let through = 0;
|
|
726
|
+
let evidence = "";
|
|
727
|
+
for (const row of rows) {
|
|
728
|
+
if (evidence.length + row.text.length > BATCH_CHARS) {
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
evidence += `${JSON.stringify({ observedAt: new Date(row.capturedAt).toISOString(), message: JSON.parse(row.text) })}
|
|
732
|
+
`;
|
|
733
|
+
through = row.id;
|
|
734
|
+
}
|
|
735
|
+
return { through, evidence };
|
|
736
|
+
}
|
|
737
|
+
dreamThrough(now) {
|
|
738
|
+
if (this.status().pending || this.db.prepare("SELECT 1 FROM retries WHERE source = '' AND retryAt > ?").get(now)) {
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
const progress = this.db.prepare("SELECT MAX(id) AS through, MIN(capturedAt) AS since FROM evidence").get();
|
|
742
|
+
const dream = this.db.prepare("SELECT through, completedAt FROM dreaming").get();
|
|
743
|
+
if (!progress.through || progress.through <= (dream?.through ?? 0) || now - (dream?.completedAt ?? progress.since ?? now) < DREAM_INTERVAL_MS) {
|
|
744
|
+
return 0;
|
|
745
|
+
}
|
|
746
|
+
return progress.through;
|
|
747
|
+
}
|
|
748
|
+
started(input) {
|
|
749
|
+
this.db.prepare("UPDATE job SET runId = ?, startedAt = ? WHERE id = ?").run(input.runId, input.now, input.id);
|
|
750
|
+
}
|
|
751
|
+
attachments(job) {
|
|
752
|
+
const rows = this.db.prepare("SELECT key, data, asset FROM attachments WHERE source = ? AND evidenceId <= ? AND data IS NOT NULL ORDER BY evidenceId").all(job.source, job.through);
|
|
753
|
+
return rows.map((row) => ({
|
|
754
|
+
key: row.key,
|
|
755
|
+
attachment: JSON.parse(row.data),
|
|
756
|
+
asset: row.asset
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
attachmentSource(input) {
|
|
760
|
+
const job = this.current();
|
|
761
|
+
if (!job || job.id !== input.jobId || !this.attachments(job).some((item) => item.key === input.key)) {
|
|
762
|
+
throw new Error("This attachment is not part of the active learning job.");
|
|
763
|
+
}
|
|
764
|
+
return job.source;
|
|
765
|
+
}
|
|
766
|
+
prepareAttachmentUpload(input) {
|
|
767
|
+
const source = this.attachmentSource(input);
|
|
768
|
+
this.db.prepare("UPDATE attachments SET uploadName = COALESCE(uploadName, ?) WHERE source = ? AND key = ?").run(input.name, source, input.key);
|
|
769
|
+
return this.db.prepare("SELECT uploadName FROM attachments WHERE source = ? AND key = ?").get(source, input.key).uploadName;
|
|
770
|
+
}
|
|
771
|
+
clearAttachmentUploadName(input) {
|
|
772
|
+
const source = this.attachmentSource(input);
|
|
773
|
+
this.db.prepare("UPDATE attachments SET uploadName = NULL WHERE source = ? AND key = ?").run(source, input.key);
|
|
774
|
+
}
|
|
775
|
+
savedAttachment(input) {
|
|
776
|
+
const source = this.attachmentSource(input);
|
|
777
|
+
this.db.prepare("UPDATE attachments SET asset = ? WHERE source = ? AND key = ?").run(input.asset, source, input.key);
|
|
778
|
+
}
|
|
779
|
+
acknowledge(input) {
|
|
780
|
+
const { sessionKey, omittedAttachments = [] } = input;
|
|
781
|
+
const job = this.current();
|
|
782
|
+
if (job?.sessionKey === sessionKey && this.attachments(job).some((item) => !item.asset && !omittedAttachments.includes(item.key))) {
|
|
783
|
+
throw new Error("Save every attachment or explicitly exclude it for a retention reason before finishing. Failed uploads must be retried.");
|
|
784
|
+
}
|
|
785
|
+
const result = this.db.prepare("UPDATE job SET acknowledged = 1 WHERE sessionKey = ?").run(sessionKey);
|
|
786
|
+
if (!result.changes) {
|
|
787
|
+
throw new Error("This learning run is no longer active.");
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
retry(input) {
|
|
791
|
+
this.transaction(() => {
|
|
792
|
+
const job = this.current();
|
|
793
|
+
if (!job || job.id !== input.id) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
this.db.prepare("INSERT OR REPLACE INTO retries VALUES (?, ?)").run(job.source, input.at);
|
|
797
|
+
this.db.prepare("DELETE FROM job WHERE id = ?").run(input.id);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
finish(input) {
|
|
801
|
+
this.transaction(() => {
|
|
802
|
+
const job = this.current();
|
|
803
|
+
if (!job || job.id !== input.id || !job.acknowledged) {
|
|
804
|
+
throw new Error("Learning must be acknowledged before removing its evidence.");
|
|
805
|
+
}
|
|
806
|
+
if (job.kind === "learn") {
|
|
807
|
+
this.db.prepare("UPDATE evidence SET text = NULL WHERE source = ? AND id <= ?").run(job.source, job.through);
|
|
808
|
+
this.db.prepare("UPDATE attachments SET data = NULL WHERE source = ? AND evidenceId <= ?").run(job.source, job.through);
|
|
809
|
+
} else {
|
|
810
|
+
this.db.prepare("INSERT OR REPLACE INTO dreaming VALUES (1, ?, ?)").run(job.through, input.now);
|
|
811
|
+
}
|
|
812
|
+
this.db.prepare("DELETE FROM retries WHERE source = ?").run(job.source);
|
|
813
|
+
this.db.prepare("DELETE FROM job WHERE id = ?").run(job.id);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
status() {
|
|
817
|
+
return {
|
|
818
|
+
pending: this.db.prepare("SELECT COUNT(*) AS count FROM evidence WHERE text IS NOT NULL").get().count,
|
|
819
|
+
running: Boolean(this.current()?.runId),
|
|
820
|
+
retryAt: this.db.prepare("SELECT MIN(retryAt) AS at FROM retries").get().at ?? undefined
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
close() {
|
|
824
|
+
this.db.close();
|
|
825
|
+
}
|
|
826
|
+
transaction(run) {
|
|
827
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
828
|
+
try {
|
|
829
|
+
const result = run();
|
|
830
|
+
this.db.exec("COMMIT");
|
|
831
|
+
return result;
|
|
832
|
+
} catch (error) {
|
|
833
|
+
this.db.exec("ROLLBACK");
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/package-installation.ts
|
|
840
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
841
|
+
import { mkdtemp, readFile as readFile2, rm as rm2 } from "node:fs/promises";
|
|
842
|
+
import { createRequire } from "node:module";
|
|
843
|
+
import { tmpdir } from "node:os";
|
|
844
|
+
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
|
845
|
+
import { fileURLToPath } from "node:url";
|
|
846
|
+
import { promisify as promisify2 } from "node:util";
|
|
847
|
+
import { z as z3 } from "zod";
|
|
848
|
+
|
|
849
|
+
// src/host-command.ts
|
|
850
|
+
import { execFile, spawn } from "node:child_process";
|
|
851
|
+
import { promisify } from "node:util";
|
|
852
|
+
var execute = promisify(execFile);
|
|
853
|
+
var HOST_TIMEOUT_MS = 120000;
|
|
854
|
+
async function runSetupCommand(input) {
|
|
855
|
+
const child = spawn(process.execPath, [...input.runtime ? ["--import", input.runtime] : [], input.script, ...input.args], { stdio: "inherit" });
|
|
856
|
+
return await new Promise((resolve, reject) => {
|
|
857
|
+
child.once("error", reject);
|
|
858
|
+
child.once("exit", (code) => resolve(code ?? 1));
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
async function openclaw(args) {
|
|
862
|
+
const result = await execute("openclaw", args, {
|
|
863
|
+
timeout: HOST_TIMEOUT_MS,
|
|
864
|
+
maxBuffer: 4000000
|
|
865
|
+
});
|
|
866
|
+
return result.stdout;
|
|
867
|
+
}
|
|
868
|
+
async function checkHost() {
|
|
869
|
+
const output = await openclaw(["--version"]);
|
|
870
|
+
const version = output.match(/\b\d{4}\.\d+\.\d+(?:-[\w.-]+)?\b/)?.[0];
|
|
871
|
+
if (!version) {
|
|
872
|
+
throw new ConnectionError("Could not determine the installed OpenClaw version.");
|
|
873
|
+
}
|
|
874
|
+
assertHostVersion(version);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/package-installation.ts
|
|
878
|
+
var execute2 = promisify2(execFile2);
|
|
879
|
+
var INSTALL_TIMEOUT_MS = 120000;
|
|
880
|
+
async function pluginInventory() {
|
|
881
|
+
const inventory = z3.object({
|
|
882
|
+
plugins: z3.array(z3.object({
|
|
883
|
+
id: z3.string(),
|
|
884
|
+
rootDir: z3.string(),
|
|
885
|
+
origin: z3.string().optional()
|
|
886
|
+
}))
|
|
887
|
+
}).parse(JSON.parse(await openclaw(["--log-level", "silent", "plugins", "list", "--json"])));
|
|
888
|
+
return inventory.plugins;
|
|
889
|
+
}
|
|
890
|
+
async function installedPlugin() {
|
|
891
|
+
return (await pluginInventory()).find((plugin) => plugin.id === PLUGIN_ID);
|
|
892
|
+
}
|
|
893
|
+
async function hostSdkAnchor() {
|
|
894
|
+
const plugins = await pluginInventory();
|
|
895
|
+
for (const plugin of plugins.filter((entry) => entry.origin === "bundled")) {
|
|
896
|
+
try {
|
|
897
|
+
return createRequire(join3(plugin.rootDir, "package.json")).resolve("openclaw/plugin-sdk/config-mutation");
|
|
898
|
+
} catch {}
|
|
899
|
+
}
|
|
900
|
+
throw new ConnectionError("Could not locate the OpenClaw SDK. Run openclaw plugins doctor and retry.");
|
|
901
|
+
}
|
|
902
|
+
async function installPackage() {
|
|
903
|
+
const existing = await installedPlugin();
|
|
904
|
+
if (existing) {
|
|
905
|
+
const installed = z3.object({ name: z3.string() }).parse(JSON.parse(await readFile2(join3(existing.rootDir, "package.json"), "utf8")));
|
|
906
|
+
if (installed.name !== package_default.name) {
|
|
907
|
+
throw new ConnectionError(`The context-use plugin ID belongs to another package: ${installed.name}.`);
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const root = resolve(dirname2(fileURLToPath(import.meta.url)), "..");
|
|
912
|
+
const directory = await mkdtemp(join3(tmpdir(), "context-use-install-"));
|
|
913
|
+
try {
|
|
914
|
+
const packed = await execute2("npm", ["pack", root, "--ignore-scripts", "--json", "--pack-destination", directory], { timeout: INSTALL_TIMEOUT_MS });
|
|
915
|
+
const [{ filename }] = z3.tuple([z3.object({ filename: z3.string() })]).parse(JSON.parse(packed.stdout));
|
|
916
|
+
console.log(await openclaw([
|
|
917
|
+
"plugins",
|
|
918
|
+
"install",
|
|
919
|
+
join3(directory, filename),
|
|
920
|
+
"--force",
|
|
921
|
+
"--accept-capabilities"
|
|
922
|
+
]));
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (error && typeof error === "object" && "stderr" in error && typeof error.stderr === "string") {
|
|
925
|
+
process.stderr.write(error.stderr);
|
|
926
|
+
}
|
|
927
|
+
throw new ConnectionError("OpenClaw could not install Context Use. Resolve the installer error above and retry connect.");
|
|
928
|
+
} finally {
|
|
929
|
+
await rm2(directory, { recursive: true, force: true });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/removal.ts
|
|
934
|
+
import { fork } from "node:child_process";
|
|
935
|
+
import { once } from "node:events";
|
|
936
|
+
import { readFile as readFile3, rm as rm3 } from "node:fs/promises";
|
|
937
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
938
|
+
import { setTimeout } from "node:timers/promises";
|
|
939
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
940
|
+
import { check, lock as lock2 } from "proper-lockfile";
|
|
941
|
+
import { z as z4 } from "zod";
|
|
942
|
+
var RemovalSchema = z4.object({ error: z4.string().optional() });
|
|
943
|
+
var removalLease = { realpath: false, stale: 30000, update: 2000 };
|
|
944
|
+
var removalFile = (directory) => `${directory}.removal.json`;
|
|
945
|
+
async function removalStatus(directory) {
|
|
946
|
+
try {
|
|
947
|
+
const operation = RemovalSchema.parse(JSON.parse(await readFile3(removalFile(directory), "utf8")));
|
|
948
|
+
const running = await check(removalFile(directory), removalLease);
|
|
949
|
+
return {
|
|
950
|
+
running,
|
|
951
|
+
error: operation.error ?? (running ? undefined : "Removal was interrupted. Run remove again.")
|
|
952
|
+
};
|
|
953
|
+
} catch (error) {
|
|
954
|
+
if (error.code === "ENOENT") {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
throw new ConnectionError("Context Use removal status is unreadable.");
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async function runRemoval(input) {
|
|
961
|
+
const release = await lock2(removalFile(input.directory), removalLease);
|
|
962
|
+
let completed = false;
|
|
963
|
+
try {
|
|
964
|
+
process.send?.("ready");
|
|
965
|
+
await input.run();
|
|
966
|
+
completed = true;
|
|
967
|
+
} catch (error) {
|
|
968
|
+
await writePrivateFile({
|
|
969
|
+
path: removalFile(input.directory),
|
|
970
|
+
data: JSON.stringify({
|
|
971
|
+
error: error instanceof ConnectionError ? error.message : "Removal failed. Run remove again to resume cleanup."
|
|
972
|
+
})
|
|
973
|
+
});
|
|
974
|
+
throw error;
|
|
975
|
+
} finally {
|
|
976
|
+
await withConnection({
|
|
977
|
+
directory: input.directory,
|
|
978
|
+
run: async () => {
|
|
979
|
+
try {
|
|
980
|
+
if (completed) {
|
|
981
|
+
await rm3(removalFile(input.directory), { force: true });
|
|
982
|
+
}
|
|
983
|
+
} finally {
|
|
984
|
+
await release();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function requestRemoval(directory) {
|
|
991
|
+
await withConnection({
|
|
992
|
+
directory,
|
|
993
|
+
run: async () => {
|
|
994
|
+
if ((await removalStatus(directory))?.running) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const state = await readState(directory);
|
|
998
|
+
if (state) {
|
|
999
|
+
state.oauth = {};
|
|
1000
|
+
await writeState({ directory, state });
|
|
1001
|
+
}
|
|
1002
|
+
await writePrivateFile({ path: removalFile(directory), data: "{}" });
|
|
1003
|
+
const scripts = dirname3(fileURLToPath2(import.meta.url));
|
|
1004
|
+
const child = fork(join4(scripts, "remove-worker.js"), [], {
|
|
1005
|
+
execArgv: ["--import", join4(scripts, "setup-runtime.js")],
|
|
1006
|
+
detached: true,
|
|
1007
|
+
stdio: "ignore"
|
|
1008
|
+
});
|
|
1009
|
+
await Promise.race([
|
|
1010
|
+
once(child, "message"),
|
|
1011
|
+
once(child, "exit").then(() => {
|
|
1012
|
+
throw new ConnectionError("Removal could not start. Run remove again.");
|
|
1013
|
+
})
|
|
1014
|
+
]);
|
|
1015
|
+
child.disconnect();
|
|
1016
|
+
child.unref();
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
var REMOVAL_WAIT_MS = 180000;
|
|
1021
|
+
var REMOVAL_POLL_MS = 500;
|
|
1022
|
+
async function waitForRemoval(directory) {
|
|
1023
|
+
const deadline = Date.now() + REMOVAL_WAIT_MS;
|
|
1024
|
+
while (Date.now() < deadline) {
|
|
1025
|
+
const current = await removalStatus(directory);
|
|
1026
|
+
if (!current) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (current.error) {
|
|
1030
|
+
throw new ConnectionError(current.error);
|
|
1031
|
+
}
|
|
1032
|
+
await setTimeout(REMOVAL_POLL_MS);
|
|
1033
|
+
}
|
|
1034
|
+
throw new ConnectionError("Removal is still running. Check status in a new turn.");
|
|
1035
|
+
}
|
|
1036
|
+
async function assertNotRemoving(directory) {
|
|
1037
|
+
if (await removalStatus(directory)) {
|
|
1038
|
+
throw new ConnectionError("Finish removing Context Use before connecting or authorizing again.");
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
589
1042
|
// src/connection.ts
|
|
1043
|
+
var authorizationFetch = (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
590
1044
|
async function activate(input) {
|
|
591
|
-
|
|
1045
|
+
try {
|
|
1046
|
+
input.state.tools = await withClient({ ...input, run: discoverTools });
|
|
1047
|
+
} catch {
|
|
1048
|
+
throw new ConnectionError("Authorization was saved, but the MCP server did not verify memory access. Check the server URL and client access in Context Use Settings, then run reconnect to authorize a fresh connection. The previous memory settings have not been replaced.");
|
|
1049
|
+
}
|
|
1050
|
+
input.state.learningId ??= randomUUID2();
|
|
592
1051
|
await mutateConfigFile({
|
|
1052
|
+
afterWrite: { mode: "none", reason: "Context Use setup requests gateway refresh after commit" },
|
|
593
1053
|
mutate: async (config) => {
|
|
594
1054
|
prepareConfiguration({ config, state: input.state });
|
|
595
1055
|
await writeState(input);
|
|
596
1056
|
}
|
|
597
1057
|
});
|
|
598
1058
|
}
|
|
1059
|
+
async function removeLearningData(input) {
|
|
1060
|
+
await rm4(join5(input.directory, attachmentDirectory(input.connectionId)), {
|
|
1061
|
+
recursive: true,
|
|
1062
|
+
force: true
|
|
1063
|
+
});
|
|
1064
|
+
for (const suffix of ["", "-journal", "-wal", "-shm"]) {
|
|
1065
|
+
await rm4(join5(input.directory, `${learningDatabase(input.connectionId)}${suffix}`), {
|
|
1066
|
+
force: true
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
599
1070
|
async function connect(input) {
|
|
600
1071
|
const config = { serverUrl: serverUrl(input.instance), agentId: input.agentId };
|
|
601
1072
|
return await withConnection({
|
|
602
1073
|
directory: input.directory,
|
|
603
1074
|
run: async () => {
|
|
1075
|
+
await assertNotRemoving(input.directory);
|
|
604
1076
|
const existing = await readState(input.directory);
|
|
605
|
-
if (existing &&
|
|
606
|
-
throw new ConnectionError("Disconnect the existing account before changing
|
|
1077
|
+
if (existing && existing.config.agentId !== config.agentId) {
|
|
1078
|
+
throw new ConnectionError("Disconnect the existing account before changing agent.");
|
|
607
1079
|
}
|
|
608
1080
|
const state = existing ?? { config, changes: [], tools: [], oauth: {} };
|
|
609
1081
|
const { snapshot } = await readConfigFileSnapshotForWrite();
|
|
@@ -611,11 +1083,26 @@ async function connect(input) {
|
|
|
611
1083
|
throw new ConnectionError("OpenClaw configuration is invalid. Run openclaw doctor first.");
|
|
612
1084
|
}
|
|
613
1085
|
assertPersonalConfiguration({ config: snapshot.config, state });
|
|
1086
|
+
await installPackage();
|
|
1087
|
+
if (input.reauthorize || state.config.serverUrl !== config.serverUrl) {
|
|
1088
|
+
const previousLearningId = state.learningId;
|
|
1089
|
+
state.config = config;
|
|
1090
|
+
state.oauth = {};
|
|
1091
|
+
state.tools = [];
|
|
1092
|
+
delete state.learningId;
|
|
1093
|
+
await writeState({ directory: input.directory, state });
|
|
1094
|
+
if (previousLearningId) {
|
|
1095
|
+
await removeLearningData({
|
|
1096
|
+
directory: input.directory,
|
|
1097
|
+
connectionId: previousLearningId
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
614
1101
|
await writeState({ directory: input.directory, state });
|
|
615
1102
|
const result = await auth(oauthProvider({ directory: input.directory, state, interactive: true }), {
|
|
616
1103
|
serverUrl: config.serverUrl,
|
|
617
|
-
scope:
|
|
618
|
-
fetchFn:
|
|
1104
|
+
scope: AUTHORIZATION_SCOPE,
|
|
1105
|
+
fetchFn: authorizationFetch
|
|
619
1106
|
});
|
|
620
1107
|
if (result === "REDIRECT") {
|
|
621
1108
|
return { authorizationUrl: state.oauth.pending?.url };
|
|
@@ -629,6 +1116,7 @@ async function finishAuthorization(input) {
|
|
|
629
1116
|
await withConnection({
|
|
630
1117
|
directory: input.directory,
|
|
631
1118
|
run: async () => {
|
|
1119
|
+
await assertNotRemoving(input.directory);
|
|
632
1120
|
const state = await readState(input.directory);
|
|
633
1121
|
if (!state) {
|
|
634
1122
|
throw new ConnectionError("Start connect before completing authorization.");
|
|
@@ -640,14 +1128,22 @@ async function finishAuthorization(input) {
|
|
|
640
1128
|
const result = await auth(oauthProvider({ directory: input.directory, state, interactive: true }), {
|
|
641
1129
|
serverUrl: state.config.serverUrl,
|
|
642
1130
|
...response,
|
|
643
|
-
fetchFn:
|
|
1131
|
+
fetchFn: authorizationFetch
|
|
644
1132
|
});
|
|
645
1133
|
if (result !== "AUTHORIZED") {
|
|
646
1134
|
throw new ConnectionError("Authorization was not completed. Start connect again.");
|
|
647
1135
|
}
|
|
648
1136
|
delete state.oauth.pending;
|
|
649
1137
|
delete state.oauth.verifier;
|
|
1138
|
+
const previousLearningId = state.learningId;
|
|
1139
|
+
state.learningId = randomUUID2();
|
|
650
1140
|
await writeState({ directory: input.directory, state });
|
|
1141
|
+
if (previousLearningId) {
|
|
1142
|
+
await removeLearningData({
|
|
1143
|
+
directory: input.directory,
|
|
1144
|
+
connectionId: previousLearningId
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
651
1147
|
await activate({ directory: input.directory, state });
|
|
652
1148
|
}
|
|
653
1149
|
});
|
|
@@ -663,6 +1159,10 @@ async function disconnect(directory) {
|
|
|
663
1159
|
}
|
|
664
1160
|
let preserved = [];
|
|
665
1161
|
await mutateConfigFile({
|
|
1162
|
+
afterWrite: {
|
|
1163
|
+
mode: "none",
|
|
1164
|
+
reason: "Context Use setup requests gateway refresh after cleanup"
|
|
1165
|
+
},
|
|
666
1166
|
writeOptions: { allowConfigSizeDrop: true },
|
|
667
1167
|
mutate: (config) => {
|
|
668
1168
|
if (state) {
|
|
@@ -670,7 +1170,7 @@ async function disconnect(directory) {
|
|
|
670
1170
|
}
|
|
671
1171
|
}
|
|
672
1172
|
});
|
|
673
|
-
await
|
|
1173
|
+
await rm4(directory, { recursive: true, force: true });
|
|
674
1174
|
return { preserved };
|
|
675
1175
|
}
|
|
676
1176
|
});
|
|
@@ -690,53 +1190,58 @@ async function status(directory) {
|
|
|
690
1190
|
...state.config
|
|
691
1191
|
};
|
|
692
1192
|
}
|
|
693
|
-
|
|
1193
|
+
let tools;
|
|
1194
|
+
try {
|
|
1195
|
+
tools = await withClient({ directory, state, run: discoverTools });
|
|
1196
|
+
} catch {
|
|
1197
|
+
return {
|
|
1198
|
+
connected: false,
|
|
1199
|
+
authenticated: false,
|
|
1200
|
+
...state.config,
|
|
1201
|
+
error: "MCP access could not be verified. Check the server and run reconnect to authorize a fresh connection."
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
694
1204
|
const { snapshot } = await readConfigFileSnapshotForWrite();
|
|
695
1205
|
const selected = snapshot.config.plugins?.slots?.memory === PLUGIN_ID && snapshot.config.plugins?.entries?.[PLUGIN_ID]?.enabled === true;
|
|
696
|
-
|
|
1206
|
+
const learning = state.learningId && existsSync(join5(directory, learningDatabase(state.learningId))) ? new LearningStore({
|
|
1207
|
+
directory,
|
|
1208
|
+
config: state.config,
|
|
1209
|
+
connectionId: state.learningId
|
|
1210
|
+
}) : undefined;
|
|
1211
|
+
try {
|
|
1212
|
+
return {
|
|
1213
|
+
connected: selected,
|
|
1214
|
+
authenticated: true,
|
|
1215
|
+
...state.config,
|
|
1216
|
+
tools: tools.length,
|
|
1217
|
+
learning: {
|
|
1218
|
+
enabled: Boolean(state.learningId),
|
|
1219
|
+
...learning?.status() ?? { pending: 0, running: false }
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
} finally {
|
|
1223
|
+
learning?.close();
|
|
1224
|
+
}
|
|
697
1225
|
}
|
|
698
1226
|
});
|
|
699
1227
|
}
|
|
700
1228
|
|
|
701
1229
|
// src/gateway.ts
|
|
702
|
-
import { resolve } from "node:path";
|
|
703
|
-
import { z as
|
|
704
|
-
|
|
705
|
-
// src/host-command.ts
|
|
706
|
-
import { execFile } from "node:child_process";
|
|
707
|
-
import { promisify } from "node:util";
|
|
708
|
-
var execute = promisify(execFile);
|
|
709
|
-
var HOST_TIMEOUT_MS = 120000;
|
|
710
|
-
async function openclaw(args) {
|
|
711
|
-
const result = await execute("openclaw", args, {
|
|
712
|
-
timeout: HOST_TIMEOUT_MS,
|
|
713
|
-
maxBuffer: 4000000
|
|
714
|
-
});
|
|
715
|
-
return result.stdout;
|
|
716
|
-
}
|
|
717
|
-
async function checkHost() {
|
|
718
|
-
const output = await openclaw(["--version"]);
|
|
719
|
-
const version = output.match(/\b\d{4}\.\d+\.\d+(?:-[\w.-]+)?\b/)?.[0];
|
|
720
|
-
if (!version) {
|
|
721
|
-
throw new ConnectionError("Could not determine the installed OpenClaw version.");
|
|
722
|
-
}
|
|
723
|
-
assertHostVersion(version);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// src/gateway.ts
|
|
1230
|
+
import { resolve as resolve2 } from "node:path";
|
|
1231
|
+
import { z as z5 } from "zod";
|
|
727
1232
|
async function refreshGateway() {
|
|
728
|
-
const local =
|
|
1233
|
+
const local = z5.object({ path: z5.string() }).parse(JSON.parse(await openclaw(["config", "file", "--json"])));
|
|
729
1234
|
let remote;
|
|
730
1235
|
try {
|
|
731
|
-
remote =
|
|
1236
|
+
remote = z5.object({ path: z5.string() }).parse(JSON.parse(await openclaw(["gateway", "call", "config.get", "--timeout", "3000", "--json"])));
|
|
732
1237
|
} catch {
|
|
733
1238
|
console.log(`Settings saved. No reachable gateway was verified; they apply on its next start. Run ${OPENCLAW_INSTALL_COMMAND} refresh to retry.`);
|
|
734
1239
|
return;
|
|
735
1240
|
}
|
|
736
|
-
if (
|
|
1241
|
+
if (resolve2(remote.path) !== resolve2(local.path)) {
|
|
737
1242
|
throw new ConnectionError(`Settings saved, but the running gateway uses another configuration. Start this profile’s gateway, then run ${OPENCLAW_INSTALL_COMMAND} refresh.`);
|
|
738
1243
|
}
|
|
739
|
-
const result =
|
|
1244
|
+
const result = z5.object({ ok: z5.boolean() }).parse(JSON.parse(await openclaw([
|
|
740
1245
|
"gateway",
|
|
741
1246
|
"call",
|
|
742
1247
|
"gateway.restart.request",
|
|
@@ -751,124 +1256,103 @@ async function refreshGateway() {
|
|
|
751
1256
|
}
|
|
752
1257
|
|
|
753
1258
|
// src/host.ts
|
|
754
|
-
import { z as
|
|
1259
|
+
import { z as z6 } from "zod";
|
|
755
1260
|
async function verifyRuntime() {
|
|
756
|
-
const { plugin } =
|
|
1261
|
+
const { plugin } = z6.object({ plugin: z6.object({ status: z6.string(), toolNames: z6.array(z6.string()) }) }).parse(JSON.parse(await openclaw(["plugins", "inspect", PLUGIN_ID, "--runtime", "--json"])));
|
|
757
1262
|
if (plugin.status !== "loaded" || !MCP_TOOL_NAMES.every((name) => plugin.toolNames.includes(toolName(name)))) {
|
|
758
1263
|
throw new ConnectionError("Context Use native tools did not load. Run openclaw plugins doctor, or disconnect to restore the previous settings.");
|
|
759
1264
|
}
|
|
760
1265
|
}
|
|
761
1266
|
|
|
762
|
-
// src/install.ts
|
|
763
|
-
import { mutateConfigFile as mutateConfigFile2 } from "openclaw/plugin-sdk/config-mutation";
|
|
764
|
-
|
|
765
|
-
// src/package-installation.ts
|
|
766
|
-
import { execFile as execFile2 } from "node:child_process";
|
|
767
|
-
import { mkdtemp, readFile as readFile2, rm as rm3 } from "node:fs/promises";
|
|
768
|
-
import { tmpdir } from "node:os";
|
|
769
|
-
import { dirname, join as join3, resolve as resolve2 } from "node:path";
|
|
770
|
-
import { fileURLToPath } from "node:url";
|
|
771
|
-
import { promisify as promisify2 } from "node:util";
|
|
772
|
-
import { z as z5 } from "zod";
|
|
773
|
-
var execute2 = promisify2(execFile2);
|
|
774
|
-
var INSTALL_TIMEOUT_MS = 120000;
|
|
775
|
-
async function installedPlugin() {
|
|
776
|
-
const inventory = z5.object({
|
|
777
|
-
plugins: z5.array(z5.object({
|
|
778
|
-
id: z5.string(),
|
|
779
|
-
rootDir: z5.string()
|
|
780
|
-
}))
|
|
781
|
-
}).parse(JSON.parse(await openclaw(["plugins", "list", "--json"])));
|
|
782
|
-
return inventory.plugins.find((plugin) => plugin.id === PLUGIN_ID);
|
|
783
|
-
}
|
|
784
|
-
async function installPackage() {
|
|
785
|
-
const existing = await installedPlugin();
|
|
786
|
-
if (existing) {
|
|
787
|
-
const installed = z5.object({ name: z5.string() }).parse(JSON.parse(await readFile2(join3(existing.rootDir, "package.json"), "utf8")));
|
|
788
|
-
if (installed.name !== package_default.name) {
|
|
789
|
-
throw new ConnectionError(`The context-use plugin ID belongs to another package: ${installed.name}.`);
|
|
790
|
-
}
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
const root = resolve2(dirname(fileURLToPath(import.meta.url)), "..");
|
|
794
|
-
const directory = await mkdtemp(join3(tmpdir(), "context-use-install-"));
|
|
795
|
-
try {
|
|
796
|
-
const packed = await execute2("npm", ["pack", root, "--ignore-scripts", "--json", "--pack-destination", directory], { timeout: INSTALL_TIMEOUT_MS });
|
|
797
|
-
const [{ filename }] = z5.tuple([z5.object({ filename: z5.string() })]).parse(JSON.parse(packed.stdout));
|
|
798
|
-
console.log(await openclaw([
|
|
799
|
-
"plugins",
|
|
800
|
-
"install",
|
|
801
|
-
join3(directory, filename),
|
|
802
|
-
"--force",
|
|
803
|
-
"--accept-capabilities"
|
|
804
|
-
]));
|
|
805
|
-
} catch (error) {
|
|
806
|
-
if (error && typeof error === "object" && "stderr" in error && typeof error.stderr === "string") {
|
|
807
|
-
process.stderr.write(error.stderr);
|
|
808
|
-
}
|
|
809
|
-
throw new ConnectionError("OpenClaw could not install Context Use. Resolve the installer error above and retry connect.");
|
|
810
|
-
} finally {
|
|
811
|
-
await rm3(directory, { recursive: true, force: true });
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// src/install.ts
|
|
816
|
-
async function uninstall() {
|
|
817
|
-
if (await installedPlugin()) {
|
|
818
|
-
console.log(await openclaw(["plugins", "uninstall", PLUGIN_ID, "--force"]));
|
|
819
|
-
}
|
|
820
|
-
await mutateConfigFile2({
|
|
821
|
-
writeOptions: { allowConfigSizeDrop: true },
|
|
822
|
-
mutate: (config) => {
|
|
823
|
-
if (config.plugins?.entries) {
|
|
824
|
-
delete config.plugins.entries[PLUGIN_ID];
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
|
|
830
1267
|
// src/usage.ts
|
|
831
1268
|
var SETUP_USAGE = `context-use-openclaw connect <instance-url> [agent-id=main]
|
|
1269
|
+
context-use-openclaw reconnect [instance-url] [agent-id]
|
|
832
1270
|
context-use-openclaw authorize
|
|
833
1271
|
context-use-openclaw status
|
|
834
1272
|
context-use-openclaw disconnect
|
|
835
|
-
context-use-openclaw remove
|
|
1273
|
+
context-use-openclaw remove [--wait]
|
|
836
1274
|
context-use-openclaw refresh
|
|
837
1275
|
|
|
838
1276
|
Connect installs the plugin and starts browser authorization. Pass the returned URL to authorize
|
|
839
1277
|
through standard input. After installation, these commands also work through openclaw context-use.
|
|
840
|
-
|
|
841
|
-
|
|
1278
|
+
Reconnect starts fresh authorization, using the saved URL when omitted. Connect with a different URL
|
|
1279
|
+
also starts fresh authorization. Use the same OpenClaw profile for every command. Remove restores your settings and uninstalls the
|
|
1280
|
+
plugin while preserving remote memories and conversation history. Remove runs in the background: end
|
|
1281
|
+
the requesting agent turn, then check status in a new turn. Use --wait only from a terminal outside
|
|
1282
|
+
an agent turn. Status, remove and refresh also work after uninstall through the npm helper.`;
|
|
842
1283
|
|
|
843
1284
|
// src/setup.ts
|
|
1285
|
+
async function connectCommand({
|
|
1286
|
+
directory,
|
|
1287
|
+
command,
|
|
1288
|
+
argument,
|
|
1289
|
+
agentArgument
|
|
1290
|
+
}) {
|
|
1291
|
+
const previous = await readState(directory);
|
|
1292
|
+
const instance = argument ?? (command === "reconnect" ? previous?.config.serverUrl : undefined);
|
|
1293
|
+
const agentId = agentArgument ?? previous?.config.agentId ?? "main";
|
|
1294
|
+
if (!instance) {
|
|
1295
|
+
throw new ConnectionError(SETUP_USAGE);
|
|
1296
|
+
}
|
|
1297
|
+
PluginConfigSchema.shape.agentId.parse(agentId);
|
|
1298
|
+
const result = await connect({
|
|
1299
|
+
directory,
|
|
1300
|
+
instance,
|
|
1301
|
+
agentId,
|
|
1302
|
+
reauthorize: command === "reconnect"
|
|
1303
|
+
});
|
|
1304
|
+
if (result.authorizationUrl) {
|
|
1305
|
+
console.log(`Open this URL to authorize Context Use:
|
|
1306
|
+
${result.authorizationUrl}
|
|
1307
|
+
|
|
1308
|
+
Copy the final localhost URL and send it back, even if the page does not load. Finish with ${OPENCLAW_INSTALL_COMMAND} authorize, passing the returned URL through standard input.`);
|
|
1309
|
+
} else {
|
|
1310
|
+
console.log("Context Use authorized. Memory settings saved.");
|
|
1311
|
+
await refreshGateway();
|
|
1312
|
+
}
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
async function statusCommand(directory) {
|
|
1316
|
+
const removal = await removalStatus(directory);
|
|
1317
|
+
if (removal) {
|
|
1318
|
+
console.log(JSON.stringify({ connected: false, removal }, null, 2));
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const result = {
|
|
1322
|
+
...await status(directory),
|
|
1323
|
+
installed: Boolean(await installedPlugin())
|
|
1324
|
+
};
|
|
1325
|
+
if (result.connected) {
|
|
1326
|
+
try {
|
|
1327
|
+
await verifyRuntime();
|
|
1328
|
+
} catch {
|
|
1329
|
+
result.connected = false;
|
|
1330
|
+
result.activationPending = true;
|
|
1331
|
+
result.error = "Memory settings are saved but native tools are not active. Finish active work, run refresh, then check status in a new turn.";
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
844
1337
|
async function main(args) {
|
|
845
|
-
const [command, argument,
|
|
1338
|
+
const [command, argument, agentArgument] = args;
|
|
846
1339
|
if (!command || command === "--help") {
|
|
847
1340
|
console.log(SETUP_USAGE);
|
|
848
1341
|
return;
|
|
849
1342
|
}
|
|
850
1343
|
await checkHost();
|
|
851
1344
|
const directory = connectionDirectory();
|
|
1345
|
+
const removal = await removalStatus(directory);
|
|
1346
|
+
if (removal && !["remove", "status", "refresh"].includes(command)) {
|
|
1347
|
+
throw new ConnectionError(removal.error ?? "Removal is still running. Check status in a new turn before reconnecting.");
|
|
1348
|
+
}
|
|
852
1349
|
switch (command) {
|
|
853
1350
|
case "refresh":
|
|
854
1351
|
await refreshGateway();
|
|
855
1352
|
return;
|
|
856
|
-
case "connect":
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
PluginConfigSchema.shape.agentId.parse(agentId);
|
|
861
|
-
const result = await connect({ directory, instance: argument, agentId });
|
|
862
|
-
if (result.authorizationUrl) {
|
|
863
|
-
console.log(`Open this URL to authorize Context Use:
|
|
864
|
-
${result.authorizationUrl}
|
|
865
|
-
|
|
866
|
-
Copy the final localhost URL and send it back, even if the page does not load. Finish with ${OPENCLAW_INSTALL_COMMAND} authorize, passing the returned URL through standard input.`);
|
|
867
|
-
} else {
|
|
868
|
-
await verifyRuntime();
|
|
869
|
-
console.log("Context Use connected.");
|
|
870
|
-
await refreshGateway();
|
|
871
|
-
}
|
|
1353
|
+
case "connect":
|
|
1354
|
+
case "reconnect": {
|
|
1355
|
+
await connectCommand({ directory, command, argument, agentArgument });
|
|
872
1356
|
return;
|
|
873
1357
|
}
|
|
874
1358
|
case "authorize": {
|
|
@@ -877,29 +1361,33 @@ Copy the final localhost URL and send it back, even if the page does not load. F
|
|
|
877
1361
|
}
|
|
878
1362
|
const redirectUrl = (await text(process.stdin)).trim();
|
|
879
1363
|
await finishAuthorization({ directory, redirectUrl });
|
|
880
|
-
|
|
881
|
-
console.log("Context Use connected.");
|
|
1364
|
+
console.log("Context Use authorized. Memory settings saved.");
|
|
882
1365
|
await refreshGateway();
|
|
883
1366
|
return;
|
|
884
1367
|
}
|
|
885
1368
|
case "status": {
|
|
886
|
-
|
|
887
|
-
if (result.connected) {
|
|
888
|
-
await verifyRuntime();
|
|
889
|
-
}
|
|
890
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1369
|
+
await statusCommand(directory);
|
|
891
1370
|
return;
|
|
892
1371
|
}
|
|
893
|
-
case "disconnect":
|
|
894
|
-
case "remove": {
|
|
1372
|
+
case "disconnect": {
|
|
895
1373
|
const { preserved } = await disconnect(directory);
|
|
896
1374
|
console.log(`Disconnected. Remote memories are preserved.${preserved.length ? ` Kept user edits: ${preserved.join(", ")}.` : ""}`);
|
|
897
|
-
if (command === "remove") {
|
|
898
|
-
await uninstall();
|
|
899
|
-
}
|
|
900
1375
|
await refreshGateway();
|
|
901
1376
|
return;
|
|
902
1377
|
}
|
|
1378
|
+
case "remove": {
|
|
1379
|
+
if (![undefined, "--wait"].includes(argument)) {
|
|
1380
|
+
throw new ConnectionError(SETUP_USAGE);
|
|
1381
|
+
}
|
|
1382
|
+
await requestRemoval(directory);
|
|
1383
|
+
if (argument !== "--wait") {
|
|
1384
|
+
console.log("Local authorization cleared. Removal is running separately so this agent turn can finish. End this turn; check status in a new turn. Do not poll from this turn or restart the gateway. Remote memories and conversation history are preserved.");
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
await waitForRemoval(directory);
|
|
1388
|
+
console.log("Context Use removed. Remote memories and conversation history are preserved.");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
903
1391
|
default:
|
|
904
1392
|
throw new ConnectionError(SETUP_USAGE);
|
|
905
1393
|
}
|