@hellolynk-ai/workbuddy-mcp 0.0.0-stage → 0.3.96
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 +88 -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 +160 -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 +160 -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,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { rememberNativeReplyRoute } from "../../../clients/core/hellolynk-native-reply-routes.mjs";
|
|
3
|
+
import { prepareModelInboxInvocation, isModelInboxTool } from "../../../clients/core/hellolynk-model-inbox.mjs";
|
|
4
|
+
import { nativeTool, optionalTarget, nativeAddressingError, replyHint, conversationGuidance, firstSendReceipt, paramText, resultText, sendArgumentKeys, sendRecipient, findContacts, writeFailure, failureText } from "../../../clients/core/hellolynk-native-addressing-tools.cjs";
|
|
5
|
+
import { withHelloLynkModelNotices } from "../../../clients/core/hellolynk-model-notices.cjs";
|
|
6
|
+
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { projectInboxReadState } from "../../../clients/core/hellolynk-inbox-read-state.mjs";
|
|
9
|
+
import readline from "node:readline";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import packageMetadata from "../package.json" with { type: "json" };
|
|
12
|
+
|
|
13
|
+
if (packageMetadata.name !== "@hellolynk-ai/workbuddy-mcp"
|
|
14
|
+
|| typeof packageMetadata.version !== "string" || !/^\d+\.\d+\.\d+$/.test(packageMetadata.version)) {
|
|
15
|
+
throw codeError("workbuddy_package_metadata_invalid");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const WORKBUDDY_MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
19
|
+
export const WORKBUDDY_MCP_MAX_FRAME_BYTES = 64 * 1024;
|
|
20
|
+
|
|
21
|
+
const READ = Object.freeze({ readOnlyHint: true, openWorldHint: false, destructiveHint: false, idempotentHint: true });
|
|
22
|
+
const SEND = Object.freeze({ readOnlyHint: false, openWorldHint: true, destructiveHint: false, idempotentHint: false });
|
|
23
|
+
|
|
24
|
+
// JSON Schema uses an unflagged ECMA-262 pattern; keep the portable anchors.
|
|
25
|
+
const WRITE_ID = Object.freeze({ type: "string", minLength: 3, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$" });
|
|
26
|
+
const WRITE_ID_PATTERN = new RegExp(WRITE_ID.pattern);
|
|
27
|
+
const CLIENT_MESSAGE_ID = Object.freeze({ ...WRITE_ID, examples: ["msg-a1b2c3-01"], description: paramText.clientMessageId });
|
|
28
|
+
const MESSAGE_TEXT = Object.freeze({ type: "string", minLength: 1, maxLength: 20000, description: paramText.text });
|
|
29
|
+
|
|
30
|
+
// Human writes belong to the same-origin shared Workbench -> Host command pair,
|
|
31
|
+
// never to an MCP App resource or an integration-owned action grant.
|
|
32
|
+
// nativeTool adds the titles and descriptions every client shares.
|
|
33
|
+
export const WORKBUDDY_TOOLS = Object.freeze([
|
|
34
|
+
tool("hellolynk_reply_authorized_message", { type: "object", additionalProperties: false, required: ["localMessageId", "conversationKey", "clientMessageId", "text"], properties: { localMessageId: { ...WRITE_ID, description: paramText.localMessageId }, conversationKey: { ...WRITE_ID, description: paramText.conversationKey }, clientMessageId: CLIENT_MESSAGE_ID, text: MESSAGE_TEXT } }, SEND),
|
|
35
|
+
tool("hellolynk_list_pending_messages", emptySchema(), { readOnlyHint: false, openWorldHint: false, destructiveHint: false, idempotentHint: true }),
|
|
36
|
+
tool("hellolynk_claim_message", pendingSchema(), { readOnlyHint: false, openWorldHint: false, destructiveHint: false, idempotentHint: false }),
|
|
37
|
+
tool("hellolynk_host_status", emptySchema(), READ),
|
|
38
|
+
tool("hellolynk_open_inbox", emptySchema(), READ),
|
|
39
|
+
tool("hellolynk_get_unread_count", emptySchema(), READ),
|
|
40
|
+
tool("hellolynk_list_authorized_contacts", emptySchema(), READ),
|
|
41
|
+
tool("hellolynk_send_authorized_message", authorizedSendSchema(), SEND),
|
|
42
|
+
tool("hellolynk_list_authorized_inbox_messages", historySchema(), READ),
|
|
43
|
+
tool("hellolynk_read_authorized_inbox_message", strictConversationSchema("localMessageId"), READ),
|
|
44
|
+
tool("hellolynk_list_authorized_contact_requests", emptySchema(), READ),
|
|
45
|
+
tool("hellolynk_read_authorized_contact_request", strictConversationSchema("localRecordId"), READ),
|
|
46
|
+
].map(nativeTool));
|
|
47
|
+
|
|
48
|
+
export const WORKBUDDY_MCP_SURFACE = Object.freeze({
|
|
49
|
+
name: "hellolynk-workbuddy", title: "HelloLynk 通讯", version: packageMetadata.version, tools: WORKBUDDY_TOOLS,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export async function runWorkBuddyStdioMcpServer(options = {}) {
|
|
53
|
+
const input = options.input || process.stdin;
|
|
54
|
+
const output = options.output || process.stdout;
|
|
55
|
+
const rl = readline.createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
56
|
+
for await (const line of rl) {
|
|
57
|
+
if (!line.trim()) continue;
|
|
58
|
+
if (Buffer.byteLength(line, "utf8") > WORKBUDDY_MCP_MAX_FRAME_BYTES) {
|
|
59
|
+
write(output, rpcError(null, -32600, "frame_too_large"));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
await handleWorkBuddyMcpLine(line, output, { adapter: options.adapter, openLocalWeb: options.openLocalWeb, nativeTaskStatus: options.nativeTaskStatus, getModelInboxCaller: options.getModelInboxCaller });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function handleWorkBuddyMcpLine(line, output, runtime = {}) {
|
|
67
|
+
let message;
|
|
68
|
+
try { message = JSON.parse(line); }
|
|
69
|
+
catch { write(output, rpcError(null, -32700, "parse_error")); return; }
|
|
70
|
+
const result = await handleWorkBuddyJsonRpcMessage(message, runtime);
|
|
71
|
+
if (result) write(output, result);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function handleWorkBuddyJsonRpcMessage(message, runtime = {}) {
|
|
75
|
+
if (!plain(message) || message.jsonrpc !== "2.0" || typeof message.method !== "string") return rpcError(message?.id ?? null, -32600, "invalid_request");
|
|
76
|
+
if (message.method.startsWith("notifications/")) return null;
|
|
77
|
+
try {
|
|
78
|
+
const result = await dispatchWorkBuddyMcp(message.method, message.params || {}, runtime);
|
|
79
|
+
return message.id === undefined ? null : { jsonrpc: "2.0", id: message.id, result };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return message.id === undefined ? null : rpcError(message.id, -32603, safeErrorCode(error));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function dispatchWorkBuddyMcp(method, params, runtime = {}) {
|
|
86
|
+
const run = () => dispatchWithoutModelNotices(method, params, runtime);
|
|
87
|
+
return method === "tools/call" ? withHelloLynkModelNotices(run) : run();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function dispatchWithoutModelNotices(method, params, runtime = {}) {
|
|
91
|
+
if (method === "initialize") return {
|
|
92
|
+
protocolVersion: WORKBUDDY_MCP_PROTOCOL_VERSION,
|
|
93
|
+
capabilities: { tools: { listChanged: false } },
|
|
94
|
+
serverInfo: { name: WORKBUDDY_MCP_SURFACE.name, title: WORKBUDDY_MCP_SURFACE.title, version: WORKBUDDY_MCP_SURFACE.version },
|
|
95
|
+
instructions: conversationGuidance + "“打开 HelloLynk”调用 hellolynk_open_inbox 一次,再用内置浏览器打开返回地址。",
|
|
96
|
+
};
|
|
97
|
+
if (method === "ping") return {};
|
|
98
|
+
if (method === "tools/list") return { tools: WORKBUDDY_TOOLS };
|
|
99
|
+
if (method === "tools/call") return callTool(params, requireAdapter(runtime.adapter), runtime.openLocalWeb, runtime.nativeTaskStatus, runtime.getModelInboxCaller);
|
|
100
|
+
if (method === "resources/list") return { resources: [] };
|
|
101
|
+
if (method === "resources/templates/list") return { resourceTemplates: [] };
|
|
102
|
+
if (method === "resources/read") throw codeError("resource_not_found");
|
|
103
|
+
throw codeError("method_not_found");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function callTool(params, adapter, openLocalWeb, nativeTaskStatus, getModelInboxCaller) {
|
|
107
|
+
if (!isModelInboxTool(params?.name)) return callToolWithoutInboxScope(params, adapter, openLocalWeb, nativeTaskStatus);
|
|
108
|
+
try {
|
|
109
|
+
const caller = await getModelInboxCaller?.(params._meta, params.name, params.arguments || {});
|
|
110
|
+
const invocation = await prepareModelInboxInvocation(adapter, { name: params.name, args: params.arguments || {}, caller, readEnvelope: true });
|
|
111
|
+
return invocation.finish(await callToolWithoutInboxScope({ ...params, arguments: invocation.args },
|
|
112
|
+
invocation.adapter, openLocalWeb, nativeTaskStatus, caller));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (params.name === 'hellolynk_reply_authorized_message') return authorizedWriteFailure(error,
|
|
115
|
+
{ phase: error?.code === 'model_inbox_scope_invalid' ? 'input_validation' : 'preparation' });
|
|
116
|
+
return failure(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function callToolWithoutInboxScope(params, adapter, openLocalWeb, nativeTaskStatus, caller) {
|
|
121
|
+
if (!plain(params) || typeof params.name !== "string" || (Object.hasOwn(params, "arguments") && !plain(params.arguments))) throw codeError("invalid_params");
|
|
122
|
+
const args = params.arguments || {};
|
|
123
|
+
const writeAttempt = ["hellolynk_send_authorized_message", "hellolynk_reply_authorized_message"].includes(params.name) ? { phase: "input_validation" } : null;
|
|
124
|
+
try {
|
|
125
|
+
const pending = await pendingTool(params.name, args, adapter);
|
|
126
|
+
if (pending) return pending;
|
|
127
|
+
if (params.name === "hellolynk_host_status") {
|
|
128
|
+
exactArgs(args, []);
|
|
129
|
+
const status = await adapter.hostStatus();
|
|
130
|
+
const native = nativeTaskStatus?.();
|
|
131
|
+
const nativeTasks = native ? { status: /^[a-z_]{1,60}$/.test(native.status) ? native.status : "unavailable", ...(typeof native.errorCode === "string" && /^[a-z_]{1,120}$/.test(native.errorCode) ? { errorCode: native.errorCode } : {}) } : undefined;
|
|
132
|
+
return success("本机 HelloLynk 已连接。", { ok: true, configured: true, status: "attached", startedByWorkBuddy: status.startedHost === true, ...(nativeTasks ? { nativeTasks } : {}) });
|
|
133
|
+
}
|
|
134
|
+
if (params.name === "hellolynk_open_inbox") {
|
|
135
|
+
exactArgs(args, []);
|
|
136
|
+
if (typeof openLocalWeb !== "function") throw codeError("workbuddy_local_web_launch_unavailable");
|
|
137
|
+
const launched = await openLocalWeb();
|
|
138
|
+
if (!/^http:\/\/127\.0\.0\.1:\d{1,5}\/inbox$/.test(String(launched?.cleanUrl || ""))) throw codeError("workbuddy_local_web_launch_invalid");
|
|
139
|
+
return success(resultText.ready(launched.cleanUrl), {
|
|
140
|
+
ok: true, configured: true, status: "ready_for_browser_navigation", url: launched.cleanUrl,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (params.name === "hellolynk_get_unread_count") {
|
|
144
|
+
exactArgs(args, []);
|
|
145
|
+
const unread = (await adapter.readUnreadCount())?.unreadCount;
|
|
146
|
+
if (!Number.isSafeInteger(unread) || unread < 0) throw codeError("widget_bundle_invalid");
|
|
147
|
+
return success(resultText.unread(unread), { ok: true, configured: true, status: "running", counts: { unread } });
|
|
148
|
+
}
|
|
149
|
+
if (params.name === "hellolynk_list_authorized_contacts") {
|
|
150
|
+
const result = await findContacts(args, (query) => adapter.listAuthorizedContacts(query));
|
|
151
|
+
const contacts = Array.isArray(result?.contacts) ? result.contacts.map((item) => ({
|
|
152
|
+
contactId: safeId(item?.contactId, 80), displayName: safeString(item?.displayName, 160),
|
|
153
|
+
recipientDelegateId: safeId(item?.recipientDelegateId, 198),
|
|
154
|
+
instanceDisplayName: safeString(item?.instanceDisplayName, 160),
|
|
155
|
+
})) : [];
|
|
156
|
+
return success(resultText.contacts(contacts.length), {
|
|
157
|
+
ok: true, authorizedContacts: contacts, ...(result.nativeConversations ? { nativeConversations: result.nativeConversations } : {}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (params.name === "hellolynk_list_authorized_inbox_messages") {
|
|
161
|
+
const older = Object.hasOwn(args, "before");
|
|
162
|
+
exactArgs(args, older ? ["before"] : []);
|
|
163
|
+
const result = await adapter.listAuthorizedInboxMessages(older ? { before: safeId(args.before, 198) } : {});
|
|
164
|
+
const messages = Array.isArray(result?.messages) ? result.messages.map((item) => ({
|
|
165
|
+
localMessageId: safeId(item?.localMessageId, 198), contactId: item?.contactId === null ? null : safeId(item?.contactId, 80),
|
|
166
|
+
conversationKey: safeId(item?.conversationKey, 240),
|
|
167
|
+
localDelegateId: safeId(item?.localDelegateId, 198), peerDelegateId: safeId(item?.peerDelegateId, 198),
|
|
168
|
+
displayName: safeString(item?.displayName, 160), ...(item.peerInstanceDisplayName == null ? {} : { peerInstanceDisplayName: safeString(item.peerInstanceDisplayName, 160) }), unread: item?.unread === true,
|
|
169
|
+
createdAt: safeString(item?.createdAt || "", 80, true), ...replyHint(item),
|
|
170
|
+
})) : [];
|
|
171
|
+
return success(resultText.inbox(messages.length, older), { ok: true, authorizedMessages: messages });
|
|
172
|
+
}
|
|
173
|
+
if (params.name === "hellolynk_reply_authorized_message") {
|
|
174
|
+
exactArgs(args, ["localMessageId", "conversationKey", "clientMessageId", "text"]);
|
|
175
|
+
if (Object.values(args).some(value => typeof value !== "string")) throw codeError("invalid_params");
|
|
176
|
+
const input = { localMessageId: authorizedWriteId(args.localMessageId), conversationKey: authorizedWriteId(args.conversationKey), clientMessageId: authorizedWriteId(args.clientMessageId) };
|
|
177
|
+
writeAttempt.clientMessageId = input.clientMessageId;
|
|
178
|
+
input.text = safeBody(args.text, 20 * 1024, true);
|
|
179
|
+
writeAttempt.phase = "preparation";
|
|
180
|
+
await adapter.assertModelInboxReply(input);
|
|
181
|
+
// Ownership was checked before this
|
|
182
|
+
// best-effort origin registration; never register a denied reply.
|
|
183
|
+
if (caller?.threadId) void (async () => {
|
|
184
|
+
await rememberNativeReplyRoute(await adapter.getNativeTaskScope(), {
|
|
185
|
+
threadId: caller.threadId, clientMessageId: input.clientMessageId, conversationKey: input.conversationKey,
|
|
186
|
+
});
|
|
187
|
+
})().catch(() => {});
|
|
188
|
+
writeAttempt.phase = "adapter_call";
|
|
189
|
+
const result = await adapter.replyAuthorizedMessage(input);
|
|
190
|
+
writeAttempt.phase = "response_validation";
|
|
191
|
+
if (result?.accepted !== true || result.ok === false || result.authorization !== "contact_delegate_allowlist" || result.clientMessageId !== input.clientMessageId
|
|
192
|
+
|| !["accepted", "delivered_to_live_connection", "acknowledged_by_recipient_local_store"].includes(result.status)) throw codeError("host_app_send_result_unknown");
|
|
193
|
+
return success(resultText.sent(result.status, input.clientMessageId), { accepted: true,
|
|
194
|
+
localMessageId: input.localMessageId, conversationKey: input.conversationKey,
|
|
195
|
+
clientMessageId: input.clientMessageId, contactId: safeId(result.contactId, 198), status: result.status });
|
|
196
|
+
}
|
|
197
|
+
if (params.name === "hellolynk_send_authorized_message") {
|
|
198
|
+
exactArgs(args, sendArgumentKeys(args));
|
|
199
|
+
if (Object.values(args).some((value) => typeof value !== "string")) throw codeError("invalid_params");
|
|
200
|
+
if (!Object.hasOwn(args, "contact")) { authorizedWriteId(args.contactId); authorizedWriteId(args.recipientDelegateId); }
|
|
201
|
+
optionalTarget(args);
|
|
202
|
+
const clientMessageId = authorizedWriteId(args.clientMessageId);
|
|
203
|
+
writeAttempt.clientMessageId = clientMessageId;
|
|
204
|
+
const text = safeBody(args.text, 20 * 1024, true);
|
|
205
|
+
writeAttempt.phase = "preparation";
|
|
206
|
+
const { contactId, recipientDelegateId } = await sendRecipient(args, (query) => adapter.listAuthorizedContacts(query));
|
|
207
|
+
writeAttempt.phase = "adapter_call";
|
|
208
|
+
const result = await adapter.sendAuthorizedMessage({ contactId, recipientDelegateId, clientMessageId, text, ...optionalTarget(args) });
|
|
209
|
+
writeAttempt.phase = "response_validation";
|
|
210
|
+
if (result?.accepted !== true || result.ok === false || result.authorization !== "contact_delegate_allowlist"
|
|
211
|
+
|| result.contactId !== contactId || result.clientMessageId !== clientMessageId
|
|
212
|
+
|| !["accepted", "delivered_to_live_connection", "acknowledged_by_recipient_local_store"].includes(result.status)) {
|
|
213
|
+
throw codeError("host_app_send_result_unknown");
|
|
214
|
+
}
|
|
215
|
+
const status = safeId(result.status, 80);
|
|
216
|
+
const receipt = firstSendReceipt(args, status);
|
|
217
|
+
return success(receipt.text, {
|
|
218
|
+
accepted: true, clientMessageId, contactId, recipientDelegateId, status, destination: receipt.destination,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (params.name === "hellolynk_read_authorized_inbox_message") {
|
|
222
|
+
exactArgs(args, ["localMessageId", "conversationKey"]);
|
|
223
|
+
const result = await adapter.readAuthorizedInboxMessage(safeId(args.localMessageId, 198), safeId(args.conversationKey, 240));
|
|
224
|
+
return success("已读取来信。", { ok: true, message: projectExternalMessage(result?.message ?? result) });
|
|
225
|
+
}
|
|
226
|
+
if (params.name === "hellolynk_list_authorized_contact_requests") {
|
|
227
|
+
exactArgs(args, []); const result = await adapter.listAuthorizedContactRequests();
|
|
228
|
+
const requests = (result?.requests || []).map(projectRequestMetadata);
|
|
229
|
+
return success(`联系申请 ${requests.length} 条。`, { ok: true, authorizedContactRequests: requests });
|
|
230
|
+
}
|
|
231
|
+
if (params.name === "hellolynk_read_authorized_contact_request") {
|
|
232
|
+
exactArgs(args, ["localRecordId", "conversationKey"]);
|
|
233
|
+
const result = await adapter.readAuthorizedContactRequest(safeId(args.localRecordId, 198), safeId(args.conversationKey, 240));
|
|
234
|
+
return success("已读取联系申请。", { ok: true, request: projectExternalRequest(result?.request ?? result) });
|
|
235
|
+
}
|
|
236
|
+
throw codeError("unknown_tool");
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return writeAttempt ? authorizedWriteFailure(error, writeAttempt) : failure(error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function authorizedWriteId(value) { if (typeof value !== "string" || !WRITE_ID_PATTERN.test(value)) throw codeError("identifier_invalid"); return value; }
|
|
243
|
+
function authorizedWriteFailure(error, attempt) {
|
|
244
|
+
const nativeError = nativeAddressingError(error, attempt);
|
|
245
|
+
if (nativeError) return nativeError;
|
|
246
|
+
const errorCode = attempt.phase === "input_validation" && error?.code === "external_content_text_invalid" ? error.code : safeErrorCode(error);
|
|
247
|
+
// Phase and retry policy come only from this invocation, never from an adapter error.
|
|
248
|
+
const { text, ...outcome } = writeFailure(errorCode, attempt.phase);
|
|
249
|
+
return { content: [{ type: "text", text }], structuredContent: { ok: false, configured: true, errorCode, ...outcome,
|
|
250
|
+
phase: attempt.phase, ...(attempt.clientMessageId ? { clientMessageId: attempt.clientMessageId } : {}) }, isError: true };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function success(text, structuredContent) { return { content: [{ type: "text", text }, { type: "text", text: JSON.stringify(structuredContent) }], structuredContent, isError: false }; }
|
|
254
|
+
function failure(error) {
|
|
255
|
+
const nativeError = nativeAddressingError(error);
|
|
256
|
+
if (nativeError) return nativeError;
|
|
257
|
+
const errorCode = safeErrorCode(error);
|
|
258
|
+
const deleted = errorCode === "host_app_reply_target_deleted";
|
|
259
|
+
return { content: [{ type: "text", text: failureText(errorCode) }], structuredContent: { ok: false, configured: true,
|
|
260
|
+
status: deleted ? "not_delivered" : "blocked", errorCode, ...(deleted ? { automaticRetryAllowed: false } : {}) }, isError: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function toWorkBuddyPublicErrorCode(error) { return safeErrorCode(error); }
|
|
264
|
+
function requireAdapter(value) { if (!value) throw codeError("neutral_host_adapter_unavailable"); return value; }
|
|
265
|
+
function tool(name, inputSchema, annotations) { return Object.freeze({ name, inputSchema, annotations }); }
|
|
266
|
+
function emptySchema() { return { type: "object", additionalProperties: false, properties: {} }; }
|
|
267
|
+
function historySchema() { return { type: "object", additionalProperties: false, properties: { before: { type: "string", minLength: 3, maxLength: 198, description: paramText.before } } }; }
|
|
268
|
+
function authorizedSendSchema() { return { type: "object", additionalProperties: false, required: ["clientMessageId", "text"], properties: { contactId: WRITE_ID, recipientDelegateId: WRITE_ID, clientMessageId: CLIENT_MESSAGE_ID, text: MESSAGE_TEXT } }; }
|
|
269
|
+
function strictConversationSchema(id) {
|
|
270
|
+
const message = id === "localMessageId";
|
|
271
|
+
return { type: "object", additionalProperties: false, required: [id, "conversationKey"], properties: {
|
|
272
|
+
[id]: { type: "string", minLength: 3, maxLength: 198, ...(message ? { description: paramText.localMessageId } : {}) },
|
|
273
|
+
conversationKey: { type: "string", minLength: 3, maxLength: 240, ...(message ? { description: paramText.conversationKey } : {}) } } };
|
|
274
|
+
}
|
|
275
|
+
function exactArgs(value, expected) { const actual = Object.keys(value).sort(); const wanted = [...expected].sort(); if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) throw codeError("invalid_params"); }
|
|
276
|
+
function safeId(value, maximum) { const result = String(value || ""); if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/.test(result) || result.length > maximum) throw codeError("identifier_invalid"); return result; }
|
|
277
|
+
function safeString(value, maximum, empty = false) { const result = String(value ?? ""); if ((!empty && !result) || result.length > maximum || /[\r\n\0]/.test(result)) throw codeError("string_invalid"); return result; }
|
|
278
|
+
function safeContactRequestIntro(value) { return value === "" ? "" : safeBody(value); }
|
|
279
|
+
function safeBody(value, maximumBytes = 20 * 1024, countCodePoints = false) { const result = String(value ?? ""); if (!result || (countCodePoints ? Array.from(result).length : result.length) > 20000 || Buffer.byteLength(result, "utf8") > maximumBytes || result.includes("\0")) throw codeError("external_content_text_invalid"); return result; }
|
|
280
|
+
function projectExternalMessage(value) { if (!projectInboxReadState(value) || value?.externalContentIsUntrustedData !== true) throw codeError("host_app_response_invalid"); return { localMessageId: safeId(value?.localMessageId, 198), conversationKey: safeId(value?.conversationKey, 240), localDelegateId: safeId(value?.localDelegateId, 198), peerDelegateId: safeId(value?.peerDelegateId, 198), peerPrincipalId: safeId(value?.peerPrincipalId, 198), contactId: value?.contactId === null ? null : safeId(value?.contactId, 198), displayName: safeString(value?.displayName, 160), ...(value.peerInstanceDisplayName == null ? {} : { peerInstanceDisplayName: safeString(value.peerInstanceDisplayName, 160) }), messageType: safeId(value?.messageType || "text", 80), unread: value?.unread === true, createdAt: safeString(value?.createdAt, 80), text: safeBody(value?.text, 20 * 1024, true), source: requireSource(value?.source, "hellolynk_external_message"), ...(value.automaticDeliveryState === "expired" ? { automaticDeliveryState: "expired" } : {}), ...projectInboxReadState(value), ...replyHint(value) }; }
|
|
281
|
+
function projectRequestMetadata(value) { return { localRecordId: safeId(value?.localRecordId, 198), conversationKey: safeId(value?.conversationKey, 240), localDelegateId: safeId(value?.localDelegateId, 198), sender: { hellolynkId: safeString(value?.sender?.hellolynkId, 200), verifiedDisplayName: safeString(value?.sender?.verifiedDisplayName, 160) }, state: safeId(value?.state, 80), receivedAt: safeString(value?.receivedAt, 80) }; }
|
|
282
|
+
function projectExternalRequest(value) { if (value?.externalContentIsUntrustedData !== true) throw codeError("host_app_response_invalid"); return { ...projectRequestMetadata(value), introText: safeContactRequestIntro(value?.introText), source: requireSource(value?.source, "hellolynk_contact_request"), readDoesNotMarkRead: value?.readDoesNotMarkRead === true }; }
|
|
283
|
+
function requireSource(value, expected) { if (value !== expected) throw codeError("external_content_source_invalid"); return expected; }
|
|
284
|
+
function safeErrorCode(error) {
|
|
285
|
+
const value = String(error?.code || error?.message || "hellolynk_operation_failed");
|
|
286
|
+
if (["host_native_inbox_upgrade_required", "hellolynk_node_22_16_required", "powershell7_missing"].includes(value)) return value;
|
|
287
|
+
if (["native_addressing_arguments_invalid", "native_directory_cursor_invalid", "native_directory_query_invalid", "native_conversation_id_invalid"].includes(value)) return value;
|
|
288
|
+
return /^(neutral_(?:host|workbuddy)_[a-z0-9_]{1,100}|host_app_[a-z0-9_]{1,100}|workbuddy_local_web_launch_[a-z0-9_]{1,100}|workbuddy_page_(?:task|service)_[a-z0-9_]{1,100}|resource_not_found|method_not_found|unknown_tool|invalid_params|identifier_invalid|string_invalid|widget_bundle_invalid)$/.test(value) ? value : "hellolynk_operation_failed";
|
|
289
|
+
}
|
|
290
|
+
function rpcError(id, code, message) { return { jsonrpc: "2.0", id, error: { code, message } }; }
|
|
291
|
+
function write(output, value) { output.write(`${JSON.stringify(value)}\n`); }
|
|
292
|
+
function plain(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
|
|
293
|
+
function codeError(code) { const error = new Error(code); error.code = code; return error; }
|
|
294
|
+
|
|
295
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
296
|
+
if (isMain) {
|
|
297
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: "workbuddy_mcp_proxy_required" })}\n`);
|
|
298
|
+
process.exitCode = 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function pendingInput(args, finish = false) {
|
|
302
|
+
const keys = finish ? ["localMessageId", "conversationKey", "claimId", "outcome"] : ["localMessageId", "conversationKey"];
|
|
303
|
+
if (!args || typeof args !== "object" || Array.isArray(args) || Object.keys(args).length !== keys.length
|
|
304
|
+
|| keys.some((key) => !Object.hasOwn(args, key))
|
|
305
|
+
|| [args.localMessageId, args.conversationKey].some((value) => typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/.test(value))
|
|
306
|
+
|| (finish && (typeof args.claimId !== "string" || !/^inclaim_[a-f0-9]{48}$/.test(args.claimId) || !["completed", "unknown"].includes(args.outcome)))) throw codeError("invalid_params");
|
|
307
|
+
return args;
|
|
308
|
+
}
|
|
309
|
+
function pendingSchema() {
|
|
310
|
+
return { type: "object", additionalProperties: false,
|
|
311
|
+
properties: { batchId: { type: "string", pattern: "^inbatch_[a-f0-9]{48}$", description: paramText.batchId },
|
|
312
|
+
localMessageId: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$", description: paramText.localMessageId },
|
|
313
|
+
conversationKey: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$", description: paramText.conversationKey } },
|
|
314
|
+
oneOf: [{ required: ["batchId"], not: { anyOf: [{ required: ["localMessageId"] }, { required: ["conversationKey"] }] } },
|
|
315
|
+
{ required: ["localMessageId", "conversationKey"], not: { required: ["batchId"] } }] };
|
|
316
|
+
}
|
|
317
|
+
export function projectPendingMessages(result) {
|
|
318
|
+
if (!Array.isArray(result?.messages) || result.messages.length > 1000) throw codeError("host_app_response_invalid");
|
|
319
|
+
return result.messages.map((value) => {
|
|
320
|
+
pendingInput({ localMessageId: value?.localMessageId, conversationKey: value?.conversationKey });
|
|
321
|
+
if (value?.wakeEligible !== true || typeof value.unread !== "boolean"
|
|
322
|
+
|| [...(value.contactId === null ? [] : [value.contactId]), value.localDelegateId, value.peerDelegateId].some((id) => typeof id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/.test(id))
|
|
323
|
+
|| typeof value.displayName !== "string" || !value.displayName || value.displayName.length > 160 || /[\r\n\0]/.test(value.displayName)
|
|
324
|
+
|| typeof value.createdAt !== "string" || value.createdAt.length > 80 || !Number.isFinite(Date.parse(value.createdAt))) throw codeError("host_app_response_invalid");
|
|
325
|
+
return { ...Object.fromEntries(["localMessageId", "conversationKey", "localDelegateId", "peerDelegateId", "contactId", "displayName", "unread", "createdAt", "wakeEligible"].map((key) => [key, value[key]])),
|
|
326
|
+
...(value.peerInstanceDisplayName == null ? {} : { peerInstanceDisplayName: safeString(value.peerInstanceDisplayName, 160) }) };
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async function pendingTool(name, args, adapter) {
|
|
330
|
+
if (name === "hellolynk_list_pending_messages") {
|
|
331
|
+
if (Object.keys(args).length) throw codeError("invalid_params");
|
|
332
|
+
const messages = projectPendingMessages(await adapter.listPendingMessages());
|
|
333
|
+
return success(resultText.pending(messages.length), { messages });
|
|
334
|
+
}
|
|
335
|
+
if (name === "hellolynk_claim_message") {
|
|
336
|
+
if (Object.hasOwn(args, "batchId")) {
|
|
337
|
+
if (Object.keys(args).length !== 1 || !/^inbatch_[a-f0-9]{48}$/.test(args.batchId || "")) throw codeError("invalid_params");
|
|
338
|
+
const result = await adapter.claimBatch(args.batchId);
|
|
339
|
+
if (!Array.isArray(result?.messages) || result.claimed !== (result.messages.length > 0)
|
|
340
|
+
|| !Number.isSafeInteger(result.expiredCount) || result.expiredCount < 0) throw codeError("host_app_response_invalid");
|
|
341
|
+
if (!result.claimed) return success(safeString(result.notice ?? "这批来信已读取,无需重复读取。", 200), { claimed: false, messages: [], expiredCount: result.expiredCount });
|
|
342
|
+
const messages = result.messages.map(projectExternalMessage);
|
|
343
|
+
if (messages.reduce((count, m) => count + Array.from(m.text).length, 0) > 20000) throw codeError("host_app_response_invalid");
|
|
344
|
+
return success(resultText.claimed(messages.length, result.expiredCount), { claimed: true, messages, expiredCount: result.expiredCount });
|
|
345
|
+
}
|
|
346
|
+
pendingInput(args);
|
|
347
|
+
const result = await adapter.claimMessage(args.localMessageId, args.conversationKey);
|
|
348
|
+
if (result?.claimed === false && result.claimId === null && result.message === null) return success("此来信已被领取或处理。", { claimed: false, claimId: null, message: null });
|
|
349
|
+
if (result?.claimed !== true || typeof result.claimId !== "string" || !/^inclaim_[a-f0-9]{48}$/.test(result.claimId)) throw codeError("host_app_response_invalid");
|
|
350
|
+
const message = projectExternalMessage(result.message);
|
|
351
|
+
if (message.localMessageId !== args.localMessageId || message.conversationKey !== args.conversationKey
|
|
352
|
+
|| !projectInboxReadState(message) || result.message?.wakeEligible !== true) throw codeError("host_app_response_invalid");
|
|
353
|
+
return success(resultText.claimed(1), { claimed: true, claimId: result.claimId, message: { ...message, wakeEligible: true } });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,240}$/;
|
|
6
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
7
|
+
const TTL = 10 * 60_000;
|
|
8
|
+
export const WORKBUDDY_MODEL_INBOX_NAMES = new Set([
|
|
9
|
+
"hellolynk_get_unread_count", "hellolynk_list_authorized_inbox_messages",
|
|
10
|
+
"hellolynk_read_authorized_inbox_message", "hellolynk_list_pending_messages",
|
|
11
|
+
"hellolynk_claim_message", "hellolynk_reply_authorized_message",
|
|
12
|
+
]);
|
|
13
|
+
const failure = () => Object.assign(new Error("model_inbox_context_unavailable"), { code: "model_inbox_context_unavailable" });
|
|
14
|
+
const hash = value => crypto.createHash("sha256").update(value).digest("hex");
|
|
15
|
+
const stable = value => Array.isArray(value) ? value.map(stable) : value && typeof value === "object"
|
|
16
|
+
? Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])) : value;
|
|
17
|
+
const argsHash = args => hash(JSON.stringify(stable(args)));
|
|
18
|
+
function object(value) {
|
|
19
|
+
if (typeof value === "string") { try { value = JSON.parse(value); } catch { return null; } }
|
|
20
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
21
|
+
}
|
|
22
|
+
export function workBuddyModelHookCall(payload) {
|
|
23
|
+
if (payload?.hook_event_name !== "PreToolUse") return null;
|
|
24
|
+
let name = payload.tool_name, args = object(payload.tool_input);
|
|
25
|
+
if (name === "DeferExecuteTool") { name = args?.toolName; args = object(args?.params); }
|
|
26
|
+
if (typeof name !== "string" || !/^mcp__hellolynk__/.test(name)) return null;
|
|
27
|
+
name = name.replace(/^mcp__hellolynk__/, "");
|
|
28
|
+
if (!name.startsWith("hellolynk_")) name = "hellolynk_" + name;
|
|
29
|
+
return WORKBUDDY_MODEL_INBOX_NAMES.has(name) && args ? { name, args } : null;
|
|
30
|
+
}
|
|
31
|
+
export async function recordWorkBuddyModelCall(inbox, payload, scopeHash, now = Date.now()) {
|
|
32
|
+
const call = workBuddyModelHookCall(payload);
|
|
33
|
+
if (!call) return false;
|
|
34
|
+
if (![payload.session_id, payload.tool_use_id || payload.call_id, payload.generation_id].every(v => ID.test(v || ""))
|
|
35
|
+
|| !HASH.test(scopeHash || "") || (payload.agent_id && payload.agent_id !== payload.session_id)) throw failure();
|
|
36
|
+
const directory = path.join(inbox, "model-calls");
|
|
37
|
+
await fs.mkdir(directory, { recursive: true });
|
|
38
|
+
const record = { version: 1, scopeHash, threadId: payload.session_id, generationId: payload.generation_id,
|
|
39
|
+
toolUseId: payload.tool_use_id || payload.call_id, name: call.name, argsHash: argsHash(call.args), createdAt: now };
|
|
40
|
+
const file = path.join(directory, hash(scopeHash + "\0" + record.toolUseId) + ".json");
|
|
41
|
+
const temporary = file + "." + crypto.randomUUID() + ".tmp";
|
|
42
|
+
try {
|
|
43
|
+
await fs.writeFile(temporary, JSON.stringify(record), { flag: "wx" });
|
|
44
|
+
await fs.rename(temporary, file);
|
|
45
|
+
} finally { await fs.rm(temporary, { force: true }); }
|
|
46
|
+
// Short-lived request evidence, cleaned on subsequent calls; no timer.
|
|
47
|
+
for (const name of await fs.readdir(directory)) if (/^[a-f0-9]{64}\.json$/.test(name)) {
|
|
48
|
+
const candidate = path.join(directory, name), stat = await fs.stat(candidate).catch(() => null);
|
|
49
|
+
if (stat && now - stat.mtimeMs > TTL) await fs.rm(candidate, { force: true });
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function baggage(meta) {
|
|
54
|
+
if (typeof meta?.baggage !== "string") return null;
|
|
55
|
+
const fields = new Map();
|
|
56
|
+
try {
|
|
57
|
+
for (const part of meta.baggage.split(",")) {
|
|
58
|
+
const item = part.split(";")[0], equal = item.indexOf("=");
|
|
59
|
+
if (equal < 0) continue;
|
|
60
|
+
const key = item.slice(0, equal).trim(), value = decodeURIComponent(item.slice(equal + 1).trim());
|
|
61
|
+
if (fields.has(key)) return null;
|
|
62
|
+
fields.set(key, value);
|
|
63
|
+
}
|
|
64
|
+
} catch { return null; }
|
|
65
|
+
const threadId = fields.get("codebuddy.session_id"), generationId = fields.get("codebuddy.conversation_request_id");
|
|
66
|
+
return ID.test(threadId || "") && ID.test(generationId || "") ? { threadId, generationId } : null;
|
|
67
|
+
}
|
|
68
|
+
export async function readWorkBuddyModelCaller(inbox, meta, name, args, scopeHash, now = Date.now()) {
|
|
69
|
+
const native = baggage(meta);
|
|
70
|
+
if (!native || !HASH.test(scopeHash || "")) return undefined;
|
|
71
|
+
const directory = path.join(inbox, "model-calls"), digest = argsHash(args);
|
|
72
|
+
const files = await fs.readdir(directory).catch(error => { if (error.code === "ENOENT") return []; throw error; });
|
|
73
|
+
const callers = new Set();
|
|
74
|
+
for (const file of files) if (/^[a-f0-9]{64}\.json$/.test(file)) {
|
|
75
|
+
let record;
|
|
76
|
+
try { record = JSON.parse(await fs.readFile(path.join(directory, file), "utf8")); }
|
|
77
|
+
catch { return undefined; }
|
|
78
|
+
if (record.version !== 1 || record.scopeHash !== scopeHash || record.name !== name || record.argsHash !== digest
|
|
79
|
+
|| record.generationId !== native.generationId || !Number.isSafeInteger(record.createdAt)
|
|
80
|
+
|| now - record.createdAt < 0 || now - record.createdAt > TTL) continue;
|
|
81
|
+
if (!ID.test(record.threadId || "")) return undefined;
|
|
82
|
+
callers.add(record.threadId);
|
|
83
|
+
}
|
|
84
|
+
// Do not trust baggage alone: it can be inherited from a parent turn.
|
|
85
|
+
// The current hook must independently agree on the native chat and arguments.
|
|
86
|
+
return callers.size === 1 && callers.has(native.threadId) ? { threadId: native.threadId } : undefined;
|
|
87
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
4
|
+
// Use the desktop conversation SDK. WorkBuddy 5.8.2 no longer registers the
|
|
5
|
+
// former wb:conversations:local:list IPC channel, although __wbInvoke still exists.
|
|
6
|
+
// Authentication and normal model permissions stay in the running desktop.
|
|
7
|
+
export function createWorkBuddyNativeTaskClient({ debugEndpoint = "http://127.0.0.1:9337", targetId, ownedSessionIds = [], timeoutMs = 30_000, transport } = {}) {
|
|
8
|
+
const endpoint = new URL(debugEndpoint);
|
|
9
|
+
if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port || endpoint.username || endpoint.password || endpoint.pathname !== "/" || endpoint.search || endpoint.hash) throw failure("workbuddy_native_debug_endpoint_invalid");
|
|
10
|
+
const owned = new Set(ownedSessionIds.map(validId));
|
|
11
|
+
const sent = new Map();
|
|
12
|
+
async function evaluate(expression, customTimeout = timeoutMs) {
|
|
13
|
+
if (transport) return transport(expression);
|
|
14
|
+
const response = await fetch(new URL("/json/list", endpoint), { signal: AbortSignal.timeout(5_000) });
|
|
15
|
+
if (!response.ok) throw failure("workbuddy_native_debug_unavailable");
|
|
16
|
+
const targets = await response.json();
|
|
17
|
+
if (!Array.isArray(targets)) throw failure("workbuddy_native_debug_response_invalid");
|
|
18
|
+
const pages = targets.filter((item) => item.type === "page" && (targetId ? item.id === targetId : item.title === "WorkBuddy"));
|
|
19
|
+
if (pages.length !== 1) throw failure("workbuddy_native_target_ambiguous");
|
|
20
|
+
// Never return or log the target page URL; it can contain accountSnapshot.
|
|
21
|
+
const address = new URL(pages[0].webSocketDebuggerUrl);
|
|
22
|
+
if (address.protocol !== "ws:" || address.hostname !== endpoint.hostname || address.port !== endpoint.port || address.username || address.password || address.search || address.hash || !/^\/devtools\/page\/[A-Za-z0-9-]+$/.test(address.pathname)) throw failure("workbuddy_native_socket_invalid");
|
|
23
|
+
return evaluateOnSocket(address.href, expression, customTimeout);
|
|
24
|
+
}
|
|
25
|
+
function requireOwned(id) { validId(id); if (!owned.has(id)) throw failure("workbuddy_native_task_not_owned"); return id; }
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
async probe() {
|
|
28
|
+
return evaluate("({available:typeof window.wb?.conversations?.create==='function'&&typeof window.wb?.conversations?.get==='function'&&typeof window.wb?.conversations?.local?.list==='function'})");
|
|
29
|
+
},
|
|
30
|
+
async mcpRegistrationStatus({ connect = false } = {}) {
|
|
31
|
+
return evaluate(mcpExpression(`let item=(await daemon.mcpList()).find(x=>x.id==='custom-mcp:hellolynk');if(${connect === true}&&item?.trusted===true&&item.enabled===true&&item.state!=='ready'){await daemon.mcpConnect({id:item.id,interactive:false});item=(await daemon.mcpList()).find(x=>x.id==='custom-mcp:hellolynk');}return project(item);`));
|
|
32
|
+
},
|
|
33
|
+
async approveHelloLynkMcp({ command, args, storeRoot }) {
|
|
34
|
+
if (typeof command !== "string" || !Array.isArray(args) || args.some(x=>typeof x!=="string") || typeof storeRoot !== "string") throw failure("workbuddy_native_mcp_expected_config_invalid");
|
|
35
|
+
const expected = { command, args, storeRoot };
|
|
36
|
+
// Refusals come back as values: a thrown renderer error would reach
|
|
37
|
+
// here only as workbuddy_native_call_failed.
|
|
38
|
+
const value = await evaluate(mcpExpression(`
|
|
39
|
+
const expected=${JSON.stringify(expected)};
|
|
40
|
+
const find=async()=>(await daemon.mcpList()).find(x=>x.id==='custom-mcp:hellolynk');
|
|
41
|
+
await daemon.mcpRefresh();
|
|
42
|
+
let item=await find();
|
|
43
|
+
const form=item?.form;
|
|
44
|
+
if(!form||item.transport!=='stdio'||form.name!=='hellolynk'||form.command!==expected.command||JSON.stringify(form.args)!==JSON.stringify(expected.args)||!Array.isArray(form.env)||form.env.length!==1||form.env[0].key!=='HELLOLYNK_NEUTRAL_STORE_ROOT'||form.env[0].value!==expected.storeRoot)return {refused:'workbuddy_native_mcp_config_mismatch'};
|
|
45
|
+
if(item.enabled!==true)return {refused:'workbuddy_native_mcp_disabled'};
|
|
46
|
+
if(item.trusted!==true){const approved=await daemon.mcpApprove({id:item.id,configHash:item.configHash});if(approved?.ok===false)return {refused:'workbuddy_native_mcp_approval_failed'};}
|
|
47
|
+
const connected=item.trusted===true&&item.state==='ready'?null:await daemon.mcpConnect({id:item.id,interactive:false});
|
|
48
|
+
item=await find();
|
|
49
|
+
if(connected?.ok===false)return {...project(item),status:'connection_failed',errorCode:connected.code??'workbuddy_native_mcp_connection_failed'};
|
|
50
|
+
// A just-approved server is still starting; wait briefly for it.
|
|
51
|
+
for(let waited=0;item?.trusted===true&&item.enabled===true&&item.state!=='ready'&&waited<10000;waited+=250){await new Promise(r=>setTimeout(r,250));item=await find();}
|
|
52
|
+
return project(item);
|
|
53
|
+
`), 60_000);
|
|
54
|
+
if (value?.refused) throw failure(/^workbuddy_native_mcp_[a-z_]{1,60}$/.test(value.refused) ? value.refused : "workbuddy_native_mcp_approval_failed");
|
|
55
|
+
return value;
|
|
56
|
+
},
|
|
57
|
+
async create({ nativeSessionId = crypto.randomUUID(), title = "HelloLynk" } = {}) {
|
|
58
|
+
validId(nativeSessionId);
|
|
59
|
+
if (owned.has(nativeSessionId)) throw failure("workbuddy_native_task_already_owned");
|
|
60
|
+
if (typeof title !== "string" || title.length < 1 || title.length > 160 || /[\u0000-\u001f]/.test(title)) throw failure("workbuddy_native_task_title_invalid");
|
|
61
|
+
const params = { conversationId: nativeSessionId, title, transport: "local", isPlayground: true, conversationOrigin: "local" };
|
|
62
|
+
const result = await evaluate(conversationExpression(`const r=await api.create(${JSON.stringify(params)});return {nativeSessionId:r?.info?.id??null,errorCode:r?.errorCode??null,transport:r?.info?.transport??null};`));
|
|
63
|
+
if (result?.errorCode || result?.nativeSessionId !== nativeSessionId || result.transport !== "local") throw failure("workbuddy_native_create_unconfirmed");
|
|
64
|
+
owned.add(nativeSessionId);
|
|
65
|
+
return { nativeSessionId, status: "created", nativeRoute: `workbuddy://chat/${encodeURIComponent(nativeSessionId)}` };
|
|
66
|
+
},
|
|
67
|
+
async send({ nativeSessionId, clientRequestId, text }) {
|
|
68
|
+
requireOwned(nativeSessionId); validId(clientRequestId);
|
|
69
|
+
if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > 64 * 1024) throw failure("workbuddy_native_prompt_invalid");
|
|
70
|
+
const key = `${nativeSessionId}:${clientRequestId}`;
|
|
71
|
+
if (sent.has(key)) return { ...sent.get(key), duplicate: true };
|
|
72
|
+
const unknown = { nativeSessionId, clientRequestId, status: "dispatch_unknown" };
|
|
73
|
+
// The owning worker must also persist this marker before calling send;
|
|
74
|
+
// this map stops accidental in-process repetition of an ambiguous call.
|
|
75
|
+
sent.set(key, unknown);
|
|
76
|
+
const args = [[{ type: "text", text }], { clientRequestId, _expectQueueReceipt: true }];
|
|
77
|
+
let value;
|
|
78
|
+
try {
|
|
79
|
+
value = await evaluate(conversationExpression(`const task=await api.get(${JSON.stringify(nativeSessionId)});if(task?.info?.id!==${JSON.stringify(nativeSessionId)}||task.info.transport!=='local'||typeof task.sendPrompt!=='function')throw Error('workbuddy_native_task_unavailable');const r=await task.sendPrompt(...${JSON.stringify(args)});return {errorCode:r?.errorCode??null,queued:r?.disposition==='queued'};`));
|
|
80
|
+
} catch { return unknown; }
|
|
81
|
+
if (value?.errorCode) return unknown;
|
|
82
|
+
const result = { nativeSessionId, clientRequestId, status: value?.queued ? "queued_unconfirmed" : "submitted_unconfirmed" };
|
|
83
|
+
sent.set(key, result);
|
|
84
|
+
return result;
|
|
85
|
+
},
|
|
86
|
+
async status(nativeSessionId) {
|
|
87
|
+
requireOwned(nativeSessionId);
|
|
88
|
+
// get() may return a pooled live object. Read fresh lifecycle metadata
|
|
89
|
+
// from the active/completed view and the separate archived view.
|
|
90
|
+
// Nothing outside this exact owned ID leaves the renderer.
|
|
91
|
+
return evaluate(conversationExpression(`${ORIGIN_ITEM}const id=${JSON.stringify(nativeSessionId)};const item=await originItem(id);return item?{nativeSessionId:id,state:item.state??null,missing:false,archived:item.lifecycle==='archived'}:{nativeSessionId:null,missing:true,archived:false};`));
|
|
92
|
+
},
|
|
93
|
+
// Original conversation of a reply: any ID the hook recorded, not only
|
|
94
|
+
// HelloLynk tasks. Returns lifecycle and busy flags only; never opens it.
|
|
95
|
+
async listConversations() {
|
|
96
|
+
return evaluate(conversationExpression(`const conversations=new Map();for(const view of ['all','archived']){for(let page=1;;page++){const r=await api.local.list({view,page,size:100});if(r?.errorCode||r?.errors?.length||!Array.isArray(r?.items)||!Number.isSafeInteger(r.total)||r.total<0)throw Error('workbuddy_native_catalog_unknown');for(const item of r.items){if(typeof item.id!=='string'||!/^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$/.test(item.id))continue;conversations.set(item.id,{targetConversationId:item.id,title:item.title||item.name||item.id,project:(item.space?.cwd||item.cwd||item.workspacePath||'').split(/[\\\\/]/).filter(Boolean).at(-1)||'',state:item.lifecycle==='archived'?'archived':['planning','working','pending'].includes(item.state)?'active':'idle'});}if(page*100>=r.total)break;if(!r.items.length)throw Error('workbuddy_native_catalog_incomplete');}}return {conversations:[...conversations.values()],coverage:'complete',exactTargetSupported:true};`));
|
|
97
|
+
},
|
|
98
|
+
async inspectOrigin(nativeSessionId) {
|
|
99
|
+
validId(nativeSessionId);
|
|
100
|
+
return evaluate(conversationExpression(`${ORIGIN_BUSY}${ORIGIN_ITEM}const id=${JSON.stringify(nativeSessionId)};const item=await originItem(id);if(!item)return {present:false,archived:false,busy:false};if(item.lifecycle==='archived')return {present:true,archived:true,busy:false};if(await busy(null,item))return {present:true,archived:false,busy:true};const task=await api.get(id);if(task?.info?.id!==id)throw Error('workbuddy_native_lifecycle_unknown');return {present:true,archived:false,busy:await busy(task,item)};`));
|
|
101
|
+
},
|
|
102
|
+
// Re-checks lifecycle and idle in the same renderer call as sendPrompt, so
|
|
103
|
+
// a reply is not sent to a conversation archived or deleted since the
|
|
104
|
+
// inspection, nor queued behind a running turn or an open question.
|
|
105
|
+
async sendToIdle({ nativeSessionId, clientRequestId, text }) {
|
|
106
|
+
validId(nativeSessionId); validId(clientRequestId);
|
|
107
|
+
if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > 64 * 1024) throw failure("workbuddy_native_prompt_invalid");
|
|
108
|
+
const key = `${nativeSessionId}:${clientRequestId}`;
|
|
109
|
+
if (sent.has(key)) return { ...sent.get(key), duplicate: true };
|
|
110
|
+
const unknown = { nativeSessionId, clientRequestId, status: "dispatch_unknown" };
|
|
111
|
+
sent.set(key, unknown);
|
|
112
|
+
const args = [[{ type: "text", text }], { clientRequestId, _expectQueueReceipt: true }];
|
|
113
|
+
let value;
|
|
114
|
+
try {
|
|
115
|
+
value = await evaluate(conversationExpression(`${ORIGIN_BUSY}${ORIGIN_ITEM}const id=${JSON.stringify(nativeSessionId)};let item;try{item=await originItem(id);}catch{return {held:'lifecycle_unknown'};}if(!item)return {held:'missing'};if(item.lifecycle==='archived')return {held:'archived'};let task;try{task=await api.get(id);if(task?.info?.id!==id||typeof task.sendPrompt!=='function')return {held:'missing'};if(await busy(task,item))return {held:'busy'};}catch{return {held:'lifecycle_unknown'};}const r=await task.sendPrompt(...${JSON.stringify(args)});return {errorCode:r?.errorCode??null,queued:r?.disposition==='queued'};`));
|
|
116
|
+
} catch { return unknown; }
|
|
117
|
+
if (["busy", "missing", "archived", "lifecycle_unknown"].includes(value?.held)) {
|
|
118
|
+
// Nothing was submitted; a later tick may try again.
|
|
119
|
+
sent.delete(key);
|
|
120
|
+
return { nativeSessionId, clientRequestId, status: value.held };
|
|
121
|
+
}
|
|
122
|
+
if (value?.errorCode) return unknown;
|
|
123
|
+
const result = { nativeSessionId, clientRequestId, status: value?.queued ? "queued_unconfirmed" : "submitted_unconfirmed" };
|
|
124
|
+
sent.set(key, result);
|
|
125
|
+
return result;
|
|
126
|
+
},
|
|
127
|
+
async open(nativeSessionId) {
|
|
128
|
+
requireOwned(nativeSessionId);
|
|
129
|
+
const nativeRoute = `workbuddy://chat/${encodeURIComponent(nativeSessionId)}`;
|
|
130
|
+
// The installed renderer's dispatchInternalDeeplink uses this exact
|
|
131
|
+
// cancelable event; its normal chat handler consumes only known routes.
|
|
132
|
+
const value = await evaluate(`(()=>{const event=new CustomEvent('workbuddy:internal-deeplink',{cancelable:true,detail:{url:${JSON.stringify(nativeRoute)}}});window.dispatchEvent(event);return {opened:event.defaultPrevented};})()`);
|
|
133
|
+
if (!value?.opened) throw failure("workbuddy_native_route_unavailable");
|
|
134
|
+
return { nativeSessionId, status: "open_requested", nativeRoute };
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// WorkBuddy 5.8.2 renderer: isBusyState is planning|working, "pending" waits
|
|
140
|
+
// for the person, and hasActiveInteraction() covers these three getters.
|
|
141
|
+
const ORIGIN_BUSY = "const busy=async(task,item)=>{for(const state of [task?.info?.state,item?.state])if(['planning','working','pending'].includes(state))return true;for(const name of ['getPendingInfo','getCloudServiceConfirmation','getPluginRecommendation'])if(typeof task?.[name]==='function'&&(await task[name]())!=null)return true;return false;};";
|
|
142
|
+
// "all" excludes archives in WorkBuddy's SDK. Fall through to its archived
|
|
143
|
+
// view before consulting get(), whose pooled object can retain old lifecycle.
|
|
144
|
+
const ORIGIN_ITEM = "const originItem=async(id)=>{let item=null;for(const view of ['all','archived']){for(let page=1;page<=100;page++){const r=await api.local.list({view,page,size:100});if(r?.errorCode||r?.errors?.length||!Array.isArray(r?.items)||!Number.isSafeInteger(r.total)||r.total<0)throw Error('workbuddy_native_lifecycle_unknown');item=r.items.find(x=>x.id===id)??null;if(item||page*100>=r.total||!r.items.length)break;}if(item)break;}if(!item){if(await api.get(id)==null)return null;throw Error('workbuddy_native_lifecycle_unknown');}if(!['active','archived','completed'].includes(item.lifecycle))throw Error('workbuddy_native_lifecycle_unknown');return item;};";
|
|
145
|
+
|
|
146
|
+
// Use the desktop SDK namespace, not a hashed renderer chunk or minified
|
|
147
|
+
// export name. Approval still uses the app's exact configuration hash.
|
|
148
|
+
function conversationExpression(body) {
|
|
149
|
+
return `(async()=>{const api=window.wb?.conversations;if(!api)throw Error('workbuddy_native_conversation_sdk_unavailable');${body}})()`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function mcpExpression(body) {
|
|
153
|
+
return `(async()=>{const api=window.wb?.mcp;if(!api)throw Error('workbuddy_native_mcp_sdk_unavailable');const daemon={mcpList:async()=>{const r=await api.list();if(!Array.isArray(r?.items))throw Error('workbuddy_native_mcp_response_invalid');return r.items;},mcpConnect:params=>api.connect(params),mcpApprove:params=>api.approve(params),mcpRefresh:()=>api.refresh()};const project=(item)=>({id:item?.id??null,status:item?.state??'missing',enabled:item?.enabled===true,trusted:item?.trusted===true,blockedReason:item?.blockedReason??null,configHash:item?.configHash??null,tools:(item?.tools??[]).map(t=>({name:t.name,enabled:t.enabled!==false}))});${body}})()`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function evaluateOnSocket(address, expression, timeoutMs) {
|
|
157
|
+
if (typeof globalThis.WebSocket !== "function") throw failure("workbuddy_native_websocket_runtime_required");
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const socket = new WebSocket(address);
|
|
160
|
+
let settled = false;
|
|
161
|
+
const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); socket.close(); error ? reject(failure(error)) : resolve(value); };
|
|
162
|
+
const timer = setTimeout(() => finish("workbuddy_native_call_unknown"), timeoutMs);
|
|
163
|
+
socket.onopen = () => socket.send(JSON.stringify({ id: 1, method: "Runtime.evaluate", params: { expression, returnByValue: true, awaitPromise: true } }));
|
|
164
|
+
socket.onerror = () => finish("workbuddy_native_socket_failed");
|
|
165
|
+
socket.onclose = () => { if (!settled) finish("workbuddy_native_call_unknown"); };
|
|
166
|
+
socket.onmessage = (event) => {
|
|
167
|
+
let message; try { message = JSON.parse(event.data); } catch { finish("workbuddy_native_response_invalid"); return; }
|
|
168
|
+
if (message.id !== 1) return;
|
|
169
|
+
if (message.error || message.result?.exceptionDetails) { finish("workbuddy_native_call_failed"); return; }
|
|
170
|
+
finish(null, message.result?.result?.value);
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function validId(value) { if (typeof value !== "string" || !ID.test(value)) throw failure("workbuddy_native_id_invalid"); return value; }
|
|
175
|
+
function failure(code) { return Object.assign(new Error(code), { code }); }
|