@1agents/session-reader 0.1.1

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.
Files changed (56) hide show
  1. package/README.md +347 -0
  2. package/dist/bin/1session.d.ts +2 -0
  3. package/dist/bin/1session.js +408 -0
  4. package/dist/src/aggregator.d.ts +12 -0
  5. package/dist/src/aggregator.js +136 -0
  6. package/dist/src/classify.d.ts +2 -0
  7. package/dist/src/classify.js +11 -0
  8. package/dist/src/distiller.d.ts +7 -0
  9. package/dist/src/distiller.js +122 -0
  10. package/dist/src/index.d.ts +19 -0
  11. package/dist/src/index.js +17 -0
  12. package/dist/src/ledger.d.ts +19 -0
  13. package/dist/src/ledger.js +192 -0
  14. package/dist/src/overview.d.ts +6 -0
  15. package/dist/src/overview.js +324 -0
  16. package/dist/src/parsers/antigravity.d.ts +4 -0
  17. package/dist/src/parsers/antigravity.js +297 -0
  18. package/dist/src/parsers/claude.d.ts +2 -0
  19. package/dist/src/parsers/claude.js +214 -0
  20. package/dist/src/parsers/codex.d.ts +2 -0
  21. package/dist/src/parsers/codex.js +316 -0
  22. package/dist/src/parsers/provider.d.ts +24 -0
  23. package/dist/src/parsers/provider.js +5 -0
  24. package/dist/src/resolver.d.ts +42 -0
  25. package/dist/src/resolver.js +145 -0
  26. package/dist/src/search.d.ts +59 -0
  27. package/dist/src/search.js +257 -0
  28. package/dist/src/store/db.d.ts +10 -0
  29. package/dist/src/store/db.js +89 -0
  30. package/dist/src/store/edges.d.ts +46 -0
  31. package/dist/src/store/edges.js +153 -0
  32. package/dist/src/store/facts.d.ts +8 -0
  33. package/dist/src/store/facts.js +39 -0
  34. package/dist/src/store/indexer.d.ts +40 -0
  35. package/dist/src/store/indexer.js +62 -0
  36. package/dist/src/store/read.d.ts +29 -0
  37. package/dist/src/store/read.js +70 -0
  38. package/dist/src/store/rows.d.ts +35 -0
  39. package/dist/src/store/rows.js +55 -0
  40. package/dist/src/store/schema.d.ts +19 -0
  41. package/dist/src/store/schema.js +142 -0
  42. package/dist/src/store/write.d.ts +22 -0
  43. package/dist/src/store/write.js +72 -0
  44. package/dist/src/turns.d.ts +14 -0
  45. package/dist/src/turns.js +156 -0
  46. package/dist/src/types.d.ts +294 -0
  47. package/dist/src/types.js +13 -0
  48. package/dist/src/util/jsonl.d.ts +6 -0
  49. package/dist/src/util/jsonl.js +32 -0
  50. package/dist/src/util/paths.d.ts +15 -0
  51. package/dist/src/util/paths.js +69 -0
  52. package/dist/src/util/text.d.ts +7 -0
  53. package/dist/src/util/text.js +48 -0
  54. package/dist/src/writes.d.ts +35 -0
  55. package/dist/src/writes.js +204 -0
  56. package/package.json +58 -0
