@hellolynk-ai/workbuddy-mcp 0.0.0-stage → 0.3.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/LICENSE +92 -0
  2. package/README.md +103 -2
  3. package/bin/hellolynk-client-setup.mjs +2748 -0
  4. package/bin/hellolynk-mcp-config-shape.mjs +23 -0
  5. package/clients/core/hellolynk-client-account.mjs +180 -0
  6. package/clients/core/hellolynk-client-core.mjs +770 -0
  7. package/clients/core/hellolynk-client-profile.mjs +191 -0
  8. package/clients/core/hellolynk-client-update-entry.mjs +31 -0
  9. package/clients/core/hellolynk-client-update-runtime.mjs +600 -0
  10. package/clients/core/hellolynk-client-update.mjs +453 -0
  11. package/clients/core/hellolynk-contact-request-e2ee.mjs +867 -0
  12. package/clients/core/hellolynk-device-enrollment.mjs +435 -0
  13. package/clients/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  14. package/clients/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  15. package/clients/core/hellolynk-host-auth.mjs +347 -0
  16. package/clients/core/hellolynk-host-protected-store.mjs +2721 -0
  17. package/clients/core/hellolynk-host-transport-client.mjs +946 -0
  18. package/clients/core/hellolynk-host.mjs +2374 -0
  19. package/clients/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  20. package/clients/core/hellolynk-human-relationship-controller.mjs +1234 -0
  21. package/clients/core/hellolynk-inbox-read-state.mjs +11 -0
  22. package/clients/core/hellolynk-indexed-message-store.mjs +1558 -0
  23. package/clients/core/hellolynk-instance-conversation.mjs +185 -0
  24. package/clients/core/hellolynk-local-contact-requests.mjs +1733 -0
  25. package/clients/core/hellolynk-local-contacts.mjs +1228 -0
  26. package/clients/core/hellolynk-local-data-migration.mjs +766 -0
  27. package/clients/core/hellolynk-local-data-protection.mjs +747 -0
  28. package/clients/core/hellolynk-local-identity-trust.mjs +274 -0
  29. package/clients/core/hellolynk-local-messaging.mjs +3043 -0
  30. package/clients/core/hellolynk-local-peer-transport.mjs +103 -0
  31. package/clients/core/hellolynk-local-process-lock.mjs +157 -0
  32. package/clients/core/hellolynk-model-inbox.mjs +230 -0
  33. package/clients/core/hellolynk-model-notices.cjs +60 -0
  34. package/clients/core/hellolynk-native-addressing-tools.cjs +231 -0
  35. package/clients/core/hellolynk-native-conversations.mjs +280 -0
  36. package/clients/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  37. package/clients/core/hellolynk-native-inbox-runtime.mjs +553 -0
  38. package/clients/core/hellolynk-native-notice.mjs +50 -0
  39. package/clients/core/hellolynk-native-reply-routes.mjs +96 -0
  40. package/clients/core/hellolynk-native-task-index.cjs +305 -0
  41. package/clients/core/hellolynk-native-worker-lease.mjs +84 -0
  42. package/clients/core/hellolynk-neutral-host-assembly.mjs +955 -0
  43. package/clients/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  44. package/clients/core/hellolynk-neutral-host-launcher.mjs +665 -0
  45. package/clients/core/hellolynk-neutral-host-process.mjs +4374 -0
  46. package/clients/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  47. package/clients/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  48. package/clients/core/hellolynk-platform-credential-store.mjs +773 -0
  49. package/clients/core/hellolynk-relay-client-cli.mjs +232 -0
  50. package/clients/core/hellolynk-release-retention.mjs +277 -0
  51. package/clients/core/hellolynk-runtime-safety.cjs +471 -0
  52. package/clients/core/hellolynk-workbench-action-grants.mjs +457 -0
  53. package/clients/core/hellolynk-workbench-connection.mjs +234 -0
  54. package/clients/platform/windows/credential-store.ps1 +844 -0
  55. package/clients/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  56. package/clients/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  57. package/clients/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  58. package/clients/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  59. package/clients/workbench/contract.mjs +1314 -0
  60. package/clients/workbench/service.mjs +485 -0
  61. package/hellolynk-client-pack-manifest.json +884 -0
  62. package/hellolynk-client-pack-pin.json +4 -0
  63. package/integrations/workbuddy/package.json +15 -0
  64. package/integrations/workbuddy/server/host-lifecycle.mjs +249 -0
  65. package/integrations/workbuddy/server/mcp-proxy.mjs +258 -0
  66. package/integrations/workbuddy/server/mcp-server.mjs +357 -0
  67. package/integrations/workbuddy/server/model-call-origin.mjs +87 -0
  68. package/integrations/workbuddy/server/native-task-cdp.mjs +175 -0
  69. package/integrations/workbuddy/server/native-task-debug-startup.mjs +117 -0
  70. package/integrations/workbuddy/server/native-task-debug-startup.ps1 +40 -0
  71. package/integrations/workbuddy/server/native-task-mcp-registration.mjs +30 -0
  72. package/integrations/workbuddy/server/native-task-service-registration.mjs +115 -0
  73. package/integrations/workbuddy/server/native-task-worker.mjs +127 -0
  74. package/integrations/workbuddy/server/neutral-host-adapter.mjs +639 -0
  75. package/integrations/workbuddy/server/origin-hook.mjs +111 -0
  76. package/integrations/workbuddy/server/origin-routes.mjs +211 -0
  77. package/integrations/workbuddy/server/package-bootstrap.mjs +111 -0
  78. package/integrations/workbuddy/server/workbench-candidate.mjs +202 -0
  79. package/integrations/workbuddy/server/workbench-hidden-launcher.vbs +46 -0
  80. package/integrations/workbuddy/server/workbench-process.mjs +373 -0
  81. package/integrations/workbuddy/server/workbench-task.ps1 +196 -0
  82. package/integrations/workbuddy/skills/hellolynk-inbox/SKILL.md +9 -0
  83. package/integrations/workbuddy/skills/hellolynk-inbox/workbuddy.json +6 -0
  84. package/package.json +65 -4
  85. package/runtime/connect-activate.mjs +837 -0
  86. package/runtime/core/hellolynk-agent-configurator.mjs +701 -0
  87. package/runtime/core/hellolynk-client-account.mjs +180 -0
  88. package/runtime/core/hellolynk-client-core.mjs +770 -0
  89. package/runtime/core/hellolynk-client-profile.mjs +191 -0
  90. package/runtime/core/hellolynk-client-update-entry.mjs +31 -0
  91. package/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
  92. package/runtime/core/hellolynk-client-update.mjs +453 -0
  93. package/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
  94. package/runtime/core/hellolynk-device-enrollment.mjs +435 -0
  95. package/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  96. package/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  97. package/runtime/core/hellolynk-host-auth.mjs +347 -0
  98. package/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
  99. package/runtime/core/hellolynk-host-transport-client.mjs +946 -0
  100. package/runtime/core/hellolynk-host.mjs +2374 -0
  101. package/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  102. package/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
  103. package/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
  104. package/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
  105. package/runtime/core/hellolynk-instance-conversation.mjs +185 -0
  106. package/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
  107. package/runtime/core/hellolynk-local-contacts.mjs +1228 -0
  108. package/runtime/core/hellolynk-local-data-migration.mjs +766 -0
  109. package/runtime/core/hellolynk-local-data-protection.mjs +747 -0
  110. package/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
  111. package/runtime/core/hellolynk-local-messaging.mjs +3043 -0
  112. package/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
  113. package/runtime/core/hellolynk-local-process-lock.mjs +157 -0
  114. package/runtime/core/hellolynk-model-inbox.mjs +230 -0
  115. package/runtime/core/hellolynk-model-notices.cjs +60 -0
  116. package/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
  117. package/runtime/core/hellolynk-native-conversations.mjs +280 -0
  118. package/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  119. package/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
  120. package/runtime/core/hellolynk-native-notice.mjs +50 -0
  121. package/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
  122. package/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
  123. package/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
  124. package/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  125. package/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
  126. package/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
  127. package/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  128. package/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  129. package/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
  130. package/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
  131. package/runtime/core/hellolynk-release-retention.mjs +277 -0
  132. package/runtime/core/hellolynk-runtime-safety.cjs +471 -0
  133. package/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
  134. package/runtime/core/hellolynk-workbench-connection.mjs +234 -0
  135. package/runtime/neutral-host-bootstrap.mjs +57 -0
  136. package/runtime/neutral-host-release-entry.mjs +81 -0
  137. package/runtime/neutral-host-release-loader.mjs +156 -0
  138. package/runtime/neutral-host-release-manifest.json +302 -0
  139. package/runtime/neutral-host-release-pin.json +4 -0
  140. package/runtime/platform/windows/configure-agent.ps1 +281 -0
  141. package/runtime/platform/windows/credential-store.ps1 +844 -0
  142. package/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  143. package/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  144. package/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  145. package/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  146. package/runtime/workbench/candidate-bootstrap.mjs +54 -0
  147. package/runtime/workbench/contract.mjs +1314 -0
  148. package/runtime/workbench/hellolynk-workbench-build.json +215 -0
  149. package/runtime/workbench/host-capabilities.mjs +80 -0
  150. package/runtime/workbench/i18n.mjs +1127 -0
  151. package/runtime/workbench/loopback-surface.mjs +740 -0
  152. package/runtime/workbench/mcp-app-bundle.mjs +97 -0
  153. package/runtime/workbench/service.mjs +485 -0
  154. package/runtime/workbench/state.mjs +421 -0
  155. package/runtime/workbench/transports.mjs +171 -0
  156. package/runtime/workbench/ui/app.mjs +6120 -0
  157. package/runtime/workbench/ui/connection-form.mjs +155 -0
  158. package/runtime/workbench/ui/hellolynk-mark.png +0 -0
  159. package/runtime/workbench/ui/http-transport.mjs +247 -0
  160. package/runtime/workbench/ui/icons/arrow-left.svg +3 -0
  161. package/runtime/workbench/ui/icons/close.svg +3 -0
  162. package/runtime/workbench/ui/icons/contacts.svg +3 -0
  163. package/runtime/workbench/ui/icons/inbox.svg +3 -0
  164. package/runtime/workbench/ui/icons/refresh.svg +3 -0
  165. package/runtime/workbench/ui/icons/search.svg +3 -0
  166. package/runtime/workbench/ui/icons/security.svg +3 -0
  167. package/runtime/workbench/ui/icons/settings.svg +3 -0
  168. package/runtime/workbench/ui/index.html +139 -0
  169. package/runtime/workbench/ui/loopback-entry.mjs +26 -0
  170. package/runtime/workbench/ui/mcp-app-client.mjs +151 -0
  171. package/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
  172. package/runtime/workbench/ui/styles.css +686 -0
  173. package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
  174. package/tools/hellolynk-install-client-skills.mjs +1211 -0
  175. package/workbuddy-package-manifest.json +1 -0
  176. package/workbuddy-package-pin.json +1 -0
