toolcraft 0.0.25 → 0.0.26

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.
Files changed (32) hide show
  1. package/dist/cli.js +11 -9
  2. package/dist/error-report.js +14 -11
  3. package/dist/redaction.d.ts +4 -0
  4. package/dist/redaction.js +70 -0
  5. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +33 -9
  6. package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +2 -1
  7. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +36 -9
  8. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +2 -0
  9. package/node_modules/@poe-code/design-system/dist/components/browser.js +1 -1
  10. package/node_modules/@poe-code/design-system/dist/explorer/actions.js +1 -1
  11. package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +11 -1
  12. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +64 -8
  13. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +9 -11
  14. package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +18 -8
  15. package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +11 -18
  16. package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +2 -10
  17. package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +32 -22
  18. package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +5 -9
  19. package/node_modules/@poe-code/design-system/dist/explorer/render/text.d.ts +12 -0
  20. package/node_modules/@poe-code/design-system/dist/explorer/render/text.js +81 -0
  21. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
  22. package/node_modules/@poe-code/design-system/dist/explorer/state.js +2 -0
  23. package/node_modules/@poe-code/design-system/dist/prompts/index.js +3 -3
  24. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +24 -3
  25. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +1 -0
  26. package/node_modules/auth-store/dist/keychain-store.js +20 -1
  27. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +6 -3
  28. package/node_modules/tiny-mcp-client/dist/internal.d.ts +2 -0
  29. package/node_modules/tiny-mcp-client/dist/internal.js +30 -13
  30. package/node_modules/tiny-mcp-client/src/internal.ts +35 -16
  31. package/node_modules/tiny-mcp-client/src/transports.test.ts +68 -0
  32. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { getExplorerStyles } from "../theme.js";
