@ian-pascoe/pi-termctrl 0.2.0 → 0.3.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/README.md +4 -0
- package/package.json +3 -3
- package/src/bash-replacement.ts +4 -2
- package/src/terminal-tools.ts +56 -6
package/README.md
CHANGED
|
@@ -97,6 +97,10 @@ for the agent, such as the user steering or another entry's Exit notification;
|
|
|
97
97
|
a watched entry that exits after that is notified as usual. With nothing to
|
|
98
98
|
wait for it returns at once. Terminal exits are noticed within about 500 ms.
|
|
99
99
|
|
|
100
|
+
### Tool annotations
|
|
101
|
+
|
|
102
|
+
Each terminal tool declares MCP-style `annotations`, which Pi reports through `pi.getAllTools()` for permission extensions and does not send to model providers. `terminal_start` and `terminal_send` run arbitrary programs, so they are marked destructive and open-world; neither is idempotent. `terminal_stop` kills processes: destructive and idempotent, closed-world. `terminal_list` and `terminal_wait` are read-only and closed-world. The `bash` replacement keeps the annotations of Pi's built-in `bash`, which declares none.
|
|
103
|
+
|
|
100
104
|
## `bash` replacement
|
|
101
105
|
|
|
102
106
|
With `termctrl.replaceBash` (default `true`), Pi Termctrl replaces Pi's `bash`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-termctrl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Interactive Terminals, Background jobs, and a /ps panel for Pi.",
|
|
6
6
|
"keywords": [
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"@kitlangton/terminal-control": "^1.2.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@earendil-works/pi-coding-agent": "
|
|
41
|
-
"@earendil-works/pi-tui": "
|
|
40
|
+
"@earendil-works/pi-coding-agent": ">=0.99.0",
|
|
41
|
+
"@earendil-works/pi-tui": ">=0.99.0",
|
|
42
42
|
"typebox": "*"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
package/src/bash-replacement.ts
CHANGED
|
@@ -248,10 +248,12 @@ export interface BashReplacementOptions {
|
|
|
248
248
|
readonly definitionFactory?: typeof createBashToolDefinition;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/** Pi's `bash` declares an object `outputSchema` since 0.99.0; extend it with the background fields. */
|
|
251
252
|
function backgroundOutputSchema(builtin: TSchema | undefined) {
|
|
252
|
-
|
|
253
|
+
if (builtin === undefined || !Type.IsObject(builtin))
|
|
254
|
+
throw new Error("Pi's bash tool does not declare an object outputSchema");
|
|
253
255
|
return Type.Object({
|
|
254
|
-
...properties,
|
|
256
|
+
...builtin.properties,
|
|
255
257
|
exit_code: Type.Optional(
|
|
256
258
|
Type.Number({ description: "Absent only when the command was moved to the background" }),
|
|
257
259
|
),
|
package/src/terminal-tools.ts
CHANGED
|
@@ -369,12 +369,32 @@ function locateCursor(
|
|
|
369
369
|
return undefined;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Find where the result's screen starts in termctrl's log. The screen is read before the log, in a
|
|
374
|
+
* separate termctrl request, so output in between can already have pushed the screen's top lines
|
|
375
|
+
* above the log's last screenful. Output only adds lines to the end of the log, so the screen is at
|
|
376
|
+
* the end or higher up, no higher than the log cursor. The screen is matched by every line except
|
|
377
|
+
* its last, which the program may still be writing. When nothing matches, as when the screen was
|
|
378
|
+
* redrawn in between, the log's last screenful is taken as the screen.
|
|
379
|
+
*/
|
|
380
|
+
function locateScreen(lines: readonly string[], screen: readonly string[], floor: number): number {
|
|
381
|
+
const end = Math.max(0, lines.length - screen.length);
|
|
382
|
+
for (let start = end; start >= floor; start--) {
|
|
383
|
+
let matches = true;
|
|
384
|
+
for (let row = 0; row < screen.length - 1 && matches; row++) {
|
|
385
|
+
matches = lines[start + row] === screen[row];
|
|
386
|
+
}
|
|
387
|
+
if (matches) return start;
|
|
388
|
+
}
|
|
389
|
+
return end;
|
|
390
|
+
}
|
|
391
|
+
|
|
372
392
|
/**
|
|
373
393
|
* Lines that left the screen since the previous result, advancing the Terminal's log cursor.
|
|
374
|
-
* Lines still on the screen stay unread, so a line rewritten after the agent saw it, such
|
|
375
|
-
* progress line, is reported in its final form once it scrolls off. When the cursor cannot be
|
|
376
|
-
* again, because termctrl dropped lines the agent never received or the screen was cleared,
|
|
377
|
-
* line termctrl still holds is reported and the result says that earlier output is missing.
|
|
394
|
+
* Lines still on the result's screen stay unread, so a line rewritten after the agent saw it, such
|
|
395
|
+
* as a progress line, is reported in its final form once it scrolls off. When the cursor cannot be
|
|
396
|
+
* found again, because termctrl dropped lines the agent never received or the screen was cleared,
|
|
397
|
+
* every line termctrl still holds is reported and the result says that earlier output is missing.
|
|
378
398
|
*/
|
|
379
399
|
async function takeScrolledOff(entry: TerminalEntry, screen: string): Promise<ScrolledOff> {
|
|
380
400
|
let logs: string;
|
|
@@ -385,8 +405,8 @@ async function takeScrolledOff(entry: TerminalEntry, screen: string): Promise<Sc
|
|
|
385
405
|
}
|
|
386
406
|
const lines = logs === "" ? [] : logs.split("\n");
|
|
387
407
|
const located = locateCursor(lines, entry.logCursor, entry.logAnchor);
|
|
388
|
-
const screenLines = screen === "" ?
|
|
389
|
-
const boundary =
|
|
408
|
+
const screenLines = screen === "" ? [] : screen.split("\n");
|
|
409
|
+
const boundary = locateScreen(lines, screenLines, located ?? 0);
|
|
390
410
|
const scrolled = lines.slice(Math.min(located ?? 0, boundary), boundary);
|
|
391
411
|
entry.logCursor = boundary;
|
|
392
412
|
entry.logAnchor =
|
|
@@ -585,6 +605,12 @@ export function createTerminalStartTool(runtime: TerminalToolRuntime) {
|
|
|
585
605
|
"Terminals keep limited scrollback, so long output can be lost; run commands whose full output you need with bash, or redirect their output to a file.",
|
|
586
606
|
],
|
|
587
607
|
parameters: StartParameters,
|
|
608
|
+
annotations: {
|
|
609
|
+
readOnlyHint: false,
|
|
610
|
+
destructiveHint: true,
|
|
611
|
+
idempotentHint: false,
|
|
612
|
+
openWorldHint: true,
|
|
613
|
+
},
|
|
588
614
|
outputSchema: TerminalResultSchema,
|
|
589
615
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
590
616
|
const entry = await runtime.registry.startTerminal(ownerOf(context), {
|
|
@@ -620,6 +646,12 @@ export function createTerminalSendTool(runtime: TerminalToolRuntime) {
|
|
|
620
646
|
"Type text and press keys in a Terminal, then return its screen once it settles: 250 ms of quiet, a wait_for_text match, exit, or wait_ms (default 500). With neither text nor keys it polls: it waits up to wait_ms (default 30000) for new output. Results include the lines that scrolled off since your previous result; a truncated result names a file with its full output.",
|
|
621
647
|
promptSnippet: "Send input to a Terminal, or poll it, and read its screen",
|
|
622
648
|
parameters: SendParameters,
|
|
649
|
+
annotations: {
|
|
650
|
+
readOnlyHint: false,
|
|
651
|
+
destructiveHint: true,
|
|
652
|
+
idempotentHint: false,
|
|
653
|
+
openWorldHint: true,
|
|
654
|
+
},
|
|
623
655
|
outputSchema: TerminalResultSchema,
|
|
624
656
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
625
657
|
const entry = runtime.registry.find(ownerOf(context), params.id);
|
|
@@ -691,6 +723,12 @@ export function createTerminalStopTool(registry: TermctrlRegistry) {
|
|
|
691
723
|
"Stop a Terminal (t1) or Background job (b1) and forget it. Running processes are killed; exited ones are removed. Returns a Terminal's final screen and scrolled-off lines, or a Background job's recent output.",
|
|
692
724
|
promptSnippet: "Stop a Terminal or Background job",
|
|
693
725
|
parameters: StopParameters,
|
|
726
|
+
annotations: {
|
|
727
|
+
readOnlyHint: false,
|
|
728
|
+
destructiveHint: true,
|
|
729
|
+
idempotentHint: true,
|
|
730
|
+
openWorldHint: false,
|
|
731
|
+
},
|
|
694
732
|
outputSchema: StopResultSchema,
|
|
695
733
|
async execute(_toolCallId, params, _signal, _onUpdate, context) {
|
|
696
734
|
const owner = ownerOf(context);
|
|
@@ -832,6 +870,12 @@ export function createTerminalWaitTool(runtime: TerminalWaitRuntime) {
|
|
|
832
870
|
"Do not poll a Background job's log or a Terminal in a loop to learn when it finishes: keep working and rely on its Exit notification, or call terminal_wait when nothing else can proceed.",
|
|
833
871
|
],
|
|
834
872
|
parameters: WaitParameters,
|
|
873
|
+
annotations: {
|
|
874
|
+
readOnlyHint: true,
|
|
875
|
+
destructiveHint: false,
|
|
876
|
+
idempotentHint: true,
|
|
877
|
+
openWorldHint: false,
|
|
878
|
+
},
|
|
835
879
|
outputSchema: WaitResultSchema,
|
|
836
880
|
async execute(_toolCallId, params, signal, _onUpdate, context) {
|
|
837
881
|
const owner = ownerOf(context);
|
|
@@ -898,6 +942,12 @@ export function createTerminalListTool(registry: TermctrlRegistry) {
|
|
|
898
942
|
"List your Terminals and Background jobs, running and exited, with their state. Background jobs include their log path.",
|
|
899
943
|
promptSnippet: "List Terminals and Background jobs",
|
|
900
944
|
parameters: ListParameters,
|
|
945
|
+
annotations: {
|
|
946
|
+
readOnlyHint: true,
|
|
947
|
+
destructiveHint: false,
|
|
948
|
+
idempotentHint: true,
|
|
949
|
+
openWorldHint: false,
|
|
950
|
+
},
|
|
901
951
|
outputSchema: ListResultSchema,
|
|
902
952
|
async execute(_toolCallId, _params, _signal, _onUpdate, context) {
|
|
903
953
|
const now = Date.now();
|