@1agents/session-reader 0.6.1 → 0.7.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 +42 -0
- package/cordis.patch.yml +4 -0
- package/dist/bin/1session.js +26 -1
- package/dist/src/dsh/adapter.d.ts +18 -0
- package/dist/src/dsh/adapter.js +198 -0
- package/dist/src/dsh/client.d.ts +2 -0
- package/dist/src/dsh/client.js +18997 -0
- package/dist/src/dsh/index.d.ts +2 -0
- package/dist/src/dsh/index.js +2 -0
- package/dist/src/dsh/plugin.d.ts +3 -0
- package/dist/src/dsh/plugin.js +392 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/resolver.d.ts +7 -0
- package/dist/src/resolver.js +13 -8
- package/dist/src/serve/http.js +13 -1
- package/dist/src/store/nul.d.ts +12 -0
- package/dist/src/store/nul.js +52 -0
- package/dist/src/store/read.d.ts +6 -2
- package/dist/src/store/read.js +18 -15
- package/dist/src/store/schema.d.ts +1 -1
- package/dist/src/store/schema.js +1 -1
- package/dist/src/store/write.js +3 -2
- package/dist/src/web/api.d.ts +72 -0
- package/dist/src/web/api.js +126 -0
- package/dist/src/web/app.d.ts +1 -0
- package/dist/src/web/app.js +18975 -0
- package/dist/src/web/server.d.ts +20 -0
- package/dist/src/web/server.js +126 -0
- package/package.json +25 -4
- package/skills/1session/SKILL.md +46 -8
- package/skills/1session/evals/evals.json +43 -1
- package/skills/1session/references/cli.md +16 -0
- package/skills/1session/references/install.md +195 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { resolveSession, searchSessions, buildOverview, summarizeTurns, turnDetail, eventDetails, } from '../index.js';
|
|
3
|
+
import { handleSessionApi } from '../web/api.js';
|
|
4
|
+
import { convertSessionToDshEvents } from './adapter.js';
|
|
5
|
+
export const name = 'session-reader';
|
|
6
|
+
export const inject = ['tools', 'webServer'];
|
|
7
|
+
export function apply(ctx) {
|
|
8
|
+
// 1. Register tools if ctx.tools is present
|
|
9
|
+
if (ctx.tools && typeof ctx.tools.register === 'function') {
|
|
10
|
+
// session_search
|
|
11
|
+
ctx.tools.register({
|
|
12
|
+
name: 'session_search',
|
|
13
|
+
description: 'Search past AI coding sessions across Claude Code, Codex, Antigravity, Grok and DSH. By default searches sessions in the current working directory / workspace.',
|
|
14
|
+
parameters: {
|
|
15
|
+
query: { type: 'string', required: true, description: 'Keywords, file paths, or error messages to search for' },
|
|
16
|
+
workspace: { type: 'string', description: 'Filter by workspace path (optional, defaults to current session workspace)' },
|
|
17
|
+
scope: { type: 'string', description: 'Search scope: cwd (default, current session directory), global (all workspaces), or specific path' },
|
|
18
|
+
since: { type: 'string', description: 'Time filter, e.g. 24h, 7d, 30d' },
|
|
19
|
+
kind: { type: 'string', description: 'Filter by kind: user, assistant, thinking, tool_call, tool_result' },
|
|
20
|
+
limit: { type: 'number', description: 'Max number of sessions to return' },
|
|
21
|
+
},
|
|
22
|
+
output: {
|
|
23
|
+
schema: { type: 'array' },
|
|
24
|
+
render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
25
|
+
},
|
|
26
|
+
async execute(args, exec) {
|
|
27
|
+
const currentCwd = exec?.agent?.session?.header?.cwd ?? process.cwd();
|
|
28
|
+
let targetWorkspace;
|
|
29
|
+
if (args.scope === 'global') {
|
|
30
|
+
targetWorkspace = undefined;
|
|
31
|
+
}
|
|
32
|
+
else if (args.workspace) {
|
|
33
|
+
targetWorkspace = args.workspace;
|
|
34
|
+
}
|
|
35
|
+
else if (args.scope && args.scope !== 'cwd') {
|
|
36
|
+
targetWorkspace = args.scope;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Default to current session PWD
|
|
40
|
+
targetWorkspace = currentCwd;
|
|
41
|
+
}
|
|
42
|
+
const hits = await searchSessions(args.query, {
|
|
43
|
+
workspace: targetWorkspace,
|
|
44
|
+
since: args.since,
|
|
45
|
+
kinds: args.kind ? [args.kind] : undefined,
|
|
46
|
+
limit: args.limit,
|
|
47
|
+
});
|
|
48
|
+
return hits.map((h) => ({
|
|
49
|
+
sessionId: h.session.id,
|
|
50
|
+
provider: h.session.provider,
|
|
51
|
+
workspace: h.session.workspace,
|
|
52
|
+
title: h.session.title,
|
|
53
|
+
updatedAt: h.session.updatedAt,
|
|
54
|
+
matchCount: h.matches.length,
|
|
55
|
+
matches: h.matches.slice(0, 5).map((m) => ({
|
|
56
|
+
turn: m.turn,
|
|
57
|
+
kind: m.kind,
|
|
58
|
+
excerpt: m.excerpt,
|
|
59
|
+
})),
|
|
60
|
+
}));
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
// session_overview
|
|
64
|
+
ctx.tools.register({
|
|
65
|
+
name: 'session_overview',
|
|
66
|
+
description: 'Get statistics, files modified, commands executed, and end-state facts of a past session.',
|
|
67
|
+
parameters: {
|
|
68
|
+
sessionId: { type: 'string', required: true, description: 'Session ID or 8-char prefix' },
|
|
69
|
+
},
|
|
70
|
+
output: {
|
|
71
|
+
schema: { type: 'object' },
|
|
72
|
+
render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
73
|
+
},
|
|
74
|
+
async execute(args) {
|
|
75
|
+
const resolved = await resolveSession(args.sessionId);
|
|
76
|
+
if (!resolved)
|
|
77
|
+
throw new Error(`Session not found: ${args.sessionId}`);
|
|
78
|
+
const normalized = await resolved.adapter.parse(resolved.candidate);
|
|
79
|
+
const overview = buildOverview(normalized);
|
|
80
|
+
return {
|
|
81
|
+
id: normalized.ref.id,
|
|
82
|
+
provider: normalized.ref.provider,
|
|
83
|
+
title: normalized.ref.title,
|
|
84
|
+
workspace: normalized.ref.workspace,
|
|
85
|
+
markdown: overview.markdown,
|
|
86
|
+
stats: overview.stats,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
// session_turns
|
|
91
|
+
ctx.tools.register({
|
|
92
|
+
name: 'session_turns',
|
|
93
|
+
description: 'Get turn-by-turn summary of what was discussed and done in a session.',
|
|
94
|
+
parameters: {
|
|
95
|
+
sessionId: { type: 'string', required: true, description: 'Session ID or 8-char prefix' },
|
|
96
|
+
},
|
|
97
|
+
output: {
|
|
98
|
+
schema: { type: 'array' },
|
|
99
|
+
render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
100
|
+
},
|
|
101
|
+
async execute(args) {
|
|
102
|
+
const resolved = await resolveSession(args.sessionId);
|
|
103
|
+
if (!resolved)
|
|
104
|
+
throw new Error(`Session not found: ${args.sessionId}`);
|
|
105
|
+
const normalized = await resolved.adapter.parse(resolved.candidate);
|
|
106
|
+
return summarizeTurns(normalized);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
// session_turn_detail
|
|
110
|
+
ctx.tools.register({
|
|
111
|
+
name: 'session_turn_detail',
|
|
112
|
+
description: 'Inspect full untruncated events, tool calls, and results for a specific turn in a session.',
|
|
113
|
+
parameters: {
|
|
114
|
+
sessionId: { type: 'string', required: true, description: 'Session ID or 8-char prefix' },
|
|
115
|
+
turn: { type: 'number', required: true, description: 'Turn number' },
|
|
116
|
+
events: { type: 'string', description: 'Event selector, e.g. "11", "11-13", "11,15,22"' },
|
|
117
|
+
},
|
|
118
|
+
output: {
|
|
119
|
+
schema: { oneOf: [{ type: 'object' }, { type: 'array' }] },
|
|
120
|
+
render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
121
|
+
},
|
|
122
|
+
async execute(args) {
|
|
123
|
+
const resolved = await resolveSession(args.sessionId);
|
|
124
|
+
if (!resolved)
|
|
125
|
+
throw new Error(`Session not found: ${args.sessionId}`);
|
|
126
|
+
const normalized = await resolved.adapter.parse(resolved.candidate);
|
|
127
|
+
if (args.events) {
|
|
128
|
+
return await eventDetails(normalized, args.events);
|
|
129
|
+
}
|
|
130
|
+
return turnDetail(normalized, args.turn);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
// session_open_in_dsh
|
|
134
|
+
ctx.tools.register({
|
|
135
|
+
name: 'session_open_in_dsh',
|
|
136
|
+
description: 'Load a cross-agent historical session into DSH native sessions and automatically assign it to the DSH workspace.',
|
|
137
|
+
parameters: {
|
|
138
|
+
sessionId: { type: 'string', required: true, description: 'Session ID to import into DSH' },
|
|
139
|
+
currentCwd: { type: 'string', description: 'Optional current session working directory fallback' },
|
|
140
|
+
},
|
|
141
|
+
output: {
|
|
142
|
+
schema: { type: 'object' },
|
|
143
|
+
render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
144
|
+
},
|
|
145
|
+
async execute(args, exec) {
|
|
146
|
+
const resolved = await resolveSession(args.sessionId);
|
|
147
|
+
if (!resolved)
|
|
148
|
+
throw new Error(`Session not found: ${args.sessionId}`);
|
|
149
|
+
const normalized = await resolved.adapter.parse(resolved.candidate);
|
|
150
|
+
const dshEvents = convertSessionToDshEvents(normalized);
|
|
151
|
+
const rawId = normalized.ref.id;
|
|
152
|
+
const sanitizedId = rawId.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
153
|
+
const dshSessionId = sanitizedId.startsWith('session-') ? sanitizedId : `session-${sanitizedId}`;
|
|
154
|
+
const currentSessionCwd = exec?.agent?.session?.header?.cwd ?? args.currentCwd;
|
|
155
|
+
let targetCwd;
|
|
156
|
+
if (normalized.ref.workspace && typeof normalized.ref.workspace === 'string' && normalized.ref.workspace.startsWith('/')) {
|
|
157
|
+
try {
|
|
158
|
+
const st = await fs.promises.stat(normalized.ref.workspace);
|
|
159
|
+
if (st.isDirectory()) {
|
|
160
|
+
targetCwd = await fs.promises.realpath(normalized.ref.workspace);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch { }
|
|
164
|
+
}
|
|
165
|
+
if (!targetCwd) {
|
|
166
|
+
const fallback = currentSessionCwd || process.cwd();
|
|
167
|
+
try {
|
|
168
|
+
const st = await fs.promises.stat(fallback);
|
|
169
|
+
if (st.isDirectory()) {
|
|
170
|
+
targetCwd = await fs.promises.realpath(fallback);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch { }
|
|
174
|
+
}
|
|
175
|
+
if (!targetCwd)
|
|
176
|
+
targetCwd = process.cwd();
|
|
177
|
+
const createdAt = normalized.ref.createdAt ? Date.parse(normalized.ref.createdAt) : Date.now();
|
|
178
|
+
const header = {
|
|
179
|
+
version: 2,
|
|
180
|
+
id: dshSessionId,
|
|
181
|
+
cwd: targetCwd,
|
|
182
|
+
createdAt: isNaN(createdAt) ? Date.now() : createdAt,
|
|
183
|
+
isSeeded: false,
|
|
184
|
+
delegationDepth: 0,
|
|
185
|
+
};
|
|
186
|
+
const persistence = ctx.get ? ctx.get('sessionPersistence') : ctx.sessionPersistence;
|
|
187
|
+
if (persistence && typeof persistence.create === 'function') {
|
|
188
|
+
try {
|
|
189
|
+
const existingStat = await persistence.stat(dshSessionId);
|
|
190
|
+
if (!existingStat) {
|
|
191
|
+
const handle = await persistence.create(header);
|
|
192
|
+
await handle.append(dshEvents);
|
|
193
|
+
await handle.flush();
|
|
194
|
+
await handle.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
console.warn('[session-reader] persistence notice:', e?.message || e);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const sessions = ctx.get ? ctx.get('sessions') : ctx.sessions;
|
|
202
|
+
if (sessions && typeof sessions.create === 'function') {
|
|
203
|
+
try {
|
|
204
|
+
if (!sessions.get(dshSessionId)) {
|
|
205
|
+
sessions.create(dshSessionId, { seed: dshEvents, meta: header });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
console.warn('[session-reader] sessions.create notice:', e?.message || e);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const workspaceRegistry = ctx.get ? (ctx.get('workspaceRegistry') ?? ctx.workspaceRegistry) : ctx.workspaceRegistry;
|
|
213
|
+
let attachedWorkspaceId;
|
|
214
|
+
if (workspaceRegistry && typeof workspaceRegistry.create === 'function') {
|
|
215
|
+
try {
|
|
216
|
+
const ws = await workspaceRegistry.create(targetCwd);
|
|
217
|
+
if (ws && typeof ws.attachSession === 'function') {
|
|
218
|
+
await ws.attachSession(dshSessionId);
|
|
219
|
+
attachedWorkspaceId = ws.id;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (wsErr) {
|
|
223
|
+
console.warn('[session-reader] workspace attach notice:', wsErr?.message || wsErr);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
dshSessionId,
|
|
229
|
+
eventCount: dshEvents.length,
|
|
230
|
+
title: normalized.ref.title,
|
|
231
|
+
workspace: targetCwd,
|
|
232
|
+
workspaceId: attachedWorkspaceId,
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// 2. Register HTTP routes if ctx.webServer is present
|
|
238
|
+
if (ctx.webServer && typeof ctx.webServer.register === 'function') {
|
|
239
|
+
ctx.webServer.register({
|
|
240
|
+
kind: 'prefix',
|
|
241
|
+
path: '/api/session-reader',
|
|
242
|
+
handler: async (req, res) => {
|
|
243
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
244
|
+
const pathname = url.pathname.replace(/^\/api\/session-reader/, '');
|
|
245
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
246
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
247
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
248
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
249
|
+
if (req.method === 'OPTIONS') {
|
|
250
|
+
res.statusCode = 204;
|
|
251
|
+
res.end();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
if (pathname === '/open-in-dsh') {
|
|
256
|
+
let sessionId = url.searchParams.get('id');
|
|
257
|
+
let currentCwd = url.searchParams.get('currentCwd') ?? undefined;
|
|
258
|
+
if (!sessionId && (req.method === 'POST' || req.method === 'PUT')) {
|
|
259
|
+
const body = await new Promise((resolve) => {
|
|
260
|
+
let data = '';
|
|
261
|
+
req.on('data', chunk => data += chunk);
|
|
262
|
+
req.on('end', () => {
|
|
263
|
+
try {
|
|
264
|
+
resolve(JSON.parse(data));
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
resolve({});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
req.on('error', () => resolve({}));
|
|
271
|
+
});
|
|
272
|
+
sessionId = body?.sessionId || body?.id;
|
|
273
|
+
currentCwd = body?.currentCwd || currentCwd;
|
|
274
|
+
}
|
|
275
|
+
if (!sessionId) {
|
|
276
|
+
res.statusCode = 400;
|
|
277
|
+
res.end(JSON.stringify({ error: 'Missing sessionId' }));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const resolved = await resolveSession(sessionId);
|
|
281
|
+
if (!resolved) {
|
|
282
|
+
res.statusCode = 404;
|
|
283
|
+
res.end(JSON.stringify({ error: `Session not found: ${sessionId}` }));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const normalized = await resolved.adapter.parse(resolved.candidate);
|
|
287
|
+
const dshEvents = convertSessionToDshEvents(normalized);
|
|
288
|
+
const rawId = normalized.ref.id;
|
|
289
|
+
const sanitizedId = rawId.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
290
|
+
const dshSessionId = sanitizedId.startsWith('session-') ? sanitizedId : `session-${sanitizedId}`;
|
|
291
|
+
let targetCwd;
|
|
292
|
+
if (normalized.ref.workspace && typeof normalized.ref.workspace === 'string' && normalized.ref.workspace.startsWith('/')) {
|
|
293
|
+
try {
|
|
294
|
+
const st = await fs.promises.stat(normalized.ref.workspace);
|
|
295
|
+
if (st.isDirectory()) {
|
|
296
|
+
targetCwd = await fs.promises.realpath(normalized.ref.workspace);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch { }
|
|
300
|
+
}
|
|
301
|
+
if (!targetCwd) {
|
|
302
|
+
const fallback = currentCwd || process.cwd();
|
|
303
|
+
try {
|
|
304
|
+
const st = await fs.promises.stat(fallback);
|
|
305
|
+
if (st.isDirectory()) {
|
|
306
|
+
targetCwd = await fs.promises.realpath(fallback);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
310
|
+
}
|
|
311
|
+
if (!targetCwd)
|
|
312
|
+
targetCwd = process.cwd();
|
|
313
|
+
const createdAt = normalized.ref.createdAt ? Date.parse(normalized.ref.createdAt) : Date.now();
|
|
314
|
+
const header = {
|
|
315
|
+
version: 2,
|
|
316
|
+
id: dshSessionId,
|
|
317
|
+
cwd: targetCwd,
|
|
318
|
+
createdAt: isNaN(createdAt) ? Date.now() : createdAt,
|
|
319
|
+
isSeeded: false,
|
|
320
|
+
delegationDepth: 0,
|
|
321
|
+
};
|
|
322
|
+
// 1. Persistence if available
|
|
323
|
+
const persistence = ctx.get ? ctx.get('sessionPersistence') : ctx.sessionPersistence;
|
|
324
|
+
if (persistence && typeof persistence.create === 'function') {
|
|
325
|
+
try {
|
|
326
|
+
const existingStat = await persistence.stat(dshSessionId);
|
|
327
|
+
if (!existingStat) {
|
|
328
|
+
const handle = await persistence.create(header);
|
|
329
|
+
await handle.append(dshEvents);
|
|
330
|
+
await handle.flush();
|
|
331
|
+
await handle.close();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (persistErr) {
|
|
335
|
+
console.warn('[session-reader] persistence notice:', persistErr?.message || persistErr);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// 2. In-memory sessions if available
|
|
339
|
+
const sessions = ctx.get ? ctx.get('sessions') : ctx.sessions;
|
|
340
|
+
if (sessions && typeof sessions.create === 'function') {
|
|
341
|
+
try {
|
|
342
|
+
if (!sessions.get(dshSessionId)) {
|
|
343
|
+
sessions.create(dshSessionId, {
|
|
344
|
+
seed: dshEvents,
|
|
345
|
+
meta: header,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch (sessionErr) {
|
|
350
|
+
console.warn('[session-reader] sessions.create notice:', sessionErr?.message || sessionErr);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// 3. Workspace attachment if workspaceRegistry is available
|
|
354
|
+
const workspaceRegistry = ctx.get ? (ctx.get('workspaceRegistry') ?? ctx.workspaceRegistry) : ctx.workspaceRegistry;
|
|
355
|
+
let attachedWorkspaceId;
|
|
356
|
+
if (workspaceRegistry && typeof workspaceRegistry.create === 'function') {
|
|
357
|
+
try {
|
|
358
|
+
const ws = await workspaceRegistry.create(targetCwd);
|
|
359
|
+
if (ws && typeof ws.attachSession === 'function') {
|
|
360
|
+
await ws.attachSession(dshSessionId);
|
|
361
|
+
attachedWorkspaceId = ws.id;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (wsErr) {
|
|
365
|
+
console.warn('[session-reader] workspace attach notice:', wsErr?.message || wsErr);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
res.statusCode = 200;
|
|
369
|
+
res.end(JSON.stringify({
|
|
370
|
+
success: true,
|
|
371
|
+
dshSessionId,
|
|
372
|
+
eventCount: dshEvents.length,
|
|
373
|
+
title: normalized.ref.title,
|
|
374
|
+
workspace: targetCwd,
|
|
375
|
+
workspaceId: attachedWorkspaceId,
|
|
376
|
+
}));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const handled = await handleSessionApi(pathname, url, res, { fallbackCwd: process.cwd() });
|
|
380
|
+
if (handled)
|
|
381
|
+
return;
|
|
382
|
+
res.statusCode = 404;
|
|
383
|
+
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
res.statusCode = 500;
|
|
387
|
+
res.end(JSON.stringify({ error: err?.message ?? String(err) }));
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -20,3 +20,5 @@ export { EDGE_VERSION, EXTRACTOR_VERSION, PARSER_VERSION, SCHEMA_VERSION, } from
|
|
|
20
20
|
export { SKILL_NAME, agentTargets, bundledSkillDir, describeState, installSkill, skillStatus, uninstallSkill, type AgentStatus, type AgentTarget, type EntryState, type InstallMode, type InstallOptions, type InstallResult, type SkillAgent, type UninstallResult, } from './skill.js';
|
|
21
21
|
export { buildManifest, nodeIdentity, sessionUri, SESSION_CAPABILITIES, type AccessDescriptor, type NetworkService, type NodeManifest, } from './serve/node.js';
|
|
22
22
|
export { createServer, serve, type ServeOptions } from './serve/http.js';
|
|
23
|
+
export { convertSessionToDshEvents, type DshSessionEvent, type ConvertOptions } from './dsh/adapter.js';
|
|
24
|
+
export { apply, inject, name } from './dsh/index.js';
|
package/dist/src/index.js
CHANGED
|
@@ -18,3 +18,5 @@ export { EDGE_VERSION, EXTRACTOR_VERSION, PARSER_VERSION, SCHEMA_VERSION, } from
|
|
|
18
18
|
export { SKILL_NAME, agentTargets, bundledSkillDir, describeState, installSkill, skillStatus, uninstallSkill, } from './skill.js';
|
|
19
19
|
export { buildManifest, nodeIdentity, sessionUri, SESSION_CAPABILITIES, } from './serve/node.js';
|
|
20
20
|
export { createServer, serve } from './serve/http.js';
|
|
21
|
+
export { convertSessionToDshEvents } from './dsh/adapter.js';
|
|
22
|
+
export { apply, inject, name } from './dsh/index.js';
|
package/dist/src/resolver.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface ListOptions {
|
|
|
10
10
|
* after the first run. Set false to read the source files directly.
|
|
11
11
|
*/
|
|
12
12
|
useIndex?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* When true (default), fingerprint-sweep every on-disk candidate so the
|
|
15
|
+
* listing is complete and fresh. Set false to query the index only — no
|
|
16
|
+
* parse, no all-files sweep. The DSH panel uses this so opening it does
|
|
17
|
+
* not load every session; click a row to load that one.
|
|
18
|
+
*/
|
|
19
|
+
refresh?: boolean;
|
|
13
20
|
/**
|
|
14
21
|
* How many session files may be opened. Defaults to a small budget so an
|
|
15
22
|
* unfiltered `list` stays cheap; pass `Infinity` when completeness matters
|
package/dist/src/resolver.js
CHANGED
|
@@ -107,6 +107,18 @@ async function listIndexedSessions(options) {
|
|
|
107
107
|
const { listSessionRows, refOf } = await import('./store/read.js');
|
|
108
108
|
const db = await openStore();
|
|
109
109
|
const sinceMs = parseSince(options.since);
|
|
110
|
+
const scoped = options.workspace ? canonicalizePath(options.workspace) : undefined;
|
|
111
|
+
const query = {
|
|
112
|
+
...(scoped && scoped !== '/' ? { workspace: scoped } : {}),
|
|
113
|
+
...(options.provider ? { provider: options.provider } : {}),
|
|
114
|
+
...(sinceMs ? { sinceMs } : {}),
|
|
115
|
+
limit: options.limit ?? 20,
|
|
116
|
+
};
|
|
117
|
+
// Skip the all-files fingerprint sweep: the caller only wants whatever
|
|
118
|
+
// the index already knows. Used by the DSH panel's lazy listing.
|
|
119
|
+
if (options.refresh === false) {
|
|
120
|
+
return listSessionRows(db, query).map(refOf);
|
|
121
|
+
}
|
|
110
122
|
const ids = [];
|
|
111
123
|
for (const adapter of adapters) {
|
|
112
124
|
if (options.provider && adapter.provider !== options.provider)
|
|
@@ -119,14 +131,7 @@ async function listIndexedSessions(options) {
|
|
|
119
131
|
ids.push(result.id);
|
|
120
132
|
}
|
|
121
133
|
}
|
|
122
|
-
|
|
123
|
-
return listSessionRows(db, {
|
|
124
|
-
ids,
|
|
125
|
-
...(scoped && scoped !== '/' ? { workspace: scoped } : {}),
|
|
126
|
-
...(options.provider ? { provider: options.provider } : {}),
|
|
127
|
-
...(sinceMs ? { sinceMs } : {}),
|
|
128
|
-
limit: options.limit ?? 20,
|
|
129
|
-
}).map(refOf);
|
|
134
|
+
return listSessionRows(db, { ids, ...query }).map(refOf);
|
|
130
135
|
}
|
|
131
136
|
export async function findSessionsByWorkspace(workspacePath, options = {}) {
|
|
132
137
|
return listRecentSessions({ ...options, limit: options.limit ?? 50, workspace: workspacePath });
|
package/dist/src/serve/http.js
CHANGED
|
@@ -172,7 +172,19 @@ export function createServer(options = {}) {
|
|
|
172
172
|
export async function serve(options = {}) {
|
|
173
173
|
const host = options.host ?? '127.0.0.1';
|
|
174
174
|
const server = createServer(options);
|
|
175
|
-
|
|
175
|
+
// 请求的端口。实际绑定到哪个从 server.address() 读——传 0 时它是随机的。
|
|
176
|
+
const wanted = options.port ?? DEFAULT_PORT;
|
|
177
|
+
// 端口占用是最常见的启动失败,默认会抛一整屏 Node 栈——对着栈猜"是不是
|
|
178
|
+
// 已经起了一个"没意义,直接说清楚怎么办。
|
|
179
|
+
await new Promise((resolve, reject) => {
|
|
180
|
+
server.once('error', (error) => {
|
|
181
|
+
reject(error.code === 'EADDRINUSE'
|
|
182
|
+
? new Error(`端口 ${wanted} 已被占用。换一个:--port <n>;或先停掉占用它的进程:` +
|
|
183
|
+
`lsof -nP -iTCP:${wanted} -sTCP:LISTEN`)
|
|
184
|
+
: error);
|
|
185
|
+
});
|
|
186
|
+
server.listen(wanted, host, resolve);
|
|
187
|
+
});
|
|
176
188
|
const { port } = server.address();
|
|
177
189
|
const identity = await nodeIdentity();
|
|
178
190
|
// 只听回环的服务,外部发现得了却连不上——如实说,别让调用方白跑一趟。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 写进 SQLite 的 TEXT 列之前调用。
|
|
3
|
+
*
|
|
4
|
+
* 绝大多数内容两个字符都不含,直接原样返回——所以常态是零拷贝。
|
|
5
|
+
*/
|
|
6
|
+
export declare function encodeText(value: string): string;
|
|
7
|
+
export declare function encodeText(value: string | null | undefined): string | null;
|
|
8
|
+
/** 从 SQLite 的 TEXT 列读出来之后调用。 */
|
|
9
|
+
export declare function decodeText(value: string): string;
|
|
10
|
+
export declare function decodeText(value: string | null | undefined): string | null;
|
|
11
|
+
/** 测试与诊断用:这段文本经过 SQLite 的 TEXT 列会不会被截断。 */
|
|
12
|
+
export declare function wouldTruncate(value: string): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite 的 TEXT 列在第一个 NUL 处截断——不报错,安静地丢掉后面全部内容。
|
|
3
|
+
*
|
|
4
|
+
* 实测:一个 antigravity 会话里 `wsl -l -v` 的 UTF-16 输出被当 UTF-8 读,产生
|
|
5
|
+
* 交错的 NUL;502 字符的 tool_result 存进索引再读出来只剩 342,后面 160 个
|
|
6
|
+
* 字符凭空消失。round-trip 测试因此长期飘红,而失败信息指向的是内容本身,
|
|
7
|
+
* 很难看出是存储层干的。
|
|
8
|
+
*
|
|
9
|
+
* 改存 BLOB 能保真,但 `text` / `tool_result` 上有 SQL 搜索(`LIKE` 对 BLOB
|
|
10
|
+
* 不工作),所以改成**写入时转义、读取时还原**。
|
|
11
|
+
*
|
|
12
|
+
* 引导符用 U+FFFF:Unicode 明确规定的 noncharacter,不会出现在有效文本里;
|
|
13
|
+
* 万一真出现也会被双写,所以还原无歧义。
|
|
14
|
+
*
|
|
15
|
+
* 两个常量用 `String.fromCharCode` 而不是字面量,免得源文件里真带上这些
|
|
16
|
+
* 字符——它们在编辑器、diff、终端里都是隐形的。
|
|
17
|
+
*/
|
|
18
|
+
const NUL = String.fromCharCode(0x00);
|
|
19
|
+
const LEAD = String.fromCharCode(0xffff);
|
|
20
|
+
const ESCAPED_NUL = `${LEAD}0`;
|
|
21
|
+
const ESCAPED_LEAD = `${LEAD}${LEAD}`;
|
|
22
|
+
export function encodeText(value) {
|
|
23
|
+
if (value === null || value === undefined)
|
|
24
|
+
return null;
|
|
25
|
+
if (!value.includes(NUL) && !value.includes(LEAD))
|
|
26
|
+
return value;
|
|
27
|
+
// 顺序要紧:先把引导符自己双写,再拿它去转义 NUL。反过来会把刚写出的
|
|
28
|
+
// 转义序列又转义一遍。
|
|
29
|
+
return value.split(LEAD).join(ESCAPED_LEAD).split(NUL).join(ESCAPED_NUL);
|
|
30
|
+
}
|
|
31
|
+
export function decodeText(value) {
|
|
32
|
+
if (value === null || value === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
if (!value.includes(LEAD))
|
|
35
|
+
return value;
|
|
36
|
+
let out = '';
|
|
37
|
+
for (let i = 0; i < value.length; i++) {
|
|
38
|
+
if (value[i] !== LEAD) {
|
|
39
|
+
out += value[i];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const next = value[++i];
|
|
43
|
+
// 双写还原成引导符本身;LEAD+'0' 还原成 NUL;落单的引导符原样留着,
|
|
44
|
+
// 宁可多留一个字符,也不要把不认识的序列吞掉。
|
|
45
|
+
out += next === LEAD ? LEAD : next === '0' ? NUL : LEAD + (next ?? '');
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** 测试与诊断用:这段文本经过 SQLite 的 TEXT 列会不会被截断。 */
|
|
50
|
+
export function wouldTruncate(value) {
|
|
51
|
+
return value.includes(NUL);
|
|
52
|
+
}
|
package/dist/src/store/read.d.ts
CHANGED
|
@@ -26,8 +26,12 @@ export declare function sessionRow(db: DatabaseSync, id: string): SessionRow | u
|
|
|
26
26
|
/** Resolves a full id, an id prefix, or a native id against the index. */
|
|
27
27
|
export declare function findSessionRow(db: DatabaseSync, needle: string): SessionRow | undefined;
|
|
28
28
|
export interface ListQuery {
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Ids seen on disk during this sweep; rows outside it are stale and skipped.
|
|
31
|
+
* Omit to list from the whole index (lazy UI listings that skip the sweep).
|
|
32
|
+
* An explicit empty array still means "nothing in scope".
|
|
33
|
+
*/
|
|
34
|
+
ids?: string[];
|
|
31
35
|
/** Canonical root; matches the directory itself and everything under it. */
|
|
32
36
|
workspace?: string;
|
|
33
37
|
provider?: string;
|
package/dist/src/store/read.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeText } from './nul.js';
|
|
1
2
|
import { turnStartsFrom } from '../turns.js';
|
|
2
3
|
import { emptyProviderStats, } from '../types.js';
|
|
3
4
|
export function sessionRow(db, id) {
|
|
@@ -21,15 +22,18 @@ export function findSessionRow(db, needle) {
|
|
|
21
22
|
* workspace never silently loses its older sessions to a candidate cap.
|
|
22
23
|
*/
|
|
23
24
|
export function listSessionRows(db, query) {
|
|
24
|
-
|
|
25
|
-
return [];
|
|
26
|
-
db.exec('DROP TABLE IF EXISTS temp.list_scope');
|
|
27
|
-
db.exec('CREATE TEMP TABLE list_scope (id TEXT PRIMARY KEY)');
|
|
28
|
-
const insert = db.prepare('INSERT OR IGNORE INTO temp.list_scope (id) VALUES (?)');
|
|
29
|
-
for (const id of query.ids)
|
|
30
|
-
insert.run(id);
|
|
31
|
-
const where = ['id IN (SELECT id FROM temp.list_scope)'];
|
|
25
|
+
const where = [];
|
|
32
26
|
const params = [];
|
|
27
|
+
if (query.ids) {
|
|
28
|
+
if (!query.ids.length)
|
|
29
|
+
return [];
|
|
30
|
+
db.exec('DROP TABLE IF EXISTS temp.list_scope');
|
|
31
|
+
db.exec('CREATE TEMP TABLE list_scope (id TEXT PRIMARY KEY)');
|
|
32
|
+
const insert = db.prepare('INSERT OR IGNORE INTO temp.list_scope (id) VALUES (?)');
|
|
33
|
+
for (const id of query.ids)
|
|
34
|
+
insert.run(id);
|
|
35
|
+
where.push('id IN (SELECT id FROM temp.list_scope)');
|
|
36
|
+
}
|
|
33
37
|
if (query.workspace) {
|
|
34
38
|
// Prefix comparison rather than LIKE: real paths contain `_`, a LIKE wildcard.
|
|
35
39
|
where.push('(workspace = ? OR substr(workspace, 1, ?) = ?)');
|
|
@@ -44,11 +48,10 @@ export function listSessionRows(db, query) {
|
|
|
44
48
|
params.push(query.sinceMs);
|
|
45
49
|
}
|
|
46
50
|
params.push(query.limit ?? 20);
|
|
47
|
-
|
|
48
|
-
.prepare(`SELECT * FROM sessions WHERE ${where.join(' AND ')} ` +
|
|
51
|
+
const sql = `SELECT * FROM sessions${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ` +
|
|
49
52
|
// Undated rows sort last instead of first, which is where DESC puts NULL.
|
|
50
|
-
'ORDER BY (ended_at IS NULL), ended_at DESC LIMIT ?'
|
|
51
|
-
|
|
53
|
+
'ORDER BY (ended_at IS NULL), ended_at DESC LIMIT ?';
|
|
54
|
+
return db.prepare(sql).all(...params);
|
|
52
55
|
}
|
|
53
56
|
/**
|
|
54
57
|
* Turn starts for one indexed session, without rebuilding it.
|
|
@@ -70,7 +73,7 @@ export function refOf(row) {
|
|
|
70
73
|
id: row.native_id,
|
|
71
74
|
provider: row.provider,
|
|
72
75
|
path: row.source_path,
|
|
73
|
-
...(row.title === null ? {} : { title: row.title }),
|
|
76
|
+
...(row.title === null ? {} : { title: decodeText(row.title) }),
|
|
74
77
|
...(row.workspace === null ? {} : { workspace: row.workspace }),
|
|
75
78
|
...(row.started_at === null ? {} : { createdAt: row.started_at }),
|
|
76
79
|
...(row.ended_at === null ? {} : { updatedAt: row.ended_at }),
|
|
@@ -89,12 +92,12 @@ function eventsOf(db, id, nativeId) {
|
|
|
89
92
|
id: `${nativeId}#${row.idx}`,
|
|
90
93
|
index: row.idx,
|
|
91
94
|
kind: row.kind,
|
|
92
|
-
...(row.text === null ? {} : { text: row.text }),
|
|
95
|
+
...(row.text === null ? {} : { text: decodeText(row.text) }),
|
|
93
96
|
...(row.tool_name === null ? {} : { toolName: row.tool_name }),
|
|
94
97
|
...(row.tool_args_json === null
|
|
95
98
|
? {}
|
|
96
99
|
: { toolArgs: JSON.parse(row.tool_args_json) }),
|
|
97
|
-
...(row.tool_result === null ? {} : { toolResult: row.tool_result }),
|
|
100
|
+
...(row.tool_result === null ? {} : { toolResult: decodeText(row.tool_result) }),
|
|
98
101
|
...(row.is_error === null ? {} : { isError: row.is_error === 1 }),
|
|
99
102
|
...(row.ts === null ? {} : { timestamp: row.ts }),
|
|
100
103
|
...(row.source_index === null ? {} : { sourceIndex: row.source_index }),
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* the behaviour — never derive them from a git hash, `dist` ships without git.
|
|
8
8
|
*/
|
|
9
9
|
/** DDL layout. A bump drops and rebuilds the whole database. */
|
|
10
|
-
export declare const SCHEMA_VERSION =
|
|
10
|
+
export declare const SCHEMA_VERSION = 2;
|
|
11
11
|
/** L1 semantics — anything in `parsers/` that changes normalized events. */
|
|
12
12
|
export declare const PARSER_VERSION = 3;
|
|
13
13
|
/** L2 rules — `writes.ts` / `ledger.ts`. Re-derives facts from stored events. */
|