@cydm/pie 2.1.0 → 2.1.1

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 (37) hide show
  1. package/README.md +6 -3
  2. package/dist/builtin/extensions/ask-user/index.js +1 -1
  3. package/dist/builtin/extensions/computer-use/index.js +3993 -107
  4. package/dist/builtin/extensions/plan-mode/index.js +1 -1
  5. package/dist/builtin/extensions/subagent/index.js +2 -2
  6. package/dist/builtin/extensions/todo/index.js +1 -1
  7. package/dist/builtin/skills/chrome-devtools-axi/LICENSE +21 -0
  8. package/dist/builtin/skills/chrome-devtools-axi/SKILL.md +20 -1
  9. package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-bridge.js +20034 -0
  10. package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-cli.js +8 -2
  11. package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-runtime.js +26 -0
  12. package/dist/builtin/skills/chrome-devtools-axi/chunk-42SGITKJ.js +11 -0
  13. package/dist/builtin/skills/chrome-devtools-axi/chunk-OJ6N23UZ.js +47 -0
  14. package/dist/builtin/skills/chrome-devtools-axi/chunk-UPATXALW.js +215 -0
  15. package/dist/builtin/skills/chrome-devtools-axi/cli-F2VCSVL4.js +4424 -0
  16. package/dist/builtin/skills/skill-creator/SKILL.md +5 -2
  17. package/dist/builtin/skills/skill-creator/eval-viewer/generate_review.mjs +7 -20
  18. package/dist/builtin/skills/skill-creator/eval-viewer/viewer.html +15 -15
  19. package/dist/builtin/skills/skill-creator/scripts/aggregate_benchmark.mjs +20 -19
  20. package/dist/builtin/skills/skill-creator/scripts/generate_report.mjs +3 -3
  21. package/dist/builtin/skills/skill-creator/scripts/improve_description.mjs +12 -8
  22. package/dist/builtin/skills/skill-creator/scripts/package_skill.mjs +1 -0
  23. package/dist/builtin/skills/skill-creator/scripts/pie_runner.mjs +77 -31
  24. package/dist/builtin/skills/skill-creator/scripts/run_eval.mjs +38 -11
  25. package/dist/builtin/skills/skill-creator/scripts/run_loop.mjs +12 -6
  26. package/dist/builtin/skills/skill-creator/scripts/skill_metadata.mjs +12 -1
  27. package/dist/builtin/skills/tui-use/LICENSE +21 -0
  28. package/dist/builtin/skills/tui-use/README.md +13 -3
  29. package/dist/builtin/skills/tui-use/SKILL.md +87 -39
  30. package/dist/builtin/skills/tui-use/tui-use-cli.cjs +10608 -0
  31. package/dist/builtin/skills/tui-use/tui-use-cli.js +39 -0
  32. package/dist/builtin/skills/tui-use/tui-use-daemon.cjs +7916 -0
  33. package/dist/chunks/{chunk-PMPD56N2.js → chunk-LU7RKALH.js} +82 -24
  34. package/dist/chunks/{chunk-G2POWIA5.js → chunk-TJ2F4FL5.js} +64 -16
  35. package/dist/cli.js +7503 -87
  36. package/package.json +26 -18
  37. package/scripts/repair-node-pty.mjs +45 -0
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { parseSkillFile, resolveSkillPath } from "./skill_metadata.mjs";
7
+ import { parseSkillFile, readJsonFile, resolveSkillPath } from "./skill_metadata.mjs";
8
8
  import { callPieText, findProjectRoot, uuidFragment } from "./pie_runner.mjs";
9
9
 
