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.
@@ -0,0 +1,153 @@
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.runDaemon = runDaemon;
37
+ exports.startDaemonDetached = startDaemonDetached;
38
+ exports.stopDaemon = stopDaemon;
39
+ exports.isDaemonRunning = isDaemonRunning;
40
+ /**
41
+ * Daemon lifecycle: run in the foreground, start detached (surviving SSH
42
+ * logout), stop, and probe whether it is up. The detached child re-invokes this
43
+ * same program with `daemon run`, preserving any loader flags (so it works both
44
+ * compiled and under tsx), with stdio redirected to daemon.log.
45
+ */
46
+ const fs = __importStar(require("fs"));
47
+ const net = __importStar(require("net"));
48
+ const path = __importStar(require("path"));
49
+ const child_process_1 = require("child_process");
50
+ const store_1 = require("./store");
51
+ const daemon_1 = require("./daemon");
52
+ const control_1 = require("./control");
53
+ async function runDaemon() {
54
+ const home = (0, store_1.kernelpmHome)();
55
+ await fs.promises.mkdir(home, { recursive: true });
56
+ const token = await (0, daemon_1.ensureToken)(home);
57
+ const daemon = new daemon_1.Daemon(token);
58
+ await daemon.init();
59
+ const sockPath = (0, daemon_1.controlSocketPath)();
60
+ await safeUnlink(sockPath);
61
+ const server = (0, control_1.createControlServer)(daemon);
62
+ await new Promise((resolve, reject) => {
63
+ server.once('error', reject);
64
+ server.listen(sockPath, () => resolve());
65
+ });
66
+ await fs.promises.writeFile((0, daemon_1.pidPath)(), String(process.pid), 'utf8');
67
+ console.log(`[kernelpm] daemon ${process.pid} listening on ${sockPath}`);
68
+ const shutdown = () => {
69
+ server.close();
70
+ safeUnlinkSync(sockPath);
71
+ safeUnlinkSync((0, daemon_1.pidPath)());
72
+ process.exit(0);
73
+ };
74
+ process.on('SIGTERM', shutdown);
75
+ process.on('SIGINT', shutdown);
76
+ }
77
+ async function startDaemonDetached() {
78
+ const home = (0, store_1.kernelpmHome)();
79
+ await fs.promises.mkdir(home, { recursive: true });
80
+ if (await isDaemonRunning()) {
81
+ console.log('kernelpm daemon already running');
82
+ return;
83
+ }
84
+ const logFd = fs.openSync(path.join(home, 'daemon.log'), 'a');
85
+ const child = (0, child_process_1.spawn)(process.execPath, [...process.execArgv, process.argv[1], 'daemon', 'run'], { detached: true, stdio: ['ignore', logFd, logFd], env: process.env });
86
+ child.unref();
87
+ const up = await waitForSocket((0, daemon_1.controlSocketPath)(), 8000);
88
+ console.log(up
89
+ ? `kernelpm daemon started (pid ${child.pid})`
90
+ : 'kernelpm daemon did not come up in time — see daemon.log');
91
+ }
92
+ async function stopDaemon() {
93
+ let pid = 0;
94
+ try {
95
+ pid = Number((await fs.promises.readFile((0, daemon_1.pidPath)(), 'utf8')).trim());
96
+ }
97
+ catch {
98
+ /* no pid file */
99
+ }
100
+ if (pid) {
101
+ try {
102
+ process.kill(pid, 'SIGTERM');
103
+ console.log(`stopped kernelpm daemon ${pid}`);
104
+ }
105
+ catch {
106
+ console.log('kernelpm daemon not running');
107
+ }
108
+ }
109
+ else {
110
+ console.log('kernelpm daemon not running');
111
+ }
112
+ await safeUnlink((0, daemon_1.controlSocketPath)());
113
+ }
114
+ function isDaemonRunning() {
115
+ return new Promise((resolve) => {
116
+ const socket = net.createConnection((0, daemon_1.controlSocketPath)());
117
+ const finish = (v) => {
118
+ socket.destroy();
119
+ resolve(v);
120
+ };
121
+ socket.once('connect', () => finish(true));
122
+ socket.once('error', () => finish(false));
123
+ setTimeout(() => finish(false), 1000);
124
+ });
125
+ }
126
+ async function waitForSocket(_p, timeoutMs) {
127
+ const start = Date.now();
128
+ while (Date.now() - start < timeoutMs) {
129
+ if (await isDaemonRunning())
130
+ return true;
131
+ await delay(150);
132
+ }
133
+ return false;
134
+ }
135
+ function delay(ms) {
136
+ return new Promise((r) => setTimeout(r, ms));
137
+ }
138
+ async function safeUnlink(p) {
139
+ try {
140
+ await fs.promises.unlink(p);
141
+ }
142
+ catch {
143
+ /* ignore */
144
+ }
145
+ }
146
+ function safeUnlinkSync(p) {
147
+ try {
148
+ fs.unlinkSync(p);
149
+ }
150
+ catch {
151
+ /* ignore */
152
+ }
153
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Session = void 0;
4
+ const opencodeDriver_1 = require("./opencodeDriver");
5
+ class Session {
6
+ store;
7
+ meta;
8
+ push;
9
+ driver;
10
+ chain = Promise.resolve();
11
+ constructor(store, meta, push, opts = {}) {
12
+ this.store = store;
13
+ this.meta = meta;
14
+ this.push = push;
15
+ this.driver = new opencodeDriver_1.ServeOpencodeDriver(meta.id, meta.cwd, meta.title, opts, {
16
+ onEvents: (events) => this.onEvents(events),
17
+ onTitle: (title) => this.onTitle(title),
18
+ onStatus: (status) => this.onStatus(status),
19
+ onDecision: (decision) => this.onDecision(decision),
20
+ onServeInfo: (patch) => this.onServeInfo(patch),
21
+ }, { opencodeId: meta.opencodeId, opencodePort: meta.opencodePort });
22
+ }
23
+ start(resume) {
24
+ return this.driver.start(resume);
25
+ }
26
+ sendMessage(text) {
27
+ return this.enqueue(async () => {
28
+ if (this.meta.pendingDecision)
29
+ await this.clearDecision();
30
+ await this.driver.sendMessage(text);
31
+ });
32
+ }
33
+ answer(decisionId, optionId) {
34
+ return this.enqueue(async () => {
35
+ const d = this.meta.pendingDecision;
36
+ if (!d || d.id !== decisionId)
37
+ throw new Error('no matching pending decision');
38
+ const option = d.options.find((o) => o.id === optionId);
39
+ if (!option)
40
+ throw new Error('no such option');
41
+ await this.appendBody({ kind: 'user_text', text: option.label }); // show the pick
42
+ await this.clearDecision();
43
+ await this.driver.answer(option);
44
+ });
45
+ }
46
+ kill() {
47
+ return this.driver.kill();
48
+ }
49
+ restart() {
50
+ return this.driver.start(true);
51
+ }
52
+ detach() {
53
+ this.driver.detach();
54
+ }
55
+ /* ------------------------------ driver glue ------------------------------- */
56
+ onEvents(events) {
57
+ for (const body of events)
58
+ void this.enqueue(() => this.appendBody(body));
59
+ }
60
+ onTitle(title) {
61
+ void this.enqueue(() => this.patchMeta({ title }));
62
+ }
63
+ onStatus(status) {
64
+ void this.enqueue(() => this.patchMeta({ status }));
65
+ }
66
+ onDecision(decision) {
67
+ void this.enqueue(async () => {
68
+ await this.patchMeta({ pendingDecision: decision, status: 'awaiting_decision' });
69
+ this.push({ t: 'decision', id: this.meta.id, decision });
70
+ });
71
+ }
72
+ onServeInfo(patch) {
73
+ void this.enqueue(() => this.patchMeta(patch));
74
+ }
75
+ /* ------------------------------- primitives ------------------------------- */
76
+ async appendBody(body) {
77
+ const event = await this.store.append(this.meta.id, body);
78
+ if (!event)
79
+ return;
80
+ const m = this.store.get(this.meta.id);
81
+ if (m)
82
+ this.meta = m;
83
+ this.push({ t: 'event', id: this.meta.id, event });
84
+ }
85
+ async patchMeta(patch) {
86
+ const m = await this.store.patch(this.meta.id, patch);
87
+ if (m) {
88
+ this.meta = m;
89
+ this.push({ t: 'status', id: m.id, meta: m });
90
+ }
91
+ }
92
+ clearDecision() {
93
+ return this.patchMeta({ pendingDecision: null });
94
+ }
95
+ /** Serialize all store writes + pushes for this session. */
96
+ enqueue(fn) {
97
+ const next = this.chain.then(fn, fn);
98
+ this.chain = next.catch(() => undefined);
99
+ return next;
100
+ }
101
+ }
102
+ exports.Session = Session;
package/dist/store.js ADDED
@@ -0,0 +1,145 @@
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.Store = void 0;
37
+ exports.kernelpmHome = kernelpmHome;
38
+ /**
39
+ * Durable daemon state.
40
+ *
41
+ * Session metadata lives in `<KERNELPM_HOME>/sessions.json`; each session's
42
+ * normalized chat is an append-only log at `sessions/<id>.jsonl`. Together they
43
+ * let a reconnecting client (or a restarted daemon) rebuild every chat exactly,
44
+ * replaying only what a client missed via `readEvents(id, sinceSeq)`.
45
+ */
46
+ const fs = __importStar(require("fs"));
47
+ const os = __importStar(require("os"));
48
+ const path = __importStar(require("path"));
49
+ function kernelpmHome() {
50
+ return process.env.KERNELPM_HOME || path.join(os.homedir(), '.kernelpm');
51
+ }
52
+ class Store {
53
+ home = kernelpmHome();
54
+ sessionsDir = path.join(this.home, 'sessions');
55
+ statePath = path.join(this.home, 'sessions.json');
56
+ meta = new Map();
57
+ appendChains = new Map();
58
+ async init() {
59
+ await fs.promises.mkdir(this.sessionsDir, { recursive: true });
60
+ try {
61
+ const parsed = JSON.parse(await fs.promises.readFile(this.statePath, 'utf8'));
62
+ for (const [id, m] of Object.entries(parsed.sessions ?? {}))
63
+ this.meta.set(id, m);
64
+ }
65
+ catch {
66
+ /* fresh install */
67
+ }
68
+ }
69
+ list() {
70
+ return [...this.meta.values()].sort((a, b) => b.updatedAt - a.updatedAt);
71
+ }
72
+ get(id) {
73
+ return this.meta.get(id);
74
+ }
75
+ async upsert(meta) {
76
+ this.meta.set(meta.id, meta);
77
+ await this.persistMeta();
78
+ }
79
+ async patch(id, patch) {
80
+ const cur = this.meta.get(id);
81
+ if (!cur)
82
+ return undefined;
83
+ const next = { ...cur, ...patch, updatedAt: Date.now() };
84
+ this.meta.set(id, next);
85
+ await this.persistMeta();
86
+ return next;
87
+ }
88
+ /** Append one event, assigning the next seq. Persists the log line + meta. */
89
+ async append(id, body) {
90
+ const cur = this.meta.get(id);
91
+ if (!cur)
92
+ return undefined;
93
+ const event = { seq: cur.lastSeq + 1, ts: Date.now(), body };
94
+ cur.lastSeq = event.seq;
95
+ cur.updatedAt = event.ts;
96
+ await this.serializeAppend(id, () => fs.promises.appendFile(this.logPath(id), JSON.stringify(event) + '\n', 'utf8'));
97
+ await this.persistMeta();
98
+ return event;
99
+ }
100
+ async readEvents(id, sinceSeq = 0) {
101
+ let raw;
102
+ try {
103
+ raw = await fs.promises.readFile(this.logPath(id), 'utf8');
104
+ }
105
+ catch {
106
+ return [];
107
+ }
108
+ const out = [];
109
+ for (const line of raw.split('\n')) {
110
+ if (!line)
111
+ continue;
112
+ try {
113
+ const ev = JSON.parse(line);
114
+ if (ev.seq > sinceSeq)
115
+ out.push(ev);
116
+ }
117
+ catch {
118
+ /* skip corrupt line */
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ async remove(id) {
124
+ this.meta.delete(id);
125
+ await this.persistMeta();
126
+ await fs.promises.rm(this.logPath(id), { force: true });
127
+ }
128
+ logPath(id) {
129
+ return path.join(this.sessionsDir, `${id}.jsonl`);
130
+ }
131
+ /** Serialize appends per session so log lines never interleave. */
132
+ serializeAppend(id, fn) {
133
+ const prev = this.appendChains.get(id) ?? Promise.resolve();
134
+ const next = prev.then(fn, fn);
135
+ this.appendChains.set(id, next.catch(() => undefined));
136
+ return next;
137
+ }
138
+ async persistMeta() {
139
+ const state = { version: 1, sessions: Object.fromEntries(this.meta) };
140
+ const tmp = this.statePath + '.tmp';
141
+ await fs.promises.writeFile(tmp, JSON.stringify(state, null, 2), 'utf8');
142
+ await fs.promises.rename(tmp, this.statePath);
143
+ }
144
+ }
145
+ exports.Store = Store;
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "kernelpm",
3
+ "version": "0.1.0",
4
+ "description": "kernelpm daemon — keeps opencode sessions alive on a server (via `opencode serve`) and exposes them over a local control socket.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "kernelpm": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "dev": "tsx src/cli.ts",
15
+ "typecheck": "tsc -p tsconfig.json --noEmit",
16
+ "test": "tsx test/run.ts",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.10.2",
24
+ "tsx": "^4.19.2",
25
+ "typescript": "^5.3.3"
26
+ }
27
+ }