toolcraft 0.0.167 → 0.0.169

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/composition.json CHANGED
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.167",
116
+ "version": "0.0.169",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.167",
126
+ "version": "0.0.169",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "toolcraft",
116
- "version": "0.0.167",
116
+ "version": "0.0.169",
117
117
  "license": "MIT"
118
118
  },
119
119
  {
@@ -123,7 +123,7 @@
123
123
  },
124
124
  {
125
125
  "name": "toolcraft-schema",
126
- "version": "0.0.167",
126
+ "version": "0.0.169",
127
127
  "license": "MIT"
128
128
  },
129
129
  {
@@ -3,6 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { readFile, realpath, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
+ import { setTimeout as sleep } from "node:timers/promises";
6
7
  import { buildDockerEnvArgs, buildDockerRunArgs } from "./args.js";
7
8
  import { readDockerBuildContextFiles } from "./build-context.js";
8
9
  import { buildContextArgs, detectContext } from "./context.js";
@@ -316,17 +317,24 @@ async function runAndRead(runner, spec) {
316
317
  }
317
318
  return output;
318
319
  }
319
- async function runAndReadBytes(runner, spec) {
320
- const handle = runner.exec(spec);
321
- const stdout = readStreamBytes(handle.stdout);
322
- const stderr = readStream(handle.stderr);
323
- const result = await handle.result;
324
- const output = await stdout;
325
- if (result.exitCode !== 0) {
326
- const errorOutput = await stderr;
327
- throw new Error(`Command failed with exit code ${result.exitCode}: ${spec.command} ${(spec.args ?? []).join(" ")}${errorOutput ? `\n${errorOutput}` : ""}`);
320
+ async function runJobRead(runner, spec) {
321
+ spec.signal?.throwIfAborted();
322
+ const controller = new AbortController();
323
+ const signal = spec.signal
324
+ ? AbortSignal.any([spec.signal, controller.signal])
325
+ : controller.signal;
326
+ const handle = runner.exec({ ...spec, signal });
327
+ const reads = [handle.result, readStreamBytes(handle.stdout), readStream(handle.stderr)];
328
+ try {
329
+ const [result, stdout, stderr] = await Promise.all(reads);
330
+ signal.throwIfAborted();
331
+ return { exitCode: result.exitCode, stdout, stderr };
332
+ }
333
+ catch (error) {
334
+ controller.abort();
335
+ await Promise.allSettled(reads);
336
+ throw error;
328
337
  }
329
- return output;
330
338
  }
331
339
  async function runOrThrow(runner, spec) {
332
340
  await runAndRead(runner, spec);
@@ -498,15 +506,15 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
498
506
  envId: containerId,
499
507
  tool: detachedJobContext?.tool ?? "docker",
500
508
  argv: detachedJobContext?.argv ?? ["attach", containerId],
501
- async status() {
509
+ async status(opts) {
502
510
  if (detachedJobContext !== null) {
503
- const detached = await readDetachedState(containerId, jobId, runner, engine, context);
511
+ const detached = await readDetachedState(containerId, jobId, runner, engine, context, opts?.signal);
504
512
  if (detached.kind === "unreachable") {
505
513
  return "lost";
506
514
  }
507
515
  return detached.kind === "exited" ? "exited" : "running";
508
516
  }
509
- const handle = runner.exec({
517
+ const result = await runJobRead(runner, {
510
518
  command: engine,
511
519
  args: [
512
520
  ...buildContextArgs(engine, context),
@@ -516,14 +524,13 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
516
524
  containerId
517
525
  ],
518
526
  stdout: "pipe",
519
- stderr: "pipe"
527
+ stderr: "pipe",
528
+ signal: opts?.signal
520
529
  });
521
- const stdout = await readStream(handle.stdout);
522
- const result = await handle.result;
523
530
  if (result.exitCode !== 0) {
524
531
  return "lost";
525
532
  }
526
- return stdout.trim() === "exited" ? "exited" : "running";
533
+ return result.stdout.toString("utf8").trim() === "exited" ? "exited" : "running";
527
534
  },
528
535
  async *stream(opts) {
529
536
  const logFile = shellQuote(`/tmp/poe-jobs/${jobId}.log`);
@@ -533,8 +540,9 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
533
540
  let byteOffset = opts?.sinceByte ?? 0;
534
541
  let pendingBytes = Buffer.alloc(0);
535
542
  let pendingByteOffset = byteOffset;
543
+ let finalRead = false;
536
544
  while (true) {
537
- const stdout = await runAndReadBytes(runner, {
545
+ const spec = {
538
546
  command: engine,
539
547
  args: [
540
548
  ...buildContextArgs(engine, context),
@@ -545,8 +553,13 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
545
553
  `test -f ${logFile}${sinceCondition} && tail -c +${byteOffset + 1} ${logFile} || true`
546
554
  ],
547
555
  stdout: "pipe",
548
- stderr: "pipe"
549
- });
556
+ stderr: "pipe",
557
+ signal: opts?.signal
558
+ };
559
+ const { stdout, stderr, exitCode } = await runJobRead(runner, spec);
560
+ if (exitCode !== 0) {
561
+ throw new Error(`Command failed with exit code ${exitCode}: ${spec.command} ${(spec.args ?? []).join(" ")}${stderr ? `\n${stderr}` : ""}`);
562
+ }
550
563
  if (stdout.byteLength > 0) {
551
564
  const combined = pendingBytes.byteLength === 0
552
565
  ? stdout
@@ -560,10 +573,18 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
560
573
  pendingByteOffset += completeLength;
561
574
  }
562
575
  }
563
- if (opts?.follow !== true || (await this.status()) !== "running") {
576
+ if (opts?.follow !== true || finalRead) {
564
577
  return;
565
578
  }
566
- await new Promise((resolve) => setTimeout(resolve, 250));
579
+ const status = await this.status({ signal: opts?.signal });
580
+ if (status === "exited" && detachedJobContext !== null) {
581
+ finalRead = true;
582
+ continue;
583
+ }
584
+ if (status !== "running") {
585
+ return;
586
+ }
587
+ await sleep(250, undefined, { signal: opts?.signal });
567
588
  }
568
589
  },
569
590
  async wait() {
@@ -660,9 +681,9 @@ function utf8SequenceLength(byte) {
660
681
  }
661
682
  return 0;
662
683
  }
663
- async function readDetachedState(containerId, jobId, runner, engine, context) {
684
+ async function readDetachedState(containerId, jobId, runner, engine, context, signal) {
664
685
  const exitFile = shellQuote(`/tmp/poe-jobs/${jobId}.exit`);
665
- const handle = runner.exec({
686
+ const result = await runJobRead(runner, {
666
687
  command: engine,
667
688
  args: [
668
689
  ...buildContextArgs(engine, context),
@@ -673,14 +694,13 @@ async function readDetachedState(containerId, jobId, runner, engine, context) {
673
694
  `test -f ${exitFile} && cat ${exitFile} || true`
674
695
  ],
675
696
  stdout: "pipe",
676
- stderr: "pipe"
697
+ stderr: "pipe",
698
+ signal
677
699
  });
678
- const stdout = await readStream(handle.stdout);
679
- const result = await handle.result;
680
700
  if (result.exitCode !== 0) {
681
701
  return { kind: "unreachable" };
682
702
  }
683
- const text = stdout.trim();
703
+ const text = result.stdout.toString("utf8").trim();
684
704
  if (text.length === 0) {
685
705
  return { kind: "running" };
686
706
  }
@@ -127,11 +127,14 @@ export interface JobHandle {
127
127
  readonly envId: string;
128
128
  readonly tool: string;
129
129
  readonly argv: string[];
130
- status(): Promise<JobStatus>;
130
+ status(opts?: {
131
+ signal?: AbortSignal;
132
+ }): Promise<JobStatus>;
131
133
  stream(opts?: {
132
134
  sinceByte?: number;
133
135
  since?: Date;
134
136
  follow?: boolean;
137
+ signal?: AbortSignal;
135
138
  }): AsyncIterable<LogChunk>;
136
139
  wait(): Promise<{
137
140
  exitCode: number;
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.167",
11
+ "version": "0.0.169",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -28,17 +28,24 @@ function stripAnsi(value) {
28
28
  return value.replace(/\u001b\[[0-9;]*m/g, "");
29
29
  }
30
30
  function matchSubsequence(query, text) {
31
+ const queryCharacters = Array.from(query);
32
+ const textCharacters = Array.from(text);
31
33
  let previousStates = [];
32
- for (let queryIndex = 0; queryIndex < query.length; queryIndex += 1) {
34
+ for (let queryIndex = 0; queryIndex < queryCharacters.length; queryIndex += 1) {
33
35
  const states = [];
34
- for (let textIndex = 0; textIndex < text.length; textIndex += 1) {
35
- if (text[textIndex] !== query[queryIndex]) {
36
+ let offset = 0;
37
+ for (let textIndex = 0; textIndex < textCharacters.length; textIndex += 1) {
38
+ const character = textCharacters[textIndex];
39
+ const position = offset;
40
+ offset += character.length;
41
+ if (character !== queryCharacters[queryIndex]) {
36
42
  continue;
37
43
  }
44
+ const positions = character.length === 2 ? [position, position + 1] : [position];
38
45
  if (queryIndex === 0) {
39
46
  states[textIndex] = {
40
- score: characterScore(text, textIndex, undefined) + Math.max(0, EARLY_MATCH_BONUS - textIndex),
41
- positions: [textIndex]
47
+ score: characterScore(textCharacters, textIndex, undefined) + Math.max(0, EARLY_MATCH_BONUS - position),
48
+ positions
42
49
  };
43
50
  continue;
44
51
  }
@@ -48,8 +55,8 @@ function matchSubsequence(query, text) {
48
55
  continue;
49
56
  }
50
57
  const next = {
51
- score: previous.score + characterScore(text, textIndex, previousIndex),
52
- positions: [...previous.positions, textIndex]
58
+ score: previous.score + characterScore(textCharacters, textIndex, previousIndex),
59
+ positions: [...previous.positions, ...positions]
53
60
  };
54
61
  if (isBetterMatch(next, states[textIndex])) {
55
62
  states[textIndex] = next;
@@ -1,9 +1,12 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import * as readline from "node:readline";
3
+ import { Readable } from "node:stream";
4
+ import { StringDecoder } from "node:string_decoder";
3
5
  import { graphemes } from "../../dashboard/terminal-width.js";
4
6
  import { CANCEL } from "./cancel-symbol.js";
5
7
  import { mapKey } from "./keys.js";
6
8
  import { wrapFrame } from "./wrap.js";
9
+ const pendingCarriageReturns = new WeakMap();
7
10
  const cursor = {
8
11
  hide: "\x1b[?25l",
9
12
  show: "\x1b[?25h",
@@ -128,6 +131,88 @@ export class Prompt extends EventEmitter {
128
131
  this.state = "cancel";
129
132
  return Promise.resolve(CANCEL);
130
133
  }
134
+ const input = this.input;
135
+ if (input instanceof Readable) {
136
+ return new Promise((resolve, reject) => {
137
+ const decoder = new StringDecoder("utf8");
138
+ let line = "";
139
+ let settled = false;
140
+ const settle = (value, remainder) => {
141
+ if (settled)
142
+ return;
143
+ settled = true;
144
+ input.removeListener("data", onData);
145
+ input.removeListener("end", onEnd);
146
+ input.removeListener("close", onCancel);
147
+ input.removeListener("error", settle);
148
+ this.signal?.removeEventListener("abort", onCancel);
149
+ input.pause();
150
+ if (remainder?.length)
151
+ input.unshift(remainder, input.readableEncoding ?? undefined);
152
+ if (value instanceof Error)
153
+ reject(value);
154
+ else
155
+ resolve(value);
156
+ };
157
+ const onCancel = () => {
158
+ this.state = "cancel";
159
+ settle(CANCEL);
160
+ };
161
+ const onEnd = () => settle(line + decoder.end());
162
+ const onData = (chunk) => {
163
+ if (chunk.length === 0)
164
+ return;
165
+ if (settled) {
166
+ if (!input.destroyed)
167
+ input.unshift(chunk, input.readableEncoding ?? undefined);
168
+ return;
169
+ }
170
+ let offset = 0;
171
+ const previousCarriageReturn = pendingCarriageReturns.get(input);
172
+ if (previousCarriageReturn !== undefined) {
173
+ pendingCarriageReturns.delete(input);
174
+ const isLineFeed = typeof chunk === "string" ? chunk[0] === "\n" : chunk[0] === 10;
175
+ if (isLineFeed && Date.now() - previousCarriageReturn <= 100)
176
+ offset = 1;
177
+ }
178
+ for (; offset < chunk.length; offset += 1) {
179
+ const character = typeof chunk === "string" ? chunk[offset] : decoder.write(chunk.subarray(offset, offset + 1));
180
+ if (character.endsWith("\r") || character.endsWith("\n")) {
181
+ line += character.slice(0, -1);
182
+ offset += 1;
183
+ if (character.endsWith("\r")) {
184
+ if (offset === chunk.length) {
185
+ pendingCarriageReturns.set(input, Date.now());
186
+ }
187
+ else if (typeof chunk === "string" ? chunk[offset] === "\n" : chunk[offset] === 10) {
188
+ offset += 1;
189
+ }
190
+ }
191
+ settle(line, chunk.slice(offset));
192
+ return;
193
+ }
194
+ line += character;
195
+ }
196
+ };
197
+ input.once("end", onEnd);
198
+ input.once("close", onCancel);
199
+ input.once("error", settle);
200
+ this.signal?.addEventListener("abort", onCancel, { once: true });
201
+ if (this.signal?.aborted)
202
+ onCancel();
203
+ else if (input.readableEnded)
204
+ onEnd();
205
+ else if (input.destroyed)
206
+ onCancel();
207
+ else if (!settled) {
208
+ input.on("data", onData);
209
+ if (!settled)
210
+ input.resume();
211
+ if (settled)
212
+ input.pause();
213
+ }
214
+ });
215
+ }
131
216
  return new Promise((resolve, reject) => {
132
217
  let rl = null;
133
218
  let settled = false;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.167",
3
+ "version": "0.0.169",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.167",
3
+ "version": "0.0.169",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -158,7 +158,7 @@
158
158
  "yaml"
159
159
  ],
160
160
  "optionalDependencies": {
161
- "toolcraft-schema": "0.0.167",
161
+ "toolcraft-schema": "0.0.169",
162
162
  "toolcraft-design": "*",
163
163
  "@poe-code/frontmatter": "*",
164
164
  "@poe-code/agent-mcp-config": "*",