pikiloom 0.4.36 → 0.4.38

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 (50) hide show
  1. package/dist/agent/drivers/codex.js +20 -3
  2. package/dist/agent/index.js +2 -2
  3. package/dist/agent/kernel-bridge.js +206 -0
  4. package/dist/agent/stream.js +60 -2
  5. package/dist/cli/kernel-app.js +115 -0
  6. package/dist/cli/main.js +7 -0
  7. package/package.json +4 -2
  8. package/packages/kernel/README.md +107 -0
  9. package/packages/kernel/dist/contracts/driver.d.ts +95 -0
  10. package/packages/kernel/dist/contracts/driver.js +1 -0
  11. package/packages/kernel/dist/contracts/ports.d.ts +84 -0
  12. package/packages/kernel/dist/contracts/ports.js +1 -0
  13. package/packages/kernel/dist/contracts/surface.d.ts +75 -0
  14. package/packages/kernel/dist/contracts/surface.js +1 -0
  15. package/packages/kernel/dist/drivers/claude.d.ts +23 -0
  16. package/packages/kernel/dist/drivers/claude.js +453 -0
  17. package/packages/kernel/dist/drivers/codex.d.ts +14 -0
  18. package/packages/kernel/dist/drivers/codex.js +346 -0
  19. package/packages/kernel/dist/drivers/echo.d.ts +20 -0
  20. package/packages/kernel/dist/drivers/echo.js +61 -0
  21. package/packages/kernel/dist/drivers/gemini.d.ts +18 -0
  22. package/packages/kernel/dist/drivers/gemini.js +143 -0
  23. package/packages/kernel/dist/drivers/hermes.d.ts +14 -0
  24. package/packages/kernel/dist/drivers/hermes.js +194 -0
  25. package/packages/kernel/dist/drivers/index.d.ts +5 -0
  26. package/packages/kernel/dist/drivers/index.js +5 -0
  27. package/packages/kernel/dist/index.d.ts +18 -0
  28. package/packages/kernel/dist/index.js +29 -0
  29. package/packages/kernel/dist/ports/defaults.d.ts +59 -0
  30. package/packages/kernel/dist/ports/defaults.js +137 -0
  31. package/packages/kernel/dist/protocol/index.d.ts +309 -0
  32. package/packages/kernel/dist/protocol/index.js +59 -0
  33. package/packages/kernel/dist/runtime/hub.d.ts +65 -0
  34. package/packages/kernel/dist/runtime/hub.js +260 -0
  35. package/packages/kernel/dist/runtime/loom.d.ts +44 -0
  36. package/packages/kernel/dist/runtime/loom.js +69 -0
  37. package/packages/kernel/dist/runtime/pty.d.ts +23 -0
  38. package/packages/kernel/dist/runtime/pty.js +69 -0
  39. package/packages/kernel/dist/runtime/session-runner.d.ts +27 -0
  40. package/packages/kernel/dist/runtime/session-runner.js +210 -0
  41. package/packages/kernel/dist/runtime/tui.d.ts +8 -0
  42. package/packages/kernel/dist/runtime/tui.js +35 -0
  43. package/packages/kernel/dist/runtime/turn.d.ts +17 -0
  44. package/packages/kernel/dist/runtime/turn.js +16 -0
  45. package/packages/kernel/dist/surfaces/cli.d.ts +19 -0
  46. package/packages/kernel/dist/surfaces/cli.js +65 -0
  47. package/packages/kernel/dist/surfaces/index.d.ts +2 -0
  48. package/packages/kernel/dist/surfaces/index.js +2 -0
  49. package/packages/kernel/dist/surfaces/web.d.ts +39 -0
  50. package/packages/kernel/dist/surfaces/web.js +244 -0