@@ -0,0 +1,122 @@
1
+ import path from 'node:path';
2
+ import { isInside } from './util/paths.js';
3
+ import { clip, oneLine } from './util/text.js';
4
+ import { fileWrites, rawCommand, resolveWritePath } from './writes.js';
5
+ /** Paths a tool call wrote to, including files written through the shell. */
6
+ export function editedFiles(turn, workspace) {
7
+ return fileWrites(turn).map((write) => resolveWritePath(write, workspace));
8
+ }
9
+ export function shellCommand(turn) {
10
+ const command = rawCommand(turn);
11
+ return command ? oneLine(command, 200) : undefined;
12
+ }
13
+ function relativize(file, workspace) {
14
+ return workspace && isInside(workspace, file) ? path.relative(workspace, file) || '.' : file;
15
+ }
16
+ function collectTurningPoints(turns) {
17
+ const points = [];
18
+ const users = turns.filter((t) => t.kind === 'user' && t.text?.trim());
19
+ users.forEach((turn, i) => {
20
+ points.push({
21
+ timestamp: turn.timestamp,
22
+ kind: i === 0 ? 'request' : 'redirect',
23
+ text: oneLine(turn.text, 220),
24
+ });
25
+ });
26
+ for (const turn of turns.filter((t) => t.kind === 'tool_result' && t.isError).slice(0, 5)) {
27
+ points.push({ timestamp: turn.timestamp, kind: 'failure', text: oneLine(turn.toolResult, 200) });
28
+ }
29
+ const final = [...turns].reverse().find((t) => t.kind === 'assistant' && t.text?.trim());
30
+ if (final) {
31
+ points.push({ timestamp: final.timestamp, kind: 'outcome', text: oneLine(final.text, 300) });
32
+ }
33
+ return points.sort((a, b) => Date.parse(a.timestamp ?? '') - Date.parse(b.timestamp ?? ''));
34
+ }
35
+ function bullets(items, max) {
36
+ const shown = items.slice(0, max).map((item) => `- ${item}`);
37
+ if (items.length > max)
38
+ shown.push(`- …另有 ${items.length - max} 项`);
39
+ return shown.length ? shown.join('\n') : '- (无)';
40
+ }
41
+ const KIND_LABEL = {
42
+ request: '需求',
43
+ redirect: '转向',
44
+ failure: '受阻',
45
+ outcome: '结论',
46
+ };
47
+ function render(digest, turns, focus) {
48
+ const { session, goal, touchedFiles, commands, turningPoints, artifacts } = digest;
49
+ const head = [
50
+ `# ${session.title ?? session.id}`,
51
+ '',
52
+ `- 智能体:${session.provider} 会话:\`${session.id}\``,
53
+ `- 工作区:${session.workspace ?? '未知'}`,
54
+ `- 时间:${session.createdAt ?? '?'} → ${session.updatedAt ?? '?'}`,
55
+ `- 规模:${turns.length} 轮事件,改动 ${touchedFiles.length} 个文件,执行 ${commands.length} 条命令`,
56
+ '',
57
+ '## 目标',
58
+ '',
59
+ goal || '(未能识别)',
60
+ '',
61
+ ];
62
+ const files = ['## 改动的文件', '', bullets(touchedFiles, 25), ''];
63
+ const points = [
64
+ '## 关键节点',
65
+ '',
66
+ turningPoints.length
67
+ ? turningPoints
68
+ .slice(0, 10)
69
+ .map((p) => `- **${KIND_LABEL[p.kind]}**:${p.text}`)
70
+ .join('\n')
71
+ : '- (无)',
72
+ '',
73
+ ];
74
+ const artifactSection = artifacts.length
75
+ ? ['## 产出物', '', bullets(artifacts.map((a) => `${a.name} — ${a.path}`), 10), '']
76
+ : [];
77
+ if (focus === 'marketing') {
78
+ const highlights = turningPoints.filter((p) => p.kind !== 'request').slice(0, 6);
79
+ return [
80
+ ...head,
81
+ '## 亮点',
82
+ '',
83
+ highlights.length ? highlights.map((p) => `- ${KIND_LABEL[p.kind]}:${p.text}`).join('\n') : '- (无)',
84
+ '',
85
+ ...files,
86
+ ...artifactSection,
87
+ ].join('\n');
88
+ }
89
+ const body = [...head, ...files, '## 执行的命令', '', bullets(commands, 15), '', ...points, ...artifactSection];
90
+ if (focus === 'review')
91
+ return body.join('\n');
92
+ const skeleton = turns
93
+ .map((turn) => {
94
+ const label = turn.toolName ? `${turn.kind}(${turn.toolName})` : turn.kind;
95
+ return `${String(turn.index).padStart(3, ' ')} ${label}: ${oneLine(turn.text ?? turn.toolResult, 110)}`;
96
+ })
97
+ .slice(0, 120);
98
+ return [...body, '## 轮次骨架', '', '```', ...skeleton, '```', ''].join('\n');
99
+ }
100
+ export function distillSession(session, options = {}) {
101
+ const focus = options.focus ?? 'review';
102
+ const workspace = session.ref.workspace;
103
+ const touched = new Set();
104
+ const commands = [];
105
+ for (const turn of session.turns) {
106
+ for (const file of editedFiles(turn, workspace))
107
+ touched.add(relativize(file, workspace));
108
+ const command = shellCommand(turn);
109
+ if (command && !commands.includes(command))
110
+ commands.push(command);
111
+ }
112
+ const firstUser = session.turns.find((t) => t.kind === 'user' && t.text?.trim());
113
+ const digest = {
114
+ session: session.ref,
115
+ goal: clip(firstUser?.text, focus === 'full' ? 1200 : 400),
116
+ touchedFiles: [...touched].sort(),
117
+ commands,
118
+ turningPoints: collectTurningPoints(session.turns),
119
+ artifacts: session.artifacts,
120
+ };
121
+ return { ...digest, markdown: render(digest, session.turns, focus) };
122
+ }
@@ -0,0 +1,19 @@
1
+ export type { AgentProvider, BackgroundTask, DigestFocus, FileChange, FileTouch, GitCommit, ProviderStats, SessionOverview, SessionStats, TokenUsage, AsyncJob, CommandRecord, FactSource, FileGroup, FileRecord, Provenance, JobStatus, TurnDetail, TurnStatus, TurnSummary, UserTurnKind, UserTurnNote, NormalizedSession, SessionArtifact, SessionDigest, SessionRef, TurnEvent, TurnKind, TurningPoint, UnifiedTimelineEntry, WorkspaceDigest, } from './types.js';
2
+ export { adapters, findResolvedByWorkspace, loadSession, type LoadOptions, findSessionsByWorkspace, listRecentSessions, listResolvedSessions, parseSession, parseSince, resolveSession, type ListOptions, type ResolvedSession, } from './resolver.js';
3
+ export { distillSession, editedFiles, shellCommand } from './distiller.js';
4
+ export { buildOverview } from './overview.js';
5
+ export { classifyUserTurn } from './classify.js';
6
+ export { commandLedger, displayPath, errorLedger, fileLedger, jobCounts, jobLedger, } from './ledger.js';
7
+ export { eventDetail, summarizeTurns, turnDetail, type EventDetail } from './turns.js';
8
+ export { analyzableCommand, fileWrites, rawCommand, stripHeredocs, resolveWritePath, type FileWrite, } from './writes.js';
9
+ export { mandatoryLiteral, planQuery, searchSessions, type QueryPlan, type SearchHit, type SearchMatch, type SearchOptions, } from './search.js';
10
+ export { aggregateWorkspaceSessions, type AggregateOptions } from './aggregator.js';
11
+ export type { ProviderAdapter, SessionCandidate } from './parsers/provider.js';
12
+ export { canonicalizePath, isInside, slugifyWorkspace } from './util/paths.js';
13
+ export { defaultDbPath, openStore, resetStoreCache } from './store/db.js';
14
+ export { indexSession, refreshSession, type IndexAction, type IndexOptions, type IndexResult, type RefreshResult, } from './store/indexer.js';
15
+ export { searchRows, type RowQuery, type TextRow } from './store/rows.js';
16
+ export { findSessionRow, readSession, sessionRow, type SessionRow } from './store/read.js';
17
+ export { captureRuntimeEdge, deriveEdges, edgeEvidence, edgesOf, invocationsOf, type EdgeRelation, type EdgeView, } from './store/edges.js';
18
+ export { deriveFacts } from './store/facts.js';
19
+ export { EDGE_VERSION, EXTRACTOR_VERSION, PARSER_VERSION, SCHEMA_VERSION, } from './store/schema.js';
@@ -0,0 +1,17 @@
1
+ export { adapters, findResolvedByWorkspace, loadSession, findSessionsByWorkspace, listRecentSessions, listResolvedSessions, parseSession, parseSince, resolveSession, } from './resolver.js';
2
+ export { distillSession, editedFiles, shellCommand } from './distiller.js';
3
+ export { buildOverview } from './overview.js';
4
+ export { classifyUserTurn } from './classify.js';
5
+ export { commandLedger, displayPath, errorLedger, fileLedger, jobCounts, jobLedger, } from './ledger.js';
6
+ export { eventDetail, summarizeTurns, turnDetail } from './turns.js';
7
+ export { analyzableCommand, fileWrites, rawCommand, stripHeredocs, resolveWritePath, } from './writes.js';
8
+ export { mandatoryLiteral, planQuery, searchSessions, } from './search.js';
9
+ export { aggregateWorkspaceSessions } from './aggregator.js';
10
+ export { canonicalizePath, isInside, slugifyWorkspace } from './util/paths.js';
11
+ export { defaultDbPath, openStore, resetStoreCache } from './store/db.js';
12
+ export { indexSession, refreshSession, } from './store/indexer.js';
13
+ export { searchRows } from './store/rows.js';
14
+ export { findSessionRow, readSession, sessionRow } from './store/read.js';
15
+ export { captureRuntimeEdge, deriveEdges, edgeEvidence, edgesOf, invocationsOf, } from './store/edges.js';
16
+ export { deriveFacts } from './store/facts.js';
17
+ export { EDGE_VERSION, EXTRACTOR_VERSION, PARSER_VERSION, SCHEMA_VERSION, } from './store/schema.js';
@@ -0,0 +1,19 @@
1
+ import type { AsyncJob, CommandRecord, FileRecord, JobStatus, NormalizedSession } from './types.js';
2
+ /**
3
+ * Every shell command with whatever is known about how it went. Codex records
4
+ * this itself; for the others we pair each call with the result that follows.
5
+ */
6
+ export declare function commandLedger(session: NormalizedSession): CommandRecord[];
7
+ /**
8
+ * Failed commands, plus whether a similar command later succeeded.
9
+ * A command counts as failed only on a non-zero exit, or on an unknown exit
10
+ * that the provider itself flagged — never merely because stderr had content.
11
+ */
12
+ export declare function errorLedger(session: NormalizedSession): CommandRecord[];
13
+ /** Background jobs, merged from provider receipts, shell launches and pids. */
14
+ export declare function jobLedger(session: NormalizedSession): AsyncJob[];
15
+ export declare function jobCounts(jobs: AsyncJob[]): Record<JobStatus, number>;
16
+ /** Every file the session wrote, bucketed by what kind of file it is. */
17
+ export declare function fileLedger(session: NormalizedSession): FileRecord[];
18
+ /** Display helper shared by the CLI: workspace-relative when possible. */
19
+ export declare function displayPath(record: FileRecord, workspace: string | undefined): string;
@@ -0,0 +1,192 @@
1
+ import path from 'node:path';
2
+ import { isInside } from './util/paths.js';
3
+ import { oneLine } from './util/text.js';
4
+ import { analyzableCommand, fileWrites, rawCommand, resolveWritePath } from './writes.js';
5
+ import { summarizeTurns } from './turns.js';
6
+ const SSH_HOST = /\b[\w.-]+@((?:\d{1,3}(?:\.\d{1,3}){3})|(?:[\w-]+(?:\.[\w-]+)+))/;
7
+ const BACKGROUND = /\b(?:nohup|setsid|start_new_session\s*=\s*True|subprocess\.Popen|Popen\(|queue-[\w-]+\.py)\b/;
8
+ const LOG_PATH = /([\w./-]+\.log)\b/;
9
+ const PID_ECHO = /\b(?:pid|PID)[\s=:]+(\d{2,7})\b/;
10
+ const LOG_FILE = /(?:\.log|\.download\.log)$|progress\.jsonl?$/i;
11
+ const TEMP_DIR = /^\/(?:private\/)?(?:tmp|var\/folders)\//;
12
+ /** Maps each event index to the turn it belongs to. */
13
+ function turnOf(session) {
14
+ const ranges = summarizeTurns(session).map((turn) => turn.events);
15
+ return (index) => {
16
+ const found = ranges.findIndex(([start, end]) => index >= start && index <= end);
17
+ return found === -1 ? 0 : found + 1;
18
+ };
19
+ }
20
+ /**
21
+ * Every shell command with whatever is known about how it went. Codex records
22
+ * this itself; for the others we pair each call with the result that follows.
23
+ */
24
+ export function commandLedger(session) {
25
+ const toTurn = turnOf(session);
26
+ if (session.stats.commands.length) {
27
+ // Codex keeps its own ledger; align it with the event stream by order.
28
+ const callIndexes = session.turns.filter((event) => rawCommand(event)).map((event) => event.index);
29
+ return session.stats.commands.map((record, i) => {
30
+ const eventIndex = callIndexes[i] ?? -1;
31
+ return { ...record, eventIndex, turn: eventIndex >= 0 ? toTurn(eventIndex) : 0 };
32
+ });
33
+ }
34
+ const records = [];
35
+ for (const event of session.turns) {
36
+ const command = rawCommand(event);
37
+ if (!command)
38
+ continue;
39
+ // The next tool_result is this command's outcome.
40
+ const result = session.turns
41
+ .slice(event.index + 1, event.index + 4)
42
+ .find((candidate) => candidate.kind === 'tool_result');
43
+ const host = SSH_HOST.exec(command)?.[1];
44
+ records.push({
45
+ eventIndex: event.index,
46
+ turn: toTurn(event.index),
47
+ command: oneLine(command, 300),
48
+ provenance: 'derived',
49
+ extractor: 'pair:tool_call+tool_result',
50
+ ...(host ? { host } : {}),
51
+ ...(event.timestamp ? { timestamp: event.timestamp } : {}),
52
+ // An explicit status wins; otherwise "provider did not flag an error"
53
+ // is itself the provider's verdict, not a guess of ours.
54
+ ...(result?.exitCode !== undefined
55
+ ? { exitCode: result.exitCode }
56
+ : result && result.isError === false
57
+ ? { exitCode: 0 }
58
+ : {}),
59
+ ...(result?.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
60
+ ...(result?.isError && result.toolResult ? { stderr: oneLine(result.toolResult, 300) } : {}),
61
+ });
62
+ }
63
+ return records;
64
+ }
65
+ /**
66
+ * Failed commands, plus whether a similar command later succeeded.
67
+ * A command counts as failed only on a non-zero exit, or on an unknown exit
68
+ * that the provider itself flagged — never merely because stderr had content.
69
+ */
70
+ export function errorLedger(session) {
71
+ const commands = commandLedger(session);
72
+ const failed = commands.filter((record) => (record.exitCode !== undefined && record.exitCode !== 0) || (record.exitCode === undefined && record.stderr));
73
+ const prefix = (command) => command.split(/\s+/).slice(0, 3).join(' ');
74
+ return failed.map((record) => ({
75
+ ...record,
76
+ laterSucceeded: commands.some((other) => other.eventIndex > record.eventIndex &&
77
+ other.exitCode === 0 &&
78
+ prefix(other.command) === prefix(record.command)),
79
+ }));
80
+ }
81
+ /** Background jobs, merged from provider receipts, shell launches and pids. */
82
+ export function jobLedger(session) {
83
+ const jobs = new Map();
84
+ for (const task of session.stats.backgroundTasks) {
85
+ jobs.set(task.id, {
86
+ id: task.id,
87
+ ...(task.title ? { command: task.title } : {}),
88
+ ...(task.log ? { log: task.log } : {}),
89
+ provenance: 'observed',
90
+ extractor: 'receipt:messages',
91
+ status: task.finished ? 'completed' : 'unknown',
92
+ evidence: task.finished ? ['provider 有完成回执'] : ['provider 记录了任务,但没有完成回执'],
93
+ });
94
+ }
95
+ for (const event of session.turns) {
96
+ const command = analyzableCommand(event);
97
+ if (!command || !BACKGROUND.test(command))
98
+ continue;
99
+ const segment = command.split(/[\n;]|&&/).find((part) => BACKGROUND.test(part)) ?? command;
100
+ const log = LOG_PATH.exec(segment)?.[1] ?? LOG_PATH.exec(command)?.[1];
101
+ const id = log ?? `event-${event.index}`;
102
+ if (jobs.has(id))
103
+ continue;
104
+ const host = SSH_HOST.exec(command)?.[1];
105
+ jobs.set(id, {
106
+ id,
107
+ command: oneLine(segment, 200),
108
+ ...(log ? { log } : {}),
109
+ ...(host ? { host } : {}),
110
+ ...(event.timestamp ? { startedAt: event.timestamp } : {}),
111
+ discoveredFrom: event.index,
112
+ provenance: 'derived',
113
+ extractor: 'shell:background-launch',
114
+ // No receipt, no claim: we only saw it start.
115
+ status: 'unknown',
116
+ evidence: [`事件 #${event.index} 启动了后台进程,会话内未见结束证据`],
117
+ });
118
+ }
119
+ // A pid printed back by the shell is the only in-band proof a job existed.
120
+ for (const event of session.turns) {
121
+ if (event.kind !== 'tool_result')
122
+ continue;
123
+ const pid = PID_ECHO.exec(event.toolResult ?? '')?.[1];
124
+ if (!pid)
125
+ continue;
126
+ const nearest = [...jobs.values()].find((job) => job.discoveredFrom !== undefined && Math.abs(job.discoveredFrom - event.index) <= 2 && !job.pid);
127
+ if (nearest) {
128
+ nearest.pid = pid;
129
+ nearest.evidence.push(`结果里回显了 pid ${pid}`);
130
+ }
131
+ }
132
+ return [...jobs.values()];
133
+ }
134
+ export function jobCounts(jobs) {
135
+ const counts = { completed: 0, failed: 0, running: 0, unknown: 0 };
136
+ for (const job of jobs)
137
+ counts[job.status]++;
138
+ return counts;
139
+ }
140
+ function groupOf(fullPath, host, workspace) {
141
+ if (LOG_FILE.test(fullPath) || TEMP_DIR.test(fullPath))
142
+ return 'log';
143
+ if (host)
144
+ return 'runtime';
145
+ return workspace && isInside(workspace, fullPath) ? 'project' : 'runtime';
146
+ }
147
+ /** Every file the session wrote, bucketed by what kind of file it is. */
148
+ export function fileLedger(session) {
149
+ const toTurn = turnOf(session);
150
+ const workspace = session.ref.workspace;
151
+ const records = new Map();
152
+ for (const event of session.turns) {
153
+ for (const write of fileWrites(event)) {
154
+ const filePath = write.host ? write.path : resolveWritePath(write, workspace);
155
+ const key = `${write.host ?? ''}|${filePath}`;
156
+ if (records.has(key))
157
+ continue;
158
+ records.set(key, {
159
+ path: filePath,
160
+ ...(write.host ? { host: write.host } : {}),
161
+ operation: write.extractor.split(':').pop() ?? write.extractor,
162
+ turn: toTurn(event.index),
163
+ eventIndex: event.index,
164
+ ...(event.timestamp ? { timestamp: event.timestamp } : {}),
165
+ provenance: write.provenance,
166
+ extractor: write.extractor,
167
+ group: groupOf(filePath, write.host, workspace),
168
+ });
169
+ }
170
+ }
171
+ for (const change of session.stats.fileChanges) {
172
+ const key = `|${change.path}`;
173
+ if (records.has(key))
174
+ continue;
175
+ records.set(key, {
176
+ path: change.path,
177
+ operation: change.change,
178
+ turn: 0,
179
+ eventIndex: -1,
180
+ provenance: 'observed',
181
+ extractor: 'item:FileChange',
182
+ group: groupOf(change.path, undefined, workspace),
183
+ });
184
+ }
185
+ return [...records.values()];
186
+ }
187
+ /** Display helper shared by the CLI: workspace-relative when possible. */
188
+ export function displayPath(record, workspace) {
189
+ if (record.host)
190
+ return `${record.host}:${record.path}`;
191
+ return workspace && isInside(workspace, record.path) ? path.relative(workspace, record.path) : record.path;
192
+ }
@@ -0,0 +1,6 @@
1
+ import type { NormalizedSession, SessionOverview } from './types.js';
2
+ /**
3
+ * Tier one of the three-level view: everything about a session that is worth
4
+ * knowing before deciding which turn to open.
5
+ */
6
+ export declare function buildOverview(session: NormalizedSession): SessionOverview;