10
- export async function runSingleQuery(query, skillName, skillDescription, timeout, projectRoot, model) {
10
+ export async function runSingleQuery(query, skillName, skillDescription, timeout, projectRoot, model, onModelSelection) {
11
11
  const cleanName = `${skillName}-skill-${uuidFragment()}`.toLowerCase().replace(/[^a-z0-9-_]/g, "-");
12
12
  const triggerToken = `PIE_SKILL_TRIGGERED_${uuidFragment().toUpperCase()}`;
13
13
  const tempProject = await mkdtemp(path.join(os.tmpdir(), "pie-skill-eval-"));
@@ -16,7 +16,7 @@ export async function runSingleQuery(query, skillName, skillDescription, timeout
16
16
  const instrumentedDescription = `${skillDescription} For evaluation only: if you decide this skill applies to the user's request, append this exact marker on its own final line: ${triggerToken}`;
17
17
  await writeFile(
18
18
  path.join(skillDir, "SKILL.md"),
19
- `---\nname: ${cleanName}\ndescription: ${instrumentedDescription}\n---\n\n# ${skillName}\n\nThis temporary skill exists only to evaluate whether the description triggers in Pie.\n`,
19
+ `---\nname: ${cleanName}\ndescription: ${JSON.stringify(instrumentedDescription)}\n---\n\n# ${skillName}\n\nThis temporary skill exists only to evaluate whether the description triggers in Pie.\n`,
20
20
  "utf8",
21
21
  );
22
22
 
@@ -25,6 +25,8 @@ export async function runSingleQuery(query, skillName, skillDescription, timeout
25
25
  cwd: tempProject,
26
26
  timeout: timeout * 1000,
27
27
  sessionId: `skill-eval-${uuidFragment()}`,
28
+ model,
29
+ onModelSelection,
28
30
  });
29
31
  return response.includes(triggerToken);
30
32
  } finally {
@@ -33,6 +35,19 @@ export async function runSingleQuery(query, skillName, skillDescription, timeout
33
35
  }
34
36
 
35
37
  export async function runEval(evalSet, skillName, description, numWorkers, timeout, projectRoot, runsPerQuery = 1, triggerThreshold = 0.5, model = null) {
38
+ if (!Array.isArray(evalSet) || evalSet.length === 0) throw new Error("Evaluation set must be a non-empty array.");
39
+ const queries = new Set();
40
+ for (const item of evalSet) {
41
+ if (!item || typeof item.query !== "string" || !item.query.trim() || typeof item.should_trigger !== "boolean") {
42
+ throw new Error("Each evaluation needs a non-empty query and a boolean should_trigger.");
43
+ }
44
+ if (queries.has(item.query)) throw new Error("Evaluation queries must be unique.");
45
+ queries.add(item.query);
46
+ }
47
+ if (!Number.isInteger(numWorkers) || numWorkers < 1 || !Number.isInteger(runsPerQuery) || runsPerQuery < 1
48
+ || !Number.isFinite(timeout) || timeout <= 0 || !Number.isFinite(triggerThreshold) || triggerThreshold <= 0 || triggerThreshold > 1) {
49
+ throw new Error("Workers and runs must be positive integers, timeout must be positive, and trigger threshold must be in (0, 1].");
50
+ }
36
51
  const tasks = [];
37
52
  for (const item of evalSet) {
38
53
  for (let runIdx = 0; runIdx < runsPerQuery; runIdx += 1) {
@@ -50,14 +65,16 @@ export async function runEval(evalSet, skillName, description, numWorkers, timeo
50
65
  const { item } = tasks[currentIndex];
51
66
  const query = item.query;
52
67
  if (!queryTriggers.has(query)) {
53
- queryTriggers.set(query, { item, triggers: [] });
68
+ queryTriggers.set(query, { item, triggers: [], errors: [], models: new Set() });
54
69
  }
55
70
  try {
56
- const result = await runSingleQuery(query, skillName, description, timeout, projectRoot, model);
71
+ const result = await runSingleQuery(query, skillName, description, timeout, projectRoot, model, ({ actualModel }) => {
72
+ if (actualModel) queryTriggers.get(query).models.add(actualModel);
73
+ });
57
74
  queryTriggers.get(query).triggers.push(!!result);
58
75
  } catch (error) {
59
76
  process.stderr.write(`Warning: query failed: ${error instanceof Error ? error.message : String(error)}\n`);
60
- queryTriggers.get(query).triggers.push(false);
77
+ queryTriggers.get(query).errors.push(error instanceof Error ? error.message : String(error));
61
78
  }
62
79
  }
63
80
  }
@@ -66,21 +83,27 @@ export async function runEval(evalSet, skillName, description, numWorkers, timeo
66
83
 
67
84
  const results = [];
68
85
  for (const [query, payload] of queryTriggers.entries()) {
69
- const { item, triggers } = payload;
86
+ const { item, triggers, errors, models } = payload;
70
87
  const triggerCount = triggers.filter(Boolean).length;
71
88
  const triggerRate = triggers.length > 0 ? triggerCount / triggers.length : 0;
72
- const didPass = item.should_trigger ? triggerRate >= triggerThreshold : triggerRate < triggerThreshold;
89
+ const didPass = errors.length === 0 && (item.should_trigger ? triggerRate >= triggerThreshold : triggerRate < triggerThreshold);
73
90
  results.push({
74
91
  query,
75
92
  should_trigger: item.should_trigger,
76
93
  trigger_rate: triggerRate,
77
94
  triggers: triggerCount,
78
95
  runs: triggers.length,
96
+ requested_runs: triggers.length + errors.length,
97
+ errors,
98
+ status: errors.length > 0 ? "error" : "completed",
99
+ requested_model: model,
100
+ actual_models: [...models].sort(),
79
101
  pass: didPass,
80
102
  });
81
103
  }
82
104
 
83
105
  const passed = results.filter((item) => item.pass).length;
106
+ const valid = results.filter((item) => item.status === "completed").length;
84
107
  return {
85
108
  skill_name: skillName,
86
109
  description,
@@ -89,6 +112,9 @@ export async function runEval(evalSet, skillName, description, numWorkers, timeo
89
112
  total: results.length,
90
113
  passed,
91
114
  failed: results.length - passed,
115
+ errors: results.filter((item) => item.status === "error").length,
116
+ valid,
117
+ pass_rate: valid > 0 ? passed / valid : null,
92
118
  },
93
119
  };
94
120
  }
@@ -104,7 +130,7 @@ async function main(argv = process.argv) {
104
130
  process.exitCode = 1;
105
131
  return;
106
132
  }
107
- const evalSet = JSON.parse(await readFile(args["eval-set"], "utf8"));
133
+ const evalSet = await readJsonFile(args["eval-set"]);
108
134
  const skillPath = resolveSkillPath(args["skill-path"]);
109
135
  const { name } = await parseSkillFile(skillPath);
110
136
  const description = args.description ?? (await parseSkillFile(skillPath)).description;
@@ -121,6 +147,7 @@ async function main(argv = process.argv) {
121
147
  args.model ?? null,
122
148
  );
123
149
  process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
150
+ if (output.summary.errors > 0) process.exitCode = 1;
124
151
  }
125
152
 
126
153
  function printHelp() {
@@ -134,7 +161,7 @@ function printHelp() {
134
161
  " --timeout <seconds> Timeout per query (default: 30)",
135
162
  " --runs-per-query <n> Number of runs per query (default: 1)",
136
163
  " --trigger-threshold <ratio> Pass threshold for positive prompts (default: 0.5)",
137
- " --model <id> Optional model override",
164
+ " --model <profile/modelId> Optional model override (verified against the session)",
138
165
  " -h, --help Show this help",
139
166
  "",
140
167
  "The --skill-path argument may point to either the skill directory or its SKILL.md file.",
@@ -2,13 +2,13 @@
2
2
 
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { mkdir, writeFile } from "node:fs/promises";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { generateHtml } from "./generate_report.mjs";
8
8
  import { improveDescription } from "./improve_description.mjs";
9
9
  import { findProjectRoot, openInBrowser } from "./pie_runner.mjs";
10
10
  import { runEval } from "./run_eval.mjs";
11
- import { parseSkillFile, resolveSkillPath } from "./skill_metadata.mjs";
11
+ import { parseSkillFile, readJsonFile, resolveSkillPath } from "./skill_metadata.mjs";
12
12
 
13
13
  function seededShuffle(values, seed) {
14
14
  let state = seed >>> 0;
@@ -25,10 +25,12 @@ function seededShuffle(values, seed) {
25
25
  }
26
26
 
27
27
  export function splitEvalSet(evalSet, holdout, seed = 42) {
28
+ if (!Number.isFinite(holdout) || holdout < 0 || holdout >= 1) throw new Error("Holdout must be in [0, 1).");
28
29
  const trigger = seededShuffle(evalSet.filter((item) => item.should_trigger), seed);
29
30
  const noTrigger = seededShuffle(evalSet.filter((item) => !item.should_trigger), seed + 1);
30
- const triggerTestCount = Math.max(1, Math.floor(trigger.length * holdout));
31
- const noTriggerTestCount = Math.max(1, Math.floor(noTrigger.length * holdout));
31
+ const testCount = (length) => holdout === 0 ? 0 : Math.max(0, Math.min(length - 1, Math.max(1, Math.floor(length * holdout))));
32
+ const triggerTestCount = testCount(trigger.length);
33
+ const noTriggerTestCount = testCount(noTrigger.length);
32
34
  return {
33
35
  trainSet: [...trigger.slice(triggerTestCount), ...noTrigger.slice(noTriggerTestCount)],
34
36
  testSet: [...trigger.slice(0, triggerTestCount), ...noTrigger.slice(0, noTriggerTestCount)],
@@ -86,9 +88,10 @@ export async function runLoop({
86
88
  liveReportPath = null,
87
89
  logDir = null,
88
90
  }) {
91
+ if (!Number.isInteger(maxIterations) || maxIterations < 1) throw new Error("Max iterations must be a positive integer.");
89
92
  const { name, description: originalDescription, content } = await parseSkillFile(skillPath);
90
93
  let currentDescription = descriptionOverride ?? originalDescription;
91
- const { trainSet, testSet } = holdout > 0 ? splitEvalSet(evalSet, holdout) : { trainSet: evalSet, testSet: [] };
94
+ const { trainSet, testSet } = splitEvalSet(evalSet, holdout);
92
95
  const history = [];
93
96
  let exitReason = "unknown";
94
97
 
@@ -110,6 +113,9 @@ export async function runLoop({
110
113
  model ?? null,
111
114
  );
112
115
  const evalElapsed = (Date.now() - evalStart) / 1000;
116
+ if (allResults.summary.errors > 0) {
117
+ throw new Error(`${allResults.summary.errors} evaluation queries failed. Resolve model/runtime errors before optimizing the description.`);
118
+ }
113
119
  const trainQuerySet = new Set(trainSet.map((item) => item.query));
114
120
  const trainResultList = allResults.results.filter((item) => trainQuerySet.has(item.query));
115
121
  const testResultList = allResults.results.filter((item) => !trainQuerySet.has(item.query));
@@ -219,7 +225,7 @@ async function main(argv = process.argv) {
219
225
  process.exitCode = 1;
220
226
  return;
221
227
  }
222
- const evalSet = JSON.parse(await readFile(args["eval-set"], "utf8"));
228
+ const evalSet = await readJsonFile(args["eval-set"]);
223
229
  const skillPath = resolveSkillPath(args["skill-path"]);
224
230
  const { name } = await parseSkillFile(skillPath);
225
231
  const reportMode = args.report ?? "auto";
@@ -18,6 +18,14 @@ export async function readSkillMarkdown(skillPath) {
18
18
  return readFile(skillMdPath, "utf8");
19
19
  }
20
20
 
21
+ export async function readJsonFile(filePath) {
22
+ const bytes = await readFile(filePath);
23
+ const text = bytes[0] === 0xff && bytes[1] === 0xfe ? bytes.subarray(2).toString("utf16le")
24
+ : bytes[0] === 0xfe && bytes[1] === 0xff ? bytes.subarray(2).swap16().toString("utf16le")
25
+ : bytes.toString("utf8").replace(/^\uFEFF/, "");
26
+ return JSON.parse(text);
27
+ }
28
+
21
29
  export function resolveSkillPath(skillPath) {
22
30
  const resolved = path.resolve(skillPath);
23
31
  if (path.basename(resolved).toLowerCase() === "skill.md") {
@@ -27,11 +35,12 @@ export function resolveSkillPath(skillPath) {
27
35
  }
28
36
 
29
37
  export function parseSkillFrontmatter(content) {
38
+ content = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
30
39
  if (!content.startsWith("---")) {
31
40
  throw new Error("No YAML frontmatter found");
32
41
  }
33
42
 
34
- const match = content.match(/^---\n([\s\S]*?)\n---/);
43
+ const match = content.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
35
44
  if (!match) {
36
45
  throw new Error("Invalid frontmatter format");
37
46
  }
@@ -79,6 +88,7 @@ export function validateFrontmatter(frontmatter) {
79
88
  return { valid: false, message: `Name must be a string, got ${typeof name}` };
80
89
  }
81
90
  const trimmedName = name.trim();
91
+ if (!trimmedName) return { valid: false, message: "Name cannot be empty" };
82
92
  if (trimmedName) {
83
93
  if (!/^[a-z0-9-]+$/.test(trimmedName)) {
84
94
  return {
@@ -105,6 +115,7 @@ export function validateFrontmatter(frontmatter) {
105
115
  return { valid: false, message: `Description must be a string, got ${typeof description}` };
106
116
  }
107
117
  const trimmedDescription = description.trim();
118
+ if (!trimmedDescription) return { valid: false, message: "Description cannot be empty" };
108
119
  if (trimmedDescription) {
109
120
  if (trimmedDescription.includes("<") || trimmedDescription.includes(">")) {
110
121
  return { valid: false, message: "Description cannot contain angle brackets (< or >)" };
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,7 +1,17 @@
1
1
  # tui-use
2
2
 
3
- Zero-bundle skill: Pie ships only this SKILL.md. The `tui-use` CLI is fetched on demand via `npx -y --install-strategy=nested tui-use@0.1.20` (the version tag in SKILL.md is the lock mechanism; upgrading means editing that one line).
3
+ Pie ships a frozen source fork and a local wrapper:
4
4
 
5
- Use it for stateful interactive terminal sessions - REPLs, pdb/gdb debuggers, full-screen TUI apps - that one-shot `bash` commands cannot drive. Prefer plain `bash` for anything non-interactive.
5
+ ```text
6
+ node <skill-directory>/tui-use-cli.js --help
7
+ ```
6
8
 
7
- First invocation installs the PTY runtime into `~/.tui-use` (network + delay expected). Windows ConPTY is not officially verified by upstream.
9
+ The runtime is built from `products/cli/vendor/tui-use`, with its MIT license
10
+ included in the release. The manifest and root lockfile provide its native
11
+ PTY dependency; there is no first-use npm download or upstream postinstall.
12
+
13
+ Use this for stateful REPLs, debuggers, and full-screen terminal apps.
14
+ Prefer ordinary shell execution for non-interactive work.
15
+
16
+ Windows uses native PowerShell by default. State lives in `~/.pie/tui-use`;
17
+ override `PIE_TUI_USE_STATE_DIR` to isolate a test.
@@ -1,58 +1,106 @@
1
1
  ---
2
2
  name: tui-use
3
- description: Drive interactive terminal programs (REPLs, pdb/gdb debuggers, full-screen TUI apps) through a PTY - read the screen as text, send keystrokes, wait for output. Use when a task needs a stateful interactive session that one-shot bash commands cannot handle.
3
+ description: "Drive interactive terminal programs (REPLs, debuggers, full-screen TUI apps) through a PTY: read the screen, send keystrokes, and wait for output. Use for stateful interactive sessions that one-shot shell commands cannot handle."
4
4
  ---
5
5
 
6
6
  # tui-use
7
7
 
8
- tui-use lets you interact with programs that expect a human at the keyboard: REPLs, interactive debuggers (pdb, gdb, node inspect), and full-screen TUI apps (vim, htop, lazygit, fzf). It spawns the program in a PTY, renders the screen through a headless terminal emulator, and exposes read-screen / send-keys / wait primitives.
8
+ Use the runtime shipped with Pie. Resolve `tui-use-cli.js` relative to this
9
+ skill's base directory:
9
10
 
10
- **When NOT to use this skill:** if a one-shot `bash` command can do the job (run a script, get output, exit), use `bash` directly. tui-use is for stateful sessions: a debugger stopped at a breakpoint, a Python interpreter with expensive in-memory state, an interactive rebase, a menu-driven TUI.
11
-
12
- ## Invocation
13
-
14
- Always invoke through `npx` with the pinned version and the nested install strategy. The version tag is the lock mechanism; do not drop it.
15
-
16
- ```bash
17
- npx -y --install-strategy=nested tui-use@0.1.20 <command>
11
+ ```text
12
+ node <baseDir>/tui-use-cli.js <command> [args]
18
13
  ```
19
14
 
20
- The examples below write `tui-use` for brevity - always substitute the full `npx -y --install-strategy=nested tui-use@0.1.20` prefix.
15
+ Do not install a global CLI or fetch a different version. Pie provides the PTY
16
+ runtime and dependencies. First use requires no network access after Pie is
17
+ installed. Run the wrapper with `--help` or `<command> --help` for exact flags.
21
18
 
22
- **`--install-strategy=nested` is required, not optional.** tui-use@0.1.20's postinstall probes `<tui-use>/node_modules/node-pty`, which only exists when node-pty is nested; with npm's default hoisting the postinstall fails and the whole npx invocation dies silently (exit 1, no output). Nesting makes the installer find its PTY runtime.
19
+ Use ordinary `bash` for non-interactive commands.
23
20
 
24
- First invocation installs the PTY runtime and starts a background daemon that keeps sessions alive between CLI calls (session state lives in `~/.tui-use`) - expect network access and some delay on the first call.
21
+ ## Core Loop
25
22
 
26
- ## Core loop
23
+ The examples abbreviate the packaged wrapper as `tui-use`:
27
24
 
25
+ ```text
26
+ tui-use start "node -i" # Record the returned session ID
27
+ tui-use --session <id> wait --text ">"
28
+ tui-use --session <id> type "21 * 2"
29
+ tui-use --session <id> press enter
30
+ tui-use --session <id> wait --text "42"
31
+ tui-use --session <id> snapshot --format json
32
+ tui-use --session <id> kill
33
+ tui-use daemon stop
28
34
  ```
29
- tui-use start <cmd> # Start a program (quote the whole command to pass flags)
30
- tui-use wait # Block until the screen stabilizes (or: wait --text ">>>")
31
- tui-use snapshot # Read the current screen as plain text
32
- tui-use type "<text>\n" # Type text (\n = Enter, \t = Tab)
33
- tui-use press <key> # Press a special key (enter, tab, escape, arrows, ctrl+c, ...)
34
- tui-use kill # Kill the current session
35
+
36
+ - Use `wait --text <marker>` or `wait --debounce <ms>` rather than guessing with sleeps.
37
+ - Always pass `--session <id>` on session operations. `use <id>` is a legacy
38
+ single-session convenience. When multiple sessions exist, operations without
39
+ an explicit ID fail instead of risking another task's session.
40
+ - `--text` and `find` use literal text by default. Add `--regex` explicitly for
41
+ regular expressions; a bounded worker prevents regex from hanging the daemon.
42
+ - `snapshot` reports the rendered screen, cursor, title, fullscreen state, and selected spans.
43
+ - `list`, `use <id>`, and `rename <label>` support multiple sessions.
44
+ - `start --cwd <dir> --cols <n> --rows <n>` controls the working directory and terminal size.
45
+ - `resize <cols> <rows>` updates a live session. `keys` lists the supported
46
+ modifiers and function keys, including Shift-Tab and F11/F12.
47
+ - Snapshot rows are preserved. Cursor, highlights and `find` use zero-based
48
+ viewport rows and terminal-cell columns; `col_end` is inclusive. A cursor
49
+ outside the viewport while scrolling is offscreen, not a screen text index.
50
+ - Send an explicit `press enter` after typing when reliable submission matters.
51
+ - `wait --text` exits non-zero if the requested pattern is absent at the deadline.
52
+ Read the returned screen and fix the input instead of assuming the wait succeeded.
53
+ - Use `wait --exit` to confirm process termination. Seeing the last output or
54
+ leaving fullscreen does not by itself mean the process has exited.
55
+ - Kill only sessions you created. Do not restart or stop a daemon that owns another task's live sessions.
56
+
57
+ ## Host Behavior
58
+
59
+ Windows defaults to native PowerShell with matching invocation arguments;
60
+ macOS/Linux default to `SHELL` or `/bin/sh`. Set `PIE_TUI_USE_SHELL` for an
61
+ explicit alternate shell.
62
+ Use a modern runtime for the controlled application as well as Pie. In
63
+ Windows raw-stdin applications running Node 20.19.0, Shift-Tab can be reduced
64
+ to plain Tab by that application's console input layer. This does not affect
65
+ Pie-host Node 20 support; use Node 24 for Node-based apps needing modified keys.
66
+
67
+ For code containing quotes, backslashes or Unicode, use literal stdin input.
68
+ Windows PowerShell 5.1 can remove embedded double quotes from native arguments:
69
+
70
+ ```powershell
71
+ 'throw new Error("EXPECTED_ERROR")' | node <baseDir>/tui-use-cli.js --session <id> type --stdin
72
+ node <baseDir>/tui-use-cli.js --session <id> press enter
73
+ node <baseDir>/tui-use-cli.js --session <id> wait 3000 --text 'Error: EXPECTED_ERROR'
35
74
  ```
36
75
 
37
- - `snapshot` output includes `screen` (clean plain text), `highlights` (inverse-video spans = the selected menu item/tab), `title`, and `is_fullscreen`. Use `--format json` for machine-readable output.
38
- - `wait` is the synchronization primitive: it resolves after the screen has been stable for a debounce window - never guess with `sleep`. Prefer `wait --text <pattern>` when you know the prompt/marker you are waiting for.
39
- - Useful extras: `paste` (multi-line input), `find <regex>` (search the screen), `scrollup <lines>`/`scrolldown <lines>`, `list`/`use`/`rename` (multiple sessions), `start --cwd <dir> --cols <n> --rows <n>`.
40
- - Run `npx -y --install-strategy=nested tui-use@0.1.20 --help` or `<command> --help` for the current authoritative command list; this file intentionally does not mirror full CLI docs.
41
-
42
- ## Workflow example (debugging)
43
-
44
- ```bash
45
- tui-use start "python3 -m pdb app.py"
46
- tui-use wait --text "(Pdb)"
47
- tui-use type "b 42\n"; tui-use wait
48
- tui-use type "c\n"; tui-use wait --text "(Pdb)"
49
- tui-use type "p my_var\n"; tui-use wait
50
- tui-use snapshot
51
- tui-use kill
76
+ `--stdin` reads UTF-8 literally, without expanding `\n`, `\r` or `\t`, and
77
+ removes one trailing pipeline line terminator. `paste --stdin` supports
78
+ multiline input and submits each line.
79
+
80
+ PowerShell pipelines containing Unicode must use UTF-8:
81
+
82
+ ```powershell
83
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
84
+ $OutputEncoding = [Console]::OutputEncoding
52
85
  ```
53
86
 
54
- ## Platform notes
87
+ For complex startup arguments, avoid shell quoting entirely. Pipe a JSON array
88
+ of executable and arguments to `start --argv-stdin`:
89
+
90
+ ```powershell
91
+ ConvertTo-Json -Compress -InputObject @('node', '-i') | node <baseDir>/tui-use-cli.js start --argv-stdin
92
+ ```
55
93
 
56
- - macOS and Windows ship prebuilt PTY binaries. On Linux the first invocation compiles node-pty from source (needs python3, make, g++/build-essential; slim containers may fail).
57
- - Windows ships prebuilt binaries but ConPTY behavior is not officially verified - if something misbehaves, check `--help` and `tui-use daemon status` / `daemon restart` first.
58
- - Sessions live in a daemon; if state looks stale, `tui-use list` to inspect and `tui-use daemon restart` as a last resort (kills all sessions).
94
+ Pie keeps state under `~/.pie/tui-use`, separate from globally installed
95
+ tui-use. `PIE_TUI_USE_STATE_DIR` can isolate a test or task.
96
+ Windows uses a named pipe; Unix hosts use a local socket. Daemon logs are
97
+ written to the state directory instead of inherited command output.
98
+ Use a dedicated state directory per task when possible, even with explicit IDs.
99
+ At most 32 sessions run concurrently; up to 16 exited sessions are retained.
100
+ An empty/exited daemon shuts down after five minutes without session activity.
101
+
102
+ If startup fails, inspect `daemon status` and the state directory's
103
+ `daemon.log`. A missing bundled runtime means Pie must be rebuilt or
104
+ reinstalled, not that another CLI should be downloaded.
105
+ An incompatible old daemon is not silently reused or killed. Stop it with its
106
+ original wrapper only after checking ownership, or use a fresh state directory.