@gramatr/mcp 0.11.12 → 0.11.15
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/dist/bin/gramatr-mcp.d.ts +3 -0
- package/dist/bin/gramatr-mcp.d.ts.map +1 -1
- package/dist/bin/gramatr-mcp.js +38 -0
- package/dist/bin/gramatr-mcp.js.map +1 -1
- package/dist/bin/login.d.ts.map +1 -1
- package/dist/bin/login.js +2 -0
- package/dist/bin/login.js.map +1 -1
- package/dist/config-runtime.d.ts +12 -0
- package/dist/config-runtime.d.ts.map +1 -1
- package/dist/config-runtime.js +18 -0
- package/dist/config-runtime.js.map +1 -1
- package/dist/daemon/db-path.d.ts +20 -0
- package/dist/daemon/db-path.d.ts.map +1 -0
- package/dist/daemon/db-path.js +28 -0
- package/dist/daemon/db-path.js.map +1 -0
- package/dist/daemon/http-server.d.ts +29 -0
- package/dist/daemon/http-server.d.ts.map +1 -0
- package/dist/daemon/http-server.js +70 -0
- package/dist/daemon/http-server.js.map +1 -0
- package/dist/daemon/index.d.ts +13 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +126 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/ipc-protocol.d.ts +56 -0
- package/dist/daemon/ipc-protocol.d.ts.map +1 -0
- package/dist/daemon/ipc-protocol.js +10 -0
- package/dist/daemon/ipc-protocol.js.map +1 -0
- package/dist/daemon/project-cache.d.ts +63 -0
- package/dist/daemon/project-cache.d.ts.map +1 -0
- package/dist/daemon/project-cache.js +139 -0
- package/dist/daemon/project-cache.js.map +1 -0
- package/dist/daemon/server.d.ts +22 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +322 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/session-registry.d.ts +26 -0
- package/dist/daemon/session-registry.d.ts.map +1 -0
- package/dist/daemon/session-registry.js +42 -0
- package/dist/daemon/session-registry.js.map +1 -0
- package/dist/daemon/sqlite-owner.d.ts +55 -0
- package/dist/daemon/sqlite-owner.d.ts.map +1 -0
- package/dist/daemon/sqlite-owner.js +133 -0
- package/dist/daemon/sqlite-owner.js.map +1 -0
- package/dist/daemon/startup.d.ts +51 -0
- package/dist/daemon/startup.d.ts.map +1 -0
- package/dist/daemon/startup.js +182 -0
- package/dist/daemon/startup.js.map +1 -0
- package/dist/hooks/generated/hook-timeouts.d.ts +3 -1
- package/dist/hooks/generated/hook-timeouts.d.ts.map +1 -1
- package/dist/hooks/generated/hook-timeouts.js +3 -1
- package/dist/hooks/generated/hook-timeouts.js.map +1 -1
- package/dist/hooks/lib/hook-state.d.ts +36 -2
- package/dist/hooks/lib/hook-state.d.ts.map +1 -1
- package/dist/hooks/lib/hook-state.js +119 -5
- package/dist/hooks/lib/hook-state.js.map +1 -1
- package/dist/hooks/session-end.d.ts.map +1 -1
- package/dist/hooks/session-end.js +48 -21
- package/dist/hooks/session-end.js.map +1 -1
- package/dist/hooks/session-start.d.ts.map +1 -1
- package/dist/hooks/session-start.js +42 -23
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/proxy/local-client.d.ts +30 -20
- package/dist/proxy/local-client.d.ts.map +1 -1
- package/dist/proxy/local-client.js +185 -50
- package/dist/proxy/local-client.js.map +1 -1
- package/dist/proxy/remote-client.d.ts.map +1 -1
- package/dist/proxy/remote-client.js +13 -4
- package/dist/proxy/remote-client.js.map +1 -1
- package/dist/server/auth.d.ts +13 -4
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +154 -20
- package/dist/server/auth.js.map +1 -1
- package/dist/setup/generated/platform-hooks.d.ts +1 -1
- package/dist/setup/generated/platform-hooks.d.ts.map +1 -1
- package/dist/setup/generated/platform-hooks.js +1 -5
- package/dist/setup/generated/platform-hooks.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server.ts — Unix socket IPC server for the gramatr daemon.
|
|
3
|
+
*
|
|
4
|
+
* Accepts newline-delimited JSON-RPC 2.0 connections. Each connection
|
|
5
|
+
* can send one request and receives one response. Connections are
|
|
6
|
+
* short-lived (hook processes are short-lived); no multiplexing needed.
|
|
7
|
+
*
|
|
8
|
+
* Error codes:
|
|
9
|
+
* -32600 Invalid request
|
|
10
|
+
* -32601 Method not found
|
|
11
|
+
* -32603 Internal error
|
|
12
|
+
*/
|
|
13
|
+
import { createServer } from 'node:net';
|
|
14
|
+
import { createInterface } from 'node:readline';
|
|
15
|
+
import { callRemoteTool } from '../proxy/remote-client.js';
|
|
16
|
+
import { sessionRegistry } from './session-registry.js';
|
|
17
|
+
import { projectCache } from './project-cache.js';
|
|
18
|
+
import { sqliteOwner } from './sqlite-owner.js';
|
|
19
|
+
import { VERSION } from '../hooks/lib/version.js';
|
|
20
|
+
const contextStore = new Map();
|
|
21
|
+
const activeSockets = new Set();
|
|
22
|
+
let _server = null;
|
|
23
|
+
// ── db/query forwarding ───────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Route read-only SQLite operations through the daemon's owned connection.
|
|
26
|
+
*
|
|
27
|
+
* Implements the same queries as hook-state.ts without importing that module
|
|
28
|
+
* (which would open a competing DB connection). Results are returned as plain
|
|
29
|
+
* JSON-serialisable objects so they can be sent over the IPC socket.
|
|
30
|
+
*
|
|
31
|
+
* Hook processes that set GRAMATR_USE_DAEMON_DB=1 (detected via daemon.active
|
|
32
|
+
* sentinel) may call this method to avoid opening the file directly.
|
|
33
|
+
* Sprint 3 will wire the async IPC call path in hook-state.ts.
|
|
34
|
+
*/
|
|
35
|
+
function handleDbQuery(operation, args) {
|
|
36
|
+
const db = sqliteOwner.getDb();
|
|
37
|
+
if (!db)
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
switch (operation) {
|
|
41
|
+
case 'getSessionContext': {
|
|
42
|
+
const sessionId = args['session_id'];
|
|
43
|
+
if (sessionId && sessionId !== 'unknown') {
|
|
44
|
+
const scoped = db
|
|
45
|
+
.prepare('SELECT * FROM session_context WHERE session_id = ? ORDER BY updated_at DESC LIMIT 1')
|
|
46
|
+
.get(sessionId);
|
|
47
|
+
if (scoped)
|
|
48
|
+
return scoped;
|
|
49
|
+
}
|
|
50
|
+
return (db
|
|
51
|
+
.prepare('SELECT * FROM session_context ORDER BY updated_at DESC LIMIT 1')
|
|
52
|
+
.get() ?? null);
|
|
53
|
+
}
|
|
54
|
+
case 'getLastSessionForProject': {
|
|
55
|
+
const projectId = args['project_id'];
|
|
56
|
+
if (!projectId)
|
|
57
|
+
return null;
|
|
58
|
+
return (db
|
|
59
|
+
.prepare(`
|
|
60
|
+
SELECT session_id, interaction_id, entity_id, client_type, agent_name, ended_at
|
|
61
|
+
FROM session_log
|
|
62
|
+
WHERE project_id = ?
|
|
63
|
+
ORDER BY id DESC
|
|
64
|
+
LIMIT 1
|
|
65
|
+
`)
|
|
66
|
+
.get(projectId) ?? null);
|
|
67
|
+
}
|
|
68
|
+
case 'getLocalProjectByDirectory': {
|
|
69
|
+
const directory = args['directory'];
|
|
70
|
+
if (!directory)
|
|
71
|
+
return null;
|
|
72
|
+
return (db
|
|
73
|
+
.prepare('SELECT * FROM projects WHERE directory = ? ORDER BY updated_at DESC LIMIT 1')
|
|
74
|
+
.get(directory) ?? null);
|
|
75
|
+
}
|
|
76
|
+
default: {
|
|
77
|
+
throw Object.assign(new Error(`db/query: unknown operation: ${String(operation)}`), { code: -32601 });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (err !== null && typeof err === 'object' && 'code' in err)
|
|
83
|
+
throw err;
|
|
84
|
+
// Wrap unexpected DB errors as internal errors.
|
|
85
|
+
throw Object.assign(new Error(`db/query(${String(operation)}) failed: ${err instanceof Error ? err.message : String(err)}`), { code: -32603 });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ── Request dispatch ──────────────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Route a JSON-RPC 2.0 request to the appropriate handler.
|
|
91
|
+
* Exported so the HTTP fallback server can share the same dispatch logic.
|
|
92
|
+
*/
|
|
93
|
+
export async function dispatchRpcRequest(req) {
|
|
94
|
+
const { method, params } = req;
|
|
95
|
+
switch (method) {
|
|
96
|
+
case 'tool/call': {
|
|
97
|
+
const name = params.name;
|
|
98
|
+
const args = (params.arguments ?? {});
|
|
99
|
+
if (typeof name !== 'string' || !name) {
|
|
100
|
+
throw Object.assign(new Error('tool/call requires params.name (string)'), { code: -32600 });
|
|
101
|
+
}
|
|
102
|
+
return callRemoteTool(name, args);
|
|
103
|
+
}
|
|
104
|
+
case 'session/register': {
|
|
105
|
+
const sessionId = params.session_id;
|
|
106
|
+
if (typeof sessionId !== 'string' || !sessionId) {
|
|
107
|
+
throw Object.assign(new Error('session/register requires params.session_id (string)'), { code: -32600 });
|
|
108
|
+
}
|
|
109
|
+
sessionRegistry.register(sessionId);
|
|
110
|
+
return { ok: true };
|
|
111
|
+
}
|
|
112
|
+
case 'session/release': {
|
|
113
|
+
const sessionId = params.session_id;
|
|
114
|
+
if (typeof sessionId !== 'string' || !sessionId) {
|
|
115
|
+
throw Object.assign(new Error('session/release requires params.session_id (string)'), { code: -32600 });
|
|
116
|
+
}
|
|
117
|
+
sessionRegistry.release(sessionId);
|
|
118
|
+
return { ok: true };
|
|
119
|
+
}
|
|
120
|
+
case 'session/context/get': {
|
|
121
|
+
const sessionId = params.session_id;
|
|
122
|
+
// Try SQLite first (survives daemon restart if same DB file is re-opened).
|
|
123
|
+
const db = sqliteOwner.getDb();
|
|
124
|
+
if (db && typeof sessionId === 'string' && sessionId) {
|
|
125
|
+
try {
|
|
126
|
+
const row = db
|
|
127
|
+
.prepare('SELECT * FROM session_context WHERE session_id = ? ORDER BY updated_at DESC LIMIT 1')
|
|
128
|
+
.get(sessionId);
|
|
129
|
+
if (row)
|
|
130
|
+
return { value: row };
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Fall through to in-memory store below.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Fall back to in-memory map (populated before DB was available or on DB error).
|
|
137
|
+
const value = typeof sessionId === 'string' ? contextStore.get(sessionId) ?? null : null;
|
|
138
|
+
return { value };
|
|
139
|
+
}
|
|
140
|
+
case 'session/context/set': {
|
|
141
|
+
const sessionId = params.session_id;
|
|
142
|
+
if (typeof sessionId !== 'string' || !sessionId) {
|
|
143
|
+
throw Object.assign(new Error('session/context/set requires params.session_id (string)'), { code: -32600 });
|
|
144
|
+
}
|
|
145
|
+
const ctx = params.context;
|
|
146
|
+
// Always write to in-memory store first (fastest, daemon-lifetime guarantee).
|
|
147
|
+
contextStore.set(sessionId, ctx);
|
|
148
|
+
// Also persist to SQLite via daemon's own connection — same SQL as
|
|
149
|
+
// hook-state.ts:setSessionContext. Do NOT import hook-state.ts here;
|
|
150
|
+
// that module opens its own DB connection which would fight with ours.
|
|
151
|
+
const dbForSet = sqliteOwner.getDb();
|
|
152
|
+
if (dbForSet && ctx !== null && typeof ctx === 'object') {
|
|
153
|
+
const c = ctx;
|
|
154
|
+
try {
|
|
155
|
+
dbForSet
|
|
156
|
+
.prepare(`
|
|
157
|
+
INSERT OR REPLACE INTO session_context
|
|
158
|
+
(session_id, project_id, interaction_id, entity_id, project_name,
|
|
159
|
+
git_root, git_branch, git_remote, working_directory,
|
|
160
|
+
session_start, updated_at, client_type, agent_name, platform, arch)
|
|
161
|
+
VALUES
|
|
162
|
+
(@session_id, @project_id, @interaction_id, @entity_id, @project_name,
|
|
163
|
+
@git_root, @git_branch, @git_remote, @working_directory,
|
|
164
|
+
@session_start, @updated_at, @client_type, @agent_name, @platform, @arch)
|
|
165
|
+
`)
|
|
166
|
+
.run({
|
|
167
|
+
'@session_id': c['session_id'] ?? sessionId,
|
|
168
|
+
'@project_id': c['project_id'] ?? null,
|
|
169
|
+
'@interaction_id': c['interaction_id'] ?? null,
|
|
170
|
+
'@entity_id': c['entity_id'] ?? null,
|
|
171
|
+
'@project_name': c['project_name'] ?? null,
|
|
172
|
+
'@git_root': c['git_root'] ?? null,
|
|
173
|
+
'@git_branch': c['git_branch'] ?? null,
|
|
174
|
+
'@git_remote': c['git_remote'] ?? null,
|
|
175
|
+
'@working_directory': c['working_directory'] ?? null,
|
|
176
|
+
'@session_start': c['session_start'] ?? null,
|
|
177
|
+
'@updated_at': c['updated_at'] ?? new Date().toISOString(),
|
|
178
|
+
'@client_type': c['client_type'] ?? null,
|
|
179
|
+
'@agent_name': c['agent_name'] ?? null,
|
|
180
|
+
'@platform': c['platform'] ?? null,
|
|
181
|
+
'@arch': c['arch'] ?? null,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Non-fatal — in-memory store is the source of truth for daemon's lifetime.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Reset idle checkpoint timer on write activity.
|
|
189
|
+
sqliteOwner.resetIdleTimer();
|
|
190
|
+
return { ok: true };
|
|
191
|
+
}
|
|
192
|
+
case 'db/query': {
|
|
193
|
+
const { operation, args } = params;
|
|
194
|
+
const result = handleDbQuery(operation, args);
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
case 'daemon/ping': {
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
version: VERSION,
|
|
201
|
+
pid: process.pid,
|
|
202
|
+
uptime: process.uptime(),
|
|
203
|
+
sessions: sessionRegistry.count(),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
case 'daemon/shutdown': {
|
|
207
|
+
// Schedule shutdown after response is sent
|
|
208
|
+
setImmediate(() => {
|
|
209
|
+
if (_server) {
|
|
210
|
+
_server.emit('daemon-shutdown');
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return { ok: true };
|
|
214
|
+
}
|
|
215
|
+
case 'project/resolve': {
|
|
216
|
+
const p = params;
|
|
217
|
+
let entry = null;
|
|
218
|
+
if (p.git_remote)
|
|
219
|
+
entry = projectCache.getByRemote(p.git_remote);
|
|
220
|
+
if (!entry && p.directory)
|
|
221
|
+
entry = projectCache.getByDirectory(p.directory);
|
|
222
|
+
if (!entry && p.slug)
|
|
223
|
+
entry = projectCache.getBySlug(p.slug);
|
|
224
|
+
return entry
|
|
225
|
+
? { found: true, project_id: entry.id, slug: entry.slug }
|
|
226
|
+
: { found: false };
|
|
227
|
+
}
|
|
228
|
+
case 'project/cache-set': {
|
|
229
|
+
projectCache.set(params);
|
|
230
|
+
return { ok: true };
|
|
231
|
+
}
|
|
232
|
+
default: {
|
|
233
|
+
throw Object.assign(new Error(`Method not found: ${String(method)}`), { code: -32601 });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ── Connection handler ────────────────────────────────────────────────────────
|
|
238
|
+
function handleConnection(socket) {
|
|
239
|
+
activeSockets.add(socket);
|
|
240
|
+
socket.once('close', () => activeSockets.delete(socket));
|
|
241
|
+
// Reset the idle checkpoint timer on every incoming connection.
|
|
242
|
+
sqliteOwner.resetIdleTimer();
|
|
243
|
+
const rl = createInterface({ input: socket, crlfDelay: Infinity });
|
|
244
|
+
rl.once('line', async (line) => {
|
|
245
|
+
rl.close();
|
|
246
|
+
let req;
|
|
247
|
+
let id = 0;
|
|
248
|
+
const sendError = (code, message, data) => {
|
|
249
|
+
const resp = {
|
|
250
|
+
jsonrpc: '2.0',
|
|
251
|
+
id,
|
|
252
|
+
error: { code, message, ...(data !== undefined ? { data } : {}) },
|
|
253
|
+
};
|
|
254
|
+
try {
|
|
255
|
+
socket.write(JSON.stringify(resp) + '\n');
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Socket may already be closed
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
socket.end();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const sendResult = (result) => {
|
|
265
|
+
const resp = { jsonrpc: '2.0', id, result };
|
|
266
|
+
try {
|
|
267
|
+
socket.write(JSON.stringify(resp) + '\n');
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Socket may already be closed
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
socket.end();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
// Parse JSON-RPC request
|
|
277
|
+
try {
|
|
278
|
+
req = JSON.parse(line);
|
|
279
|
+
id = req.id ?? 0;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
sendError(-32600, 'Invalid JSON in request');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!req || req.jsonrpc !== '2.0' || !req.method) {
|
|
286
|
+
sendError(-32600, 'Invalid JSON-RPC 2.0 request');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// Dispatch
|
|
290
|
+
try {
|
|
291
|
+
const result = await dispatchRpcRequest(req);
|
|
292
|
+
sendResult(result);
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
const isRpcError = err !== null && typeof err === 'object' && 'code' in err;
|
|
296
|
+
const code = isRpcError ? err.code : -32603;
|
|
297
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
298
|
+
sendError(code, message);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
// Errors on the socket itself — just remove and destroy
|
|
302
|
+
socket.on('error', () => {
|
|
303
|
+
activeSockets.delete(socket);
|
|
304
|
+
try {
|
|
305
|
+
socket.destroy();
|
|
306
|
+
}
|
|
307
|
+
catch { /* ignore */ }
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
311
|
+
export function createDaemonServer() {
|
|
312
|
+
const server = createServer(handleConnection);
|
|
313
|
+
_server = server;
|
|
314
|
+
server.on('error', (err) => {
|
|
315
|
+
process.stderr.write(`[gramatr-daemon] server error: ${err.message}\n`);
|
|
316
|
+
});
|
|
317
|
+
return server;
|
|
318
|
+
}
|
|
319
|
+
export function getActiveSockets() {
|
|
320
|
+
return activeSockets;
|
|
321
|
+
}
|
|
322
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/daemon/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-registry.ts — Reference-counted session tracking for the gramatr daemon.
|
|
3
|
+
*
|
|
4
|
+
* Sessions are tracked in-memory only (no SQLite in Sprint 1).
|
|
5
|
+
* When the last session is released, a 30-second grace timer fires
|
|
6
|
+
* the onEmpty callback so the daemon can checkpoint WAL and exit.
|
|
7
|
+
*/
|
|
8
|
+
interface SessionRegistration {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
registeredAt: number;
|
|
11
|
+
lastHeartbeatAt: number;
|
|
12
|
+
}
|
|
13
|
+
declare class SessionRegistry {
|
|
14
|
+
private sessions;
|
|
15
|
+
private shutdownTimer;
|
|
16
|
+
private onEmptyFn;
|
|
17
|
+
private readonly GRACE_MS;
|
|
18
|
+
setOnEmpty(fn: () => void): void;
|
|
19
|
+
register(sessionId: string): void;
|
|
20
|
+
release(sessionId: string): void;
|
|
21
|
+
count(): number;
|
|
22
|
+
list(): SessionRegistration[];
|
|
23
|
+
}
|
|
24
|
+
export declare const sessionRegistry: SessionRegistry;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=session-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../../src/daemon/session-registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,UAAU,mBAAmB;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,cAAM,eAAe;IACnB,OAAO,CAAC,QAAQ,CAA0C;IAC1D,OAAO,CAAC,aAAa,CAA8C;IACnE,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IAEnC,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAIhC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAajC,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAOhC,KAAK,IAAI,MAAM;IAIf,IAAI,IAAI,mBAAmB,EAAE;CAG9B;AAED,eAAO,MAAM,eAAe,iBAAwB,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-registry.ts — Reference-counted session tracking for the gramatr daemon.
|
|
3
|
+
*
|
|
4
|
+
* Sessions are tracked in-memory only (no SQLite in Sprint 1).
|
|
5
|
+
* When the last session is released, a 30-second grace timer fires
|
|
6
|
+
* the onEmpty callback so the daemon can checkpoint WAL and exit.
|
|
7
|
+
*/
|
|
8
|
+
class SessionRegistry {
|
|
9
|
+
sessions = new Map();
|
|
10
|
+
shutdownTimer = null;
|
|
11
|
+
onEmptyFn = null;
|
|
12
|
+
GRACE_MS = 30_000;
|
|
13
|
+
setOnEmpty(fn) {
|
|
14
|
+
this.onEmptyFn = fn;
|
|
15
|
+
}
|
|
16
|
+
register(sessionId) {
|
|
17
|
+
this.sessions.set(sessionId, {
|
|
18
|
+
sessionId,
|
|
19
|
+
registeredAt: Date.now(),
|
|
20
|
+
lastHeartbeatAt: Date.now(),
|
|
21
|
+
});
|
|
22
|
+
// Cancel any pending shutdown timer — a new session arrived
|
|
23
|
+
if (this.shutdownTimer !== null) {
|
|
24
|
+
clearTimeout(this.shutdownTimer);
|
|
25
|
+
this.shutdownTimer = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
release(sessionId) {
|
|
29
|
+
this.sessions.delete(sessionId);
|
|
30
|
+
if (this.sessions.size === 0 && this.onEmptyFn !== null) {
|
|
31
|
+
this.shutdownTimer = setTimeout(this.onEmptyFn, this.GRACE_MS);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
count() {
|
|
35
|
+
return this.sessions.size;
|
|
36
|
+
}
|
|
37
|
+
list() {
|
|
38
|
+
return [...this.sessions.values()];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export const sessionRegistry = new SessionRegistry();
|
|
42
|
+
//# sourceMappingURL=session-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../src/daemon/session-registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,MAAM,eAAe;IACX,QAAQ,GAAG,IAAI,GAAG,EAA+B,CAAC;IAClD,aAAa,GAAyC,IAAI,CAAC;IAC3D,SAAS,GAAwB,IAAI,CAAC;IAC7B,QAAQ,GAAG,MAAM,CAAC;IAEnC,UAAU,CAAC,EAAc;QACvB,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;IACtB,CAAC;IAED,QAAQ,CAAC,SAAiB;QACxB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE;YAC3B,SAAS;YACT,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE;YACxB,eAAe,EAAE,IAAI,CAAC,GAAG,EAAE;SAC5B,CAAC,CAAC;QACH,4DAA4D;QAC5D,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YAChC,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YACjC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,OAAO,CAAC,SAAiB;QACvB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAChC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YACxD,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED,KAAK;QACH,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sqlite-owner.ts — The daemon's owned SQLite connection.
|
|
3
|
+
*
|
|
4
|
+
* The daemon holds the single long-lived DatabaseSync connection to
|
|
5
|
+
* ~/.gramatr/state.db. It is NOT an exclusive lock — hook processes can
|
|
6
|
+
* still open WAL-mode concurrent readers/writers. The daemon becomes the
|
|
7
|
+
* one reliable checkpointer because short-lived hook processes exit before
|
|
8
|
+
* running checkpoints themselves (fix for #1296).
|
|
9
|
+
*
|
|
10
|
+
* Checkpoint lifecycle:
|
|
11
|
+
* PASSIVE — every 30 s of idle (no active sessions; timer resets on activity)
|
|
12
|
+
* TRUNCATE — on graceful shutdown (SIGTERM, SIGINT, idle-empty)
|
|
13
|
+
*
|
|
14
|
+
* When GRAMATR_STATE_DB=:memory: (tests), open() and checkpoints are skipped.
|
|
15
|
+
*/
|
|
16
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
17
|
+
export declare class SqliteOwner {
|
|
18
|
+
private db;
|
|
19
|
+
private idleTimer;
|
|
20
|
+
private readonly IDLE_CHECKPOINT_MS;
|
|
21
|
+
/**
|
|
22
|
+
* Open the database connection and start the idle checkpoint interval.
|
|
23
|
+
*
|
|
24
|
+
* No-op when GRAMATR_STATE_DB=:memory:.
|
|
25
|
+
*/
|
|
26
|
+
open(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Run PRAGMA wal_checkpoint(PASSIVE) — allows readers/writers to continue.
|
|
29
|
+
* Pages are only checkpointed when no reader is blocking; safe to call anytime.
|
|
30
|
+
*/
|
|
31
|
+
passiveCheckpoint(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Run PRAGMA wal_checkpoint(TRUNCATE) — waits for readers then zeroes the WAL.
|
|
34
|
+
* Called on graceful shutdown to leave the DB in a clean state.
|
|
35
|
+
*/
|
|
36
|
+
truncateCheckpoint(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Reset (restart) the 30-second idle passive-checkpoint interval.
|
|
39
|
+
* Call this on any activity (new connection, session register/release, context set).
|
|
40
|
+
*/
|
|
41
|
+
resetIdleTimer(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Run a TRUNCATE checkpoint then close the connection.
|
|
44
|
+
* Called in gracefulShutdown() before the process exits.
|
|
45
|
+
*/
|
|
46
|
+
close(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Expose the underlying DatabaseSync for direct SQL in server.ts handlers.
|
|
49
|
+
* Returns null when the DB is not open (memory mode or open() failed).
|
|
50
|
+
*/
|
|
51
|
+
getDb(): DatabaseSync | null;
|
|
52
|
+
}
|
|
53
|
+
/** Module singleton — imported by server.ts and index.ts. */
|
|
54
|
+
export declare const sqliteOwner: SqliteOwner;
|
|
55
|
+
//# sourceMappingURL=sqlite-owner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-owner.d.ts","sourceRoot":"","sources":["../../src/daemon/sqlite-owner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAA6B;IACvC,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAU;IAE7C;;;;OAIG;IACH,IAAI,IAAI,IAAI;IAgCZ;;;OAGG;IACH,iBAAiB,IAAI,IAAI;IASzB;;;OAGG;IACH,kBAAkB,IAAI,IAAI;IAS1B;;;OAGG;IACH,cAAc,IAAI,IAAI;IAiBtB;;;OAGG;IACH,KAAK,IAAI,IAAI;IAcb;;;OAGG;IACH,KAAK,IAAI,YAAY,GAAG,IAAI;CAG7B;AAED,6DAA6D;AAC7D,eAAO,MAAM,WAAW,aAAoB,CAAC"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sqlite-owner.ts — The daemon's owned SQLite connection.
|
|
3
|
+
*
|
|
4
|
+
* The daemon holds the single long-lived DatabaseSync connection to
|
|
5
|
+
* ~/.gramatr/state.db. It is NOT an exclusive lock — hook processes can
|
|
6
|
+
* still open WAL-mode concurrent readers/writers. The daemon becomes the
|
|
7
|
+
* one reliable checkpointer because short-lived hook processes exit before
|
|
8
|
+
* running checkpoints themselves (fix for #1296).
|
|
9
|
+
*
|
|
10
|
+
* Checkpoint lifecycle:
|
|
11
|
+
* PASSIVE — every 30 s of idle (no active sessions; timer resets on activity)
|
|
12
|
+
* TRUNCATE — on graceful shutdown (SIGTERM, SIGINT, idle-empty)
|
|
13
|
+
*
|
|
14
|
+
* When GRAMATR_STATE_DB=:memory: (tests), open() and checkpoints are skipped.
|
|
15
|
+
*/
|
|
16
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
17
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
18
|
+
import { dirname } from 'node:path';
|
|
19
|
+
import { getStateDatabasePath } from './db-path.js';
|
|
20
|
+
export class SqliteOwner {
|
|
21
|
+
db = null;
|
|
22
|
+
idleTimer = null;
|
|
23
|
+
IDLE_CHECKPOINT_MS = 30_000;
|
|
24
|
+
/**
|
|
25
|
+
* Open the database connection and start the idle checkpoint interval.
|
|
26
|
+
*
|
|
27
|
+
* No-op when GRAMATR_STATE_DB=:memory:.
|
|
28
|
+
*/
|
|
29
|
+
open() {
|
|
30
|
+
const path = getStateDatabasePath();
|
|
31
|
+
// Skip filesystem operations for in-memory test databases.
|
|
32
|
+
if (path === ':memory:')
|
|
33
|
+
return;
|
|
34
|
+
// Ensure the parent directory exists.
|
|
35
|
+
try {
|
|
36
|
+
const dir = dirname(path);
|
|
37
|
+
if (!existsSync(dir))
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Non-fatal — if we can't create the dir we'll fall through and let
|
|
42
|
+
// DatabaseSync throw, which we also catch below.
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
this.db = new DatabaseSync(path);
|
|
46
|
+
// Ensure WAL mode is set — safe to re-run if hook-state.ts already set it.
|
|
47
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
48
|
+
this.db.exec('PRAGMA synchronous = NORMAL');
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
// Non-fatal — daemon degrades without the checkpoint lifecycle.
|
|
52
|
+
process.stderr.write(`[gramatr-daemon] sqlite-owner: failed to open ${path}: ${String(err)}\n`);
|
|
53
|
+
this.db = null;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.resetIdleTimer();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run PRAGMA wal_checkpoint(PASSIVE) — allows readers/writers to continue.
|
|
60
|
+
* Pages are only checkpointed when no reader is blocking; safe to call anytime.
|
|
61
|
+
*/
|
|
62
|
+
passiveCheckpoint() {
|
|
63
|
+
if (!this.db)
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
this.db.exec('PRAGMA wal_checkpoint(PASSIVE)');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Non-fatal — WAL file will be checkpointed on next opportunity.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Run PRAGMA wal_checkpoint(TRUNCATE) — waits for readers then zeroes the WAL.
|
|
74
|
+
* Called on graceful shutdown to leave the DB in a clean state.
|
|
75
|
+
*/
|
|
76
|
+
truncateCheckpoint() {
|
|
77
|
+
if (!this.db)
|
|
78
|
+
return;
|
|
79
|
+
try {
|
|
80
|
+
this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Non-fatal — passive checkpoint already ran; truncate is best-effort.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reset (restart) the 30-second idle passive-checkpoint interval.
|
|
88
|
+
* Call this on any activity (new connection, session register/release, context set).
|
|
89
|
+
*/
|
|
90
|
+
resetIdleTimer() {
|
|
91
|
+
if (this.idleTimer !== null) {
|
|
92
|
+
clearInterval(this.idleTimer);
|
|
93
|
+
this.idleTimer = null;
|
|
94
|
+
}
|
|
95
|
+
if (!this.db)
|
|
96
|
+
return;
|
|
97
|
+
this.idleTimer = setInterval(() => {
|
|
98
|
+
this.passiveCheckpoint();
|
|
99
|
+
}, this.IDLE_CHECKPOINT_MS);
|
|
100
|
+
// setInterval keeps the event loop alive — unref so it doesn't prevent exit.
|
|
101
|
+
if (typeof this.idleTimer === 'object' && this.idleTimer !== null && 'unref' in this.idleTimer) {
|
|
102
|
+
this.idleTimer.unref();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Run a TRUNCATE checkpoint then close the connection.
|
|
107
|
+
* Called in gracefulShutdown() before the process exits.
|
|
108
|
+
*/
|
|
109
|
+
close() {
|
|
110
|
+
if (this.idleTimer !== null) {
|
|
111
|
+
clearInterval(this.idleTimer);
|
|
112
|
+
this.idleTimer = null;
|
|
113
|
+
}
|
|
114
|
+
this.truncateCheckpoint();
|
|
115
|
+
try {
|
|
116
|
+
this.db?.close();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Non-fatal — process is exiting anyway.
|
|
120
|
+
}
|
|
121
|
+
this.db = null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Expose the underlying DatabaseSync for direct SQL in server.ts handlers.
|
|
125
|
+
* Returns null when the DB is not open (memory mode or open() failed).
|
|
126
|
+
*/
|
|
127
|
+
getDb() {
|
|
128
|
+
return this.db;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Module singleton — imported by server.ts and index.ts. */
|
|
132
|
+
export const sqliteOwner = new SqliteOwner();
|
|
133
|
+
//# sourceMappingURL=sqlite-owner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-owner.js","sourceRoot":"","sources":["../../src/daemon/sqlite-owner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* startup.ts — Daemon lifecycle helpers.
|
|
3
|
+
*
|
|
4
|
+
* Two halves:
|
|
5
|
+
* Daemon side — writePidFile, removePidFile, removeSocketFile, path helpers
|
|
6
|
+
* Hook side — isDaemonRunning, launchDaemon, waitForSocket
|
|
7
|
+
*
|
|
8
|
+
* The lock file (O_CREAT|O_EXCL) prevents races when multiple hooks fire
|
|
9
|
+
* simultaneously at session start. Only one process wins the lock and
|
|
10
|
+
* launches the daemon; the rest see 'already-launching'.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getDaemonSocketPath(): string;
|
|
13
|
+
export declare function getDaemonPidPath(): string;
|
|
14
|
+
export declare function getDaemonLockPath(): string;
|
|
15
|
+
export declare function getDaemonHttpPortPath(): string;
|
|
16
|
+
export declare function getDaemonTokenPath(): string;
|
|
17
|
+
/** Write HTTP fallback credentials (port + auth token) to disk at 0600 perms. */
|
|
18
|
+
export declare function writeHttpCredentials(port: number, token: string): void;
|
|
19
|
+
/** Remove HTTP fallback credential files on daemon shutdown. */
|
|
20
|
+
export declare function removeHttpCredentials(): void;
|
|
21
|
+
/** Read HTTP fallback credentials. Returns null if either file is missing or malformed. */
|
|
22
|
+
export declare function readHttpCredentials(): {
|
|
23
|
+
port: number;
|
|
24
|
+
token: string;
|
|
25
|
+
} | null;
|
|
26
|
+
export declare function writePidFile(): void;
|
|
27
|
+
export declare function removePidFile(): void;
|
|
28
|
+
export declare function removeSocketFile(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Returns true if the daemon process is alive AND the socket file exists.
|
|
31
|
+
* Uses `process.kill(pid, 0)` as a lightweight liveness probe.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isDaemonRunning(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Launch the daemon as a detached background process.
|
|
36
|
+
*
|
|
37
|
+
* Uses a lock file (O_CREAT|O_EXCL) to prevent simultaneous launches when
|
|
38
|
+
* multiple hooks fire at the same time.
|
|
39
|
+
*
|
|
40
|
+
* Returns:
|
|
41
|
+
* 'launched' — daemon successfully spawned
|
|
42
|
+
* 'already-launching' — another process holds the lock
|
|
43
|
+
* 'already-running' — daemon is already alive
|
|
44
|
+
*/
|
|
45
|
+
export declare function launchDaemon(): 'launched' | 'already-launching' | 'already-running';
|
|
46
|
+
/**
|
|
47
|
+
* Poll for the daemon socket to appear, up to maxWaitMs milliseconds.
|
|
48
|
+
* Used by session-start after launchDaemon() to wait for the socket to be ready.
|
|
49
|
+
*/
|
|
50
|
+
export declare function waitForSocket(maxWaitMs?: number): Promise<boolean>;
|
|
51
|
+
//# sourceMappingURL=startup.d.ts.map
|