omniagent 0.1.7 → 0.1.8

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.
@@ -0,0 +1,362 @@
1
+ import { constants } from "node:fs";
2
+ import { access, chmod } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import headless from "@xterm/headless";
7
+ import pty from "node-pty";
8
+ const { Terminal } = headless;
9
+ const require$1 = createRequire(import.meta.url);
10
+ class PtyScenarioError extends Error {
11
+ command;
12
+ args;
13
+ timedOut;
14
+ raw;
15
+ screen;
16
+ snapshots;
17
+ debug;
18
+ constructor(message, options) {
19
+ super(message);
20
+ this.name = "PtyScenarioError";
21
+ this.command = options.command;
22
+ this.args = options.args;
23
+ this.timedOut = options.timedOut;
24
+ this.raw = options.raw;
25
+ this.screen = options.screen;
26
+ this.snapshots = options.snapshots;
27
+ this.debug = options.debug;
28
+ }
29
+ }
30
+ function enterKey() {
31
+ return os.platform() === "win32" ? "\r" : "\r";
32
+ }
33
+ function escapeKey() {
34
+ return "\x1B";
35
+ }
36
+ function typeTextSteps(text, delayMs) {
37
+ return [...text].map((char) => ({ write: char, waitMs: delayMs }));
38
+ }
39
+ function createHeadlessTerminal(cols = 100, rows = 40) {
40
+ return new Terminal({
41
+ allowProposedApi: true,
42
+ cols,
43
+ rows,
44
+ scrollback: 1e3
45
+ });
46
+ }
47
+ async function runPtyScenario(options) {
48
+ const args = options.args ?? [];
49
+ const cols = options.cols ?? 100;
50
+ const rows = options.rows ?? 40;
51
+ const terminal = createHeadlessTerminal(cols, rows);
52
+ const snapshots = {};
53
+ let raw = "";
54
+ let exited = false;
55
+ let exitCode = null;
56
+ let timedOut = false;
57
+ let child = null;
58
+ let terminalDataDisposable = null;
59
+ let terminalBinaryDisposable = null;
60
+ let timeout = null;
61
+ let removeAbortListener = null;
62
+ let cancelScenario = null;
63
+ const timeoutMs = options.timeoutMs ?? 45e3;
64
+ const buildScenarioError = (message) => new PtyScenarioError(message, {
65
+ command: options.command,
66
+ args,
67
+ timedOut,
68
+ raw,
69
+ screen: readScreen(terminal),
70
+ snapshots: { ...snapshots },
71
+ debug: buildDebugArtifacts({
72
+ options,
73
+ args,
74
+ raw,
75
+ screen: readScreen(terminal),
76
+ snapshots
77
+ })
78
+ });
79
+ const cancellationPromise = new Promise((_, reject) => {
80
+ cancelScenario = (message) => {
81
+ if (!exited) {
82
+ timedOut = true;
83
+ if (child) {
84
+ safeKillPty(child);
85
+ }
86
+ }
87
+ reject(buildScenarioError(message));
88
+ };
89
+ });
90
+ cancellationPromise.catch(() => {
91
+ });
92
+ if (options.signal) {
93
+ const abortHandler = () => {
94
+ cancelScenario?.(formatAbortReason(options.signal, timeoutMs));
95
+ };
96
+ if (options.signal.aborted) {
97
+ abortHandler();
98
+ } else {
99
+ options.signal.addEventListener("abort", abortHandler, { once: true });
100
+ removeAbortListener = () => options.signal?.removeEventListener("abort", abortHandler);
101
+ }
102
+ } else {
103
+ timeout = setTimeout(() => {
104
+ cancelScenario?.(`PTY scenario timed out after ${formatDuration(timeoutMs)}.`);
105
+ }, timeoutMs);
106
+ }
107
+ const withScenarioTimeout = async (promise) => Promise.race([promise, cancellationPromise]);
108
+ const throwIfTimedOut = () => {
109
+ if (timedOut) {
110
+ throw buildScenarioError(`PTY scenario timed out after ${formatDuration(timeoutMs)}.`);
111
+ }
112
+ };
113
+ try {
114
+ await withScenarioTimeout(ensureNodePtySpawnHelperExecutable());
115
+ child = pty.spawn(options.command, args, {
116
+ name: "xterm-256color",
117
+ cols,
118
+ rows,
119
+ cwd: options.cwd ?? process.cwd(),
120
+ env: {
121
+ ...process.env,
122
+ TERM: "xterm-256color",
123
+ ...options.env
124
+ }
125
+ });
126
+ terminalDataDisposable = terminal.onData((data) => {
127
+ if (!exited && child) {
128
+ child.write(data);
129
+ }
130
+ });
131
+ terminalBinaryDisposable = terminal.onBinary((data) => {
132
+ if (!exited && child) {
133
+ child.write(data);
134
+ }
135
+ });
136
+ child.onData((chunk) => {
137
+ raw += chunk;
138
+ terminal.write(chunk);
139
+ });
140
+ child.onExit((event) => {
141
+ exited = true;
142
+ exitCode = event.exitCode;
143
+ });
144
+ if (!exited) {
145
+ throwIfTimedOut();
146
+ }
147
+ for (const step of options.steps) {
148
+ throwIfTimedOut();
149
+ if (step.skipIf != null && matchesWaitFor(
150
+ step.skipIf,
151
+ step.skipIfSource ?? "raw",
152
+ () => raw,
153
+ () => readScreen(terminal)
154
+ )) {
155
+ continue;
156
+ }
157
+ if (step.waitMs != null) {
158
+ await withScenarioTimeout(sleep(step.waitMs));
159
+ throwIfTimedOut();
160
+ }
161
+ if (step.write != null) {
162
+ throwIfTimedOut();
163
+ child.write(step.write);
164
+ }
165
+ if (step.waitFor != null) {
166
+ const matched = await withScenarioTimeout(
167
+ waitForOutput({
168
+ match: step.waitFor,
169
+ source: step.waitForSource ?? "raw",
170
+ timeoutMs: step.waitForTimeoutMs ?? options.timeoutMs ?? 45e3,
171
+ getRaw: () => raw,
172
+ getScreen: () => readScreen(terminal)
173
+ })
174
+ );
175
+ throwIfTimedOut();
176
+ if (!matched && !step.optional) {
177
+ throw buildScenarioError(
178
+ `Timed out waiting for ${step.capture ?? "expected TUI output"}.`
179
+ );
180
+ }
181
+ }
182
+ if (step.capture != null) {
183
+ await withScenarioTimeout(sleep(step.captureWaitMs ?? 250));
184
+ throwIfTimedOut();
185
+ snapshots[step.capture] = {
186
+ raw,
187
+ screen: readScreen(terminal)
188
+ };
189
+ }
190
+ }
191
+ await withScenarioTimeout(sleep(options.finalWaitMs ?? 250));
192
+ throwIfTimedOut();
193
+ const screen = readScreen(terminal);
194
+ const debug = buildDebugArtifacts({ options, args, raw, screen, snapshots });
195
+ return {
196
+ command: options.command,
197
+ args,
198
+ exitCode,
199
+ timedOut,
200
+ raw,
201
+ screen,
202
+ snapshots,
203
+ debug
204
+ };
205
+ } catch (error) {
206
+ if (error instanceof PtyScenarioError) {
207
+ throw error;
208
+ }
209
+ const message = error instanceof Error ? error.message : String(error);
210
+ throw buildScenarioError(message);
211
+ } finally {
212
+ if (timeout) {
213
+ clearTimeout(timeout);
214
+ }
215
+ removeAbortListener?.();
216
+ terminalDataDisposable?.dispose();
217
+ terminalBinaryDisposable?.dispose();
218
+ if (!exited && child) {
219
+ safeKillPty(child);
220
+ }
221
+ terminal.dispose();
222
+ }
223
+ }
224
+ function formatAbortReason(signal, timeoutMs) {
225
+ const reason = signal?.reason;
226
+ if (reason instanceof Error) {
227
+ return reason.message;
228
+ }
229
+ if (typeof reason === "string" && reason.trim().length > 0) {
230
+ return reason;
231
+ }
232
+ return `PTY scenario timed out after ${formatDuration(timeoutMs)}.`;
233
+ }
234
+ function readScreen(terminal) {
235
+ const buffer = terminal.buffer.active;
236
+ const lines = [];
237
+ for (let i = 0; i < buffer.length; i += 1) {
238
+ const line = buffer.getLine(i);
239
+ if (line == null) {
240
+ continue;
241
+ }
242
+ lines.push(line.translateToString(true));
243
+ }
244
+ return lines.join("\n");
245
+ }
246
+ function safeKillPty(child) {
247
+ try {
248
+ child.kill();
249
+ } catch {
250
+ }
251
+ }
252
+ function formatCommand(command, args) {
253
+ return [command, ...args].join(" ");
254
+ }
255
+ function buildDebugArtifacts(options) {
256
+ if (!options.options.debug?.enabled) {
257
+ return [];
258
+ }
259
+ const debug = [];
260
+ if (options.options.debug.includeRawOutput) {
261
+ debug.push({
262
+ type: "raw-output",
263
+ label: "pty.raw",
264
+ content: options.raw,
265
+ command: formatCommand(options.options.command, options.args)
266
+ });
267
+ }
268
+ if (options.options.debug.includeScreenSnapshots) {
269
+ for (const [label, snapshot] of Object.entries(options.snapshots)) {
270
+ debug.push({
271
+ type: "screen-snapshot",
272
+ label,
273
+ content: snapshot.screen,
274
+ mimeType: "text/plain"
275
+ });
276
+ }
277
+ debug.push({
278
+ type: "screen-snapshot",
279
+ label: "final",
280
+ content: options.screen,
281
+ mimeType: "text/plain"
282
+ });
283
+ }
284
+ return debug;
285
+ }
286
+ function formatDuration(timeoutMs) {
287
+ if (timeoutMs % 6e4 === 0) {
288
+ return `${timeoutMs / 6e4}m`;
289
+ }
290
+ if (timeoutMs % 1e3 === 0) {
291
+ return `${timeoutMs / 1e3}s`;
292
+ }
293
+ return `${timeoutMs}ms`;
294
+ }
295
+ async function waitForOutput(options) {
296
+ const intervalMs = 50;
297
+ const deadline = Date.now() + options.timeoutMs;
298
+ while (Date.now() <= deadline) {
299
+ if (matchesWaitFor(options.match, options.source, options.getRaw, options.getScreen)) {
300
+ return true;
301
+ }
302
+ const remainingMs = deadline - Date.now();
303
+ if (remainingMs <= 0) {
304
+ break;
305
+ }
306
+ await sleep(Math.min(intervalMs, remainingMs));
307
+ }
308
+ return matchesWaitFor(options.match, options.source, options.getRaw, options.getScreen);
309
+ }
310
+ function matchesWaitFor(match, source, getRaw, getScreen) {
311
+ if (typeof match === "function") {
312
+ return match({ raw: getRaw(), screen: getScreen() });
313
+ }
314
+ const value = source === "screen" ? getScreen() : getRaw();
315
+ if (typeof match === "string") {
316
+ return value.includes(match);
317
+ }
318
+ match.lastIndex = 0;
319
+ return match.test(value);
320
+ }
321
+ async function ensureNodePtySpawnHelperExecutable() {
322
+ if (process.platform === "win32") {
323
+ return;
324
+ }
325
+ const packageRoot = path.dirname(require$1.resolve("node-pty/package.json"));
326
+ const candidates = [
327
+ path.join(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
328
+ path.join(packageRoot, "build", "Release", "spawn-helper")
329
+ ];
330
+ for (const candidate of candidates) {
331
+ if (!await fileExists(candidate)) {
332
+ continue;
333
+ }
334
+ try {
335
+ await access(candidate, constants.X_OK);
336
+ return;
337
+ } catch {
338
+ try {
339
+ await chmod(candidate, 493);
340
+ return;
341
+ } catch {
342
+ }
343
+ }
344
+ }
345
+ }
346
+ async function fileExists(filePath) {
347
+ try {
348
+ await access(filePath, constants.F_OK);
349
+ return true;
350
+ } catch {
351
+ return false;
352
+ }
353
+ }
354
+ function sleep(ms) {
355
+ return new Promise((resolve) => setTimeout(resolve, ms));
356
+ }
357
+ export {
358
+ escapeKey as a,
359
+ enterKey as e,
360
+ runPtyScenario as r,
361
+ typeTextSteps as t
362
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniagent",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Unified agent configuration CLI that compiles canonical agent configs to multiple runtimes.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,8 +43,10 @@
43
43
  "author": "plsdontemailme@joeroddy.net",
44
44
  "license": "MIT",
45
45
  "dependencies": {
46
+ "@xterm/headless": "^5.5.0",
46
47
  "jiti": "^2.6.1",
47
48
  "minimatch": "^10.2.5",
49
+ "node-pty": "^1.0.0",
48
50
  "yargs": "^17.7.2"
49
51
  },
50
52
  "devDependencies": {