kernelpm 0.1.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 +72 -0
- package/dist/cli.js +243 -0
- package/dist/client.js +137 -0
- package/dist/control.js +184 -0
- package/dist/daemon.js +189 -0
- package/dist/decisions.js +56 -0
- package/dist/files.js +226 -0
- package/dist/opencodeDriver.js +611 -0
- package/dist/protocol.js +12 -0
- package/dist/runtime.js +153 -0
- package/dist/session.js +102 -0
- package/dist/store.js +145 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# kernelpm daemon
|
|
2
|
+
|
|
3
|
+
Keeps **opencode sessions alive on a server** and exposes them over a local control socket. Each
|
|
4
|
+
session is a headless `opencode serve` process driven over its HTTP API; the daemon normalizes
|
|
5
|
+
opencode's structured events into the chat the app renders.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
app ──WebSocket──> broker ──SSH (streamlocal)──> ~/.kernelpm/control.sock
|
|
9
|
+
│
|
|
10
|
+
kernelpm daemon
|
|
11
|
+
│ one `opencode serve` per chat
|
|
12
|
+
opencode serve (headless, allow-all permissions)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **One `opencode serve` per chat**, scoped to the session's working directory. The serve process survives SSH drops and daemon restarts; a restarted daemon reattaches to it by its persisted port.
|
|
16
|
+
- **Auto-yes is structural.** Each serve launches with an inline permission config that allows everything (`OPENCODE_PERMISSION`), so opencode never stops to ask — the unattended equivalent of the old `bypassPermissions`.
|
|
17
|
+
- **Chat = the API.** Prompts go out through `POST /session/:id/prompt_async`; the conversation comes back from the server-sent event stream (`GET /event`) as structured message parts, normalized to message-granular chat events.
|
|
18
|
+
- **Forwarded options.** When a finished turn poses a numbered/multiple-choice question, it's surfaced as a tappable `decision`.
|
|
19
|
+
- **Durable + resumable.** Every chat event is logged under `~/.kernelpm/`; a reconnecting client replays only what it missed. The opencode session id and serve port are persisted, so a dead serve is respawned and its session resumed.
|
|
20
|
+
|
|
21
|
+
## Prerequisites (on each server)
|
|
22
|
+
|
|
23
|
+
- Linux with **opencode** installed and logged in for at least one provider: `opencode auth login` (verify with `opencode auth list`).
|
|
24
|
+
- Node ≥ 18 (to run the daemon).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Published on npm — the simplest path (Node ≥ 18 already on the server):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g kernelpm
|
|
32
|
+
kernelpm daemon start
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
From source instead:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cd daemon
|
|
39
|
+
npm install
|
|
40
|
+
npm run build # -> dist/
|
|
41
|
+
npm link # puts `kernelpm` on PATH (or: npm i -g .)
|
|
42
|
+
kernelpm daemon start
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
State lives in `~/.kernelpm/` (`control.sock`, `token`, `sessions.json`, `sessions/<id>.jsonl`,
|
|
46
|
+
`daemon.log`). Override the location with `KERNELPM_HOME`.
|
|
47
|
+
|
|
48
|
+
## CLI
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
kernelpm daemon start|stop|status|run # run = foreground (used by start)
|
|
52
|
+
kernelpm new <cwd> [--title T] [--model M] # model in provider/model form, e.g. anthropic/claude-...
|
|
53
|
+
kernelpm list
|
|
54
|
+
kernelpm attach <id> # stream the chat; type to send a message
|
|
55
|
+
kernelpm send <id> <text...>
|
|
56
|
+
kernelpm kill <id>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Verification runbook (standalone, over plain SSH)
|
|
60
|
+
|
|
61
|
+
This is the bottom layer of the kernelpm verification — exercise it before wiring the broker/app.
|
|
62
|
+
|
|
63
|
+
1. **Schema sanity** — `kernelpm new ~/some/project`; `kernelpm attach <id>`; type `say hi in one word`. You should see the assistant reply stream in.
|
|
64
|
+
2. **Auto-yes** — ask it to `create a file test.txt with the word ok`. The edit/run happens with no prompt blocking (allow-all permissions).
|
|
65
|
+
3. **Forwarded options** — ask `give me three options for X, numbered, and ask me to choose`. When the turn ends you'll get a `decision`; `kernelpm attach` prints `[1]…[2]…`. (From the app these render as chips.)
|
|
66
|
+
4. **Survives disconnect** — close the SSH login, reconnect, `kernelpm list` → the session is still there. `kernelpm daemon stop && kernelpm daemon start` → it reattaches with history intact. Kill the serve (`pkill -f "opencode serve"`, a reboot stand-in) then `kernelpm daemon start` → a fresh serve is spawned and the opencode session resumed.
|
|
67
|
+
|
|
68
|
+
## Notes
|
|
69
|
+
|
|
70
|
+
- opencode stores its own sessions under `~/.local/share/opencode/`. kernelpm keeps its own normalized event log under `~/.kernelpm/` as the source of truth for the app, and uses the persisted opencode session id only to resume.
|
|
71
|
+
- The per-session serve binds to `127.0.0.1` only. There is no between-process auth on that socket (it relies on local isolation); point sessions at git-tracked / disposable trees given the allow-all permission config.
|
|
72
|
+
- Chat updates are message-granular, not token-by-token — text for a turn appears once the assistant message completes, while tool start/result stream live.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
/**
|
|
38
|
+
* kernelpm CLI — manage the daemon and its sessions over plain SSH, before any
|
|
39
|
+
* broker or app is involved.
|
|
40
|
+
*
|
|
41
|
+
* kernelpm daemon start|stop|status|run
|
|
42
|
+
* kernelpm list
|
|
43
|
+
* kernelpm new <cwd> [--title T] [--model M]
|
|
44
|
+
* kernelpm attach <id> # stream the chat; type to send a message
|
|
45
|
+
* kernelpm send <id> <text...>
|
|
46
|
+
* kernelpm kill <id>
|
|
47
|
+
*/
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const readline = __importStar(require("readline"));
|
|
51
|
+
const store_1 = require("./store");
|
|
52
|
+
const daemon_1 = require("./daemon");
|
|
53
|
+
const runtime_1 = require("./runtime");
|
|
54
|
+
const client_1 = require("./client");
|
|
55
|
+
async function main() {
|
|
56
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
57
|
+
switch (cmd) {
|
|
58
|
+
case 'daemon':
|
|
59
|
+
return daemonCmd(rest);
|
|
60
|
+
case 'list':
|
|
61
|
+
return listCmd();
|
|
62
|
+
case 'new':
|
|
63
|
+
return newCmd(rest);
|
|
64
|
+
case 'attach':
|
|
65
|
+
return attachCmd(rest);
|
|
66
|
+
case 'send':
|
|
67
|
+
return sendCmd(rest);
|
|
68
|
+
case 'kill':
|
|
69
|
+
return killCmd(rest);
|
|
70
|
+
case undefined:
|
|
71
|
+
case 'help':
|
|
72
|
+
case '-h':
|
|
73
|
+
case '--help':
|
|
74
|
+
return printHelp();
|
|
75
|
+
default:
|
|
76
|
+
console.error(`unknown command: ${cmd}`);
|
|
77
|
+
printHelp();
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function daemonCmd(args) {
|
|
82
|
+
switch (args[0]) {
|
|
83
|
+
case 'run':
|
|
84
|
+
return (0, runtime_1.runDaemon)();
|
|
85
|
+
case 'start':
|
|
86
|
+
return (0, runtime_1.startDaemonDetached)();
|
|
87
|
+
case 'stop':
|
|
88
|
+
return (0, runtime_1.stopDaemon)();
|
|
89
|
+
case 'status': {
|
|
90
|
+
if (!(await (0, runtime_1.isDaemonRunning)())) {
|
|
91
|
+
console.log('kernelpm daemon: stopped');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const c = await connect();
|
|
95
|
+
const sessions = await c.list();
|
|
96
|
+
console.log(`kernelpm daemon: running (${sessions.length} session(s))`);
|
|
97
|
+
c.close();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
console.error('usage: kernelpm daemon <run|start|stop|status>');
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function listCmd() {
|
|
106
|
+
const c = await connect();
|
|
107
|
+
const sessions = await c.list();
|
|
108
|
+
if (!sessions.length)
|
|
109
|
+
console.log('(no sessions)');
|
|
110
|
+
for (const s of sessions) {
|
|
111
|
+
console.log(`${s.id} ${pad(s.status, 16)} ${s.title} — ${s.cwd}`);
|
|
112
|
+
}
|
|
113
|
+
c.close();
|
|
114
|
+
}
|
|
115
|
+
async function newCmd(args) {
|
|
116
|
+
const { positionals, flags } = parseArgs(args);
|
|
117
|
+
const cwd = path.resolve(positionals[0] ?? '.');
|
|
118
|
+
const c = await connect();
|
|
119
|
+
const s = await c.create(cwd, flags.title, flags.model);
|
|
120
|
+
console.log(`created ${s.id} (${s.title}) in ${s.cwd}`);
|
|
121
|
+
c.close();
|
|
122
|
+
}
|
|
123
|
+
async function attachCmd(args) {
|
|
124
|
+
const id = args[0];
|
|
125
|
+
if (!id) {
|
|
126
|
+
console.error('usage: kernelpm attach <id>');
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const c = new client_1.ControlClient();
|
|
131
|
+
await c.connect((0, daemon_1.controlSocketPath)(), await readToken(), printPush);
|
|
132
|
+
await c.attach(id);
|
|
133
|
+
console.error(`— attached to ${id}. Type a message and press Enter; Ctrl-C to quit. —`);
|
|
134
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
135
|
+
rl.on('line', (line) => {
|
|
136
|
+
if (line.trim())
|
|
137
|
+
void c.send(id, line);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async function sendCmd(args) {
|
|
141
|
+
const [id, ...words] = args;
|
|
142
|
+
const text = words.join(' ');
|
|
143
|
+
if (!id || !text) {
|
|
144
|
+
console.error('usage: kernelpm send <id> <text...>');
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const c = await connect();
|
|
149
|
+
await c.send(id, text);
|
|
150
|
+
c.close();
|
|
151
|
+
}
|
|
152
|
+
async function killCmd(args) {
|
|
153
|
+
const id = args[0];
|
|
154
|
+
if (!id) {
|
|
155
|
+
console.error('usage: kernelpm kill <id>');
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const c = await connect();
|
|
160
|
+
await c.kill(id);
|
|
161
|
+
console.log(`killed ${id}`);
|
|
162
|
+
c.close();
|
|
163
|
+
}
|
|
164
|
+
/* --------------------------------- helpers --------------------------------- */
|
|
165
|
+
async function connect() {
|
|
166
|
+
const c = new client_1.ControlClient();
|
|
167
|
+
try {
|
|
168
|
+
await c.connect((0, daemon_1.controlSocketPath)(), await readToken());
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
throw new Error('cannot reach the kernelpm daemon — start it with `kernelpm daemon start`');
|
|
172
|
+
}
|
|
173
|
+
return c;
|
|
174
|
+
}
|
|
175
|
+
async function readToken() {
|
|
176
|
+
return (await fs.promises.readFile(path.join((0, store_1.kernelpmHome)(), 'token'), 'utf8')).trim();
|
|
177
|
+
}
|
|
178
|
+
function printPush(msg) {
|
|
179
|
+
if (msg.t === 'event')
|
|
180
|
+
printEvent(msg.event);
|
|
181
|
+
else if (msg.t === 'status')
|
|
182
|
+
console.error(`· ${msg.meta.status}`);
|
|
183
|
+
else if (msg.t === 'decision')
|
|
184
|
+
console.error(`· ${msg.decision.prompt}\n` +
|
|
185
|
+
msg.decision.options.map((o) => ` [${o.id}] ${o.label}`).join('\n'));
|
|
186
|
+
}
|
|
187
|
+
function printEvent(e) {
|
|
188
|
+
const b = e.body;
|
|
189
|
+
switch (b.kind) {
|
|
190
|
+
case 'user_text':
|
|
191
|
+
console.log(`\n> ${b.text}`);
|
|
192
|
+
break;
|
|
193
|
+
case 'assistant_text':
|
|
194
|
+
console.log(`\n${b.text}`);
|
|
195
|
+
break;
|
|
196
|
+
case 'tool_use':
|
|
197
|
+
console.log(` ⚙ ${b.name}(${short(JSON.stringify(b.input ?? ''))})`);
|
|
198
|
+
break;
|
|
199
|
+
case 'tool_result':
|
|
200
|
+
console.log(` ${b.ok ? '✓' : '✗'} ${short(b.summary)}`);
|
|
201
|
+
break;
|
|
202
|
+
case 'system':
|
|
203
|
+
console.log(` · ${b.text}`);
|
|
204
|
+
break;
|
|
205
|
+
case 'assistant_thinking':
|
|
206
|
+
case 'turn_complete':
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function parseArgs(args) {
|
|
211
|
+
const positionals = [];
|
|
212
|
+
const flags = {};
|
|
213
|
+
for (let i = 0; i < args.length; i++) {
|
|
214
|
+
const a = args[i];
|
|
215
|
+
if (a.startsWith('--'))
|
|
216
|
+
flags[a.slice(2)] = args[++i] ?? '';
|
|
217
|
+
else
|
|
218
|
+
positionals.push(a);
|
|
219
|
+
}
|
|
220
|
+
return { positionals, flags };
|
|
221
|
+
}
|
|
222
|
+
function pad(s, n) {
|
|
223
|
+
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
224
|
+
}
|
|
225
|
+
function short(s) {
|
|
226
|
+
return s.length > 80 ? s.slice(0, 80) + '…' : s;
|
|
227
|
+
}
|
|
228
|
+
function printHelp() {
|
|
229
|
+
console.log([
|
|
230
|
+
'kernelpm — keep opencode sessions alive on a server',
|
|
231
|
+
'',
|
|
232
|
+
' kernelpm daemon start|stop|status|run',
|
|
233
|
+
' kernelpm list',
|
|
234
|
+
' kernelpm new <cwd> [--title T] [--model M]',
|
|
235
|
+
' kernelpm attach <id>',
|
|
236
|
+
' kernelpm send <id> <text...>',
|
|
237
|
+
' kernelpm kill <id>',
|
|
238
|
+
].join('\n'));
|
|
239
|
+
}
|
|
240
|
+
main().catch((err) => {
|
|
241
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
});
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ControlClient = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* A minimal control-socket client used by the kernelpm CLI (and handy for
|
|
39
|
+
* tests). Correlates requests to replies by `rid`; everything without a pending
|
|
40
|
+
* `rid` (events, status, decisions) is delivered to the push handler.
|
|
41
|
+
*/
|
|
42
|
+
const net = __importStar(require("net"));
|
|
43
|
+
class ControlClient {
|
|
44
|
+
socket = null;
|
|
45
|
+
buf = '';
|
|
46
|
+
ridSeq = 0;
|
|
47
|
+
pending = new Map();
|
|
48
|
+
onPush = null;
|
|
49
|
+
connect(sockPath, token, onPush) {
|
|
50
|
+
this.onPush = onPush ?? null;
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const socket = net.createConnection(sockPath);
|
|
53
|
+
this.socket = socket;
|
|
54
|
+
socket.setEncoding('utf8');
|
|
55
|
+
socket.once('error', reject);
|
|
56
|
+
socket.on('data', (d) => this.onData(d));
|
|
57
|
+
socket.on('close', () => {
|
|
58
|
+
for (const p of this.pending.values())
|
|
59
|
+
p.reject(new Error('connection closed'));
|
|
60
|
+
this.pending.clear();
|
|
61
|
+
});
|
|
62
|
+
socket.once('connect', () => {
|
|
63
|
+
socket.removeListener('error', reject);
|
|
64
|
+
socket.on('error', () => undefined);
|
|
65
|
+
this.request({ t: 'hello', rid: this.rid(), token })
|
|
66
|
+
.then((m) => (m.t === 'hello.ok' ? resolve() : reject(new Error('handshake failed'))))
|
|
67
|
+
.catch(reject);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
rid() {
|
|
72
|
+
return ++this.ridSeq;
|
|
73
|
+
}
|
|
74
|
+
request(msg) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
if (!this.socket)
|
|
77
|
+
return reject(new Error('not connected'));
|
|
78
|
+
this.pending.set(msg.rid, { resolve, reject });
|
|
79
|
+
this.socket.write(JSON.stringify(msg) + '\n');
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async list() {
|
|
83
|
+
const r = await this.request({ t: 'listSessions', rid: this.rid() });
|
|
84
|
+
return r.t === 'sessions' ? r.sessions : [];
|
|
85
|
+
}
|
|
86
|
+
async create(cwd, title, model) {
|
|
87
|
+
const r = await this.request({ t: 'createSession', rid: this.rid(), cwd, title, model });
|
|
88
|
+
if (r.t !== 'created')
|
|
89
|
+
throw new Error('create failed');
|
|
90
|
+
return r.session;
|
|
91
|
+
}
|
|
92
|
+
attach(id, sinceSeq = 0) {
|
|
93
|
+
return this.request({ t: 'attach', rid: this.rid(), id, sinceSeq });
|
|
94
|
+
}
|
|
95
|
+
send(id, text) {
|
|
96
|
+
return this.request({ t: 'sendMessage', rid: this.rid(), id, text });
|
|
97
|
+
}
|
|
98
|
+
answer(id, decisionId, optionId) {
|
|
99
|
+
return this.request({ t: 'answerDecision', rid: this.rid(), id, decisionId, optionId });
|
|
100
|
+
}
|
|
101
|
+
kill(id) {
|
|
102
|
+
return this.request({ t: 'killSession', rid: this.rid(), id });
|
|
103
|
+
}
|
|
104
|
+
close() {
|
|
105
|
+
this.socket?.end();
|
|
106
|
+
}
|
|
107
|
+
onData(chunk) {
|
|
108
|
+
this.buf += chunk;
|
|
109
|
+
let i;
|
|
110
|
+
while ((i = this.buf.indexOf('\n')) >= 0) {
|
|
111
|
+
const line = this.buf.slice(0, i).trim();
|
|
112
|
+
this.buf = this.buf.slice(i + 1);
|
|
113
|
+
if (!line)
|
|
114
|
+
continue;
|
|
115
|
+
let msg;
|
|
116
|
+
try {
|
|
117
|
+
msg = JSON.parse(line);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const rid = msg.rid;
|
|
123
|
+
const waiter = rid ? this.pending.get(rid) : undefined;
|
|
124
|
+
if (waiter) {
|
|
125
|
+
this.pending.delete(rid);
|
|
126
|
+
if (msg.t === 'error')
|
|
127
|
+
waiter.reject(new Error(msg.message));
|
|
128
|
+
else
|
|
129
|
+
waiter.resolve(msg);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.onPush?.(msg);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.ControlClient = ControlClient;
|
package/dist/control.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createControlServer = createControlServer;
|
|
37
|
+
/**
|
|
38
|
+
* The control socket: a line-delimited JSON server speaking the kernelpm wire
|
|
39
|
+
* protocol over a Unix domain socket. Each connection authenticates with the
|
|
40
|
+
* daemon token (read by the local CLI from disk, or by the broker over SSH),
|
|
41
|
+
* then issues requests and receives pushes for the sessions it attaches to.
|
|
42
|
+
*/
|
|
43
|
+
const net = __importStar(require("net"));
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
48
|
+
const daemon_1 = require("./daemon");
|
|
49
|
+
const files_1 = require("./files");
|
|
50
|
+
class SocketClient {
|
|
51
|
+
socket;
|
|
52
|
+
daemon;
|
|
53
|
+
id = (0, crypto_1.randomUUID)();
|
|
54
|
+
authed = false;
|
|
55
|
+
buf = '';
|
|
56
|
+
constructor(socket, daemon) {
|
|
57
|
+
this.socket = socket;
|
|
58
|
+
this.daemon = daemon;
|
|
59
|
+
socket.setEncoding('utf8');
|
|
60
|
+
socket.on('data', (chunk) => this.onData(chunk));
|
|
61
|
+
socket.on('close', () => this.daemon.dropClient(this));
|
|
62
|
+
socket.on('error', () => {
|
|
63
|
+
/* a dropped client is handled on close */
|
|
64
|
+
});
|
|
65
|
+
daemon.addClient(this);
|
|
66
|
+
}
|
|
67
|
+
send(msg) {
|
|
68
|
+
try {
|
|
69
|
+
this.socket.write(JSON.stringify(msg) + '\n');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* socket closing */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
onData(chunk) {
|
|
76
|
+
this.buf += chunk;
|
|
77
|
+
let idx;
|
|
78
|
+
while ((idx = this.buf.indexOf('\n')) >= 0) {
|
|
79
|
+
const line = this.buf.slice(0, idx).trim();
|
|
80
|
+
this.buf = this.buf.slice(idx + 1);
|
|
81
|
+
if (line)
|
|
82
|
+
void this.dispatch(line);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async dispatch(line) {
|
|
86
|
+
let msg;
|
|
87
|
+
try {
|
|
88
|
+
msg = JSON.parse(line);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
this.send({ t: 'error', message: 'invalid json' });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const rid = msg.rid ?? 0;
|
|
95
|
+
try {
|
|
96
|
+
if (msg.t === 'hello') {
|
|
97
|
+
if (msg.token !== this.daemon.token)
|
|
98
|
+
throw new Error('bad token');
|
|
99
|
+
this.authed = true;
|
|
100
|
+
this.send({ t: 'hello.ok', rid, daemonVersion: daemon_1.DAEMON_VERSION });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!this.authed)
|
|
104
|
+
throw new Error('not authenticated');
|
|
105
|
+
switch (msg.t) {
|
|
106
|
+
case 'listSessions':
|
|
107
|
+
this.send({ t: 'sessions', rid, sessions: this.daemon.list() });
|
|
108
|
+
break;
|
|
109
|
+
case 'createSession': {
|
|
110
|
+
const session = await this.daemon.create(msg.cwd, msg.title, msg.model);
|
|
111
|
+
this.send({ t: 'created', rid, session });
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case 'attach':
|
|
115
|
+
await this.daemon.attachClient(this, msg.id, rid, msg.sinceSeq ?? 0);
|
|
116
|
+
break;
|
|
117
|
+
case 'detach':
|
|
118
|
+
this.daemon.detachClient(this, msg.id);
|
|
119
|
+
this.send({ t: 'ok', rid });
|
|
120
|
+
break;
|
|
121
|
+
case 'sendMessage':
|
|
122
|
+
await this.daemon.send(msg.id, msg.text);
|
|
123
|
+
this.send({ t: 'ok', rid });
|
|
124
|
+
break;
|
|
125
|
+
case 'answerDecision':
|
|
126
|
+
await this.daemon.answer(msg.id, msg.decisionId, msg.optionId);
|
|
127
|
+
this.send({ t: 'ok', rid });
|
|
128
|
+
break;
|
|
129
|
+
case 'killSession':
|
|
130
|
+
await this.daemon.kill(msg.id);
|
|
131
|
+
this.send({ t: 'ok', rid });
|
|
132
|
+
break;
|
|
133
|
+
case 'restartSession':
|
|
134
|
+
await this.daemon.restart(msg.id);
|
|
135
|
+
this.send({ t: 'ok', rid });
|
|
136
|
+
break;
|
|
137
|
+
case 'listDir': {
|
|
138
|
+
const dir = resolveDir(msg.path);
|
|
139
|
+
this.send({ t: 'dir', rid, path: dir, entries: await readDir(dir) });
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'getTree': {
|
|
143
|
+
const cwd = resolveDir(msg.cwd);
|
|
144
|
+
this.send({ t: 'tree', rid, cwd, tree: await (0, files_1.buildTree)(cwd) });
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'readFile': {
|
|
148
|
+
const cwd = resolveDir(msg.cwd);
|
|
149
|
+
const r = await (0, files_1.readProjectFile)(cwd, msg.path);
|
|
150
|
+
this.send({ t: 'file', rid, path: msg.path, git: r.git, lines: r.lines, diff: r.diff });
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
throw new Error(`unknown message: ${msg.t}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
this.send({ t: 'error', rid, message: errorMessage(err) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function createControlServer(daemon) {
|
|
163
|
+
return net.createServer((socket) => {
|
|
164
|
+
new SocketClient(socket, daemon);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function errorMessage(err) {
|
|
168
|
+
return err instanceof Error ? err.message : String(err);
|
|
169
|
+
}
|
|
170
|
+
/** Expand '~' / empty to the daemon user's home; otherwise normalize absolute. */
|
|
171
|
+
function resolveDir(p) {
|
|
172
|
+
if (!p || p === '~')
|
|
173
|
+
return os.homedir();
|
|
174
|
+
if (p === '~/' || p.startsWith('~/'))
|
|
175
|
+
return path.join(os.homedir(), p.slice(2));
|
|
176
|
+
return path.resolve(p);
|
|
177
|
+
}
|
|
178
|
+
/** List a directory, folders first then files, alphabetically within each group. */
|
|
179
|
+
async function readDir(dir) {
|
|
180
|
+
const dirents = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
181
|
+
return dirents
|
|
182
|
+
.map((d) => ({ name: d.name, dir: d.isDirectory() }))
|
|
183
|
+
.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1));
|
|
184
|
+
}
|