2
+ import { cellWidth, fitToWidth } from "./text.js";
2
3
  export function renderFooter(state, screen, layout) {
3
4
  const rect = layout.footer;
4
5
  const styles = getExplorerStyles();
@@ -9,22 +10,31 @@ export function renderFooter(state, screen, layout) {
9
10
  const hints = footerHints(state);
10
11
  let x = rect.x + 2;
11
12
  const y = rect.y;
13
+ const endX = rect.x + rect.width;
12
14
  for (const hint of hints) {
13
- if (x >= rect.x + rect.width) {
15
+ if (x >= endX) {
14
16
  break;
15
17
  }
16
18
  if (hint.bracketed === false) {
17
19
  const text = `${hint.key} ${hint.label}`;
18
- screen.put(x, y, text, hint.running ? styles.muted : {});
19
- x += text.length + 2;
20
+ x += putFooterText(screen, x, y, endX, text, hint.running ? styles.muted : {}) + 2;
20
21
  continue;
21
22
  }
22
- screen.put(x, y, `[${hint.key}]`, hint.running ? styles.muted : styles.accent);
23
- x += hint.key.length + 2;
24
- screen.put(x, y, ` ${hint.label}`, hint.running ? styles.muted : {});
25
- x += hint.label.length + 3;
23
+ const keyText = `[${hint.key}]`;
24
+ const keyWidth = putFooterText(screen, x, y, endX, keyText, hint.running ? styles.muted : styles.accent);
25
+ x += keyWidth;
26
+ if (keyWidth < cellWidth(keyText) || x >= endX) {
27
+ break;
28
+ }
29
+ x += putFooterText(screen, x, y, endX, ` ${hint.label}`, hint.running ? styles.muted : {}) + 2;
26
30
  }
27
31
  }
32
+ function putFooterText(screen, x, y, endX, text, style = {}) {
33
+ const remaining = Math.max(0, endX - x);
34
+ const fitted = fitToWidth(text, remaining, x);
35
+ screen.put(x, y, fitted, style);
36
+ return cellWidth(fitted, x);
37
+ }
28
38
  function footerHints(state) {
29
39
  const hints = [];
30
40
  if (state.focused === "detail") {
@@ -36,7 +46,7 @@ function footerHints(state) {
36
46
  continue;
37
47
  }
38
48
  const key = actionKey(entry, id);
39
- const label = state.selected.size > 0 && entry.source === "row"
49
+ const label = state.multiSelect && state.selected.size > 0 && entry.source === "row"
40
50
  ? `${entry.label} ${state.selected.size}`
41
51
  : entry.label;
42
52
  hints.push({ key, label, running: entry.running === true });
@@ -1,4 +1,5 @@
1
1
  import { getExplorerStyles } from "../theme.js";
2
+ import { cellWidth, fitToWidth, padEndCells } from "./text.js";
2
3
  export function renderHeader(state, screen, layout) {
3
4
  const rect = layout.header;
4
5
  const styles = getExplorerStyles();
@@ -7,7 +8,7 @@ export function renderHeader(state, screen, layout) {
7
8
  return;
8
9
  }
9
10
  if (layout.mode === "too-narrow") {
10
- screen.put(0, 0, fit("Terminal too narrow", rect.width), styles.borderFocused);
11
+ screen.put(0, 0, fitToWidth("Terminal too narrow", rect.width), styles.borderFocused);
11
12
  return;
12
13
  }
13
14
  drawTopBorder(screen, state.title, rect.width, styles.border);
@@ -15,13 +16,15 @@ export function renderHeader(state, screen, layout) {
15
16
  const prompt = `${state.title.toLocaleLowerCase()}>`;
16
17
  const filter = state.filter.length > 0 ? ` ${state.filter}` : "";
17
18
  const count = `${state.filtered.length}/${state.rows.length}`;
18
- const selected = state.selected.size > 0 ? ` (${state.selected.size} selected)` : "";
19
+ const selected = state.multiSelect && state.selected.size > 0 ? ` (${state.selected.size} selected)` : "";
19
20
  const spinner = state.detail.loading ? " *" : "";
20
21
  const right = `${count}${selected}${spinner}`;
21
22
  screen.put(0, 1, "│", styles.border);
22
23
  screen.put(Math.max(0, rect.width - 1), 1, "│", styles.border);
23
- screen.put(2, 1, fit(`${prompt}${filter}`, Math.max(0, rect.width - right.length - 5)), styles.accent);
24
- screen.put(Math.max(2, rect.width - right.length - 2), 1, right, styles.muted);
24
+ const rightWidth = cellWidth(right);
25
+ const promptWidth = Math.max(0, rect.width - rightWidth - 5);
26
+ screen.put(2, 1, fitToWidth(`${prompt}${filter}`, promptWidth, 2), styles.accent);
27
+ screen.put(Math.max(2, rect.width - rightWidth - 2), 1, right, styles.muted);
25
28
  }
26
29
  if (rect.height > 2) {
27
30
  drawHorizontal(screen, 2, rect.width, styles.border);
@@ -32,11 +35,10 @@ function drawTopBorder(screen, title, width, style) {
32
35
  screen.put(0, 0, "┌", style);
33
36
  return;
34
37
  }
35
- const label = `─ ${title} `;
36
- const middle = label.length < width - 1
37
- ? `${label}${"─".repeat(width - 1 - label.length)}`
38
- : label.slice(0, Math.max(0, width - 1));
39
- screen.put(0, 0, `┌${middle.slice(0, Math.max(0, width - 2))}┐`, style);
38
+ const innerWidth = Math.max(0, width - 2);
39
+ const label = fitToWidth(`─ ${title} `, innerWidth, 1);
40
+ const middle = padEndCells(label, innerWidth, "─", 1);
41
+ screen.put(0, 0, `┌${middle}┐`, style);
40
42
  }
41
43
  function drawHorizontal(screen, y, width, style) {
42
44
  if (width === 1) {
@@ -45,12 +47,3 @@ function drawHorizontal(screen, y, width, style) {
45
47
  }
46
48
  screen.put(0, y, `├${"─".repeat(Math.max(0, width - 2))}┤`, style);
47
49
  }
48
- function fit(text, width) {
49
- if (width <= 0) {
50
- return "";
51
- }
52
- if (text.length <= width) {
53
- return text;
54
- }
55
- return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
56
- }
@@ -6,6 +6,7 @@ import { renderFooter } from "./footer.js";
6
6
  import { renderHeader } from "./header.js";
7
7
  import { renderList } from "./list.js";
8
8
  import { renderModal } from "./modal.js";
9
+ import { fitToWidth } from "./text.js";
9
10
  const REGION_RENDERERS = [
10
11
  [REGION_HEADER, renderHeader],
11
12
  [REGION_LIST, renderList],
@@ -42,18 +43,9 @@ function renderToast(state, screen) {
42
43
  return;
43
44
  }
44
45
  const styles = getExplorerStyles();
45
- const message = fit(` ${state.toast.message} `, screen.width);
46
+ const message = fitToWidth(` ${state.toast.message} `, screen.width);
46
47
  screen.put(0, y, message, styles.accent);
47
48
  }
48
- function fit(text, width) {
49
- if (width <= 0) {
50
- return "";
51
- }
52
- if (text.length <= width) {
53
- return text;
54
- }
55
- return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
56
- }
57
49
  export { renderDetail } from "./detail.js";
58
50
  export { renderFooter } from "./footer.js";
59
51
  export { renderHeader } from "./header.js";
@@ -1,4 +1,5 @@
1
1
  import { getExplorerStyles } from "../theme.js";
2
+ import { cellWidth, centerCells, fitToWidth, splitGraphemeCells, stripAnsi } from "./text.js";
2
3
  const listLineCache = new WeakMap();
3
4
  export function renderList(state, screen, layout) {
4
5
  const styles = getExplorerStyles();
@@ -17,7 +18,7 @@ export function renderList(state, screen, layout) {
17
18
  listLineCache.set(screen, { rectKey, lines: cache });
18
19
  if (state.filtered.length === 0) {
19
20
  const hint = state.emptyHint;
20
- writeLine(screen, rect, Math.floor(rect.height / 2), center(hint, rect.width), styles.muted);
21
+ writeLine(screen, rect, Math.floor(rect.height / 2), centerCells(hint, rect.width, rect.x), styles.muted);
21
22
  cache.clear();
22
23
  return;
23
24
  }
@@ -43,7 +44,7 @@ export function renderList(state, screen, layout) {
43
44
  if (y >= rect.height) {
44
45
  break;
45
46
  }
46
- const selected = state.selected.has(row.id);
47
+ const selected = state.multiSelect && state.selected.has(row.id);
47
48
  const cursor = rowIndex === state.filtered[state.cursor];
48
49
  const positions = state.matchPositions.get(rowIndex) ?? [];
49
50
  const hash = lineHash(row, selected, cursor, positions);
@@ -67,40 +68,49 @@ function renderRow(screen, rect, rowY, row, opts) {
67
68
  const marker = opts.selected ? "┃" : " ";
68
69
  const cursor = opts.cursor ? "●" : "◌";
69
70
  const focus = opts.cursor && opts.focused ? " ▌" : "";
70
- const badge = row.badge ? ` ${row.badge.text}` : "";
71
71
  const prefix = `${marker} ${cursor} `;
72
- const available = Math.max(0, rect.width - prefix.length - focus.length - badge.length);
73
- const title = stripAnsi(row.title).slice(0, available);
72
+ const prefixWidth = cellWidth(prefix, rect.x);
73
+ const focusWidth = cellWidth(focus);
74
+ const badge = row.badge
75
+ ? fitToWidth(` ${row.badge.text}`, Math.max(0, rect.width - prefixWidth - focusWidth), rect.x + prefixWidth)
76
+ : "";
77
+ const badgeWidth = cellWidth(badge);
78
+ const available = Math.max(0, rect.width - prefixWidth - focusWidth - badgeWidth);
79
+ const rawTitle = stripAnsi(row.title);
80
+ const titleX = rect.x + prefixWidth;
81
+ const title = fitToWidth(rawTitle, available, titleX);
82
+ const titleWasTruncated = cellWidth(rawTitle, titleX) > available;
83
+ const positions = new Set(opts.positions);
74
84
  let x = rect.x;
75
85
  const y = rect.y + rowY;
76
86
  screen.put(x, y, prefix, opts.cursor ? styles.accent : styles.muted);
77
- x += prefix.length;
78
- for (let index = 0; index < title.length; index += 1) {
79
- const style = opts.positions.includes(index) ? styles.matchHighlight : {};
80
- screen.put(x + index, y, title[index], style);
87
+ x += prefixWidth;
88
+ for (const segment of splitGraphemeCells(title, x)) {
89
+ const isTruncationMarker = titleWasTruncated && segment.end === title.length && segment.value === "…";
90
+ const style = !isTruncationMarker && hasMatchPosition(segment.start, segment.end, positions)
91
+ ? styles.matchHighlight
92
+ : {};
93
+ screen.put(x, y, segment.value, style);
94
+ x += segment.width;
81
95
  }
82
96
  if (row.badge) {
83
- screen.put(rect.x + rect.width - badge.length - focus.length, y, badge, styles.tones[row.badge.tone ?? "muted"]);
97
+ screen.put(rect.x + rect.width - badgeWidth - focusWidth, y, badge, styles.tones[row.badge.tone ?? "muted"]);
84
98
  }
85
99
  if (focus) {
86
- screen.put(rect.x + rect.width - focus.length, y, focus, styles.borderFocused);
100
+ screen.put(rect.x + rect.width - focusWidth, y, focus, styles.borderFocused);
87
101
  }
88
102
  }
89
103
  function writeLine(screen, rect, row, text, style = {}) {
90
- screen.put(rect.x, rect.y + row, fit(text, rect.width), style);
104
+ screen.put(rect.x, rect.y + row, fitToWidth(text, rect.width, rect.x), style);
91
105
  }
92
106
  function lineHash(row, selected, cursor, positions) {
93
107
  return `${row.id}:${selected ? 1 : 0}:${cursor ? 1 : 0}:${positions.join(",")}`;
94
108
  }
95
- function center(text, width) {
96
- return `${" ".repeat(Math.max(0, Math.floor((width - text.length) / 2)))}${text}`;
97
- }
98
- function fit(text, width) {
99
- if (text.length <= width) {
100
- return text;
109
+ function hasMatchPosition(start, end, positions) {
110
+ for (let position = start; position < end; position += 1) {
111
+ if (positions.has(position)) {
112
+ return true;
113
+ }
101
114
  }
102
- return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
103
- }
104
- function stripAnsi(value) {
105
- return value.replace(/\u001b\[[0-9;]*m/g, "");
115
+ return false;
106
116
  }
@@ -1,4 +1,5 @@
1
1
  import { getExplorerStyles } from "../theme.js";
2
+ import { fitToWidth, padEndCells } from "./text.js";
2
3
  export function renderModal(state, screen) {
3
4
  if (state.modal === null || screen.width <= 0 || screen.height <= 0) {
4
5
  return;
@@ -11,7 +12,7 @@ export function renderModal(state, screen) {
11
12
  drawBox(screen, x, y, width, height, title(state), styles.borderFocused);
12
13
  const lines = modalLines(state);
13
14
  for (let row = 0; row < Math.min(lines.length, height - 2); row += 1) {
14
- screen.put(x + 2, y + 1 + row, fit(lines[row], width - 4), row === 1 ? styles.accent : {});
15
+ screen.put(x + 2, y + 1 + row, fitToWidth(lines[row], width - 4, x + 2), row === 1 ? styles.accent : {});
15
16
  }
16
17
  }
17
18
  function modalLines(state) {
@@ -54,8 +55,9 @@ function modalHeight(state) {
54
55
  }
55
56
  function drawBox(screen, x, y, width, height, boxTitle, style) {
56
57
  screen.clearRect({ x, y, width, height });
57
- const titleSegment = `─ ${boxTitle} `;
58
- screen.put(x, y, `╭${titleSegment}${"─".repeat(Math.max(0, width - titleSegment.length - 2))}╮`, style);
58
+ const innerWidth = Math.max(0, width - 2);
59
+ const titleSegment = fitToWidth(`─ ${boxTitle} `, innerWidth, x + 1);
60
+ screen.put(x, y, `╭${padEndCells(titleSegment, innerWidth, "─", x + 1)}╮`, style);
59
61
  for (let row = 1; row < height - 1; row += 1) {
60
62
  screen.put(x, y + row, "│", style);
61
63
  screen.put(x + width - 1, y + row, "│", style);
@@ -83,9 +85,3 @@ function paletteLines(state) {
83
85
  function labelFor(action) {
84
86
  return typeof action.label === "function" ? action.label() : action.label;
85
87
  }
86
- function fit(text, width) {
87
- if (text.length <= width) {
88
- return text;
89
- }
90
- return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
91
- }
@@ -0,0 +1,12 @@
1
+ export interface GraphemeCell {
2
+ value: string;
3
+ start: number;
4
+ end: number;
5
+ width: number;
6
+ }
7
+ export declare function cellWidth(value: string, startColumn?: number): number;
8
+ export declare function fitToWidth(text: string, width: number, startColumn?: number): string;
9
+ export declare function centerCells(text: string, width: number, startColumn?: number): string;
10
+ export declare function padEndCells(text: string, width: number, fill?: string, startColumn?: number): string;
11
+ export declare function splitGraphemeCells(value: string, startColumn?: number): GraphemeCell[];
12
+ export declare function stripAnsi(value: string): string;
@@ -0,0 +1,81 @@
1
+ import { displayWidth, graphemes } from "../../dashboard/terminal-width.js";
2
+ const ELLIPSIS = "…";
3
+ export function cellWidth(value, startColumn = 0) {
4
+ return displayWidth(value, startColumn);
5
+ }
6
+ export function fitToWidth(text, width, startColumn = 0) {
7
+ if (width <= 0) {
8
+ return "";
9
+ }
10
+ if (cellWidth(text, startColumn) <= width) {
11
+ return text;
12
+ }
13
+ const ellipsisWidth = cellWidth(ELLIPSIS, startColumn);
14
+ if (ellipsisWidth > width) {
15
+ return takeCells(text, width, startColumn);
16
+ }
17
+ const prefix = takeCells(text, width - ellipsisWidth, startColumn);
18
+ return `${prefix}${ELLIPSIS}`;
19
+ }
20
+ export function centerCells(text, width, startColumn = 0) {
21
+ const fitted = fitToWidth(text, width, startColumn);
22
+ const padding = Math.max(0, Math.floor((width - cellWidth(fitted, startColumn)) / 2));
23
+ return `${" ".repeat(padding)}${fitted}`;
24
+ }
25
+ export function padEndCells(text, width, fill = " ", startColumn = 0) {
26
+ let output = takeCells(text, width, startColumn);
27
+ let used = cellWidth(output, startColumn);
28
+ let column = startColumn + used;
29
+ while (used < width) {
30
+ const fillWidth = cellWidth(fill, column);
31
+ if (fill.length === 0 || fillWidth <= 0 || used + fillWidth > width) {
32
+ const spaces = width - used;
33
+ output += " ".repeat(spaces);
34
+ used += spaces;
35
+ column += spaces;
36
+ continue;
37
+ }
38
+ output += fill;
39
+ used += fillWidth;
40
+ column += fillWidth;
41
+ }
42
+ return output;
43
+ }
44
+ export function splitGraphemeCells(value, startColumn = 0) {
45
+ const cells = [];
46
+ let offset = 0;
47
+ let column = startColumn;
48
+ for (const segment of graphemes(value)) {
49
+ const width = cellWidth(segment, column);
50
+ cells.push({
51
+ value: segment,
52
+ start: offset,
53
+ end: offset + segment.length,
54
+ width
55
+ });
56
+ offset += segment.length;
57
+ column += width;
58
+ }
59
+ return cells;
60
+ }
61
+ export function stripAnsi(value) {
62
+ return value.replace(/\u001b\[[0-9;]*m/g, "");
63
+ }
64
+ function takeCells(text, width, startColumn) {
65
+ if (width <= 0) {
66
+ return "";
67
+ }
68
+ let output = "";
69
+ let used = 0;
70
+ let column = startColumn;
71
+ for (const segment of graphemes(text)) {
72
+ const segmentWidth = cellWidth(segment, column);
73
+ if (used + segmentWidth > width) {
74
+ break;
75
+ }
76
+ output += segment;
77
+ used += segmentWidth;
78
+ column += segmentWidth;
79
+ }
80
+ return output;
81
+ }
@@ -102,6 +102,7 @@ export interface ExplorerState {
102
102
  loading: boolean;
103
103
  };
104
104
  selected: Set<string>;
105
+ multiSelect: boolean;
105
106
  modal: null | {
106
107
  kind: "help";
107
108
  } | {
@@ -16,6 +16,7 @@ export function createInitialState(config, size) {
16
16
  cols: normalizeSize(size.cols),
17
17
  rows: normalizeSize(size.rows)
18
18
  };
19
+ const multiSelect = config.multiSelect ?? true;
19
20
  return {
20
21
  title: config.title,
21
22
  emptyHint: config.emptyHint ?? "No detail",
@@ -35,6 +36,7 @@ export function createInitialState(config, size) {
35
36
  loading: false
36
37
  },
37
38
  selected: new Set(),
39
+ multiSelect,
38
40
  modal: null,
39
41
  toast: null,
40
42
  dirty: REGION_ALL,
@@ -95,12 +95,12 @@ export async function withSpinner(options) {
95
95
  const result = await fn();
96
96
  const msg = stopMessage ? stopMessage(result) : undefined;
97
97
  if (msg) {
98
- process.stdout.write(`\x1b[32m◆\x1b[0m ${msg}\n`);
98
+ process.stdout.write(`${color.green("◆")} ${msg}\n`);
99
99
  }
100
100
  const sub = subtext ? subtext(result) : undefined;
101
101
  if (sub) {
102
102
  for (const line of sub.split("\n")) {
103
- process.stdout.write(`\x1b[90m│\x1b[0m ${line}\n`);
103
+ process.stdout.write(`${color.gray("│")} ${line}\n`);
104
104
  }
105
105
  }
106
106
  return result;
@@ -120,7 +120,7 @@ export async function withSpinner(options) {
120
120
  const sub = subtext ? subtext(result) : undefined;
121
121
  if (sub) {
122
122
  for (const line of sub.split("\n")) {
123
- process.stdout.write(`\x1b[90m│\x1b[0m ${line}\n`);
123
+ process.stdout.write(`${color.gray("│")} ${line}\n`);
124
124
  }
125
125
  }
126
126
  return result;
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
- import { readdir, readFile, writeFile } from "node:fs/promises";
3
+ import { readdir, readFile, realpath, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { buildDockerEnvArgs, buildDockerRunArgs } from "./args.js";
@@ -183,8 +183,7 @@ export async function buildDockerRuntimeTemplate(input) {
183
183
  const runner = input.runner ?? createHostRunner();
184
184
  const engine = input.runtime.engine ?? detectEngine();
185
185
  const context = detectContext();
186
- const dockerfilePath = path.resolve(input.cwd, input.runtime.dockerfile ?? path.join(".poe-code", "Dockerfile"));
187
- const buildContext = path.resolve(input.cwd, input.runtime.build_context ?? ".");
186
+ const { dockerfilePath, buildContext } = await resolveRuntimeBuildPaths(input.cwd, input.runtime);
188
187
  const dockerfileBytes = await readFile(dockerfilePath);
189
188
  const buildContextFiles = await readBuildContextFiles(buildContext);
190
189
  const hash = hashDockerTemplate(dockerfileBytes, buildContextFiles, input.runtime.build_args ?? {}, engine);
@@ -221,6 +220,28 @@ export async function buildDockerRuntimeTemplate(input) {
221
220
  cached: false
222
221
  };
223
222
  }
223
+ async function resolveRuntimeBuildPaths(cwd, runtime) {
224
+ const dockerfilePath = path.resolve(cwd, runtime.dockerfile ?? path.join(".poe-code", "Dockerfile"));
225
+ const buildContext = path.resolve(cwd, runtime.build_context ?? ".");
226
+ const canonicalCwd = await realpath(cwd);
227
+ const canonicalDockerfilePath = await realpath(dockerfilePath);
228
+ const canonicalBuildContext = await realpath(buildContext);
229
+ assertRuntimePathInsideCwd(canonicalCwd, canonicalDockerfilePath, "runtime.dockerfile");
230
+ assertRuntimePathInsideCwd(canonicalCwd, canonicalBuildContext, "runtime.build_context");
231
+ return {
232
+ dockerfilePath: canonicalDockerfilePath,
233
+ buildContext: canonicalBuildContext
234
+ };
235
+ }
236
+ function assertRuntimePathInsideCwd(cwd, targetPath, fieldName) {
237
+ if (!isPathInsideOrEqual(cwd, targetPath)) {
238
+ throw new Error(`${fieldName} must remain inside runtime cwd ${cwd}.`);
239
+ }
240
+ }
241
+ function isPathInsideOrEqual(rootPath, targetPath) {
242
+ const relativePath = path.relative(rootPath, targetPath);
243
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
244
+ }
224
245
  function hashDockerTemplate(dockerfileBytes, buildContextFiles, buildArgs, engine) {
225
246
  const hash = createHash("sha256");
226
247
  hash.update(dockerfileBytes);
@@ -171,6 +171,7 @@ function spawnControlCommand(engine, context, args) {
171
171
  stdio: "ignore"
172
172
  });
173
173
  child.once("error", () => undefined);
174
+ child.unref();
174
175
  }
175
176
  catch {
176
177
  return;
@@ -64,6 +64,19 @@ function runSecurityCommand(command, args, options) {
64
64
  });
65
65
  let stdout = "";
66
66
  let stderr = "";
67
+ let stdinErrorMessage;
68
+ const appendStderr = (message) => {
69
+ stderr = stderr.length === 0
70
+ ? message
71
+ : `${stderr}${stderr.endsWith("\n") ? "" : "\n"}${message}`;
72
+ };
73
+ const appendStdinError = () => {
74
+ if (stdinErrorMessage === undefined) {
75
+ return;
76
+ }
77
+ appendStderr(stdinErrorMessage);
78
+ stdinErrorMessage = undefined;
79
+ };
67
80
  child.stdout?.setEncoding("utf8");
68
81
  child.stdout?.on("data", (chunk) => {
69
82
  stdout += chunk.toString();
@@ -73,17 +86,23 @@ function runSecurityCommand(command, args, options) {
73
86
  stderr += chunk.toString();
74
87
  });
75
88
  if (options?.stdin !== undefined) {
89
+ child.stdin?.once("error", (error) => {
90
+ stdinErrorMessage = error instanceof Error ? error.message : String(error);
91
+ });
76
92
  child.stdin?.end(options.stdin);
77
93
  }
78
94
  child.on("error", (error) => {
79
95
  const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
96
+ appendStdinError();
97
+ appendStderr(message);
80
98
  resolve({
81
99
  stdout,
82
- stderr: stderr ? `${stderr}${message}` : message,
100
+ stderr,
83
101
  exitCode: 127
84
102
  });
85
103
  });
86
104
  child.on("close", (code) => {
105
+ appendStdinError();
87
106
  resolve({
88
107
  stdout,
89
108
  stderr,
@@ -61,12 +61,15 @@ export function createAuthStoreClientStore(options) {
61
61
  }
62
62
  function createNamedSecretStore(key, options, defaults) {
63
63
  const hash = crypto.createHash("sha256").update(key).digest("hex");
64
- const parsedFilePath = options.fileStore?.filePath === undefined ? null : path.parse(options.fileStore.filePath);
64
+ const configuredFilePath = options.fileStore?.filePath;
65
+ const parsedFilePath = configuredFilePath === undefined ? null : path.parse(configuredFilePath);
65
66
  const fileStore = {
66
67
  ...options.fileStore,
68
+ filePath: parsedFilePath === null
69
+ ? undefined
70
+ : path.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
67
71
  salt: options.fileStore?.salt ?? defaults.salt,
68
- defaultDirectory: parsedFilePath?.dir ||
69
- options.fileStore?.defaultDirectory ||
72
+ defaultDirectory: options.fileStore?.defaultDirectory ||
70
73
  defaults.directory,
71
74
  defaultFileName: parsedFilePath === null
72
75
  ? `${hash}.enc`
@@ -400,6 +400,7 @@ export declare class StdioTransport implements McpTransport {
400
400
  private static readonly STDERR_MAX_LENGTH;
401
401
  constructor({ command, args, cwd, env, spawn: spawnProcess, }: StdioTransportOptions);
402
402
  getStderrOutput(): string;
403
+ private appendStderrOutput;
403
404
  dispose(reason?: Error): void;
404
405
  }
405
406
  export declare class HttpTransport implements McpTransport {
@@ -542,6 +543,7 @@ export declare class JsonRpcMessageLayer {
542
543
  onRequest(method: string, handler: JsonRpcRequestHandler): void;
543
544
  onNotification(method: string, handler: JsonRpcNotificationHandler): void;
544
545
  sendRequest(method: string, params?: unknown, options?: JsonRpcRequestOptions): Promise<unknown>;
546
+ cancelRequest(requestId: RequestId, reason: unknown): boolean;
545
547
  dispose(reason?: Error): void;
546
548
  private consumeInput;
547
549
  private resolveInputStreamClosedReason;
@@ -285,6 +285,9 @@ export class McpClient {
285
285
  const abortPromise = new Promise((_, reject) => {
286
286
  const rejectWithAbortReason = () => {
287
287
  sendCancellationNotification();
288
+ if (requestId !== undefined) {
289
+ messageLayer.cancelRequest(requestId, signal.reason);
290
+ }
288
291
  reject(signal.reason);
289
292
  };
290
293
  abortListener = rejectWithAbortReason;
@@ -1511,11 +1514,15 @@ export class StdioTransport {
1511
1514
  const child = this.child;
1512
1515
  this.readable = child.stdout;
1513
1516
  this.writable = child.stdin;
1517
+ const stderrDecoder = new TextDecoder();
1514
1518
  child.stderr.on("data", (chunk) => {
1515
- this.stderrOutput += chunkToString(chunk);
1516
- if (this.stderrOutput.length > StdioTransport.STDERR_MAX_LENGTH) {
1517
- this.stderrOutput = this.stderrOutput.slice(-StdioTransport.STDERR_MAX_LENGTH);
1518
- }
1519
+ const decoded = chunk instanceof Uint8Array
1520
+ ? stderrDecoder.decode(chunk, { stream: true })
1521
+ : `${stderrDecoder.decode()}${String(chunk)}`;
1522
+ this.appendStderrOutput(decoded);
1523
+ });
1524
+ child.stderr.once("end", () => {
1525
+ this.appendStderrOutput(stderrDecoder.decode());
1519
1526
  });
1520
1527
  this.closed = new Promise((resolve) => {
1521
1528
  let settled = false;
@@ -1555,6 +1562,15 @@ export class StdioTransport {
1555
1562
  getStderrOutput() {
1556
1563
  return this.stderrOutput;
1557
1564
  }
1565
+ appendStderrOutput(chunk) {
1566
+ if (chunk.length === 0) {
1567
+ return;
1568
+ }
1569
+ this.stderrOutput += chunk;
1570
+ if (this.stderrOutput.length > StdioTransport.STDERR_MAX_LENGTH) {
1571
+ this.stderrOutput = this.stderrOutput.slice(-StdioTransport.STDERR_MAX_LENGTH);
1572
+ }
1573
+ }
1558
1574
  dispose(reason = new Error("Stdio transport disposed")) {
1559
1575
  void reason;
1560
1576
  if (this.disposed) {
@@ -1966,15 +1982,6 @@ export class McpError extends Error {
1966
1982
  export function serializeJsonRpcMessage(message) {
1967
1983
  return `${JSON.stringify(message)}\n`;
1968
1984
  }
1969
- function chunkToString(chunk) {
1970
- if (typeof chunk === "string") {
1971
- return chunk;
1972
- }
1973
- if (chunk instanceof Uint8Array) {
1974
- return Buffer.from(chunk).toString("utf8");
1975
- }
1976
- return String(chunk);
1977
- }
1978
1985
  function normalizeLine(line) {
1979
1986
  return line.endsWith("\r") ? line.slice(0, -1) : line;
1980
1987
  }
@@ -2168,6 +2175,16 @@ export class JsonRpcMessageLayer {
2168
2175
  }
2169
2176
  });
2170
2177
  }
2178
+ cancelRequest(requestId, reason) {
2179
+ const pending = this.pendingRequests.get(requestId);
2180
+ if (pending === undefined) {
2181
+ return false;
2182
+ }
2183
+ this.pendingRequests.delete(requestId);
2184
+ clearTimeout(pending.timeout);
2185
+ pending.reject(reason);
2186
+ return true;
2187
+ }
2171
2188
  dispose(reason = new Error("JSON-RPC message layer disposed")) {
2172
2189
  if (this.disposedError !== undefined) {
2173
2190
  return;