@context-use/openclaw-memory 0.1.0-beta.30 → 0.1.0-beta.63
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 +15 -9
- package/dist/index.js +909 -85
- package/dist/setup-prompt.js +1 -1
- package/dist/setup.js +306 -31
- package/openclaw.plugin.json +3 -1
- package/package.json +1 -1
package/dist/setup-prompt.js
CHANGED
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.63",
|
|
6
6
|
description: "Context Use as OpenClaw's sole durable personal memory",
|
|
7
7
|
type: "module",
|
|
8
8
|
license: "MIT",
|
|
@@ -91,8 +91,10 @@ var OPENCLAW_REMOVAL_PROMPT = `Remove the Context Use memory plugin using ${OPEN
|
|
|
91
91
|
import { text } from "node:stream/consumers";
|
|
92
92
|
|
|
93
93
|
// src/connection.ts
|
|
94
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
95
|
+
import { existsSync } from "node:fs";
|
|
94
96
|
import { rm as rm2 } from "node:fs/promises";
|
|
95
|
-
import { join as
|
|
97
|
+
import { join as join3 } from "node:path";
|
|
96
98
|
import { auth } from "@modelcontextprotocol/client";
|
|
97
99
|
import {
|
|
98
100
|
mutateConfigFile,
|
|
@@ -117,7 +119,10 @@ var OPENCLAW_VERSION_RANGE = package_default.peerDependencies.openclaw;
|
|
|
117
119
|
var CALLBACK_URL = "http://127.0.0.1:49187/context-use/callback";
|
|
118
120
|
var REQUEST_TIMEOUT_MS = 30000;
|
|
119
121
|
var AUTHORIZATION_TIMEOUT_MS = 600000;
|
|
122
|
+
var AUTHORIZATION_SCOPE = "mcp offline_access";
|
|
120
123
|
var toolName = (name) => `context_use_${name}`;
|
|
124
|
+
var FINISH_LEARNING_TOOL = toolName("finish_learning");
|
|
125
|
+
var SAVE_ATTACHMENT_TOOL = toolName("save_learning_attachment");
|
|
121
126
|
var PluginConfigSchema = z.strictObject({
|
|
122
127
|
agentId: z.string().regex(/^[a-z0-9][a-z0-9_-]*$/),
|
|
123
128
|
serverUrl: z.url()
|
|
@@ -164,13 +169,27 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
164
169
|
|
|
165
170
|
// src/state.ts
|
|
166
171
|
import { readFileSync } from "node:fs";
|
|
167
|
-
import { mkdir, readFile
|
|
172
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
168
173
|
import { join } from "node:path";
|
|
169
174
|
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
170
175
|
import { lock } from "proper-lockfile";
|
|
171
176
|
import { z as z2 } from "zod";
|
|
177
|
+
|
|
178
|
+
// src/private-files.ts
|
|
179
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
172
180
|
var PRIVATE_DIRECTORY_MODE = 448;
|
|
173
181
|
var PRIVATE_FILE_MODE = 384;
|
|
182
|
+
async function writePrivateFile(input) {
|
|
183
|
+
const temporary = `${input.path}.tmp`;
|
|
184
|
+
try {
|
|
185
|
+
await writeFile(temporary, input.data, { mode: PRIVATE_FILE_MODE });
|
|
186
|
+
await rename(temporary, input.path);
|
|
187
|
+
} finally {
|
|
188
|
+
await rm(temporary, { force: true });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/state.ts
|
|
174
193
|
var LOCK_STALE_MS = 120000;
|
|
175
194
|
var LOCK_RETRIES = 300;
|
|
176
195
|
var LOCK_RETRY_MS = 100;
|
|
@@ -180,6 +199,7 @@ var ChangeSchema = z2.object({
|
|
|
180
199
|
applied: z2.unknown()
|
|
181
200
|
});
|
|
182
201
|
var ConnectionStateSchema = z2.object({
|
|
202
|
+
learningId: z2.string().uuid().optional(),
|
|
183
203
|
config: PluginConfigSchema,
|
|
184
204
|
changes: z2.array(ChangeSchema),
|
|
185
205
|
tools: z2.array(z2.object({
|
|
@@ -209,14 +229,10 @@ async function readState(directory) {
|
|
|
209
229
|
}
|
|
210
230
|
}
|
|
211
231
|
async function writeState(input) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
await rename(temporary, path);
|
|
217
|
-
} finally {
|
|
218
|
-
await rm(temporary, { force: true });
|
|
219
|
-
}
|
|
232
|
+
await writePrivateFile({
|
|
233
|
+
path: join(input.directory, "connection.json"),
|
|
234
|
+
data: JSON.stringify(input.state)
|
|
235
|
+
});
|
|
220
236
|
}
|
|
221
237
|
async function withConnection(input) {
|
|
222
238
|
await mkdir(input.directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
@@ -254,7 +270,7 @@ function oauthProvider(input) {
|
|
|
254
270
|
grant_types: ["authorization_code", "refresh_token"],
|
|
255
271
|
response_types: ["code"],
|
|
256
272
|
token_endpoint_auth_method: "none",
|
|
257
|
-
scope:
|
|
273
|
+
scope: AUTHORIZATION_SCOPE
|
|
258
274
|
},
|
|
259
275
|
state: () => randomBytes(STATE_BYTES).toString("hex"),
|
|
260
276
|
clientInformation: () => oauth.client,
|
|
@@ -386,28 +402,29 @@ async function discoverTools(client) {
|
|
|
386
402
|
}
|
|
387
403
|
return tools.filter((tool) => MCP_TOOL_NAMES.some((name) => name === tool.name));
|
|
388
404
|
}
|
|
389
|
-
async function
|
|
405
|
+
async function withMemoryClient(input) {
|
|
390
406
|
return await withConnection({
|
|
391
407
|
directory: input.directory,
|
|
392
408
|
run: async () => {
|
|
393
409
|
input.signal?.throwIfAborted();
|
|
394
410
|
const state = await readState(input.directory);
|
|
395
|
-
if (!state || state.config.serverUrl !== input.serverUrl || state.config.agentId !== input.agentId || !state.oauth.tokens) {
|
|
411
|
+
if (!state || state.learningId !== input.connectionId || state.config.serverUrl !== input.serverUrl || state.config.agentId !== input.agentId || !state.oauth.tokens) {
|
|
396
412
|
throw new ConnectionError("Context Use is disconnected or requires authorization.");
|
|
397
413
|
}
|
|
398
414
|
return await withClient({
|
|
399
415
|
directory: input.directory,
|
|
400
416
|
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
|
-
}
|
|
417
|
+
run: input.run
|
|
407
418
|
});
|
|
408
419
|
}
|
|
409
420
|
});
|
|
410
421
|
}
|
|
422
|
+
async function callMemoryTool(input) {
|
|
423
|
+
return await withMemoryClient({
|
|
424
|
+
...input,
|
|
425
|
+
run: (client) => client.callTool({ name: input.name, arguments: input.arguments }, { timeout: REQUEST_TIMEOUT_MS, signal: input.signal })
|
|
426
|
+
});
|
|
427
|
+
}
|
|
411
428
|
|
|
412
429
|
// src/configuration.ts
|
|
413
430
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -430,6 +447,12 @@ Do not save transient chatter, generic knowledge, unsupported inference, secrets
|
|
|
430
447
|
authorization links, redirect URLs or authorization codes. Respect requests not to retain facts.
|
|
431
448
|
If nothing merits preservation, make no write. If an operation fails or its outcome is uncertain,
|
|
432
449
|
do not claim success or blindly replay a create. Keep the final reply focused on the user's task.`;
|
|
450
|
+
var CONVERSATION_LEARNING_GUIDANCE = `When background learning is enabled, Context Use queues conversation evidence for background learning after turns and before resets
|
|
451
|
+
and compactions. The Gateway curates this evidence without holding up your reply. Leave routine
|
|
452
|
+
learning to that worker. If the user explicitly asks you to remember, correct or forget something,
|
|
453
|
+
handle it now through Context Use tools: read the curation guide, search and read existing knowledge,
|
|
454
|
+
then make the smallest supported change. Respect retention preferences. Do not claim information
|
|
455
|
+
has been saved until the required writes succeed; queued background work is not a completed save.`;
|
|
433
456
|
var MEMORY_GUIDANCE = `Context Use is your sole durable personal memory. Read and write
|
|
434
457
|
through its context_use_ tools whenever useful, including background work. Use it across your
|
|
435
458
|
connected conversations. If Context Use is unavailable, report the limitation when relevant;
|
|
@@ -586,9 +609,214 @@ function assertPersonalConfiguration(input) {
|
|
|
586
609
|
}
|
|
587
610
|
}
|
|
588
611
|
|
|
612
|
+
// src/learning-store.ts
|
|
613
|
+
import { randomUUID } from "node:crypto";
|
|
614
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
615
|
+
import { join as join2 } from "node:path";
|
|
616
|
+
import { DatabaseSync } from "node:sqlite";
|
|
617
|
+
var BATCH_CHARS = 48000;
|
|
618
|
+
var LEARNING_SESSION_PREFIX = "subagent:context-use-learning:";
|
|
619
|
+
var LEARNING_SESSION_PATTERN = new RegExp(`^agent:[^:]+:${LEARNING_SESSION_PREFIX}`);
|
|
620
|
+
function isLearningSession(sessionKey) {
|
|
621
|
+
return LEARNING_SESSION_PATTERN.test(sessionKey ?? "");
|
|
622
|
+
}
|
|
623
|
+
var attachmentDirectory = (connectionId) => `learning-${encodeURIComponent(connectionId)}-attachments`;
|
|
624
|
+
var DREAM_INTERVAL_MS = 21600000;
|
|
625
|
+
var learningDatabase = (connectionId) => `learning-${encodeURIComponent(connectionId)}.sqlite`;
|
|
626
|
+
|
|
627
|
+
class LearningStore {
|
|
628
|
+
db;
|
|
629
|
+
constructor(input) {
|
|
630
|
+
mkdirSync(input.directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
631
|
+
const path = join2(input.directory, learningDatabase(input.connectionId));
|
|
632
|
+
this.db = new DatabaseSync(path);
|
|
633
|
+
chmodSync(path, PRIVATE_FILE_MODE);
|
|
634
|
+
this.db.exec(`
|
|
635
|
+
PRAGMA busy_timeout = 1000;
|
|
636
|
+
PRAGMA secure_delete = ON;
|
|
637
|
+
CREATE TABLE IF NOT EXISTS owner (config TEXT NOT NULL);
|
|
638
|
+
CREATE TABLE IF NOT EXISTS evidence (
|
|
639
|
+
id INTEGER PRIMARY KEY, source TEXT NOT NULL, fingerprint TEXT NOT NULL,
|
|
640
|
+
text TEXT, capturedAt INTEGER NOT NULL, UNIQUE(source, fingerprint)
|
|
641
|
+
);
|
|
642
|
+
CREATE TABLE IF NOT EXISTS job (
|
|
643
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
|
|
644
|
+
id TEXT NOT NULL, kind TEXT NOT NULL, sessionKey TEXT NOT NULL,
|
|
645
|
+
source TEXT NOT NULL, through INTEGER NOT NULL, evidence TEXT NOT NULL,
|
|
646
|
+
runId TEXT, acknowledged INTEGER NOT NULL DEFAULT 0, startedAt INTEGER NOT NULL DEFAULT 0
|
|
647
|
+
);
|
|
648
|
+
CREATE TABLE IF NOT EXISTS retries (source TEXT PRIMARY KEY, retryAt INTEGER NOT NULL);
|
|
649
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
650
|
+
source TEXT NOT NULL, key TEXT NOT NULL, evidenceId INTEGER NOT NULL,
|
|
651
|
+
data TEXT, asset TEXT, uploadName TEXT, PRIMARY KEY (source, key)
|
|
652
|
+
);
|
|
653
|
+
CREATE TABLE IF NOT EXISTS dreaming (
|
|
654
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton = 1), through INTEGER NOT NULL,
|
|
655
|
+
completedAt INTEGER NOT NULL
|
|
656
|
+
);
|
|
657
|
+
`);
|
|
658
|
+
const owner = JSON.stringify({ ...input.config, connectionId: input.connectionId });
|
|
659
|
+
this.db.prepare("INSERT INTO owner SELECT ? WHERE NOT EXISTS (SELECT 1 FROM owner)").run(owner);
|
|
660
|
+
if (this.db.prepare("SELECT config FROM owner").get().config !== owner) {
|
|
661
|
+
this.db.close();
|
|
662
|
+
throw new Error("Context Use learning queue belongs to a different connection.");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
capture(input) {
|
|
666
|
+
this.transaction(() => {
|
|
667
|
+
const insert = this.db.prepare("INSERT OR IGNORE INTO evidence(source, fingerprint, text, capturedAt) VALUES (?, ?, ?, ?)");
|
|
668
|
+
for (const item of input.evidence) {
|
|
669
|
+
insert.run(input.source, item.key, item.text, input.now);
|
|
670
|
+
if (item.attachment) {
|
|
671
|
+
this.db.prepare(`INSERT OR IGNORE INTO attachments(source, key, evidenceId, data)
|
|
672
|
+
SELECT source, fingerprint, id, ? FROM evidence WHERE source = ? AND fingerprint = ?`).run(JSON.stringify(item.attachment), input.source, item.key);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
current() {
|
|
678
|
+
return this.db.prepare("SELECT * FROM job").get();
|
|
679
|
+
}
|
|
680
|
+
next(input) {
|
|
681
|
+
return this.transaction(() => {
|
|
682
|
+
const existing = this.current();
|
|
683
|
+
if (existing) {
|
|
684
|
+
return existing;
|
|
685
|
+
}
|
|
686
|
+
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);
|
|
687
|
+
let { through, evidence } = this.batch(first?.source);
|
|
688
|
+
if (!first) {
|
|
689
|
+
through = this.dreamThrough(input.now);
|
|
690
|
+
if (!through) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const id = randomUUID();
|
|
695
|
+
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);
|
|
696
|
+
return this.current();
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
batch(source) {
|
|
700
|
+
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) : [];
|
|
701
|
+
let through = 0;
|
|
702
|
+
let evidence = "";
|
|
703
|
+
for (const row of rows) {
|
|
704
|
+
if (evidence.length + row.text.length > BATCH_CHARS) {
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
evidence += `${JSON.stringify({ observedAt: new Date(row.capturedAt).toISOString(), message: JSON.parse(row.text) })}
|
|
708
|
+
`;
|
|
709
|
+
through = row.id;
|
|
710
|
+
}
|
|
711
|
+
return { through, evidence };
|
|
712
|
+
}
|
|
713
|
+
dreamThrough(now) {
|
|
714
|
+
if (this.status().pending || this.db.prepare("SELECT 1 FROM retries WHERE source = '' AND retryAt > ?").get(now)) {
|
|
715
|
+
return 0;
|
|
716
|
+
}
|
|
717
|
+
const progress = this.db.prepare("SELECT MAX(id) AS through, MIN(capturedAt) AS since FROM evidence").get();
|
|
718
|
+
const dream = this.db.prepare("SELECT through, completedAt FROM dreaming").get();
|
|
719
|
+
if (!progress.through || progress.through <= (dream?.through ?? 0) || now - (dream?.completedAt ?? progress.since ?? now) < DREAM_INTERVAL_MS) {
|
|
720
|
+
return 0;
|
|
721
|
+
}
|
|
722
|
+
return progress.through;
|
|
723
|
+
}
|
|
724
|
+
started(input) {
|
|
725
|
+
this.db.prepare("UPDATE job SET runId = ?, startedAt = ? WHERE id = ?").run(input.runId, input.now, input.id);
|
|
726
|
+
}
|
|
727
|
+
attachments(job) {
|
|
728
|
+
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);
|
|
729
|
+
return rows.map((row) => ({
|
|
730
|
+
key: row.key,
|
|
731
|
+
attachment: JSON.parse(row.data),
|
|
732
|
+
asset: row.asset
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
attachmentSource(input) {
|
|
736
|
+
const job = this.current();
|
|
737
|
+
if (!job || job.id !== input.jobId || !this.attachments(job).some((item) => item.key === input.key)) {
|
|
738
|
+
throw new Error("This attachment is not part of the active learning job.");
|
|
739
|
+
}
|
|
740
|
+
return job.source;
|
|
741
|
+
}
|
|
742
|
+
prepareAttachmentUpload(input) {
|
|
743
|
+
const source = this.attachmentSource(input);
|
|
744
|
+
this.db.prepare("UPDATE attachments SET uploadName = COALESCE(uploadName, ?) WHERE source = ? AND key = ?").run(input.name, source, input.key);
|
|
745
|
+
return this.db.prepare("SELECT uploadName FROM attachments WHERE source = ? AND key = ?").get(source, input.key).uploadName;
|
|
746
|
+
}
|
|
747
|
+
clearAttachmentUploadName(input) {
|
|
748
|
+
const source = this.attachmentSource(input);
|
|
749
|
+
this.db.prepare("UPDATE attachments SET uploadName = NULL WHERE source = ? AND key = ?").run(source, input.key);
|
|
750
|
+
}
|
|
751
|
+
savedAttachment(input) {
|
|
752
|
+
const source = this.attachmentSource(input);
|
|
753
|
+
this.db.prepare("UPDATE attachments SET asset = ? WHERE source = ? AND key = ?").run(input.asset, source, input.key);
|
|
754
|
+
}
|
|
755
|
+
acknowledge(input) {
|
|
756
|
+
const { sessionKey, omittedAttachments = [] } = input;
|
|
757
|
+
const job = this.current();
|
|
758
|
+
if (job?.sessionKey === sessionKey && this.attachments(job).some((item) => !item.asset && !omittedAttachments.includes(item.key))) {
|
|
759
|
+
throw new Error("Save every attachment or explicitly exclude it for a retention reason before finishing. Failed uploads must be retried.");
|
|
760
|
+
}
|
|
761
|
+
const result = this.db.prepare("UPDATE job SET acknowledged = 1 WHERE sessionKey = ?").run(sessionKey);
|
|
762
|
+
if (!result.changes) {
|
|
763
|
+
throw new Error("This learning run is no longer active.");
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
retry(input) {
|
|
767
|
+
this.transaction(() => {
|
|
768
|
+
const job = this.current();
|
|
769
|
+
if (!job || job.id !== input.id) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.db.prepare("INSERT OR REPLACE INTO retries VALUES (?, ?)").run(job.source, input.at);
|
|
773
|
+
this.db.prepare("DELETE FROM job WHERE id = ?").run(input.id);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
finish(input) {
|
|
777
|
+
this.transaction(() => {
|
|
778
|
+
const job = this.current();
|
|
779
|
+
if (!job || job.id !== input.id || !job.acknowledged) {
|
|
780
|
+
throw new Error("Learning must be acknowledged before removing its evidence.");
|
|
781
|
+
}
|
|
782
|
+
if (job.kind === "learn") {
|
|
783
|
+
this.db.prepare("UPDATE evidence SET text = NULL WHERE source = ? AND id <= ?").run(job.source, job.through);
|
|
784
|
+
this.db.prepare("UPDATE attachments SET data = NULL WHERE source = ? AND evidenceId <= ?").run(job.source, job.through);
|
|
785
|
+
} else {
|
|
786
|
+
this.db.prepare("INSERT OR REPLACE INTO dreaming VALUES (1, ?, ?)").run(job.through, input.now);
|
|
787
|
+
}
|
|
788
|
+
this.db.prepare("DELETE FROM retries WHERE source = ?").run(job.source);
|
|
789
|
+
this.db.prepare("DELETE FROM job WHERE id = ?").run(job.id);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
status() {
|
|
793
|
+
return {
|
|
794
|
+
pending: this.db.prepare("SELECT COUNT(*) AS count FROM evidence WHERE text IS NOT NULL").get().count,
|
|
795
|
+
running: Boolean(this.current()?.runId),
|
|
796
|
+
retryAt: this.db.prepare("SELECT MIN(retryAt) AS at FROM retries").get().at ?? undefined
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
close() {
|
|
800
|
+
this.db.close();
|
|
801
|
+
}
|
|
802
|
+
transaction(run) {
|
|
803
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
804
|
+
try {
|
|
805
|
+
const result = run();
|
|
806
|
+
this.db.exec("COMMIT");
|
|
807
|
+
return result;
|
|
808
|
+
} catch (error) {
|
|
809
|
+
this.db.exec("ROLLBACK");
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
589
815
|
// src/connection.ts
|
|
816
|
+
var authorizationFetch = (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
590
817
|
async function activate(input) {
|
|
591
818
|
input.state.tools = await withClient({ ...input, run: discoverTools });
|
|
819
|
+
input.state.learningId ??= randomUUID2();
|
|
592
820
|
await mutateConfigFile({
|
|
593
821
|
mutate: async (config) => {
|
|
594
822
|
prepareConfiguration({ config, state: input.state });
|
|
@@ -596,13 +824,24 @@ async function activate(input) {
|
|
|
596
824
|
}
|
|
597
825
|
});
|
|
598
826
|
}
|
|
827
|
+
async function removeLearningData(input) {
|
|
828
|
+
await rm2(join3(input.directory, attachmentDirectory(input.connectionId)), {
|
|
829
|
+
recursive: true,
|
|
830
|
+
force: true
|
|
831
|
+
});
|
|
832
|
+
for (const suffix of ["", "-journal"]) {
|
|
833
|
+
await rm2(join3(input.directory, `${learningDatabase(input.connectionId)}${suffix}`), {
|
|
834
|
+
force: true
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
599
838
|
async function connect(input) {
|
|
600
839
|
const config = { serverUrl: serverUrl(input.instance), agentId: input.agentId };
|
|
601
840
|
return await withConnection({
|
|
602
841
|
directory: input.directory,
|
|
603
842
|
run: async () => {
|
|
604
843
|
const existing = await readState(input.directory);
|
|
605
|
-
if (existing && (
|
|
844
|
+
if (existing && !configMatches({ actual: existing.config, expected: config })) {
|
|
606
845
|
throw new ConnectionError("Disconnect the existing account before changing instance or agent.");
|
|
607
846
|
}
|
|
608
847
|
const state = existing ?? { config, changes: [], tools: [], oauth: {} };
|
|
@@ -614,8 +853,8 @@ async function connect(input) {
|
|
|
614
853
|
await writeState({ directory: input.directory, state });
|
|
615
854
|
const result = await auth(oauthProvider({ directory: input.directory, state, interactive: true }), {
|
|
616
855
|
serverUrl: config.serverUrl,
|
|
617
|
-
scope:
|
|
618
|
-
fetchFn:
|
|
856
|
+
scope: AUTHORIZATION_SCOPE,
|
|
857
|
+
fetchFn: authorizationFetch
|
|
619
858
|
});
|
|
620
859
|
if (result === "REDIRECT") {
|
|
621
860
|
return { authorizationUrl: state.oauth.pending?.url };
|
|
@@ -640,14 +879,22 @@ async function finishAuthorization(input) {
|
|
|
640
879
|
const result = await auth(oauthProvider({ directory: input.directory, state, interactive: true }), {
|
|
641
880
|
serverUrl: state.config.serverUrl,
|
|
642
881
|
...response,
|
|
643
|
-
fetchFn:
|
|
882
|
+
fetchFn: authorizationFetch
|
|
644
883
|
});
|
|
645
884
|
if (result !== "AUTHORIZED") {
|
|
646
885
|
throw new ConnectionError("Authorization was not completed. Start connect again.");
|
|
647
886
|
}
|
|
648
887
|
delete state.oauth.pending;
|
|
649
888
|
delete state.oauth.verifier;
|
|
889
|
+
const previousLearningId = state.learningId;
|
|
890
|
+
state.learningId = randomUUID2();
|
|
650
891
|
await writeState({ directory: input.directory, state });
|
|
892
|
+
if (previousLearningId) {
|
|
893
|
+
await removeLearningData({
|
|
894
|
+
directory: input.directory,
|
|
895
|
+
connectionId: previousLearningId
|
|
896
|
+
});
|
|
897
|
+
}
|
|
651
898
|
await activate({ directory: input.directory, state });
|
|
652
899
|
}
|
|
653
900
|
});
|
|
@@ -670,7 +917,10 @@ async function disconnect(directory) {
|
|
|
670
917
|
}
|
|
671
918
|
}
|
|
672
919
|
});
|
|
673
|
-
await rm2(
|
|
920
|
+
await rm2(join3(directory, "connection.json"), { force: true });
|
|
921
|
+
if (state?.learningId) {
|
|
922
|
+
await removeLearningData({ directory, connectionId: state.learningId });
|
|
923
|
+
}
|
|
674
924
|
return { preserved };
|
|
675
925
|
}
|
|
676
926
|
});
|
|
@@ -693,7 +943,25 @@ async function status(directory) {
|
|
|
693
943
|
const tools = await withClient({ directory, state, run: discoverTools });
|
|
694
944
|
const { snapshot } = await readConfigFileSnapshotForWrite();
|
|
695
945
|
const selected = snapshot.config.plugins?.slots?.memory === PLUGIN_ID && snapshot.config.plugins?.entries?.[PLUGIN_ID]?.enabled === true;
|
|
696
|
-
|
|
946
|
+
const learning = state.learningId && existsSync(join3(directory, learningDatabase(state.learningId))) ? new LearningStore({
|
|
947
|
+
directory,
|
|
948
|
+
config: state.config,
|
|
949
|
+
connectionId: state.learningId
|
|
950
|
+
}) : undefined;
|
|
951
|
+
try {
|
|
952
|
+
return {
|
|
953
|
+
connected: selected,
|
|
954
|
+
authenticated: true,
|
|
955
|
+
...state.config,
|
|
956
|
+
tools: tools.length,
|
|
957
|
+
learning: {
|
|
958
|
+
enabled: Boolean(state.learningId),
|
|
959
|
+
...learning?.status() ?? { pending: 0, running: false }
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
} finally {
|
|
963
|
+
learning?.close();
|
|
964
|
+
}
|
|
697
965
|
}
|
|
698
966
|
});
|
|
699
967
|
}
|
|
@@ -703,10 +971,17 @@ import { resolve } from "node:path";
|
|
|
703
971
|
import { z as z3 } from "zod";
|
|
704
972
|
|
|
705
973
|
// src/host-command.ts
|
|
706
|
-
import { execFile } from "node:child_process";
|
|
974
|
+
import { execFile, spawn } from "node:child_process";
|
|
707
975
|
import { promisify } from "node:util";
|
|
708
976
|
var execute = promisify(execFile);
|
|
709
977
|
var HOST_TIMEOUT_MS = 120000;
|
|
978
|
+
async function runSetupCommand(input) {
|
|
979
|
+
const child = spawn(process.execPath, [input.script, ...input.args], { stdio: "inherit" });
|
|
980
|
+
return await new Promise((resolve, reject) => {
|
|
981
|
+
child.once("error", reject);
|
|
982
|
+
child.once("exit", (code) => resolve(code ?? 1));
|
|
983
|
+
});
|
|
984
|
+
}
|
|
710
985
|
async function openclaw(args) {
|
|
711
986
|
const result = await execute("openclaw", args, {
|
|
712
987
|
timeout: HOST_TIMEOUT_MS,
|
|
@@ -766,7 +1041,7 @@ import { mutateConfigFile as mutateConfigFile2 } from "openclaw/plugin-sdk/confi
|
|
|
766
1041
|
import { execFile as execFile2 } from "node:child_process";
|
|
767
1042
|
import { mkdtemp, readFile as readFile2, rm as rm3 } from "node:fs/promises";
|
|
768
1043
|
import { tmpdir } from "node:os";
|
|
769
|
-
import { dirname, join as
|
|
1044
|
+
import { dirname, join as join4, resolve as resolve2 } from "node:path";
|
|
770
1045
|
import { fileURLToPath } from "node:url";
|
|
771
1046
|
import { promisify as promisify2 } from "node:util";
|
|
772
1047
|
import { z as z5 } from "zod";
|
|
@@ -784,21 +1059,21 @@ async function installedPlugin() {
|
|
|
784
1059
|
async function installPackage() {
|
|
785
1060
|
const existing = await installedPlugin();
|
|
786
1061
|
if (existing) {
|
|
787
|
-
const installed = z5.object({ name: z5.string() }).parse(JSON.parse(await readFile2(
|
|
1062
|
+
const installed = z5.object({ name: z5.string() }).parse(JSON.parse(await readFile2(join4(existing.rootDir, "package.json"), "utf8")));
|
|
788
1063
|
if (installed.name !== package_default.name) {
|
|
789
1064
|
throw new ConnectionError(`The context-use plugin ID belongs to another package: ${installed.name}.`);
|
|
790
1065
|
}
|
|
791
1066
|
return;
|
|
792
1067
|
}
|
|
793
1068
|
const root = resolve2(dirname(fileURLToPath(import.meta.url)), "..");
|
|
794
|
-
const directory = await mkdtemp(
|
|
1069
|
+
const directory = await mkdtemp(join4(tmpdir(), "context-use-install-"));
|
|
795
1070
|
try {
|
|
796
1071
|
const packed = await execute2("npm", ["pack", root, "--ignore-scripts", "--json", "--pack-destination", directory], { timeout: INSTALL_TIMEOUT_MS });
|
|
797
1072
|
const [{ filename }] = z5.tuple([z5.object({ filename: z5.string() })]).parse(JSON.parse(packed.stdout));
|
|
798
1073
|
console.log(await openclaw([
|
|
799
1074
|
"plugins",
|
|
800
1075
|
"install",
|
|
801
|
-
|
|
1076
|
+
join4(directory, filename),
|
|
802
1077
|
"--force",
|
|
803
1078
|
"--accept-capabilities"
|
|
804
1079
|
]));
|
package/openclaw.plugin.json
CHANGED
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"context_use_read_knowledge_page",
|
|
34
34
|
"context_use_update_knowledge_page",
|
|
35
35
|
"context_use_archive_knowledge_page",
|
|
36
|
-
"context_use_read_record"
|
|
36
|
+
"context_use_read_record",
|
|
37
|
+
"context_use_finish_learning",
|
|
38
|
+
"context_use_save_learning_attachment"
|
|
37
39
|
]
|
|
38
40
|
},
|
|
39
41
|
"configSchema": {
|