agent-from-zero 0.34.2 → 0.35.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/mcp.js CHANGED
@@ -543,6 +543,7 @@ function registerTools(name, specs, client) {
543
543
  for (const spec of specs) {
544
544
  const toolName = `mcp__${name}__${spec.name}`; // namespaced, collision-proof
545
545
  const tool = {
546
+ annotations: spec.annotations,
546
547
  definition: {
547
548
  type: "function",
548
549
  function: {
package/dist/menu.js CHANGED
@@ -1,6 +1,41 @@
1
1
  import readline from "node:readline"; // raw-mode keypress events + the interface to pause
2
+ import chalk from "chalk";
3
+ import { reduceEditor, truncateToWidth } from "./editor.js";
2
4
  import { renderMenu, MENU_HINT } from "./ui.js"; // the pure drawing + the footer hint
3
5
  import { initFormState, reduceForm, renderForm, collectAnswers } from "./form.js"; // the multi-question form
6
+ // Navigation owns arrows; printable text belongs to this editor. Sending a
7
+ // follow-up cancels the pending choice, never implicitly approves it.
8
+ function promptDraft(submit, redraw, cancel) {
9
+ let state = { buffer: "", cursor: 0 };
10
+ let sending = false;
11
+ let error = "";
12
+ return {
13
+ render: () => submit ? `\n${truncateToWidth(`Follow-up ❯ ${state.buffer.slice(0, state.cursor)}${chalk.inverse(state.buffer[state.cursor] ?? " ")}${state.buffer.slice(state.cursor + 1)}`, Math.max(10, (process.stdout.columns || 80) - 1))}\n${truncateToWidth(error || "Type a follow-up · Enter sends text; empty input selects", Math.max(10, (process.stdout.columns || 80) - 1))}` : "",
14
+ handle(str, key) {
15
+ if (sending)
16
+ return true;
17
+ if (!submit || ["up", "down", "tab", "escape"].includes(key.name ?? "") || (key.ctrl && key.name === "c"))
18
+ return false;
19
+ const next = reduceEditor(state, str, key);
20
+ if (next.action === "submit" && state.buffer.trim()) {
21
+ sending = true;
22
+ void Promise.resolve().then(() => submit(state.buffer.trim())).then((accepted) => { if (accepted)
23
+ cancel();
24
+ else
25
+ redraw(); })
26
+ .catch(() => { error = "Could not submit follow-up; draft retained."; redraw(); }).finally(() => { sending = false; });
27
+ return true;
28
+ }
29
+ if (next.action === "edit") {
30
+ state = next.state;
31
+ error = "";
32
+ redraw();
33
+ return true;
34
+ }
35
+ return next.action !== "submit";
36
+ },
37
+ };
38
+ }
4
39
  // An arrow-key selection menu. The whole point: an approval is one keystroke
5
40
  // (↑/↓ then Enter), not "type y, then N, then realize you meant the other".
6
41
  // Selecting costs the user less than typing.
@@ -14,7 +49,8 @@ import { initFormState, reduceForm, renderForm, collectAnswers } from "./form.js
14
49
  // callers treat -1 as "no" (fail closed for an approval).
15
50
  export function promptSelect(rl, // the session's line reader, paused while we own the keys
16
51
  options, // the choices, top to bottom
17
- input = process.stdin) {
52
+ input = process.stdin, // injectable for tests
53
+ onFollowUp) {
18
54
  return new Promise((resolve) => {
19
55
  if (!input.isTTY)
20
56
  return resolve(-1); // no interactive selection without a terminal — caller falls back
@@ -24,11 +60,11 @@ input = process.stdin) {
24
60
  // Redraw the menu in place: move the cursor back up over the previous
25
61
  // render (options + the one footer line), clear downward, reprint. The
26
62
  // question above the menu stays put.
27
- const lineCount = options.length + 1; // options, plus the hint footer
63
+ const lineCount = options.length + 1 + (onFollowUp ? 2 : 0);
28
64
  const draw = (first) => {
29
65
  if (!first)
30
66
  process.stdout.write(`\x1b[${lineCount}A`); // up to the menu's top
31
- process.stdout.write(`\x1b[J${renderMenu(options, selected)}\n${MENU_HINT}\n`); // clear down, repaint + hint
67
+ process.stdout.write(`\x1b[J${renderMenu(options, selected)}\n${MENU_HINT}${draft.render()}\n`);
32
68
  };
33
69
  const finish = (result) => {
34
70
  if (done)
@@ -44,9 +80,12 @@ input = process.stdin) {
44
80
  rl.resume(); // the line reader takes over again
45
81
  resolve(result);
46
82
  };
47
- const onKey = (_str, key) => {
83
+ const draft = promptDraft(onFollowUp, () => draw(false), () => finish(-1));
84
+ const onKey = (str, key) => {
48
85
  if (!key || done)
49
86
  return;
87
+ if (draft.handle(str, key))
88
+ return;
50
89
  switch (key.name) {
51
90
  case "up":
52
91
  case "k":
@@ -88,18 +127,18 @@ input = process.stdin) {
88
127
  // the user answers each and submits, and we hand back organized answers. Same
89
128
  // raw-mode borrow/return dance as promptSelect; the state machine is in form.ts.
90
129
  // Returns the answers, or null if cancelled / not a TTY.
91
- export function promptForm(rl, questions, input = process.stdin) {
130
+ export function promptForm(rl, questions, input = process.stdin, onFollowUp) {
92
131
  return new Promise((resolve) => {
93
132
  if (!input.isTTY || !questions.length)
94
133
  return resolve(null); // no form without a terminal or questions
95
134
  let state = initFormState(questions);
96
135
  let done = false;
97
136
  const wasRaw = input.isRaw === true; // restore this on exit, not false (see promptSelect)
98
- const lineCount = renderForm(questions, state).split("\n").length; // constant for these questions
137
+ const lineCount = renderForm(questions, state).split("\n").length + (onFollowUp ? 2 : 0);
99
138
  const draw = (first) => {
100
139
  if (!first)
101
140
  process.stdout.write(`\x1b[${lineCount}A`); // up over the whole form
102
- process.stdout.write(`\x1b[J${renderForm(questions, state)}\n`); // clear down, repaint
141
+ process.stdout.write(`\x1b[J${renderForm(questions, state)}${draft.render()}\n`);
103
142
  };
104
143
  const finish = (result) => {
105
144
  if (done)
@@ -115,9 +154,12 @@ export function promptForm(rl, questions, input = process.stdin) {
115
154
  rl.resume();
116
155
  resolve(result);
117
156
  };
118
- const onKey = (_str, key) => {
157
+ const draft = promptDraft(onFollowUp, () => draw(false), () => finish(null));
158
+ const onKey = (str, key) => {
119
159
  if (!key || done)
120
160
  return;
161
+ if (draft.handle(str, key))
162
+ return;
121
163
  if (key.name === "up" || key.name === "k") {
122
164
  state = reduceForm(questions, state, "up").state;
123
165
  draw(false);
package/dist/output.js CHANGED
@@ -24,7 +24,7 @@ class StdoutSpinner {
24
24
  // `output`. Each method is the exact line the loop used to run inline.
25
25
  export const STDOUT_OUTPUT = {
26
26
  spinner: (text) => new StdoutSpinner(text),
27
- reasoning: (line) => process.stdout.write(line + "\n"),
27
+ reasoning: () => { },
28
28
  answer: () => {
29
29
  // Lazily build the MarkdownStream on the first token (so a tool-only or
30
30
  // empty turn prints nothing), then end with a single trailing newline —
@@ -0,0 +1,32 @@
1
+ // Terminal-independent storage for folded text. Labels are presentation only:
2
+ // expand exactly once before hooks, permission context or model submission.
3
+ // Keep entries for the session so recalling an earlier draft never loses text.
4
+ export class PastedTextStore {
5
+ entries = new Map();
6
+ sequence = 0;
7
+ fold(text) {
8
+ const lines = text.split(/\r\n|\r|\n/).length;
9
+ if (lines < 4 && text.length < 1000)
10
+ return text;
11
+ const size = lines > 1 ? `${lines - 1} lines` : `${Array.from(text).length} chars`;
12
+ const label = `[Pasted text #${++this.sequence} +${size}]`;
13
+ this.entries.set(label, { label, text });
14
+ return label;
15
+ }
16
+ expand(input) {
17
+ return input.replace(/\[Pasted text #\d+ \+\d+ (?:lines|chars)\]/g, (label) => this.entries.get(label)?.text ?? label);
18
+ }
19
+ // A capsule is one editing unit: arrows jump over it and Backspace removes
20
+ // it whole, including when a mouse/other editor leaves the caret inside it.
21
+ at(input, cursor, direction) {
22
+ for (const match of input.matchAll(/\[Pasted text #\d+ \+\d+ (?:lines|chars)\]/g)) {
23
+ if (!this.entries.has(match[0]))
24
+ continue;
25
+ const start = match.index;
26
+ const end = start + match[0].length;
27
+ if (direction === "left" ? start < cursor && cursor <= end : start <= cursor && cursor < end)
28
+ return { start, end };
29
+ }
30
+ return undefined;
31
+ }
32
+ }