@1agents/session-reader 0.6.2 → 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.
@@ -0,0 +1,2 @@
1
+ export * from './adapter.js';
2
+ export * from './plugin.js';
@@ -0,0 +1,2 @@
1
+ export * from './adapter.js';
2
+ export * from './plugin.js';
@@ -0,0 +1,3 @@
1
+ export declare const name = "session-reader";
2
+ export declare const inject: string[];
3
+ export declare function apply(ctx: any): void;
@@ -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
+ }
@@ -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';
@@ -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
@@ -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
- const scoped = options.workspace ? canonicalizePath(options.workspace) : undefined;
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 });
@@ -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
- /** Ids seen on disk during this sweep; rows outside it are stale and skipped. */
30
- ids: string[];
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;
@@ -22,15 +22,18 @@ export function findSessionRow(db, needle) {
22
22
  * workspace never silently loses its older sessions to a candidate cap.
23
23
  */
24
24
  export function listSessionRows(db, query) {
25
- if (!query.ids.length)
26
- return [];
27
- db.exec('DROP TABLE IF EXISTS temp.list_scope');
28
- db.exec('CREATE TEMP TABLE list_scope (id TEXT PRIMARY KEY)');
29
- const insert = db.prepare('INSERT OR IGNORE INTO temp.list_scope (id) VALUES (?)');
30
- for (const id of query.ids)
31
- insert.run(id);
32
- const where = ['id IN (SELECT id FROM temp.list_scope)'];
25
+ const where = [];
33
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
+ }
34
37
  if (query.workspace) {
35
38
  // Prefix comparison rather than LIKE: real paths contain `_`, a LIKE wildcard.
36
39
  where.push('(workspace = ? OR substr(workspace, 1, ?) = ?)');
@@ -45,11 +48,10 @@ export function listSessionRows(db, query) {
45
48
  params.push(query.sinceMs);
46
49
  }
47
50
  params.push(query.limit ?? 20);
48
- return db
49
- .prepare(`SELECT * FROM sessions WHERE ${where.join(' AND ')} ` +
51
+ const sql = `SELECT * FROM sessions${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ` +
50
52
  // Undated rows sort last instead of first, which is where DESC puts NULL.
51
- 'ORDER BY (ended_at IS NULL), ended_at DESC LIMIT ?')
52
- .all(...params);
53
+ 'ORDER BY (ended_at IS NULL), ended_at DESC LIMIT ?';
54
+ return db.prepare(sql).all(...params);
53
55
  }
54
56
  /**
55
57
  * Turn starts for one indexed session, without rebuilding it.
@@ -0,0 +1,72 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ import type { NormalizedSession } from '../types.js';
3
+ export interface SessionApiOptions {
4
+ /** Used when the client asks for scope=cwd but does not send ?workspace=. */
5
+ fallbackCwd: string;
6
+ }
7
+ export declare function resolveListWorkspace(scope: string | null, workspace: string | null, fallbackCwd: string): string | undefined;
8
+ /** Chat-preview turns. Built only after the user clicks a session. */
9
+ export declare function previewTurns(normalized: NormalizedSession): {
10
+ no: number;
11
+ turn: number;
12
+ status: import("../types.js").TurnStatus;
13
+ prompt: string;
14
+ userPrompt: string;
15
+ outcome: string;
16
+ assistantReply: string;
17
+ thinking: string;
18
+ thinkingBlocks: string[];
19
+ toolCalls: any[];
20
+ files: string[];
21
+ commands: number;
22
+ errors: number;
23
+ startedAt: string | undefined;
24
+ endedAt: string | undefined;
25
+ durationMs: number | undefined;
26
+ }[];
27
+ export declare function sessionPreview(sessionId: string): Promise<{
28
+ ref: import("../types.js").SessionRef;
29
+ stats: import("../types.js").ProviderStats;
30
+ overview: import("../types.js").SessionOverview;
31
+ turns: {
32
+ no: number;
33
+ turn: number;
34
+ status: import("../types.js").TurnStatus;
35
+ prompt: string;
36
+ userPrompt: string;
37
+ outcome: string;
38
+ assistantReply: string;
39
+ thinking: string;
40
+ thinkingBlocks: string[];
41
+ toolCalls: any[];
42
+ files: string[];
43
+ commands: number;
44
+ errors: number;
45
+ startedAt: string | undefined;
46
+ endedAt: string | undefined;
47
+ durationMs: number | undefined;
48
+ }[];
49
+ files: {
50
+ displayPath: string;
51
+ path: string;
52
+ host?: string;
53
+ operation: string;
54
+ turn: number;
55
+ eventIndex: number;
56
+ timestamp?: string;
57
+ provenance: import("../types.js").Provenance;
58
+ extractor: string;
59
+ group: import("../types.js").FileGroup;
60
+ }[];
61
+ }>;
62
+ export declare function listSessionsForUi(options: {
63
+ limit?: number;
64
+ provider?: string;
65
+ since?: string;
66
+ workspace?: string;
67
+ }): Promise<import("../types.js").SessionRef[]>;
68
+ /**
69
+ * Shared JSON API used by the DSH plugin and `1session web`.
70
+ * Path is already stripped of `/api/session-reader`. Returns false if unmatched.
71
+ */
72
+ export declare function handleSessionApi(pathname: string, url: URL, res: ServerResponse, options: SessionApiOptions): Promise<boolean>;