@@ -0,0 +1,46 @@
1
+ Option Explicit
2
+
3
+ If WScript.Arguments.Count <> 5 And WScript.Arguments.Count <> 7 Then WScript.Quit 2
4
+
5
+ Dim shell, fso, command, exitCode, nodePath, entryPath, rootPath, manifest, storeRoot, expectedLauncher, expectedEntry, serviceMode, debugEndpoint, endpointPattern
6
+ Set shell = CreateObject("WScript.Shell")
7
+ Set fso = CreateObject("Scripting.FileSystemObject")
8
+
9
+ Function QuoteArgument(value)
10
+ QuoteArgument = Chr(34) & Replace(value, Chr(34), Chr(34) & Chr(34)) & Chr(34)
11
+ End Function
12
+
13
+ Function IsSha256(value)
14
+ Dim index, valueChar
15
+ IsSha256 = Len(value) = 64
16
+ For index = 1 To Len(value)
17
+ valueChar = Mid(value, index, 1)
18
+ If InStr("0123456789abcdef", valueChar) = 0 Then IsSha256 = False
19
+ Next
20
+ End Function
21
+
22
+ nodePath = WScript.Arguments(0)
23
+ entryPath = WScript.Arguments(1)
24
+ rootPath = WScript.Arguments(2)
25
+ manifest = WScript.Arguments(3)
26
+ storeRoot = WScript.Arguments(4)
27
+ If StrComp(fso.GetAbsolutePathName(rootPath), rootPath, 1) <> 0 Then WScript.Quit 3
28
+ If StrComp(fso.GetAbsolutePathName(storeRoot), storeRoot, 1) <> 0 Or LCase(fso.GetFileName(storeRoot)) <> "host" Then WScript.Quit 3
29
+ expectedLauncher = fso.BuildPath(rootPath, "integrations\workbuddy\server\workbench-hidden-launcher.vbs")
30
+ expectedEntry = fso.BuildPath(rootPath, "integrations\workbuddy\server\package-bootstrap.mjs")
31
+ If StrComp(fso.GetAbsolutePathName(WScript.ScriptFullName), expectedLauncher, 1) <> 0 Then WScript.Quit 3
32
+ If StrComp(fso.GetAbsolutePathName(nodePath), nodePath, 1) <> 0 Or StrComp(fso.GetAbsolutePathName(entryPath), expectedEntry, 1) <> 0 Or Not fso.FileExists(entryPath) Or Not fso.FileExists(nodePath) Or Not IsSha256(manifest) Then WScript.Quit 3
33
+ shell.Environment("Process")("HELLOLYNK_NEUTRAL_STORE_ROOT") = storeRoot
34
+ serviceMode = "workbench-service"
35
+ If WScript.Arguments.Count = 7 Then
36
+ serviceMode = WScript.Arguments(5)
37
+ debugEndpoint = WScript.Arguments(6)
38
+ Set endpointPattern = New RegExp
39
+ endpointPattern.Pattern = "^http://127\.0\.0\.1:[0-9]{1,5}$"
40
+ If serviceMode <> "native-task-service" Or Not endpointPattern.Test(debugEndpoint) Then WScript.Quit 3
41
+ shell.Environment("Process")("HELLOLYNK_WORKBUDDY_DEBUG_ENDPOINT") = debugEndpoint
42
+ End If
43
+ command = QuoteArgument(nodePath) & " " & QuoteArgument(entryPath) & " " & serviceMode & " --root " & QuoteArgument(rootPath) & _
44
+ " --expected-manifest-sha256 " & manifest
45
+ exitCode = shell.Run(command, 0, True)
46
+ WScript.Quit exitCode
@@ -0,0 +1,373 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import net from "node:net";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { promisify } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+ import runtimeSafety from "../../../clients/core/hellolynk-runtime-safety.cjs";
9
+ import { defaultNeutralHostStoreRoot } from "../../../clients/core/hellolynk-neutral-host-installation-store.mjs";
10
+ import { bootstrapWorkBuddyWorkbenchCandidate } from "./workbench-candidate.mjs";
11
+ import { registerWorkBuddyNativeMcp } from "./native-task-mcp-registration.mjs";
12
+ import { configureWorkBuddyDebugStartup, transferWorkBuddyDebugStartupOwner } from "./native-task-debug-startup.mjs";
13
+ import { withWorkBuddyNativeServiceRecord } from "./native-task-service-registration.mjs";
14
+ import { removeWorkBuddyOriginHook, workBuddyOriginHookCommand, workBuddyOriginInbox } from "./origin-routes.mjs";
15
+ import { loadPinnedWorkBuddyProxy } from "./package-bootstrap.mjs";
16
+
17
+ const exec = promisify(execFile);
18
+ const STARTUP_TIMEOUT_MS = 90_000;
19
+
20
+ // WorkBuddy may replace its conversation worker after a normal turn. Windows
21
+ // uses the installed on-demand page service; other platforms use a detached
22
+ // handoff. Both run only the pinned package.
23
+ export async function launchDetachedWorkBuddyWorkbench(options) {
24
+ const args = bootstrapArgs("handoff-workbench", options);
25
+ if ((options.platform || process.platform) === "win32") return launchScheduledWorkbench(options);
26
+ const executable = await runtimeSafety.resolveHelloLynkStandaloneNode({ preferredExecutable: options.nodePath });
27
+ let stdout;
28
+ try {
29
+ ({ stdout } = await (options.exec || exec)(executable, args, {
30
+ cwd: options.root,
31
+ env: runtimeSafety.standaloneHelloLynkNodeEnvironment(process.env),
32
+ windowsHide: true,
33
+ timeout: STARTUP_TIMEOUT_MS + 5_000,
34
+ maxBuffer: 4096,
35
+ }));
36
+ } catch (error) {
37
+ // Only bounded, known diagnostic codes cross the subprocess boundary.
38
+ // Never return the command line, stderr or native exception to the client.
39
+ if (error?.killed === true && error?.signal === "SIGTERM"
40
+ && error?.code !== "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
41
+ throw failure("workbuddy_local_web_launch_timeout");
42
+ }
43
+ let reported;
44
+ try { reported = JSON.parse(String(error?.stderr || "").slice(0, 4096)); } catch {}
45
+ const known = ["workbuddy_local_web_launch_timeout", "workbuddy_local_web_launch_failed",
46
+ "workbuddy_local_web_launch_invalid", "workbuddy_local_web_launch_handoff_missing"];
47
+ throw failure(reported?.ok === false && known.includes(reported.error)
48
+ ? reported.error : "workbuddy_local_web_launch_failed");
49
+ }
50
+ try { return validateReady(JSON.parse(stdout)); }
51
+ catch { throw failure("workbuddy_local_web_launch_invalid"); }
52
+ }
53
+
54
+ // Installed once by setup. The on-demand task owns the page, so a client's
55
+ // Windows Job cleanup cannot terminate an active browser session.
56
+ export async function configureWorkBuddyWorkbenchTask(options, operation = "Install") {
57
+ bootstrapArgs("workbench-service", options);
58
+ if (!["Install", "Verify", "Start", "Uninstall"].includes(operation)) throw failure("workbuddy_page_task_operation_invalid");
59
+ try {
60
+ const driverRoot = operation === "Verify" ? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..") : options.root;
61
+ const { stdout } = await exec("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File",
62
+ path.join(driverRoot, "integrations/workbuddy/server/workbench-task.ps1"),
63
+ "-Command", operation, "-Root", options.root, "-Manifest", options.expectedManifestSha256,
64
+ "-DriverRoot", driverRoot,
65
+ "-NodePath", options.nodePath || process.execPath,
66
+ "-StoreRoot", options.storeRoot || defaultNeutralHostStoreRoot()], {
67
+ windowsHide: true, timeout: 30_000, maxBuffer: 4096,
68
+ env: runtimeSafety.sanitizeHelloLynkChildEnvironment(process.env),
69
+ });
70
+ const result = JSON.parse(stdout);
71
+ if (result?.ok !== true) throw failure("workbuddy_page_task_failed");
72
+ return result;
73
+ } catch (error) {
74
+ const code = String(error?.stderr || "").trim();
75
+ throw failure(/^workbuddy_[a-z_]+$/.test(code) ? code : "workbuddy_page_task_failed");
76
+ }
77
+ }
78
+
79
+ // Uses the same installed, hidden launcher and pinned verifier as the page
80
+ // service, with a separate task identity and explicit loopback CDP opt-in.
81
+ export async function configureWorkBuddyNativeTaskService(options, operation = "Install") {
82
+ if (!["Install", "Uninstall"].includes(operation)) return configureNativeTaskService(options, operation);
83
+ bootstrapArgs("native-task-service", options);
84
+ const endpoint = new URL(options.debugEndpoint || "");
85
+ if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || endpoint.username || endpoint.password) throw failure("workbuddy_native_debug_endpoint_invalid");
86
+ // MCP connection can initialize a new proxy which reads the service record.
87
+ // Complete that handshake before taking the record/Start lock.
88
+ const debugStartup = operation === "Install" ? await prepareNativeTaskServiceInstall(options, endpoint) : undefined;
89
+ return withWorkBuddyNativeServiceRecord(options.storeRoot, async registry => {
90
+ if (!registry && operation === "Install") throw failure("workbuddy_native_service_store_missing");
91
+ // Withdraw before stopping, under the same lock used by MCP Verify/Start.
92
+ if (operation === "Uninstall") await registry?.removeOwned(options);
93
+ const result = await configureNativeTaskService(options, operation, debugStartup);
94
+ if (operation === "Install") await registry.save(options, result.taskName);
95
+ // Without the service nobody imports hook records; leave WorkBuddy's
96
+ // other hooks and settings untouched.
97
+ if (operation === "Uninstall") result.originHook = await removeWorkBuddyOriginHook({ settingsPath: options.workBuddySettingsPath,
98
+ command: workBuddyOriginHookCommand({ nodePath: options.nodePath || process.execPath,
99
+ hookPath: path.join(options.root, "integrations/workbuddy/server/origin-hook.mjs"),
100
+ inbox: workBuddyOriginInbox(options.storeRoot || defaultNeutralHostStoreRoot()) }) })
101
+ .catch(() => ({ changed: false, status: "unavailable" }));
102
+ return result;
103
+ });
104
+ }
105
+
106
+ // An ordinary package upgrade must carry forward an existing listener even
107
+ // while WorkBuddy is closed. First customer install explicitly enables it. The
108
+ // new release's MCP command needs WorkBuddy's trust again: leave one request,
109
+ // which that release's own service approves once WorkBuddy is open.
110
+ export async function updateRecordedWorkBuddyNativeTaskService(options, operation = "Install") {
111
+ if (!["Install", "Verify"].includes(operation)) throw failure("workbuddy_page_task_operation_invalid");
112
+ let pendingService = null;
113
+ const updated = await withWorkBuddyNativeServiceRecord(options.storeRoot, async registry => {
114
+ const record = await registry?.read();
115
+ if (!record) {
116
+ if (operation !== "Install" || options.installIfMissing !== true) return { configured: false, status: "not_installed" };
117
+ if (!registry) throw failure("workbuddy_native_service_store_missing");
118
+ const target = { ...options, nodePath: options.nodePath || process.execPath };
119
+ await loadPinnedWorkBuddyProxy(target);
120
+ const startupOptions = { storeRoot: registry.storeRoot, debugEndpoint: target.debugEndpoint,
121
+ installationId: nativeDebugStartupOwner(target), enableDebugStartup: true,
122
+ ...(options.debugStartupEnvironment ? { environment: options.debugStartupEnvironment } : {}) };
123
+ const debugStartup = await configureWorkBuddyDebugStartup(startupOptions, "Install");
124
+ target.debugEndpoint = debugStartup.debugEndpoint;
125
+ try {
126
+ const result = await configureNativeTaskService(target, "Install", debugStartup);
127
+ // Trust is completed by this receiver after the client reopens. Setup
128
+ // does not require or attach to a running WorkBuddy window.
129
+ await registry.save({ ...target, pendingMcpTrust: { command: target.nodePath } }, result.taskName);
130
+ pendingService = target;
131
+ return { ...result, configured: true, restartRequired: true };
132
+ } catch (error) {
133
+ let rollbackError;
134
+ // Each cleanup is independent: a Scheduler failure must not leave our
135
+ // newly enabled startup setting behind. Preserve any user change.
136
+ for (const undo of [() => registry.removeOwned(target),
137
+ () => configureNativeTaskService({ ...target, debugStartupEnvironment: options.debugStartupEnvironment }, "Uninstall"),
138
+ () => configureWorkBuddyDebugStartup(startupOptions, "Uninstall")]) {
139
+ try { await undo(); } catch (cause) { rollbackError ||= cause; }
140
+ }
141
+ if (rollbackError) {
142
+ throw Object.assign(failure("local_configuration_outcome_uncertain"), { cause: rollbackError, configurationCause: error });
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+ const target = { ...options, nodePath: record.nodePath, debugEndpoint: record.debugEndpoint };
148
+ const sameRelease = path.resolve(record.root).toLowerCase() === path.resolve(target.root).toLowerCase()
149
+ && record.expectedManifestSha256 === target.expectedManifestSha256;
150
+ if (operation === "Verify" && !sameRelease) throw failure("workbuddy_native_service_record_mismatch");
151
+ await loadPinnedWorkBuddyProxy(target);
152
+ const result = await configureNativeTaskService(target, operation);
153
+ if (operation === "Install") {
154
+ await transferWorkBuddyDebugStartupOwner({ storeRoot: registry.storeRoot,
155
+ previousInstallationId: nativeDebugStartupOwner(record), installationId: nativeDebugStartupOwner(target) });
156
+ // The installer wrote mcp.json with this process's node.
157
+ const pendingMcpTrust = sameRelease ? record.pendingMcpTrust : { command: process.execPath };
158
+ await registry.save({ ...target, pendingMcpTrust }, result.taskName);
159
+ if (pendingMcpTrust) pendingService = target;
160
+ }
161
+ return { ...result, configured: true };
162
+ });
163
+ // After the lock: the MCP that approval connects takes it to Verify/Start.
164
+ if (pendingService) updated.serviceStarted = await (options.startNativeService || (target => configureNativeTaskService(target, "Start")))(pendingService)
165
+ .then(() => true, () => false);
166
+ return updated;
167
+ }
168
+
169
+ async function prepareNativeTaskServiceInstall(options, endpoint) {
170
+ // Explicit opt-in only. Persist ownership before configuring the task so a
171
+ // later repair/uninstall can recover a partially completed installation.
172
+ const debugStartup = await configureWorkBuddyDebugStartup({
173
+ storeRoot: options.storeRoot || defaultNeutralHostStoreRoot(), debugEndpoint: endpoint.origin,
174
+ installationId: nativeDebugStartupOwner(options),
175
+ enableDebugStartup: options.enableDebugStartup === true,
176
+ }, "Install");
177
+ await registerWorkBuddyNativeMcp({
178
+ debugEndpoint: endpoint.origin,
179
+ command: options.nodePath || process.execPath,
180
+ releaseRoot: options.mcpRegistration?.releaseRoot || options.root,
181
+ expectedManifestSha256: options.mcpRegistration?.expectedManifestSha256 || options.expectedManifestSha256,
182
+ storeRoot: options.storeRoot || defaultNeutralHostStoreRoot(),
183
+ });
184
+ return debugStartup;
185
+ }
186
+
187
+ async function configureNativeTaskService(options, operation, debugStartup) {
188
+ bootstrapArgs("native-task-service", options);
189
+ const endpoint = new URL(options.debugEndpoint || "");
190
+ if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || endpoint.username || endpoint.password) throw failure("workbuddy_native_debug_endpoint_invalid");
191
+ if (!["Install", "Verify", "Start", "Uninstall"].includes(operation)) throw failure("workbuddy_page_task_operation_invalid");
192
+ try {
193
+ const { stdout } = await exec("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File",
194
+ path.join(options.root, "integrations/workbuddy/server/workbench-task.ps1"), "-Command", operation,
195
+ "-Root", options.root, "-Manifest", options.expectedManifestSha256, "-NodePath", options.nodePath || process.execPath,
196
+ "-StoreRoot", options.storeRoot || defaultNeutralHostStoreRoot(), "-Mode", "native-task-service", "-DebugEndpoint", endpoint.origin], {
197
+ windowsHide: true, timeout: 30_000, maxBuffer: 4096, env: runtimeSafety.sanitizeHelloLynkChildEnvironment(process.env),
198
+ });
199
+ const result = JSON.parse(stdout); if (result?.ok !== true) throw failure("workbuddy_page_task_failed");
200
+ if (operation === "Uninstall") result.debugStartup = await configureWorkBuddyDebugStartup({
201
+ storeRoot: options.storeRoot || defaultNeutralHostStoreRoot(), debugEndpoint: endpoint.origin,
202
+ installationId: nativeDebugStartupOwner(options),
203
+ ...(options.debugStartupEnvironment ? { environment: options.debugStartupEnvironment } : {}),
204
+ }, operation);
205
+ else if (debugStartup) result.debugStartup = debugStartup;
206
+ return result;
207
+ } catch { throw failure("workbuddy_native_service_configuration_failed"); }
208
+ }
209
+
210
+ function nativeDebugStartupOwner(options) {
211
+ return crypto.createHash("sha256").update(`${path.resolve(options.root).toLowerCase()}|${options.expectedManifestSha256}`).digest("hex");
212
+ }
213
+
214
+ function controlPipe(options) {
215
+ const key = crypto.createHash("sha256").update(`${options.root.toLowerCase()}|${options.expectedManifestSha256}`).digest("hex");
216
+ return `\\\\.\\pipe\\hellolynk-workbuddy-${key}`;
217
+ }
218
+
219
+ async function requestExistingWorkbench(options) {
220
+ return new Promise((resolve, reject) => {
221
+ const socket = net.connect(controlPipe(options));
222
+ let text = "";
223
+ socket.setTimeout(3000, () => socket.destroy(failure("workbuddy_page_service_timeout")));
224
+ socket.on("error", reject);
225
+ socket.on("connect", () => socket.write("open\n"));
226
+ socket.on("data", (chunk) => {
227
+ text += chunk.toString("utf8");
228
+ if (text.length > 4096) socket.destroy(failure("workbuddy_local_web_launch_invalid"));
229
+ });
230
+ socket.on("end", () => {
231
+ try { resolve(validateReady(JSON.parse(text))); }
232
+ catch { reject(failure("workbuddy_local_web_launch_invalid")); }
233
+ });
234
+ });
235
+ }
236
+
237
+ async function launchScheduledWorkbench(options) {
238
+ try { return await requestExistingWorkbench(options); }
239
+ catch (error) { if (!["ENOENT", "ECONNREFUSED"].includes(error?.code)) throw error; }
240
+ await configureWorkBuddyWorkbenchTask(options, "Start");
241
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
242
+ do {
243
+ await delay(200);
244
+ try { return await requestExistingWorkbench(options); }
245
+ catch (error) { if (!["ENOENT", "ECONNREFUSED"].includes(error?.code)) throw error; }
246
+ } while (Date.now() < deadline);
247
+ throw failure("workbuddy_local_web_launch_timeout");
248
+ }
249
+
250
+ export async function serveWorkBuddyWorkbenchService(options) {
251
+ let server;
252
+ const sockets = new Set();
253
+ try {
254
+ await serveWorkBuddyWorkbench({ ...options, async publishReady(value, surface) {
255
+ server = net.createServer((socket) => {
256
+ sockets.add(socket);
257
+ socket.on("close", () => sockets.delete(socket));
258
+ socket.on("error", () => {});
259
+ socket.setTimeout(3000, () => socket.destroy());
260
+ let text = "";
261
+ socket.on("data", (chunk) => {
262
+ text += chunk.toString("utf8");
263
+ if (text === "open\n") { surface.armNavigation(); socket.end(`${JSON.stringify(validateReady(value))}\n`); }
264
+ else if (text.length >= 5) socket.destroy();
265
+ });
266
+ });
267
+ await new Promise((resolve, reject) => {
268
+ server.once("error", reject);
269
+ server.listen(controlPipe(options), () => { server.off("error", reject); resolve(); });
270
+ });
271
+ } });
272
+ } finally {
273
+ for (const socket of sockets) socket.destroy();
274
+ if (server?.listening) await new Promise((resolve) => server.close(resolve));
275
+ }
276
+ }
277
+
278
+ export async function handoffWorkBuddyWorkbench(options) {
279
+ const executable = await runtimeSafety.resolveHelloLynkStandaloneNode();
280
+ const child = spawn(executable, bootstrapArgs("workbench", options), {
281
+ cwd: options.root,
282
+ env: runtimeSafety.standaloneHelloLynkNodeEnvironment(process.env),
283
+ detached: true,
284
+ windowsHide: true,
285
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
286
+ });
287
+ try {
288
+ const ready = await new Promise((resolve, reject) => {
289
+ const timer = setTimeout(() => done(failure("workbuddy_local_web_launch_timeout")), STARTUP_TIMEOUT_MS);
290
+ const failed = () => done(failure("workbuddy_local_web_launch_failed"));
291
+ const message = (value) => {
292
+ try { done(null, validateReady(value)); }
293
+ catch { failed(); }
294
+ };
295
+ const done = (error, value) => {
296
+ clearTimeout(timer);
297
+ child.off("message", message);
298
+ child.off("error", failed);
299
+ child.off("exit", failed);
300
+ if (error) reject(error); else resolve(value);
301
+ };
302
+ child.once("error", failed);
303
+ child.once("exit", failed);
304
+ child.once("message", message);
305
+ });
306
+ child.disconnect();
307
+ child.unref();
308
+ return ready;
309
+ } catch (error) {
310
+ child.kill();
311
+ throw error;
312
+ }
313
+ }
314
+
315
+ export async function serveWorkBuddyWorkbench(options) {
316
+ const { adapter, signal, publishReady = sendReady } = options;
317
+ let page;
318
+ try {
319
+ page = await bootstrapWorkBuddyWorkbenchCandidate({
320
+ adapter, pagePath: "/inbox",
321
+ ...(options.bootstrapWorkbenchCandidate ? {
322
+ bootstrapWorkbenchCandidate: options.bootstrapWorkbenchCandidate,
323
+ workbenchContract: options.workbenchContract,
324
+ } : {}),
325
+ });
326
+ if (signal?.aborted) return;
327
+ await publishReady({ cleanUrl: page.cleanUrl }, page.surface);
328
+ while (!signal?.aborted) {
329
+ const state = page.surface.status();
330
+ // Reuse the existing navigation and session expiry rules. Keeping a
331
+ // browser page alive does not grant any extra Host or message access.
332
+ if (!state.navigationArmed && state.liveSessionCount === 0) return;
333
+ try { await delay(options.checkIntervalMs ?? 1000, undefined, { signal }); }
334
+ catch (error) { if (!signal?.aborted) throw error; }
335
+ }
336
+ } finally {
337
+ try { await page?.close(); }
338
+ finally { await adapter.close(); }
339
+ }
340
+ }
341
+
342
+ function bootstrapArgs(mode, options) {
343
+ if (!path.isAbsolute(options?.root || "")
344
+ || path.resolve(options.root) !== options.root
345
+ || !/^[a-f0-9]{64}$/.test(options?.expectedManifestSha256 || "")) {
346
+ throw failure("workbuddy_local_web_launch_configuration_invalid");
347
+ }
348
+ return [path.join(options.root, "integrations/workbuddy/server/package-bootstrap.mjs"),
349
+ mode, "--root", options.root, "--expected-manifest-sha256", options.expectedManifestSha256];
350
+ }
351
+
352
+ function validateReady(value) {
353
+ if (!value || typeof value !== "object" || Array.isArray(value)
354
+ || Object.keys(value).length !== 1 || typeof value.cleanUrl !== "string"
355
+ || !/^http:\/\/127\.0\.0\.1:\d{1,5}\/inbox$/.test(value.cleanUrl)) {
356
+ throw failure("workbuddy_local_web_launch_invalid");
357
+ }
358
+ const port = Number(new URL(value.cleanUrl).port);
359
+ if (port < 1 || port > 65535) throw failure("workbuddy_local_web_launch_invalid");
360
+ return Object.freeze({ cleanUrl: value.cleanUrl });
361
+ }
362
+
363
+ function sendReady(value) {
364
+ return new Promise((resolve, reject) => {
365
+ if (!process.connected || typeof process.send !== "function") {
366
+ reject(failure("workbuddy_local_web_launch_handoff_missing"));
367
+ return;
368
+ }
369
+ process.send(validateReady(value), (error) => error ? reject(error) : resolve());
370
+ });
371
+ }
372
+
373
+ function failure(code) { return Object.assign(new Error(code), { code }); }
@@ -0,0 +1,196 @@
1
+ param(
2
+ [Parameter(Mandatory=$true)][ValidateSet('Install','Verify','Start','Run','Uninstall')][string]$Command,
3
+ [Parameter(Mandatory=$true)][string]$Root,
4
+ [Parameter(Mandatory=$true)][ValidatePattern('^[a-f0-9]{64}$')][string]$Manifest,
5
+ [Parameter(Mandatory=$true)][string]$NodePath,
6
+ [Parameter(Mandatory=$true)][string]$StoreRoot,
7
+ [string]$DriverRoot = '',
8
+ [ValidateSet('workbench-service','native-task-service')][string]$Mode = 'workbench-service',
9
+ [string]$DebugEndpoint = ''
10
+ )
11
+ $ErrorActionPreference = 'Stop'
12
+ try {
13
+ $nativeService = $Mode -eq 'native-task-service'
14
+ if (-not $DriverRoot) { $DriverRoot = $Root }
15
+ # A new, verified installer may check an older immutable release with its
16
+ # current task driver. It may not mutate that release through this path.
17
+ if ($DriverRoot -ne $Root -and $Command -ne 'Verify') { throw 'workbuddy_page_task_path_invalid' }
18
+ if ($nativeService -and ($DebugEndpoint -notmatch '^http://127\.0\.0\.1:[0-9]{1,5}$' -or ([uri]$DebugEndpoint).Port -lt 1)) { throw 'workbuddy_native_debug_endpoint_invalid' }
19
+ if (-not $nativeService -and $DebugEndpoint) { throw 'workbuddy_native_debug_endpoint_invalid' }
20
+ if (-not [IO.Path]::IsPathRooted($Root) -or $Root -match '["\r\n]' -or
21
+ -not [IO.Path]::IsPathRooted($NodePath) -or $NodePath -match '["\r\n]' -or
22
+ -not [IO.Path]::IsPathRooted($StoreRoot) -or $StoreRoot -match '["\r\n]' -or
23
+ [IO.Path]::GetFileName($StoreRoot) -ne 'host') { throw 'workbuddy_page_task_path_invalid' }
24
+ $entry = Join-Path $Root 'integrations\workbuddy\server\package-bootstrap.mjs'
25
+ $script = Join-Path $DriverRoot 'integrations\workbuddy\server\workbench-task.ps1'
26
+ $launcher = Join-Path $Root 'integrations\workbuddy\server\workbench-hidden-launcher.vbs'
27
+ $wscript = Join-Path $env:SystemRoot 'System32\wscript.exe'
28
+ if (-not (Test-Path -LiteralPath $entry -PathType Leaf) -or -not (Test-Path -LiteralPath $launcher -PathType Leaf) -or
29
+ -not (Test-Path -LiteralPath $wscript -PathType Leaf) -or -not (Test-Path -LiteralPath $NodePath -PathType Leaf) -or
30
+ [IO.Path]::GetFullPath($PSCommandPath) -ne [IO.Path]::GetFullPath($script)) { throw 'workbuddy_page_task_entry_invalid' }
31
+ if ($Command -eq 'Run') {
32
+ $arguments = '//B //NoLogo "{0}" "{1}" "{2}" "{3}" {4} "{5}"' -f $launcher,$NodePath,$entry,$Root,$Manifest,$StoreRoot
33
+ if ($nativeService) { $arguments += ' native-task-service "{0}"' -f $DebugEndpoint }
34
+ $worker = Start-Process -FilePath $wscript -ArgumentList $arguments -WindowStyle Hidden -PassThru -Wait
35
+ exit $worker.ExitCode
36
+ }
37
+ $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
38
+ $namespaceRoot = [IO.Path]::GetDirectoryName($Root)
39
+ if ([IO.Path]::GetFileName($namespaceRoot) -ne 'releases') { $namespaceRoot = $Root }
40
+ $namespace = $sid + '|' + [IO.Path]::GetFullPath($namespaceRoot).ToLowerInvariant()
41
+ $hash = [Security.Cryptography.SHA256]::Create()
42
+ try {
43
+ $suffix = ([BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($namespace)))).Replace('-','').Substring(0,20)
44
+ $releaseIdentity = [IO.Path]::GetFullPath($Root).ToLowerInvariant() + '|' + $Manifest
45
+ $releaseSuffix = ([BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($releaseIdentity)))).Replace('-','').Substring(0,16)
46
+ }
47
+ finally { $hash.Dispose() }
48
+ $taskBase = "HelloLynk WorkBuddy - $suffix"
49
+ if ($nativeService) { $taskBase = "HelloLynk WorkBuddy Native - $suffix" }
50
+ $taskName = "$taskBase - $releaseSuffix"
51
+ $description = 'HelloLynk WorkBuddy page service; starts on demand and stops when browser sessions expire.'
52
+ if ($nativeService) { $description = 'HelloLynk WorkBuddy native inbox listener; explicit loopback desktop bridge; no model polling.' }
53
+ $executable = $wscript
54
+ $arguments = '//B //NoLogo "{0}" "{1}" "{2}" "{3}" {4} "{5}"' -f $launcher,$NodePath,$entry,$Root,$Manifest,$StoreRoot
55
+ if ($nativeService) { $arguments += ' native-task-service "{0}"' -f $DebugEndpoint }
56
+ # The native listener also resumes at this user's logon with normal rights,
57
+ # so an update's trust request survives a restart before WorkBuddy opens.
58
+ # One instance per release. The page task stays on demand.
59
+ $instances = 0; $installedTrigger = 'none'
60
+ if ($nativeService) { $instances = 2; $installedTrigger = 'logon' }
61
+ # Use the Scheduler API directly; its CIM wrapper can time out while the
62
+ # registered task and on-demand service are both healthy.
63
+ $scheduler = New-Object -ComObject 'Schedule.Service'; $scheduler.Connect()
64
+ $folder = $scheduler.GetFolder('\')
65
+ function Read-Task($registered) {
66
+ $d = $registered.Definition
67
+ [pscustomobject]@{ TaskName=$registered.Name; Description=$d.RegistrationInfo.Description; Principal=$d.Principal; Settings=$d.Settings
68
+ Triggers=@($d.Triggers); Actions=@($d.Actions | ForEach-Object { [pscustomobject]@{Execute=$_.Path; Arguments=$_.Arguments; WorkingDirectory=$_.WorkingDirectory; Type=$_.Type} })
69
+ State=$registered.State; Registered=$registered }
70
+ }
71
+ # 'none', 'logon' (this user's logon trigger), 'logon_disabled' or 'other'.
72
+ function Get-TriggerState($triggers) {
73
+ $list = @($triggers | Where-Object { $null -ne $_ })
74
+ if ($list.Count -eq 0) { return 'none' }
75
+ if (-not $nativeService -or $list.Count -ne 1 -or $list[0].Type -ne 9) { return 'other' }
76
+ $user = [string]$list[0].UserId
77
+ if ($user -notmatch '^S-1-') { $user = (New-Object Security.Principal.NTAccount($user)).Translate([Security.Principal.SecurityIdentifier]).Value }
78
+ if ($user -ne $sid) { return 'other' }
79
+ if ($list[0].Enabled) { return 'logon' }
80
+ return 'logon_disabled'
81
+ }
82
+ $task = $null
83
+ try { $task = Read-Task ($folder.GetTask($taskName)) }
84
+ catch { if ($_.Exception.HResult -ne -2147024894) { throw } }
85
+ if ($task) {
86
+ $taskSid = [string]$task.Principal.UserId
87
+ if ($taskSid -notmatch '^S-1-') { $taskSid = (New-Object Security.Principal.NTAccount($taskSid)).Translate([Security.Principal.SecurityIdentifier]).Value }
88
+ if ($task.Description -ne $description -or $taskSid -ne $sid) { throw 'workbuddy_page_task_conflict' }
89
+ }
90
+ $namespaceTasks = @($folder.GetTasks(1) | Where-Object { $_.Name -eq $taskBase -or $_.Name.StartsWith($taskBase + ' - ') } | ForEach-Object { Read-Task $_ })
91
+ foreach ($candidate in $namespaceTasks) {
92
+ $candidateSid = [string]$candidate.Principal.UserId
93
+ if ($candidateSid -notmatch '^S-1-') { $candidateSid = (New-Object Security.Principal.NTAccount($candidateSid)).Translate([Security.Principal.SecurityIdentifier]).Value }
94
+ if ($candidate.Description -ne $description -or $candidateSid -ne $sid) { throw 'workbuddy_page_task_conflict' }
95
+ }
96
+ function Stop-VerifiedNativeTaskProcesses($candidate) {
97
+ # Task Scheduler can stop wscript while leaving its shell.Run child alive.
98
+ # Match the entire verified native command, never a generic node process.
99
+ if (@($candidate.Actions).Count -ne 1 -or $candidate.Actions[0].Execute -ne $wscript) { throw 'workbuddy_native_service_identity_invalid' }
100
+ $pattern = '^//B //NoLogo "([^"\r\n]+)" "([^"\r\n]+)" "([^"\r\n]+)" "([^"\r\n]+)" ([a-f0-9]{64}) "([^"\r\n]+)" native-task-service "(http://127\.0\.0\.1:[0-9]{1,5})"$'
101
+ $match = [regex]::Match([string]$candidate.Actions[0].Arguments, $pattern)
102
+ if (-not $match.Success) { throw 'workbuddy_native_service_identity_invalid' }
103
+ $oldLauncher = $match.Groups[1].Value; $oldNode = $match.Groups[2].Value
104
+ $oldEntry = $match.Groups[3].Value; $oldRoot = $match.Groups[4].Value
105
+ $oldManifest = $match.Groups[5].Value; $oldStore = $match.Groups[6].Value
106
+ if (-not [IO.Path]::IsPathRooted($oldRoot) -or $oldNode -ne $NodePath -or $oldStore -ne $StoreRoot -or
107
+ $oldLauncher -ne (Join-Path $oldRoot 'integrations\workbuddy\server\workbench-hidden-launcher.vbs') -or
108
+ $oldEntry -ne (Join-Path $oldRoot 'integrations\workbuddy\server\package-bootstrap.mjs')) { throw 'workbuddy_native_service_identity_invalid' }
109
+ $oldNamespace = [IO.Path]::GetDirectoryName($oldRoot)
110
+ if ([IO.Path]::GetFileName($oldNamespace) -ne 'releases') { $oldNamespace = $oldRoot }
111
+ if ([IO.Path]::GetFullPath($oldNamespace) -ne [IO.Path]::GetFullPath($namespaceRoot)) { throw 'workbuddy_native_service_identity_invalid' }
112
+ $identityHash = [Security.Cryptography.SHA256]::Create()
113
+ try { $oldSuffix = ([BitConverter]::ToString($identityHash.ComputeHash([Text.Encoding]::UTF8.GetBytes([IO.Path]::GetFullPath($oldRoot).ToLowerInvariant() + '|' + $oldManifest)))).Replace('-','').Substring(0,16) }
114
+ finally { $identityHash.Dispose() }
115
+ if ($candidate.TaskName -ne "$taskBase - $oldSuffix") { throw 'workbuddy_native_service_identity_invalid' }
116
+ $nodeTail = '"{0}" native-task-service --root "{1}" --expected-manifest-sha256 {2}' -f $oldEntry,$oldRoot,$oldManifest
117
+ # WScript.Shell.Run can add a second space after the executable. Only
118
+ # that argv separator varies; all paths, mode and manifest remain exact.
119
+ $nodeCommandPattern = '^(?:"' + [regex]::Escape($oldNode) + '"|' + [regex]::Escape($oldNode) + ')[ \t]+' + [regex]::Escape($nodeTail) + '$'
120
+ $helperCommandPattern = '^(?:"' + [regex]::Escape($wscript) + '"|' + [regex]::Escape($wscript) + ')[ \t]+' + [regex]::Escape([string]$candidate.Actions[0].Arguments) + '$'
121
+ foreach ($observed in @(Get-CimInstance Win32_Process -Filter "Name = 'node.exe' OR Name = 'wscript.exe'")) {
122
+ $observedCommand = ([string]$observed.CommandLine).Trim()
123
+ if (-not (($observed.ExecutablePath -eq $oldNode -and $observedCommand -match $nodeCommandPattern) -or
124
+ ($observed.ExecutablePath -eq $wscript -and $observedCommand -match $helperCommandPattern))) { continue }
125
+ if ((Invoke-CimMethod -InputObject $observed -MethodName GetOwnerSid).Sid -ne $sid) { continue }
126
+ $process = $null
127
+ try {
128
+ $process = Get-Process -Id $observed.ProcessId -ErrorAction SilentlyContinue
129
+ if (-not $process) { continue }
130
+ $openedHandle = $process.Handle
131
+ $current = Get-CimInstance Win32_Process -Filter "ProcessId = $($observed.ProcessId)"
132
+ if (-not $current -or $current.CreationDate -ne $observed.CreationDate -or $current.CommandLine -ne $observed.CommandLine -or $current.ExecutablePath -ne $observed.ExecutablePath) { continue }
133
+ $process.Kill()
134
+ if (-not $process.WaitForExit(5000)) { throw 'workbuddy_native_service_stop_timeout' }
135
+ } finally { if ($process) { $process.Dispose() } }
136
+ }
137
+ }
138
+ if ($Command -eq 'Uninstall') {
139
+ foreach ($candidate in $namespaceTasks) {
140
+ # An obsolete release may be uninstalled after its replacement is active.
141
+ # Its manifest/root identity only owns this exact task, not the namespace.
142
+ if ($candidate.TaskName -ne $taskName) { continue }
143
+ if ($nativeService) { Stop-VerifiedNativeTaskProcesses $candidate }
144
+ $candidate.Registered.Stop(0)
145
+ $folder.DeleteTask($candidate.TaskName, 0)
146
+ }
147
+ } elseif ($Command -eq 'Install') {
148
+ if ($task -and @($task.Actions).Count -eq 1 -and $task.Actions[0].Type -eq 0 -and $task.Actions[0].Execute -eq $executable -and $task.Actions[0].Arguments -eq $arguments -and $task.Actions[0].WorkingDirectory -eq $Root -and
149
+ (Get-TriggerState $task.Triggers) -eq $installedTrigger -and $task.Principal.RunLevel -eq 0 -and $task.Principal.LogonType -eq 3 -and
150
+ $task.Settings.MultipleInstances -eq $instances -and $task.Settings.ExecutionTimeLimit -eq 'PT0S' -and $task.Settings.AllowDemandStart) {
151
+ # The exact current package is already registered; leave its page alone.
152
+ } else {
153
+ # Registering a new immutable release must not terminate a page that a
154
+ # user already has open. Different releases use different control pipes;
155
+ # Parallel lets the new release start while the old page drains. Calls
156
+ # for the same release still reuse its existing pipe before task start.
157
+ $definition = $scheduler.NewTask(0); $definition.RegistrationInfo.Description = $description
158
+ $definition.Principal.UserId = $sid; $definition.Principal.LogonType = 3; $definition.Principal.RunLevel = 0
159
+ $definition.Settings.MultipleInstances = $instances; $definition.Settings.ExecutionTimeLimit = 'PT0S'; $definition.Settings.AllowDemandStart = $true
160
+ $definition.Settings.DisallowStartIfOnBatteries = $false; $definition.Settings.StopIfGoingOnBatteries = $false
161
+ if ($nativeService) { $logon = $definition.Triggers.Create(9); $logon.UserId = $sid }
162
+ $action = $definition.Actions.Create(0); $action.Path = $executable; $action.Arguments = $arguments; $action.WorkingDirectory = $Root
163
+ $null = $folder.RegisterTaskDefinition($taskName, $definition, 6, $sid, $null, 3)
164
+ }
165
+ foreach ($candidate in $namespaceTasks) {
166
+ if ($nativeService -and $candidate.TaskName -ne $taskName) {
167
+ Stop-VerifiedNativeTaskProcesses $candidate
168
+ $candidate.Registered.Stop(0)
169
+ }
170
+ if ($candidate.TaskName -ne $taskName -and ($nativeService -or $candidate.State -ne 4)) {
171
+ $folder.DeleteTask($candidate.TaskName, 0)
172
+ }
173
+ }
174
+ } else {
175
+ if (-not $task) { throw 'workbuddy_page_task_missing' }
176
+ # A logon trigger the user removed or disabled still leaves on-demand start.
177
+ if (@($task.Actions).Count -ne 1 -or $task.Actions[0].Type -ne 0 -or $task.Actions[0].Execute -ne $executable -or $task.Actions[0].Arguments -ne $arguments -or $task.Actions[0].WorkingDirectory -ne $Root -or
178
+ (Get-TriggerState $task.Triggers) -eq 'other' -or $task.Principal.RunLevel -ne 0 -or $task.Principal.LogonType -ne 3 -or
179
+ $task.Settings.MultipleInstances -ne $instances -or $task.Settings.ExecutionTimeLimit -ne 'PT0S' -or -not $task.Settings.AllowDemandStart) { throw 'workbuddy_page_task_configuration_invalid' }
180
+ if ($Command -eq 'Start' -and $task.State -ne 4) {
181
+ for ($attempt = 0; $attempt -lt 3; $attempt += 1) {
182
+ try { $null = $task.Registered.Run($null); break }
183
+ catch {
184
+ if ($attempt -eq 2) { throw }
185
+ Start-Sleep -Milliseconds (100 * ($attempt + 1))
186
+ }
187
+ }
188
+ }
189
+ }
190
+ [pscustomobject]@{ok=$true;operation=$Command;taskName=$taskName} | ConvertTo-Json -Compress
191
+ } catch {
192
+ $code = [string]$_.Exception.Message
193
+ if ($code -notmatch '^workbuddy_[a-z_]+$') { $code = 'workbuddy_page_task_failed' }
194
+ [Console]::Error.WriteLine($code)
195
+ exit 1
196
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: hellolynk-inbox
3
+ description: 在 WorkBuddy 中打开和使用 HelloLynk 通讯;不用于邮件。
4
+ ---
5
+ # HelloLynk 通讯
6
+ HelloLynk 是本人授权的 AI 通讯。接着已有来往用 hellolynk_reply_authorized_message,沿用来信的 localMessageId 和 conversationKey;发起新通信用 hellolynk_send_authorized_message,contact 填对方名字。每条新消息用新的 clientMessageId;发送失败或结果未知时保留编号并停止,不重发;参数无效时改正后重新调用。收到本聊天的 HelloLynk 自动提醒时,用 hellolynk_claim_message 按 batchId 读一次。没有本聊天的提醒或本人查信要求时,不主动查信。用户要看全部来信时,打开本客户端的 HelloLynk 界面供本人查看。
7
+ ## 打开
8
+ 1. 调用 hellolynk_open_inbox(空参数)一次。
9
+ 2. 用内置浏览器打开返回的地址。
@@ -0,0 +1,6 @@
1
+ {
2
+ "display_name": "HelloLynk 通讯",
3
+ "display_name_en": "HelloLynk Messaging",
4
+ "description_zh": "在 WorkBuddy 中打开和使用 HelloLynk 通讯。",
5
+ "description_en": "Open and use HelloLynk Messaging in WorkBuddy."
6
+ }