@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.24

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 (66) hide show
  1. package/README.md +5 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +6 -1
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/core/boss-cards/index.js +199 -199
  14. package/src/core/browser/index.js +286 -136
  15. package/src/core/capture/index.js +2930 -1201
  16. package/src/core/cv-acquisition/index.js +512 -238
  17. package/src/core/cv-capture-target/index.js +513 -299
  18. package/src/core/greet-quota/index.js +71 -71
  19. package/src/core/infinite-list/index.js +31 -31
  20. package/src/core/reporting/legacy-csv.js +12 -12
  21. package/src/core/run/detached-launcher.js +305 -0
  22. package/src/core/run/index.js +105 -52
  23. package/src/core/run/timing.js +33 -33
  24. package/src/core/run/windows-detached-worker.ps1 +106 -0
  25. package/src/core/screening/index.js +2135 -2135
  26. package/src/core/self-heal/index.js +989 -973
  27. package/src/core/self-heal/viewport.js +1505 -564
  28. package/src/detached-worker.js +99 -99
  29. package/src/domains/chat/action-journal.js +443 -0
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +1684 -401
  33. package/src/domains/chat/index.js +8 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +157 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +1801 -760
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +515 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +92 -92
  44. package/src/domains/recommend/detail.js +12 -12
  45. package/src/domains/recommend/filters.js +611 -611
  46. package/src/domains/recommend/index.js +9 -9
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -736
  49. package/src/domains/recommend/refresh.js +422 -422
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1791 -1205
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1817 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +16 -1
  64. package/src/parser.js +1296 -1296
  65. package/src/recommend-mcp.js +551 -362
  66. package/src/recommend-scheduler.js +75 -75
