@itookit/dsht 0.3.7 → 0.5.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.i18n.yaml +2 -2
- package/README.md +31 -11
- package/README.zh.md +31 -11
- package/dist/cli/dsht.js +207 -19
- package/dist/cli/startup.d.ts +40 -0
- package/dist/cli/startup.js +295 -0
- package/dist/cli/trace-summary.d.ts +78 -0
- package/dist/cli/trace-summary.js +241 -0
- package/dist/cli/verifier.d.ts +60 -0
- package/dist/cli/verifier.js +242 -0
- package/dist/contracts.d.ts +344 -0
- package/dist/contracts.js +1 -0
- package/dist/controller/commands.d.ts +47 -0
- package/dist/controller/commands.js +322 -0
- package/dist/controller/connection.d.ts +11 -29
- package/dist/controller/connection.js +26 -60
- package/dist/controller/controller.d.ts +619 -164
- package/dist/controller/controller.js +1420 -141
- package/dist/controller/index.d.ts +8 -1
- package/dist/controller/index.js +5 -0
- package/dist/controller/loop-contract.d.ts +136 -0
- package/dist/controller/loop-contract.js +308 -0
- package/dist/controller/loop-prompts-schema.d.ts +56 -0
- package/dist/controller/loop-prompts-schema.js +144 -0
- package/dist/controller/loop-prompts.d.ts +55 -0
- package/dist/controller/loop-prompts.generated.d.ts +104 -0
- package/dist/controller/loop-prompts.generated.js +185 -0
- package/dist/controller/loop-prompts.js +104 -0
- package/dist/controller/loop-protocols.d.ts +39 -0
- package/dist/controller/loop-protocols.js +115 -0
- package/dist/controller/loop.d.ts +275 -0
- package/dist/controller/loop.js +378 -0
- package/dist/controller/prompts.d.ts +54 -0
- package/dist/controller/prompts.js +162 -0
- package/dist/controller/trace-log.d.ts +45 -0
- package/dist/controller/trace-log.js +144 -0
- package/dist/controller/verifier.d.ts +126 -0
- package/dist/controller/verifier.js +75 -0
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger.d.ts +0 -1
- package/dist/cost/ledger.js +0 -1
- package/dist/json.d.ts +18 -0
- package/dist/json.js +19 -0
- package/dist/references.d.ts +25 -0
- package/dist/references.js +26 -0
- package/dist/session/connection-view.d.ts +2 -11
- package/dist/session/controller.d.ts +82 -72
- package/dist/session/controller.js +211 -209
- package/dist/session/history.d.ts +9 -1
- package/dist/session/history.js +1 -9
- package/dist/session/index.d.ts +9 -4
- package/dist/session/index.js +7 -3
- package/dist/session/info.d.ts +25 -52
- package/dist/session/info.js +39 -25
- package/dist/session/markdown.js +1 -1
- package/dist/session/math.js +1 -1
- package/dist/session/mutation-gate.d.ts +51 -0
- package/dist/session/mutation-gate.js +73 -0
- package/dist/session/navigation.d.ts +2 -89
- package/dist/session/navigation.js +2 -129
- package/dist/session/peek.d.ts +38 -0
- package/dist/session/peek.js +103 -0
- package/dist/session/references.d.ts +2 -20
- package/dist/session/references.js +1 -26
- package/dist/session/runtime.d.ts +26 -0
- package/dist/session/runtime.js +28 -0
- package/dist/session/telemetry.d.ts +12 -13
- package/dist/session/telemetry.js +27 -58
- package/dist/session/transcript.d.ts +0 -6
- package/dist/session/transcript.js +2 -15
- package/dist/session/types.d.ts +25 -0
- package/dist/session/types.js +0 -1
- package/dist/session-title.d.ts +9 -0
- package/dist/session-title.js +21 -0
- package/dist/shell/controller.d.ts +97 -0
- package/dist/shell/controller.js +158 -0
- package/dist/shell/index.d.ts +5 -0
- package/dist/shell/index.js +3 -0
- package/dist/shell/runner.d.ts +38 -0
- package/dist/shell/runner.js +147 -0
- package/dist/slash/index.d.ts +10 -0
- package/dist/slash/index.js +7 -0
- package/dist/slash/parse.d.ts +166 -0
- package/dist/slash/parse.js +259 -0
- package/dist/slash/pipeline.d.ts +140 -0
- package/dist/slash/pipeline.js +115 -0
- package/dist/slash/registry.d.ts +88 -0
- package/dist/slash/registry.js +177 -0
- package/dist/state.d.ts +14 -4
- package/dist/state.js +3 -2
- package/dist/text.d.ts +28 -0
- package/dist/text.js +55 -0
- package/dist/transport/events.d.ts +104 -0
- package/dist/transport/events.js +149 -0
- package/dist/transport/wire.d.ts +9 -17
- package/dist/transport/wire.js +2 -27
- package/dist/ui/app.js +865 -431
- package/dist/ui/chat/header.js +1 -1
- package/dist/ui/chat/history-view.d.ts +1 -1
- package/dist/ui/chat/history-view.js +1 -1
- package/dist/ui/chat/loop-status.d.ts +11 -0
- package/dist/ui/chat/loop-status.js +28 -0
- package/dist/ui/chat/navigation-model.d.ts +86 -0
- package/dist/ui/chat/navigation-model.js +107 -0
- package/dist/ui/chat/shell-view.d.ts +47 -0
- package/dist/ui/chat/shell-view.js +145 -0
- package/dist/ui/chat/status.d.ts +47 -3
- package/dist/ui/chat/status.js +65 -50
- package/dist/ui/chat/viewport.d.ts +1 -1
- package/dist/ui/dialogs/cost.d.ts +21 -4
- package/dist/ui/dialogs/cost.js +7 -12
- package/dist/ui/dialogs/index.d.ts +22 -5
- package/dist/ui/dialogs/index.js +19 -3
- package/dist/ui/dialogs/loop.d.ts +43 -0
- package/dist/ui/dialogs/loop.js +224 -0
- package/dist/ui/dialogs/peek.d.ts +25 -0
- package/dist/ui/dialogs/peek.js +35 -0
- package/dist/ui/dialogs/picker.d.ts +2 -0
- package/dist/ui/dialogs/picker.js +4 -2
- package/dist/ui/input/mouse.d.ts +12 -2
- package/dist/ui/input/mouse.js +20 -7
- package/dist/ui/input/references.d.ts +1 -1
- package/dist/ui/status/model.d.ts +7 -0
- package/dist/ui/status/model.js +5 -0
- package/dist/ui/theme/index.d.ts +6 -1
- package/dist/ui/theme/index.js +2 -1
- package/package.json +6 -4
- package/dist/ui/commands/parse.d.ts +0 -99
- package/dist/ui/commands/parse.js +0 -126
- package/dist/ui/commands/registry.d.ts +0 -33
- package/dist/ui/commands/registry.js +0 -73
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Read a session summary's display title.
|
|
2
|
+
*
|
|
3
|
+
* Host rows carry their title inside a projection, and both the domain (matching a target by name)
|
|
4
|
+
* and the UI (labelling a row) need the same reading, so it lives in its own leaf rather than in
|
|
5
|
+
* either layer.
|
|
6
|
+
*/
|
|
7
|
+
import { type ObjectValue } from './json.ts';
|
|
8
|
+
/** Resolve the host's title projection, falling back to the session ID. */
|
|
9
|
+
export declare function sessionLabel(session: ObjectValue): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Read a session summary's display title.
|
|
2
|
+
*
|
|
3
|
+
* Host rows carry their title inside a projection, and both the domain (matching a target by name)
|
|
4
|
+
* and the UI (labelling a row) need the same reading, so it lives in its own leaf rather than in
|
|
5
|
+
* either layer.
|
|
6
|
+
*/
|
|
7
|
+
import { string } from "./json.js";
|
|
8
|
+
import { safeText } from "./text.js";
|
|
9
|
+
/** Resolve the host's title projection, falling back to the session ID. */
|
|
10
|
+
export function sessionLabel(session) {
|
|
11
|
+
const projections = session.projections;
|
|
12
|
+
if (projections && typeof projections === 'object' && !Array.isArray(projections)) {
|
|
13
|
+
const values = projections.values;
|
|
14
|
+
const title = values && typeof values === 'object' && !Array.isArray(values) ? values.title : undefined;
|
|
15
|
+
if (typeof title === 'string' && safeText(title).trim())
|
|
16
|
+
return safeText(title).trim();
|
|
17
|
+
if (title && typeof title === 'object' && !Array.isArray(title) && typeof title.title === 'string' && safeText(title.title).trim())
|
|
18
|
+
return safeText(title.title).trim();
|
|
19
|
+
}
|
|
20
|
+
return string(session.sessionId);
|
|
21
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Readable id of one `!` run's own output, so its transcript bar and its source entry agree. */
|
|
2
|
+
export declare function localSourceId(id: number): string;
|
|
3
|
+
/** One local command and the output retained for it.
|
|
4
|
+
*
|
|
5
|
+
* A block is either a `!` process (`shell`) or a client command echoed so its bar can be read and
|
|
6
|
+
* clicked (`note`). A note runs nothing; it carries the readable source its bar opens, which the
|
|
7
|
+
* application links once the thing it announced exists.
|
|
8
|
+
*/
|
|
9
|
+
export interface ShellBlock {
|
|
10
|
+
id: number;
|
|
11
|
+
/** What produced the block: a local process, or a command echoed for reading. */
|
|
12
|
+
kind: 'shell' | 'note';
|
|
13
|
+
/** Readable source this block's bar opens: its own lines, or the session a note announces. */
|
|
14
|
+
source?: string;
|
|
15
|
+
command: string;
|
|
16
|
+
/** Retained output lines, oldest first. */
|
|
17
|
+
lines: string[];
|
|
18
|
+
/** Lines dropped from the front because the block exceeded its budget. */
|
|
19
|
+
dropped: number;
|
|
20
|
+
/** Durable sequence that was newest when the command started; the block is shown after it. */
|
|
21
|
+
anchor: number;
|
|
22
|
+
status: 'running' | 'exited';
|
|
23
|
+
/** Exit code, once the command ended; null when a signal ended it. */
|
|
24
|
+
code?: number | null;
|
|
25
|
+
signal?: string | null;
|
|
26
|
+
startedAt: number;
|
|
27
|
+
endedAt?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Plain read-only view of the local `!` runs, so the UI never reads the service object. */
|
|
30
|
+
export interface ShellSnapshot {
|
|
31
|
+
running: boolean;
|
|
32
|
+
blocks: readonly ShellBlock[];
|
|
33
|
+
}
|
|
34
|
+
/** What one shell controller needs from its owner. */
|
|
35
|
+
export interface ShellHost {
|
|
36
|
+
/** Repaint after output or a status change. */
|
|
37
|
+
publish(): void;
|
|
38
|
+
/** Client working directory the command runs in. */
|
|
39
|
+
cwd(): string;
|
|
40
|
+
/** Environment for the child, already stripped of client credentials. */
|
|
41
|
+
env(): NodeJS.ProcessEnv;
|
|
42
|
+
/** Newest durable sequence of the selected session, so a block stays where it happened. */
|
|
43
|
+
anchor(): number;
|
|
44
|
+
}
|
|
45
|
+
/** Owns the `!` commands of this client process: one at a time, bounded, killable. */
|
|
46
|
+
export declare class ShellController {
|
|
47
|
+
private readonly host;
|
|
48
|
+
readonly enabled: boolean;
|
|
49
|
+
private readonly blocks;
|
|
50
|
+
private readonly bytes;
|
|
51
|
+
private nextId;
|
|
52
|
+
private task;
|
|
53
|
+
private abort;
|
|
54
|
+
private lastPublish;
|
|
55
|
+
constructor(host: ShellHost, /** Whether `!` is allowed at all. */ enabled?: boolean);
|
|
56
|
+
/** Runs in creation order, oldest first; the newest is what the transcript shows at its end. */
|
|
57
|
+
get runs(): readonly ShellBlock[];
|
|
58
|
+
/** Whether a command is still running. */
|
|
59
|
+
get running(): boolean;
|
|
60
|
+
/** The plain snapshot `AppState.shell` publishes; blocks stay owned here. */
|
|
61
|
+
snapshot(): ShellSnapshot;
|
|
62
|
+
/** Start one command.
|
|
63
|
+
*
|
|
64
|
+
* One command runs at a time: a second `!` while the first is live is refused rather than queued,
|
|
65
|
+
* because the transcript shows a single result block and the reader can stop the first with Ctrl+C.
|
|
66
|
+
* @param command - Command line typed after `!`.
|
|
67
|
+
* @returns The block created for it.
|
|
68
|
+
*/
|
|
69
|
+
start(command: string): ShellBlock;
|
|
70
|
+
/** Stop the running command: its process group gets SIGTERM, then SIGKILL after a grace period.
|
|
71
|
+
* @returns Whether a run was stopped.
|
|
72
|
+
*/
|
|
73
|
+
cancel(): boolean;
|
|
74
|
+
/** Echo one client command as a local block, so the transcript holds a bar for it.
|
|
75
|
+
*
|
|
76
|
+
* Nothing runs and nothing is retained: the bar exists to be read and clicked, and the application
|
|
77
|
+
* links it to a readable source afterwards (`link`). A command that only fills a composer frame
|
|
78
|
+
* never reaches here, so the transcript does not collect bars for forms that were never run.
|
|
79
|
+
* @param command - The command line as the operator submitted it.
|
|
80
|
+
* @param source - Readable source the bar opens, when one already exists; `link` adds a later one.
|
|
81
|
+
* @returns The block created for it.
|
|
82
|
+
*/
|
|
83
|
+
note(command: string, source?: string): ShellBlock;
|
|
84
|
+
/** Point the newest note at a readable source, so its bar opens what the run is doing now. */
|
|
85
|
+
link(source: string): void;
|
|
86
|
+
/** One run's retained output, with dropped lines made explicit.
|
|
87
|
+
* @param id - Run to read; defaults to the newest.
|
|
88
|
+
* @returns The text, or undefined when the run is unknown.
|
|
89
|
+
*/
|
|
90
|
+
output(id?: number): string | undefined;
|
|
91
|
+
/** Stop the running command and wait for it, so no child outlives this client. */
|
|
92
|
+
stop(): Promise<void>;
|
|
93
|
+
/** Append one output line, trimming the block to its budgets from the front. */
|
|
94
|
+
private append;
|
|
95
|
+
/** Repaint, at most once per interval unless the change is a status change. */
|
|
96
|
+
private publish;
|
|
97
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Local shell runs the reader started with `!`, kept as bounded blocks for inline display.
|
|
2
|
+
*
|
|
3
|
+
* A block is the command line plus its retained output. Output is capped by both lines and bytes, so
|
|
4
|
+
* a runaway command can only fill its block, and only the newest blocks are kept. Nothing here is
|
|
5
|
+
* durable: no host record, no session state, no file.
|
|
6
|
+
*/
|
|
7
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
+
import { runShell } from "./runner.js";
|
|
9
|
+
/** Output lines one block keeps before it drops the oldest. */
|
|
10
|
+
const BLOCK_LINES = 200;
|
|
11
|
+
/** Output bytes one block keeps before it drops the oldest. */
|
|
12
|
+
const BLOCK_BYTES = 64 * 1024;
|
|
13
|
+
/** Runs kept for display; the newest always survives. */
|
|
14
|
+
const BLOCK_LIMIT = 20;
|
|
15
|
+
/** Fastest repaint cadence while output streams; status changes always publish. */
|
|
16
|
+
const PUBLISH_INTERVAL_MS = 80;
|
|
17
|
+
/** Readable id of one `!` run's own output, so its transcript bar and its source entry agree. */
|
|
18
|
+
export function localSourceId(id) { return `shell:${id}`; }
|
|
19
|
+
/** Owns the `!` commands of this client process: one at a time, bounded, killable. */
|
|
20
|
+
export class ShellController {
|
|
21
|
+
host;
|
|
22
|
+
enabled;
|
|
23
|
+
blocks = [];
|
|
24
|
+
bytes = new WeakMap();
|
|
25
|
+
nextId = 1;
|
|
26
|
+
task;
|
|
27
|
+
abort;
|
|
28
|
+
lastPublish = 0;
|
|
29
|
+
constructor(host, /** Whether `!` is allowed at all. */ enabled = true) {
|
|
30
|
+
this.host = host;
|
|
31
|
+
this.enabled = enabled;
|
|
32
|
+
}
|
|
33
|
+
/** Runs in creation order, oldest first; the newest is what the transcript shows at its end. */
|
|
34
|
+
get runs() { return this.blocks; }
|
|
35
|
+
/** Whether a command is still running. */
|
|
36
|
+
get running() { return this.blocks.some(block => block.status === 'running'); }
|
|
37
|
+
/** The plain snapshot `AppState.shell` publishes; blocks stay owned here. */
|
|
38
|
+
snapshot() { return { running: this.running, blocks: this.blocks }; }
|
|
39
|
+
/** Start one command.
|
|
40
|
+
*
|
|
41
|
+
* One command runs at a time: a second `!` while the first is live is refused rather than queued,
|
|
42
|
+
* because the transcript shows a single result block and the reader can stop the first with Ctrl+C.
|
|
43
|
+
* @param command - Command line typed after `!`.
|
|
44
|
+
* @returns The block created for it.
|
|
45
|
+
*/
|
|
46
|
+
start(command) {
|
|
47
|
+
if (!this.enabled)
|
|
48
|
+
throw new Error('Shell commands are disabled (--no-shell or DSHT_NO_SHELL=1)');
|
|
49
|
+
if (this.task)
|
|
50
|
+
throw new Error('A shell command is already running; Ctrl+C stops it');
|
|
51
|
+
const id = this.nextId++;
|
|
52
|
+
const block = { id, kind: 'shell', source: localSourceId(id), command, lines: [], dropped: 0,
|
|
53
|
+
status: 'running', startedAt: Date.now(), anchor: this.host.anchor() };
|
|
54
|
+
this.blocks.push(block);
|
|
55
|
+
while (this.blocks.length > BLOCK_LIMIT)
|
|
56
|
+
this.blocks.shift();
|
|
57
|
+
this.bytes.set(block, 0);
|
|
58
|
+
const abort = new AbortController();
|
|
59
|
+
this.abort = abort;
|
|
60
|
+
this.publish(true);
|
|
61
|
+
const task = runShell(command, {
|
|
62
|
+
cwd: this.host.cwd(), env: this.host.env(), signal: abort.signal,
|
|
63
|
+
onLine: (line, stream) => this.append(block, line),
|
|
64
|
+
}).then(exit => {
|
|
65
|
+
block.status = 'exited';
|
|
66
|
+
block.code = exit.code;
|
|
67
|
+
block.signal = exit.signal;
|
|
68
|
+
block.endedAt = Date.now();
|
|
69
|
+
}, error => {
|
|
70
|
+
block.status = 'exited';
|
|
71
|
+
block.code = null;
|
|
72
|
+
this.append(block, `! ${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
+
}).finally(() => {
|
|
74
|
+
if (this.abort === abort)
|
|
75
|
+
this.abort = undefined;
|
|
76
|
+
if (this.task === task)
|
|
77
|
+
this.task = undefined;
|
|
78
|
+
this.publish(true);
|
|
79
|
+
});
|
|
80
|
+
this.task = task;
|
|
81
|
+
return block;
|
|
82
|
+
}
|
|
83
|
+
/** Stop the running command: its process group gets SIGTERM, then SIGKILL after a grace period.
|
|
84
|
+
* @returns Whether a run was stopped.
|
|
85
|
+
*/
|
|
86
|
+
cancel() {
|
|
87
|
+
if (!this.abort)
|
|
88
|
+
return false;
|
|
89
|
+
this.abort.abort();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
/** Echo one client command as a local block, so the transcript holds a bar for it.
|
|
93
|
+
*
|
|
94
|
+
* Nothing runs and nothing is retained: the bar exists to be read and clicked, and the application
|
|
95
|
+
* links it to a readable source afterwards (`link`). A command that only fills a composer frame
|
|
96
|
+
* never reaches here, so the transcript does not collect bars for forms that were never run.
|
|
97
|
+
* @param command - The command line as the operator submitted it.
|
|
98
|
+
* @param source - Readable source the bar opens, when one already exists; `link` adds a later one.
|
|
99
|
+
* @returns The block created for it.
|
|
100
|
+
*/
|
|
101
|
+
note(command, source) {
|
|
102
|
+
const block = { id: this.nextId++, kind: 'note', command, lines: [], dropped: 0,
|
|
103
|
+
status: 'exited', startedAt: Date.now(), endedAt: Date.now(), anchor: this.host.anchor(),
|
|
104
|
+
...(source === undefined ? {} : { source }) };
|
|
105
|
+
this.blocks.push(block);
|
|
106
|
+
while (this.blocks.length > BLOCK_LIMIT)
|
|
107
|
+
this.blocks.shift();
|
|
108
|
+
this.publish(true);
|
|
109
|
+
return block;
|
|
110
|
+
}
|
|
111
|
+
/** Point the newest note at a readable source, so its bar opens what the run is doing now. */
|
|
112
|
+
link(source) {
|
|
113
|
+
const note = [...this.blocks].reverse().find(block => block.kind === 'note');
|
|
114
|
+
if (note === undefined || note.source === source)
|
|
115
|
+
return;
|
|
116
|
+
note.source = source;
|
|
117
|
+
this.publish(true);
|
|
118
|
+
}
|
|
119
|
+
/** One run's retained output, with dropped lines made explicit.
|
|
120
|
+
* @param id - Run to read; defaults to the newest.
|
|
121
|
+
* @returns The text, or undefined when the run is unknown.
|
|
122
|
+
*/
|
|
123
|
+
output(id) {
|
|
124
|
+
const block = id === undefined ? this.blocks.at(-1) : this.blocks.find(item => item.id === id);
|
|
125
|
+
if (!block)
|
|
126
|
+
return undefined;
|
|
127
|
+
const head = block.dropped > 0 ? [`… ${block.dropped} earlier lines dropped …`] : [];
|
|
128
|
+
return [...head, ...block.lines].join('\n');
|
|
129
|
+
}
|
|
130
|
+
/** Stop the running command and wait for it, so no child outlives this client. */
|
|
131
|
+
async stop() {
|
|
132
|
+
this.cancel();
|
|
133
|
+
await this.task;
|
|
134
|
+
while (this.running)
|
|
135
|
+
await delay(20);
|
|
136
|
+
}
|
|
137
|
+
/** Append one output line, trimming the block to its budgets from the front. */
|
|
138
|
+
append(block, line) {
|
|
139
|
+
block.lines.push(line);
|
|
140
|
+
let bytes = (this.bytes.get(block) ?? 0) + line.length * 2;
|
|
141
|
+
while (block.lines.length > BLOCK_LINES || bytes > BLOCK_BYTES) {
|
|
142
|
+
if (block.lines.length === 1)
|
|
143
|
+
break;
|
|
144
|
+
bytes -= block.lines.shift().length * 2;
|
|
145
|
+
block.dropped++;
|
|
146
|
+
}
|
|
147
|
+
this.bytes.set(block, bytes);
|
|
148
|
+
this.publish(false);
|
|
149
|
+
}
|
|
150
|
+
/** Repaint, at most once per interval unless the change is a status change. */
|
|
151
|
+
publish(force) {
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
if (!force && now - this.lastPublish < PUBLISH_INTERVAL_MS)
|
|
154
|
+
return;
|
|
155
|
+
this.lastPublish = now;
|
|
156
|
+
this.host.publish();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Shell domain: the local `!` command runner and the bounded blocks it produces. */
|
|
2
|
+
export { ShellController, localSourceId } from './controller.ts';
|
|
3
|
+
export type { ShellBlock, ShellHost, ShellSnapshot } from './controller.ts';
|
|
4
|
+
export { runProcess, runShell } from './runner.ts';
|
|
5
|
+
export type { ShellExit, ShellRunOptions, ShellStream } from './runner.ts';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Which pipe one line arrived on. */
|
|
2
|
+
export type ShellStream = 'stdout' | 'stderr';
|
|
3
|
+
/** How one command ended. */
|
|
4
|
+
export interface ShellExit {
|
|
5
|
+
code: number | null;
|
|
6
|
+
signal: string | null;
|
|
7
|
+
}
|
|
8
|
+
/** One command's execution contract. */
|
|
9
|
+
export interface ShellRunOptions {
|
|
10
|
+
/** Working directory; the client's own directory, not the host's. */
|
|
11
|
+
cwd: string;
|
|
12
|
+
/** Environment for the child; the caller strips client credentials first. */
|
|
13
|
+
env: NodeJS.ProcessEnv;
|
|
14
|
+
/** Cancels the run; the child's process group is terminated. */
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
/** Receives every complete line, in arrival order across both pipes. */
|
|
17
|
+
onLine(line: string, stream: ShellStream): void;
|
|
18
|
+
}
|
|
19
|
+
/** Run one command through the operator's shell and stream its lines.
|
|
20
|
+
*
|
|
21
|
+
* Both pipes are merged into one line stream in arrival order. A line longer than the assemble
|
|
22
|
+
* budget is emitted once, truncated, and the remainder is discarded until the next newline, so a
|
|
23
|
+
* command that never emits one cannot grow the client's memory.
|
|
24
|
+
* @param command - Command line, exactly as typed after `!`.
|
|
25
|
+
* @param options - Directory, environment, cancellation and the line sink.
|
|
26
|
+
* @returns How the command ended.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runShell(command: string, options: ShellRunOptions): Promise<ShellExit>;
|
|
29
|
+
/** Run one program with its own arguments, without a shell.
|
|
30
|
+
*
|
|
31
|
+
* A forked verifier is spawned this way: arguments reach the child verbatim, so a prompt can never be
|
|
32
|
+
* re-read as shell syntax.
|
|
33
|
+
* @param file - Program to run.
|
|
34
|
+
* @param args - Arguments, passed verbatim.
|
|
35
|
+
* @param options - Directory, environment, cancellation and the line sink.
|
|
36
|
+
* @returns How the program ended.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runProcess(file: string, args: readonly string[], options: ShellRunOptions): Promise<ShellExit>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Running one local shell command for the reader, and nothing else.
|
|
2
|
+
*
|
|
3
|
+
* `!cmd` executes on the machine this client runs on — the operator's laptop or a jump host — never
|
|
4
|
+
* on the host the agent works in. The host's shell belongs to the model's own tools; this module is
|
|
5
|
+
* a separate, local facility, and it is the only place in `src/` allowed to spawn a process — including
|
|
6
|
+
* the `dsht` child that verifies a scored round in a session of its own.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
/** Longest single output line kept while it is still being assembled. */
|
|
10
|
+
const MAX_LINE_CHARS = 8 * 1024;
|
|
11
|
+
/** How long a cancelled command may ignore SIGTERM before it is killed. */
|
|
12
|
+
const KILL_GRACE_MS = 2_000;
|
|
13
|
+
/** The interactive shell to run under, falling back to `sh` when `$SHELL` is unusable. */
|
|
14
|
+
function shellPath() {
|
|
15
|
+
const chosen = process.env.SHELL;
|
|
16
|
+
return chosen !== undefined && chosen !== '' ? chosen : '/bin/sh';
|
|
17
|
+
}
|
|
18
|
+
/** Terminate one child's whole process group, so pipelines and background children die with it.
|
|
19
|
+
*
|
|
20
|
+
* The child is spawned detached, which gives it its own group; signalling the group is what makes
|
|
21
|
+
* cancellation behave like Ctrl+C in a terminal instead of leaving orphans holding the pipes.
|
|
22
|
+
* @param child - The spawned shell.
|
|
23
|
+
* @param signal - Signal to send the group.
|
|
24
|
+
*/
|
|
25
|
+
function signalGroup(child, signal) {
|
|
26
|
+
const pid = child.pid;
|
|
27
|
+
if (pid === undefined)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
process.kill(-pid, signal);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
try {
|
|
34
|
+
child.kill(signal);
|
|
35
|
+
}
|
|
36
|
+
catch { /* already gone */ }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Run one command through the operator's shell and stream its lines.
|
|
40
|
+
*
|
|
41
|
+
* Both pipes are merged into one line stream in arrival order. A line longer than the assemble
|
|
42
|
+
* budget is emitted once, truncated, and the remainder is discarded until the next newline, so a
|
|
43
|
+
* command that never emits one cannot grow the client's memory.
|
|
44
|
+
* @param command - Command line, exactly as typed after `!`.
|
|
45
|
+
* @param options - Directory, environment, cancellation and the line sink.
|
|
46
|
+
* @returns How the command ended.
|
|
47
|
+
*/
|
|
48
|
+
export function runShell(command, options) {
|
|
49
|
+
return spawnLines(shellPath(), ['-c', command], options);
|
|
50
|
+
}
|
|
51
|
+
/** Run one program with its own arguments, without a shell.
|
|
52
|
+
*
|
|
53
|
+
* A forked verifier is spawned this way: arguments reach the child verbatim, so a prompt can never be
|
|
54
|
+
* re-read as shell syntax.
|
|
55
|
+
* @param file - Program to run.
|
|
56
|
+
* @param args - Arguments, passed verbatim.
|
|
57
|
+
* @param options - Directory, environment, cancellation and the line sink.
|
|
58
|
+
* @returns How the program ended.
|
|
59
|
+
*/
|
|
60
|
+
export function runProcess(file, args, options) {
|
|
61
|
+
return spawnLines(file, [...args], options);
|
|
62
|
+
}
|
|
63
|
+
/** Spawn one program, merge both pipes into a single line stream, and resolve when it closes.
|
|
64
|
+
* @param file - Program to run.
|
|
65
|
+
* @param args - Arguments, passed verbatim.
|
|
66
|
+
* @param options - Directory, environment, cancellation and the line sink.
|
|
67
|
+
* @returns How the program ended.
|
|
68
|
+
*/
|
|
69
|
+
function spawnLines(file, args, options) {
|
|
70
|
+
return new Promise(resolve => {
|
|
71
|
+
// Nothing to cancel yet: never spawn a process the caller already abandoned.
|
|
72
|
+
if (options.signal.aborted) {
|
|
73
|
+
resolve({ code: null, signal: 'SIGTERM' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const child = spawn(file, [...args], {
|
|
77
|
+
cwd: options.cwd, env: options.env, detached: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
78
|
+
});
|
|
79
|
+
const carry = { stdout: '', stderr: '' };
|
|
80
|
+
const discarding = { stdout: false, stderr: false };
|
|
81
|
+
let settled = false;
|
|
82
|
+
let graceTimer;
|
|
83
|
+
/** Emit one assembled line, truncated to the budget.
|
|
84
|
+
*
|
|
85
|
+
* The bound is applied to every emitted line, not only to a partial remainder that overflowed:
|
|
86
|
+
* pipe chunking decides whether one huge line arrives whole or in pieces, so a bound applied only
|
|
87
|
+
* to the remainder would make the client's memory depend on the operating system's read size.
|
|
88
|
+
*/
|
|
89
|
+
const emit = (line, stream) => {
|
|
90
|
+
if (discarding[stream]) {
|
|
91
|
+
discarding[stream] = false;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const clean = line.replace(/\r$/, '');
|
|
95
|
+
options.onLine(clean.length > MAX_LINE_CHARS ? `${clean.slice(0, MAX_LINE_CHARS)}…` : clean, stream);
|
|
96
|
+
};
|
|
97
|
+
/** Split one chunk into lines, keeping the partial remainder for the next chunk. */
|
|
98
|
+
const feed = (stream, chunk) => {
|
|
99
|
+
carry[stream] += chunk;
|
|
100
|
+
for (;;) {
|
|
101
|
+
const newline = carry[stream].indexOf('\n');
|
|
102
|
+
if (newline < 0)
|
|
103
|
+
break;
|
|
104
|
+
const line = carry[stream].slice(0, newline);
|
|
105
|
+
carry[stream] = carry[stream].slice(newline + 1);
|
|
106
|
+
emit(line, stream);
|
|
107
|
+
}
|
|
108
|
+
if (carry[stream].length > MAX_LINE_CHARS) {
|
|
109
|
+
// No newline in sight: emit the head once and drop the rest of this line.
|
|
110
|
+
if (!discarding[stream]) {
|
|
111
|
+
options.onLine(`${carry[stream].slice(0, MAX_LINE_CHARS)}…`, stream);
|
|
112
|
+
discarding[stream] = true;
|
|
113
|
+
}
|
|
114
|
+
carry[stream] = '';
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const flush = (stream) => {
|
|
118
|
+
if (carry[stream] !== '')
|
|
119
|
+
emit(carry[stream], stream);
|
|
120
|
+
carry[stream] = '';
|
|
121
|
+
};
|
|
122
|
+
const finish = (exit) => {
|
|
123
|
+
if (settled)
|
|
124
|
+
return;
|
|
125
|
+
settled = true;
|
|
126
|
+
clearTimeout(graceTimer);
|
|
127
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
128
|
+
flush('stdout');
|
|
129
|
+
flush('stderr');
|
|
130
|
+
resolve(exit);
|
|
131
|
+
};
|
|
132
|
+
const onAbort = () => {
|
|
133
|
+
signalGroup(child, 'SIGTERM');
|
|
134
|
+
graceTimer = setTimeout(() => { signalGroup(child, 'SIGKILL'); }, KILL_GRACE_MS);
|
|
135
|
+
};
|
|
136
|
+
child.stdout?.setEncoding('utf8');
|
|
137
|
+
child.stderr?.setEncoding('utf8');
|
|
138
|
+
child.stdout?.on('data', (chunk) => feed('stdout', chunk));
|
|
139
|
+
child.stderr?.on('data', (chunk) => feed('stderr', chunk));
|
|
140
|
+
child.on('error', error => { options.onLine(`! ${error.message}`, 'stderr'); finish({ code: null, signal: null }); });
|
|
141
|
+
child.on('close', (code, signal) => finish({ code, signal }));
|
|
142
|
+
if (options.signal.aborted)
|
|
143
|
+
onAbort();
|
|
144
|
+
else
|
|
145
|
+
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Slash-command domain: the command catalog, its syntax and the completion helpers.
|
|
2
|
+
*
|
|
3
|
+
* A pure leaf: it imports nothing from the application, the features or the UI.
|
|
4
|
+
*/
|
|
5
|
+
export { COMMAND_HINTS, COMMAND_LABELS, COMMAND_LABEL_WIDTH, COMMANDS, COMMAND_POLICY, argumentHint, commandMatches, commonPrefix, completeCommand, resolveCommand, suggestedCommands } from './registry.ts';
|
|
6
|
+
export type { CommandHint, CommandPolicy } from './registry.ts';
|
|
7
|
+
export { parseCommand, loopNameQuery, validLoopOption, LOOP_ABORT_USAGE, LOOP_ANSWER_USAGE, LOOP_STOP_USAGE, LOOP_USAGE } from './parse.ts';
|
|
8
|
+
export type { Command, LoopOptions } from './parse.ts';
|
|
9
|
+
export { interpret, normalize, authorize } from './pipeline.ts';
|
|
10
|
+
export type { AuthorizeFacts, DeferReason, ExecutableSubmission, InterpretFacts, LineCommand, NormalizeFacts, Submission, UiAction, Verdict } from './pipeline.ts';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Slash-command domain: the command catalog, its syntax and the completion helpers.
|
|
2
|
+
*
|
|
3
|
+
* A pure leaf: it imports nothing from the application, the features or the UI.
|
|
4
|
+
*/
|
|
5
|
+
export { COMMAND_HINTS, COMMAND_LABELS, COMMAND_LABEL_WIDTH, COMMANDS, COMMAND_POLICY, argumentHint, commandMatches, commonPrefix, completeCommand, resolveCommand, suggestedCommands } from "./registry.js";
|
|
6
|
+
export { parseCommand, loopNameQuery, validLoopOption, LOOP_ABORT_USAGE, LOOP_ANSWER_USAGE, LOOP_STOP_USAGE, LOOP_USAGE } from "./parse.js";
|
|
7
|
+
export { interpret, normalize, authorize } from "./pipeline.js";
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** One parsed command; every side effect stays with the caller. */
|
|
2
|
+
export type Command = {
|
|
3
|
+
kind: 'ignore';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'copy';
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'quit';
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'panel';
|
|
10
|
+
panel: 'cost' | 'status' | 'help';
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'remove';
|
|
13
|
+
target: 'workspace' | 'session';
|
|
14
|
+
query: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'navigate';
|
|
17
|
+
target: 'workspace' | 'session';
|
|
18
|
+
query?: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'latest';
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'models';
|
|
23
|
+
args: string[];
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'queue';
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'newSession';
|
|
28
|
+
}
|
|
29
|
+
/** Open the saved-prompt picker, or save the following text as a shortcut prompt. */
|
|
30
|
+
| {
|
|
31
|
+
kind: 'prompts';
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'savePrompt';
|
|
34
|
+
text: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'history';
|
|
37
|
+
query: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'sessionSearch';
|
|
40
|
+
command: '/ssearch' | '/wsearch';
|
|
41
|
+
query: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'historySearch';
|
|
44
|
+
query: string;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'think';
|
|
47
|
+
target: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'older';
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'compact';
|
|
52
|
+
}
|
|
53
|
+
/** Clear the client's own HANDOFF.md, then ask the agent to write a fresh session handoff. */
|
|
54
|
+
| {
|
|
55
|
+
kind: 'handoff';
|
|
56
|
+
}
|
|
57
|
+
/** Start the client-driven scored loop for one `loop.yaml` record. */
|
|
58
|
+
| {
|
|
59
|
+
kind: 'loop';
|
|
60
|
+
name: string;
|
|
61
|
+
options: LoopOptions;
|
|
62
|
+
}
|
|
63
|
+
/** Offer the `loop.yaml` records so one can be chosen instead of typed. */
|
|
64
|
+
| {
|
|
65
|
+
kind: 'loops';
|
|
66
|
+
}
|
|
67
|
+
/** Stop the running loop, running or paused. */
|
|
68
|
+
| {
|
|
69
|
+
kind: 'loopStop';
|
|
70
|
+
}
|
|
71
|
+
/** Answer a paused run, so the current artifact is judged again with the operator's addition. */
|
|
72
|
+
| {
|
|
73
|
+
kind: 'loopAnswer';
|
|
74
|
+
text: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'cancel';
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'approval';
|
|
79
|
+
allowed: boolean;
|
|
80
|
+
} | {
|
|
81
|
+
kind: 'hostCommand';
|
|
82
|
+
line: string;
|
|
83
|
+
}
|
|
84
|
+
/** A local `!` command, run on this machine rather than the host. */
|
|
85
|
+
| {
|
|
86
|
+
kind: 'shell';
|
|
87
|
+
command: string;
|
|
88
|
+
} | {
|
|
89
|
+
kind: 'export';
|
|
90
|
+
destination?: string;
|
|
91
|
+
} | {
|
|
92
|
+
kind: 'exportHtml';
|
|
93
|
+
destination?: string;
|
|
94
|
+
} | {
|
|
95
|
+
kind: 'coredump';
|
|
96
|
+
tag?: string;
|
|
97
|
+
} | {
|
|
98
|
+
kind: 'error';
|
|
99
|
+
message: string;
|
|
100
|
+
} | {
|
|
101
|
+
kind: 'prompt';
|
|
102
|
+
text: string;
|
|
103
|
+
};
|
|
104
|
+
/** Options carried by `/loop`.
|
|
105
|
+
*
|
|
106
|
+
* Only the fields the operator actually typed are present, so the syntax layer validates each value
|
|
107
|
+
* it sees and the application owns the defaults — command line first, then the record's `defaults`,
|
|
108
|
+
* then the global ones.
|
|
109
|
+
*/
|
|
110
|
+
export interface LoopOptions {
|
|
111
|
+
/** First step to run; default 1. */
|
|
112
|
+
from?: number;
|
|
113
|
+
/** Last step to run; default the record's last step. */
|
|
114
|
+
to?: number;
|
|
115
|
+
/** Passing score per step, 0-10 and possibly fractional; default the record's, else 8. */
|
|
116
|
+
score?: number;
|
|
117
|
+
/** Attempts allowed per step; default the record's, else 10. */
|
|
118
|
+
tries?: number;
|
|
119
|
+
/** Values that replace the record's own `vars` for this run, such as the document under review.
|
|
120
|
+
*
|
|
121
|
+
* The interactive form fills this in; the command line has no syntax for it, because the record —
|
|
122
|
+
* not the syntax layer — is what knows which names exist.
|
|
123
|
+
*/
|
|
124
|
+
vars?: Readonly<Record<string, string>>;
|
|
125
|
+
}
|
|
126
|
+
/** The numeric options a flag can carry; `vars` is not one, so it stays out of the flag table. */
|
|
127
|
+
type LoopNumberOption = 'from' | 'to' | 'score' | 'tries';
|
|
128
|
+
/** Whether one numeric value is acceptable for one loop option.
|
|
129
|
+
*
|
|
130
|
+
* The command line and the interactive form share this rule, so a value the form accepts is exactly
|
|
131
|
+
* one the syntax would have accepted if it had been typed.
|
|
132
|
+
* @param key - Option being set.
|
|
133
|
+
* @param value - Candidate number.
|
|
134
|
+
* @returns True when the option may carry that value.
|
|
135
|
+
*/
|
|
136
|
+
export declare function validLoopOption(key: LoopNumberOption, value: number): boolean;
|
|
137
|
+
/** The record name the composer is currently typing after `/loop`, if any.
|
|
138
|
+
*
|
|
139
|
+
* The loop-name menu appears while the line is `/loop` or `/loop <one unfinished token>`; a second
|
|
140
|
+
* token means the operator moved on to the flags, so the menu stays out of the way. A trailing space
|
|
141
|
+
* after a complete name still counts: the menu then confirms that name rather than filtering it out.
|
|
142
|
+
* @param line - Composer draft exactly as typed.
|
|
143
|
+
* @returns The unfinished name (empty when none was started), or undefined for any other line.
|
|
144
|
+
*/
|
|
145
|
+
export declare function loopNameQuery(line: string): string | undefined;
|
|
146
|
+
/** The one message every malformed `/loop` line receives. */
|
|
147
|
+
export declare const LOOP_USAGE = "Use /loop <name> [score] [tries] [--from N] [--to N] [--score X] [--tries N]";
|
|
148
|
+
/** The one message a malformed `/loop stop` line receives. */
|
|
149
|
+
export declare const LOOP_STOP_USAGE = "Use /loop stop (no arguments)";
|
|
150
|
+
/** The one message a `/loop answer` line without an answer receives. */
|
|
151
|
+
export declare const LOOP_ANSWER_USAGE = "Use /loop answer <text>";
|
|
152
|
+
/** The one message a malformed `/loop abort` line receives. */
|
|
153
|
+
export declare const LOOP_ABORT_USAGE = "Use /loop abort (no arguments)";
|
|
154
|
+
/** Parse one composer line into a command without performing any of its effects.
|
|
155
|
+
*
|
|
156
|
+
* The order of the checks is the command precedence: the in-place panels, navigation and removal,
|
|
157
|
+
* then the session and host commands, then a plain prompt. Facts about the current screen or a
|
|
158
|
+
* pending interaction are deliberately absent: they decide whether a command may run, not what it is.
|
|
159
|
+
*
|
|
160
|
+
* A leading token that names exactly one command is resolved first, so `/pro Add tests` runs
|
|
161
|
+
* `/prompt Add tests`; an ambiguous token is left alone and reported with its candidates.
|
|
162
|
+
* @param line - Draft exactly as submitted.
|
|
163
|
+
* @returns The parsed command.
|
|
164
|
+
*/
|
|
165
|
+
export declare function parseCommand(line: string): Command;
|
|
166
|
+
export {};
|