@xdsjs/dossierx-daemon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2327 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { stat as stat4 } from "fs/promises";
5
+ import os3 from "os";
6
+ import path8 from "path";
7
+ import { Command } from "commander";
8
+ import { DOSSIERX_DEFAULT_WORKSPACE_PATH } from "@xdsjs/dossierx-workspace";
9
+
10
+ // src/api/client.ts
11
+ import {
12
+ AppendTaskEventsRequestSchema,
13
+ AppendTaskEventsResponseSchema,
14
+ ClaimTaskResponseSchema,
15
+ CompleteTaskRequestSchema,
16
+ CompleteTaskResponseSchema,
17
+ DaemonBootstrapRequestSchema,
18
+ DaemonBootstrapResponseSchema,
19
+ DaemonHeartbeatRequestSchema,
20
+ DaemonHeartbeatResponseSchema,
21
+ FailTaskRequestSchema,
22
+ FailTaskResponseSchema
23
+ } from "@xdsjs/dossierx-shared";
24
+ var ApiClient = class {
25
+ serverUrl;
26
+ machineKey;
27
+ fetchImpl;
28
+ constructor(options) {
29
+ this.serverUrl = options.serverUrl.replace(/\/$/, "");
30
+ this.machineKey = options.machineKey;
31
+ this.fetchImpl = options.fetchImpl ?? fetch;
32
+ }
33
+ async post(path9, body, schema) {
34
+ const response = await this.fetchImpl(`${this.serverUrl}${path9}`, {
35
+ method: "POST",
36
+ headers: {
37
+ authorization: `Bearer ${this.machineKey}`,
38
+ "content-type": "application/json"
39
+ },
40
+ body: JSON.stringify(body)
41
+ });
42
+ const data = await response.json().catch(() => ({}));
43
+ if (!response.ok) {
44
+ const message = typeof data.error === "string" ? data.error : `HTTP ${response.status} ${response.statusText}`;
45
+ throw new Error(message);
46
+ }
47
+ return schema.parse(data);
48
+ }
49
+ async bootstrap(input) {
50
+ return this.post(
51
+ "/api/daemon/bootstrap",
52
+ DaemonBootstrapRequestSchema.parse(input),
53
+ DaemonBootstrapResponseSchema
54
+ );
55
+ }
56
+ async heartbeat(input) {
57
+ return this.post(
58
+ "/api/daemon/heartbeat",
59
+ DaemonHeartbeatRequestSchema.parse(input),
60
+ DaemonHeartbeatResponseSchema
61
+ );
62
+ }
63
+ async claimTask(input) {
64
+ return this.post("/api/tasks/claim", input, ClaimTaskResponseSchema);
65
+ }
66
+ async appendTaskEvents(taskId, input) {
67
+ await this.post(
68
+ `/api/tasks/${taskId}/events`,
69
+ AppendTaskEventsRequestSchema.parse(input),
70
+ AppendTaskEventsResponseSchema
71
+ );
72
+ }
73
+ async completeTask(taskId, input) {
74
+ await this.post(
75
+ `/api/tasks/${taskId}/complete`,
76
+ CompleteTaskRequestSchema.parse(input),
77
+ CompleteTaskResponseSchema
78
+ );
79
+ }
80
+ async failTask(taskId, input) {
81
+ await this.post(
82
+ `/api/tasks/${taskId}/fail`,
83
+ FailTaskRequestSchema.parse(input),
84
+ FailTaskResponseSchema
85
+ );
86
+ }
87
+ };
88
+
89
+ // src/codex/runner.ts
90
+ import { execa } from "execa";
91
+ var CodexSandboxModes = [
92
+ "workspace-write",
93
+ "danger-full-access"
94
+ ];
95
+ var CodexRuntimeError = class extends Error {
96
+ code;
97
+ step;
98
+ constructor(code, message, step) {
99
+ super(message);
100
+ this.name = "CodexRuntimeError";
101
+ this.code = code;
102
+ this.step = step;
103
+ }
104
+ };
105
+ function truncateCodexOutput(output, maxLength = 2e3) {
106
+ return output.length > maxLength ? output.slice(0, maxLength) : output;
107
+ }
108
+ function resolveCodexCommand(options, env = process.env) {
109
+ return options.codexCommand?.trim() || env.DOSSIERX_CODEX_COMMAND?.trim() || "codex";
110
+ }
111
+ function resolveCodexModel(options, env = process.env) {
112
+ return options.codexModel?.trim() || env.DOSSIERX_CODEX_MODEL?.trim() || void 0;
113
+ }
114
+ function resolveCodexSandbox(options, env = process.env) {
115
+ const value = options.codexSandbox?.trim() || env.DOSSIERX_CODEX_SANDBOX?.trim() || "workspace-write";
116
+ if (CodexSandboxModes.includes(value)) {
117
+ return value;
118
+ }
119
+ throw new CodexRuntimeError(
120
+ "VALIDATION_ERROR",
121
+ `Invalid Codex sandbox: ${value}`,
122
+ "codex.runner.config"
123
+ );
124
+ }
125
+ function buildInvestWikiFlowPrompt(input) {
126
+ const identity = [
127
+ `ticker: ${input.ticker}`,
128
+ `market: ${input.market}`,
129
+ input.companyName ? `company_name: ${input.companyName}` : void 0,
130
+ input.cik ? `cik: ${input.cik}` : void 0,
131
+ input.exchange ? `exchange: ${input.exchange}` : void 0
132
+ ].filter((line) => Boolean(line));
133
+ return [
134
+ "\u4F60\u5728\u4E00\u4E2A\u5DF2\u7ECF\u521D\u59CB\u5316\u7684 llm-wiki-invest vault \u6839\u76EE\u5F55\u4E2D\u5DE5\u4F5C\u3002",
135
+ "\u8BF7\u4F7F\u7528 invest-wiki-flow skill\uFF0C\u6267\u884C invest-wiki-flow \u7EF4\u62A4\u6D41\u7A0B\u3002",
136
+ "",
137
+ "\u516C\u53F8\u8EAB\u4EFD\uFF1A",
138
+ ...identity.map((line) => `- ${line}`),
139
+ "",
140
+ "\u6267\u884C\u8981\u6C42\uFF1A",
141
+ "- \u5148\u8BFB\u53D6 AGENTS.md\u3001wiki-purpose.md\u3001wiki-schema.md\uFF1B\u5982\u5B58\u5728 wiki-agent.md \u4E5F\u8981\u8BFB\u53D6\u3002",
142
+ "- \u4E25\u683C\u9075\u5B88 invest-wiki-flow \u7684 sources -> wiki -> wiki/right \u987A\u5E8F\u3002",
143
+ "- \u4E0D\u8981\u624B\u5DE5\u4FEE\u6539 sources/ \u4E0B\u5DF2\u6709\u6765\u6E90\u6B63\u6587\u3002",
144
+ "- \u6267\u884C llm-wiki-invest\u3001Node fetch \u6216 SEC \u83B7\u53D6\u547D\u4EE4\u65F6\uFF0C\u547D\u4EE4\u524D\u5FC5\u987B\u663E\u5F0F\u52A0 NODE_USE_ENV_PROXY=1\u3002",
145
+ "- \u9996\u6B21\u83B7\u53D6 SEC submissions \u65F6\u5FC5\u987B\u4FDD\u5B58\u5230 .llm-wiki-invest/sec-submissions.json\uFF0C\u540E\u7EED\u6B65\u9AA4\u590D\u7528\u8BE5\u6587\u4EF6\uFF0C\u907F\u514D\u53CD\u590D\u8054\u7F51\u83B7\u53D6\u540C\u4E00 JSON\u3002",
146
+ "- \u5982\u679C\u6CA1\u6709\u65B0\u589E\u6216\u9700\u8981\u66F4\u65B0\u7684\u5185\u5BB9\uFF0C\u53EA\u8F93\u51FA no-op \u6458\u8981\u3002",
147
+ "- \u5982\u4EA7\u751F\u5B9E\u9645\u53D8\u66F4\uFF0C\u6309 vault \u89C4\u5219\u66F4\u65B0 wiki-log.md \u5E76\u8FD0\u884C llm-wiki-invest sync\u3002",
148
+ "",
149
+ `\u8BF7\u5F00\u59CB\u6267\u884C\uFF1Ainvest-wiki-flow ${input.ticker}`
150
+ ].join("\n");
151
+ }
152
+ function getStringProperty(error, property) {
153
+ if (!error || typeof error !== "object") {
154
+ return void 0;
155
+ }
156
+ const value = error[property];
157
+ return typeof value === "string" ? value : void 0;
158
+ }
159
+ function getNumberProperty(error, property) {
160
+ if (!error || typeof error !== "object") {
161
+ return void 0;
162
+ }
163
+ const value = error[property];
164
+ return typeof value === "number" ? value : void 0;
165
+ }
166
+ function extractCodexJsonFailure(stdout) {
167
+ if (!stdout) {
168
+ return void 0;
169
+ }
170
+ for (const line of stdout.split("\n")) {
171
+ if (!line.trim()) {
172
+ continue;
173
+ }
174
+ try {
175
+ const event = JSON.parse(line);
176
+ if (event.type === "error" && typeof event.message === "string") {
177
+ return event.message;
178
+ }
179
+ if (event.type === "turn.failed" && event.error && typeof event.error.message === "string") {
180
+ return event.error.message;
181
+ }
182
+ } catch {
183
+ continue;
184
+ }
185
+ }
186
+ return void 0;
187
+ }
188
+ function commandFailureCode(error) {
189
+ const code = getStringProperty(error, "code");
190
+ return code === "ENOENT" ? "RUNTIME_NOT_FOUND" : "COMMAND_FAILED";
191
+ }
192
+ function commandFailureStep(error) {
193
+ return commandFailureCode(error) === "RUNTIME_NOT_FOUND" ? "codex.runner.resolve" : "codex.runner.run";
194
+ }
195
+ function commandFailureMessage(error) {
196
+ const stdout = getStringProperty(error, "stdout");
197
+ const stderr = getStringProperty(error, "stderr");
198
+ const exitCode = getNumberProperty(error, "exitCode");
199
+ const signal = getStringProperty(error, "signal");
200
+ const codexError = extractCodexJsonFailure(stdout);
201
+ const details = [
202
+ "Codex command failed",
203
+ codexError ? `reason: ${truncateCodexOutput(codexError, 1e3)}` : void 0,
204
+ exitCode === void 0 ? void 0 : `exitCode: ${exitCode}`,
205
+ signal ? `signal: ${signal}` : void 0,
206
+ stdout === void 0 ? void 0 : `stdoutChars: ${stdout.length}`,
207
+ stderr === void 0 ? void 0 : `stderrChars: ${stderr.length}`,
208
+ stdout === void 0 ? void 0 : `stdoutBytes: ${Buffer.byteLength(stdout)}`,
209
+ stderr === void 0 ? void 0 : `stderrBytes: ${Buffer.byteLength(stderr)}`,
210
+ stdout === void 0 ? void 0 : `stdoutTruncated: ${stdout.length > 2e3}`,
211
+ stderr === void 0 ? void 0 : `stderrTruncated: ${stderr.length > 2e3}`
212
+ ].filter((detail) => Boolean(detail));
213
+ return truncateCodexOutput(details.join("\n"), 2e3);
214
+ }
215
+ function hasCodexJsonEvent(stdout, eventType) {
216
+ return stdout.split("\n").filter(Boolean).some((line) => {
217
+ try {
218
+ const event = JSON.parse(line);
219
+ return event.type === eventType;
220
+ } catch {
221
+ return false;
222
+ }
223
+ });
224
+ }
225
+ function getRecordProperty(value, property) {
226
+ if (!value || typeof value !== "object") {
227
+ return void 0;
228
+ }
229
+ const nested = value[property];
230
+ return nested && typeof nested === "object" ? nested : void 0;
231
+ }
232
+ function getEventPayload(event) {
233
+ return getRecordProperty(event, "payload") ?? event;
234
+ }
235
+ function getEventType(event) {
236
+ const payload = getEventPayload(event);
237
+ const payloadType = payload.type;
238
+ if (typeof payloadType === "string") {
239
+ return payloadType;
240
+ }
241
+ const type = event.type;
242
+ return typeof type === "string" ? type : void 0;
243
+ }
244
+ function getText(value) {
245
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
246
+ }
247
+ function truncateEventMessage(message) {
248
+ return message.length > 1e3 ? `${message.slice(0, 1e3)}...` : message;
249
+ }
250
+ function extractMessageText(content) {
251
+ if (typeof content === "string") {
252
+ return getText(content);
253
+ }
254
+ if (!Array.isArray(content)) {
255
+ return void 0;
256
+ }
257
+ const text = content.map((item) => {
258
+ if (typeof item === "string") {
259
+ return item;
260
+ }
261
+ if (!item || typeof item !== "object") {
262
+ return "";
263
+ }
264
+ const record = item;
265
+ return getText(record.text) ?? getText(record.content) ?? "";
266
+ }).filter(Boolean).join("\n").trim();
267
+ return text || void 0;
268
+ }
269
+ function codexJsonLineToTaskEvent(line) {
270
+ if (!line.trim()) {
271
+ return null;
272
+ }
273
+ let event;
274
+ try {
275
+ const parsed = JSON.parse(line);
276
+ if (!parsed || typeof parsed !== "object") {
277
+ return null;
278
+ }
279
+ event = parsed;
280
+ } catch {
281
+ return null;
282
+ }
283
+ const payload = getEventPayload(event);
284
+ const type = getEventType(event);
285
+ if (!type) {
286
+ return null;
287
+ }
288
+ const data = { codexType: type };
289
+ const item = getRecordProperty(event, "item");
290
+ const itemType = getText(item?.type);
291
+ const itemData = itemType ? { ...data, itemType } : data;
292
+ switch (type) {
293
+ case "thread.started":
294
+ return { level: "info", message: "Codex session started", data };
295
+ case "turn.started":
296
+ case "task_started":
297
+ return { level: "info", message: "Codex turn started", data };
298
+ case "turn.completed":
299
+ case "task_complete":
300
+ return { level: "info", message: "Codex turn completed", data };
301
+ case "turn.failed":
302
+ case "error": {
303
+ const errorRecord = getRecordProperty(payload, "error");
304
+ const message = getText(errorRecord?.message) ?? getText(payload.message) ?? "Codex turn failed";
305
+ return {
306
+ level: "error",
307
+ message: truncateEventMessage(message),
308
+ data
309
+ };
310
+ }
311
+ case "agent_message": {
312
+ const message = getText(payload.message);
313
+ return message ? { level: "info", message: truncateEventMessage(message), data } : null;
314
+ }
315
+ case "message": {
316
+ const message = extractMessageText(payload.content);
317
+ return message ? { level: "info", message: truncateEventMessage(message), data } : null;
318
+ }
319
+ case "function_call":
320
+ case "custom_tool_call": {
321
+ const name = getText(payload.name) ?? "tool";
322
+ return {
323
+ level: "info",
324
+ message: `Codex tool call: ${name}`,
325
+ data
326
+ };
327
+ }
328
+ case "function_call_output":
329
+ case "custom_tool_call_output":
330
+ return {
331
+ level: "debug",
332
+ message: "Codex tool output received",
333
+ data
334
+ };
335
+ case "item.started":
336
+ if (itemType === "command_execution") {
337
+ return {
338
+ level: "info",
339
+ message: "Codex command started",
340
+ data: itemData
341
+ };
342
+ }
343
+ if (itemType === "todo_list") {
344
+ return {
345
+ level: "info",
346
+ message: "Codex todo list started",
347
+ data: itemData
348
+ };
349
+ }
350
+ return null;
351
+ case "item.completed":
352
+ if (itemType === "agent_message") {
353
+ const message = getText(item?.text) ?? getText(item?.message);
354
+ return message ? {
355
+ level: "info",
356
+ message: truncateEventMessage(message),
357
+ data: itemData
358
+ } : null;
359
+ }
360
+ if (itemType === "command_execution") {
361
+ return {
362
+ level: "info",
363
+ message: "Codex command completed",
364
+ data: itemData
365
+ };
366
+ }
367
+ if (itemType === "todo_list") {
368
+ return {
369
+ level: "info",
370
+ message: "Codex todo list updated",
371
+ data: itemData
372
+ };
373
+ }
374
+ return null;
375
+ default:
376
+ return null;
377
+ }
378
+ }
379
+ function emitCodexEvent(callbacks, line, pendingCallbacks) {
380
+ const event = codexJsonLineToTaskEvent(line);
381
+ if (!event) {
382
+ return;
383
+ }
384
+ pendingCallbacks.push(
385
+ Promise.resolve(callbacks?.onEvent?.(event)).catch(() => void 0)
386
+ );
387
+ }
388
+ function emitRawOutput(callbacks, stream, chunk, pendingCallbacks) {
389
+ pendingCallbacks.push(
390
+ Promise.resolve(callbacks?.onRawOutput?.(stream, chunk)).catch(
391
+ () => void 0
392
+ )
393
+ );
394
+ }
395
+ function consumeStdoutChunk(callbacks, buffer, chunk, pendingCallbacks) {
396
+ emitRawOutput(callbacks, "stdout", chunk, pendingCallbacks);
397
+ buffer.value += chunk;
398
+ let newlineIndex = buffer.value.indexOf("\n");
399
+ while (newlineIndex >= 0) {
400
+ const line = buffer.value.slice(0, newlineIndex);
401
+ buffer.value = buffer.value.slice(newlineIndex + 1);
402
+ emitCodexEvent(callbacks, line, pendingCallbacks);
403
+ newlineIndex = buffer.value.indexOf("\n");
404
+ }
405
+ }
406
+ function assertCodexTurnCompleted(stdout) {
407
+ if (hasCodexJsonEvent(stdout, "turn.completed")) {
408
+ return;
409
+ }
410
+ if (hasCodexJsonEvent(stdout, "turn.failed") || hasCodexJsonEvent(stdout, "error")) {
411
+ throw new CodexRuntimeError(
412
+ "COMMAND_FAILED",
413
+ "Codex command reported a failed turn",
414
+ "codex.runner.run"
415
+ );
416
+ }
417
+ throw new CodexRuntimeError(
418
+ "COMMAND_FAILED",
419
+ "Codex command ended before turn.completed",
420
+ "codex.runner.run"
421
+ );
422
+ }
423
+ function createCodexRunner(config) {
424
+ return {
425
+ kind: "codex",
426
+ async runInvestWikiFlow(input, callbacks) {
427
+ const modelArgs = config.model ? ["-m", config.model] : [];
428
+ const sandbox = config.sandbox ?? "workspace-write";
429
+ let stdout = "";
430
+ let stderr = "";
431
+ const stdoutLineBuffer = { value: "" };
432
+ const pendingCallbacks = [];
433
+ try {
434
+ const subprocess = execa(
435
+ config.command,
436
+ [
437
+ "exec",
438
+ ...modelArgs,
439
+ "-C",
440
+ input.vaultRoot,
441
+ "--sandbox",
442
+ sandbox,
443
+ "--skip-git-repo-check",
444
+ "--json",
445
+ buildInvestWikiFlowPrompt(input)
446
+ ],
447
+ {
448
+ cwd: input.vaultRoot,
449
+ env: {
450
+ NODE_USE_ENV_PROXY: process.env.NODE_USE_ENV_PROXY?.trim() || "1"
451
+ },
452
+ stdin: "ignore",
453
+ shell: false,
454
+ reject: true
455
+ }
456
+ );
457
+ subprocess.stdout?.on("data", (chunk) => {
458
+ const text = chunk.toString();
459
+ stdout += text;
460
+ consumeStdoutChunk(callbacks, stdoutLineBuffer, text, pendingCallbacks);
461
+ });
462
+ subprocess.stderr?.on("data", (chunk) => {
463
+ const text = chunk.toString();
464
+ stderr += text;
465
+ emitRawOutput(callbacks, "stderr", text, pendingCallbacks);
466
+ });
467
+ const result = await subprocess;
468
+ if (stdoutLineBuffer.value.trim()) {
469
+ emitCodexEvent(callbacks, stdoutLineBuffer.value, pendingCallbacks);
470
+ }
471
+ await Promise.all(pendingCallbacks);
472
+ const finalStdout = stdout || result.stdout;
473
+ const finalStderr = stderr || result.stderr;
474
+ assertCodexTurnCompleted(finalStdout);
475
+ return {
476
+ stdout: truncateCodexOutput(finalStdout),
477
+ stderr: truncateCodexOutput(finalStderr)
478
+ };
479
+ } catch (error) {
480
+ await Promise.all(pendingCallbacks);
481
+ throw new CodexRuntimeError(
482
+ commandFailureCode(error),
483
+ commandFailureMessage(error),
484
+ commandFailureStep(error)
485
+ );
486
+ }
487
+ }
488
+ };
489
+ }
490
+ function createCodexRunnerFromOptions(options, env = process.env) {
491
+ return createCodexRunner({
492
+ command: resolveCodexCommand(options, env),
493
+ model: resolveCodexModel(options, env),
494
+ sandbox: resolveCodexSandbox(options, env)
495
+ });
496
+ }
497
+
498
+ // src/invest-wiki/runner.ts
499
+ import { access } from "fs/promises";
500
+ import path from "path";
501
+ import { execa as execa2 } from "execa";
502
+
503
+ // src/invest-wiki/config.ts
504
+ function resolveInvestWikiConfig(options, env = process.env) {
505
+ const mode = options.investWikiMode ?? env.DOSSIERX_INVEST_WIKI_MODE;
506
+ const configuredCommand = options.investWikiCommand ?? env.DOSSIERX_INVEST_WIKI_COMMAND;
507
+ const command = configuredCommand?.trim() || "llm-wiki-invest";
508
+ if (mode === "local-repo") {
509
+ const localRepo = options.investWikiLocalRepo ?? env.DOSSIERX_INVEST_WIKI_LOCAL_REPO;
510
+ if (!localRepo) {
511
+ return { mode: void 0, command };
512
+ }
513
+ return { mode: "local-repo", localRepo, command };
514
+ }
515
+ if (mode === "package") {
516
+ return { mode: "package", command };
517
+ }
518
+ return { mode: void 0, command };
519
+ }
520
+
521
+ // src/invest-wiki/runner.ts
522
+ var InvestWikiRuntimeError = class extends Error {
523
+ code;
524
+ step;
525
+ constructor(code, message, step) {
526
+ super(message);
527
+ this.name = "InvestWikiRuntimeError";
528
+ this.code = code;
529
+ this.step = step;
530
+ }
531
+ };
532
+ function truncateCommandOutput(output, maxLength = 2e3) {
533
+ return output.length > maxLength ? output.slice(0, maxLength) : output;
534
+ }
535
+ async function assertFileExists(filePath) {
536
+ try {
537
+ await access(filePath);
538
+ } catch {
539
+ throw new InvestWikiRuntimeError(
540
+ "RUNTIME_NOT_FOUND",
541
+ `llm-wiki-invest CLI not found at ${filePath}; build the sibling repo first`,
542
+ "invest_wiki.runner.resolve"
543
+ );
544
+ }
545
+ }
546
+ function getStringProperty2(error, property) {
547
+ if (!error || typeof error !== "object") {
548
+ return void 0;
549
+ }
550
+ const value = error[property];
551
+ return typeof value === "string" ? value : void 0;
552
+ }
553
+ function getNumberProperty2(error, property) {
554
+ if (!error || typeof error !== "object") {
555
+ return void 0;
556
+ }
557
+ const value = error[property];
558
+ return typeof value === "number" ? value : void 0;
559
+ }
560
+ function commandFailureMessage2(error) {
561
+ const shortMessage = getStringProperty2(error, "shortMessage") ?? "Unknown command failure";
562
+ const stdout = getStringProperty2(error, "stdout");
563
+ const stderr = getStringProperty2(error, "stderr");
564
+ const exitCode = getNumberProperty2(error, "exitCode");
565
+ const signal = getStringProperty2(error, "signal");
566
+ const details = [
567
+ `Invest wiki command failed: ${shortMessage}`,
568
+ exitCode === void 0 ? void 0 : `exitCode: ${exitCode}`,
569
+ signal ? `signal: ${signal}` : void 0,
570
+ stdout === void 0 ? void 0 : `stdoutChars: ${stdout.length}`,
571
+ stderr === void 0 ? void 0 : `stderrChars: ${stderr.length}`,
572
+ stdout === void 0 ? void 0 : `stdoutBytes: ${Buffer.byteLength(stdout)}`,
573
+ stderr === void 0 ? void 0 : `stderrBytes: ${Buffer.byteLength(stderr)}`,
574
+ stdout === void 0 ? void 0 : `stdoutTruncated: ${stdout.length > 2e3}`,
575
+ stderr === void 0 ? void 0 : `stderrTruncated: ${stderr.length > 2e3}`
576
+ ].filter((detail) => Boolean(detail));
577
+ return truncateCommandOutput(details.join("\n"), 2e3);
578
+ }
579
+ async function runInvestWikiCommand(command, args, options) {
580
+ try {
581
+ return await execa2(command, args, options);
582
+ } catch (error) {
583
+ throw new InvestWikiRuntimeError(
584
+ "COMMAND_FAILED",
585
+ commandFailureMessage2(error),
586
+ "invest_wiki.runner.run"
587
+ );
588
+ }
589
+ }
590
+ function createLocalRepoRunner(config) {
591
+ return {
592
+ kind: "local-repo",
593
+ async run(args, options) {
594
+ const cliPath = path.join(config.localRepo, "dist", "cli.js");
595
+ await assertFileExists(cliPath);
596
+ const result = await runInvestWikiCommand(
597
+ process.execPath,
598
+ [cliPath, ...args],
599
+ {
600
+ cwd: options.cwd,
601
+ shell: false,
602
+ reject: true
603
+ }
604
+ );
605
+ return {
606
+ stdout: truncateCommandOutput(result.stdout),
607
+ stderr: truncateCommandOutput(result.stderr)
608
+ };
609
+ }
610
+ };
611
+ }
612
+ function createPackageRunner(config) {
613
+ return {
614
+ kind: "package",
615
+ async run(args, options) {
616
+ const result = await runInvestWikiCommand(config.command, args, {
617
+ cwd: options.cwd,
618
+ shell: false,
619
+ reject: true
620
+ });
621
+ return {
622
+ stdout: truncateCommandOutput(result.stdout),
623
+ stderr: truncateCommandOutput(result.stderr)
624
+ };
625
+ }
626
+ };
627
+ }
628
+ function createInvestWikiRunner(config) {
629
+ if (config.mode === "local-repo") {
630
+ return createLocalRepoRunner(config);
631
+ }
632
+ if (config.mode === "package") {
633
+ return createPackageRunner(config);
634
+ }
635
+ return null;
636
+ }
637
+ function createInvestWikiRunnerFromOptions(options, env = process.env) {
638
+ return createInvestWikiRunner(resolveInvestWikiConfig(options, env));
639
+ }
640
+
641
+ // src/local-config.ts
642
+ import { chmod, mkdir, readFile, writeFile } from "fs/promises";
643
+ import os from "os";
644
+ import path2 from "path";
645
+ import { z } from "zod";
646
+ import { DOSSIERX_CONFIG_DIR } from "@xdsjs/dossierx-workspace";
647
+ var DaemonLocalConfigSchema = z.object({
648
+ machineId: z.string().uuid().optional(),
649
+ machineKey: z.string().startsWith("dx_machine_"),
650
+ serverUrl: z.string().min(1),
651
+ supabaseUrl: z.string().min(1),
652
+ supabaseAnonKey: z.string().min(1),
653
+ workspacePath: z.string().min(1),
654
+ investWikiMode: z.string().optional(),
655
+ investWikiLocalRepo: z.string().optional(),
656
+ investWikiCommand: z.string().optional(),
657
+ codexCommand: z.string().optional(),
658
+ codexModel: z.string().optional(),
659
+ codexSandbox: z.string().optional(),
660
+ localApiPort: z.number().int().positive().optional()
661
+ });
662
+ function expandHomePath(input) {
663
+ if (input === "~") {
664
+ return os.homedir();
665
+ }
666
+ if (input.startsWith("~/")) {
667
+ return path2.join(os.homedir(), input.slice(2));
668
+ }
669
+ return input;
670
+ }
671
+ function getDaemonConfigDir(env = process.env) {
672
+ return path2.resolve(
673
+ expandHomePath(env.DOSSIERX_CONFIG_DIR ?? DOSSIERX_CONFIG_DIR)
674
+ );
675
+ }
676
+ function daemonConfigPath(configDir = getDaemonConfigDir()) {
677
+ return path2.join(configDir, "config.json");
678
+ }
679
+ async function readDaemonLocalConfig(configDir = getDaemonConfigDir()) {
680
+ try {
681
+ return DaemonLocalConfigSchema.parse(
682
+ JSON.parse(await readFile(daemonConfigPath(configDir), "utf8"))
683
+ );
684
+ } catch (error) {
685
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
686
+ return null;
687
+ }
688
+ throw error;
689
+ }
690
+ }
691
+ async function writeDaemonLocalConfig(config, configDir = getDaemonConfigDir()) {
692
+ const parsed = DaemonLocalConfigSchema.parse(config);
693
+ await mkdir(configDir, { recursive: true, mode: 448 });
694
+ const target = daemonConfigPath(configDir);
695
+ await writeFile(target, `${JSON.stringify(parsed, null, 2)}
696
+ `, {
697
+ mode: 384
698
+ });
699
+ await chmod(target, 384);
700
+ }
701
+
702
+ // src/local-api/server.ts
703
+ import { createServer } from "http";
704
+ import { readFile as readFile3, realpath, stat } from "fs/promises";
705
+ import path4 from "path";
706
+ import { execa as execa3 } from "execa";
707
+ import {
708
+ resolveInsideWorkspace as resolveInsideWorkspace2,
709
+ scanCompanyManifest
710
+ } from "@xdsjs/dossierx-workspace";
711
+
712
+ // src/task-archive.ts
713
+ import { randomUUID } from "crypto";
714
+ import { appendFile, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
715
+ import path3 from "path";
716
+ import { resolveInsideWorkspace } from "@xdsjs/dossierx-workspace";
717
+ var TASK_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
718
+ function assertTaskId(taskId) {
719
+ if (!TASK_ID_PATTERN.test(taskId)) {
720
+ throw new Error("Invalid task id");
721
+ }
722
+ }
723
+ function taskDirectory(workspaceRoot, taskId) {
724
+ assertTaskId(taskId);
725
+ return resolveInsideWorkspace(
726
+ workspaceRoot,
727
+ path3.posix.join(".dossierx", "tasks", taskId)
728
+ );
729
+ }
730
+ async function ensureTaskDirectory(workspaceRoot, taskId) {
731
+ const directory = taskDirectory(workspaceRoot, taskId);
732
+ await mkdir2(directory, { recursive: true });
733
+ return directory;
734
+ }
735
+ function parseEventLine(line) {
736
+ if (!line.trim()) {
737
+ return null;
738
+ }
739
+ try {
740
+ const parsed = JSON.parse(line);
741
+ return parsed;
742
+ } catch {
743
+ return null;
744
+ }
745
+ }
746
+ function createTaskArchive(options) {
747
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
748
+ return {
749
+ async appendEvent(taskId, event) {
750
+ const directory = await ensureTaskDirectory(options.workspaceRoot, taskId);
751
+ const archivedEvent = {
752
+ id: randomUUID(),
753
+ task_id: taskId,
754
+ level: event.level,
755
+ message: event.message,
756
+ data: event.data,
757
+ created_at: now().toISOString()
758
+ };
759
+ await appendFile(
760
+ path3.join(directory, "events.jsonl"),
761
+ `${JSON.stringify(archivedEvent)}
762
+ `,
763
+ "utf8"
764
+ );
765
+ return archivedEvent;
766
+ },
767
+ async appendRawOutput(taskId, stream, chunk) {
768
+ const directory = await ensureTaskDirectory(options.workspaceRoot, taskId);
769
+ await appendFile(path3.join(directory, `${stream}.log`), chunk, "utf8");
770
+ },
771
+ async listEvents(taskId) {
772
+ const directory = taskDirectory(options.workspaceRoot, taskId);
773
+ let content = "";
774
+ try {
775
+ content = await readFile2(path3.join(directory, "events.jsonl"), "utf8");
776
+ } catch (error) {
777
+ if (error.code === "ENOENT") {
778
+ return [];
779
+ }
780
+ throw error;
781
+ }
782
+ return content.split("\n").map(parseEventLine).filter((event) => event !== null);
783
+ }
784
+ };
785
+ }
786
+
787
+ // src/local-api/server.ts
788
+ var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
789
+ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".txt", ".json"]);
790
+ var MARKETS = /* @__PURE__ */ new Set(["us", "hk", "cn"]);
791
+ function json(response, status, body, origin) {
792
+ response.writeHead(status, {
793
+ "content-type": "application/json; charset=utf-8",
794
+ ...origin ? { "access-control-allow-origin": origin } : {},
795
+ "access-control-allow-methods": "GET, OPTIONS",
796
+ "access-control-allow-headers": "content-type",
797
+ vary: "Origin"
798
+ });
799
+ response.end(JSON.stringify(body));
800
+ }
801
+ function allowedOrigin(request, allowedOrigins) {
802
+ const origin = request.headers.origin;
803
+ if (!origin) {
804
+ return "";
805
+ }
806
+ return allowedOrigins.includes(origin) ? origin : null;
807
+ }
808
+ function requestUrl(request) {
809
+ return new URL(request.url ?? "/", "http://127.0.0.1");
810
+ }
811
+ async function readWorkspaceFile(options) {
812
+ const extension = path4.extname(options.relativePath).toLowerCase();
813
+ if (!ALLOWED_EXTENSIONS.has(extension)) {
814
+ throw new Error("Only markdown, text, and json files can be previewed");
815
+ }
816
+ const absolutePath = resolveInsideWorkspace2(
817
+ options.workspaceRoot,
818
+ options.relativePath
819
+ );
820
+ const stats = await stat(absolutePath);
821
+ if (!stats.isFile()) {
822
+ throw new Error("Path is not a file");
823
+ }
824
+ if (stats.size > options.maxFileBytes) {
825
+ throw new Error("File is too large to preview");
826
+ }
827
+ return {
828
+ path: options.relativePath,
829
+ content: await readFile3(absolutePath, "utf8"),
830
+ size: stats.size,
831
+ updatedAt: stats.mtime.toISOString()
832
+ };
833
+ }
834
+ async function readCompanyManifest(options) {
835
+ if (!MARKETS.has(options.market)) {
836
+ throw new Error("Invalid market");
837
+ }
838
+ return {
839
+ manifest: await scanCompanyManifest({
840
+ workspaceRoot: options.workspaceRoot,
841
+ ticker: options.ticker,
842
+ market: options.market
843
+ })
844
+ };
845
+ }
846
+ function uniqueProbes(probes) {
847
+ const seen = /* @__PURE__ */ new Set();
848
+ return probes.filter((probe) => {
849
+ const command = probe.command?.trim();
850
+ if (!command) {
851
+ return false;
852
+ }
853
+ const key = `${probe.source}:${command}`;
854
+ if (seen.has(key)) {
855
+ return false;
856
+ }
857
+ seen.add(key);
858
+ probe.command = command;
859
+ return true;
860
+ });
861
+ }
862
+ async function resolveCommand(command) {
863
+ const normalizedCommand = command.trim();
864
+ if (path4.isAbsolute(normalizedCommand)) {
865
+ return realpath(normalizedCommand).catch(() => normalizedCommand);
866
+ }
867
+ const result = await execa3("which", [normalizedCommand], {
868
+ stdin: "ignore",
869
+ reject: false,
870
+ timeout: 3e3
871
+ });
872
+ const resolved = result.stdout.trim().split("\n")[0];
873
+ if (!resolved || result.exitCode !== 0) {
874
+ return normalizedCommand;
875
+ }
876
+ return realpath(resolved).catch(() => resolved);
877
+ }
878
+ async function detectCodexCommand(probe) {
879
+ const command = probe.command?.trim();
880
+ if (!command) {
881
+ return null;
882
+ }
883
+ try {
884
+ const result = await execa3(command, ["--version"], {
885
+ stdin: "ignore",
886
+ timeout: 5e3,
887
+ reject: false
888
+ });
889
+ if (result.exitCode !== 0) {
890
+ return null;
891
+ }
892
+ const version = result.stdout.trim() || result.stderr.trim();
893
+ if (!version) {
894
+ return null;
895
+ }
896
+ return {
897
+ command,
898
+ version,
899
+ resolvedCommand: await resolveCommand(command),
900
+ source: probe.source,
901
+ recommended: probe.recommended ?? false
902
+ };
903
+ } catch {
904
+ return null;
905
+ }
906
+ }
907
+ async function detectCodexCommands(configuredCommand) {
908
+ const probes = uniqueProbes([
909
+ {
910
+ command: configuredCommand,
911
+ source: "configured",
912
+ recommended: true
913
+ },
914
+ {
915
+ command: process.env.DOSSIERX_CODEX_COMMAND,
916
+ source: "env",
917
+ recommended: true
918
+ },
919
+ {
920
+ command: "/opt/homebrew/bin/codex",
921
+ source: "homebrew",
922
+ recommended: true
923
+ },
924
+ {
925
+ command: "/Applications/Codex.app/Contents/Resources/codex",
926
+ source: "app"
927
+ },
928
+ {
929
+ command: "/usr/local/bin/codex",
930
+ source: "npm"
931
+ },
932
+ {
933
+ command: "codex",
934
+ source: "path"
935
+ }
936
+ ]);
937
+ const results = await Promise.all(probes.map(detectCodexCommand));
938
+ const candidates = results.filter(
939
+ (candidate) => candidate !== null
940
+ );
941
+ const seen = /* @__PURE__ */ new Set();
942
+ return candidates.filter((candidate) => {
943
+ const dedupeKey = candidate.resolvedCommand || candidate.command;
944
+ if (seen.has(dedupeKey)) {
945
+ return false;
946
+ }
947
+ seen.add(dedupeKey);
948
+ return true;
949
+ });
950
+ }
951
+ async function startWorkspaceReadServer(options) {
952
+ const host = options.host ?? "127.0.0.1";
953
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
954
+ const taskArchive = options.taskArchive ?? createTaskArchive({ workspaceRoot: options.workspaceRoot });
955
+ const server = createServer((request, response) => {
956
+ void (async () => {
957
+ const origin = allowedOrigin(request, options.allowedOrigins);
958
+ if (origin === null) {
959
+ json(response, 403, { error: "Origin not allowed" });
960
+ return;
961
+ }
962
+ if (request.method === "OPTIONS") {
963
+ json(response, 204, {}, origin);
964
+ return;
965
+ }
966
+ const url = requestUrl(request);
967
+ if (request.method === "GET" && url.pathname === "/runtime/codex") {
968
+ json(
969
+ response,
970
+ 200,
971
+ { candidates: await detectCodexCommands(options.codexCommand) },
972
+ origin
973
+ );
974
+ return;
975
+ }
976
+ if (request.method === "GET" && url.pathname === "/workspace/manifest") {
977
+ const ticker = url.searchParams.get("ticker");
978
+ const market = url.searchParams.get("market");
979
+ if (!ticker || !market) {
980
+ json(response, 400, { error: "Missing ticker or market" }, origin);
981
+ return;
982
+ }
983
+ try {
984
+ json(
985
+ response,
986
+ 200,
987
+ await readCompanyManifest({
988
+ workspaceRoot: options.workspaceRoot,
989
+ ticker,
990
+ market
991
+ }),
992
+ origin
993
+ );
994
+ } catch (error) {
995
+ json(
996
+ response,
997
+ 400,
998
+ {
999
+ error: error instanceof Error ? error.message : "Workspace manifest failed"
1000
+ },
1001
+ origin
1002
+ );
1003
+ }
1004
+ return;
1005
+ }
1006
+ const taskEventsMatch = url.pathname.match(
1007
+ /^\/tasks\/([^/]+)\/events$/
1008
+ );
1009
+ if (request.method === "GET" && taskEventsMatch) {
1010
+ try {
1011
+ json(
1012
+ response,
1013
+ 200,
1014
+ { events: await taskArchive.listEvents(taskEventsMatch[1] ?? "") },
1015
+ origin
1016
+ );
1017
+ } catch (error) {
1018
+ json(
1019
+ response,
1020
+ 400,
1021
+ {
1022
+ error: error instanceof Error ? error.message : "Task event read failed"
1023
+ },
1024
+ origin
1025
+ );
1026
+ }
1027
+ return;
1028
+ }
1029
+ if (request.method !== "GET" || url.pathname !== "/workspace/read") {
1030
+ json(response, 404, { error: "Not found" }, origin);
1031
+ return;
1032
+ }
1033
+ const relativePath = url.searchParams.get("path");
1034
+ if (!relativePath) {
1035
+ json(response, 400, { error: "Missing workspace-relative path" }, origin);
1036
+ return;
1037
+ }
1038
+ try {
1039
+ json(
1040
+ response,
1041
+ 200,
1042
+ await readWorkspaceFile({
1043
+ workspaceRoot: options.workspaceRoot,
1044
+ relativePath,
1045
+ maxFileBytes
1046
+ }),
1047
+ origin
1048
+ );
1049
+ } catch (error) {
1050
+ json(
1051
+ response,
1052
+ error instanceof Error && error.message.includes("large") ? 413 : 400,
1053
+ { error: error instanceof Error ? error.message : "Read failed" },
1054
+ origin
1055
+ );
1056
+ }
1057
+ })();
1058
+ });
1059
+ await new Promise((resolve, reject) => {
1060
+ server.once("error", reject);
1061
+ server.listen(options.port, host, () => {
1062
+ server.off("error", reject);
1063
+ resolve();
1064
+ });
1065
+ });
1066
+ const address = server.address();
1067
+ const port = typeof address === "object" && address ? address.port : options.port;
1068
+ return {
1069
+ url: `http://${host}:${port}`,
1070
+ async close() {
1071
+ await new Promise((resolve, reject) => {
1072
+ server.close((error) => error ? reject(error) : resolve());
1073
+ });
1074
+ }
1075
+ };
1076
+ }
1077
+
1078
+ // src/loop.ts
1079
+ import { z as z2 } from "zod";
1080
+ import {
1081
+ TaskEventInputSchema,
1082
+ TaskSchema
1083
+ } from "@xdsjs/dossierx-shared";
1084
+
1085
+ // src/errors.ts
1086
+ function failTaskError(error, code = "UNKNOWN", step) {
1087
+ return {
1088
+ error: {
1089
+ code,
1090
+ message: error instanceof Error ? error.message : String(error),
1091
+ step,
1092
+ recoverable: false
1093
+ }
1094
+ };
1095
+ }
1096
+
1097
+ // src/executors/codex.ts
1098
+ import { readdir, stat as stat2 } from "fs/promises";
1099
+ import {
1100
+ buildCompanyManifest,
1101
+ companyManifestPath,
1102
+ investWikiRoot,
1103
+ resolveInsideWorkspace as resolveInsideWorkspace3
1104
+ } from "@xdsjs/dossierx-workspace";
1105
+ function isWorkspaceGuardError(error) {
1106
+ if (!(error instanceof Error)) {
1107
+ return false;
1108
+ }
1109
+ return [
1110
+ "Workspace root must be absolute",
1111
+ "Path must be relative to workspace",
1112
+ "Path escapes workspace"
1113
+ ].includes(error.message);
1114
+ }
1115
+ function workspacePath(workspaceRoot, relativePath, step) {
1116
+ try {
1117
+ return resolveInsideWorkspace3(workspaceRoot, relativePath);
1118
+ } catch (error) {
1119
+ if (isWorkspaceGuardError(error)) {
1120
+ throw new CodexRuntimeError(
1121
+ "WORKSPACE_ACCESS_DENIED",
1122
+ error instanceof Error ? error.message : "Workspace path is not allowed",
1123
+ step
1124
+ );
1125
+ }
1126
+ throw error;
1127
+ }
1128
+ }
1129
+ function requireRunner(context) {
1130
+ if (!context.codex) {
1131
+ throw new CodexRuntimeError(
1132
+ "RUNTIME_NOT_FOUND",
1133
+ "Codex runner is not configured",
1134
+ "codex.runner.resolve"
1135
+ );
1136
+ }
1137
+ return context.codex;
1138
+ }
1139
+ async function requireFile(filePath, step) {
1140
+ try {
1141
+ const fileStat = await stat2(filePath);
1142
+ if (fileStat.isFile()) {
1143
+ return;
1144
+ }
1145
+ } catch (error) {
1146
+ if (error.code !== "ENOENT") {
1147
+ throw error;
1148
+ }
1149
+ }
1150
+ throw new CodexRuntimeError(
1151
+ "WORKSPACE_NOT_FOUND",
1152
+ "Invest wiki vault is not initialized",
1153
+ step
1154
+ );
1155
+ }
1156
+ async function requireInitializedVault(workspaceRoot, ticker, step) {
1157
+ const vaultRoot = workspacePath(workspaceRoot, investWikiRoot(ticker), step);
1158
+ try {
1159
+ const vaultStat = await stat2(vaultRoot);
1160
+ if (!vaultStat.isDirectory()) {
1161
+ throw new CodexRuntimeError(
1162
+ "WORKSPACE_NOT_FOUND",
1163
+ "Invest wiki vault is not a directory",
1164
+ step
1165
+ );
1166
+ }
1167
+ } catch (error) {
1168
+ if (error.code !== "ENOENT") {
1169
+ throw error;
1170
+ }
1171
+ throw new CodexRuntimeError(
1172
+ "WORKSPACE_NOT_FOUND",
1173
+ "Invest wiki vault does not exist",
1174
+ step
1175
+ );
1176
+ }
1177
+ await Promise.all([
1178
+ requireFile(
1179
+ workspacePath(
1180
+ workspaceRoot,
1181
+ `${investWikiRoot(ticker)}/.llm-wiki-invest/config.toml`,
1182
+ step
1183
+ ),
1184
+ step
1185
+ ),
1186
+ requireFile(
1187
+ workspacePath(workspaceRoot, `${investWikiRoot(ticker)}/AGENTS.md`, step),
1188
+ step
1189
+ ),
1190
+ requireFile(
1191
+ workspacePath(
1192
+ workspaceRoot,
1193
+ `${investWikiRoot(ticker)}/.agents/skills/invest-wiki-flow/SKILL.md`,
1194
+ step
1195
+ ),
1196
+ step
1197
+ )
1198
+ ]).catch((error) => {
1199
+ if (error.code === "ENOENT") {
1200
+ throw new CodexRuntimeError(
1201
+ "WORKSPACE_NOT_FOUND",
1202
+ "Invest wiki vault is not initialized",
1203
+ step
1204
+ );
1205
+ }
1206
+ throw error;
1207
+ });
1208
+ return vaultRoot;
1209
+ }
1210
+ async function appendOutputEvent(context, message, output) {
1211
+ await context.appendEvent({
1212
+ level: "info",
1213
+ message,
1214
+ data: {
1215
+ stdoutChars: output.stdout.length,
1216
+ stderrChars: output.stderr.length,
1217
+ stdoutBytes: Buffer.byteLength(output.stdout),
1218
+ stderrBytes: Buffer.byteLength(output.stderr),
1219
+ stdoutTruncated: output.stdout.length >= 2e3,
1220
+ stderrTruncated: output.stderr.length >= 2e3
1221
+ }
1222
+ });
1223
+ }
1224
+ async function directoryHasFiles(root, relativePath) {
1225
+ const directory = resolveInsideWorkspace3(root, relativePath);
1226
+ let entries;
1227
+ try {
1228
+ entries = await readdir(directory, { withFileTypes: true });
1229
+ } catch (error) {
1230
+ if (error.code === "ENOENT") {
1231
+ return false;
1232
+ }
1233
+ throw error;
1234
+ }
1235
+ for (const entry of entries) {
1236
+ if (entry.name.startsWith(".")) {
1237
+ continue;
1238
+ }
1239
+ const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1240
+ if (entry.isFile()) {
1241
+ return true;
1242
+ }
1243
+ if (entry.isDirectory() && await directoryHasFiles(root, childPath)) {
1244
+ return true;
1245
+ }
1246
+ }
1247
+ return false;
1248
+ }
1249
+ async function assertProducedKnowledgeFiles(vaultRoot, step) {
1250
+ const hasFiles = await directoryHasFiles(vaultRoot, "sources") || await directoryHasFiles(vaultRoot, "wiki");
1251
+ if (hasFiles) {
1252
+ return;
1253
+ }
1254
+ throw new CodexRuntimeError(
1255
+ "COMMAND_FAILED",
1256
+ "Codex invest-wiki flow produced no source or wiki files",
1257
+ step
1258
+ );
1259
+ }
1260
+ async function runCodexTask(task, context) {
1261
+ const runner = requireRunner(context);
1262
+ const ticker = task.payload.ticker;
1263
+ const step = "codex.run_invest_wiki_flow";
1264
+ const vaultRoot = await requireInitializedVault(
1265
+ context.workspaceRoot,
1266
+ ticker,
1267
+ step
1268
+ );
1269
+ await context.appendEvent({
1270
+ level: "info",
1271
+ message: "Codex invest-wiki flow started",
1272
+ data: { ticker, path: investWikiRoot(ticker) }
1273
+ });
1274
+ const output = await runner.runInvestWikiFlow(
1275
+ {
1276
+ vaultRoot,
1277
+ ticker,
1278
+ market: task.payload.market,
1279
+ companyName: task.payload.companyName,
1280
+ cik: task.payload.cik,
1281
+ exchange: task.payload.exchange
1282
+ },
1283
+ {
1284
+ onEvent: (event) => context.appendEvent(event),
1285
+ onRawOutput: (stream, chunk) => context.appendRawOutput?.(stream, chunk)
1286
+ }
1287
+ );
1288
+ await assertProducedKnowledgeFiles(vaultRoot, step);
1289
+ await appendOutputEvent(context, "Codex invest-wiki flow completed", output);
1290
+ const manifest = await buildCompanyManifest({
1291
+ workspaceRoot: context.workspaceRoot,
1292
+ ticker,
1293
+ market: task.payload.market
1294
+ });
1295
+ return {
1296
+ generatedFiles: [],
1297
+ manifestPath: companyManifestPath(ticker),
1298
+ manifest
1299
+ };
1300
+ }
1301
+
1302
+ // src/executors/investWiki.ts
1303
+ import { access as access2, mkdir as mkdir3, readFile as readFile4, stat as stat3, writeFile as writeFile2 } from "fs/promises";
1304
+ import path5 from "path";
1305
+ import {
1306
+ buildCompanyManifest as buildCompanyManifest2,
1307
+ companyManifestPath as companyManifestPath2,
1308
+ investWikiConfigPath,
1309
+ investWikiRoot as investWikiRoot2,
1310
+ resolveInsideWorkspace as resolveInsideWorkspace4
1311
+ } from "@xdsjs/dossierx-workspace";
1312
+ async function exists(absolutePath) {
1313
+ try {
1314
+ await access2(absolutePath);
1315
+ return true;
1316
+ } catch (error) {
1317
+ if (error.code === "ENOENT") {
1318
+ return false;
1319
+ }
1320
+ throw error;
1321
+ }
1322
+ }
1323
+ function isWorkspaceGuardError2(error) {
1324
+ if (!(error instanceof Error)) {
1325
+ return false;
1326
+ }
1327
+ return [
1328
+ "Workspace root must be absolute",
1329
+ "Path must be relative to workspace",
1330
+ "Path escapes workspace"
1331
+ ].includes(error.message);
1332
+ }
1333
+ function workspacePath2(workspaceRoot, relativePath, step) {
1334
+ try {
1335
+ return resolveInsideWorkspace4(workspaceRoot, relativePath);
1336
+ } catch (error) {
1337
+ if (isWorkspaceGuardError2(error)) {
1338
+ throw new InvestWikiRuntimeError(
1339
+ "WORKSPACE_ACCESS_DENIED",
1340
+ error instanceof Error ? error.message : "Workspace path is not allowed",
1341
+ step
1342
+ );
1343
+ }
1344
+ throw error;
1345
+ }
1346
+ }
1347
+ function requireRunner2(context) {
1348
+ if (!context.investWiki) {
1349
+ throw new InvestWikiRuntimeError(
1350
+ "RUNTIME_NOT_FOUND",
1351
+ "Invest wiki runner is not configured",
1352
+ "invest_wiki.runner.resolve"
1353
+ );
1354
+ }
1355
+ return context.investWiki;
1356
+ }
1357
+ async function requireVaultRoot(workspaceRoot, ticker, step) {
1358
+ const vaultRoot = workspacePath2(workspaceRoot, investWikiRoot2(ticker), step);
1359
+ try {
1360
+ const vaultStat = await stat3(vaultRoot);
1361
+ if (vaultStat.isDirectory()) {
1362
+ return vaultRoot;
1363
+ }
1364
+ } catch (error) {
1365
+ if (error.code !== "ENOENT") {
1366
+ throw error;
1367
+ }
1368
+ }
1369
+ throw new InvestWikiRuntimeError(
1370
+ "WORKSPACE_NOT_FOUND",
1371
+ "Invest wiki vault does not exist",
1372
+ step
1373
+ );
1374
+ }
1375
+ async function appendOutputEvent2(context, message, output) {
1376
+ const stdoutTruncated = output.stdout.length > 2e3;
1377
+ const stderrTruncated = output.stderr.length > 2e3;
1378
+ await context.appendEvent({
1379
+ level: "info",
1380
+ message,
1381
+ data: {
1382
+ stdoutChars: output.stdout.length,
1383
+ stderrChars: output.stderr.length,
1384
+ stdoutBytes: Buffer.byteLength(output.stdout),
1385
+ stderrBytes: Buffer.byteLength(output.stderr),
1386
+ stdoutTruncated,
1387
+ stderrTruncated
1388
+ }
1389
+ });
1390
+ }
1391
+ function dossierInitArgs(payload) {
1392
+ const args = [
1393
+ "dossier",
1394
+ "init",
1395
+ "--market",
1396
+ payload.market,
1397
+ "--ticker",
1398
+ payload.ticker,
1399
+ "--company-name",
1400
+ payload.companyName
1401
+ ];
1402
+ if (payload.cik) {
1403
+ args.push("--cik", payload.cik);
1404
+ }
1405
+ if (payload.exchange) {
1406
+ args.push("--exchange", payload.exchange);
1407
+ }
1408
+ return args;
1409
+ }
1410
+ function isMarket(value) {
1411
+ return value === "us" || value === "hk" || value === "cn";
1412
+ }
1413
+ async function readManifestMarket(workspaceRoot, ticker) {
1414
+ const manifestPath = workspacePath2(
1415
+ workspaceRoot,
1416
+ companyManifestPath2(ticker),
1417
+ "invest_wiki.sync"
1418
+ );
1419
+ try {
1420
+ const manifest = JSON.parse(await readFile4(manifestPath, "utf8"));
1421
+ if (!isMarket(manifest.market)) {
1422
+ throw new InvestWikiRuntimeError(
1423
+ "VALIDATION_ERROR",
1424
+ "Existing company manifest has missing or invalid market",
1425
+ "invest_wiki.sync"
1426
+ );
1427
+ }
1428
+ return manifest.market;
1429
+ } catch (error) {
1430
+ if (error.code === "ENOENT") {
1431
+ return "us";
1432
+ }
1433
+ if (error instanceof SyntaxError) {
1434
+ throw new InvestWikiRuntimeError(
1435
+ "VALIDATION_ERROR",
1436
+ "Existing company manifest is not valid JSON",
1437
+ "invest_wiki.sync"
1438
+ );
1439
+ }
1440
+ throw error;
1441
+ }
1442
+ }
1443
+ async function runInitCompanyVault(task, context) {
1444
+ const runner = requireRunner2(context);
1445
+ const ticker = task.payload.ticker;
1446
+ const vaultRelativePath = investWikiRoot2(ticker);
1447
+ const vaultRoot = workspacePath2(
1448
+ context.workspaceRoot,
1449
+ vaultRelativePath,
1450
+ "invest_wiki.init_company_vault"
1451
+ );
1452
+ const markerRelativePath = investWikiConfigPath(ticker);
1453
+ const markerPath = workspacePath2(
1454
+ context.workspaceRoot,
1455
+ markerRelativePath,
1456
+ "invest_wiki.init_company_vault"
1457
+ );
1458
+ const dossierStatePath = workspacePath2(
1459
+ context.workspaceRoot,
1460
+ `${vaultRelativePath}/.llm-wiki-invest/dossier-state.json`,
1461
+ "invest_wiki.init_company_vault"
1462
+ );
1463
+ await mkdir3(vaultRoot, { recursive: true });
1464
+ await context.appendEvent({
1465
+ level: "info",
1466
+ message: "Initializing invest-wiki vault",
1467
+ data: { ticker, path: vaultRelativePath }
1468
+ });
1469
+ if (!await exists(markerPath)) {
1470
+ const output = await runner.run(["init"], { cwd: vaultRoot });
1471
+ await appendOutputEvent2(context, "Invest wiki init completed", output);
1472
+ }
1473
+ if (!await exists(dossierStatePath)) {
1474
+ const output = await runner.run(dossierInitArgs(task.payload), {
1475
+ cwd: vaultRoot
1476
+ });
1477
+ await appendOutputEvent2(context, "Invest wiki dossier initialized", output);
1478
+ }
1479
+ const statusOutput = await runner.run(["dossier", "status"], { cwd: vaultRoot });
1480
+ await appendOutputEvent2(context, "Invest wiki dossier status completed", statusOutput);
1481
+ if (!await exists(markerPath)) {
1482
+ await mkdir3(path5.dirname(markerPath), { recursive: true });
1483
+ await writeFile2(markerPath, "# Created by dossierx-daemon\n");
1484
+ }
1485
+ const manifest = await buildCompanyManifest2({
1486
+ workspaceRoot: context.workspaceRoot,
1487
+ ticker,
1488
+ market: task.payload.market
1489
+ });
1490
+ await context.appendEvent({
1491
+ level: "info",
1492
+ message: "Invest wiki company vault initialized",
1493
+ data: { ticker }
1494
+ });
1495
+ return {
1496
+ generatedFiles: [
1497
+ vaultRelativePath,
1498
+ markerRelativePath,
1499
+ companyManifestPath2(ticker)
1500
+ ],
1501
+ manifestPath: companyManifestPath2(ticker),
1502
+ manifest
1503
+ };
1504
+ }
1505
+ async function runStatus(task, context) {
1506
+ const runner = requireRunner2(context);
1507
+ const ticker = task.payload.ticker;
1508
+ const vaultRoot = await requireVaultRoot(
1509
+ context.workspaceRoot,
1510
+ ticker,
1511
+ "invest_wiki.status"
1512
+ );
1513
+ const statusOutput = await runner.run(["status"], { cwd: vaultRoot });
1514
+ await appendOutputEvent2(context, "Invest wiki status completed", statusOutput);
1515
+ const dossierStatusOutput = await runner.run(["dossier", "status"], {
1516
+ cwd: vaultRoot
1517
+ });
1518
+ await appendOutputEvent2(
1519
+ context,
1520
+ "Invest wiki dossier status completed",
1521
+ dossierStatusOutput
1522
+ );
1523
+ return {
1524
+ generatedFiles: [],
1525
+ manifestPath: companyManifestPath2(ticker)
1526
+ };
1527
+ }
1528
+ async function runSync(task, context) {
1529
+ const runner = requireRunner2(context);
1530
+ const ticker = task.payload.ticker;
1531
+ const vaultRoot = await requireVaultRoot(
1532
+ context.workspaceRoot,
1533
+ ticker,
1534
+ "invest_wiki.sync"
1535
+ );
1536
+ const market = await readManifestMarket(context.workspaceRoot, ticker);
1537
+ const args = task.payload.dryRun ? ["sync", "--dry-run"] : ["sync"];
1538
+ const output = await runner.run(args, { cwd: vaultRoot });
1539
+ await appendOutputEvent2(context, "Invest wiki sync completed", output);
1540
+ const manifest = await buildCompanyManifest2({
1541
+ workspaceRoot: context.workspaceRoot,
1542
+ ticker,
1543
+ market
1544
+ });
1545
+ return {
1546
+ generatedFiles: [],
1547
+ manifestPath: companyManifestPath2(ticker),
1548
+ manifest
1549
+ };
1550
+ }
1551
+ async function runInvestWikiTask(task, context) {
1552
+ if (task.type === "invest_wiki.init_company_vault") {
1553
+ return runInitCompanyVault(task, context);
1554
+ }
1555
+ if (task.type === "invest_wiki.status") {
1556
+ return runStatus(task, context);
1557
+ }
1558
+ if (task.type === "invest_wiki.sync") {
1559
+ return runSync(task, context);
1560
+ }
1561
+ throw new InvestWikiRuntimeError(
1562
+ "VALIDATION_ERROR",
1563
+ "Unsupported invest wiki task type",
1564
+ "invest_wiki.executor"
1565
+ );
1566
+ }
1567
+
1568
+ // src/executors/mockWriteCompanyReport.ts
1569
+ import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
1570
+ import path6 from "path";
1571
+ import {
1572
+ buildCompanyManifest as buildCompanyManifest3,
1573
+ companyManifestPath as companyManifestPath3,
1574
+ companyRoot,
1575
+ resolveInsideWorkspace as resolveInsideWorkspace5,
1576
+ rightBusinessPath,
1577
+ rightPeoplePath,
1578
+ rightPricePath
1579
+ } from "@xdsjs/dossierx-workspace";
1580
+ var rightBusiness = (ticker) => `# Right Business - ${ticker}
1581
+
1582
+ > Mock output generated by dossierx-daemon.
1583
+
1584
+ ## One-line conclusion
1585
+
1586
+ This is a placeholder right-business analysis for ${ticker}.
1587
+
1588
+ ## Evidence
1589
+
1590
+ - No real source used in MVP mock executor.
1591
+ `;
1592
+ var rightPeople = (ticker) => `# Right People - ${ticker}
1593
+
1594
+ > Mock output generated by dossierx-daemon.
1595
+
1596
+ ## One-line conclusion
1597
+
1598
+ This is a placeholder right-people analysis for ${ticker}.
1599
+ `;
1600
+ var rightPrice = (ticker) => `# Right Price - ${ticker}
1601
+
1602
+ > Mock output generated by dossierx-daemon.
1603
+
1604
+ ## One-line conclusion
1605
+
1606
+ This is a placeholder right-price analysis for ${ticker}.
1607
+ `;
1608
+ async function writeWorkspaceFile(context, relativePath, content) {
1609
+ const absolutePath = resolveInsideWorkspace5(context.workspaceRoot, relativePath);
1610
+ await mkdir4(path6.dirname(absolutePath), { recursive: true });
1611
+ if (!context.dryRun) {
1612
+ await writeFile3(absolutePath, content);
1613
+ }
1614
+ }
1615
+ async function runMockWriteCompanyReport(task, context) {
1616
+ const ticker = task.payload.ticker;
1617
+ const generatedFiles = [
1618
+ rightBusinessPath(ticker),
1619
+ rightPeoplePath(ticker),
1620
+ rightPricePath(ticker)
1621
+ ];
1622
+ await context.appendEvent({
1623
+ level: "info",
1624
+ message: "Creating company directory",
1625
+ data: { ticker }
1626
+ });
1627
+ await mkdir4(resolveInsideWorkspace5(context.workspaceRoot, companyRoot(ticker)), {
1628
+ recursive: true
1629
+ });
1630
+ await context.appendEvent({
1631
+ level: "info",
1632
+ message: "Writing right-business.md",
1633
+ data: { path: rightBusinessPath(ticker) }
1634
+ });
1635
+ await writeWorkspaceFile(context, rightBusinessPath(ticker), rightBusiness(ticker));
1636
+ await context.appendEvent({
1637
+ level: "info",
1638
+ message: "Writing right-people.md",
1639
+ data: { path: rightPeoplePath(ticker) }
1640
+ });
1641
+ await writeWorkspaceFile(context, rightPeoplePath(ticker), rightPeople(ticker));
1642
+ await context.appendEvent({
1643
+ level: "info",
1644
+ message: "Writing right-price.md",
1645
+ data: { path: rightPricePath(ticker) }
1646
+ });
1647
+ await writeWorkspaceFile(context, rightPricePath(ticker), rightPrice(ticker));
1648
+ await context.appendEvent({
1649
+ level: "info",
1650
+ message: "Generating manifest.json",
1651
+ data: { path: companyManifestPath3(ticker) }
1652
+ });
1653
+ let manifest;
1654
+ if (!context.dryRun) {
1655
+ manifest = await buildCompanyManifest3({
1656
+ workspaceRoot: context.workspaceRoot,
1657
+ ticker,
1658
+ market: task.payload.market
1659
+ });
1660
+ }
1661
+ await context.appendEvent({
1662
+ level: "info",
1663
+ message: "Mock company report completed",
1664
+ data: { ticker }
1665
+ });
1666
+ return {
1667
+ generatedFiles: [...generatedFiles, companyManifestPath3(ticker)],
1668
+ manifestPath: companyManifestPath3(ticker),
1669
+ manifest
1670
+ };
1671
+ }
1672
+
1673
+ // src/executors/index.ts
1674
+ function getExecutor(task) {
1675
+ if (task.type.startsWith("codex.")) {
1676
+ return runCodexTask;
1677
+ }
1678
+ if (task.type.startsWith("invest_wiki.")) {
1679
+ return runInvestWikiTask;
1680
+ }
1681
+ if (task.type === "mock.write_company_report") {
1682
+ return runMockWriteCompanyReport;
1683
+ }
1684
+ return null;
1685
+ }
1686
+
1687
+ // src/loop.ts
1688
+ function taskIdFromUnknown(value) {
1689
+ if (!value || typeof value !== "object" || !("id" in value)) {
1690
+ return null;
1691
+ }
1692
+ const parsed = z2.string().uuid().safeParse(value.id);
1693
+ return parsed.success ? parsed.data : null;
1694
+ }
1695
+ async function appendEvent(ctx, taskId, message) {
1696
+ await appendLocalEvent(ctx, taskId, { level: "info", message, data: {} });
1697
+ }
1698
+ async function appendLocalEvent(ctx, taskId, event) {
1699
+ if (!ctx.taskArchive) {
1700
+ return;
1701
+ }
1702
+ try {
1703
+ await ctx.taskArchive.appendEvent(taskId, event);
1704
+ } catch (error) {
1705
+ ctx.logger.warn({ err: error, taskId }, "failed to archive task event");
1706
+ }
1707
+ }
1708
+ async function appendLocalRawOutput(ctx, taskId, stream, chunk) {
1709
+ if (!ctx.taskArchive) {
1710
+ return;
1711
+ }
1712
+ try {
1713
+ await ctx.taskArchive.appendRawOutput(taskId, stream, chunk);
1714
+ } catch (error) {
1715
+ ctx.logger.warn({ err: error, taskId }, "failed to archive raw task output");
1716
+ }
1717
+ }
1718
+ function codexRunnerForTask(ctx, agent) {
1719
+ if (!agent || agent.runtime !== "codex_cli") {
1720
+ return ctx.codex;
1721
+ }
1722
+ const codexCommand = agent.config.codexCommand === "codex" ? ctx.codexOptions?.codexCommand ?? agent.config.codexCommand : agent.config.codexCommand;
1723
+ return createCodexRunnerFromOptions({
1724
+ codexCommand,
1725
+ codexModel: agent.model,
1726
+ codexSandbox: agent.config.codexSandbox
1727
+ });
1728
+ }
1729
+ async function runTask(ctx, task, agent) {
1730
+ const executor = getExecutor(task);
1731
+ if (!executor) {
1732
+ await ctx.api.failTask(task.id, {
1733
+ error: {
1734
+ code: "VALIDATION_ERROR",
1735
+ message: `Unsupported task type: ${task.type}`,
1736
+ step: "executor.lookup",
1737
+ recoverable: false
1738
+ }
1739
+ });
1740
+ return;
1741
+ }
1742
+ await appendEvent(ctx, task.id, "Task claimed");
1743
+ try {
1744
+ const result = await executor(task, {
1745
+ workspaceRoot: ctx.workspaceRoot,
1746
+ dryRun: ctx.dryRun,
1747
+ codex: codexRunnerForTask(ctx, agent),
1748
+ investWiki: ctx.investWiki,
1749
+ appendEvent: async (event) => {
1750
+ await appendLocalEvent(ctx, task.id, TaskEventInputSchema.parse(event));
1751
+ },
1752
+ appendRawOutput: async (stream, chunk) => {
1753
+ await appendLocalRawOutput(ctx, task.id, stream, chunk);
1754
+ }
1755
+ });
1756
+ await appendEvent(ctx, task.id, "Task completed");
1757
+ await ctx.api.completeTask(task.id, { result });
1758
+ } catch (error) {
1759
+ if (error instanceof InvestWikiRuntimeError) {
1760
+ await appendLocalEvent(ctx, task.id, {
1761
+ level: "error",
1762
+ message: error.message,
1763
+ data: { code: error.code, step: error.step }
1764
+ });
1765
+ await ctx.api.failTask(task.id, {
1766
+ error: {
1767
+ code: error.code,
1768
+ message: error.message,
1769
+ step: error.step,
1770
+ recoverable: false
1771
+ }
1772
+ });
1773
+ return;
1774
+ }
1775
+ if (error instanceof CodexRuntimeError) {
1776
+ await appendLocalEvent(ctx, task.id, {
1777
+ level: "error",
1778
+ message: error.message,
1779
+ data: { code: error.code, step: error.step }
1780
+ });
1781
+ await ctx.api.failTask(task.id, {
1782
+ error: {
1783
+ code: error.code,
1784
+ message: error.message,
1785
+ step: error.step,
1786
+ recoverable: false
1787
+ }
1788
+ });
1789
+ return;
1790
+ }
1791
+ await appendLocalEvent(ctx, task.id, {
1792
+ level: "error",
1793
+ message: error instanceof Error ? error.message : "Task failed",
1794
+ data: { code: "UNKNOWN", step: "executor" }
1795
+ });
1796
+ await ctx.api.failTask(task.id, failTaskError(error, "UNKNOWN", "executor"));
1797
+ }
1798
+ }
1799
+ async function claimAndRunClaimedTask(ctx) {
1800
+ const response = await ctx.api.claimTask({
1801
+ machineId: ctx.machineId,
1802
+ maxTasks: 1
1803
+ });
1804
+ if (!response.task) {
1805
+ return;
1806
+ }
1807
+ const parsedTask = TaskSchema.safeParse(response.task);
1808
+ if (!parsedTask.success) {
1809
+ const taskId = taskIdFromUnknown(response.task);
1810
+ if (taskId) {
1811
+ await ctx.api.failTask(taskId, {
1812
+ error: {
1813
+ code: "VALIDATION_ERROR",
1814
+ message: "Claimed task payload failed schema validation",
1815
+ step: "task.validation",
1816
+ recoverable: false
1817
+ }
1818
+ });
1819
+ }
1820
+ return;
1821
+ }
1822
+ await runTask(ctx, parsedTask.data, response.agent ?? null);
1823
+ }
1824
+ async function claimAndRunOne(ctx) {
1825
+ if (ctx.state.running) {
1826
+ ctx.state.pending = true;
1827
+ return;
1828
+ }
1829
+ ctx.state.running = true;
1830
+ try {
1831
+ do {
1832
+ ctx.state.pending = false;
1833
+ await claimAndRunClaimedTask(ctx);
1834
+ } while (ctx.state.pending);
1835
+ } finally {
1836
+ ctx.state.running = false;
1837
+ }
1838
+ }
1839
+ function startPollingClaimLoop(ctx, intervalMs) {
1840
+ return setInterval(() => {
1841
+ void claimAndRunOne(ctx).catch((error) => {
1842
+ ctx.logger.warn({ err: error }, "fallback claim failed");
1843
+ });
1844
+ }, intervalMs);
1845
+ }
1846
+
1847
+ // src/logger.ts
1848
+ import pino from "pino";
1849
+ function createLogger(level = "info", stream) {
1850
+ return pino(
1851
+ {
1852
+ level,
1853
+ redact: {
1854
+ paths: [
1855
+ "machineKey",
1856
+ "machine_key",
1857
+ "authorization",
1858
+ "Authorization",
1859
+ "headers.authorization"
1860
+ ],
1861
+ censor: "[redacted]"
1862
+ }
1863
+ },
1864
+ stream
1865
+ );
1866
+ }
1867
+
1868
+ // src/runtime/detect.ts
1869
+ import { execa as execa4 } from "execa";
1870
+ async function commandResponds(command, args = ["--version"]) {
1871
+ try {
1872
+ await execa4(command, args, {
1873
+ reject: true,
1874
+ timeout: 2e3,
1875
+ stdout: "ignore",
1876
+ stderr: "ignore"
1877
+ });
1878
+ return true;
1879
+ } catch {
1880
+ return false;
1881
+ }
1882
+ }
1883
+ async function detectCapabilities() {
1884
+ const [git, python3, python, investWikiRuntime, codex, claude] = await Promise.all([
1885
+ commandResponds("git"),
1886
+ commandResponds("python3"),
1887
+ commandResponds("python"),
1888
+ commandResponds("invest-wiki-runtime"),
1889
+ commandResponds("codex"),
1890
+ commandResponds("claude")
1891
+ ]);
1892
+ return {
1893
+ git,
1894
+ node: true,
1895
+ python: python3 || python,
1896
+ investWikiRuntime,
1897
+ codex,
1898
+ claude
1899
+ };
1900
+ }
1901
+
1902
+ // src/realtime/client.ts
1903
+ import { createClient } from "@supabase/supabase-js";
1904
+ import {
1905
+ RealtimeEventSchema
1906
+ } from "@xdsjs/dossierx-shared";
1907
+ function normalizeBroadcast(message) {
1908
+ const value = message;
1909
+ if (value.payload && typeof value.payload === "object" && "event" in value.payload) {
1910
+ return value.payload;
1911
+ }
1912
+ return {
1913
+ event: value.event,
1914
+ payload: value.payload
1915
+ };
1916
+ }
1917
+ async function waitForSubscribed(channel) {
1918
+ await new Promise((resolve, reject) => {
1919
+ const timeout = setTimeout(
1920
+ () => reject(new Error("Realtime subscribe timeout")),
1921
+ 5e3
1922
+ );
1923
+ channel.subscribe((status) => {
1924
+ if (status === "SUBSCRIBED") {
1925
+ clearTimeout(timeout);
1926
+ resolve();
1927
+ }
1928
+ if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
1929
+ clearTimeout(timeout);
1930
+ reject(new Error(`Realtime subscribe failed: ${status}`));
1931
+ }
1932
+ });
1933
+ });
1934
+ }
1935
+ async function subscribeToTaskAvailable(options, onEvent, onInvalidEvent) {
1936
+ const client = createClient(options.supabaseUrl, options.supabaseAnonKey, {
1937
+ auth: {
1938
+ persistSession: false,
1939
+ autoRefreshToken: false
1940
+ }
1941
+ });
1942
+ const channel = client.channel(options.topic, {
1943
+ config: {
1944
+ private: options.privateChannel
1945
+ }
1946
+ });
1947
+ channel.on("broadcast", { event: "task_available" }, async (message) => {
1948
+ const parsed = RealtimeEventSchema.safeParse(normalizeBroadcast(message));
1949
+ if (!parsed.success) {
1950
+ onInvalidEvent?.(message);
1951
+ return;
1952
+ }
1953
+ await onEvent(parsed.data);
1954
+ });
1955
+ channel.on("broadcast", { event: "task_cancelled" }, async (message) => {
1956
+ const parsed = RealtimeEventSchema.safeParse(normalizeBroadcast(message));
1957
+ if (!parsed.success) {
1958
+ onInvalidEvent?.(message);
1959
+ return;
1960
+ }
1961
+ await onEvent(parsed.data);
1962
+ });
1963
+ await waitForSubscribed(channel);
1964
+ return {
1965
+ async unsubscribe() {
1966
+ await client.removeChannel(channel);
1967
+ }
1968
+ };
1969
+ }
1970
+
1971
+ // src/service.ts
1972
+ import { mkdir as mkdir5, unlink, writeFile as writeFile4 } from "fs/promises";
1973
+ import os2 from "os";
1974
+ import path7 from "path";
1975
+ var LAUNCH_AGENT_LABEL = "com.xdsjs.dossierx-daemon";
1976
+ function xmlEscape(value) {
1977
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1978
+ }
1979
+ function shellQuote(value) {
1980
+ return `'${value.replaceAll("'", "'\\''")}'`;
1981
+ }
1982
+ function launchAgentsDir() {
1983
+ return path7.join(os2.homedir(), "Library", "LaunchAgents");
1984
+ }
1985
+ function launchAgentPlistPath(label = LAUNCH_AGENT_LABEL, dir = launchAgentsDir()) {
1986
+ return path7.join(dir, `${label}.plist`);
1987
+ }
1988
+ function resolveDaemonProgramArguments(input) {
1989
+ if (input.daemonCommand?.trim()) {
1990
+ return ["/bin/zsh", "-lc", input.daemonCommand.trim()];
1991
+ }
1992
+ const argv = input.argv ?? process.argv;
1993
+ const entry = argv[1];
1994
+ if (!entry) {
1995
+ throw new Error("Unable to resolve daemon entrypoint for LaunchAgent");
1996
+ }
1997
+ if (path7.extname(entry) === ".ts") {
1998
+ throw new Error(
1999
+ "LaunchAgent cannot run a TypeScript daemon entrypoint directly; pass --daemon-command when installing from source"
2000
+ );
2001
+ }
2002
+ return [
2003
+ input.execPath ?? process.execPath,
2004
+ path7.resolve(entry),
2005
+ "--log-level",
2006
+ input.logLevel ?? "info"
2007
+ ];
2008
+ }
2009
+ function buildLaunchAgentPlist(input) {
2010
+ const args = input.programArguments.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
2011
+ return `<?xml version="1.0" encoding="UTF-8"?>
2012
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2013
+ <plist version="1.0">
2014
+ <dict>
2015
+ <key>Label</key>
2016
+ <string>${xmlEscape(input.label)}</string>
2017
+ <key>ProgramArguments</key>
2018
+ <array>
2019
+ ${args}
2020
+ </array>
2021
+ <key>WorkingDirectory</key>
2022
+ <string>${xmlEscape(input.workingDirectory)}</string>
2023
+ <key>RunAtLoad</key>
2024
+ <true/>
2025
+ <key>KeepAlive</key>
2026
+ <true/>
2027
+ <key>EnvironmentVariables</key>
2028
+ <dict>
2029
+ <key>DOSSIERX_CONFIG_DIR</key>
2030
+ <string>${xmlEscape(input.configDir)}</string>
2031
+ </dict>
2032
+ <key>StandardOutPath</key>
2033
+ <string>${xmlEscape(input.stdoutPath)}</string>
2034
+ <key>StandardErrorPath</key>
2035
+ <string>${xmlEscape(input.stderrPath)}</string>
2036
+ </dict>
2037
+ </plist>
2038
+ `;
2039
+ }
2040
+ function hasRuntimeOption(options) {
2041
+ return Object.values(options).some((value) => value !== void 0);
2042
+ }
2043
+ function runtimeOptionsFrom(input) {
2044
+ return {
2045
+ investWikiMode: input.investWikiMode,
2046
+ investWikiLocalRepo: input.investWikiLocalRepo,
2047
+ investWikiCommand: input.investWikiCommand,
2048
+ codexCommand: input.codexCommand,
2049
+ codexModel: input.codexModel,
2050
+ codexSandbox: input.codexSandbox,
2051
+ localApiPort: input.localApiPort
2052
+ };
2053
+ }
2054
+ async function installLaunchAgent(options = {}) {
2055
+ if (process.platform !== "darwin") {
2056
+ throw new Error("DossierX LaunchAgent service is only supported on macOS");
2057
+ }
2058
+ const label = options.label ?? LAUNCH_AGENT_LABEL;
2059
+ const configDir = path7.resolve(
2060
+ expandHomePath(options.configDir ?? getDaemonConfigDir())
2061
+ );
2062
+ const config = await readDaemonLocalConfig(configDir);
2063
+ if (!config) {
2064
+ throw new Error(
2065
+ `Missing local daemon config at ${daemonConfigPath(configDir)}. Run the generated daemon command once before installing the service.`
2066
+ );
2067
+ }
2068
+ const runtimeOptions = runtimeOptionsFrom(options);
2069
+ if (hasRuntimeOption(runtimeOptions)) {
2070
+ await writeDaemonLocalConfig(
2071
+ {
2072
+ ...config,
2073
+ ...runtimeOptions
2074
+ },
2075
+ configDir
2076
+ );
2077
+ }
2078
+ const plistPath = launchAgentPlistPath(label, options.launchAgentsDir);
2079
+ const stdoutPath = path7.join(configDir, "daemon.out.log");
2080
+ const stderrPath = path7.join(configDir, "daemon.err.log");
2081
+ const uid = typeof process.getuid === "function" ? process.getuid() : "<uid>";
2082
+ const programArguments = resolveDaemonProgramArguments({
2083
+ daemonCommand: options.daemonCommand,
2084
+ logLevel: options.logLevel,
2085
+ argv: options.argv,
2086
+ execPath: options.execPath
2087
+ });
2088
+ await mkdir5(path7.dirname(plistPath), { recursive: true });
2089
+ await mkdir5(configDir, { recursive: true, mode: 448 });
2090
+ await writeFile4(
2091
+ plistPath,
2092
+ buildLaunchAgentPlist({
2093
+ label,
2094
+ programArguments,
2095
+ configDir,
2096
+ workingDirectory: os2.homedir(),
2097
+ stdoutPath,
2098
+ stderrPath
2099
+ }),
2100
+ { mode: 420 }
2101
+ );
2102
+ return {
2103
+ label,
2104
+ plistPath,
2105
+ stdoutPath,
2106
+ stderrPath,
2107
+ loadCommand: `launchctl bootstrap gui/${uid} ${shellQuote(plistPath)} && launchctl kickstart -k gui/${uid}/${label}`,
2108
+ unloadCommand: `launchctl bootout gui/${uid} ${shellQuote(plistPath)}`
2109
+ };
2110
+ }
2111
+ async function uninstallLaunchAgent(options = {}) {
2112
+ const label = options.label ?? LAUNCH_AGENT_LABEL;
2113
+ const plistPath = launchAgentPlistPath(label, options.launchAgentsDir);
2114
+ const uid = typeof process.getuid === "function" ? process.getuid() : "<uid>";
2115
+ try {
2116
+ await unlink(plistPath);
2117
+ } catch (error) {
2118
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
2119
+ throw error;
2120
+ }
2121
+ }
2122
+ return {
2123
+ label,
2124
+ plistPath,
2125
+ unloadCommand: `launchctl bootout gui/${uid} ${shellQuote(plistPath)}`
2126
+ };
2127
+ }
2128
+
2129
+ // src/cli.ts
2130
+ async function assertWorkspaceExists(workspace) {
2131
+ const stats = await stat4(workspace);
2132
+ if (!stats.isDirectory()) {
2133
+ throw new Error("Workspace path is not a directory");
2134
+ }
2135
+ }
2136
+ var DOSSIERX_DEFAULT_LOCAL_API_PORT = 48731;
2137
+ function parsePort(value) {
2138
+ if (value === void 0) {
2139
+ return void 0;
2140
+ }
2141
+ const port = typeof value === "number" ? value : Number(value);
2142
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
2143
+ throw new Error("Local API port must be an integer from 1 to 65535");
2144
+ }
2145
+ return port;
2146
+ }
2147
+ function originFromUrl(value) {
2148
+ try {
2149
+ return new URL(value).origin;
2150
+ } catch {
2151
+ return null;
2152
+ }
2153
+ }
2154
+ function localApiAllowedOrigins(serverUrl) {
2155
+ return Array.from(
2156
+ new Set(
2157
+ [
2158
+ originFromUrl(serverUrl),
2159
+ "http://localhost:3000",
2160
+ "http://127.0.0.1:3000",
2161
+ ...(process.env.DOSSIERX_LOCAL_API_ALLOWED_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter(Boolean)
2162
+ ].filter((origin) => Boolean(origin))
2163
+ )
2164
+ );
2165
+ }
2166
+ function buildProgram() {
2167
+ const program = new Command().name("dossierx-daemon").description("DossierX local task daemon").option("--server-url <url>").option("--supabase-url <url>").option("--supabase-anon-key <key>").option("--machine-key <key>").option("--workspace <path>").option("--log-level <level>", "log level", "info").option("--once", "claim once and exit").option("--no-realtime", "disable Supabase Realtime subscription").option("--dry-run", "run without writing files").option("--invest-wiki-mode <mode>", "invest wiki runner mode: local-repo or package").option("--invest-wiki-local-repo <path>", "local llm-wiki-invest repo path").option(
2168
+ "--invest-wiki-command <command>",
2169
+ "installed llm-wiki-invest command"
2170
+ ).option("--codex-command <command>", "installed Codex CLI command").option("--codex-model <model>", "Codex model override for exec mode").option(
2171
+ "--codex-sandbox <mode>",
2172
+ "Codex sandbox mode: workspace-write or danger-full-access"
2173
+ ).option("--local-api-port <port>", "local workspace preview API port").option("--no-local-api", "disable local workspace preview API").action(async (options) => {
2174
+ await runDaemon(options);
2175
+ });
2176
+ const service = program.command("service").description("Manage the macOS LaunchAgent for dossierx-daemon");
2177
+ service.command("install").description("Write a macOS LaunchAgent plist for persistent daemon runs").option("--label <label>", "LaunchAgent label").option("--daemon-command <command>", "custom command used by launchd").option("--log-level <level>", "daemon log level", "info").option("--invest-wiki-mode <mode>", "invest wiki runner mode").option("--invest-wiki-local-repo <path>", "local llm-wiki-invest repo path").option("--invest-wiki-command <command>", "installed llm-wiki-invest command").option("--codex-command <command>", "installed Codex CLI command").option("--codex-model <model>", "Codex model override for exec mode").option("--codex-sandbox <mode>", "Codex sandbox mode").option("--local-api-port <port>", "local workspace preview API port").action(async (options) => {
2178
+ const result = await installLaunchAgent({
2179
+ ...options,
2180
+ localApiPort: parsePort(options.localApiPort)
2181
+ });
2182
+ console.log(`LaunchAgent written: ${result.plistPath}`);
2183
+ console.log(`Start: ${result.loadCommand}`);
2184
+ console.log(`Stop: ${result.unloadCommand}`);
2185
+ console.log(`Logs: ${result.stdoutPath}`);
2186
+ console.log(`Errors: ${result.stderrPath}`);
2187
+ });
2188
+ service.command("uninstall").description("Remove the macOS LaunchAgent plist").option("--label <label>", "LaunchAgent label").action(async (options) => {
2189
+ const result = await uninstallLaunchAgent(options);
2190
+ console.log(`LaunchAgent removed: ${result.plistPath}`);
2191
+ console.log(`If it is loaded, stop it first or now with: ${result.unloadCommand}`);
2192
+ });
2193
+ return program;
2194
+ }
2195
+ async function runDaemon(options) {
2196
+ const logger = createLogger(options.logLevel);
2197
+ const localConfig = await readDaemonLocalConfig();
2198
+ const serverUrl = options.serverUrl ?? localConfig?.serverUrl;
2199
+ const supabaseUrl = options.supabaseUrl ?? localConfig?.supabaseUrl;
2200
+ const supabaseAnonKey = options.supabaseAnonKey ?? localConfig?.supabaseAnonKey;
2201
+ const machineKey = options.machineKey ?? localConfig?.machineKey;
2202
+ const workspacePath3 = options.workspace ?? localConfig?.workspacePath ?? DOSSIERX_DEFAULT_WORKSPACE_PATH;
2203
+ const runtimeOptions = {
2204
+ ...options,
2205
+ investWikiMode: options.investWikiMode ?? localConfig?.investWikiMode,
2206
+ investWikiLocalRepo: options.investWikiLocalRepo ?? localConfig?.investWikiLocalRepo,
2207
+ investWikiCommand: options.investWikiCommand ?? localConfig?.investWikiCommand,
2208
+ codexCommand: options.codexCommand ?? localConfig?.codexCommand,
2209
+ codexModel: options.codexModel ?? localConfig?.codexModel,
2210
+ codexSandbox: options.codexSandbox ?? localConfig?.codexSandbox,
2211
+ localApiPort: parsePort(options.localApiPort) ?? localConfig?.localApiPort ?? parsePort(process.env.DOSSIERX_LOCAL_API_PORT) ?? DOSSIERX_DEFAULT_LOCAL_API_PORT
2212
+ };
2213
+ if (!serverUrl || !supabaseUrl || !supabaseAnonKey || !machineKey) {
2214
+ throw new Error(
2215
+ "Missing daemon connection config. Run the generated daemon command from DossierX first."
2216
+ );
2217
+ }
2218
+ const workspaceRoot = path8.resolve(expandHomePath(workspacePath3));
2219
+ await assertWorkspaceExists(workspaceRoot);
2220
+ const capabilities = await detectCapabilities();
2221
+ const investWiki = createInvestWikiRunnerFromOptions(runtimeOptions);
2222
+ const codex = createCodexRunnerFromOptions(runtimeOptions);
2223
+ const taskArchive = createTaskArchive({ workspaceRoot });
2224
+ const api = new ApiClient({
2225
+ serverUrl,
2226
+ machineKey
2227
+ });
2228
+ const bootstrap = await api.bootstrap({
2229
+ hostname: os3.hostname(),
2230
+ os: `${os3.platform()} ${os3.release()}`,
2231
+ workspacePath: workspaceRoot,
2232
+ daemonVersion: "0.1.0",
2233
+ capabilities
2234
+ });
2235
+ await writeDaemonLocalConfig({
2236
+ machineId: bootstrap.machineId,
2237
+ machineKey,
2238
+ serverUrl,
2239
+ supabaseUrl,
2240
+ supabaseAnonKey,
2241
+ workspacePath: workspacePath3,
2242
+ investWikiMode: runtimeOptions.investWikiMode,
2243
+ investWikiLocalRepo: runtimeOptions.investWikiLocalRepo,
2244
+ investWikiCommand: runtimeOptions.investWikiCommand,
2245
+ codexCommand: runtimeOptions.codexCommand,
2246
+ codexModel: runtimeOptions.codexModel,
2247
+ codexSandbox: runtimeOptions.codexSandbox,
2248
+ localApiPort: runtimeOptions.localApiPort
2249
+ });
2250
+ const state = { running: false, pending: false };
2251
+ const ctx = {
2252
+ api,
2253
+ machineId: bootstrap.machineId,
2254
+ workspaceRoot,
2255
+ logger,
2256
+ state,
2257
+ dryRun: options.dryRun,
2258
+ codex,
2259
+ codexOptions: runtimeOptions,
2260
+ investWiki,
2261
+ taskArchive
2262
+ };
2263
+ async function heartbeat(status) {
2264
+ await api.heartbeat({
2265
+ machineId: bootstrap.machineId,
2266
+ workspacePath: workspaceRoot,
2267
+ status,
2268
+ capabilities
2269
+ });
2270
+ }
2271
+ await heartbeat("idle");
2272
+ const localApiServer = options.once || options.localApi === false ? null : await startWorkspaceReadServer({
2273
+ workspaceRoot,
2274
+ port: runtimeOptions.localApiPort,
2275
+ allowedOrigins: localApiAllowedOrigins(serverUrl),
2276
+ codexCommand: runtimeOptions.codexCommand,
2277
+ taskArchive
2278
+ }).catch((error) => {
2279
+ logger.warn({ err: error }, "local workspace preview API unavailable");
2280
+ return null;
2281
+ });
2282
+ if (localApiServer) {
2283
+ logger.info({ url: localApiServer.url }, "local workspace preview API ready");
2284
+ }
2285
+ const heartbeatTimer = options.once ? null : setInterval(() => {
2286
+ void heartbeat(state.running ? "running" : "idle").catch((error) => {
2287
+ logger.warn({ err: error }, "heartbeat failed");
2288
+ });
2289
+ }, 15e3);
2290
+ const pollingTimer = options.once ? null : startPollingClaimLoop(ctx, bootstrap.polling.fallbackIntervalMs);
2291
+ const subscription = options.realtime === false ? null : await subscribeToTaskAvailable(
2292
+ {
2293
+ supabaseUrl,
2294
+ supabaseAnonKey,
2295
+ topic: bootstrap.realtime.topic,
2296
+ privateChannel: bootstrap.realtime.private
2297
+ },
2298
+ async (event) => {
2299
+ if (event.event === "task_available") {
2300
+ await claimAndRunOne(ctx);
2301
+ }
2302
+ },
2303
+ (event) => logger.debug({ event }, "ignored invalid realtime event")
2304
+ );
2305
+ await claimAndRunOne(ctx);
2306
+ if (options.once) {
2307
+ if (heartbeatTimer) {
2308
+ clearInterval(heartbeatTimer);
2309
+ }
2310
+ if (pollingTimer) {
2311
+ clearInterval(pollingTimer);
2312
+ }
2313
+ await localApiServer?.close();
2314
+ await subscription?.unsubscribe();
2315
+ return;
2316
+ }
2317
+ await new Promise(() => void 0);
2318
+ }
2319
+ async function runCli(argv = process.argv) {
2320
+ await buildProgram().parseAsync(argv);
2321
+ }
2322
+
2323
+ // src/index.ts
2324
+ runCli().catch((error) => {
2325
+ createLogger("error").error({ err: error }, "dossierx-daemon failed");
2326
+ process.exitCode = 1;
2327
+ });