@hellolynk-ai/trae-work-mcp 0.0.0-stage → 0.4.95

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.
Files changed (123) hide show
  1. package/LICENSE +92 -0
  2. package/README.md +101 -2
  3. package/bin/hellolynk-client-setup.mjs +2748 -0
  4. package/bin/hellolynk-mcp-config-shape.mjs +23 -0
  5. package/hellolynk-client-pack-manifest.json +619 -0
  6. package/hellolynk-client-pack-pin.json +4 -0
  7. package/integrations/trae-work/package.json +11 -0
  8. package/integrations/trae-work/runtime/neutral-host-bootstrap.mjs +38 -0
  9. package/integrations/trae-work/runtime/neutral-host-release-entry.mjs +16 -0
  10. package/integrations/trae-work/runtime/trae-work-bootstrap.mjs +30 -0
  11. package/integrations/trae-work/server/dynamic-ui-contract.mjs +376 -0
  12. package/integrations/trae-work/server/host-lifecycle.mjs +128 -0
  13. package/integrations/trae-work/server/mcp-proxy.mjs +181 -0
  14. package/integrations/trae-work/server/mcp-server.mjs +784 -0
  15. package/integrations/trae-work/server/native-task-cdp.mjs +241 -0
  16. package/integrations/trae-work/server/native-task-worker.mjs +199 -0
  17. package/integrations/trae-work/server/neutral-host-adapter.mjs +725 -0
  18. package/integrations/trae-work/server/trae-neutral-production.mjs +97 -0
  19. package/integrations/trae-work/server/trae-ui-launch.mjs +182 -0
  20. package/integrations/trae-work/server/ui-action-gate.mjs +122 -0
  21. package/integrations/trae-work/skills/hellolynk-inbox/SKILL.md +9 -0
  22. package/package.json +61 -4
  23. package/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
  24. package/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
  25. package/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
  26. package/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
  27. package/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
  28. package/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
  29. package/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  30. package/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
  31. package/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
  32. package/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  33. package/runtime/connect-activate.mjs +837 -0
  34. package/runtime/core/hellolynk-agent-configurator.mjs +701 -0
  35. package/runtime/core/hellolynk-client-account.mjs +180 -0
  36. package/runtime/core/hellolynk-client-core.mjs +770 -0
  37. package/runtime/core/hellolynk-client-profile.mjs +191 -0
  38. package/runtime/core/hellolynk-client-update-entry.mjs +31 -0
  39. package/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
  40. package/runtime/core/hellolynk-client-update.mjs +453 -0
  41. package/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
  42. package/runtime/core/hellolynk-device-enrollment.mjs +435 -0
  43. package/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  44. package/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  45. package/runtime/core/hellolynk-host-auth.mjs +347 -0
  46. package/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
  47. package/runtime/core/hellolynk-host-transport-client.mjs +946 -0
  48. package/runtime/core/hellolynk-host.mjs +2374 -0
  49. package/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  50. package/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
  51. package/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
  52. package/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
  53. package/runtime/core/hellolynk-instance-conversation.mjs +185 -0
  54. package/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
  55. package/runtime/core/hellolynk-local-contacts.mjs +1228 -0
  56. package/runtime/core/hellolynk-local-data-migration.mjs +766 -0
  57. package/runtime/core/hellolynk-local-data-protection.mjs +747 -0
  58. package/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
  59. package/runtime/core/hellolynk-local-messaging.mjs +3043 -0
  60. package/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
  61. package/runtime/core/hellolynk-local-process-lock.mjs +157 -0
  62. package/runtime/core/hellolynk-model-inbox.mjs +230 -0
  63. package/runtime/core/hellolynk-model-notices.cjs +60 -0
  64. package/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
  65. package/runtime/core/hellolynk-native-conversations.mjs +280 -0
  66. package/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  67. package/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
  68. package/runtime/core/hellolynk-native-notice.mjs +50 -0
  69. package/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
  70. package/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
  71. package/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
  72. package/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  73. package/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
  74. package/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
  75. package/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  76. package/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  77. package/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
  78. package/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
  79. package/runtime/core/hellolynk-release-retention.mjs +277 -0
  80. package/runtime/core/hellolynk-runtime-safety.cjs +471 -0
  81. package/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
  82. package/runtime/core/hellolynk-workbench-connection.mjs +234 -0
  83. package/runtime/neutral-host-bootstrap.mjs +38 -0
  84. package/runtime/neutral-host-release-entry.mjs +16 -0
  85. package/runtime/neutral-host-release-loader.mjs +156 -0
  86. package/runtime/neutral-host-release-manifest.json +394 -0
  87. package/runtime/neutral-host-release-pin.json +4 -0
  88. package/runtime/platform/windows/configure-agent.ps1 +281 -0
  89. package/runtime/platform/windows/credential-store.ps1 +844 -0
  90. package/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  91. package/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  92. package/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  93. package/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  94. package/runtime/trae-work-bootstrap.mjs +30 -0
  95. package/runtime/workbench/candidate-bootstrap.mjs +54 -0
  96. package/runtime/workbench/contract.mjs +1314 -0
  97. package/runtime/workbench/hellolynk-workbench-build.json +215 -0
  98. package/runtime/workbench/host-capabilities.mjs +80 -0
  99. package/runtime/workbench/i18n.mjs +1127 -0
  100. package/runtime/workbench/loopback-surface.mjs +740 -0
  101. package/runtime/workbench/mcp-app-bundle.mjs +97 -0
  102. package/runtime/workbench/service.mjs +485 -0
  103. package/runtime/workbench/state.mjs +421 -0
  104. package/runtime/workbench/transports.mjs +171 -0
  105. package/runtime/workbench/ui/app.mjs +6120 -0
  106. package/runtime/workbench/ui/connection-form.mjs +155 -0
  107. package/runtime/workbench/ui/hellolynk-mark.png +0 -0
  108. package/runtime/workbench/ui/http-transport.mjs +247 -0
  109. package/runtime/workbench/ui/icons/arrow-left.svg +3 -0
  110. package/runtime/workbench/ui/icons/close.svg +3 -0
  111. package/runtime/workbench/ui/icons/contacts.svg +3 -0
  112. package/runtime/workbench/ui/icons/inbox.svg +3 -0
  113. package/runtime/workbench/ui/icons/refresh.svg +3 -0
  114. package/runtime/workbench/ui/icons/search.svg +3 -0
  115. package/runtime/workbench/ui/icons/security.svg +3 -0
  116. package/runtime/workbench/ui/icons/settings.svg +3 -0
  117. package/runtime/workbench/ui/index.html +139 -0
  118. package/runtime/workbench/ui/loopback-entry.mjs +26 -0
  119. package/runtime/workbench/ui/mcp-app-client.mjs +151 -0
  120. package/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
  121. package/runtime/workbench/ui/styles.css +686 -0
  122. package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
  123. package/tools/hellolynk-install-client-skills.mjs +1211 -0
