@threads-weave/openclaw-agentic-weave 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,1258 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import { promises as fsp } from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import readline from "node:readline";
7
+ import { fileURLToPath } from "node:url";
8
+ import { promisify } from "node:util";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const PLUGIN_PACKAGE_ROOT = __dirname;
15
+ const BUNDLED_SIDE_CAR_PATH = path.resolve(
16
+ PLUGIN_PACKAGE_ROOT,
17
+ "sidecar/threads_weaving_sidecar.cjs",
18
+ );
19
+ const DEFAULT_SIDE_CAR_PATH = BUNDLED_SIDE_CAR_PATH;
20
+
21
+ const PLUGIN_ID = "openclaw-agentic-weave";
22
+ const SIDE_CAR_CLIENT_TYPE = "threads-weave-sidecar";
23
+ const SIDE_CAR_VERSION = "0.1.0";
24
+ const DEFAULT_PROTOCOL_VERSION = "2025-03-26";
25
+ const MANAGEMENT_COMMAND = "openclaw-agentic-weave";
26
+ const TOOL_NAME_PREFIX = "threads_weave_";
27
+ const DEFAULT_SERVER_URL = "https://threads-weave.com";
28
+ const DEFAULT_OPENCLAW_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json");
29
+ const DEFAULT_PLUGIN_SECRET_STORAGE = "auto";
30
+ const DEFAULT_REQUEST_TIMEOUT_MS = 45000;
31
+ const DEFAULT_STOP_GRACE_PERIOD_MS = 3000;
32
+ const DEFAULT_FORCE_KILL_TIMEOUT_MS = 1000;
33
+ const TOOL_DEFINITIONS = JSON.parse(
34
+ fs.readFileSync(path.join(__dirname, "tool-definitions.json"), "utf8"),
35
+ );
36
+
37
+ const WRITE_TOOL_NAMES = new Set([
38
+ "sidecar.collect_item",
39
+ "sidecar.record_recommendation_feedback",
40
+ "sidecar.participate_claim",
41
+ "sidecar.save_post_draft",
42
+ "sidecar.create_post",
43
+ "sidecar.create_thread_node",
44
+ "sidecar.claim_and_create_thread_node",
45
+ "sidecar.set_node_content_ttl",
46
+ ]);
47
+
48
+ function createLogger(api) {
49
+ const logger = api && api.logger ? api.logger : console;
50
+ return {
51
+ debug(message, meta) {
52
+ if (typeof logger.debug === "function") {
53
+ logger.debug(message, meta);
54
+ }
55
+ },
56
+ info(message, meta) {
57
+ if (typeof logger.info === "function") {
58
+ logger.info(message, meta);
59
+ return;
60
+ }
61
+ if (typeof logger.log === "function") {
62
+ logger.log(message, meta);
63
+ }
64
+ },
65
+ warn(message, meta) {
66
+ if (typeof logger.warn === "function") {
67
+ logger.warn(message, meta);
68
+ return;
69
+ }
70
+ if (typeof logger.log === "function") {
71
+ logger.log(message, meta);
72
+ }
73
+ },
74
+ error(message, meta) {
75
+ if (typeof logger.error === "function") {
76
+ logger.error(message, meta);
77
+ return;
78
+ }
79
+ if (typeof logger.log === "function") {
80
+ logger.log(message, meta);
81
+ }
82
+ },
83
+ };
84
+ }
85
+
86
+ function getPluginConfig(api) {
87
+ return (
88
+ api?.config?.plugins?.entries?.[PLUGIN_ID]?.config
89
+ ?? api?.config?.plugins?.[PLUGIN_ID]
90
+ ?? {}
91
+ );
92
+ }
93
+
94
+ function getDefaultNodeBin() {
95
+ return process.execPath;
96
+ }
97
+
98
+ function isTruthyString(value) {
99
+ return typeof value === "string" && value.trim().length > 0;
100
+ }
101
+
102
+ function resolveMaybeRelativePath(input, fromDir) {
103
+ if (!isTruthyString(input)) {
104
+ return null;
105
+ }
106
+ if (path.isAbsolute(input)) {
107
+ return input;
108
+ }
109
+ return path.resolve(fromDir, input);
110
+ }
111
+
112
+ function resolveSidecarPath(config) {
113
+ const configuredPath = isTruthyString(config.sidecarPath) ? config.sidecarPath.trim() : null;
114
+ const candidates = [];
115
+ if (configuredPath) {
116
+ if (path.isAbsolute(configuredPath)) {
117
+ candidates.push(configuredPath);
118
+ } else {
119
+ // Keep relative paths anchored to the published plugin package, never the current workspace.
120
+ candidates.push(resolveMaybeRelativePath(configuredPath, PLUGIN_PACKAGE_ROOT));
121
+ }
122
+ }
123
+ candidates.push(DEFAULT_SIDE_CAR_PATH);
124
+ for (const candidate of candidates) {
125
+ if (candidate && fs.existsSync(candidate)) {
126
+ return candidate;
127
+ }
128
+ }
129
+ if (configuredPath) {
130
+ return path.isAbsolute(configuredPath)
131
+ ? configuredPath
132
+ : path.resolve(PLUGIN_PACKAGE_ROOT, configuredPath);
133
+ }
134
+ return DEFAULT_SIDE_CAR_PATH;
135
+ }
136
+
137
+ function normalizeConfig(api) {
138
+ const raw = getPluginConfig(api);
139
+ const secretStorageConfigured = isTruthyString(raw.secretStorage);
140
+ return {
141
+ nodeBin: isTruthyString(raw.nodeBin) ? raw.nodeBin : getDefaultNodeBin(),
142
+ sidecarPath: resolveSidecarPath(raw),
143
+ serverUrl: isTruthyString(raw.serverUrl) ? raw.serverUrl.trim() : null,
144
+ profile: isTruthyString(raw.profile) ? raw.profile.trim() : "openclaw",
145
+ configDir: isTruthyString(raw.configDir) ? raw.configDir.trim() : null,
146
+ secretStorage: secretStorageConfigured
147
+ ? raw.secretStorage.trim()
148
+ : DEFAULT_PLUGIN_SECRET_STORAGE,
149
+ secretStorageConfigured,
150
+ agentName: isTruthyString(raw.agentName) ? raw.agentName.trim() : "OpenClaw",
151
+ agentProvider: isTruthyString(raw.agentProvider) ? raw.agentProvider.trim() : "openclaw",
152
+ deviceName: isTruthyString(raw.deviceName) ? raw.deviceName.trim() : os.hostname(),
153
+ autoStart: Boolean(raw.autoStart),
154
+ persistSession: Boolean(raw.persistSession),
155
+ startupTimeoutMs: Number.isFinite(raw.startupTimeoutMs) && raw.startupTimeoutMs >= 1000
156
+ ? Number(raw.startupTimeoutMs)
157
+ : 15000,
158
+ requestTimeoutMs: Number.isFinite(raw.requestTimeoutMs) && raw.requestTimeoutMs >= 1000
159
+ ? Number(raw.requestTimeoutMs)
160
+ : DEFAULT_REQUEST_TIMEOUT_MS,
161
+ };
162
+ }
163
+
164
+ function describeSidecarRequest(method, params) {
165
+ const normalizedMethod = String(method || "").trim() || "unknown";
166
+ if (
167
+ normalizedMethod === "tools/call"
168
+ && params
169
+ && typeof params === "object"
170
+ && isTruthyString(params.name)
171
+ ) {
172
+ return `${normalizedMethod} (${params.name.trim()})`;
173
+ }
174
+ return normalizedMethod;
175
+ }
176
+
177
+ function buildSidecarRequestTimeoutError(method, params, timeoutMs) {
178
+ const requestLabel = describeSidecarRequest(method, params);
179
+ return `Threads Weave sidecar request timed out after ${timeoutMs}ms while waiting for ${requestLabel}.`;
180
+ }
181
+
182
+ function hasChildProcessExited(child) {
183
+ return !child || child.exitCode !== null || child.signalCode !== null;
184
+ }
185
+
186
+ function toolNameForOpenClaw(sidecarName) {
187
+ return `${TOOL_NAME_PREFIX}${String(sidecarName || "")
188
+ .replace(/^sidecar\./, "")
189
+ .replace(/[^A-Za-z0-9]+/g, "_")
190
+ .replace(/^_+|_+$/g, "")
191
+ .toLowerCase()}`;
192
+ }
193
+
194
+ function toolLabelForOpenClaw(sidecarName) {
195
+ const normalized = String(sidecarName || "")
196
+ .replace(/^sidecar\./, "")
197
+ .trim();
198
+ if (!normalized) {
199
+ return "Threads Weave Tool";
200
+ }
201
+ return normalized
202
+ .split(/[^A-Za-z0-9]+/g)
203
+ .filter(Boolean)
204
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase())
205
+ .join(" ");
206
+ }
207
+
208
+ function sidecarNameForOpenClaw(openClawName) {
209
+ const normalized = String(openClawName || "").trim();
210
+ if (normalized.startsWith(TOOL_NAME_PREFIX)) {
211
+ return `sidecar.${normalized.slice(TOOL_NAME_PREFIX.length)}`;
212
+ }
213
+ return normalized;
214
+ }
215
+
216
+ function summarizeContentItems(items) {
217
+ if (!Array.isArray(items)) {
218
+ return "";
219
+ }
220
+ return items
221
+ .map((item) => {
222
+ if (!item || typeof item !== "object") {
223
+ return "";
224
+ }
225
+ if (typeof item.text === "string" && item.text.trim()) {
226
+ return item.text.trim();
227
+ }
228
+ if (typeof item.type === "string") {
229
+ return `[${item.type}]`;
230
+ }
231
+ return "";
232
+ })
233
+ .filter(Boolean)
234
+ .join("\n");
235
+ }
236
+
237
+ function formatSidecarErrorPayload(payload) {
238
+ if (!payload || typeof payload !== "object") {
239
+ return "Threads Weave request failed.";
240
+ }
241
+ if (typeof payload.message === "string" && payload.message.trim()) {
242
+ return payload.message.trim();
243
+ }
244
+ if (typeof payload.error === "string" && payload.error.trim()) {
245
+ return payload.error.trim();
246
+ }
247
+ if (payload.structuredContent && typeof payload.structuredContent.error === "string") {
248
+ return payload.structuredContent.error;
249
+ }
250
+ if (payload.content) {
251
+ const summary = summarizeContentItems(payload.content);
252
+ if (summary) {
253
+ return summary;
254
+ }
255
+ }
256
+ return "Threads Weave request failed.";
257
+ }
258
+
259
+ function formatStructuredContentForOpenClaw(value) {
260
+ if (value === undefined) {
261
+ return "";
262
+ }
263
+ if (typeof value === "string") {
264
+ return value;
265
+ }
266
+ try {
267
+ return JSON.stringify(value, null, 2);
268
+ } catch (error) {
269
+ return String(value);
270
+ }
271
+ }
272
+
273
+ function normalizeToolResultForOpenClaw(result) {
274
+ if (!result || typeof result !== "object" || result.isError) {
275
+ return result;
276
+ }
277
+ if (!Object.prototype.hasOwnProperty.call(result, "structuredContent")) {
278
+ return result;
279
+ }
280
+ const structuredText = formatStructuredContentForOpenClaw(result.structuredContent);
281
+ if (!structuredText) {
282
+ return result;
283
+ }
284
+ return {
285
+ ...result,
286
+ content: [
287
+ {
288
+ type: "text",
289
+ text: structuredText,
290
+ },
291
+ ],
292
+ };
293
+ }
294
+
295
+ function buildManagementToolResult(payload, summaryText) {
296
+ const fallbackText = formatStructuredContentForOpenClaw(payload) || "Threads Weave request completed.";
297
+ return {
298
+ content: [
299
+ {
300
+ type: "text",
301
+ text: isTruthyString(summaryText) ? summaryText.trim() : fallbackText,
302
+ },
303
+ ],
304
+ structuredContent: payload,
305
+ };
306
+ }
307
+
308
+ function summarizeSidecarStatus(status) {
309
+ const profile = isTruthyString(status?.profile) ? status.profile.trim() : "unknown";
310
+ return `Threads Weave profile "${profile}" paired=${String(Boolean(status?.paired))} expired=${String(Boolean(status?.expired))}`;
311
+ }
312
+
313
+ function summarizePairingAction(payload) {
314
+ const action = isTruthyString(payload?.action) ? payload.action.trim() : "pair";
315
+ const status = payload?.status && typeof payload.status === "object" ? payload.status : {};
316
+ const profile = isTruthyString(status.profile) ? status.profile.trim() : "unknown";
317
+ return `Threads Weave ${action} completed for profile "${profile}" paired=${String(Boolean(status.paired))} expired=${String(Boolean(status.expired))}`;
318
+ }
319
+
320
+ function summarizeClearAction(payload) {
321
+ const profile = isTruthyString(payload?.profile) ? payload.profile.trim() : "unknown";
322
+ if (payload?.cleared === false) {
323
+ return `No cached local Threads Weave pairing found for profile "${profile}".`;
324
+ }
325
+ return `Threads Weave local pairing cleared for profile "${profile}".`;
326
+ }
327
+
328
+ function summarizeDoctorResult(payload) {
329
+ const profile = isTruthyString(payload?.profile) ? payload.profile.trim() : "unknown";
330
+ const checks = Array.isArray(payload?.checks) ? payload.checks : [];
331
+ const failedChecks = checks
332
+ .filter((check) => check && check.ok === false)
333
+ .map((check) => (isTruthyString(check?.name) ? check.name.trim() : ""))
334
+ .filter(Boolean);
335
+ if (failedChecks.length === 0) {
336
+ return `Threads Weave doctor passed for profile "${profile}" (${checks.length}/${checks.length} checks ok).`;
337
+ }
338
+ return `Threads Weave doctor found issues for profile "${profile}": ${failedChecks.join(", ")}.`;
339
+ }
340
+
341
+ function shouldSuggestFileSecretStorage(errorMessage) {
342
+ const message = String(errorMessage || "");
343
+ return (
344
+ message.includes("Secure secret storage is required for ")
345
+ || message.includes("Keyring storage is unavailable for ")
346
+ );
347
+ }
348
+
349
+ function isSingleUsePairingCommand(command) {
350
+ const normalized = String(command || "").trim().toLowerCase();
351
+ return normalized === "pair" || normalized === "renew";
352
+ }
353
+
354
+ function maybeAugmentSecretStorageError(errorMessage, cfg, attemptedSecretStorage = cfg.secretStorage) {
355
+ const message = String(errorMessage || "").trim();
356
+ if (!message || attemptedSecretStorage === "file" || !shouldSuggestFileSecretStorage(message)) {
357
+ return message;
358
+ }
359
+ return `${message} To use the OpenClaw plugin on a headless host, set plugins.entries["${PLUGIN_ID}"].config.secretStorage to "file" in ${DEFAULT_OPENCLAW_CONFIG_PATH}.`;
360
+ }
361
+
362
+ async function readJsonFileWithTimeout(filePath, timeoutMs, onTimeout, getAbortError) {
363
+ const deadline = Date.now() + timeoutMs;
364
+ for (;;) {
365
+ try {
366
+ const raw = await fsp.readFile(filePath, "utf8");
367
+ return JSON.parse(raw);
368
+ } catch (error) {
369
+ if (typeof getAbortError === "function") {
370
+ const abortError = getAbortError();
371
+ if (abortError) {
372
+ throw abortError;
373
+ }
374
+ }
375
+ if (Date.now() >= deadline) {
376
+ if (typeof onTimeout === "function") {
377
+ onTimeout();
378
+ }
379
+ throw error;
380
+ }
381
+ }
382
+ await new Promise((resolve) => setTimeout(resolve, 100));
383
+ }
384
+ }
385
+
386
+ class ThreadsWeaveSidecarManager {
387
+ constructor(api) {
388
+ this.api = api;
389
+ this.log = createLogger(api);
390
+ this.child = null;
391
+ this.stdoutInterface = null;
392
+ this.stderrInterface = null;
393
+ this.startPromise = null;
394
+ this.initializePromise = null;
395
+ this.initialized = false;
396
+ this.requestCounter = 1;
397
+ this.pending = new Map();
398
+ this.readyInfo = null;
399
+ this.tempReadyDir = null;
400
+ }
401
+
402
+ get config() {
403
+ return normalizeConfig(this.api);
404
+ }
405
+
406
+ ensureSidecarConfigured() {
407
+ const cfg = this.config;
408
+ if (!isTruthyString(cfg.sidecarPath)) {
409
+ throw new Error(
410
+ "Threads Weave plugin is missing sidecarPath. Reinstall the package so the bundled sidecar is present, or configure sidecarPath explicitly.",
411
+ );
412
+ }
413
+ if (!fs.existsSync(cfg.sidecarPath)) {
414
+ throw new Error(
415
+ `Threads Weave sidecar not found at ${cfg.sidecarPath}. Reinstall the package or point sidecarPath at a valid threads_weaving_sidecar.cjs.`,
416
+ );
417
+ }
418
+ if (!String(cfg.sidecarPath).endsWith(".cjs")) {
419
+ throw new Error(
420
+ `Threads Weave sidecar must be a Node.js .cjs entrypoint. Received ${cfg.sidecarPath}.`,
421
+ );
422
+ }
423
+ return cfg;
424
+ }
425
+
426
+ buildBaseArgs(cfg) {
427
+ const args = [];
428
+ if (isTruthyString(cfg.configDir)) {
429
+ args.push("--config-dir", cfg.configDir);
430
+ }
431
+ return args;
432
+ }
433
+
434
+ buildProfileArgs(cfg) {
435
+ return [
436
+ "--profile",
437
+ cfg.profile,
438
+ ];
439
+ }
440
+
441
+ buildAgentRuntimeArgs(cfg, secretStorageOverride = null) {
442
+ const secretStorage = isTruthyString(secretStorageOverride)
443
+ ? secretStorageOverride.trim()
444
+ : cfg.secretStorage;
445
+ const args = [
446
+ ...this.buildProfileArgs(cfg),
447
+ "--device-name",
448
+ cfg.deviceName,
449
+ "--client-type",
450
+ SIDE_CAR_CLIENT_TYPE,
451
+ "--sidecar-version",
452
+ SIDE_CAR_VERSION,
453
+ "--agent-name",
454
+ cfg.agentName,
455
+ "--agent-provider",
456
+ cfg.agentProvider,
457
+ "--secret-storage",
458
+ secretStorage,
459
+ "--non-interactive",
460
+ ];
461
+ return args;
462
+ }
463
+
464
+ getSecretStorageCandidates(cfg) {
465
+ const preferred = isTruthyString(cfg.secretStorage)
466
+ ? cfg.secretStorage.trim()
467
+ : DEFAULT_PLUGIN_SECRET_STORAGE;
468
+ if (preferred === "file") {
469
+ return ["file"];
470
+ }
471
+ if (preferred === "auto") {
472
+ return ["auto", "file"];
473
+ }
474
+ return [preferred];
475
+ }
476
+
477
+ extractCommandErrorMessage(error, command) {
478
+ const details = [
479
+ error?.stderr,
480
+ error?.stdout,
481
+ error?.message,
482
+ ]
483
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
484
+ .filter(Boolean);
485
+ return details[0] || `Threads Weave ${command} failed.`;
486
+ }
487
+
488
+ async runOneShotCommand(command, commandArgs = [], { parseJson = true, mode = "basic" } = {}) {
489
+ const cfg = this.ensureSidecarConfigured();
490
+ const secretStorageCandidates = mode === "runtime"
491
+ ? this.getSecretStorageCandidates(cfg)
492
+ : [null];
493
+ for (let index = 0; index < secretStorageCandidates.length; index += 1) {
494
+ const secretStorage = secretStorageCandidates[index];
495
+ const args = [
496
+ cfg.sidecarPath,
497
+ ...this.buildBaseArgs(cfg),
498
+ command,
499
+ ...(mode === "runtime"
500
+ ? this.buildAgentRuntimeArgs(cfg, secretStorage)
501
+ : this.buildProfileArgs(cfg)),
502
+ ...commandArgs,
503
+ ];
504
+ let result;
505
+ try {
506
+ result = await execFileAsync(cfg.nodeBin, args, {
507
+ cwd: process.cwd(),
508
+ env: process.env,
509
+ maxBuffer: 8 * 1024 * 1024,
510
+ });
511
+ } catch (error) {
512
+ const message = this.extractCommandErrorMessage(error, command);
513
+ const canFallbackToFile = (
514
+ mode === "runtime"
515
+ && secretStorage === "auto"
516
+ && !isSingleUsePairingCommand(command)
517
+ && secretStorageCandidates.slice(index + 1).includes("file")
518
+ && shouldSuggestFileSecretStorage(message)
519
+ );
520
+ if (canFallbackToFile) {
521
+ this.log.warn(
522
+ `Threads Weave sidecar ${command} could not use secretStorage="${secretStorage}". Retrying with file-backed storage.`,
523
+ );
524
+ continue;
525
+ }
526
+ throw new Error(
527
+ maybeAugmentSecretStorageError(message, cfg, secretStorage),
528
+ );
529
+ }
530
+ const stdout = String(result.stdout || "").trim();
531
+ if (!parseJson) {
532
+ return stdout;
533
+ }
534
+ if (!stdout) {
535
+ throw new Error(`Threads Weave ${command} returned no JSON payload.`);
536
+ }
537
+ return JSON.parse(stdout);
538
+ }
539
+ throw new Error(`Threads Weave ${command} failed.`);
540
+ }
541
+
542
+ async status() {
543
+ return this.runOneShotCommand("status", ["--json"]);
544
+ }
545
+
546
+ async doctor(options = {}) {
547
+ const args = ["--json"];
548
+ if (options.skipRemoteProbe) {
549
+ args.unshift("--skip-remote-probe");
550
+ } else {
551
+ await this.stop();
552
+ }
553
+ return this.runOneShotCommand("doctor", args);
554
+ }
555
+
556
+ resolveServerUrl(serverUrl, { required = false, useBundledDefault = false } = {}) {
557
+ const candidate = isTruthyString(serverUrl)
558
+ ? serverUrl.trim()
559
+ : this.config.serverUrl || (useBundledDefault ? DEFAULT_SERVER_URL : null);
560
+ if (candidate) {
561
+ return candidate;
562
+ }
563
+ if (required) {
564
+ throw new Error(
565
+ `Threads Weave server URL is required for first-time pairing. Pass "--server-url <URL>" or set plugins.entries["${PLUGIN_ID}"].config.serverUrl in ${DEFAULT_OPENCLAW_CONFIG_PATH}. The bundled default is ${DEFAULT_SERVER_URL}.`,
566
+ );
567
+ }
568
+ return null;
569
+ }
570
+
571
+ async pair(pairingCode, options = {}) {
572
+ if (!isTruthyString(pairingCode)) {
573
+ throw new Error("Pairing code is required.");
574
+ }
575
+ const serverUrl = this.resolveServerUrl(options.serverUrl, {
576
+ required: true,
577
+ useBundledDefault: true,
578
+ });
579
+ await this.stop();
580
+ return this.runOneShotCommand(
581
+ "pair",
582
+ ["--server-url", serverUrl, "--pairing-code", pairingCode, "--json"],
583
+ { mode: "runtime" },
584
+ );
585
+ }
586
+
587
+ async renew(pairingCode, options = {}) {
588
+ if (!isTruthyString(pairingCode)) {
589
+ throw new Error("Pairing code is required.");
590
+ }
591
+ const serverUrl = this.resolveServerUrl(options.serverUrl);
592
+ await this.stop();
593
+ return this.runOneShotCommand(
594
+ "renew",
595
+ [
596
+ ...(serverUrl ? ["--server-url", serverUrl] : []),
597
+ "--pairing-code",
598
+ pairingCode,
599
+ "--json",
600
+ ],
601
+ { mode: "runtime" },
602
+ );
603
+ }
604
+
605
+ async clear() {
606
+ await this.stop();
607
+ return this.runOneShotCommand("clear-token", ["--json"]);
608
+ }
609
+
610
+ async ensureStarted() {
611
+ if (this.startPromise) {
612
+ return this.startPromise;
613
+ }
614
+ if (this.child && !this.child.killed && this.readyInfo) {
615
+ return this.readyInfo;
616
+ }
617
+ const startPromise = this.startInternal();
618
+ this.startPromise = startPromise;
619
+ try {
620
+ return await startPromise;
621
+ } finally {
622
+ if (this.startPromise === startPromise) {
623
+ this.startPromise = null;
624
+ }
625
+ }
626
+ }
627
+
628
+ async startInternal() {
629
+ const cfg = this.ensureSidecarConfigured();
630
+ const status = await this.status().catch((error) => {
631
+ throw new Error(`Unable to read Threads Weave sidecar status: ${error.message}`);
632
+ });
633
+ if (!status || typeof status !== "object") {
634
+ throw new Error("Threads Weave sidecar status response was invalid.");
635
+ }
636
+ if (status.pairingRequired) {
637
+ throw new Error(
638
+ `Threads Weave sidecar profile "${cfg.profile}" is not paired or has expired. Run "openclaw ${MANAGEMENT_COMMAND} pair --code <PAIR_CODE>" first.`,
639
+ );
640
+ }
641
+
642
+ const readyDir = await fsp.mkdtemp(path.join(os.tmpdir(), "threads-weave-sidecar-"));
643
+ const readyFile = path.join(readyDir, "ready.json");
644
+ const args = [
645
+ cfg.sidecarPath,
646
+ ...this.buildBaseArgs(cfg),
647
+ "stdio",
648
+ ...this.buildAgentRuntimeArgs(cfg),
649
+ "--ready-file",
650
+ readyFile,
651
+ ];
652
+ if (cfg.serverUrl) {
653
+ args.push("--server-url", cfg.serverUrl);
654
+ }
655
+ if (cfg.persistSession) {
656
+ args.push("--persist-session");
657
+ }
658
+ const child = spawn(cfg.nodeBin, args, {
659
+ cwd: process.cwd(),
660
+ env: process.env,
661
+ stdio: ["pipe", "pipe", "pipe"],
662
+ });
663
+ this.child = child;
664
+ this.tempReadyDir = readyDir;
665
+ this.initialized = false;
666
+ this.pending.clear();
667
+ let startupError = null;
668
+ let startupReady = false;
669
+
670
+ child.once("error", (error) => {
671
+ this.log.error("Threads Weave sidecar failed to start", { error });
672
+ this.rejectAllPending(new Error(`Threads Weave sidecar failed to start: ${error.message}`));
673
+ if (!startupReady) {
674
+ startupError = new Error(`Threads Weave sidecar failed to start: ${error.message}`);
675
+ }
676
+ });
677
+
678
+ child.once("exit", (code, signal) => {
679
+ const message = `Threads Weave sidecar exited (code=${String(code)}, signal=${String(signal)}).`;
680
+ this.log.warn(message);
681
+ this.child = null;
682
+ this.initialized = false;
683
+ this.readyInfo = null;
684
+ this.rejectAllPending(new Error(message));
685
+ void this.cleanupTempReadyDir();
686
+ if (!startupReady) {
687
+ startupError = new Error(message);
688
+ }
689
+ });
690
+
691
+ this.stdoutInterface = readline.createInterface({ input: child.stdout });
692
+ this.stdoutInterface.on("line", (line) => this.handleStdoutLine(line));
693
+ this.stderrInterface = readline.createInterface({ input: child.stderr });
694
+ this.stderrInterface.on("line", (line) => {
695
+ if (line && line.trim()) {
696
+ this.log.warn(`Threads Weave sidecar: ${line.trim()}`);
697
+ }
698
+ });
699
+
700
+ let readyPayload;
701
+ try {
702
+ readyPayload = await readJsonFileWithTimeout(readyFile, cfg.startupTimeoutMs, () => {
703
+ if (this.child && !this.child.killed) {
704
+ this.child.kill();
705
+ }
706
+ }, () => startupError);
707
+ startupReady = true;
708
+ } catch (error) {
709
+ await this.stop();
710
+ if (startupError && error === startupError) {
711
+ throw error;
712
+ }
713
+ throw new Error(`Timed out waiting for Threads Weave sidecar readiness: ${error.message}`);
714
+ }
715
+ this.readyInfo = readyPayload;
716
+ return readyPayload;
717
+ }
718
+
719
+ async initializeIfNeeded() {
720
+ if (this.initialized) {
721
+ return;
722
+ }
723
+ if (this.initializePromise) {
724
+ await this.initializePromise;
725
+ return;
726
+ }
727
+ const initializePromise = (async () => {
728
+ await this.ensureStarted();
729
+ await this.sendRequest("initialize", {
730
+ protocolVersion: DEFAULT_PROTOCOL_VERSION,
731
+ capabilities: {},
732
+ clientInfo: {
733
+ name: PLUGIN_ID,
734
+ version: SIDE_CAR_VERSION,
735
+ },
736
+ });
737
+ this.sendNotification("notifications/initialized");
738
+ this.initialized = true;
739
+ })();
740
+ this.initializePromise = initializePromise;
741
+ try {
742
+ await initializePromise;
743
+ } finally {
744
+ if (this.initializePromise === initializePromise) {
745
+ this.initializePromise = null;
746
+ }
747
+ }
748
+ }
749
+
750
+ async callTool(sidecarToolName, argumentsValue) {
751
+ await this.initializeIfNeeded();
752
+ const result = await this.sendRequest("tools/call", {
753
+ name: sidecarToolName,
754
+ arguments: argumentsValue && typeof argumentsValue === "object" ? argumentsValue : {},
755
+ });
756
+ if (result && typeof result === "object" && result.isError) {
757
+ throw new Error(formatSidecarErrorPayload(result));
758
+ }
759
+ return normalizeToolResultForOpenClaw(result);
760
+ }
761
+
762
+ sendNotification(method, params) {
763
+ if (!this.child || this.child.killed || !this.child.stdin) {
764
+ return;
765
+ }
766
+ const payload = {
767
+ jsonrpc: "2.0",
768
+ method,
769
+ };
770
+ if (params !== undefined) {
771
+ payload.params = params;
772
+ }
773
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`);
774
+ }
775
+
776
+ sendRequest(method, params, options = {}) {
777
+ if (!this.child || this.child.killed || !this.child.stdin) {
778
+ return Promise.reject(new Error("Threads Weave sidecar is not running."));
779
+ }
780
+ const timeoutMs = Number.isFinite(options?.timeoutMs) && options.timeoutMs > 0
781
+ ? Number(options.timeoutMs)
782
+ : this.config.requestTimeoutMs;
783
+ const id = this.requestCounter++;
784
+ const payload = {
785
+ jsonrpc: "2.0",
786
+ id,
787
+ method,
788
+ };
789
+ if (params !== undefined) {
790
+ payload.params = params;
791
+ }
792
+ return new Promise((resolve, reject) => {
793
+ let settled = false;
794
+ const finish = (callback, value) => {
795
+ if (settled) {
796
+ return;
797
+ }
798
+ settled = true;
799
+ if (timer) {
800
+ clearTimeout(timer);
801
+ }
802
+ callback(value);
803
+ };
804
+ const timer = setTimeout(() => {
805
+ const pendingId = String(id);
806
+ if (!this.pending.has(pendingId)) {
807
+ return;
808
+ }
809
+ this.pending.delete(pendingId);
810
+ this.log.warn("Threads Weave sidecar request timed out", {
811
+ id,
812
+ method,
813
+ params,
814
+ timeoutMs,
815
+ });
816
+ finish(reject, new Error(buildSidecarRequestTimeoutError(method, params, timeoutMs)));
817
+ }, timeoutMs);
818
+ timer.unref?.();
819
+ this.pending.set(String(id), {
820
+ resolve: (value) => finish(resolve, value),
821
+ reject: (error) => finish(reject, error),
822
+ });
823
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`, (error) => {
824
+ if (error) {
825
+ this.pending.delete(String(id));
826
+ finish(reject, error);
827
+ }
828
+ });
829
+ });
830
+ }
831
+
832
+ handleStdoutLine(line) {
833
+ if (!line || !line.trim()) {
834
+ return;
835
+ }
836
+ let payload;
837
+ try {
838
+ payload = JSON.parse(line);
839
+ } catch (error) {
840
+ this.log.warn("Threads Weave sidecar emitted non-JSON stdout", { line });
841
+ return;
842
+ }
843
+ const id = payload && Object.prototype.hasOwnProperty.call(payload, "id")
844
+ ? String(payload.id)
845
+ : null;
846
+ if (!id || !this.pending.has(id)) {
847
+ this.log.debug("Threads Weave sidecar notification", payload);
848
+ return;
849
+ }
850
+ const entry = this.pending.get(id);
851
+ this.pending.delete(id);
852
+ if (payload && typeof payload === "object" && payload.error) {
853
+ const message = formatSidecarErrorPayload(payload.error);
854
+ entry.reject(new Error(message));
855
+ return;
856
+ }
857
+ entry.resolve(payload.result);
858
+ }
859
+
860
+ rejectAllPending(error) {
861
+ for (const [id, entry] of this.pending.entries()) {
862
+ this.pending.delete(id);
863
+ entry.reject(error);
864
+ }
865
+ }
866
+
867
+ async waitForChildExit(child, timeoutMs) {
868
+ if (hasChildProcessExited(child)) {
869
+ return true;
870
+ }
871
+ const normalizedTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0
872
+ ? Number(timeoutMs)
873
+ : DEFAULT_STOP_GRACE_PERIOD_MS;
874
+ return new Promise((resolve) => {
875
+ let settled = false;
876
+ const finish = (didExit) => {
877
+ if (settled) {
878
+ return;
879
+ }
880
+ settled = true;
881
+ if (timer) {
882
+ clearTimeout(timer);
883
+ }
884
+ child?.removeListener?.("exit", onExit);
885
+ resolve(didExit);
886
+ };
887
+ const onExit = () => finish(true);
888
+ child?.once?.("exit", onExit);
889
+ const timer = setTimeout(() => finish(hasChildProcessExited(child)), normalizedTimeoutMs);
890
+ timer.unref?.();
891
+ });
892
+ }
893
+
894
+ signalChildProcess(child, signal) {
895
+ if (!child || hasChildProcessExited(child) || typeof child.kill !== "function") {
896
+ return false;
897
+ }
898
+ try {
899
+ return child.kill(signal);
900
+ } catch (error) {
901
+ this.log.warn(`Threads Weave sidecar could not receive ${signal}`, { error });
902
+ return false;
903
+ }
904
+ }
905
+
906
+ async terminateChildProcess(
907
+ child,
908
+ { gracePeriodMs = DEFAULT_STOP_GRACE_PERIOD_MS, forceKillGracePeriodMs = DEFAULT_FORCE_KILL_TIMEOUT_MS } = {},
909
+ ) {
910
+ if (hasChildProcessExited(child)) {
911
+ return;
912
+ }
913
+ this.signalChildProcess(child, "SIGINT");
914
+ const exitedGracefully = await this.waitForChildExit(child, gracePeriodMs);
915
+ if (exitedGracefully) {
916
+ return;
917
+ }
918
+ this.log.warn(
919
+ `Threads Weave sidecar did not exit after ${gracePeriodMs}ms; sending SIGKILL.`,
920
+ );
921
+ this.signalChildProcess(child, "SIGKILL");
922
+ const exitedAfterForceKill = await this.waitForChildExit(child, forceKillGracePeriodMs);
923
+ if (exitedAfterForceKill) {
924
+ return;
925
+ }
926
+ throw new Error(
927
+ `Threads Weave sidecar did not exit within ${forceKillGracePeriodMs}ms after SIGKILL.`,
928
+ );
929
+ }
930
+
931
+ async cleanupTempReadyDir() {
932
+ if (!this.tempReadyDir) {
933
+ return;
934
+ }
935
+ const target = this.tempReadyDir;
936
+ this.tempReadyDir = null;
937
+ await fsp.rm(target, { recursive: true, force: true }).catch(() => {});
938
+ }
939
+
940
+ async stop(options = {}) {
941
+ const child = this.child;
942
+ const gracePeriodMs = Number.isFinite(options?.gracePeriodMs) && options.gracePeriodMs > 0
943
+ ? Number(options.gracePeriodMs)
944
+ : DEFAULT_STOP_GRACE_PERIOD_MS;
945
+ const forceKillGracePeriodMs = Number.isFinite(options?.forceKillGracePeriodMs)
946
+ && options.forceKillGracePeriodMs > 0
947
+ ? Number(options.forceKillGracePeriodMs)
948
+ : DEFAULT_FORCE_KILL_TIMEOUT_MS;
949
+ this.child = null;
950
+ this.initialized = false;
951
+ this.initializePromise = null;
952
+ this.readyInfo = null;
953
+ if (this.stdoutInterface) {
954
+ this.stdoutInterface.close();
955
+ this.stdoutInterface = null;
956
+ }
957
+ if (this.stderrInterface) {
958
+ this.stderrInterface.close();
959
+ this.stderrInterface = null;
960
+ }
961
+ let stopError = null;
962
+ try {
963
+ if (child && !hasChildProcessExited(child)) {
964
+ await this.terminateChildProcess(child, {
965
+ gracePeriodMs,
966
+ forceKillGracePeriodMs,
967
+ });
968
+ }
969
+ } catch (error) {
970
+ stopError = error;
971
+ this.log.error("Threads Weave sidecar failed to stop cleanly", { error });
972
+ }
973
+ this.rejectAllPending(
974
+ new Error(
975
+ stopError
976
+ ? `Threads Weave sidecar stop failed: ${stopError.message}`
977
+ : "Threads Weave sidecar stopped.",
978
+ ),
979
+ );
980
+ await this.cleanupTempReadyDir();
981
+ if (stopError) {
982
+ throw stopError;
983
+ }
984
+ }
985
+ }
986
+
987
+ function formatJsonForCli(payload) {
988
+ return JSON.stringify(payload, null, 2);
989
+ }
990
+
991
+ function printCliPayload(payload, options) {
992
+ if (options?.json) {
993
+ process.stdout.write(`${formatJsonForCli(payload)}\n`);
994
+ return;
995
+ }
996
+ if (!payload || typeof payload !== "object") {
997
+ process.stdout.write(`${String(payload)}\n`);
998
+ return;
999
+ }
1000
+ if (payload.status && typeof payload.status === "object") {
1001
+ process.stdout.write(`${formatJsonForCli(payload.status)}\n`);
1002
+ return;
1003
+ }
1004
+ process.stdout.write(`${formatJsonForCli(payload)}\n`);
1005
+ }
1006
+
1007
+ function registerCli(api, manager) {
1008
+ if (typeof api.registerCli !== "function") {
1009
+ return;
1010
+ }
1011
+ api.registerCli(({ program }) => {
1012
+ const root = program
1013
+ .command(MANAGEMENT_COMMAND)
1014
+ .description("Manage the Threads Weave sidecar connection");
1015
+
1016
+ root
1017
+ .command("pair")
1018
+ .requiredOption("--code <pairingCode>", "One-time pairing code from Threads Weave")
1019
+ .option(
1020
+ "--server-url <serverUrl>",
1021
+ `Override the Threads Weave server URL for pairing. Defaults to ${DEFAULT_SERVER_URL}.`,
1022
+ )
1023
+ .option("--json", "Print raw JSON payload")
1024
+ .action(async (options) => {
1025
+ const payload = await manager.pair(options.code, { serverUrl: options.serverUrl });
1026
+ printCliPayload(payload, options);
1027
+ });
1028
+
1029
+ root
1030
+ .command("renew")
1031
+ .requiredOption("--code <pairingCode>", "Fresh one-time pairing code from Threads Weave")
1032
+ .option(
1033
+ "--server-url <serverUrl>",
1034
+ "Optional override for the Threads Weave server URL when renewing the cached token.",
1035
+ )
1036
+ .option("--json", "Print raw JSON payload")
1037
+ .action(async (options) => {
1038
+ const payload = await manager.renew(options.code, { serverUrl: options.serverUrl });
1039
+ printCliPayload(payload, options);
1040
+ });
1041
+
1042
+ root
1043
+ .command("status")
1044
+ .option("--json", "Print raw JSON payload")
1045
+ .action(async (options) => {
1046
+ const payload = await manager.status();
1047
+ printCliPayload(payload, options);
1048
+ });
1049
+
1050
+ root
1051
+ .command("doctor")
1052
+ .option("--skip-remote-probe", "Run only local checks and skip the remote MCP initialize probe")
1053
+ .option("--json", "Print raw JSON payload")
1054
+ .action(async (options) => {
1055
+ const payload = await manager.doctor({
1056
+ skipRemoteProbe: Boolean(options.skipRemoteProbe),
1057
+ });
1058
+ printCliPayload(payload, options);
1059
+ });
1060
+
1061
+ root
1062
+ .command("clear")
1063
+ .option("--json", "Print raw JSON payload")
1064
+ .action(async (options) => {
1065
+ const payload = await manager.clear();
1066
+ printCliPayload(payload, options);
1067
+ });
1068
+ }, { commands: [MANAGEMENT_COMMAND] });
1069
+ }
1070
+
1071
+ function registerService(api, manager) {
1072
+ if (typeof api.registerService !== "function") {
1073
+ return;
1074
+ }
1075
+ api.registerService({
1076
+ id: "threads-weave-sidecar",
1077
+ start: async () => {
1078
+ if (!manager.config.autoStart) {
1079
+ return;
1080
+ }
1081
+ try {
1082
+ await manager.ensureStarted();
1083
+ } catch (error) {
1084
+ manager.log.warn(`Threads Weave sidecar autostart skipped: ${error.message}`);
1085
+ }
1086
+ },
1087
+ stop: async () => {
1088
+ await manager.stop();
1089
+ },
1090
+ });
1091
+ }
1092
+
1093
+ function registerTools(api, manager) {
1094
+ if (typeof api.registerTool !== "function") {
1095
+ return;
1096
+ }
1097
+ for (const definition of TOOL_DEFINITIONS) {
1098
+ const openClawName = toolNameForOpenClaw(definition.name);
1099
+ const openClawLabel = toolLabelForOpenClaw(definition.name);
1100
+ api.registerTool(
1101
+ {
1102
+ name: openClawName,
1103
+ label: openClawLabel,
1104
+ description: definition.description,
1105
+ parameters: definition.inputSchema,
1106
+ execute: async (_toolCallId, params) => {
1107
+ return manager.callTool(definition.name, params ?? {});
1108
+ },
1109
+ },
1110
+ {
1111
+ optional: WRITE_TOOL_NAMES.has(definition.name),
1112
+ },
1113
+ );
1114
+ }
1115
+ }
1116
+
1117
+ function registerManagementTools(api, manager) {
1118
+ if (typeof api.registerTool !== "function") {
1119
+ return;
1120
+ }
1121
+
1122
+ api.registerTool(
1123
+ {
1124
+ name: "threads_weave_pair",
1125
+ label: toolLabelForOpenClaw("pair"),
1126
+ description: "Exchange a one-time Threads Weave pairing code for a cached local sidecar token. This pairs the local sidecar profile and does not create or renew the server-side grant itself. If the profile is already paired, calling pair again with a fresh pairing code replaces the local cached token and profile state; retrying the same one-time code will fail because pairing codes are single-use. Prefer renew when rotating an existing pairing.",
1127
+ parameters: {
1128
+ type: "object",
1129
+ properties: {
1130
+ pairingCode: {
1131
+ type: "string",
1132
+ description: "One-time pairing code from Threads Weave.",
1133
+ },
1134
+ serverUrl: {
1135
+ type: "string",
1136
+ description: `Optional override for the Threads Weave server URL. Defaults to the plugin config value or ${DEFAULT_SERVER_URL}.`,
1137
+ },
1138
+ },
1139
+ required: ["pairingCode"],
1140
+ additionalProperties: false,
1141
+ },
1142
+ execute: async (_toolCallId, params) => {
1143
+ const payload = await manager.pair(params?.pairingCode, { serverUrl: params?.serverUrl });
1144
+ return buildManagementToolResult(payload, summarizePairingAction(payload));
1145
+ },
1146
+ },
1147
+ { optional: true },
1148
+ );
1149
+
1150
+ api.registerTool(
1151
+ {
1152
+ name: "threads_weave_renew",
1153
+ label: toolLabelForOpenClaw("renew"),
1154
+ description: "Replace the cached local sidecar token using a fresh one-time pairing code. This renews local sidecar pairing only and does not extend or renew the server-side grant duration. Use this when the profile is already paired and you want to rotate credentials without relying on pair to overwrite the local state.",
1155
+ parameters: {
1156
+ type: "object",
1157
+ properties: {
1158
+ pairingCode: {
1159
+ type: "string",
1160
+ description: "Fresh one-time pairing code from Threads Weave.",
1161
+ },
1162
+ serverUrl: {
1163
+ type: "string",
1164
+ description: "Optional override for the Threads Weave server URL when renewing the cached token.",
1165
+ },
1166
+ },
1167
+ required: ["pairingCode"],
1168
+ additionalProperties: false,
1169
+ },
1170
+ execute: async (_toolCallId, params) => {
1171
+ const payload = await manager.renew(params?.pairingCode, { serverUrl: params?.serverUrl });
1172
+ return buildManagementToolResult(payload, summarizePairingAction(payload));
1173
+ },
1174
+ },
1175
+ { optional: true },
1176
+ );
1177
+
1178
+ api.registerTool(
1179
+ {
1180
+ name: "threads_weave_doctor",
1181
+ label: toolLabelForOpenClaw("doctor"),
1182
+ description: "Run local Threads Weave sidecar diagnostics for the current profile. By default this includes local checks and a remote MCP initialize/cleanup probe; when a live stdio bridge is running, the plugin stops it first to avoid probing and deleting the active remote session. Set skipRemoteProbe to true to run local-only checks.",
1183
+ parameters: {
1184
+ type: "object",
1185
+ properties: {
1186
+ skipRemoteProbe: {
1187
+ type: "boolean",
1188
+ description: "When true, skip the remote MCP initialize probe and run local-only checks.",
1189
+ },
1190
+ },
1191
+ additionalProperties: false,
1192
+ },
1193
+ execute: async (_toolCallId, params) => {
1194
+ const payload = await manager.doctor({
1195
+ skipRemoteProbe: Boolean(params?.skipRemoteProbe),
1196
+ });
1197
+ return buildManagementToolResult(payload, summarizeDoctorResult(payload));
1198
+ },
1199
+ },
1200
+ { optional: true },
1201
+ );
1202
+
1203
+ api.registerTool(
1204
+ {
1205
+ name: "threads_weave_status",
1206
+ label: toolLabelForOpenClaw("status"),
1207
+ description: "Inspect the local Threads Weave sidecar pairing and profile state. This reports local token and profile status, not the grant-renew lifecycle action.",
1208
+ parameters: {
1209
+ type: "object",
1210
+ properties: {},
1211
+ additionalProperties: false,
1212
+ },
1213
+ execute: async () => {
1214
+ const status = await manager.status();
1215
+ return buildManagementToolResult(status, summarizeSidecarStatus(status));
1216
+ },
1217
+ },
1218
+ { optional: true },
1219
+ );
1220
+
1221
+ api.registerTool(
1222
+ {
1223
+ name: "threads_weave_unpair",
1224
+ label: toolLabelForOpenClaw("unpair"),
1225
+ description: "Delete the cached local sidecar token and clear local Threads Weave pairing state for the current profile. This unpairs the local sidecar only and does not revoke or renew the server-side grant.",
1226
+ parameters: {
1227
+ type: "object",
1228
+ properties: {},
1229
+ additionalProperties: false,
1230
+ },
1231
+ execute: async () => {
1232
+ const payload = await manager.clear();
1233
+ return buildManagementToolResult(payload, summarizeClearAction(payload));
1234
+ },
1235
+ },
1236
+ { optional: true },
1237
+ );
1238
+ }
1239
+
1240
+ export default function registerThreadsWeavePlugin(api) {
1241
+ const manager = new ThreadsWeaveSidecarManager(api);
1242
+ registerService(api, manager);
1243
+ registerCli(api, manager);
1244
+ registerTools(api, manager);
1245
+ registerManagementTools(api, manager);
1246
+ }
1247
+
1248
+ export {
1249
+ BUNDLED_SIDE_CAR_PATH,
1250
+ MANAGEMENT_COMMAND,
1251
+ PLUGIN_ID,
1252
+ DEFAULT_SIDE_CAR_PATH,
1253
+ DEFAULT_SERVER_URL,
1254
+ PLUGIN_PACKAGE_ROOT,
1255
+ sidecarNameForOpenClaw,
1256
+ toolNameForOpenClaw,
1257
+ ThreadsWeaveSidecarManager,
1258
+ };