@genesislcap/ai-assistant 15.86.1 → 15.87.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/ai-assistant.api.json +5026 -645
- package/dist/ai-assistant.d.ts +802 -0
- package/dist/chat-driver.cjs +1487 -132
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +1480 -132
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +2779 -657
- package/dist/dts/autopilot/autopilot-session.d.ts +25 -0
- package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -1
- package/dist/dts/chat-driver-node.d.ts +2 -0
- package/dist/dts/chat-driver-node.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +15 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +6 -0
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/interaction-controller.d.ts +12 -0
- package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
- package/dist/dts/headless/describe-interaction.d.ts +17 -0
- package/dist/dts/headless/describe-interaction.d.ts.map +1 -0
- package/dist/dts/headless/headless-debug.d.ts +28 -0
- package/dist/dts/headless/headless-debug.d.ts.map +1 -0
- package/dist/dts/headless/headless-persistence.d.ts +35 -0
- package/dist/dts/headless/headless-persistence.d.ts.map +1 -0
- package/dist/dts/headless/headless-session.d.ts +180 -0
- package/dist/dts/headless/headless-session.d.ts.map +1 -0
- package/dist/dts/headless/headless.types.d.ts +435 -0
- package/dist/dts/headless/headless.types.d.ts.map +1 -0
- package/dist/dts/headless/index.d.ts +5 -0
- package/dist/dts/headless/index.d.ts.map +1 -0
- package/dist/dts/headless-server/compact.d.ts +52 -0
- package/dist/dts/headless-server/compact.d.ts.map +1 -0
- package/dist/dts/headless-server/index.d.ts +23 -0
- package/dist/dts/headless-server/index.d.ts.map +1 -0
- package/dist/dts/headless-server/server.d.ts +13 -0
- package/dist/dts/headless-server/server.d.ts.map +1 -0
- package/dist/dts/headless-server/types.d.ts +210 -0
- package/dist/dts/headless-server/types.d.ts.map +1 -0
- package/dist/dts/index.d.ts +2 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/state/debug-event-log.d.ts +1 -1
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/state/decision-surface.d.ts +5 -4
- package/dist/dts/state/decision-surface.d.ts.map +1 -1
- package/dist/dts/state/persistence/debug-log-assembly.d.ts +49 -0
- package/dist/dts/state/persistence/debug-log-assembly.d.ts.map +1 -0
- package/dist/dts/state/persistence/index.d.ts +1 -0
- package/dist/dts/state/persistence/index.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persister.d.ts +7 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/dts/types/interaction-surface.d.ts +30 -0
- package/dist/dts/types/interaction-surface.d.ts.map +1 -1
- package/dist/esm/autopilot/autopilot-session.js +31 -16
- package/dist/esm/chat-driver-node.js +8 -0
- package/dist/esm/components/ai-driver/base-driver-decorator.js +12 -0
- package/dist/esm/components/chat-driver/chat-driver.js +8 -0
- package/dist/esm/components/chat-driver/interaction-controller.js +66 -0
- package/dist/esm/headless/describe-interaction.js +55 -0
- package/dist/esm/headless/headless-debug.js +62 -0
- package/dist/esm/headless/headless-persistence.js +192 -0
- package/dist/esm/headless/headless-session.js +808 -0
- package/dist/esm/headless/headless.types.js +1 -0
- package/dist/esm/headless/index.js +3 -0
- package/dist/esm/headless-server/compact.js +160 -0
- package/dist/esm/headless-server/index.js +21 -0
- package/dist/esm/headless-server/server.js +746 -0
- package/dist/esm/headless-server/types.js +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/main/main.js +18 -37
- package/dist/esm/state/debug-event-log.js +6 -1
- package/dist/esm/state/decision-surface.js +75 -48
- package/dist/esm/state/persistence/debug-log-assembly.js +69 -0
- package/dist/esm/state/persistence/index.js +1 -0
- package/dist/esm/state/persistence/session-persister.js +16 -0
- package/dist/headless-server.cjs +853 -0
- package/dist/headless-server.cjs.map +7 -0
- package/dist/headless-server.mjs +819 -0
- package/dist/headless-server.mjs.map +7 -0
- package/package.json +28 -18
- package/scripts/build-headless-server-node.mjs +42 -0
- package/scripts/smoke-chat-driver-node.mjs +3 -0
- package/scripts/smoke-headless-server-node.mjs +112 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/headless-server/index.ts
|
|
20
|
+
var index_exports = {};
|
|
21
|
+
__export(index_exports, {
|
|
22
|
+
DECISION_TEXTS: () => DECISION_TEXTS,
|
|
23
|
+
DEFAULT_DECISION_TEXT: () => DEFAULT_DECISION_TEXT,
|
|
24
|
+
capOutput: () => capOutput,
|
|
25
|
+
compactDecision: () => compactDecision,
|
|
26
|
+
compactEvent: () => compactEvent,
|
|
27
|
+
createHeadlessServer: () => createHeadlessServer,
|
|
28
|
+
firstLines: () => firstLines,
|
|
29
|
+
leanDecisionText: () => leanDecisionText,
|
|
30
|
+
shortDecision: () => shortDecision
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/headless-server/compact.ts
|
|
35
|
+
var MESSAGE_LINES = 3;
|
|
36
|
+
var MESSAGE_CHARS = 300;
|
|
37
|
+
var TURN_TEXT_CHARS = 1500;
|
|
38
|
+
function firstLines(text, lines, chars) {
|
|
39
|
+
const kept = text.split("\n").slice(0, lines).join("\n");
|
|
40
|
+
const cut = kept.length > chars ? `${kept.slice(0, chars)}\u2026` : kept;
|
|
41
|
+
return cut === text ? { text } : { text: cut, chars: text.length };
|
|
42
|
+
}
|
|
43
|
+
function present(source) {
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const [key, value] of Object.entries(source)) {
|
|
46
|
+
if (value === void 0 || value === null || value === "" || value === false) continue;
|
|
47
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
48
|
+
out[key] = value;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function compactEvent(event, options = {}) {
|
|
53
|
+
switch (event.type) {
|
|
54
|
+
case "decision":
|
|
55
|
+
case "working":
|
|
56
|
+
return void 0;
|
|
57
|
+
case "settled":
|
|
58
|
+
return {
|
|
59
|
+
event: "settled",
|
|
60
|
+
component: event.component,
|
|
61
|
+
...present({
|
|
62
|
+
answeredBy: event.answeredBy,
|
|
63
|
+
auto: event.auto,
|
|
64
|
+
raw: event.raw,
|
|
65
|
+
resolvedItself: event.resolvedItself
|
|
66
|
+
})
|
|
67
|
+
};
|
|
68
|
+
case "message": {
|
|
69
|
+
const { text, chars } = firstLines(event.text, MESSAGE_LINES, MESSAGE_CHARS);
|
|
70
|
+
return { event: "message", text, ...present({ chars: chars ?? event.chars }) };
|
|
71
|
+
}
|
|
72
|
+
case "turn": {
|
|
73
|
+
const out = {
|
|
74
|
+
event: "turn",
|
|
75
|
+
reason: event.reason,
|
|
76
|
+
...present({ failureReason: event.failureReason })
|
|
77
|
+
};
|
|
78
|
+
if (event.text) {
|
|
79
|
+
const { text, chars } = firstLines(event.text, Number.MAX_SAFE_INTEGER, TURN_TEXT_CHARS);
|
|
80
|
+
out.text = text;
|
|
81
|
+
if (chars ?? event.chars) out.chars = chars ?? event.chars;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
case "autopilot":
|
|
86
|
+
if (event.mode === "unavailable" && !event.needsUser) return void 0;
|
|
87
|
+
return {
|
|
88
|
+
event: "autopilot",
|
|
89
|
+
mode: event.mode,
|
|
90
|
+
status: event.status,
|
|
91
|
+
...present({ needsUser: event.needsUser, working: event.working, reason: event.reason })
|
|
92
|
+
};
|
|
93
|
+
case "error":
|
|
94
|
+
return { event: "error", where: event.where, message: event.message };
|
|
95
|
+
case "probe": {
|
|
96
|
+
const view = options.probeEvent;
|
|
97
|
+
if (!view) {
|
|
98
|
+
const changed = Object.fromEntries(event.changed.map((key) => [key, event.probe[key]]));
|
|
99
|
+
return { event: "probe", ...changed };
|
|
100
|
+
}
|
|
101
|
+
if (!event.changed.some((key) => view.keys.includes(key))) return void 0;
|
|
102
|
+
return {
|
|
103
|
+
event: view.name,
|
|
104
|
+
...Object.fromEntries(view.keys.map((key) => [key, event.probe[key] ?? null]))
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
var PREVIEW_MARK = " (shown below as Now)";
|
|
112
|
+
var NOW_PREFIX = "Now: ";
|
|
113
|
+
var CUT_PREFIX = "[Cut at ";
|
|
114
|
+
function leanDecisionText(text, views) {
|
|
115
|
+
const names = Object.keys(views ?? {});
|
|
116
|
+
if (!names.length) return text;
|
|
117
|
+
const lines = text.split("\n");
|
|
118
|
+
const marked = lines.findIndex((line) => line.endsWith(PREVIEW_MARK));
|
|
119
|
+
if (marked < 0) return text;
|
|
120
|
+
const startsEntry = (line, name) => line === ` ${name}${PREVIEW_MARK}` || line.startsWith(` ${name} \u2014 `);
|
|
121
|
+
let entry = marked;
|
|
122
|
+
while (entry >= 0 && !names.some((n) => startsEntry(lines[entry], n))) entry -= 1;
|
|
123
|
+
const view = entry < 0 ? void 0 : names.find((n) => startsEntry(lines[entry], n));
|
|
124
|
+
if (view === void 0) return text;
|
|
125
|
+
const now = lines.findIndex((line, i) => i > marked && line.startsWith(NOW_PREFIX));
|
|
126
|
+
if (now < 0) return text;
|
|
127
|
+
const end = lines[now + 1]?.startsWith(CUT_PREFIX) ? now + 2 : now + 1;
|
|
128
|
+
lines.splice(
|
|
129
|
+
now,
|
|
130
|
+
end - now,
|
|
131
|
+
`(The "${view}" view is not repeated here: read it with read ${view}.)`
|
|
132
|
+
);
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
function compactDecision(description, decisionText = "full") {
|
|
136
|
+
const actions = description.actions.map((a) => ({
|
|
137
|
+
...a.n !== void 0 ? { n: a.n } : {},
|
|
138
|
+
name: a.name,
|
|
139
|
+
...present({ label: a.label !== a.name ? a.label : void 0 }),
|
|
140
|
+
enabled: a.enabled,
|
|
141
|
+
...present({
|
|
142
|
+
unavailable: a.unavailable,
|
|
143
|
+
args: a.args,
|
|
144
|
+
argsSchema: a.args === "fields" ? a.argsSchema : void 0
|
|
145
|
+
})
|
|
146
|
+
}));
|
|
147
|
+
return {
|
|
148
|
+
interactionId: description.interactionId,
|
|
149
|
+
component: description.component,
|
|
150
|
+
text: decisionText === "lean" ? leanDecisionText(description.text, description.views) : description.text,
|
|
151
|
+
actions,
|
|
152
|
+
...description.views && Object.keys(description.views).length ? { views: { ...description.views } } : {},
|
|
153
|
+
...description.controller ? {} : { controller: false }
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function shortDecision(interactionId, component, actions) {
|
|
157
|
+
return {
|
|
158
|
+
interactionId,
|
|
159
|
+
component,
|
|
160
|
+
actions: actions.map((a) => ({
|
|
161
|
+
...a.n !== void 0 ? { n: a.n } : {},
|
|
162
|
+
name: a.name,
|
|
163
|
+
enabled: a.enabled
|
|
164
|
+
}))
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
var OUTPUT_CHARS = 4e3;
|
|
168
|
+
function capOutput(output) {
|
|
169
|
+
let json;
|
|
170
|
+
try {
|
|
171
|
+
json = JSON.stringify(output);
|
|
172
|
+
} catch {
|
|
173
|
+
return String(output);
|
|
174
|
+
}
|
|
175
|
+
if (json === void 0 || json.length <= OUTPUT_CHARS) return output;
|
|
176
|
+
return { truncated: `${json.slice(0, OUTPUT_CHARS)}\u2026`, chars: json.length };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/headless-server/server.ts
|
|
180
|
+
var import_node_fs = require("node:fs");
|
|
181
|
+
var import_promises = require("node:fs/promises");
|
|
182
|
+
var import_node_http = require("node:http");
|
|
183
|
+
var import_node_path = require("node:path");
|
|
184
|
+
|
|
185
|
+
// src/headless-server/types.ts
|
|
186
|
+
var DECISION_TEXTS = ["full", "lean"];
|
|
187
|
+
var DEFAULT_DECISION_TEXT = "lean";
|
|
188
|
+
|
|
189
|
+
// src/headless-server/server.ts
|
|
190
|
+
var DEFAULT_MAX_WAIT_MS = 1e5;
|
|
191
|
+
var MAX_WAIT_CAP_MS = 36e5;
|
|
192
|
+
var BUILTIN_OPS = [
|
|
193
|
+
"send",
|
|
194
|
+
"act",
|
|
195
|
+
"resolve",
|
|
196
|
+
"autopilot",
|
|
197
|
+
"wait",
|
|
198
|
+
"state",
|
|
199
|
+
"read",
|
|
200
|
+
"debugLog",
|
|
201
|
+
"stop"
|
|
202
|
+
];
|
|
203
|
+
var BYTES_PER_KB = 1024;
|
|
204
|
+
var MAX_BODY_MB = 16;
|
|
205
|
+
var MAX_BODY_BYTES = MAX_BODY_MB * BYTES_PER_KB * BYTES_PER_KB;
|
|
206
|
+
var HTTP_OK = 200;
|
|
207
|
+
var HTTP_BAD_REQUEST = 400;
|
|
208
|
+
var HTTP_FORBIDDEN = 403;
|
|
209
|
+
var HTTP_NOT_FOUND = 404;
|
|
210
|
+
var HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
|
|
211
|
+
var LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "[::1]", "::1"];
|
|
212
|
+
var HTTP_SERVER_ERROR = 500;
|
|
213
|
+
var CLOSE_GRACE_MS = 500;
|
|
214
|
+
var MONEY_SCALE = 1e4;
|
|
215
|
+
var WATCH_MS = 2e3;
|
|
216
|
+
var ALL_EVENT_TYPES = [
|
|
217
|
+
"decision",
|
|
218
|
+
"working",
|
|
219
|
+
"settled",
|
|
220
|
+
"message",
|
|
221
|
+
"turn",
|
|
222
|
+
"autopilot",
|
|
223
|
+
"probe",
|
|
224
|
+
"error"
|
|
225
|
+
];
|
|
226
|
+
function untilOf(value) {
|
|
227
|
+
if (value === void 0) return void 0;
|
|
228
|
+
const names = (Array.isArray(value) ? value : [value]).map(String);
|
|
229
|
+
return {
|
|
230
|
+
types: names.filter(
|
|
231
|
+
(n) => ALL_EVENT_TYPES.includes(n)
|
|
232
|
+
),
|
|
233
|
+
idle: names.includes("idle")
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function waitMsOf(value, fallback) {
|
|
237
|
+
const n = Number(value ?? fallback);
|
|
238
|
+
return Number.isFinite(n) ? Math.min(Math.max(0, n), MAX_WAIT_CAP_MS) : fallback;
|
|
239
|
+
}
|
|
240
|
+
var hasOwn = (target, key) => !!target && Object.prototype.hasOwnProperty.call(target, key);
|
|
241
|
+
var money = (usd) => Math.round(usd * MONEY_SCALE) / MONEY_SCALE;
|
|
242
|
+
function createHeadlessServer(options) {
|
|
243
|
+
const runDir = (0, import_node_path.resolve)(options.runDir);
|
|
244
|
+
(0, import_node_fs.mkdirSync)(runDir, { recursive: true });
|
|
245
|
+
const defaultMaxWaitMs = waitMsOf(options.defaultMaxWaitMs, DEFAULT_MAX_WAIT_MS);
|
|
246
|
+
let decisionText = options.decisionText ?? DEFAULT_DECISION_TEXT;
|
|
247
|
+
const announce = options.announce ?? ((line) => console.log(line));
|
|
248
|
+
const events = (0, import_node_fs.createWriteStream)((0, import_node_path.join)(runDir, "events.jsonl"), { flags: "a" });
|
|
249
|
+
const startedAt = Date.now();
|
|
250
|
+
let session;
|
|
251
|
+
let http;
|
|
252
|
+
let closing = false;
|
|
253
|
+
let loggedSeq = 0;
|
|
254
|
+
let applying = Promise.resolve();
|
|
255
|
+
let flushing = Promise.resolve();
|
|
256
|
+
let resolveClosed;
|
|
257
|
+
const closed = new Promise((r) => {
|
|
258
|
+
resolveClosed = r;
|
|
259
|
+
});
|
|
260
|
+
const appendLog = (line) => {
|
|
261
|
+
if (!events.writableEnded) events.write(`${JSON.stringify(line)}
|
|
262
|
+
`);
|
|
263
|
+
};
|
|
264
|
+
function drainLog() {
|
|
265
|
+
if (!session) return;
|
|
266
|
+
for (const event of session.eventsSince(loggedSeq)) {
|
|
267
|
+
appendLog({ kind: "event", ...event });
|
|
268
|
+
loggedSeq = event.seq;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function flushAll() {
|
|
272
|
+
flushing = flushing.then(async () => {
|
|
273
|
+
try {
|
|
274
|
+
await options.flush?.();
|
|
275
|
+
await session?.flush();
|
|
276
|
+
} catch (e) {
|
|
277
|
+
appendLog({
|
|
278
|
+
kind: "error",
|
|
279
|
+
where: "flush",
|
|
280
|
+
message: e instanceof Error ? e.message : String(e)
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
return flushing;
|
|
285
|
+
}
|
|
286
|
+
async function watch(s) {
|
|
287
|
+
let cursor = s.state().lastSeq;
|
|
288
|
+
drainLog();
|
|
289
|
+
const watching = () => !closing && session === s;
|
|
290
|
+
while (watching()) {
|
|
291
|
+
try {
|
|
292
|
+
const woken = await s.wait({ since: cursor, until: ALL_EVENT_TYPES, maxWaitMs: WATCH_MS });
|
|
293
|
+
cursor = woken.nextSeq;
|
|
294
|
+
drainLog();
|
|
295
|
+
if (woken.events.some((e) => e.type === "turn")) await flushAll();
|
|
296
|
+
if (woken.status === "closed") break;
|
|
297
|
+
} catch (e) {
|
|
298
|
+
appendLog({
|
|
299
|
+
kind: "error",
|
|
300
|
+
where: "watch",
|
|
301
|
+
message: e instanceof Error ? e.message : String(e)
|
|
302
|
+
});
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const handle = {
|
|
308
|
+
get session() {
|
|
309
|
+
return session;
|
|
310
|
+
},
|
|
311
|
+
runDir,
|
|
312
|
+
get decisionText() {
|
|
313
|
+
return decisionText;
|
|
314
|
+
},
|
|
315
|
+
setDecisionText(mode) {
|
|
316
|
+
decisionText = mode;
|
|
317
|
+
},
|
|
318
|
+
attach(next) {
|
|
319
|
+
if (session === next) return;
|
|
320
|
+
session = next;
|
|
321
|
+
loggedSeq = 0;
|
|
322
|
+
void watch(next);
|
|
323
|
+
},
|
|
324
|
+
listen,
|
|
325
|
+
handle: runCommand,
|
|
326
|
+
shutdown,
|
|
327
|
+
closed
|
|
328
|
+
};
|
|
329
|
+
if (options.session) handle.attach(options.session);
|
|
330
|
+
const composerOf = (state) => {
|
|
331
|
+
if (!state) return "blocked:not-started";
|
|
332
|
+
const blocked = options.composerBlockedReason?.();
|
|
333
|
+
if (blocked) return `blocked:${blocked}`;
|
|
334
|
+
return state.busy ? "busy" : "open";
|
|
335
|
+
};
|
|
336
|
+
function statusNow(state) {
|
|
337
|
+
if (!state) return "idle";
|
|
338
|
+
if (state.pending) return "decision";
|
|
339
|
+
if (options.isEnded?.()) return "ended";
|
|
340
|
+
if (state.autopilot?.needsUser) return "needs-user";
|
|
341
|
+
return state.busy ? "working" : "idle";
|
|
342
|
+
}
|
|
343
|
+
const probeView = (state) => {
|
|
344
|
+
const view = options.probeEvent;
|
|
345
|
+
if (!view) return void 0;
|
|
346
|
+
return Object.fromEntries(view.keys.map((key) => [key, state.probe[key] ?? null]));
|
|
347
|
+
};
|
|
348
|
+
function stageOf(state, other) {
|
|
349
|
+
if (other) {
|
|
350
|
+
let stage;
|
|
351
|
+
try {
|
|
352
|
+
stage = options.describeWorking?.(other.interactionId, other.component);
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
if (stage) return stage;
|
|
356
|
+
const idle = other.heldBy === "autopilot" && state.autopilot?.mode !== "running";
|
|
357
|
+
return other.heldBy !== "user" && !idle ? `${other.heldBy} is answering it` : void 0;
|
|
358
|
+
}
|
|
359
|
+
if (state.autopilot?.working && state.autopilot.mode === "running") {
|
|
360
|
+
return "autopilot is working";
|
|
361
|
+
}
|
|
362
|
+
return state.busy ? "assistant is thinking (no widget open)" : void 0;
|
|
363
|
+
}
|
|
364
|
+
function workingInfo(state, startedMs) {
|
|
365
|
+
const other = state.others.find((o) => !o.ready || o.heldBy !== "user") ?? state.others[0];
|
|
366
|
+
return {
|
|
367
|
+
...other ? { component: other.component, heldBy: other.heldBy } : {},
|
|
368
|
+
elapsedMs: Date.now() - startedMs,
|
|
369
|
+
...present({ probe: probeView(state), stage: stageOf(state, other) })
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const decisionTextFor = (command) => DECISION_TEXTS.includes(command?.decisionText) ? command.decisionText : decisionText;
|
|
373
|
+
function reply(command, status, since, extra = {}, startedMs = Date.now()) {
|
|
374
|
+
const state = session?.state();
|
|
375
|
+
const compact = since.map((e) => compactEvent(e, { probeEvent: options.probeEvent })).filter((e) => !!e);
|
|
376
|
+
return {
|
|
377
|
+
ok: extra.ok ?? true,
|
|
378
|
+
...command?.id !== void 0 ? { id: command.id } : {},
|
|
379
|
+
status,
|
|
380
|
+
...compact.length ? { since: compact } : {},
|
|
381
|
+
...state?.pending ? { decision: compactDecision(state.pending, decisionTextFor(command)) } : {},
|
|
382
|
+
...status === "working" && state ? { working: workingInfo(state, startedMs) } : {},
|
|
383
|
+
composer: composerOf(state),
|
|
384
|
+
cost: { usd: money(state?.cost.costUsd ?? 0) },
|
|
385
|
+
nextSeq: state?.lastSeq ?? 0,
|
|
386
|
+
...extra
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function refuse(command, error, reason, since = void 0, shortFor) {
|
|
390
|
+
const state = session?.state();
|
|
391
|
+
const missed = session && since !== void 0 ? session.eventsSince(since) : [];
|
|
392
|
+
const pending = state?.pending;
|
|
393
|
+
return reply(command, statusNow(state), missed, {
|
|
394
|
+
ok: false,
|
|
395
|
+
error,
|
|
396
|
+
...reason ? { reason } : {},
|
|
397
|
+
...pending && shortFor !== void 0 && pending.interactionId === shortFor ? { decision: shortDecision(pending.interactionId, pending.component, pending.actions) } : {}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
function edited(command, interactionId, result, cursor) {
|
|
401
|
+
const s = session;
|
|
402
|
+
const state = s.state();
|
|
403
|
+
const open = state.pending?.interactionId === interactionId ? state.pending : void 0;
|
|
404
|
+
const actions = result.description?.actions ?? open?.actions ?? result.actions;
|
|
405
|
+
const component = result.description?.component ?? open?.component ?? state.others.find((o) => o.interactionId === interactionId)?.component ?? "";
|
|
406
|
+
return reply(command, "edited", s.eventsSince(cursor), {
|
|
407
|
+
decision: shortDecision(interactionId, component, actions),
|
|
408
|
+
...result.output !== void 0 ? { output: capOutput(result.output) } : {}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
async function poll(command, cursor, startedMs, signal) {
|
|
412
|
+
const s = session;
|
|
413
|
+
if (!s) return reply(command, "idle", [], {}, startedMs);
|
|
414
|
+
const maxWaitMs = waitMsOf(command.maxWaitMs, defaultMaxWaitMs);
|
|
415
|
+
const deadline = startedMs + maxWaitMs;
|
|
416
|
+
const until = untilOf(command.until);
|
|
417
|
+
let waitFrom = cursor;
|
|
418
|
+
for (; ; ) {
|
|
419
|
+
const since = s.eventsSince(cursor);
|
|
420
|
+
const state = s.state();
|
|
421
|
+
const verdict = terminal(since, state, until);
|
|
422
|
+
if (verdict) {
|
|
423
|
+
if (since.some((e) => e.type === "turn") || verdict === "ended") await flushAll();
|
|
424
|
+
drainLog();
|
|
425
|
+
return reply(command, verdict, s.eventsSince(cursor), {}, startedMs);
|
|
426
|
+
}
|
|
427
|
+
const remaining = deadline - Date.now();
|
|
428
|
+
if (remaining <= 0 || signal?.aborted) {
|
|
429
|
+
drainLog();
|
|
430
|
+
return reply(command, "working", since, {}, startedMs);
|
|
431
|
+
}
|
|
432
|
+
const woken = await s.wait({
|
|
433
|
+
since: waitFrom,
|
|
434
|
+
...until ? { until: until.types } : {},
|
|
435
|
+
maxWaitMs: remaining,
|
|
436
|
+
...signal ? { signal } : {}
|
|
437
|
+
});
|
|
438
|
+
waitFrom = woken.nextSeq;
|
|
439
|
+
if (woken.status === "closed") {
|
|
440
|
+
drainLog();
|
|
441
|
+
return reply(command, "ended", s.eventsSince(cursor), {}, startedMs);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function terminal(since, state, until) {
|
|
446
|
+
const turnEnded = since.some((e) => e.type === "turn");
|
|
447
|
+
const failed = since.some((e) => e.type === "error" || e.type === "turn" && e.failureReason);
|
|
448
|
+
if (options.isEnded?.() && !state.pending && (turnEnded || !state.busy)) return "ended";
|
|
449
|
+
if (until) {
|
|
450
|
+
if (state.pending && until.types.includes("decision")) return "decision";
|
|
451
|
+
if (since.some((e) => until.types.includes(e.type))) {
|
|
452
|
+
return since.some((e) => e.type === "error") ? "error" : turnEnded ? "turn" : statusNow(state);
|
|
453
|
+
}
|
|
454
|
+
if (until.idle && !state.busy && !state.pending) return "idle";
|
|
455
|
+
return void 0;
|
|
456
|
+
}
|
|
457
|
+
if (state.pending) return "decision";
|
|
458
|
+
if (failed) return "error";
|
|
459
|
+
if (turnEnded) return "turn";
|
|
460
|
+
if (state.autopilot?.needsUser) return "needs-user";
|
|
461
|
+
return void 0;
|
|
462
|
+
}
|
|
463
|
+
const isOpen = (state, interactionId) => state.pending?.interactionId === interactionId || state.others.some((o) => o.interactionId === interactionId);
|
|
464
|
+
const gone = (command, interactionId, since) => refuse(
|
|
465
|
+
command,
|
|
466
|
+
`interaction ${interactionId} is not pending: it has settled, or never existed`,
|
|
467
|
+
"ended",
|
|
468
|
+
since
|
|
469
|
+
);
|
|
470
|
+
const notStarted = (command) => refuse(
|
|
471
|
+
command,
|
|
472
|
+
options.extraOps?.start ? 'not started: send {"op":"start", \u2026} first' : "no session is attached",
|
|
473
|
+
"not-started"
|
|
474
|
+
);
|
|
475
|
+
const str = (value) => typeof value === "string" ? value : void 0;
|
|
476
|
+
const actionOf = (value) => typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
|
|
477
|
+
async function applyOp(command, s, cursor) {
|
|
478
|
+
const state = s.state();
|
|
479
|
+
switch (command.op) {
|
|
480
|
+
case "send": {
|
|
481
|
+
const text = str(command.text);
|
|
482
|
+
if (text === void 0) return refuse(command, "`send` needs `text`", "invalid-command");
|
|
483
|
+
const paths = Array.isArray(command.files) ? command.files.map(String) : [];
|
|
484
|
+
let attachments;
|
|
485
|
+
if (paths.length) {
|
|
486
|
+
if (!options.readAttachment) {
|
|
487
|
+
return refuse(command, "this server cannot read files", "invalid-command");
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
attachments = await Promise.all(paths.map((p) => options.readAttachment(p)));
|
|
491
|
+
} catch (e) {
|
|
492
|
+
return refuse(
|
|
493
|
+
command,
|
|
494
|
+
`could not read a file: ${e instanceof Error ? e.message : String(e)}`,
|
|
495
|
+
"invalid-command"
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const sent = s.send(text, {
|
|
500
|
+
...attachments ? { attachments } : {},
|
|
501
|
+
...str(command.agent) ? { agent: str(command.agent) } : {}
|
|
502
|
+
});
|
|
503
|
+
return sent.ok ? "poll" : refuse(command, sent.error, "refused", cursor);
|
|
504
|
+
}
|
|
505
|
+
case "act": {
|
|
506
|
+
const interactionId = str(command.interactionId);
|
|
507
|
+
if (!interactionId || command.action === void 0) {
|
|
508
|
+
return refuse(command, "`act` needs `interactionId` and `action`", "invalid-command");
|
|
509
|
+
}
|
|
510
|
+
if (!isOpen(state, interactionId)) return gone(command, interactionId, cursor);
|
|
511
|
+
const result = await s.act(interactionId, actionOf(command.action), {
|
|
512
|
+
...str(command.text) !== void 0 ? { text: str(command.text) } : {},
|
|
513
|
+
...Array.isArray(command.options) ? { options: command.options.map(String) } : {},
|
|
514
|
+
...command.fields !== void 0 ? { fields: command.fields } : {}
|
|
515
|
+
});
|
|
516
|
+
if (!result.ok) return refuse(command, result.message, result.error, cursor, interactionId);
|
|
517
|
+
if (result.finished === false) return edited(command, interactionId, result, cursor);
|
|
518
|
+
return "poll";
|
|
519
|
+
}
|
|
520
|
+
case "resolve": {
|
|
521
|
+
const interactionId = str(command.interactionId);
|
|
522
|
+
if (!interactionId)
|
|
523
|
+
return refuse(command, "`resolve` needs `interactionId` and `result`", "invalid-command");
|
|
524
|
+
if (!isOpen(state, interactionId)) return gone(command, interactionId, cursor);
|
|
525
|
+
const resolved = s.resolve(interactionId, command.result);
|
|
526
|
+
return resolved.ok ? "poll" : refuse(command, resolved.error, "refused", cursor);
|
|
527
|
+
}
|
|
528
|
+
case "autopilot": {
|
|
529
|
+
const mode = {
|
|
530
|
+
pause: "pause",
|
|
531
|
+
paused: "pause",
|
|
532
|
+
resume: "resume",
|
|
533
|
+
running: "resume",
|
|
534
|
+
stop: "stop"
|
|
535
|
+
}[String(command.mode)];
|
|
536
|
+
if (!mode)
|
|
537
|
+
return refuse(
|
|
538
|
+
command,
|
|
539
|
+
"`autopilot` needs `mode`: running, paused or stop",
|
|
540
|
+
"invalid-command"
|
|
541
|
+
);
|
|
542
|
+
const done = s.autopilot(mode);
|
|
543
|
+
return done.ok ? "poll" : refuse(command, done.error, "refused", cursor);
|
|
544
|
+
}
|
|
545
|
+
case "wait":
|
|
546
|
+
return "poll";
|
|
547
|
+
default:
|
|
548
|
+
return refuse(command, `unknown op "${command.op}"`, "unknown-op");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function readOp(command, s) {
|
|
552
|
+
switch (command.op) {
|
|
553
|
+
case "state": {
|
|
554
|
+
const state = s.state();
|
|
555
|
+
return reply(command, statusNow(state), [], {
|
|
556
|
+
...state.others.length ? { others: state.others } : {},
|
|
557
|
+
...state.autopilot ? { autopilot: state.autopilot } : {},
|
|
558
|
+
...Object.keys(state.probe).length ? { probe: state.probe } : {},
|
|
559
|
+
busy: state.busy
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
case "read": {
|
|
563
|
+
const interactionId = str(command.interactionId);
|
|
564
|
+
if (!interactionId)
|
|
565
|
+
return refuse(command, "`read` needs `interactionId`", "invalid-command");
|
|
566
|
+
const read = await s.read(interactionId, {
|
|
567
|
+
...str(command.view) ? { view: str(command.view) } : {},
|
|
568
|
+
...str(command.tool) ? { tool: str(command.tool) } : {},
|
|
569
|
+
...command.args && typeof command.args === "object" ? { args: command.args } : {}
|
|
570
|
+
});
|
|
571
|
+
return read.ok ? {
|
|
572
|
+
ok: true,
|
|
573
|
+
...command.id !== void 0 ? { id: command.id } : {},
|
|
574
|
+
content: read.content
|
|
575
|
+
} : {
|
|
576
|
+
ok: false,
|
|
577
|
+
...command.id !== void 0 ? { id: command.id } : {},
|
|
578
|
+
error: read.error
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
case "debugLog": {
|
|
582
|
+
await flushAll();
|
|
583
|
+
const log = await s.debugLog();
|
|
584
|
+
const path = (0, import_node_path.join)(runDir, "debug-log.json");
|
|
585
|
+
await (0, import_promises.writeFile)(path, JSON.stringify(log));
|
|
586
|
+
const byKind = {};
|
|
587
|
+
for (const entry of log.timeline) {
|
|
588
|
+
const kind = String(entry.kind ?? "unknown");
|
|
589
|
+
byKind[kind] = (byKind[kind] ?? 0) + 1;
|
|
590
|
+
}
|
|
591
|
+
const usd = log.sessionUsage?.costUsd ?? s.state().cost.costUsd;
|
|
592
|
+
return reply(command, statusNow(s.state()), [], {
|
|
593
|
+
path,
|
|
594
|
+
entries: { total: log.timeline.length, byKind },
|
|
595
|
+
cost: { usd: money(usd) }
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
default:
|
|
599
|
+
return void 0;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async function runCommand(raw, signal) {
|
|
603
|
+
const startedMs = Date.now();
|
|
604
|
+
const command = raw;
|
|
605
|
+
if (!command || typeof command !== "object" || typeof command.op !== "string") {
|
|
606
|
+
return reply(void 0, statusNow(session?.state()), [], {
|
|
607
|
+
ok: false,
|
|
608
|
+
error: "a command is a JSON object with a string `op`",
|
|
609
|
+
reason: "invalid-command"
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
appendLog({ kind: "command", at: (/* @__PURE__ */ new Date()).toISOString(), command });
|
|
613
|
+
if (command.decisionText !== void 0 && !DECISION_TEXTS.includes(command.decisionText)) {
|
|
614
|
+
return refuse(
|
|
615
|
+
command,
|
|
616
|
+
`\`decisionText\` must be one of ${DECISION_TEXTS.join(", ")} (got ${JSON.stringify(command.decisionText)})`,
|
|
617
|
+
"invalid-command"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const out = await dispatch(command, startedMs, signal);
|
|
622
|
+
drainLog();
|
|
623
|
+
return out;
|
|
624
|
+
} catch (e) {
|
|
625
|
+
drainLog();
|
|
626
|
+
appendLog({
|
|
627
|
+
kind: "error",
|
|
628
|
+
where: `op:${command.op}`,
|
|
629
|
+
message: e instanceof Error ? e.message : String(e)
|
|
630
|
+
});
|
|
631
|
+
return reply(command, statusNow(session?.state()), [], {
|
|
632
|
+
ok: false,
|
|
633
|
+
status: "error",
|
|
634
|
+
error: e instanceof Error ? e.message : String(e),
|
|
635
|
+
reason: "threw"
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function dispatch(command, startedMs, signal) {
|
|
640
|
+
if (command.op === "stop") {
|
|
641
|
+
session?.stop();
|
|
642
|
+
const out = reply(command, "ended", [], { stopped: true });
|
|
643
|
+
setTimeout(() => void shutdown(), 0);
|
|
644
|
+
return out;
|
|
645
|
+
}
|
|
646
|
+
const isBuiltin = BUILTIN_OPS.includes(command.op);
|
|
647
|
+
const extra = !isBuiltin && hasOwn(options.extraOps, command.op) ? options.extraOps[command.op] : void 0;
|
|
648
|
+
if (!extra && !isBuiltin) {
|
|
649
|
+
return refuse(command, `unknown op "${command.op}"`, "unknown-op");
|
|
650
|
+
}
|
|
651
|
+
if (command.op === "state" || command.op === "read" || command.op === "debugLog") {
|
|
652
|
+
if (!session) return notStarted(command);
|
|
653
|
+
return await readOp(command, session);
|
|
654
|
+
}
|
|
655
|
+
const cursor = typeof command.since === "number" ? command.since : session?.state().lastSeq ?? 0;
|
|
656
|
+
if (command.op === "wait") {
|
|
657
|
+
if (!session) return notStarted(command);
|
|
658
|
+
return poll(command, cursor, startedMs, signal);
|
|
659
|
+
}
|
|
660
|
+
const previous = applying;
|
|
661
|
+
let release;
|
|
662
|
+
applying = new Promise((r) => {
|
|
663
|
+
release = r;
|
|
664
|
+
});
|
|
665
|
+
let applied;
|
|
666
|
+
try {
|
|
667
|
+
await previous;
|
|
668
|
+
if (extra) {
|
|
669
|
+
applied = await extra(command, handle);
|
|
670
|
+
} else {
|
|
671
|
+
if (!session) return notStarted(command);
|
|
672
|
+
applied = await applyOp(command, session, cursor);
|
|
673
|
+
}
|
|
674
|
+
} finally {
|
|
675
|
+
release();
|
|
676
|
+
}
|
|
677
|
+
if (applied === "poll") return poll(command, cursor, startedMs, signal);
|
|
678
|
+
if ("status" in applied) return applied;
|
|
679
|
+
if (!applied.ok)
|
|
680
|
+
return refuse(
|
|
681
|
+
command,
|
|
682
|
+
applied.error ?? "refused",
|
|
683
|
+
"refused",
|
|
684
|
+
typeof command.since === "number" ? cursor : void 0
|
|
685
|
+
);
|
|
686
|
+
const wait = applied.poll ?? !!session;
|
|
687
|
+
if (!wait || !session) {
|
|
688
|
+
const pending = session?.state().pending;
|
|
689
|
+
return reply(
|
|
690
|
+
command,
|
|
691
|
+
statusNow(session?.state()),
|
|
692
|
+
[],
|
|
693
|
+
{
|
|
694
|
+
...applied.fields,
|
|
695
|
+
...applied.shortDecision && pending ? { decision: shortDecision(pending.interactionId, pending.component, pending.actions) } : {}
|
|
696
|
+
},
|
|
697
|
+
startedMs
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const polled = await poll(command, cursor, startedMs, signal);
|
|
701
|
+
return { ...polled, ...applied.fields };
|
|
702
|
+
}
|
|
703
|
+
function readBody(req) {
|
|
704
|
+
return new Promise((resolveBody, reject) => {
|
|
705
|
+
const chunks = [];
|
|
706
|
+
let size = 0;
|
|
707
|
+
req.on("data", (chunk) => {
|
|
708
|
+
size += chunk.length;
|
|
709
|
+
if (size > MAX_BODY_BYTES) {
|
|
710
|
+
reject(new Error("the command is too large"));
|
|
711
|
+
req.destroy();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
chunks.push(chunk);
|
|
715
|
+
});
|
|
716
|
+
req.on("end", () => resolveBody(Buffer.concat(chunks).toString("utf8")));
|
|
717
|
+
req.on("error", reject);
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
function send(res, status, body) {
|
|
721
|
+
const json = JSON.stringify(body);
|
|
722
|
+
res.writeHead(status, {
|
|
723
|
+
"content-type": "application/json; charset=utf-8",
|
|
724
|
+
"content-length": Buffer.byteLength(json)
|
|
725
|
+
});
|
|
726
|
+
res.end(json);
|
|
727
|
+
}
|
|
728
|
+
function refusal(req, port) {
|
|
729
|
+
if (req.headers.origin !== void 0) {
|
|
730
|
+
return { status: HTTP_FORBIDDEN, error: "a request from a web page is refused" };
|
|
731
|
+
}
|
|
732
|
+
const bound = options.host ?? "127.0.0.1";
|
|
733
|
+
if (LOOPBACK_HOSTS.includes(bound)) {
|
|
734
|
+
const host = (req.headers.host ?? "").toLowerCase();
|
|
735
|
+
if (!LOOPBACK_HOSTS.some((name) => host === `${name}:${port}`)) {
|
|
736
|
+
return {
|
|
737
|
+
status: HTTP_FORBIDDEN,
|
|
738
|
+
error: `the Host must be 127.0.0.1:${port} or localhost:${port}`
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (req.method === "POST") {
|
|
743
|
+
const type = (req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
|
|
744
|
+
if (type !== "application/json") {
|
|
745
|
+
return {
|
|
746
|
+
status: HTTP_UNSUPPORTED_MEDIA_TYPE,
|
|
747
|
+
error: "POST /cmd takes content-type: application/json"
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return void 0;
|
|
752
|
+
}
|
|
753
|
+
async function onRequest(req, res) {
|
|
754
|
+
const refused = refusal(req, http.address().port);
|
|
755
|
+
if (refused) {
|
|
756
|
+
send(res, refused.status, { ok: false, status: "error", error: refused.error });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const url = (req.url ?? "/").split("?")[0];
|
|
760
|
+
if (req.method === "GET" && url === "/health") {
|
|
761
|
+
const state = session?.state();
|
|
762
|
+
send(res, HTTP_OK, {
|
|
763
|
+
ok: true,
|
|
764
|
+
started: !!session,
|
|
765
|
+
runDir,
|
|
766
|
+
pid: process.pid,
|
|
767
|
+
uptimeMs: Date.now() - startedAt,
|
|
768
|
+
...state ? { status: statusNow(state), nextSeq: state.lastSeq } : {}
|
|
769
|
+
});
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (req.method === "POST" && url === "/cmd") {
|
|
773
|
+
let command;
|
|
774
|
+
try {
|
|
775
|
+
command = JSON.parse(await readBody(req));
|
|
776
|
+
} catch (e) {
|
|
777
|
+
send(res, HTTP_BAD_REQUEST, {
|
|
778
|
+
ok: false,
|
|
779
|
+
status: "error",
|
|
780
|
+
error: `bad command: ${e instanceof Error ? e.message : String(e)}`
|
|
781
|
+
});
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const gone2 = new AbortController();
|
|
785
|
+
res.on("close", () => {
|
|
786
|
+
if (!res.writableEnded) gone2.abort();
|
|
787
|
+
});
|
|
788
|
+
const out = await runCommand(command, gone2.signal);
|
|
789
|
+
if (!gone2.signal.aborted) send(res, HTTP_OK, out);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
send(res, HTTP_NOT_FOUND, {
|
|
793
|
+
ok: false,
|
|
794
|
+
error: "GET /health, or POST /cmd with one JSON command"
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
function listen() {
|
|
798
|
+
return new Promise((resolveListen, reject) => {
|
|
799
|
+
http = (0, import_node_http.createServer)((req, res) => {
|
|
800
|
+
onRequest(req, res).catch((e) => {
|
|
801
|
+
if (!res.headersSent)
|
|
802
|
+
send(res, HTTP_SERVER_ERROR, { ok: false, status: "error", error: String(e) });
|
|
803
|
+
else res.end();
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
http.once("error", reject);
|
|
807
|
+
http.listen(options.port ?? 0, options.host ?? "127.0.0.1", () => {
|
|
808
|
+
const { port } = http.address();
|
|
809
|
+
announce(`HEADLESS_READY port=${port} runDir=${runDir}`);
|
|
810
|
+
resolveListen({ port, runDir });
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async function shutdown() {
|
|
815
|
+
if (closing) return closed;
|
|
816
|
+
closing = true;
|
|
817
|
+
try {
|
|
818
|
+
session?.stop();
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
await flushAll();
|
|
822
|
+
drainLog();
|
|
823
|
+
await new Promise((r) => {
|
|
824
|
+
if (!http) {
|
|
825
|
+
r();
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
http.close(() => r());
|
|
829
|
+
http.closeIdleConnections?.();
|
|
830
|
+
setTimeout(() => http?.closeAllConnections?.(), CLOSE_GRACE_MS).unref();
|
|
831
|
+
});
|
|
832
|
+
await new Promise((r) => {
|
|
833
|
+
events.end(r);
|
|
834
|
+
});
|
|
835
|
+
await options.onClosed?.();
|
|
836
|
+
resolveClosed();
|
|
837
|
+
return closed;
|
|
838
|
+
}
|
|
839
|
+
return handle;
|
|
840
|
+
}
|
|
841
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
842
|
+
0 && (module.exports = {
|
|
843
|
+
DECISION_TEXTS,
|
|
844
|
+
DEFAULT_DECISION_TEXT,
|
|
845
|
+
capOutput,
|
|
846
|
+
compactDecision,
|
|
847
|
+
compactEvent,
|
|
848
|
+
createHeadlessServer,
|
|
849
|
+
firstLines,
|
|
850
|
+
leanDecisionText,
|
|
851
|
+
shortDecision
|
|
852
|
+
});
|
|
853
|
+
//# sourceMappingURL=headless-server.cjs.map
|