@@ -0,0 +1,241 @@
1
+ import { NATIVE_NOTICE_PREFIXES } from '../../../runtime/core/hellolynk-native-notice.mjs';
2
+
3
+ const ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,240}$/;
4
+ export async function discoverTraeNativeTaskTarget(debugEndpoint = "http://127.0.0.1:9338", request = fetch) {
5
+ const endpoint = checkedEndpoint(debugEndpoint);
6
+ const response = await request(new URL("/json/list", endpoint), { signal: AbortSignal.timeout(5_000) });
7
+ if (!response.ok) throw failure("trae_native_debug_unavailable");
8
+ const targets = await response.json();
9
+ if (!Array.isArray(targets)) throw failure("trae_native_debug_response_invalid");
10
+ const pages = targets.filter((item) => {
11
+ if (item?.type !== "page" || typeof item.id !== "string" || !/^[A-Za-z0-9-]{8,100}$/.test(item.id)) return false;
12
+ try {
13
+ const url = new URL(item.url);
14
+ return url.protocol === "vscode-file:" && url.hostname === "vscode-app" && /\/vs\/code\/electron-browser\/solo\/solo-lite\.html$/.test(url.pathname);
15
+ } catch { return false; }
16
+ });
17
+ if (pages.length !== 1) throw failure("trae_native_target_not_unique");
18
+ return pages[0].id;
19
+ }
20
+
21
+ // Executed in TRAE's already initialized SOLO Lite SDK. No editor, selection,
22
+ // window command, synthetic keyboard event or DOM mutation is used.
23
+ export async function invokeTraeSdk(requireModule, operation, input, noticePrefixes = NATIVE_NOTICE_PREFIXES) {
24
+ // Read only modules TRAE already loaded: requiring an ID would run its factory, and TRAE
25
+ // builds renumber the SDK entry module, which is found by its named exports instead.
26
+ const own = (value, key) => { try { return Object.prototype.hasOwnProperty.call(value, key); } catch { return false; } };
27
+ const loaded = id => own(requireModule?.c, id) && requireModule.c[id]?.loaded === true ? requireModule.c[id].exports : undefined;
28
+ const entries = Object.keys(requireModule?.c || {}).map(loaded).filter(exports => exports
29
+ && ["streamDomainService", "sessionStore", "isSessionProcessing", "SessionStatus", "SoloEnv", "SoloMode"].every(name => own(exports, name)));
30
+ const sdk = entries.length === 1 ? entries[0] : undefined, service = loaded(40650)?.kd;
31
+ if (typeof service?.createSession !== "function" || typeof service?.sendMessage !== "function"
32
+ || typeof service?.initSessionForSoloLite !== "function"
33
+ || typeof service?.chatApi?.getSession !== "function" || typeof sdk?.streamDomainService?.createUserMessage !== "function"
34
+ || typeof sdk?.sessionStore?.sessionById?.get !== "function" || typeof sdk?.sessionStore?.sessionMessageIds?.get !== "function"
35
+ || typeof sdk?.sessionStore?.lastUserMessage?.get !== "function" || typeof sdk?.isSessionProcessing !== "function")
36
+ throw new Error("trae_native_api_unavailable");
37
+ const idPattern = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,240}$/;
38
+ const id = input?.nativeTaskId;
39
+ let emptyNewSession = false;
40
+ const valid = value => typeof value === "string" && idPattern.test(value);
41
+ // TRAE 3.4.11 rejects a deleted session with ApiRequestError 995001 "session not found".
42
+ const missing = r => r?.code === 991200 || r?.code === 995001
43
+ && (r.message === `get_chat_session failed: chat session not found for session_id: ${id}` || r.message === "session not found");
44
+ async function inspect() {
45
+ if (!valid(id)) throw new Error("trae_native_target_id_required");
46
+ const current = sdk.sessionStore.sessionById.get(id);
47
+ // Local and remote native sessions retain the host's own routing choice.
48
+ let response;
49
+ try { response = await service.chatApi.getSession({ chat_session_id: id, env: current?.env ?? sdk.SoloEnv.Local }); }
50
+ catch (error) {
51
+ // The native SDK may reject with the same exact not-found API result.
52
+ if (missing(error)) return { state: "deleted" };
53
+ throw error;
54
+ }
55
+ if (missing(response)) return { state: "deleted" };
56
+ if (response?.code !== 0 || response.data?.chat_session_id !== id) throw new Error("trae_native_lifecycle_unknown");
57
+ if (input.directory === true && (response.data.parent_session_id || current?.parent_session_id)) return { state: "deleted" };
58
+ const latest = sdk.sessionStore.sessionById.get(id);
59
+ const terminal = [sdk.SessionStatus.Finished, sdk.SessionStatus.Stopped, sdk.SessionStatus.Failed];
60
+ const emptyNew = input.allowEmpty === true && response.data.status === sdk.SessionStatus.Pending
61
+ && (sdk.sessionStore.sessionMessageIds.get(id) || []).length === 0;
62
+ emptyNewSession = emptyNew;
63
+ return { state: "present", idle: (terminal.includes(response.data.status) || emptyNew)
64
+ && (!latest || !sdk.isSessionProcessing(latest.status) || emptyNew), nativeTaskId: id };
65
+ }
66
+ if (operation === "directory") {
67
+ if (typeof service.chatApi.listSessions !== "function") throw new Error("trae_native_catalog_unavailable");
68
+ const conversations = [], projects = new Map();
69
+ if (typeof service.projectApi?.listProjects === "function") {
70
+ let token;
71
+ const seenProjectTokens = new Set();
72
+ do {
73
+ const response = await service.projectApi.listProjects({ env: sdk.SoloEnv.Local, page_size: 100, ...(token ? { page_token: token } : {}) });
74
+ const data = response.data?.data ?? response.data;
75
+ if (response.code !== 0 || !Array.isArray(data?.items)) break;
76
+ for (const row of data.items) projects.set(row.local_project_id || row.project_id, row.name || "");
77
+ if (data.next_page_token && (data.next_page_token === token || seenProjectTokens.has(data.next_page_token))) throw new Error("trae_native_catalog_cursor_invalid");
78
+ if (data.next_page_token) seenProjectTokens.add(data.next_page_token);
79
+ token = data.next_page_token;
80
+ } while (token);
81
+ }
82
+ let token;
83
+ const seenSessionTokens = new Set();
84
+ do {
85
+ const response = await service.chatApi.listSessions({ env: sdk.SoloEnv.Local, page_size: 100, ...(token ? { page_token: token } : {}) });
86
+ const data = response.data?.data ?? response.data;
87
+ if (response.code !== 0 || !Array.isArray(data?.items)) throw new Error("trae_native_catalog_unavailable");
88
+ for (const row of data.items) if (valid(row.chat_session_id) && !row.parent_session_id) conversations.push({
89
+ targetConversationId: row.chat_session_id, title: row.title || row.chat_session_id,
90
+ project: projects.get(row.local_project_id) || "", state: sdk.isSessionProcessing(row.status) ? "active" : "idle" });
91
+ if (data.next_page_token && (data.next_page_token === token || seenSessionTokens.has(data.next_page_token) || !data.items.length)) throw new Error("trae_native_catalog_cursor_invalid");
92
+ if (data.next_page_token) seenSessionTokens.add(data.next_page_token);
93
+ token = data.next_page_token;
94
+ } while (token);
95
+ // This native endpoint enumerates local SOLO sessions; remote sessions have
96
+ // different lifecycle/transport semantics and are not guessed from cache.
97
+ return { conversations, coverage: "partial", exactTargetSupported: true, exactTitleSupported: true };
98
+ }
99
+ if (operation === "probe") return { available: true };
100
+ if (operation === "inspect") return inspect();
101
+ if (operation === "create") {
102
+ const nativeTaskId = await service.createSession({ mode: sdk.SoloMode.Work, env: sdk.SoloEnv.Local,
103
+ project_name: "HelloLynk", auto_create_project: true, origin: "hellolynk" });
104
+ if (!valid(nativeTaskId)) return { status: "dispatch_unknown", nativeTaskId: null };
105
+ return { status: "created", nativeTaskId };
106
+ }
107
+ if (operation !== "send" || !valid(id) || typeof input.text !== "string" || input.text.length > 16384
108
+ || !noticePrefixes.some(prefix => input.text.startsWith(prefix)) || !valid(input.clientRequestId)
109
+ || !input.text.includes(input.clientRequestId)) throw new Error("trae_native_prompt_invalid");
110
+ const before = await inspect();
111
+ if (before.state !== "present" || !before.idle) return { status: "not_submitted", nativeTaskId: id, reason: before.state === "deleted" ? "deleted" : "busy" };
112
+ // An unopened conversation may lack its selected agent/history in memory.
113
+ // Use the host's bounded initialization (20 messages), never choose a model.
114
+ if (!sdk.sessionStore.lastUserMessage.get(id) && !emptyNewSession) {
115
+ try {
116
+ await service.initSessionForSoloLite(id, sdk.sessionStore.sessionById.get(id)?.env ?? sdk.SoloEnv.Local,
117
+ { skipSubscription: true, preserveLocalChangesDuringInit: true, strictMessageLoad: true });
118
+ } catch (error) {
119
+ // The user may delete the session after inspect; no prompt exists yet.
120
+ if (missing(error)) return { status: "not_submitted", nativeTaskId: id, reason: "deleted" };
121
+ throw error;
122
+ }
123
+ }
124
+ const session = sdk.sessionStore.sessionById.get(id);
125
+ if (!session || sdk.isSessionProcessing(session.status) && !(input.allowEmpty === true && session.status === sdk.SessionStatus.Pending))
126
+ return { status: "not_submitted", nativeTaskId: id, reason: "busy" };
127
+ const userMessage = sdk.streamDomainService.createUserMessage({ sessionId: id,
128
+ query: [{ type: "text", data: { content: input.text } }], mode: session.mode, env: session.env });
129
+ let admitted = false, refused = false;
130
+ let result;
131
+ try { result = await service.sendMessage({ userMessage, origin: "hellolynk" }, () => {
132
+ // The SDK calls this before submission and again after its own stream has
133
+ // started. Its own Running status must not invalidate an accepted send.
134
+ if (admitted) return true;
135
+ const live = sdk.sessionStore.sessionById.get(id);
136
+ admitted = !!live && (!sdk.isSessionProcessing(live.status)
137
+ || input.allowEmpty === true && live.status === sdk.SessionStatus.Pending
138
+ && (sdk.sessionStore.sessionMessageIds.get(id) || []).length === 0);
139
+ refused = !admitted;
140
+ return admitted;
141
+ }); } catch {
142
+ if (refused && !admitted) return { status: "not_submitted", nativeTaskId: id, reason: "busy" };
143
+ return { status: "dispatch_unknown", nativeTaskId: id };
144
+ }
145
+ if (refused && !admitted) return { status: "not_submitted", nativeTaskId: id, reason: "busy" };
146
+ if (!valid(result?.userMessageId)) return { status: "dispatch_unknown", nativeTaskId: id };
147
+ return { status: "submitted_unconfirmed", nativeTaskId: id, userMessageId: result.userMessageId };
148
+ }
149
+
150
+ export async function findTraeLoadedRuntime(call) {
151
+ const imported = await call("Runtime.evaluate", { expression: `(async()=>{const url=new URL('../../../../../node_modules/@byted-icube/solo-lite/dist/index.mjs',location.href);return (await import(url.href)).bootstrapSoloLite})()`, awaitPromise: true, returnByValue: false });
152
+ if (imported.exceptionDetails || !imported.result?.objectId) throw failure("trae_native_api_unavailable");
153
+ const props = await call("Runtime.getProperties", { objectId: imported.result.objectId, ownProperties: true });
154
+ const scopes = props.internalProperties?.find(p => p.name === "[[Scopes]]")?.value?.objectId;
155
+ if (!scopes) throw failure("trae_native_api_unavailable");
156
+ const list = await call("Runtime.getProperties", { objectId: scopes, ownProperties: true });
157
+ for (const scope of list.result || []) {
158
+ if (!scope.value?.objectId || scope.value.description === "Global") continue;
159
+ const vars = await call("Runtime.getProperties", { objectId: scope.value.objectId, ownProperties: true });
160
+ for (const variable of vars.result || []) {
161
+ if (variable.name !== "l" || variable.value?.type !== "function") continue;
162
+ const check = await call("Runtime.callFunctionOn", { objectId: variable.value.objectId,
163
+ functionDeclaration: `function(){return (${invokeTraeSdk.toString()})(this,"probe",{},${JSON.stringify(NATIVE_NOTICE_PREFIXES)})}`, awaitPromise: true, returnByValue: true });
164
+ if (!check.exceptionDetails && check.result?.value?.available === true) return variable.value.objectId;
165
+ }
166
+ }
167
+ throw failure("trae_native_api_unavailable");
168
+ }
169
+
170
+ export function createTraeNativeTaskCdp({ debugEndpoint = "http://127.0.0.1:9338", targetId,
171
+ ownedTaskIds = [], originalTaskIds = [], timeoutMs = 15_000, connect = connectTraeCdp, invoke } = {}) {
172
+ const endpoint = checkedEndpoint(debugEndpoint);
173
+ if (typeof targetId !== "string" || !/^[A-Za-z0-9-]{8,100}$/.test(targetId)) throw failure("trae_native_target_id_required");
174
+ const owned = new Set(ownedTaskIds.map(validId)), originals = new Set(originalTaskIds.map(validId));
175
+ let connection, runtime, opening;
176
+ const callSdk = invoke || (async (operation, input) => {
177
+ opening ||= connect(endpoint, targetId, timeoutMs).then(async c => { connection = c; runtime = await findTraeLoadedRuntime(c.call); });
178
+ await opening;
179
+ const reply = await connection.call("Runtime.callFunctionOn", { objectId: runtime,
180
+ functionDeclaration: `function(operation,input){return (${invokeTraeSdk.toString()})(this,operation,input,${JSON.stringify(NATIVE_NOTICE_PREFIXES)})}`,
181
+ arguments: [{ value: operation }, { value: input }], awaitPromise: true, returnByValue: true });
182
+ if (reply.exceptionDetails || !reply.result?.value) throw failure("trae_native_api_call_failed");
183
+ return reply.result.value;
184
+ });
185
+ const addressable = value => owned.has(validId(value)) || originals.has(value);
186
+ return Object.freeze({
187
+ probe: () => callSdk("probe", {}),
188
+ listConversations: () => callSdk("directory", {}),
189
+ inspectLifecycle(nativeTaskId, { directory = false } = {}) {
190
+ if (!addressable(nativeTaskId)) throw failure("trae_native_task_not_owned");
191
+ return callSdk("inspect", { nativeTaskId, allowEmpty: owned.has(nativeTaskId), directory });
192
+ },
193
+ async create({ beforeSend } = {}) {
194
+ if (beforeSend && !await beforeSend()) return { status: "not_submitted", reason: "authorization_changed" };
195
+ const result = await callSdk("create", {});
196
+ if (result?.status !== "created" || !ID.test(result.nativeTaskId || "")) return { status: "dispatch_unknown", nativeTaskId: null };
197
+ owned.add(result.nativeTaskId);
198
+ return result;
199
+ },
200
+ async send({ nativeTaskId, clientRequestId, text, beforeSend } = {}) {
201
+ if (!addressable(nativeTaskId)) throw failure("trae_native_task_not_owned");
202
+ validId(clientRequestId);
203
+ if (typeof text !== "string" || Buffer.byteLength(text) > 16384 || !text.includes(clientRequestId)) throw failure("trae_native_prompt_invalid");
204
+ if (beforeSend && !await beforeSend()) return { status: "not_submitted", nativeTaskId, reason: "authorization_changed" };
205
+ // Once the RPC starts, a lost result is uncertain. The worker's durable
206
+ // fence prevents replay; there is no typing/navigation fallback.
207
+ const result = await callSdk("send", { nativeTaskId, clientRequestId, text, allowEmpty: owned.has(nativeTaskId) });
208
+ if (result?.nativeTaskId !== nativeTaskId || !["not_submitted", "submitted_unconfirmed", "dispatch_unknown"].includes(result?.status))
209
+ return { status: "dispatch_unknown", nativeTaskId };
210
+ return result;
211
+ },
212
+ async close() { await opening?.catch(() => {}); connection?.close(); },
213
+ });
214
+ }
215
+
216
+ async function connectTraeCdp(endpoint, targetId, timeoutMs) {
217
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > 120000) throw failure("trae_native_timeout_invalid");
218
+ const url = new URL(endpoint); url.protocol = "ws:"; url.pathname = `/devtools/page/${targetId}`;
219
+ const socket = new WebSocket(url);
220
+ const pending = new Map(); let sequence = 0;
221
+ await new Promise((resolve, reject) => { const timer = setTimeout(() => { socket.close(); reject(failure("trae_native_debug_unavailable")); }, timeoutMs);
222
+ socket.onopen = () => { clearTimeout(timer); resolve(); }; socket.onerror = () => { clearTimeout(timer); reject(failure("trae_native_debug_unavailable")); }; });
223
+ socket.onmessage = event => { let message; try { message = JSON.parse(event.data); } catch { return; }
224
+ const item = pending.get(message.id); if (!item) return; pending.delete(message.id); clearTimeout(item.timer);
225
+ message.error ? item.reject(failure("trae_native_api_call_failed")) : item.resolve(message.result); };
226
+ const close = () => { for (const item of pending.values()) { clearTimeout(item.timer); item.reject(failure("trae_native_debug_unavailable")); } pending.clear(); socket.close(); };
227
+ socket.onclose = () => { for (const item of pending.values()) { clearTimeout(item.timer); item.reject(failure("trae_native_debug_unavailable")); } pending.clear(); };
228
+ return { close, call(method, params) { return new Promise((resolve, reject) => { const id = ++sequence;
229
+ const timer = setTimeout(() => { pending.delete(id); reject(failure("trae_native_api_timeout")); }, timeoutMs);
230
+ pending.set(id, { resolve, reject, timer });
231
+ try { socket.send(JSON.stringify({ id, method, params })); } catch { clearTimeout(timer); pending.delete(id); reject(failure("trae_native_debug_unavailable")); }
232
+ }); } };
233
+ }
234
+ function checkedEndpoint(value) {
235
+ let url; try { url = new URL(value); } catch { throw failure("trae_native_debug_endpoint_invalid"); }
236
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port || url.pathname !== "/" || url.username || url.password || url.search || url.hash)
237
+ throw failure("trae_native_debug_endpoint_invalid");
238
+ return url.href;
239
+ }
240
+ function validId(value) { if (!ID.test(value || "")) throw failure("trae_native_target_id_required"); return value; }
241
+ function failure(code) { return Object.assign(new Error(code), { code }); }
@@ -0,0 +1,199 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createNativeInboxRuntime } from "../../../runtime/core/hellolynk-native-inbox-runtime.mjs";
6
+ import { readNativeReplyRoute } from "../../../runtime/core/hellolynk-native-reply-routes.mjs";
7
+ import { createTraeNativeTaskCdp, discoverTraeNativeTaskTarget } from "./native-task-cdp.mjs";
8
+
9
+ const ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/;
10
+ const HASH = /^[a-f0-9]{64}$/;
11
+ const DELIVERY = new Set(["dispatching", "prepared_not_submitted", "dispatch_unknown", "submitted_unconfirmed", "native_idle"]);
12
+ const hash = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
13
+ const fail = (code) => Object.assign(new Error(code), { code });
14
+ const scopeKey = (scope) => hash([path.resolve(scope.runtimeRoot).toLowerCase(), scope.adapterId, scope.region, scope.principalId, scope.delegateId]);
15
+ const CONFIG = "trae-native-cdp.json";
16
+
17
+ export async function configureTraeNativeTaskWorker({ storeRoot, enabled, debugEndpoint = "http://127.0.0.1:9338" } = {}) {
18
+ const filename = await configPath(storeRoot, true);
19
+ if (enabled === false) { await fs.rm(filename, { force: true }); return { enabled: false }; }
20
+ if (enabled !== true) throw fail("trae_native_config_invalid");
21
+ const endpoint = checkedDebugEndpoint(debugEndpoint);
22
+ const temporary = `${filename}.${crypto.randomUUID()}.tmp`;
23
+ try {
24
+ await fs.writeFile(temporary, `${JSON.stringify({ contract: "hellolynk-trae-native-cdp-v1", debugEndpoint: endpoint })}\n`, { flag: "wx", mode: 0o600 });
25
+ await fs.rename(temporary, filename);
26
+ } finally { await fs.rm(temporary, { force: true }).catch(() => {}); }
27
+ return { enabled: true, debugEndpoint: endpoint };
28
+ }
29
+
30
+ export async function readTraeNativeTaskWorkerConfig(storeRoot) {
31
+ if (!storeRoot) return null;
32
+ const filename = await configPath(storeRoot, false);
33
+ try {
34
+ const stat = await fs.lstat(filename);
35
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024) throw fail("trae_native_config_invalid");
36
+ const value = JSON.parse(await fs.readFile(filename, "utf8"));
37
+ if (!value || value.contract !== "hellolynk-trae-native-cdp-v1" || Object.keys(value).sort().join(",") !== "contract,debugEndpoint") throw fail("trae_native_config_invalid");
38
+ return checkedDebugEndpoint(value.debugEndpoint);
39
+ } catch (error) { if (error?.code === "ENOENT") return null; throw error; }
40
+ }
41
+
42
+ async function configPath(storeRoot, create) {
43
+ if (typeof storeRoot !== "string" || !path.isAbsolute(storeRoot)) throw fail("trae_native_config_root_invalid");
44
+ const runtime = path.join(storeRoot, "runtime"), directory = path.join(runtime, "native-tasks");
45
+ if (create) await fs.mkdir(directory, { recursive: true, mode: 0o700 });
46
+ await safeParents(create ? directory : storeRoot);
47
+ if (!create) {
48
+ for (const candidate of [runtime, directory]) {
49
+ const stat = await fs.lstat(candidate).catch((error) => error?.code === "ENOENT" ? null : Promise.reject(error));
50
+ if (!stat) break;
51
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw fail("trae_native_config_invalid");
52
+ }
53
+ }
54
+ return path.join(directory, CONFIG);
55
+ }
56
+ function checkedDebugEndpoint(value) {
57
+ let url;
58
+ try { url = new URL(value); } catch { throw fail("trae_native_config_invalid"); }
59
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port || url.pathname !== "/"
60
+ || url.username || url.password || url.search || url.hash) throw fail("trae_native_config_invalid");
61
+ return url.href;
62
+ }
63
+
64
+ // Native SDK operations stay here; batching and delivery state are shared
65
+ // with Codex and WorkBuddy rather than three separate per-message engines.
66
+ export function startTraeNativeTaskWorker({ adapter, debugEndpoint = process.env.HELLOLYNK_TRAE_DEBUG_ENDPOINT || "http://127.0.0.1:9338",
67
+ recoveryMs = 5_000, acquireLease, readNoticeLocale, discoverTarget = discoverTraeNativeTaskTarget,
68
+ clientFactory = createTraeNativeTaskCdp, storeFactory = openTraeNativeTaskStore, readRoute = readNativeReplyRoute } = {}) {
69
+ if (!adapter || !Number.isSafeInteger(recoveryMs) || recoveryMs < 10) throw fail("trae_native_worker_options_invalid");
70
+ let store, client, status = { status: "starting" };
71
+ const unsent = () => Object.assign(fail("trae_native_not_submitted"), { nativeDispatchStarted: false });
72
+ const native = {
73
+ async listConversations(query = {}) {
74
+ const directoryClient = clientFactory({ debugEndpoint, targetId: await discoverTarget(debugEndpoint), ownedTaskIds: [],
75
+ originalTaskIds: query.targetConversationId ? [query.targetConversationId] : [] });
76
+ try {
77
+ if (query.targetConversationId) {
78
+ const current = await directoryClient.inspectLifecycle(query.targetConversationId, { directory: true }).catch(() => null);
79
+ const state = current?.state === 'deleted' ? 'deleted' : current?.state === 'present' ? current.idle ? 'idle' : 'active' : 'unavailable';
80
+ return { conversations: [{ targetConversationId: query.targetConversationId, title: query.targetConversationId, project: '', state }], coverage: 'partial', exactTargetSupported: true };
81
+ }
82
+ return await directoryClient.listConversations();
83
+ } finally { await directoryClient.close?.(); }
84
+ },
85
+ async configureScope(scope) { store = await storeFactory(scope); },
86
+ async releaseScope() { await client?.close?.(); store = client = null; },
87
+ async legacyDelivery(item, scope) {
88
+ const route = item.replyToClientMessageId ? await readRoute(scope, item) : null;
89
+ const old = store.records[hash(route ? [item.conversationKey, route.threadId] : item.conversationKey)]?.deliveries[hash(item.localMessageId)];
90
+ return old ? /unknown|dispatching/.test(old.status) ? "unknown" : "sent" : null;
91
+ },
92
+ legacyCarrier(key) { return store.records[hash(key)]?.nativeTaskId ?? null; },
93
+ async prepare(scope, state, groups, batches) {
94
+ await client?.close?.(); client = null;
95
+ if (!groups.size && !batches.size && !Object.keys(state.carriers).length) { status = { status: "listening" }; return; }
96
+ const ownedTaskIds = [...new Set([...Object.values(state.carriers),
97
+ ...Object.values(state.batches).filter(b => b.target.startsWith("trae-work-conv:")).map(b => b.threadId),
98
+ ...Object.values(store.records).filter(r => !r.originalThreadId || r.ownedOriginal).map(r => r.nativeTaskId)].filter(Boolean))];
99
+ const originalTaskIds = [...new Set([...groups.keys(), ...[...batches.values()].map(b => b.target)])]
100
+ .filter(t => t.startsWith("trae-work-origin:")).map(t => t.slice("trae-work-origin:".length));
101
+ client = clientFactory({ debugEndpoint, targetId: await discoverTarget(debugEndpoint), ownedTaskIds, originalTaskIds });
102
+ if ((await client.probe())?.available !== true) throw fail("trae_native_api_unavailable");
103
+ },
104
+ async inspectThread(id) {
105
+ const current = await client.inspectLifecycle(id);
106
+ if (current?.state === "deleted") return { state: "deleted" };
107
+ if (current?.state !== "present" || typeof current.idle !== "boolean") throw fail("trae_native_lifecycle_unknown");
108
+ return { state: "present", thread: { status: current.idle ? "idle" : "active" } };
109
+ },
110
+ canNotifyOrigin(value) { return value?.state === "present" && value.thread?.status === "idle"; },
111
+ async call(operation, args, context) {
112
+ if (operation === "list_threads") { if ((await client.probe())?.available !== true) throw fail("trae_native_api_unavailable"); return {}; }
113
+ let id = args.threadId;
114
+ if (operation === "create_thread") {
115
+ const created = await client.create({ beforeSend: context.stillCurrent });
116
+ if (created?.status === "not_submitted") throw unsent();
117
+ if (created?.status !== "created" || !ID.test(created.nativeTaskId || "")) throw fail("trae_native_create_unknown");
118
+ id = created.nativeTaskId;
119
+ try { await context.rememberThread(id); }
120
+ catch (error) { throw Object.assign(error, { nativeDispatchStarted: false }); }
121
+ }
122
+ if (!await context.stillCurrent()) throw unsent();
123
+ const result = await client.send({ nativeTaskId: id, clientRequestId: context.batchId, text: args.prompt, beforeSend: context.stillCurrent });
124
+ if (result?.status === "not_submitted") throw unsent();
125
+ if (result?.nativeTaskId !== id || result?.status !== "submitted_unconfirmed") throw fail("trae_native_dispatch_unknown");
126
+ status = { status: "delivered", nativeTaskId: id };
127
+ return { ok: true, threadId: id };
128
+ },
129
+ };
130
+ const runtime = createNativeInboxRuntime({ adapterId: "trae-work", native, acquireLease, readNoticeLocale, pollMs: recoveryMs,
131
+ adapter: { nativeTaskScope: () => adapter.getNativeTaskScope(), listPendingMessages: input => adapter.listPendingMessages(input), inboxBatch: (...args) => adapter.inboxBatch(...args) },
132
+ readReplyRoute: readRoute, onError: e => { if (e.operation !== "directory") status = { status: "unavailable", errorCode: e.code }; } });
133
+ const tick = () => runtime.tick().catch(error => { status = { status: "unavailable", errorCode: error.code || error.message }; });
134
+ const ready = tick(); runtime.start();
135
+ return Object.freeze({ ready, tick, get status() { return { ...status }; },
136
+ taskStatus() { return { ...status }; }, close: () => runtime.close() });
137
+ }
138
+
139
+ export async function openTraeNativeTaskStore(scope) {
140
+ if (!scope || !path.isAbsolute(scope.runtimeRoot || "") || scope.adapterId !== "trae-work"
141
+ || [scope.region, scope.principalId, scope.delegateId].some((value) => typeof value !== "string" || !ID.test(value))) throw fail("trae_native_scope_invalid");
142
+ await safeParents(scope.runtimeRoot);
143
+ const directory = path.join(scope.runtimeRoot, "native-tasks");
144
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
145
+ await safeParents(directory);
146
+ const filename = path.join(directory, `${hash([scope.adapterId, scope.region, scope.principalId, scope.delegateId])}.json`);
147
+ let records = {};
148
+ try {
149
+ const stat = await fs.lstat(filename);
150
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4 * 1024 * 1024) throw fail("trae_native_state_invalid");
151
+ records = JSON.parse(await fs.readFile(filename, "utf8"));
152
+ if (!records || typeof records !== "object" || Array.isArray(records) || Object.keys(records).length > 5_000) throw fail("trae_native_state_invalid");
153
+ for (const [conversation, record] of Object.entries(records)) {
154
+ if (!HASH.test(conversation) || !record || typeof record !== "object" || Array.isArray(record)
155
+ || record.nativeTaskId !== null && !ID.test(record.nativeTaskId || "")
156
+ || record.originalThreadId != null && (!ID.test(record.originalThreadId) || record.originalThreadId !== record.nativeTaskId)
157
+ || record.ownedOriginal != null && (typeof record.ownedOriginal !== "boolean" || !record.originalThreadId)
158
+ || !record.deliveries || typeof record.deliveries !== "object" || Array.isArray(record.deliveries)) throw fail("trae_native_state_invalid");
159
+ for (const [message, delivery] of Object.entries(record.deliveries)) {
160
+ if (!HASH.test(message) || !delivery || !DELIVERY.has(delivery.status)
161
+ || delivery.nativeTaskId != null && !ID.test(delivery.nativeTaskId)
162
+ || !Number.isSafeInteger(delivery.assistantBaseline) || delivery.assistantBaseline < 0) throw fail("trae_native_state_invalid");
163
+ }
164
+ }
165
+ } catch (error) { if (error?.code !== "ENOENT") throw error; }
166
+ return { records, async save() {
167
+ await safeParents(directory);
168
+ const temporary = `${filename}.${crypto.randomUUID()}.tmp`;
169
+ try {
170
+ await fs.writeFile(temporary, JSON.stringify(records), { flag: "wx", mode: 0o600 });
171
+ await fs.rename(temporary, filename);
172
+ } finally { await fs.rm(temporary, { force: true }).catch(() => {}); }
173
+ } };
174
+ }
175
+
176
+ async function safeParents(target) {
177
+ const resolved = path.resolve(target), root = path.parse(resolved).root;
178
+ let cursor = root;
179
+ for (const part of path.relative(root, resolved).split(path.sep).filter(Boolean)) {
180
+ cursor = path.join(cursor, part);
181
+ const stat = await fs.lstat(cursor);
182
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw fail("trae_native_state_invalid");
183
+ }
184
+ }
185
+ function safeCode(error, fallback) { return /^[A-Za-z][A-Za-z0-9_]{1,119}$/.test(error?.code || "") ? error.code : fallback; }
186
+
187
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
188
+ const [action, flag, storeRoot, endpointFlag, endpoint] = process.argv.slice(2);
189
+ const valid = flag === "--root" && typeof storeRoot === "string"
190
+ && (action === "disable" && endpointFlag === undefined || action === "enable" && (endpointFlag === undefined || endpointFlag === "--endpoint" && typeof endpoint === "string"));
191
+ if (!valid) {
192
+ process.stderr.write("usage: native-task-worker.mjs enable|disable --root ABSOLUTE_STORE_ROOT [--endpoint http://127.0.0.1:9338]\n");
193
+ process.exitCode = 2;
194
+ } else {
195
+ configureTraeNativeTaskWorker({ storeRoot, enabled: action === "enable", ...(endpoint ? { debugEndpoint: endpoint } : {}) })
196
+ .then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
197
+ .catch((error) => { process.stderr.write(`${JSON.stringify({ error: safeCode(error, "trae_native_config_failed") })}\n`); process.exitCode = 1; });
198
+ }
199
+ }