@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.d.ts +1 -0
- package/dist/index.js +2327 -0
- package/package.json +48 -0
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
+
});
|