@@ -0,0 +1,44 @@
1
+ import type { AgentDriver, TuiSpec } from '../contracts/driver.js';
2
+ import type { SessionStore, ModelResolver, ToolProvider, SystemPromptBuilder, Catalog, InteractionHandler } from '../contracts/ports.js';
3
+ import type { LoomIO, Surface, Plugin } from '../contracts/surface.js';
4
+ import { PtyBridge, type PtyOpenOpts, type PtyExit } from './pty.js';
5
+ export interface TuiLaunchOptions {
6
+ agent?: string;
7
+ workdir?: string;
8
+ model?: string | null;
9
+ sessionId?: string | null;
10
+ }
11
+ export interface LoomConfig {
12
+ appNamespace?: string;
13
+ workdir?: string;
14
+ defaultAgent?: string;
15
+ drivers?: AgentDriver[];
16
+ surfaces?: Surface[];
17
+ plugins?: Plugin[];
18
+ sessionStore?: SessionStore;
19
+ modelResolver?: ModelResolver;
20
+ toolProvider?: ToolProvider;
21
+ systemPromptBuilder?: SystemPromptBuilder;
22
+ catalog?: Catalog;
23
+ interactionHandler?: InteractionHandler;
24
+ serialPerSession?: boolean;
25
+ systemPromptBase?: string;
26
+ log?: (msg: string) => void;
27
+ }
28
+ export interface Loom {
29
+ readonly io: LoomIO;
30
+ registerDriver(driver: AgentDriver): void;
31
+ start(): Promise<void>;
32
+ stop(): Promise<void>;
33
+ status(): {
34
+ agents: string[];
35
+ surfaces: string[];
36
+ started: boolean;
37
+ };
38
+ resolveTui(opts: TuiLaunchOptions): Promise<TuiSpec>;
39
+ openTui(opts: TuiLaunchOptions & PtyOpenOpts): Promise<PtyBridge>;
40
+ runTui(opts: TuiLaunchOptions & {
41
+ observers?: Array<(c: string) => void>;
42
+ }): Promise<PtyExit>;
43
+ }
44
+ export declare function createLoom(config?: LoomConfig): Loom;
@@ -0,0 +1,69 @@
1
+ import { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, NoopCatalog, DeferToTerminalInteractionHandler, defaultBaseDir, } from '../ports/defaults.js';
2
+ import { Hub } from './hub.js';
3
+ import { PtyBridge } from './pty.js';
4
+ import { attachTui } from './tui.js';
5
+ export function createLoom(config = {}) {
6
+ const appNamespace = config.appNamespace || 'loom';
7
+ const workdir = config.workdir || process.cwd();
8
+ const log = config.log || (() => { });
9
+ const drivers = new Map();
10
+ for (const d of config.drivers || [])
11
+ drivers.set(d.id, d);
12
+ const defaultAgent = config.defaultAgent || config.drivers?.[0]?.id || 'echo';
13
+ const hub = new Hub({
14
+ drivers,
15
+ defaultAgent,
16
+ workdir,
17
+ sessionStore: config.sessionStore || new FsSessionStore(defaultBaseDir(appNamespace)),
18
+ modelResolver: config.modelResolver || new NullModelResolver(),
19
+ toolProvider: config.toolProvider || new NoopToolProvider(),
20
+ systemPromptBuilder: config.systemPromptBuilder || new PassthroughSystemPromptBuilder(),
21
+ catalog: config.catalog || new NoopCatalog(),
22
+ interactionHandler: config.interactionHandler || new DeferToTerminalInteractionHandler(),
23
+ serialPerSession: config.serialPerSession,
24
+ plugins: config.plugins || [],
25
+ systemPromptBase: config.systemPromptBase,
26
+ log,
27
+ });
28
+ const surfaces = config.surfaces || [];
29
+ let started = false;
30
+ // Lane R opener (raw PTY): resolve the agent's TUI spec (with model injection) and
31
+ // spawn it in a PtyBridge. Shared by Loom.openTui and the TuiHost given to surfaces.
32
+ const openTui = async (opts) => {
33
+ const spec = await hub.resolveTui(opts);
34
+ return PtyBridge.open(spec, { cols: opts.cols, rows: opts.rows });
35
+ };
36
+ return {
37
+ io: hub,
38
+ registerDriver(driver) { drivers.set(driver.id, driver); },
39
+ async start() {
40
+ if (started)
41
+ return;
42
+ started = true;
43
+ for (const t of surfaces) {
44
+ await t.start(hub, { openTui }); // hub = Lane S (LoomIO); openTui = Lane R
45
+ log(`[loom] terminal started: ${t.id}`);
46
+ }
47
+ },
48
+ async stop() {
49
+ if (!started)
50
+ return;
51
+ started = false;
52
+ for (const t of surfaces) {
53
+ try {
54
+ await t.stop();
55
+ }
56
+ catch { /* ignore */ }
57
+ }
58
+ },
59
+ status() {
60
+ return { agents: [...drivers.keys()], surfaces: surfaces.map(t => t.id), started };
61
+ },
62
+ resolveTui(opts) { return hub.resolveTui(opts); },
63
+ openTui,
64
+ async runTui(opts) {
65
+ const spec = await hub.resolveTui(opts);
66
+ return attachTui(spec, { observers: opts.observers });
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,23 @@
1
+ import type { TuiSpec } from '../contracts/driver.js';
2
+ export declare function ptyAvailable(): boolean;
3
+ export interface PtyOpenOpts {
4
+ cols?: number;
5
+ rows?: number;
6
+ }
7
+ export interface PtyExit {
8
+ exitCode: number;
9
+ signal?: number;
10
+ }
11
+ export declare class PtyBridge {
12
+ private readonly proc;
13
+ private readonly dataCbs;
14
+ private readonly exitCbs;
15
+ private constructor();
16
+ static open(spec: TuiSpec, opts?: PtyOpenOpts): PtyBridge;
17
+ onData(cb: (d: string) => void): () => void;
18
+ onExit(cb: (e: PtyExit) => void): () => void;
19
+ write(data: string): void;
20
+ resize(cols: number, rows: number): void;
21
+ kill(signal?: string): void;
22
+ get pid(): number;
23
+ }
@@ -0,0 +1,69 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ let _pty;
4
+ function loadPty() {
5
+ if (_pty === undefined) {
6
+ try {
7
+ _pty = require('node-pty');
8
+ }
9
+ catch {
10
+ _pty = null;
11
+ }
12
+ }
13
+ return _pty;
14
+ }
15
+ export function ptyAvailable() { return !!loadPty(); }
16
+ // A pseudo-terminal bridge around an agent's interactive process. Carries raw TUI bytes
17
+ // in both directions and fans `onData` out to many observers (full passthrough + tee/mirror).
18
+ // This is the capability the kernel previously lacked: transparently passing through
19
+ // Claude/Codex TUI traffic instead of only the structured -p stream.
20
+ export class PtyBridge {
21
+ proc;
22
+ dataCbs = new Set();
23
+ exitCbs = new Set();
24
+ constructor(proc) {
25
+ this.proc = proc;
26
+ proc.onData((d) => { for (const cb of this.dataCbs) {
27
+ try {
28
+ cb(d);
29
+ }
30
+ catch { /* isolate */ }
31
+ } });
32
+ proc.onExit((e) => {
33
+ for (const cb of this.exitCbs) {
34
+ try {
35
+ cb({ exitCode: e.exitCode, signal: e.signal });
36
+ }
37
+ catch { /* isolate */ }
38
+ }
39
+ });
40
+ }
41
+ static open(spec, opts = {}) {
42
+ const pty = loadPty();
43
+ if (!pty)
44
+ throw new Error('node-pty is not available — install the optional dependency `node-pty` to use TUI passthrough.');
45
+ const proc = pty.spawn(spec.command, spec.args, {
46
+ name: 'xterm-256color',
47
+ cols: opts.cols || 80,
48
+ rows: opts.rows || 24,
49
+ cwd: spec.cwd,
50
+ env: { ...process.env, ...(spec.env || {}) },
51
+ });
52
+ return new PtyBridge(proc);
53
+ }
54
+ onData(cb) { this.dataCbs.add(cb); return () => this.dataCbs.delete(cb); }
55
+ onExit(cb) { this.exitCbs.add(cb); return () => this.exitCbs.delete(cb); }
56
+ write(data) { try {
57
+ this.proc.write(data);
58
+ }
59
+ catch { /* closed */ } }
60
+ resize(cols, rows) { try {
61
+ this.proc.resize(Math.max(1, cols), Math.max(1, rows));
62
+ }
63
+ catch { /* closed */ } }
64
+ kill(signal) { try {
65
+ this.proc.kill(signal);
66
+ }
67
+ catch { /* closed */ } }
68
+ get pid() { return this.proc.pid; }
69
+ }
@@ -0,0 +1,27 @@
1
+ import type { UniversalSnapshot } from '../protocol/index.js';
2
+ import type { AgentDriver, AgentTurnInput, DriverResult } from '../contracts/driver.js';
3
+ import type { InteractionHandler } from '../contracts/ports.js';
4
+ export declare class SessionRunner {
5
+ readonly sessionKey: string;
6
+ readonly agent: string;
7
+ readonly taskId: string;
8
+ private readonly onUpdate;
9
+ private readonly interactionHandler?;
10
+ readonly snapshot: UniversalSnapshot;
11
+ private seq;
12
+ private readonly abort;
13
+ private steerFn;
14
+ private pending;
15
+ private finished;
16
+ constructor(sessionKey: string, agent: string, taskId: string, onUpdate: (snapshot: UniversalSnapshot, seq: number) => void, interactionHandler?: InteractionHandler | undefined);
17
+ get currentSeq(): number;
18
+ private publish;
19
+ run(driver: AgentDriver, input: AgentTurnInput, prompt: string, model: string | null, effort: string | null): Promise<DriverResult>;
20
+ private applyEvent;
21
+ private askUser;
22
+ private flushPending;
23
+ stop(): void;
24
+ steer(prompt: string, attachments?: string[]): Promise<boolean>;
25
+ interact(promptId: string, action: 'select' | 'text' | 'skip' | 'cancel', value?: string): boolean;
26
+ }
27
+ export declare function projectActivity(snap: UniversalSnapshot): string;
@@ -0,0 +1,210 @@
1
+ // Drives ONE turn of ONE session: applies driver events into an accumulating
2
+ // UniversalSnapshot and owns the single-session control verbs (stop/steer/interact).
3
+ // Knows nothing about chat ids, queues, or transports.
4
+ export class SessionRunner {
5
+ sessionKey;
6
+ agent;
7
+ taskId;
8
+ onUpdate;
9
+ interactionHandler;
10
+ snapshot;
11
+ seq = 0;
12
+ abort = new AbortController();
13
+ steerFn = null;
14
+ pending = null;
15
+ finished = false;
16
+ constructor(sessionKey, agent, taskId, onUpdate, interactionHandler) {
17
+ this.sessionKey = sessionKey;
18
+ this.agent = agent;
19
+ this.taskId = taskId;
20
+ this.onUpdate = onUpdate;
21
+ this.interactionHandler = interactionHandler;
22
+ this.snapshot = {
23
+ phase: 'queued', taskId, agent, sessionId: null,
24
+ text: '', reasoning: '', activity: '',
25
+ toolCalls: [], interactions: [], artifacts: [],
26
+ updatedAt: Date.now(),
27
+ };
28
+ }
29
+ get currentSeq() { return this.seq; }
30
+ publish() {
31
+ this.snapshot.updatedAt = Date.now();
32
+ this.seq += 1;
33
+ this.onUpdate(this.snapshot, this.seq);
34
+ }
35
+ async run(driver, input, prompt, model, effort) {
36
+ this.snapshot.phase = 'streaming';
37
+ this.snapshot.prompt = prompt;
38
+ this.snapshot.model = model;
39
+ this.snapshot.effort = effort;
40
+ this.snapshot.startedAt = Date.now();
41
+ this.publish();
42
+ const ctx = {
43
+ signal: this.abort.signal,
44
+ emit: (e) => this.applyEvent(e),
45
+ askUser: (interaction) => this.askUser(interaction),
46
+ registerSteer: (fn) => { this.steerFn = fn; },
47
+ };
48
+ let result;
49
+ try {
50
+ result = await driver.run(input, ctx);
51
+ }
52
+ catch (err) {
53
+ result = { ok: false, text: this.snapshot.text || '', error: err?.message || String(err), stopReason: 'error' };
54
+ }
55
+ // Merge terminal result into the snapshot (driver may have only streamed deltas).
56
+ if (result.text && result.text.trim() && result.text.length >= (this.snapshot.text || '').length) {
57
+ this.snapshot.text = result.text;
58
+ }
59
+ if (result.reasoning && !(this.snapshot.reasoning || '').trim())
60
+ this.snapshot.reasoning = result.reasoning;
61
+ if (result.sessionId)
62
+ this.snapshot.sessionId = result.sessionId;
63
+ if (result.usage)
64
+ this.snapshot.usage = result.usage;
65
+ this.snapshot.error = result.error ?? null;
66
+ this.snapshot.incomplete = !result.ok;
67
+ this.snapshot.phase = 'done';
68
+ this.snapshot.interactions = [];
69
+ this.finished = true;
70
+ this.flushPending({});
71
+ this.publish();
72
+ return result;
73
+ }
74
+ applyEvent(e) {
75
+ switch (e.type) {
76
+ case 'session':
77
+ this.snapshot.sessionId = e.sessionId;
78
+ break;
79
+ case 'text':
80
+ this.snapshot.text = (this.snapshot.text || '') + e.delta;
81
+ break;
82
+ case 'reasoning':
83
+ this.snapshot.reasoning = (this.snapshot.reasoning || '') + e.delta;
84
+ break;
85
+ case 'activity':
86
+ this.snapshot.activity = e.line;
87
+ break;
88
+ case 'plan':
89
+ this.snapshot.plan = e.plan;
90
+ break;
91
+ case 'artifact':
92
+ (this.snapshot.artifacts ||= []).push(e.artifact);
93
+ break;
94
+ case 'usage':
95
+ this.snapshot.usage = mergeUsage(this.snapshot.usage, e.usage);
96
+ break;
97
+ // toolCalls is the structured SSOT; activity is its derived human-readable view.
98
+ // A driver that streams explicit `activity` lines (e.g. echo) owns activity directly;
99
+ // any driver that emits structured tool/subagent events gets the projection for free.
100
+ case 'tool':
101
+ upsertTool(this.snapshot, e.call);
102
+ this.snapshot.activity = projectActivity(this.snapshot);
103
+ break;
104
+ case 'subagent':
105
+ upsertSubAgent(this.snapshot, e.subagent);
106
+ this.snapshot.activity = projectActivity(this.snapshot);
107
+ break;
108
+ }
109
+ this.publish();
110
+ }
111
+ askUser(interaction) {
112
+ if (this.finished || this.abort.signal.aborted)
113
+ return Promise.resolve({});
114
+ (this.snapshot.interactions ||= []).push(interaction);
115
+ this.publish();
116
+ return new Promise((resolve) => {
117
+ let settled = false;
118
+ const finish = (answers) => {
119
+ if (settled)
120
+ return;
121
+ settled = true;
122
+ this.pending = null;
123
+ this.snapshot.interactions = (this.snapshot.interactions || []).filter(i => i.promptId !== interaction.promptId);
124
+ this.publish();
125
+ resolve(answers);
126
+ };
127
+ this.pending = { promptId: interaction.promptId, resolve: finish };
128
+ // A programmatic resolver may answer (or auto-cancel) the interaction; returning
129
+ // null defers to a terminal calling interact(). The default handler defers.
130
+ Promise.resolve(this.interactionHandler?.askUser(interaction) ?? null)
131
+ .then((answers) => { if (answers)
132
+ finish(answers); })
133
+ .catch(() => { });
134
+ });
135
+ }
136
+ flushPending(answers) {
137
+ this.pending?.resolve(answers);
138
+ }
139
+ // ---- control verbs ----
140
+ stop() {
141
+ this.abort.abort();
142
+ this.flushPending({});
143
+ }
144
+ async steer(prompt, attachments) {
145
+ if (this.finished || !this.steerFn)
146
+ return false;
147
+ return this.steerFn(prompt, attachments);
148
+ }
149
+ interact(promptId, action, value) {
150
+ if (!this.pending || this.pending.promptId !== promptId)
151
+ return false;
152
+ const interaction = (this.snapshot.interactions || []).find(i => i.promptId === promptId);
153
+ const q = interaction?.questions[interaction.currentIndex ?? 0] || interaction?.questions[0];
154
+ const qid = q?.id || 'answer';
155
+ const resolve = this.pending.resolve; // resolver does its own cleanup (pending/snapshot/publish)
156
+ if (action === 'cancel')
157
+ resolve({});
158
+ else if (action === 'skip')
159
+ resolve({ [qid]: [] });
160
+ else
161
+ resolve({ [qid]: [value ?? ''] });
162
+ return true;
163
+ }
164
+ }
165
+ // Derive the human-readable activity trail from the structured tool/subagent calls.
166
+ // One line per call, with a status suffix: running -> "summary", done -> "summary done"
167
+ // (or "summary -> detail"), failed -> "summary failed[: detail]". This is the kernel's
168
+ // activity line contract — a lowest-common-denominator view every terminal can render
169
+ // without re-deriving it from toolCalls itself.
170
+ export function projectActivity(snap) {
171
+ const lines = [];
172
+ for (const t of snap.toolCalls || []) {
173
+ if (t.status === 'failed')
174
+ lines.push(t.result ? `${t.summary} failed: ${t.result}` : `${t.summary} failed`);
175
+ else if (t.status === 'done')
176
+ lines.push(t.result ? `${t.summary} -> ${t.result}` : `${t.summary} done`);
177
+ else
178
+ lines.push(t.summary);
179
+ }
180
+ for (const sub of snap.subAgents || []) {
181
+ const label = sub.description || sub.kind || 'subagent';
182
+ if (sub.status === 'failed')
183
+ lines.push(`Run task: ${label} failed`);
184
+ else if (sub.status === 'done')
185
+ lines.push(`Run task: ${label} done`);
186
+ else
187
+ lines.push(`Run task: ${label}`);
188
+ }
189
+ return lines.join('\n');
190
+ }
191
+ function upsertTool(snap, call) {
192
+ const list = (snap.toolCalls ||= []);
193
+ const idx = list.findIndex(t => t.id === call.id);
194
+ if (idx >= 0)
195
+ list[idx] = { ...list[idx], ...call };
196
+ else
197
+ list.push(call);
198
+ }
199
+ function upsertSubAgent(snap, sub) {
200
+ const list = (snap.subAgents ||= []);
201
+ const idx = list.findIndex(s => s.id === sub.id);
202
+ if (idx >= 0)
203
+ list[idx] = { ...list[idx], ...sub };
204
+ else
205
+ list.push(sub);
206
+ }
207
+ function mergeUsage(prev, next) {
208
+ const base = prev || { inputTokens: null, outputTokens: null, cachedInputTokens: null, contextPercent: null };
209
+ return { ...base, ...next };
210
+ }
@@ -0,0 +1,8 @@
1
+ import type { TuiSpec } from '../contracts/driver.js';
2
+ import { type PtyExit } from './pty.js';
3
+ export interface AttachTuiOptions {
4
+ stdin?: NodeJS.ReadStream;
5
+ stdout?: NodeJS.WriteStream;
6
+ observers?: Array<(chunk: string) => void>;
7
+ }
8
+ export declare function attachTui(spec: TuiSpec, opts?: AttachTuiOptions): Promise<PtyExit>;
@@ -0,0 +1,35 @@
1
+ import { PtyBridge } from './pty.js';
2
+ // Full terminal passthrough: spawn the agent's TUI in a PTY and wire the caller's
3
+ // stdin/stdout to it raw (so the user is "inside" the real Claude/Codex TUI), while
4
+ // optionally tee-ing every byte to observers. Resolves with the exit code.
5
+ export async function attachTui(spec, opts = {}) {
6
+ const stdin = opts.stdin ?? process.stdin;
7
+ const stdout = opts.stdout ?? process.stdout;
8
+ const bridge = PtyBridge.open(spec, { cols: stdout.columns, rows: stdout.rows });
9
+ for (const obs of opts.observers ?? [])
10
+ bridge.onData(obs);
11
+ const offData = bridge.onData((d) => { stdout.write(d); });
12
+ const wasRaw = !!stdin.isRaw;
13
+ if (stdin.isTTY)
14
+ stdin.setRawMode(true);
15
+ stdin.resume();
16
+ const onInput = (chunk) => bridge.write(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
17
+ stdin.on('data', onInput);
18
+ const onResize = () => bridge.resize(stdout.columns || 80, stdout.rows || 24);
19
+ stdout.on('resize', onResize);
20
+ return await new Promise((resolve) => {
21
+ bridge.onExit((e) => {
22
+ stdin.off('data', onInput);
23
+ stdout.off('resize', onResize);
24
+ offData();
25
+ if (stdin.isTTY) {
26
+ try {
27
+ stdin.setRawMode(wasRaw);
28
+ }
29
+ catch { /* ignore */ }
30
+ }
31
+ stdin.pause();
32
+ resolve(e);
33
+ });
34
+ });
35
+ }
@@ -0,0 +1,17 @@
1
+ import type { UniversalSnapshot } from '../protocol/index.js';
2
+ import type { AgentDriver, AgentTurnInput, DriverResult } from '../contracts/driver.js';
3
+ import type { InteractionHandler } from '../contracts/ports.js';
4
+ export interface RunTurnOptions {
5
+ prompt?: string;
6
+ model?: string | null;
7
+ effort?: string | null;
8
+ onSnapshot?: (snapshot: UniversalSnapshot) => void;
9
+ onSteer?: (steer: (prompt: string, attachments?: string[]) => Promise<boolean>) => void;
10
+ signal?: AbortSignal;
11
+ interactionHandler?: InteractionHandler;
12
+ }
13
+ export interface TurnOutcome {
14
+ result: DriverResult;
15
+ snapshot: UniversalSnapshot;
16
+ }
17
+ export declare function runTurn(driver: AgentDriver, input: AgentTurnInput, opts?: RunTurnOptions): Promise<TurnOutcome>;
@@ -0,0 +1,16 @@
1
+ import { SessionRunner } from './session-runner.js';
2
+ import { AutoCancelInteractionHandler } from '../ports/defaults.js';
3
+ export async function runTurn(driver, input, opts = {}) {
4
+ const handler = opts.interactionHandler ?? new AutoCancelInteractionHandler();
5
+ const runner = new SessionRunner('turn', driver.id, 'turn', (snap) => { opts.onSnapshot?.(snap); }, handler);
6
+ if (opts.onSteer)
7
+ opts.onSteer((p, a) => runner.steer(p, a));
8
+ if (opts.signal) {
9
+ if (opts.signal.aborted)
10
+ runner.stop();
11
+ else
12
+ opts.signal.addEventListener('abort', () => runner.stop(), { once: true });
13
+ }
14
+ const result = await runner.run(driver, input, opts.prompt ?? input.prompt, opts.model ?? input.model ?? null, opts.effort ?? input.effort ?? null);
15
+ return { result, snapshot: runner.snapshot };
16
+ }
@@ -0,0 +1,19 @@
1
+ import type { LoomIO, Surface } from '../contracts/surface.js';
2
+ export declare class CliSurface implements Surface {
3
+ private readonly opts;
4
+ readonly id = "cli";
5
+ readonly capabilities: {
6
+ editMessages: boolean;
7
+ images: boolean;
8
+ buttons: boolean;
9
+ };
10
+ private rl?;
11
+ private unsub?;
12
+ constructor(opts?: {
13
+ agent?: string;
14
+ input?: NodeJS.ReadableStream;
15
+ output?: NodeJS.WritableStream;
16
+ });
17
+ start(io: LoomIO): Promise<void>;
18
+ stop(): Promise<void>;
19
+ }
@@ -0,0 +1,65 @@
1
+ import readline from 'node:readline';
2
+ // Simplest possible terminal: stdin lines -> prompts, snapshot text -> stdout.
3
+ // Demonstrates that IM/Web/CLI are all just Terminals over the same LoomIO.
4
+ export class CliSurface {
5
+ opts;
6
+ id = 'cli';
7
+ capabilities = { editMessages: false, images: false, buttons: false };
8
+ rl;
9
+ unsub;
10
+ constructor(opts = {}) {
11
+ this.opts = opts;
12
+ }
13
+ async start(io) {
14
+ const out = this.opts.output || process.stdout;
15
+ let sessionKey; // conversation continuity across lines
16
+ let pendingPromptId = null; // a HITL question awaiting an answer from this terminal
17
+ let lastQueued = 0;
18
+ this.unsub = io.subscribe((key, snap) => {
19
+ if (sessionKey && key !== sessionKey)
20
+ return; // single-conversation terminal
21
+ const interaction = (snap.interactions || [])[0];
22
+ if (interaction && interaction.promptId !== pendingPromptId && snap.phase !== 'done') {
23
+ pendingPromptId = interaction.promptId; // surface HITL; next line answers it
24
+ const q = interaction.questions[interaction.currentIndex ?? 0] || interaction.questions[0];
25
+ out.write(`\n[?] ${q?.text || interaction.title}\n> `);
26
+ return;
27
+ }
28
+ const q = snap.queued?.length ?? 0;
29
+ if (q !== lastQueued && snap.phase !== 'done') {
30
+ lastQueued = q;
31
+ if (q)
32
+ out.write(`\n[queued: ${q}]\n> `);
33
+ }
34
+ if (snap.phase !== 'done')
35
+ return;
36
+ pendingPromptId = null;
37
+ lastQueued = 0;
38
+ out.write(`\n${snap.text || snap.error || '(no output)'}\n> `);
39
+ });
40
+ this.rl = readline.createInterface({ input: this.opts.input || process.stdin, output: out, prompt: '> ' });
41
+ this.rl.prompt();
42
+ this.rl.on('line', (line) => {
43
+ const text = line.trim();
44
+ if (!text) {
45
+ this.rl.prompt();
46
+ return;
47
+ }
48
+ if (pendingPromptId) {
49
+ const pid = pendingPromptId;
50
+ pendingPromptId = null;
51
+ io.interact(pid, 'text', text);
52
+ return;
53
+ } // HITL answer
54
+ if (text === '/models') { // discovery from the terminal (C2)
55
+ void io.listModels(this.opts.agent || io.listAgents()[0] || '').then(ms => out.write(`\n${ms.map(m => m.id).join(', ') || '(no models)'}\n> `));
56
+ return;
57
+ }
58
+ void io.prompt({ prompt: text, agent: this.opts.agent, sessionKey }).then(r => { sessionKey = r.sessionKey; }); // continue the conversation
59
+ });
60
+ }
61
+ async stop() {
62
+ this.unsub?.();
63
+ this.rl?.close();
64
+ }
65
+ }
@@ -0,0 +1,2 @@
1
+ export { WebSurface, type WebSurfaceOptions } from './web.js';
2
+ export { CliSurface } from './cli.js';
@@ -0,0 +1,2 @@
1
+ export { WebSurface } from './web.js';
2
+ export { CliSurface } from './cli.js';
@@ -0,0 +1,39 @@
1
+ import type http from 'node:http';
2
+ import type { LoomIO, Surface, SurfaceCapabilities, TuiHost } from '../contracts/surface.js';
3
+ export interface WebSurfaceOptions {
4
+ port?: number;
5
+ server?: http.Server;
6
+ token?: string;
7
+ name?: string;
8
+ allowTui?: boolean;
9
+ access?: {
10
+ prompt?: boolean;
11
+ tui?: boolean;
12
+ tuiReadonly?: boolean;
13
+ };
14
+ }
15
+ export declare class WebSurface implements Surface {
16
+ private readonly opts;
17
+ readonly id = "web";
18
+ readonly capabilities: SurfaceCapabilities;
19
+ private wss?;
20
+ private io?;
21
+ private host?;
22
+ private unsub?;
23
+ private unsubSessions?;
24
+ private readonly peers;
25
+ constructor(opts?: WebSurfaceOptions);
26
+ get port(): number | null;
27
+ start(io: LoomIO, host?: TuiHost): Promise<void>;
28
+ stop(): Promise<void>;
29
+ private get tuiEnabled();
30
+ private get promptEnabled();
31
+ private killPeerPtys;
32
+ private hostInfo;
33
+ private onConnection;
34
+ private handle;
35
+ private sendFull;
36
+ private broadcastPatch;
37
+ private broadcast;
38
+ private send;
39
+ }