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,244 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import { PROTOCOL_VERSION, isClientMessage, } from '../protocol/index.js';
3
+ import { randomUUID } from 'node:crypto';
4
+ const SUBSCRIBE_ALL = '*';
5
+ // The built-in Web/remote terminal: a ws host speaking the UniversalSnapshot wire
6
+ // protocol over LoomIO. Any pikichannel client (incl. the existing pikiloom dashboard
7
+ // SPA) can connect: hello -> subscribe -> receive `session` patches -> prompt/stop/steer.
8
+ export class WebSurface {
9
+ opts;
10
+ id = 'web';
11
+ capabilities = { tunnel: true, images: true, buttons: true, editMessages: true };
12
+ wss;
13
+ io;
14
+ host;
15
+ unsub;
16
+ unsubSessions;
17
+ peers = new Set();
18
+ constructor(opts = {}) {
19
+ this.opts = opts;
20
+ }
21
+ get port() {
22
+ const a = this.wss?.address();
23
+ return a && typeof a === 'object' ? a.port : null;
24
+ }
25
+ async start(io, host) {
26
+ this.io = io;
27
+ this.host = host;
28
+ this.wss = this.opts.server
29
+ ? new WebSocketServer({ server: this.opts.server })
30
+ : new WebSocketServer({ port: this.opts.port ?? 0 });
31
+ if (!this.opts.server) {
32
+ await new Promise((resolve, reject) => {
33
+ this.wss.once('listening', resolve);
34
+ this.wss.once('error', reject);
35
+ });
36
+ }
37
+ this.wss.on('connection', (ws) => this.onConnection(ws));
38
+ this.unsub = io.subscribe((key, _snap, patch, seq) => this.broadcastPatch(key, patch, seq));
39
+ this.unsubSessions = io.onSessionsChanged((sessions) => this.broadcast({ type: 'sessions', sessions }, true));
40
+ }
41
+ async stop() {
42
+ this.unsub?.();
43
+ this.unsubSessions?.();
44
+ for (const p of this.peers) {
45
+ this.killPeerPtys(p);
46
+ try {
47
+ p.ws.close();
48
+ }
49
+ catch { /* ignore */ }
50
+ }
51
+ this.peers.clear();
52
+ await new Promise((resolve) => { if (this.wss)
53
+ this.wss.close(() => resolve());
54
+ else
55
+ resolve(); });
56
+ }
57
+ get tuiEnabled() { return !!this.host && this.opts.allowTui !== false && this.opts.access?.tui !== false; }
58
+ get promptEnabled() { return this.opts.access?.prompt !== false; }
59
+ killPeerPtys(peer) {
60
+ for (const b of peer.ptys.values()) {
61
+ try {
62
+ b.kill();
63
+ }
64
+ catch { /* ignore */ }
65
+ }
66
+ peer.ptys.clear();
67
+ }
68
+ hostInfo() {
69
+ const capabilities = ['subscribe-all', 'artifacts', 'history', 'catalog'];
70
+ if (this.promptEnabled)
71
+ capabilities.push('prompt', 'stop', 'steer', 'interact');
72
+ if (this.tuiEnabled)
73
+ capabilities.push('tui');
74
+ return { name: this.opts.name || 'loom', version: '0.1.0', transport: 'websocket', capabilities, authRequired: !!this.opts.token };
75
+ }
76
+ onConnection(ws) {
77
+ const peer = { ws, subs: new Set(), authed: !this.opts.token, ptys: new Map() };
78
+ this.peers.add(peer);
79
+ ws.on('message', (data) => {
80
+ let msg;
81
+ try {
82
+ msg = JSON.parse(data.toString());
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ if (!isClientMessage(msg))
88
+ return;
89
+ this.handle(peer, msg).catch((err) => this.send(peer, { type: 'error', message: err?.message || String(err) }));
90
+ });
91
+ const cleanup = () => { this.killPeerPtys(peer); this.peers.delete(peer); };
92
+ ws.on('close', cleanup);
93
+ ws.on('error', cleanup);
94
+ }
95
+ async handle(peer, msg) {
96
+ if (msg.type === 'ping') {
97
+ this.send(peer, { type: 'pong', t: msg.t });
98
+ return;
99
+ }
100
+ if (!peer.authed) {
101
+ if (msg.type !== 'hello') {
102
+ this.send(peer, { type: 'error', message: 'not authenticated', code: 'auth' });
103
+ return;
104
+ }
105
+ if (this.opts.token && msg.token !== this.opts.token) {
106
+ this.send(peer, { type: 'error', message: 'unauthorized', code: 'auth' });
107
+ peer.ws.close();
108
+ return;
109
+ }
110
+ peer.authed = true;
111
+ }
112
+ if (msg.type === 'hello') {
113
+ this.send(peer, { type: 'welcome', v: PROTOCOL_VERSION, host: this.hostInfo(), sessions: this.io.listSessions() });
114
+ return;
115
+ }
116
+ switch (msg.type) {
117
+ case 'subscribe': {
118
+ peer.subs.add(msg.sessionKey);
119
+ if (msg.sessionKey === SUBSCRIBE_ALL)
120
+ for (const m of this.io.listSessions())
121
+ this.sendFull(peer, m.sessionKey);
122
+ else
123
+ this.sendFull(peer, msg.sessionKey);
124
+ return;
125
+ }
126
+ case 'unsubscribe':
127
+ peer.subs.delete(msg.sessionKey);
128
+ return;
129
+ case 'getSnapshot':
130
+ this.sendFull(peer, msg.sessionKey);
131
+ return;
132
+ case 'listSessions':
133
+ this.send(peer, { type: 'sessions', sessions: this.io.listSessions() });
134
+ return;
135
+ case 'getHistory': {
136
+ const turns = await this.io.getHistory(msg.sessionKey);
137
+ this.send(peer, { type: 'history', sessionKey: msg.sessionKey, turns, ref: msg.ref });
138
+ return;
139
+ }
140
+ case 'getCatalog': {
141
+ const agent = msg.agent;
142
+ const agents = this.io.listAgentInfo();
143
+ const models = agent ? await this.io.listModels(agent) : [];
144
+ const effort = agent ? await this.io.listEffort(agent, msg.model ?? null) : [];
145
+ const tools = agent ? await this.io.listTools(agent, msg.workdir) : [];
146
+ const skills = agent ? await this.io.listSkills(agent, msg.workdir) : [];
147
+ this.send(peer, { type: 'catalog', agents, agent, models, effort, tools, skills, ref: msg.ref });
148
+ return;
149
+ }
150
+ case 'prompt': {
151
+ if (!this.promptEnabled) {
152
+ this.send(peer, { type: 'error', message: 'prompt not permitted (read-only)', code: 'access', clientRef: msg.clientRef });
153
+ return;
154
+ }
155
+ const res = await this.io.prompt({
156
+ sessionKey: msg.sessionKey, prompt: msg.prompt, agent: msg.agent, workdir: msg.workdir,
157
+ model: msg.model, effort: msg.effort, attachments: msg.attachments,
158
+ }).catch((e) => ({ error: e?.message || 'prompt failed' }));
159
+ if (res.error) {
160
+ this.send(peer, { type: 'error', message: res.error, clientRef: msg.clientRef });
161
+ return;
162
+ }
163
+ peer.subs.add(res.sessionKey);
164
+ this.send(peer, { type: 'accepted', sessionKey: res.sessionKey, taskId: res.taskId, clientRef: msg.clientRef });
165
+ this.sendFull(peer, res.sessionKey);
166
+ return;
167
+ }
168
+ case 'stop':
169
+ this.io.stop(msg.sessionKey);
170
+ return;
171
+ case 'steer':
172
+ await this.io.steer(msg.taskId, msg.prompt);
173
+ return;
174
+ case 'interact':
175
+ this.io.interact(msg.promptId, msg.action, msg.value);
176
+ return;
177
+ // ---- Lane R: raw PTY (TUI passthrough) ----
178
+ case 'openTui': {
179
+ if (!this.tuiEnabled || !this.host) {
180
+ this.send(peer, { type: 'error', message: 'TUI not available', code: 'tui', clientRef: msg.ref });
181
+ return;
182
+ }
183
+ let bridge;
184
+ try {
185
+ bridge = await this.host.openTui({ agent: msg.agent, workdir: msg.workdir, model: msg.model, sessionId: msg.sessionId, cols: msg.cols, rows: msg.rows });
186
+ }
187
+ catch (e) {
188
+ this.send(peer, { type: 'error', message: e?.message || 'openTui failed', code: 'tui', clientRef: msg.ref });
189
+ return;
190
+ }
191
+ const tuiId = randomUUID();
192
+ peer.ptys.set(tuiId, bridge);
193
+ bridge.onData((data) => this.send(peer, { type: 'tuiData', tuiId, data }));
194
+ bridge.onExit((e) => { peer.ptys.delete(tuiId); this.send(peer, { type: 'tuiExit', tuiId, exitCode: e.exitCode, signal: e.signal }); });
195
+ this.send(peer, { type: 'tuiOpened', tuiId, ref: msg.ref });
196
+ return;
197
+ }
198
+ case 'tuiInput':
199
+ if (!this.opts.access?.tuiReadonly)
200
+ peer.ptys.get(msg.tuiId)?.write(msg.data);
201
+ return; // readonly = spectator
202
+ case 'tuiResize':
203
+ peer.ptys.get(msg.tuiId)?.resize(msg.cols, msg.rows);
204
+ return;
205
+ case 'tuiClose': {
206
+ const b = peer.ptys.get(msg.tuiId);
207
+ if (b) {
208
+ peer.ptys.delete(msg.tuiId);
209
+ try {
210
+ b.kill();
211
+ }
212
+ catch { /* ignore */ }
213
+ }
214
+ return;
215
+ }
216
+ }
217
+ }
218
+ sendFull(peer, sessionKey) {
219
+ const snap = this.io.getSnapshot(sessionKey);
220
+ if (!snap)
221
+ return;
222
+ this.send(peer, { type: 'session', sessionKey, seq: snap.seq, patch: { full: snap.snapshot } });
223
+ }
224
+ broadcastPatch(sessionKey, patch, seq) {
225
+ const frame = { type: 'session', sessionKey, seq, patch };
226
+ for (const peer of this.peers) {
227
+ if (peer.authed && (peer.subs.has(SUBSCRIBE_ALL) || peer.subs.has(sessionKey)))
228
+ this.send(peer, frame);
229
+ }
230
+ }
231
+ broadcast(frame, authedOnly = false) {
232
+ for (const peer of this.peers)
233
+ if (!authedOnly || peer.authed)
234
+ this.send(peer, frame);
235
+ }
236
+ send(peer, frame) {
237
+ if (peer.ws.readyState !== WebSocket.OPEN)
238
+ return;
239
+ try {
240
+ peer.ws.send(JSON.stringify(frame));
241
+ }
242
+ catch { /* ignore */ }
243
+ }
244
+ }