@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.
- package/README.md +6 -3
- package/dist/builtin/extensions/ask-user/index.js +1 -1
- package/dist/builtin/extensions/computer-use/index.js +3993 -107
- package/dist/builtin/extensions/plan-mode/index.js +1 -1
- package/dist/builtin/extensions/subagent/index.js +2 -2
- package/dist/builtin/extensions/todo/index.js +1 -1
- package/dist/builtin/skills/chrome-devtools-axi/LICENSE +21 -0
- package/dist/builtin/skills/chrome-devtools-axi/SKILL.md +20 -1
- package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-bridge.js +20034 -0
- package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-cli.js +8 -2
- package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-runtime.js +26 -0
- package/dist/builtin/skills/chrome-devtools-axi/chunk-42SGITKJ.js +11 -0
- package/dist/builtin/skills/chrome-devtools-axi/chunk-OJ6N23UZ.js +47 -0
- package/dist/builtin/skills/chrome-devtools-axi/chunk-UPATXALW.js +215 -0
- package/dist/builtin/skills/chrome-devtools-axi/cli-F2VCSVL4.js +4424 -0
- package/dist/builtin/skills/skill-creator/SKILL.md +5 -2
- package/dist/builtin/skills/skill-creator/eval-viewer/generate_review.mjs +7 -20
- package/dist/builtin/skills/skill-creator/eval-viewer/viewer.html +15 -15
- package/dist/builtin/skills/skill-creator/scripts/aggregate_benchmark.mjs +20 -19
- package/dist/builtin/skills/skill-creator/scripts/generate_report.mjs +3 -3
- package/dist/builtin/skills/skill-creator/scripts/improve_description.mjs +12 -8
- package/dist/builtin/skills/skill-creator/scripts/package_skill.mjs +1 -0
- package/dist/builtin/skills/skill-creator/scripts/pie_runner.mjs +77 -31
- package/dist/builtin/skills/skill-creator/scripts/run_eval.mjs +38 -11
- package/dist/builtin/skills/skill-creator/scripts/run_loop.mjs +12 -6
- package/dist/builtin/skills/skill-creator/scripts/skill_metadata.mjs +12 -1
- package/dist/builtin/skills/tui-use/LICENSE +21 -0
- package/dist/builtin/skills/tui-use/README.md +13 -3
- package/dist/builtin/skills/tui-use/SKILL.md +87 -39
- package/dist/builtin/skills/tui-use/tui-use-cli.cjs +10608 -0
- package/dist/builtin/skills/tui-use/tui-use-cli.js +39 -0
- package/dist/builtin/skills/tui-use/tui-use-daemon.cjs +7916 -0
- package/dist/chunks/{chunk-PMPD56N2.js → chunk-LU7RKALH.js} +82 -24
- package/dist/chunks/{chunk-G2POWIA5.js → chunk-TJ2F4FL5.js} +64 -16
- package/dist/cli.js +7503 -87
- package/package.json +26 -18
- package/scripts/repair-node-pty.mjs +45 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { mkdir, mkdtemp,
|
|
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).
|
|
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 =
|
|
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 <
|
|
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,
|
|
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
|
|
31
|
-
const
|
|
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 } =
|
|
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 =
|
|
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
|
-
|
|
3
|
+
Pie ships a frozen source fork and a local wrapper:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```text
|
|
6
|
+
node <skill-directory>/tui-use-cli.js --help
|
|
7
|
+
```
|
|
6
8
|
|
|
7
|
-
|
|
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,
|
|
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
|
-
|
|
8
|
+
Use the runtime shipped with Pie. Resolve `tui-use-cli.js` relative to this
|
|
9
|
+
skill's base directory:
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
Use ordinary `bash` for non-interactive commands.
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
## Core Loop
|
|
25
22
|
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
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.
|