@@ -1,71 +1,71 @@
1
- import { normalizeText } from "../screening/index.js";
2
-
3
- export const GREET_CREDITS_EXHAUSTED_CODE = "GREET_CREDITS_EXHAUSTED";
4
-
5
- function coerceQuota(quota) {
6
- if (!quota || typeof quota !== "object") return null;
7
- return {
8
- found: Boolean(quota.found),
9
- text: normalizeText(quota.text),
10
- numerator: Number.isFinite(Number(quota.numerator)) ? Number(quota.numerator) : null,
11
- denominator: Number.isFinite(Number(quota.denominator)) ? Number(quota.denominator) : null,
12
- exhausted: Boolean(quota.exhausted)
13
- };
14
- }
15
-
16
- export function parseGreetQuota(label = "") {
17
- const text = normalizeText(label);
18
- const match = text.match(/立即沟通\s*[\((]\s*(\d+)\s*[//]\s*(\d+)\s*[\))]/);
19
- if (!match) {
20
- return {
21
- found: false,
22
- text,
23
- numerator: null,
24
- denominator: null,
25
- exhausted: false
26
- };
27
- }
28
- const numerator = Number(match[1]);
29
- const denominator = Number(match[2]);
30
- return {
31
- found: true,
32
- text,
33
- numerator,
34
- denominator,
35
- exhausted: numerator > denominator
36
- };
37
- }
38
-
39
- export function normalizeGreetQuotaSource(source = "") {
40
- return coerceQuota(source) || parseGreetQuota(source);
41
- }
42
-
43
- export function assertGreetQuotaAvailable(source = "") {
44
- const quota = normalizeGreetQuotaSource(source);
45
- if (quota.exhausted) {
46
- const error = new Error(
47
- `Greet credits exhausted according to Boss quota text: ${quota.numerator}/${quota.denominator}`
48
- );
49
- error.code = GREET_CREDITS_EXHAUSTED_CODE;
50
- error.greet_quota = quota;
51
- throw error;
52
- }
53
- return quota;
54
- }
55
-
56
- export function describeGreetQuotaAfterSpend(source = "") {
57
- const quota = normalizeGreetQuotaSource(source);
58
- if (!quota.found || quota.numerator === null || quota.denominator === null) {
59
- return {
60
- ...quota,
61
- remaining_after_spend: null,
62
- exhausted_after_spend: false
63
- };
64
- }
65
- const remaining = quota.denominator - quota.numerator;
66
- return {
67
- ...quota,
68
- remaining_after_spend: remaining,
69
- exhausted_after_spend: remaining < quota.numerator
70
- };
71
- }
1
+ import { normalizeText } from "../screening/index.js";
2
+
3
+ export const GREET_CREDITS_EXHAUSTED_CODE = "GREET_CREDITS_EXHAUSTED";
4
+
5
+ function coerceQuota(quota) {
6
+ if (!quota || typeof quota !== "object") return null;
7
+ return {
8
+ found: Boolean(quota.found),
9
+ text: normalizeText(quota.text),
10
+ numerator: Number.isFinite(Number(quota.numerator)) ? Number(quota.numerator) : null,
11
+ denominator: Number.isFinite(Number(quota.denominator)) ? Number(quota.denominator) : null,
12
+ exhausted: Boolean(quota.exhausted)
13
+ };
14
+ }
15
+
16
+ export function parseGreetQuota(label = "") {
17
+ const text = normalizeText(label);
18
+ const match = text.match(/立即沟通\s*[\((]\s*(\d+)\s*[//]\s*(\d+)\s*[\))]/);
19
+ if (!match) {
20
+ return {
21
+ found: false,
22
+ text,
23
+ numerator: null,
24
+ denominator: null,
25
+ exhausted: false
26
+ };
27
+ }
28
+ const numerator = Number(match[1]);
29
+ const denominator = Number(match[2]);
30
+ return {
31
+ found: true,
32
+ text,
33
+ numerator,
34
+ denominator,
35
+ exhausted: numerator > denominator
36
+ };
37
+ }
38
+
39
+ export function normalizeGreetQuotaSource(source = "") {
40
+ return coerceQuota(source) || parseGreetQuota(source);
41
+ }
42
+
43
+ export function assertGreetQuotaAvailable(source = "") {
44
+ const quota = normalizeGreetQuotaSource(source);
45
+ if (quota.exhausted) {
46
+ const error = new Error(
47
+ `Greet credits exhausted according to Boss quota text: ${quota.numerator}/${quota.denominator}`
48
+ );
49
+ error.code = GREET_CREDITS_EXHAUSTED_CODE;
50
+ error.greet_quota = quota;
51
+ throw error;
52
+ }
53
+ return quota;
54
+ }
55
+
56
+ export function describeGreetQuotaAfterSpend(source = "") {
57
+ const quota = normalizeGreetQuotaSource(source);
58
+ if (!quota.found || quota.numerator === null || quota.denominator === null) {
59
+ return {
60
+ ...quota,
61
+ remaining_after_spend: null,
62
+ exhausted_after_spend: false
63
+ };
64
+ }
65
+ const remaining = quota.denominator - quota.numerator;
66
+ return {
67
+ ...quota,
68
+ remaining_after_spend: remaining,
69
+ exhausted_after_spend: remaining < quota.numerator
70
+ };
71
+ }
@@ -889,13 +889,13 @@ export async function detectInfiniteListBottomMarker(client, {
889
889
  };
890
890
  }
891
891
 
892
- export async function readVisibleInfiniteListItems({
893
- nodeIds = [],
894
- readCandidate,
895
- keyForCandidate = candidateKeyFromProfile,
896
- state = null,
897
- shouldRethrowReadError = null
898
- } = {}) {
892
+ export async function readVisibleInfiniteListItems({
893
+ nodeIds = [],
894
+ readCandidate,
895
+ keyForCandidate = candidateKeyFromProfile,
896
+ state = null,
897
+ shouldRethrowReadError = null
898
+ } = {}) {
899
899
  if (typeof readCandidate !== "function") {
900
900
  throw new Error("readVisibleInfiniteListItems requires readCandidate");
901
901
  }
@@ -905,25 +905,25 @@ export async function readVisibleInfiniteListItems({
905
905
  let candidate;
906
906
  try {
907
907
  candidate = await readCandidate(nodeId, { visibleIndex });
908
- } catch (error) {
909
- if (state) {
910
- state.read_error_count = (state.read_error_count || 0) + 1;
908
+ } catch (error) {
909
+ if (state) {
910
+ state.read_error_count = (state.read_error_count || 0) + 1;
911
911
  state.ledger?.push({
912
912
  at: nowIso(),
913
913
  event: "candidate_read_error",
914
914
  node_id: nodeId,
915
915
  visible_index: visibleIndex,
916
- error: error?.message || String(error)
917
- });
918
- }
919
- if (
920
- typeof shouldRethrowReadError === "function"
921
- && shouldRethrowReadError(error, { nodeId, visibleIndex })
922
- ) {
923
- throw error;
924
- }
925
- continue;
926
- }
916
+ error: error?.message || String(error)
917
+ });
918
+ }
919
+ if (
920
+ typeof shouldRethrowReadError === "function"
921
+ && shouldRethrowReadError(error, { nodeId, visibleIndex })
922
+ ) {
923
+ throw error;
924
+ }
925
+ continue;
926
+ }
927
927
  const key = keyForCandidate(candidate, {
928
928
  nodeId,
929
929
  visibleIndex,
@@ -1139,9 +1139,9 @@ export async function getNextInfiniteListCandidate({
1139
1139
  state,
1140
1140
  findNodeIds,
1141
1141
  readCandidate,
1142
- detectBottomMarker = null,
1143
- keyForCandidate = candidateKeyFromProfile,
1144
- shouldRethrowReadError = null,
1142
+ detectBottomMarker = null,
1143
+ keyForCandidate = candidateKeyFromProfile,
1144
+ shouldRethrowReadError = null,
1145
1145
  maxScrolls = 20,
1146
1146
  stableSignatureLimit = 2,
1147
1147
  minScrollsBeforeEnd = 3,
@@ -1164,13 +1164,13 @@ export async function getNextInfiniteListCandidate({
1164
1164
  const maxAttempts = Math.max(0, Number(maxScrolls) || 0);
1165
1165
  for (let scrollAttempt = 0; scrollAttempt <= maxAttempts; scrollAttempt += 1) {
1166
1166
  const nodeIds = await findNodeIds();
1167
- const items = await readVisibleInfiniteListItems({
1168
- nodeIds,
1169
- readCandidate,
1170
- keyForCandidate,
1171
- state,
1172
- shouldRethrowReadError
1173
- });
1167
+ const items = await readVisibleInfiniteListItems({
1168
+ nodeIds,
1169
+ readCandidate,
1170
+ keyForCandidate,
1171
+ state,
1172
+ shouldRethrowReadError
1173
+ });
1174
1174
  const signature = updateInfiniteListVisibleSignature(state, items);
1175
1175
  const next = firstUnseenInfiniteListItem(state, items);
1176
1176
  attempts.push({
@@ -41,18 +41,18 @@ export const LEGACY_RESULT_HEADER = [
41
41
  ];
42
42
 
43
43
  const SEARCH_PARAM_ORDER = [
44
- "school_tag",
45
- "degree",
46
- "degrees",
47
- "gender",
48
- "recent_not_view",
49
- "current_city_only",
50
- "activity_level",
51
- "city",
52
- "schools",
53
- "keyword",
54
- "filter_recent_viewed",
55
- "skip_recent_colleague_contacted",
44
+ "school_tag",
45
+ "degree",
46
+ "degrees",
47
+ "gender",
48
+ "recent_not_view",
49
+ "current_city_only",
50
+ "activity_level",
51
+ "city",
52
+ "schools",
53
+ "keyword",
54
+ "filter_recent_viewed",
55
+ "skip_recent_colleague_contacted",
56
56
  "job",
57
57
  "start_from",
58
58
  "target_count",
@@ -0,0 +1,305 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { spawn, spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const WINDOWS_WRAPPER_PATH = fileURLToPath(new URL("./windows-detached-worker.ps1", import.meta.url));
8
+ const SAFE_DOMAIN_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/;
9
+ const SAFE_RUN_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
10
+ const SAFE_DOMAINS = new Set(["chat", "recommend", "recruit"]);
11
+
12
+ function createLauncherError(code, message, cause = null) {
13
+ const error = new Error(message);
14
+ error.code = code;
15
+ if (cause) error.cause = cause;
16
+ return error;
17
+ }
18
+
19
+ function ensureLogFile(filePath) {
20
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
21
+ fs.closeSync(fs.openSync(filePath, "a"));
22
+ }
23
+
24
+ function assertControlledPath(value, label, platform) {
25
+ const normalized = String(value || "");
26
+ const pathApi = platform === "win32" ? path.win32 : path;
27
+ if (!normalized || !pathApi.isAbsolute(normalized)) {
28
+ throw createLauncherError("DETACHED_WORKER_PATH_INVALID", `${label} must be an absolute path`);
29
+ }
30
+ if (/[\0\r\n"]/.test(normalized)) {
31
+ throw createLauncherError("DETACHED_WORKER_PATH_INVALID", `${label} contains unsupported characters`);
32
+ }
33
+ return normalized;
34
+ }
35
+
36
+ function assertControlledToken(value, label, pattern) {
37
+ const normalized = String(value || "");
38
+ if (!pattern.test(normalized)) {
39
+ throw createLauncherError("DETACHED_WORKER_ARGUMENT_INVALID", `${label} contains unsupported characters`);
40
+ }
41
+ return normalized;
42
+ }
43
+
44
+ export function quoteWindowsCommandLineArgument(value) {
45
+ const input = String(value ?? "");
46
+ if (input.includes("\0")) {
47
+ throw createLauncherError("DETACHED_WORKER_ARGUMENT_INVALID", "Windows command-line arguments cannot contain NUL");
48
+ }
49
+ if (input && !/[\s"]/.test(input)) return input;
50
+ let output = '"';
51
+ let backslashes = 0;
52
+ for (const character of input) {
53
+ if (character === "\\") {
54
+ backslashes += 1;
55
+ continue;
56
+ }
57
+ if (character === '"') {
58
+ output += "\\".repeat((backslashes * 2) + 1);
59
+ output += '"';
60
+ backslashes = 0;
61
+ continue;
62
+ }
63
+ output += "\\".repeat(backslashes);
64
+ output += character;
65
+ backslashes = 0;
66
+ }
67
+ output += "\\".repeat(backslashes * 2);
68
+ output += '"';
69
+ return output;
70
+ }
71
+
72
+ function quotePowerShellLiteral(value) {
73
+ return `'${String(value ?? "").replaceAll("'", "''")}'`;
74
+ }
75
+
76
+ function windowsPowerShellPath(environment = process.env) {
77
+ const systemRoot = String(environment?.SystemRoot || environment?.WINDIR || "").trim();
78
+ return systemRoot
79
+ ? path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
80
+ : "powershell.exe";
81
+ }
82
+
83
+ export function buildWindowsDetachedWorkerCommand({
84
+ powershellPath,
85
+ wrapperScriptPath,
86
+ nodePath,
87
+ workerScriptPath,
88
+ domain,
89
+ runId,
90
+ stdoutPath,
91
+ stderrPath,
92
+ chatRuntimeHomePath = "",
93
+ screenConfigPath = ""
94
+ }) {
95
+ const args = [
96
+ powershellPath,
97
+ "-NoProfile",
98
+ "-NonInteractive",
99
+ "-ExecutionPolicy",
100
+ "Bypass",
101
+ "-WindowStyle",
102
+ "Hidden",
103
+ "-File",
104
+ wrapperScriptPath,
105
+ "-NodePath",
106
+ nodePath,
107
+ "-WorkerScriptPath",
108
+ workerScriptPath,
109
+ "-Domain",
110
+ domain,
111
+ "-RunId",
112
+ runId,
113
+ "-StdoutPath",
114
+ stdoutPath,
115
+ "-StderrPath",
116
+ stderrPath
117
+ ];
118
+ if (chatRuntimeHomePath) {
119
+ args.push("-ChatRuntimeHomePath", chatRuntimeHomePath);
120
+ }
121
+ if (screenConfigPath) {
122
+ args.push("-ScreenConfigPath", screenConfigPath);
123
+ }
124
+ return args.map(quoteWindowsCommandLineArgument).join(" ");
125
+ }
126
+
127
+ export function buildWindowsCimEncodedCommand(commandLine) {
128
+ const script = [
129
+ "$ErrorActionPreference = 'Stop'",
130
+ `$commandLine = ${quotePowerShellLiteral(commandLine)}`,
131
+ "$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = $commandLine }",
132
+ "$payload = [ordered]@{ return_value = [int]$result.ReturnValue; process_id = [int]$result.ProcessId }",
133
+ "[Console]::Out.Write(($payload | ConvertTo-Json -Compress))",
134
+ "if ([int]$result.ReturnValue -ne 0) { exit 1 }"
135
+ ].join("; ");
136
+ return Buffer.from(script, "utf16le").toString("base64");
137
+ }
138
+
139
+ function parseCimPayload(stdout = "") {
140
+ const lines = String(stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
141
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
142
+ try {
143
+ const parsed = JSON.parse(lines[index]);
144
+ if (parsed && typeof parsed === "object") return parsed;
145
+ } catch {
146
+ // PowerShell may emit non-JSON diagnostics before the compact payload.
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ function launchWindowsDetachedWorker(options) {
153
+ const {
154
+ nodePath,
155
+ workerScriptPath,
156
+ domain,
157
+ runId,
158
+ stdoutPath,
159
+ stderrPath,
160
+ chatRuntimeHomePath,
161
+ screenConfigPath,
162
+ wrapperScriptPath = WINDOWS_WRAPPER_PATH,
163
+ powershellPath = windowsPowerShellPath(options.environment),
164
+ spawnSyncImpl = spawnSync
165
+ } = options;
166
+ const commandLine = buildWindowsDetachedWorkerCommand({
167
+ powershellPath,
168
+ wrapperScriptPath,
169
+ nodePath,
170
+ workerScriptPath,
171
+ domain,
172
+ runId,
173
+ stdoutPath,
174
+ stderrPath,
175
+ chatRuntimeHomePath,
176
+ screenConfigPath
177
+ });
178
+ const encodedCommand = buildWindowsCimEncodedCommand(commandLine);
179
+ const result = spawnSyncImpl(powershellPath, [
180
+ "-NoProfile",
181
+ "-NonInteractive",
182
+ "-ExecutionPolicy",
183
+ "Bypass",
184
+ "-EncodedCommand",
185
+ encodedCommand
186
+ ], {
187
+ encoding: "utf8",
188
+ windowsHide: true,
189
+ stdio: ["ignore", "pipe", "pipe"]
190
+ });
191
+ if (result?.error) {
192
+ throw createLauncherError(
193
+ "WINDOWS_CIM_LAUNCH_FAILED",
194
+ `Unable to invoke the Windows detached-worker launcher: ${result.error.message || result.error}`,
195
+ result.error
196
+ );
197
+ }
198
+ const payload = parseCimPayload(result?.stdout);
199
+ const returnValue = Number(payload?.return_value);
200
+ const workerPid = Number(payload?.process_id);
201
+ if (Number.isFinite(returnValue) && returnValue !== 0) {
202
+ throw createLauncherError(
203
+ "WINDOWS_CIM_CREATE_FAILED",
204
+ `Win32_Process.Create returned ${returnValue} while launching the detached worker.`
205
+ );
206
+ }
207
+ if (result?.signal || !Number.isInteger(result?.status) || result.status !== 0) {
208
+ const stderr = String(result?.stderr || "").trim();
209
+ throw createLauncherError(
210
+ "WINDOWS_CIM_LAUNCH_FAILED",
211
+ stderr || `PowerShell CIM launcher exited with status ${result?.status ?? "unknown"}.`
212
+ );
213
+ }
214
+ if (!Number.isInteger(workerPid) || workerPid <= 0 || returnValue !== 0) {
215
+ throw createLauncherError(
216
+ "WINDOWS_CIM_RESULT_INVALID",
217
+ "Win32_Process.Create did not return a valid detached worker PID."
218
+ );
219
+ }
220
+ return {
221
+ pid: workerPid,
222
+ unref() {}
223
+ };
224
+ }
225
+
226
+ function launchPosixDetachedWorker({
227
+ nodePath,
228
+ workerScriptPath,
229
+ domain,
230
+ runId,
231
+ stdoutPath,
232
+ stderrPath,
233
+ spawnImpl = spawn,
234
+ environment = process.env
235
+ }) {
236
+ const stdoutFd = fs.openSync(stdoutPath, "a");
237
+ const stderrFd = fs.openSync(stderrPath, "a");
238
+ try {
239
+ return spawnImpl(nodePath, [
240
+ workerScriptPath,
241
+ "--domain",
242
+ domain,
243
+ "--run-id",
244
+ runId
245
+ ], {
246
+ detached: true,
247
+ stdio: ["ignore", stdoutFd, stderrFd],
248
+ windowsHide: true,
249
+ env: environment
250
+ });
251
+ } finally {
252
+ fs.closeSync(stdoutFd);
253
+ fs.closeSync(stderrFd);
254
+ }
255
+ }
256
+
257
+ export function launchDetachedWorker(options = {}) {
258
+ const platform = String(options.platform || process.platform);
259
+ const nodePath = assertControlledPath(options.nodePath || process.execPath, "nodePath", platform);
260
+ const workerScriptPath = assertControlledPath(options.workerScriptPath, "workerScriptPath", platform);
261
+ const stdoutPath = assertControlledPath(options.stdoutPath, "stdoutPath", platform);
262
+ const stderrPath = assertControlledPath(options.stderrPath, "stderrPath", platform);
263
+ const domain = assertControlledToken(options.domain, "domain", SAFE_DOMAIN_PATTERN);
264
+ if (!SAFE_DOMAINS.has(domain)) {
265
+ throw createLauncherError("DETACHED_WORKER_ARGUMENT_INVALID", "domain is not supported by the detached launcher");
266
+ }
267
+ const runId = assertControlledToken(options.runId, "runId", SAFE_RUN_ID_PATTERN);
268
+ const wrapperScriptPath = platform === "win32"
269
+ ? assertControlledPath(options.wrapperScriptPath || WINDOWS_WRAPPER_PATH, "wrapperScriptPath", platform)
270
+ : options.wrapperScriptPath;
271
+ const chatRuntimeHomePath = options.chatRuntimeHomePath
272
+ ? assertControlledPath(options.chatRuntimeHomePath, "chatRuntimeHomePath", platform)
273
+ : "";
274
+ const screenConfigPath = options.screenConfigPath
275
+ ? assertControlledPath(options.screenConfigPath, "screenConfigPath", platform)
276
+ : "";
277
+ const prepareLogFile = typeof options.prepareLogFileImpl === "function"
278
+ ? options.prepareLogFileImpl
279
+ : ensureLogFile;
280
+ prepareLogFile(stdoutPath);
281
+ prepareLogFile(stderrPath);
282
+ const normalized = {
283
+ ...options,
284
+ platform,
285
+ nodePath,
286
+ workerScriptPath,
287
+ stdoutPath,
288
+ stderrPath,
289
+ domain,
290
+ runId,
291
+ wrapperScriptPath,
292
+ chatRuntimeHomePath,
293
+ screenConfigPath
294
+ };
295
+ return platform === "win32"
296
+ ? launchWindowsDetachedWorker(normalized)
297
+ : launchPosixDetachedWorker(normalized);
298
+ }
299
+
300
+ export const __testables = {
301
+ WINDOWS_WRAPPER_PATH,
302
+ parseCimPayload,
303
+ quotePowerShellLiteral,
304
+ windowsPowerShellPath
305
+ };