@hellolynk-ai/workbuddy-mcp 0.0.0-stage → 0.3.97
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/LICENSE +92 -0
- package/README.md +103 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/clients/core/hellolynk-client-account.mjs +180 -0
- package/clients/core/hellolynk-client-core.mjs +770 -0
- package/clients/core/hellolynk-client-profile.mjs +191 -0
- package/clients/core/hellolynk-client-update-entry.mjs +31 -0
- package/clients/core/hellolynk-client-update-runtime.mjs +600 -0
- package/clients/core/hellolynk-client-update.mjs +453 -0
- package/clients/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/clients/core/hellolynk-device-enrollment.mjs +435 -0
- package/clients/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/clients/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/clients/core/hellolynk-host-auth.mjs +347 -0
- package/clients/core/hellolynk-host-protected-store.mjs +2721 -0
- package/clients/core/hellolynk-host-transport-client.mjs +946 -0
- package/clients/core/hellolynk-host.mjs +2374 -0
- package/clients/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/clients/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/clients/core/hellolynk-inbox-read-state.mjs +11 -0
- package/clients/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/clients/core/hellolynk-instance-conversation.mjs +185 -0
- package/clients/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/clients/core/hellolynk-local-contacts.mjs +1228 -0
- package/clients/core/hellolynk-local-data-migration.mjs +766 -0
- package/clients/core/hellolynk-local-data-protection.mjs +747 -0
- package/clients/core/hellolynk-local-identity-trust.mjs +274 -0
- package/clients/core/hellolynk-local-messaging.mjs +3043 -0
- package/clients/core/hellolynk-local-peer-transport.mjs +103 -0
- package/clients/core/hellolynk-local-process-lock.mjs +157 -0
- package/clients/core/hellolynk-model-inbox.mjs +230 -0
- package/clients/core/hellolynk-model-notices.cjs +60 -0
- package/clients/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/clients/core/hellolynk-native-conversations.mjs +280 -0
- package/clients/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/clients/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/clients/core/hellolynk-native-notice.mjs +50 -0
- package/clients/core/hellolynk-native-reply-routes.mjs +96 -0
- package/clients/core/hellolynk-native-task-index.cjs +305 -0
- package/clients/core/hellolynk-native-worker-lease.mjs +84 -0
- package/clients/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/clients/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/clients/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/clients/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/clients/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/clients/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/clients/core/hellolynk-platform-credential-store.mjs +773 -0
- package/clients/core/hellolynk-relay-client-cli.mjs +232 -0
- package/clients/core/hellolynk-release-retention.mjs +277 -0
- package/clients/core/hellolynk-runtime-safety.cjs +471 -0
- package/clients/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/clients/core/hellolynk-workbench-connection.mjs +234 -0
- package/clients/platform/windows/credential-store.ps1 +844 -0
- package/clients/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/clients/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/clients/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/clients/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/clients/workbench/contract.mjs +1314 -0
- package/clients/workbench/service.mjs +485 -0
- package/hellolynk-client-pack-manifest.json +884 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/integrations/workbuddy/package.json +15 -0
- package/integrations/workbuddy/server/host-lifecycle.mjs +249 -0
- package/integrations/workbuddy/server/mcp-proxy.mjs +258 -0
- package/integrations/workbuddy/server/mcp-server.mjs +357 -0
- package/integrations/workbuddy/server/model-call-origin.mjs +87 -0
- package/integrations/workbuddy/server/native-task-cdp.mjs +175 -0
- package/integrations/workbuddy/server/native-task-debug-startup.mjs +117 -0
- package/integrations/workbuddy/server/native-task-debug-startup.ps1 +40 -0
- package/integrations/workbuddy/server/native-task-mcp-registration.mjs +30 -0
- package/integrations/workbuddy/server/native-task-service-registration.mjs +115 -0
- package/integrations/workbuddy/server/native-task-worker.mjs +127 -0
- package/integrations/workbuddy/server/neutral-host-adapter.mjs +639 -0
- package/integrations/workbuddy/server/origin-hook.mjs +111 -0
- package/integrations/workbuddy/server/origin-routes.mjs +211 -0
- package/integrations/workbuddy/server/package-bootstrap.mjs +111 -0
- package/integrations/workbuddy/server/workbench-candidate.mjs +202 -0
- package/integrations/workbuddy/server/workbench-hidden-launcher.vbs +46 -0
- package/integrations/workbuddy/server/workbench-process.mjs +373 -0
- package/integrations/workbuddy/server/workbench-task.ps1 +196 -0
- package/integrations/workbuddy/skills/hellolynk-inbox/SKILL.md +9 -0
- package/integrations/workbuddy/skills/hellolynk-inbox/workbuddy.json +6 -0
- package/package.json +65 -4
- package/runtime/connect-activate.mjs +837 -0
- package/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/runtime/core/hellolynk-client-account.mjs +180 -0
- package/runtime/core/hellolynk-client-core.mjs +770 -0
- package/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/runtime/core/hellolynk-client-update.mjs +453 -0
- package/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/runtime/core/hellolynk-host.mjs +2374 -0
- package/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/runtime/neutral-host-bootstrap.mjs +57 -0
- package/runtime/neutral-host-release-entry.mjs +81 -0
- package/runtime/neutral-host-release-loader.mjs +156 -0
- package/runtime/neutral-host-release-manifest.json +302 -0
- package/runtime/neutral-host-release-pin.json +4 -0
- package/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/runtime/platform/windows/credential-store.ps1 +844 -0
- package/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/runtime/workbench/contract.mjs +1314 -0
- package/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/runtime/workbench/host-capabilities.mjs +80 -0
- package/runtime/workbench/i18n.mjs +1127 -0
- package/runtime/workbench/loopback-surface.mjs +740 -0
- package/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/runtime/workbench/service.mjs +485 -0
- package/runtime/workbench/state.mjs +421 -0
- package/runtime/workbench/transports.mjs +171 -0
- package/runtime/workbench/ui/app.mjs +6120 -0
- package/runtime/workbench/ui/connection-form.mjs +155 -0
- package/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/runtime/workbench/ui/http-transport.mjs +247 -0
- package/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/runtime/workbench/ui/icons/close.svg +3 -0
- package/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/runtime/workbench/ui/icons/search.svg +3 -0
- package/runtime/workbench/ui/icons/security.svg +3 -0
- package/runtime/workbench/ui/icons/settings.svg +3 -0
- package/runtime/workbench/ui/index.html +139 -0
- package/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/runtime/workbench/ui/styles.css +686 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
- package/tools/hellolynk-install-client-skills.mjs +1211 -0
- package/workbuddy-package-manifest.json +1 -0
- package/workbuddy-package-pin.json +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hellolynk-ai/workbuddy-mcp",
|
|
3
|
+
"version": "0.3.97",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "WorkBuddy one-click bridge for local HelloLynk Messaging.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"smoke": "node test/workbuddy-stdio-smoke.mjs",
|
|
9
|
+
"test": "node test/workbuddy-stdio-smoke.mjs && node test/local-web-smoke.mjs && node test/local-web-package-smoke.mjs && node test/workbench-process-smoke.mjs && node test/pending-consumption-smoke.mjs && node test/pending-loopback-smoke.mjs && node --test test/model-inbox-caller.test.mjs test/proxy-reply.test.mjs test/native-task-store.test.mjs test/native-task-worker.test.mjs test/origin-routes.test.mjs test/native-task-scheduled.test.mjs test/native-task-debug-startup.test.mjs test/native-task-service-registration.test.mjs test/native-task-upgrade.test.mjs",
|
|
10
|
+
"smoke:web": "node test/local-web-smoke.mjs",
|
|
11
|
+
"build:immutable-package": "node scripts/build-local-web-package.mjs --output workbuddy-immutable-package",
|
|
12
|
+
"smoke:pending": "node test/pending-consumption-smoke.mjs",
|
|
13
|
+
"smoke:pending:loopback": "node test/pending-loopback-smoke.mjs"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { launchPinnedNeutralHostBootstrap } from "../../../clients/core/hellolynk-neutral-host-launcher.mjs";
|
|
5
|
+
|
|
6
|
+
const RUNNER_RELATIVE = "runtime/neutral-host-bootstrap.mjs";
|
|
7
|
+
const LOCK_STALE_MS = 10_000;
|
|
8
|
+
const SAFE_RELATIVE = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*[A-Za-z0-9][A-Za-z0-9._-]*\.(?:cjs|mjs|ps1)$/;
|
|
9
|
+
const REQUIRED_RUNTIME_RECORDS = Object.freeze([
|
|
10
|
+
"neutral-host-bootstrap.mjs",
|
|
11
|
+
"neutral-host-release-loader.mjs",
|
|
12
|
+
"neutral-host-release-entry.mjs",
|
|
13
|
+
"core/hellolynk-host-protected-store.mjs",
|
|
14
|
+
"core/hellolynk-local-data-migration.mjs",
|
|
15
|
+
"core/hellolynk-local-data-protection.mjs",
|
|
16
|
+
"core/hellolynk-local-identity-trust.mjs",
|
|
17
|
+
"core/hellolynk-neutral-host-installation-store.mjs",
|
|
18
|
+
"core/hellolynk-neutral-host-assembly.mjs",
|
|
19
|
+
"core/hellolynk-neutral-host-process.mjs",
|
|
20
|
+
"core/hellolynk-neutral-host-workbench-read-model.mjs",
|
|
21
|
+
"core/hellolynk-workbench-action-grants.mjs",
|
|
22
|
+
"platform/windows/hellolynk-host-protected-store.ps1",
|
|
23
|
+
"platform/windows/hellolynk-local-data-key.ps1",
|
|
24
|
+
"workbench/contract.mjs",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export async function launchWorkBuddyNeutralHost(binding, options = {}) {
|
|
28
|
+
const launch = normalizeLaunchBinding(binding, options);
|
|
29
|
+
await verifyPinnedBootstrap(launch);
|
|
30
|
+
const lockPath = path.join(
|
|
31
|
+
launch.runtimeRoot,
|
|
32
|
+
"neutral-host",
|
|
33
|
+
`${tupleDigest(launch)}.workbuddy-launch.lock`,
|
|
34
|
+
);
|
|
35
|
+
await ensureSafeDirectory(launch.runtimeRoot, true);
|
|
36
|
+
await ensureSafeDirectory(path.dirname(lockPath), true);
|
|
37
|
+
const wait = options.wait || delay;
|
|
38
|
+
const deadline = Date.now() + bounded(options.lockTimeoutMs, 15_000, 500, 60_000);
|
|
39
|
+
let ownsLock = false;
|
|
40
|
+
while (Date.now() < deadline) {
|
|
41
|
+
try {
|
|
42
|
+
await fs.mkdir(lockPath, { mode: 0o700 });
|
|
43
|
+
await fs.writeFile(path.join(lockPath, "owner.json"), `${JSON.stringify({
|
|
44
|
+
contract: "hellolynk-workbuddy-host-launch-lock-v1",
|
|
45
|
+
pid: process.pid,
|
|
46
|
+
createdAtMs: Date.now(),
|
|
47
|
+
})}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
48
|
+
ownsLock = true;
|
|
49
|
+
break;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error?.code !== "EEXIST") throw codeError("neutral_workbuddy_launch_failed");
|
|
52
|
+
await ensureSafeDirectory(lockPath, false);
|
|
53
|
+
await reclaimDeadLock(lockPath).catch(() => false);
|
|
54
|
+
await wait(50);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!ownsLock) throw codeError("neutral_workbuddy_launch_lock_timeout");
|
|
58
|
+
try {
|
|
59
|
+
await launchPinnedNeutralHostBootstrap(launch, {
|
|
60
|
+
storeRoot: launch.storeRoot,
|
|
61
|
+
...(options.spawnRunner ? { spawnRunner: options.spawnRunner } : {}),
|
|
62
|
+
...(options.bootstrapObservationMs !== undefined ? { bootstrapObservationMs: options.bootstrapObservationMs } : {}),
|
|
63
|
+
...(options.bootstrapFailureObservationMs !== undefined ? { bootstrapFailureObservationMs: options.bootstrapFailureObservationMs } : {}),
|
|
64
|
+
...(options.bootstrapFailureDeadlineMs !== undefined ? { bootstrapFailureDeadlineMs: options.bootstrapFailureDeadlineMs } : {}),
|
|
65
|
+
env: options.env || process.env,
|
|
66
|
+
});
|
|
67
|
+
return Object.freeze({ started: true, owner: "workbuddy_mcp_process" });
|
|
68
|
+
} finally {
|
|
69
|
+
// Only the lock created by this process is cleaned up. The neutral Host owns
|
|
70
|
+
// its own process lifetime and stops after the last attachment lease ends.
|
|
71
|
+
await fs.rm(path.join(lockPath, "owner.json"), { force: true }).catch(() => {});
|
|
72
|
+
await fs.rmdir(lockPath).catch(() => {});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function verifyPinnedBootstrap(binding, options = {}) {
|
|
77
|
+
const launch = normalizeLaunchBinding(binding, options);
|
|
78
|
+
const pinPath = path.join(launch.releaseRoot, "runtime", "neutral-host-release-pin.json");
|
|
79
|
+
const manifestPath = path.join(launch.releaseRoot, "runtime", "neutral-host-release-manifest.json");
|
|
80
|
+
const [pinBytes, manifestBytes] = await Promise.all([
|
|
81
|
+
readSafeFile(pinPath, 16 * 1024),
|
|
82
|
+
readSafeFile(manifestPath, 256 * 1024),
|
|
83
|
+
]);
|
|
84
|
+
const pin = parseJson(pinBytes);
|
|
85
|
+
const manifest = parseJson(manifestBytes);
|
|
86
|
+
if (!plain(pin) || !exactKeys(pin, ["contract", "manifestSha256"])
|
|
87
|
+
|| pin.contract !== "hellolynk-neutral-host-release-pin-v1"
|
|
88
|
+
|| pin.manifestSha256 !== launch.releaseManifestSha256
|
|
89
|
+
|| sha256(manifestBytes) !== launch.releaseManifestSha256
|
|
90
|
+
|| !plain(manifest) || manifest.contract !== "hellolynk-neutral-host-release-manifest-v1"
|
|
91
|
+
|| !exactKeys(manifest, ["contract", "entry", "files"])
|
|
92
|
+
|| !Array.isArray(manifest.files) || manifest.files.length < 3 || manifest.files.length > 256
|
|
93
|
+
|| manifest.entry !== "neutral-host-release-entry.mjs") {
|
|
94
|
+
throw codeError("neutral_workbuddy_runner_integrity_invalid");
|
|
95
|
+
}
|
|
96
|
+
const records = new Map();
|
|
97
|
+
const resolvedPaths = new Map();
|
|
98
|
+
for (const item of manifest.files) {
|
|
99
|
+
if (!plain(item) || !exactKeys(item, ["path", "sha256"])
|
|
100
|
+
|| !/^[a-f0-9]{64}$/.test(String(item.sha256 || ""))) fail();
|
|
101
|
+
const recordPath = String(item.path || "");
|
|
102
|
+
const file = resolveManifestFile(launch, recordPath);
|
|
103
|
+
const canonical = canonicalPath(file);
|
|
104
|
+
if (records.has(recordPath) || resolvedPaths.has(canonical)) fail();
|
|
105
|
+
const bytes = await readSafeFile(file, 2 * 1024 * 1024).catch(() => fail());
|
|
106
|
+
if (sha256(bytes) !== item.sha256) fail();
|
|
107
|
+
const record = Object.freeze({ recordPath, file, bytes });
|
|
108
|
+
records.set(recordPath, record);
|
|
109
|
+
resolvedPaths.set(canonical, record);
|
|
110
|
+
}
|
|
111
|
+
for (const required of REQUIRED_RUNTIME_RECORDS) {
|
|
112
|
+
if (!records.has(required)) fail();
|
|
113
|
+
}
|
|
114
|
+
if (!samePath(records.get("neutral-host-bootstrap.mjs").file, launch.bootstrapPath)
|
|
115
|
+
|| !records.has(manifest.entry)) fail();
|
|
116
|
+
verifyStaticImportClosure(records, resolvedPaths, launch.releaseRoot);
|
|
117
|
+
return Object.freeze({ ok: true, bootstrapPath: launch.bootstrapPath, verifiedFileCount: records.size });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveManifestFile(launch, recordPath) {
|
|
121
|
+
if (!SAFE_RELATIVE.test(recordPath) || recordPath.split("/").includes("..")) fail();
|
|
122
|
+
const releaseFile = recordPath.startsWith("release/");
|
|
123
|
+
const base = releaseFile ? launch.releaseRoot : path.join(launch.releaseRoot, "runtime");
|
|
124
|
+
const relative = releaseFile ? recordPath.slice("release/".length) : recordPath;
|
|
125
|
+
return insideRoot(base, relative);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function verifyStaticImportClosure(records, resolvedPaths, releaseRoot) {
|
|
129
|
+
const importPattern = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)["'](\.{1,2}\/[^"']+)["']/g;
|
|
130
|
+
for (const record of records.values()) {
|
|
131
|
+
if (!record.file.endsWith(".mjs")) continue;
|
|
132
|
+
const source = record.bytes.toString("utf8");
|
|
133
|
+
importPattern.lastIndex = 0;
|
|
134
|
+
for (let match = importPattern.exec(source); match; match = importPattern.exec(source)) {
|
|
135
|
+
const imported = path.resolve(path.dirname(record.file), ...match[1].split("/"));
|
|
136
|
+
if (!inside(releaseRoot, imported) || !resolvedPaths.has(canonicalPath(imported))) fail();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function insideRoot(root, relative) {
|
|
142
|
+
const result = path.resolve(root, ...relative.split("/"));
|
|
143
|
+
if (!inside(root, result)) fail();
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function inside(root, target) {
|
|
148
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
149
|
+
return Boolean(relative) && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeLaunchBinding(value, options) {
|
|
153
|
+
if (!plain(value)) throw codeError("neutral_workbuddy_launch_configuration_required");
|
|
154
|
+
const runtimeRoot = absolute(value.runtimeRoot);
|
|
155
|
+
const releaseRoot = absolute(value.releaseRoot);
|
|
156
|
+
const profileManifestPath = absolute(value.profileManifestPath);
|
|
157
|
+
const releaseManifestSha256 = String(value.releaseManifestSha256 || "");
|
|
158
|
+
if (value.runnerRelativePath !== RUNNER_RELATIVE || !/^[a-f0-9]{64}$/.test(releaseManifestSha256)) {
|
|
159
|
+
throw codeError("neutral_workbuddy_launch_configuration_required");
|
|
160
|
+
}
|
|
161
|
+
const derivedStoreRoot = path.dirname(runtimeRoot);
|
|
162
|
+
if (path.basename(runtimeRoot).toLowerCase() !== "runtime") {
|
|
163
|
+
throw codeError("neutral_workbuddy_launch_configuration_required");
|
|
164
|
+
}
|
|
165
|
+
const storeRoot = options.storeRoot ? absolute(options.storeRoot) : derivedStoreRoot;
|
|
166
|
+
if (!samePath(storeRoot, derivedStoreRoot)
|
|
167
|
+
|| !samePath(profileManifestPath, path.join(storeRoot, "profile-manifest-v1.json"))) {
|
|
168
|
+
throw codeError("neutral_workbuddy_launch_configuration_required");
|
|
169
|
+
}
|
|
170
|
+
return Object.freeze({
|
|
171
|
+
runtimeRoot, releaseRoot, profileManifestPath, releaseManifestSha256, storeRoot,
|
|
172
|
+
runnerRelativePath: RUNNER_RELATIVE,
|
|
173
|
+
bootstrapPath: path.join(releaseRoot, ...RUNNER_RELATIVE.split("/")),
|
|
174
|
+
region: safeId(value.region), principalId: safeId(value.principalId),
|
|
175
|
+
delegateId: safeId(value.delegateId), hostInstallationId: safeId(value.hostInstallationId),
|
|
176
|
+
profileId: safeId(value.profileId), adapterId: value.adapterId === "workbuddy" ? "workbuddy" : fail(),
|
|
177
|
+
version: Number(value.version), scopes: Object.freeze([...(value.scopes || [])].map(String)),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function readSafeFile(file, maximum) {
|
|
182
|
+
await assertNoReparseAncestors(file);
|
|
183
|
+
let handle;
|
|
184
|
+
try { handle = await fs.open(file, "r"); }
|
|
185
|
+
catch { throw codeError("neutral_workbuddy_runner_unavailable"); }
|
|
186
|
+
try {
|
|
187
|
+
const before = await handle.stat({ bigint: true });
|
|
188
|
+
if (!before.isFile() || before.nlink !== 1n || before.size > BigInt(maximum)) fail();
|
|
189
|
+
const bytes = await handle.readFile();
|
|
190
|
+
const after = await handle.stat({ bigint: true });
|
|
191
|
+
if (!sameIdentity(before, after)) fail();
|
|
192
|
+
return bytes;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (/^neutral_workbuddy_/.test(String(error?.code || ""))) throw error;
|
|
195
|
+
throw codeError("neutral_workbuddy_runner_integrity_invalid");
|
|
196
|
+
} finally { await handle.close().catch(() => {}); }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function assertNoReparseAncestors(target) {
|
|
200
|
+
const resolved = path.resolve(target);
|
|
201
|
+
const parsed = path.parse(resolved);
|
|
202
|
+
let cursor = parsed.root;
|
|
203
|
+
for (const segment of path.relative(parsed.root, resolved).split(path.sep).filter(Boolean)) {
|
|
204
|
+
cursor = path.join(cursor, segment);
|
|
205
|
+
let metadata;
|
|
206
|
+
try { metadata = await fs.lstat(cursor); }
|
|
207
|
+
catch { throw codeError("neutral_workbuddy_runner_unavailable"); }
|
|
208
|
+
if (metadata.isSymbolicLink()) throw codeError("neutral_workbuddy_runner_integrity_invalid");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function ensureSafeDirectory(target, create) {
|
|
213
|
+
if (create) await fs.mkdir(target, { recursive: true, mode: 0o700 });
|
|
214
|
+
const metadata = await fs.lstat(target).catch(() => null);
|
|
215
|
+
if (!metadata?.isDirectory() || metadata.isSymbolicLink()) {
|
|
216
|
+
throw codeError("neutral_workbuddy_launch_configuration_required");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function reclaimDeadLock(lockPath) {
|
|
221
|
+
const metadata = await fs.lstat(lockPath);
|
|
222
|
+
let owner = null;
|
|
223
|
+
try { owner = JSON.parse(await fs.readFile(path.join(lockPath, "owner.json"), "utf8")); }
|
|
224
|
+
catch { return false; }
|
|
225
|
+
const pid = Number(owner?.pid);
|
|
226
|
+
if (Date.now() - metadata.mtimeMs <= LOCK_STALE_MS || (Number.isSafeInteger(pid) && pid > 0 && processAlive(pid))) return false;
|
|
227
|
+
await fs.rm(path.join(lockPath, "owner.json"), { force: true });
|
|
228
|
+
await fs.rmdir(lockPath).catch(() => {});
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function tupleDigest(value) { return sha256(`${value.region}\0${value.principalId}\0${value.hostInstallationId}`); }
|
|
233
|
+
function sha256(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|
|
234
|
+
function parseJson(value) { try { return JSON.parse(value); } catch { fail(); } }
|
|
235
|
+
function sameIdentity(left, right) { return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.nlink === right.nlink; }
|
|
236
|
+
function processAlive(pid) { try { process.kill(pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } }
|
|
237
|
+
function safeId(value) { const result = String(value || ""); if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/.test(result)) fail(); return result; }
|
|
238
|
+
function absolute(value) { const result = String(value || ""); if (!path.isAbsolute(result) || path.resolve(result) !== result) fail(); return result; }
|
|
239
|
+
function samePath(left, right) { return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right; }
|
|
240
|
+
function canonicalPath(value) { const result = path.resolve(value); return process.platform === "win32" ? result.toLowerCase() : result; }
|
|
241
|
+
function exactKeys(value, expected) {
|
|
242
|
+
const actual = Object.keys(value).sort(); const wanted = [...expected].sort();
|
|
243
|
+
return actual.length === wanted.length && actual.every((item, index) => item === wanted[index]);
|
|
244
|
+
}
|
|
245
|
+
function bounded(value, fallback, minimum, maximum) { const result = Number(value ?? fallback); return Number.isSafeInteger(result) && result >= minimum && result <= maximum ? result : fallback; }
|
|
246
|
+
function plain(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
|
|
247
|
+
function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
248
|
+
function fail() { throw codeError("neutral_workbuddy_runner_integrity_invalid"); }
|
|
249
|
+
function codeError(code) { const error = new Error(code); error.code = code; return error; }
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { readClientAccount } from "../../../clients/core/hellolynk-client-account.mjs";
|
|
6
|
+
import { workbenchConnectionError, isWorkbenchConnectionRequired } from "../../../clients/core/hellolynk-workbench-connection.mjs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { startInstalledClientUpdate } from "../../../clients/core/hellolynk-client-update-entry.mjs";
|
|
9
|
+
import {
|
|
10
|
+
createWorkBuddyNeutralHostAdapter,
|
|
11
|
+
createWorkBuddyProtectedBindingProvider,
|
|
12
|
+
validateWorkBuddyProtectedBinding,
|
|
13
|
+
} from "./neutral-host-adapter.mjs";
|
|
14
|
+
import {
|
|
15
|
+
runWorkBuddyStdioMcpServer,
|
|
16
|
+
toWorkBuddyPublicErrorCode,
|
|
17
|
+
projectPendingMessages,
|
|
18
|
+
} from "./mcp-server.mjs";
|
|
19
|
+
import { launchWorkBuddyNeutralHost } from "./host-lifecycle.mjs";
|
|
20
|
+
import { startWorkBuddyNativeTaskWorker } from "./native-task-worker.mjs";
|
|
21
|
+
import { prepareWorkBuddyOriginHook, workBuddyOriginInbox, markWorkBuddyActiveScope } from "./origin-routes.mjs";
|
|
22
|
+
import { readWorkBuddyModelCaller } from "./model-call-origin.mjs";
|
|
23
|
+
import { nativeReplyRouteLocation } from "../../../clients/core/hellolynk-native-reply-routes.mjs";
|
|
24
|
+
import { defaultNeutralHostStoreRoot } from "../../../clients/core/hellolynk-neutral-host-installation-store.mjs";
|
|
25
|
+
import { startRecordedWorkBuddyNativeService, workBuddyMcpTrustRequests } from "./native-task-service-registration.mjs";
|
|
26
|
+
import { closeWorkBuddyWorkbench, launchWorkBuddyWorkbench } from "./workbench-candidate.mjs";
|
|
27
|
+
import { serveWorkBuddyWorkbench, serveWorkBuddyWorkbenchService } from "./workbench-process.mjs";
|
|
28
|
+
export { launchDetachedWorkBuddyWorkbench, handoffWorkBuddyWorkbench, configureWorkBuddyNativeTaskService } from "./workbench-process.mjs";
|
|
29
|
+
export { registerWorkBuddyNativeMcp } from "./native-task-mcp-registration.mjs";
|
|
30
|
+
|
|
31
|
+
function createProxyAdapter(options) {
|
|
32
|
+
if (options.adapter) return options.adapter;
|
|
33
|
+
const updating = options.createAdapter ? Promise.resolve(null) : startInstalledClientUpdate({
|
|
34
|
+
client: "workbuddy", packageRoot: fileURLToPath(new URL("../../../", import.meta.url)), storeRoot: options.storeRoot,
|
|
35
|
+
});
|
|
36
|
+
const connect = async () => {
|
|
37
|
+
await updating;
|
|
38
|
+
const connected = await (options.createAdapter || createWorkBuddyNeutralHostAdapter)({
|
|
39
|
+
capabilityProvider: options.capabilityProvider || createWorkBuddyProtectedBindingProvider(options),
|
|
40
|
+
discoverHost: options.discoverHost,
|
|
41
|
+
requestHost: options.requestHost,
|
|
42
|
+
hostLauncher: options.hostLauncher || ((binding) => launchWorkBuddyNeutralHost(binding, {
|
|
43
|
+
storeRoot: path.dirname(binding.runtimeRoot),
|
|
44
|
+
...(options.spawnRunner ? { spawnRunner: options.spawnRunner } : {}),
|
|
45
|
+
})),
|
|
46
|
+
adapterInstanceId: options.adapterInstanceId,
|
|
47
|
+
heartbeatMs: options.heartbeatMs,
|
|
48
|
+
});
|
|
49
|
+
try { await markWorkBuddyActiveScope(workBuddyOriginInbox(options.storeRoot || defaultNeutralHostStoreRoot()), await connected.getNativeTaskScope()); }
|
|
50
|
+
catch { /* Model calls still require matching current-account hook evidence. */ }
|
|
51
|
+
return connected;
|
|
52
|
+
};
|
|
53
|
+
const adapter = recoveringAdapter(connect, toWorkBuddyPublicErrorCode, [
|
|
54
|
+
"hostStatus", "getNativeTaskScope", "readBundle", "readUnreadCount", "markMessageRead", "deleteRead", "saveContact",
|
|
55
|
+
"deleteContact", "setAutonomousSendAuthorization", "listAuthorizedContacts", "listAuthorizedInboxMessages", "modelInbox", "listPendingMessages", "claimMessage", "claimBatch", "inboxBatch", "readAuthorizedInboxMessage", "listAuthorizedContactRequests", "readAuthorizedContactRequest", "sendAuthorizedMessage", "replyAuthorizedMessage",
|
|
56
|
+
"issueRetryGrant", "retryOutgoing", "requestHumanSend", "getHumanSendStatus",
|
|
57
|
+
"readWorkbenchSnapshot", "readWorkbenchChanges", "issueWorkbenchActionGrant", "executeWorkbenchCommand",
|
|
58
|
+
], () => readClientAccount("workbuddy", options.storeRoot).revision);
|
|
59
|
+
// Opening the page is owned by its separate service. Do not cold-start the
|
|
60
|
+
// Host inside WorkBuddy's short-lived conversation Job just for discovery.
|
|
61
|
+
return adapter;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runWorkBuddyWorkbenchProcess(options = {}) {
|
|
65
|
+
return serveWorkBuddyWorkbench({ ...options, adapter: createProxyAdapter(options) });
|
|
66
|
+
}
|
|
67
|
+
export async function runWorkBuddyWorkbenchService(options = {}) {
|
|
68
|
+
return serveWorkBuddyWorkbenchService({ ...options, adapter: createProxyAdapter(options) });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Installed outside WorkBuddy's conversation Job. No AI task has to remain
|
|
72
|
+
// running to receive the next message; this is an ordinary program listener.
|
|
73
|
+
export async function runWorkBuddyNativeTaskService(options = {}) {
|
|
74
|
+
if (!process.env.HELLOLYNK_WORKBUDDY_DEBUG_ENDPOINT && !options.nativeTaskOptions?.debugEndpoint) throw codeError("workbuddy_native_debug_enable_required");
|
|
75
|
+
const adapter = createProxyAdapter(options);
|
|
76
|
+
// Only the installed service (package bootstrap) registers the hook that
|
|
77
|
+
// records which conversation sent each message; tests opt in explicitly.
|
|
78
|
+
const origin = options.originHook ? await prepareWorkBuddyOriginHook({
|
|
79
|
+
storeRoot: options.storeRoot || defaultNeutralHostStoreRoot(), ...(options.originHook === true ? {} : options.originHook) }) : null;
|
|
80
|
+
// The installed service approves the trust request its own release's
|
|
81
|
+
// update left; a service without a pinned release approves nothing.
|
|
82
|
+
const mcpTrust = options.root && options.expectedManifestSha256 ? workBuddyMcpTrustRequests({ storeRoot: options.storeRoot,
|
|
83
|
+
root: options.root, expectedManifestSha256: options.expectedManifestSha256 }) : null;
|
|
84
|
+
const worker = startWorkBuddyNativeTaskWorker({ adapter, ...(origin ? { originInbox: origin.inbox } : {}), mcpTrust, ...options.nativeTaskOptions });
|
|
85
|
+
const keepAlive = setInterval(() => {}, 60_000);
|
|
86
|
+
const stop = () => { void worker.close(); };
|
|
87
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
88
|
+
if (options.signal?.aborted) stop();
|
|
89
|
+
try {
|
|
90
|
+
await worker.ready;
|
|
91
|
+
if (!options.signal?.aborted) await new Promise((resolve) => options.signal?.addEventListener("abort", resolve, { once: true }));
|
|
92
|
+
} finally { options.signal?.removeEventListener("abort", stop); clearInterval(keepAlive); await worker.close(); await adapter.close(); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Called only by a native background shell in a dedicated HelloLynk task.
|
|
96
|
+
// This process performs no model calls and emits no message content or identity.
|
|
97
|
+
export async function waitForWorkBuddyPending(options = {}) {
|
|
98
|
+
if (options.signal?.aborted) return { pending: false, cancelled: true };
|
|
99
|
+
const adapter = options.adapter || await (options.createAdapter || createWorkBuddyNeutralHostAdapter)({
|
|
100
|
+
capabilityProvider: options.capabilityProvider || createWorkBuddyProtectedBindingProvider(options),
|
|
101
|
+
discoverHost: options.discoverHost, requestHost: options.requestHost,
|
|
102
|
+
hostLauncher: options.hostLauncher || ((binding) => launchWorkBuddyNeutralHost(binding, { storeRoot: path.dirname(binding.runtimeRoot) })),
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
while (!options.signal?.aborted) {
|
|
106
|
+
const messages = projectPendingMessages(await adapter.listPendingMessages());
|
|
107
|
+
if (options.signal?.aborted) break;
|
|
108
|
+
if (messages.length) {
|
|
109
|
+
(options.output || process.stdout).write("HELLOLYNK_INBOX_PENDING\n");
|
|
110
|
+
return { pending: true };
|
|
111
|
+
}
|
|
112
|
+
try { await (options.wait || delay)(5000, undefined, { signal: options.signal }); }
|
|
113
|
+
catch (error) { if (options.signal?.aborted && error?.name === "AbortError") break; throw error; }
|
|
114
|
+
}
|
|
115
|
+
return { pending: false, cancelled: true };
|
|
116
|
+
} finally { await adapter.close(); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function runWorkBuddyMcpProxy(options = {}) {
|
|
120
|
+
const capabilityProvider = options.capabilityProvider || (!options.adapter && !options.createAdapter
|
|
121
|
+
? createWorkBuddyProtectedBindingProvider(options) : null);
|
|
122
|
+
const adapter = createProxyAdapter({ ...options, ...(capabilityProvider ? { capabilityProvider } : {}) });
|
|
123
|
+
const originInbox = workBuddyOriginInbox(options.storeRoot || defaultNeutralHostStoreRoot());
|
|
124
|
+
// Publish the account before tools become available and their PreToolUse hook
|
|
125
|
+
// runs. Reading the protected binding does not start a Host or receiver.
|
|
126
|
+
try {
|
|
127
|
+
const binding = capabilityProvider ? validateWorkBuddyProtectedBinding(await capabilityProvider()) : null;
|
|
128
|
+
const scope = binding ? { runtimeRoot: binding.runtimeRoot, adapterId: "workbuddy", region: binding.region,
|
|
129
|
+
principalId: binding.principalId, delegateId: binding.delegateId } : await options.adapter?.getNativeTaskScope?.();
|
|
130
|
+
if (scope) await markWorkBuddyActiveScope(originInbox, scope);
|
|
131
|
+
} catch { /* Unconnected discovery remains available; scoped calls fail closed. */ }
|
|
132
|
+
let recordedService = { configured: false, status: "not_installed" };
|
|
133
|
+
if (options.nativeTasks !== false) {
|
|
134
|
+
try { recordedService = await (options.startRecordedNativeService || startRecordedWorkBuddyNativeService)({ storeRoot: options.storeRoot }); }
|
|
135
|
+
catch (error) {
|
|
136
|
+
recordedService = { configured: true, status: "unavailable", errorCode: /^workbuddy_[a-z_]{1,100}$/.test(error?.code || "") ? error.code : "workbuddy_native_service_start_failed" };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// CDP must be explicitly enabled for this desktop profile. Existing MCP
|
|
140
|
+
// discovery alone must not cold-start a Host in a short-lived native Job.
|
|
141
|
+
const nativeEnabled = !recordedService.configured && options.nativeTasks !== false && Boolean(options.nativeTaskOptions?.debugEndpoint || process.env.HELLOLYNK_WORKBUDDY_DEBUG_ENDPOINT);
|
|
142
|
+
const nativeTasks = nativeEnabled ? startWorkBuddyNativeTaskWorker({ adapter, ...options.nativeTaskOptions }) : null;
|
|
143
|
+
try {
|
|
144
|
+
return await (options.runServer || runWorkBuddyStdioMcpServer)({
|
|
145
|
+
adapter,
|
|
146
|
+
getModelInboxCaller: options.getModelInboxCaller || (async (meta, name, args) => {
|
|
147
|
+
const scope = await adapter.getNativeTaskScope();
|
|
148
|
+
await markWorkBuddyActiveScope(originInbox, scope);
|
|
149
|
+
return readWorkBuddyModelCaller(originInbox,
|
|
150
|
+
meta, name, args, nativeReplyRouteLocation(scope).scopeHash);
|
|
151
|
+
}),
|
|
152
|
+
nativeTaskStatus: () => nativeTasks?.status || (recordedService.configured ? { status: recordedService.status, ...(recordedService.errorCode ? { errorCode: recordedService.errorCode } : {}) } : { status: "disabled", errorCode: "workbuddy_native_debug_enable_required" }),
|
|
153
|
+
// The resulting MCP response contains only a clean loopback URL. The
|
|
154
|
+
// one explicit top-level navigation receives a short local session.
|
|
155
|
+
openLocalWeb: options.openLocalWeb || (() => launchWorkBuddyWorkbench({ adapter })),
|
|
156
|
+
input: options.input || process.stdin,
|
|
157
|
+
output: options.output || process.stdout,
|
|
158
|
+
});
|
|
159
|
+
} finally {
|
|
160
|
+
try { await nativeTasks?.close(); await closeWorkBuddyWorkbench(); }
|
|
161
|
+
finally { await adapter.close(); }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function recoveringAdapter(connectAdapter, publicCode, methods, accountRevision = () => null) {
|
|
166
|
+
let current = null;
|
|
167
|
+
let pending = null;
|
|
168
|
+
let closed = false;
|
|
169
|
+
let revision = accountRevision();
|
|
170
|
+
let accountReset = null;
|
|
171
|
+
async function connect() {
|
|
172
|
+
if (closed) throw codeError("neutral_host_adapter_closed");
|
|
173
|
+
const nextRevision = accountRevision();
|
|
174
|
+
if (!accountReset && revision !== nextRevision) {
|
|
175
|
+
const old = current, previousFlight = pending;
|
|
176
|
+
current = null; pending = null; revision = nextRevision;
|
|
177
|
+
accountReset = Promise.resolve().then(async () => {
|
|
178
|
+
const previous = await previousFlight?.catch(() => null);
|
|
179
|
+
await (previous || old)?.close?.().catch(() => {});
|
|
180
|
+
}).finally(() => { accountReset = null; });
|
|
181
|
+
}
|
|
182
|
+
if (accountReset) await accountReset;
|
|
183
|
+
if (revision !== accountRevision()) throw codeError("workbench_account_changed");
|
|
184
|
+
if (current && current.state !== "dead" && current.state !== "closed") return current;
|
|
185
|
+
if (current) await current.close().catch(() => {});
|
|
186
|
+
current = null;
|
|
187
|
+
if (!pending) {
|
|
188
|
+
const generation = revision;
|
|
189
|
+
const flight = Promise.resolve().then(connectAdapter).then(async (value) => {
|
|
190
|
+
if (closed || generation !== accountRevision()) {
|
|
191
|
+
await value?.close?.().catch(() => {});
|
|
192
|
+
throw codeError("neutral_host_adapter_closed");
|
|
193
|
+
}
|
|
194
|
+
current = value;
|
|
195
|
+
return value;
|
|
196
|
+
}, (error) => { throw isWorkbenchConnectionRequired(error) ? workbenchConnectionError(error) : codeError(publicCode(error)); }).finally(() => { if (pending === flight) pending = null; });
|
|
197
|
+
pending = flight;
|
|
198
|
+
}
|
|
199
|
+
return pending;
|
|
200
|
+
}
|
|
201
|
+
const facade = {
|
|
202
|
+
async close() {
|
|
203
|
+
if (closed) return;
|
|
204
|
+
closed = true;
|
|
205
|
+
const value = current;
|
|
206
|
+
current = null;
|
|
207
|
+
await value?.close?.().catch(() => {});
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
const workbenchPair = methods.includes("issueWorkbenchActionGrant")
|
|
211
|
+
&& methods.includes("executeWorkbenchCommand");
|
|
212
|
+
for (const method of methods) {
|
|
213
|
+
if (workbenchPair && ["issueWorkbenchActionGrant", "executeWorkbenchCommand"].includes(method)) continue;
|
|
214
|
+
facade[method] = async (...args) => (await connect())[method](...args);
|
|
215
|
+
}
|
|
216
|
+
if (workbenchPair) {
|
|
217
|
+
const issueWorkbenchActionGrant = async function issueWorkbenchActionGrant(command) {
|
|
218
|
+
const target = await connect();
|
|
219
|
+
requireWorkbenchPair(target);
|
|
220
|
+
return target.issueWorkbenchActionGrant(command);
|
|
221
|
+
};
|
|
222
|
+
facade.executeWorkbenchCommand = async function executeWorkbenchCommand(command, context) {
|
|
223
|
+
const target = await connect();
|
|
224
|
+
requireWorkbenchPair(target);
|
|
225
|
+
return target.executeWorkbenchCommand(command, context);
|
|
226
|
+
};
|
|
227
|
+
Object.defineProperty(facade, "issueWorkbenchActionGrant", {
|
|
228
|
+
enumerable: true,
|
|
229
|
+
get() { return !current || hasWorkbenchPair(current) ? issueWorkbenchActionGrant : undefined; },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return Object.freeze(facade);
|
|
233
|
+
|
|
234
|
+
function requireWorkbenchPair(value) {
|
|
235
|
+
if (!hasWorkbenchPair(value)) throw codeError("neutral_host_workbench_action_grant_unavailable");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function hasWorkbenchPair(value) {
|
|
239
|
+
return typeof value?.issueWorkbenchActionGrant === "function"
|
|
240
|
+
&& value.issueWorkbenchActionGrant.length >= 1
|
|
241
|
+
&& typeof value.executeWorkbenchCommand === "function"
|
|
242
|
+
&& value.executeWorkbenchCommand.length >= 2;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function formatWorkBuddyProxyError(error) {
|
|
247
|
+
return `${JSON.stringify({ ok: false, error: toWorkBuddyPublicErrorCode(error) })}\n`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function codeError(code) { const error = new Error(code); error.code = code; return error; }
|
|
251
|
+
|
|
252
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
253
|
+
if (isMain) {
|
|
254
|
+
runWorkBuddyMcpProxy().catch((error) => {
|
|
255
|
+
process.stderr.write(formatWorkBuddyProxyError(error));
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
});
|
|
258
|
+
}
|