@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.
- package/README.md +347 -0
- package/dist/bin/1session.d.ts +2 -0
- package/dist/bin/1session.js +408 -0
- package/dist/src/aggregator.d.ts +12 -0
- package/dist/src/aggregator.js +136 -0
- package/dist/src/classify.d.ts +2 -0
- package/dist/src/classify.js +11 -0
- package/dist/src/distiller.d.ts +7 -0
- package/dist/src/distiller.js +122 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +17 -0
- package/dist/src/ledger.d.ts +19 -0
- package/dist/src/ledger.js +192 -0
- package/dist/src/overview.d.ts +6 -0
- package/dist/src/overview.js +324 -0
- package/dist/src/parsers/antigravity.d.ts +4 -0
- package/dist/src/parsers/antigravity.js +297 -0
- package/dist/src/parsers/claude.d.ts +2 -0
- package/dist/src/parsers/claude.js +214 -0
- package/dist/src/parsers/codex.d.ts +2 -0
- package/dist/src/parsers/codex.js +316 -0
- package/dist/src/parsers/provider.d.ts +24 -0
- package/dist/src/parsers/provider.js +5 -0
- package/dist/src/resolver.d.ts +42 -0
- package/dist/src/resolver.js +145 -0
- package/dist/src/search.d.ts +59 -0
- package/dist/src/search.js +257 -0
- package/dist/src/store/db.d.ts +10 -0
- package/dist/src/store/db.js +89 -0
- package/dist/src/store/edges.d.ts +46 -0
- package/dist/src/store/edges.js +153 -0
- package/dist/src/store/facts.d.ts +8 -0
- package/dist/src/store/facts.js +39 -0
- package/dist/src/store/indexer.d.ts +40 -0
- package/dist/src/store/indexer.js +62 -0
- package/dist/src/store/read.d.ts +29 -0
- package/dist/src/store/read.js +70 -0
- package/dist/src/store/rows.d.ts +35 -0
- package/dist/src/store/rows.js +55 -0
- package/dist/src/store/schema.d.ts +19 -0
- package/dist/src/store/schema.js +142 -0
- package/dist/src/store/write.d.ts +22 -0
- package/dist/src/store/write.js +72 -0
- package/dist/src/turns.d.ts +14 -0
- package/dist/src/turns.js +156 -0
- package/dist/src/types.d.ts +294 -0
- package/dist/src/types.js +13 -0
- package/dist/src/util/jsonl.d.ts +6 -0
- package/dist/src/util/jsonl.js +32 -0
- package/dist/src/util/paths.d.ts +15 -0
- package/dist/src/util/paths.js +69 -0
- package/dist/src/util/text.d.ts +7 -0
- package/dist/src/util/text.js +48 -0
- package/dist/src/writes.d.ts +35 -0
- package/dist/src/writes.js +204 -0
- package/package.json +58 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { aggregateWorkspaceSessions } from '../src/aggregator.js';
|
|
4
|
+
import { distillSession } from '../src/distiller.js';
|
|
5
|
+
import { buildOverview } from '../src/overview.js';
|
|
6
|
+
import { eventDetail, summarizeTurns, turnDetail } from '../src/turns.js';
|
|
7
|
+
import { commandLedger, displayPath, errorLedger, fileLedger, jobLedger } from '../src/ledger.js';
|
|
8
|
+
import { listRecentSessions, loadSession } from '../src/resolver.js';
|
|
9
|
+
import { searchSessions } from '../src/search.js';
|
|
10
|
+
import { canonicalizePath } from '../src/util/paths.js';
|
|
11
|
+
import { oneLine } from '../src/util/text.js';
|
|
12
|
+
const USAGE = `1session — cross-agent session Read Plane
|
|
13
|
+
|
|
14
|
+
1session list [--limit <n>] [--workspace <path>] [--provider <name>] [--since 24h] [--json]
|
|
15
|
+
1session overview <session-id> [--json] 第 1 层:会话概要
|
|
16
|
+
1session turns <session-id> [--json] 第 2 层:逐轮概要
|
|
17
|
+
1session turn <session-id> <n> [--event <k>] [--json] 第 3 层:单轮 / 单次工具调用明细
|
|
18
|
+
1session jobs <session-id> [--json] 异步作业账本
|
|
19
|
+
1session commands <session-id> [--failed] [--host h] [--turn n] [--json]
|
|
20
|
+
1session files <session-id> [--group project|runtime|log|all] [--json]
|
|
21
|
+
1session errors <session-id> [--json]
|
|
22
|
+
1session digest <session-id> [--focus marketing|review|full] [--json]
|
|
23
|
+
1session workspace [path] [--since 24h] [--limit <n>] [--digest] [--focus <f>] [--json]
|
|
24
|
+
1session index [<session-id>] [--all] [--force] [--since 30d] 建立/刷新索引
|
|
25
|
+
1session graph <session-id> [--json] 会话之间的引用关系
|
|
26
|
+
1session search <query> [--workspace path] [--since 24h] [--limit n] [--provider name]
|
|
27
|
+
[--kind user,assistant,thinking,tool_call,tool_result]
|
|
28
|
+
[--regex] [--case] [--context n] [--max-hits n] [--json]
|
|
29
|
+
|
|
30
|
+
全局:--no-index 绕过索引直读源文件。索引位于 ~/.1agents/session-reader/index.db
|
|
31
|
+
|
|
32
|
+
Providers: antigravity (~/.gemini/antigravity/brain), claude (~/.claude/projects), codex (~/.codex/sessions).
|
|
33
|
+
`;
|
|
34
|
+
/** Flags that never take a value, so they cannot swallow a positional. */
|
|
35
|
+
const BOOLEAN_FLAGS = new Set([
|
|
36
|
+
'json', 'failed', 'digest', 'regex', 'case', 'all', 'force', 'no-index',
|
|
37
|
+
]);
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const [command = 'help', ...rest] = argv;
|
|
40
|
+
const positional = [];
|
|
41
|
+
const flags = {};
|
|
42
|
+
for (let i = 0; i < rest.length; i++) {
|
|
43
|
+
const token = rest[i];
|
|
44
|
+
if (token === '-n') {
|
|
45
|
+
flags.n = rest[++i] ?? '';
|
|
46
|
+
}
|
|
47
|
+
else if (token.startsWith('--')) {
|
|
48
|
+
const name = token.slice(2);
|
|
49
|
+
const next = rest[i + 1];
|
|
50
|
+
if (!BOOLEAN_FLAGS.has(name) && next && !next.startsWith('--')) {
|
|
51
|
+
flags[name] = next;
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
flags[name] = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
positional.push(token);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { command, positional, flags };
|
|
63
|
+
}
|
|
64
|
+
const TURN_MARK = {
|
|
65
|
+
completed: '✓',
|
|
66
|
+
unfinished: '⚠',
|
|
67
|
+
nudged: '↻',
|
|
68
|
+
interrupted: '✂',
|
|
69
|
+
no_response: '⊘',
|
|
70
|
+
failed_tail: '✗',
|
|
71
|
+
};
|
|
72
|
+
const str = (value) => typeof value === 'string' ? value : undefined;
|
|
73
|
+
const num = (value) => {
|
|
74
|
+
const text = typeof value === 'string' ? value : undefined;
|
|
75
|
+
if (text === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const parsed = Number(text);
|
|
78
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
79
|
+
};
|
|
80
|
+
const kindsOf = (value) => str(value)
|
|
81
|
+
?.split(',')
|
|
82
|
+
.map((kind) => kind.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
const focusOf = (value) => {
|
|
85
|
+
const focus = str(value);
|
|
86
|
+
return focus === 'marketing' || focus === 'full' ? focus : 'review';
|
|
87
|
+
};
|
|
88
|
+
function print(json, data, text) {
|
|
89
|
+
console.log(json ? JSON.stringify(data, null, 2) : text);
|
|
90
|
+
}
|
|
91
|
+
let useIndex = true;
|
|
92
|
+
async function load(sessionId) {
|
|
93
|
+
if (!sessionId)
|
|
94
|
+
throw new Error('missing <session-id>');
|
|
95
|
+
return loadSession(sessionId, { useIndex });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Records "this session read that one" while it is happening, when the caller
|
|
99
|
+
* identity was injected. Costs nothing and is silently skipped otherwise.
|
|
100
|
+
*/
|
|
101
|
+
async function noteRead(verb, target) {
|
|
102
|
+
if (!useIndex || !target || !process.env.SESSION_READER_CALLER_SESSION)
|
|
103
|
+
return;
|
|
104
|
+
const { openStore } = await import('../src/store/db.js');
|
|
105
|
+
const { captureRuntimeEdge } = await import('../src/store/edges.js');
|
|
106
|
+
captureRuntimeEdge(await openStore(), verb, target);
|
|
107
|
+
}
|
|
108
|
+
async function main() {
|
|
109
|
+
const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
110
|
+
const json = flags.json === true;
|
|
111
|
+
useIndex = flags['no-index'] !== true;
|
|
112
|
+
await noteRead(command, positional[0]);
|
|
113
|
+
switch (command) {
|
|
114
|
+
case 'list': {
|
|
115
|
+
const refs = await listRecentSessions({
|
|
116
|
+
limit: num(flags.limit) ?? 20,
|
|
117
|
+
workspace: str(flags.workspace),
|
|
118
|
+
since: str(flags.since),
|
|
119
|
+
provider: str(flags.provider),
|
|
120
|
+
});
|
|
121
|
+
print(json, refs, refs
|
|
122
|
+
.map((ref) => `${ref.provider.padEnd(11)} ${ref.id.slice(0, 8)} ${ref.updatedAt ?? '?'} ` +
|
|
123
|
+
`${ref.workspace ? path.basename(ref.workspace) : '-'} ${oneLine(ref.title, 70)}`)
|
|
124
|
+
.join('\n') || '(no sessions found)');
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'overview': {
|
|
128
|
+
const session = await load(positional[0]);
|
|
129
|
+
const overview = buildOverview(session);
|
|
130
|
+
print(json, overview, overview.markdown);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'jobs': {
|
|
134
|
+
const session = await load(positional[0]);
|
|
135
|
+
const jobs = jobLedger(session);
|
|
136
|
+
print(json, jobs, jobs.length
|
|
137
|
+
? jobs
|
|
138
|
+
.map((job) => `[${job.status}] ${job.log ?? job.id}${job.pid ? ` pid ${job.pid}` : ''}` +
|
|
139
|
+
`${job.host ? ` @${job.host}` : ''}` +
|
|
140
|
+
`${job.startedAt ? ` ${job.startedAt.slice(0, 19)}` : ''}` +
|
|
141
|
+
` [${job.provenance} · ${job.extractor}]` +
|
|
142
|
+
`\n ${job.evidence.join(';')}` +
|
|
143
|
+
`${job.command ? `\n ${oneLine(job.command, 150)}` : ''}`)
|
|
144
|
+
.join('\n')
|
|
145
|
+
: '(该会话没有发现后台作业)');
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 'commands': {
|
|
149
|
+
const session = await load(positional[0]);
|
|
150
|
+
const host = str(flags.host);
|
|
151
|
+
const turn = num(flags.turn);
|
|
152
|
+
const records = commandLedger(session).filter((record) => (flags.failed !== true || (record.exitCode ?? 0) !== 0) &&
|
|
153
|
+
(host === undefined || record.host === host) &&
|
|
154
|
+
(turn === undefined || record.turn === turn));
|
|
155
|
+
print(json, records, [
|
|
156
|
+
`${records.length} 条命令(来源:${[...new Set(records.map((r) => `${r.provenance}/${r.extractor}`))].join(' ') || '-'})`,
|
|
157
|
+
'',
|
|
158
|
+
...records.map((record) => `T${String(record.turn).padStart(2, ' ')} #${String(record.eventIndex).padStart(4, ' ')} ` +
|
|
159
|
+
`exit ${String(record.exitCode ?? '?').padStart(3, ' ')}` +
|
|
160
|
+
`${record.durationMs ? ` ${(record.durationMs / 1000).toFixed(1)}s` : ''}` +
|
|
161
|
+
`${record.host ? ` @${record.host}` : ''} ${oneLine(record.command, 130)}`),
|
|
162
|
+
].join('\n'));
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'files': {
|
|
166
|
+
const session = await load(positional[0]);
|
|
167
|
+
const wanted = str(flags.group);
|
|
168
|
+
const records = fileLedger(session).filter((record) =>
|
|
169
|
+
// `all` means all; only the default view hides logs.
|
|
170
|
+
wanted === 'all' ? true : wanted === undefined ? record.group !== 'log' : record.group === wanted);
|
|
171
|
+
print(json, records, [
|
|
172
|
+
`${records.length} 个文件${wanted ? `(group=${wanted})` : '(默认不含 log,用 --group all 看全部)'}`,
|
|
173
|
+
'',
|
|
174
|
+
...records.map((record) => `[${record.group}] ${record.provenance.padEnd(9)} T${record.turn} ` +
|
|
175
|
+
`${displayPath(record, session.ref.workspace)} \`${record.extractor}${record.eventIndex >= 0 ? ` E${record.eventIndex}` : ''}\``),
|
|
176
|
+
].join('\n'));
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'errors': {
|
|
180
|
+
const session = await load(positional[0]);
|
|
181
|
+
const records = errorLedger(session);
|
|
182
|
+
print(json, records, [
|
|
183
|
+
`${records.length} 条失败命令`,
|
|
184
|
+
'',
|
|
185
|
+
...records.map((record) => `T${record.turn} #${record.eventIndex} exit ${record.exitCode ?? '?'}` +
|
|
186
|
+
`${record.laterSucceeded ? ' → 同前缀命令后续成功过' : ''}` +
|
|
187
|
+
`\n ${oneLine(record.command, 150)}` +
|
|
188
|
+
`${record.stderr ? `\n ${oneLine(record.stderr, 150)}` : ''}`),
|
|
189
|
+
].join('\n'));
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'digest': {
|
|
193
|
+
const session = await load(positional[0]);
|
|
194
|
+
const digest = distillSession(session, { focus: focusOf(flags.focus) });
|
|
195
|
+
print(json, digest, digest.markdown);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case 'workspace': {
|
|
199
|
+
const target = canonicalizePath(positional[0] ?? '.');
|
|
200
|
+
if (flags.digest === true || flags.focus) {
|
|
201
|
+
const digest = await aggregateWorkspaceSessions(target, {
|
|
202
|
+
since: str(flags.since),
|
|
203
|
+
limit: num(flags.limit),
|
|
204
|
+
focus: focusOf(flags.focus),
|
|
205
|
+
});
|
|
206
|
+
print(json, digest, digest.markdown);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
const refs = await listRecentSessions({
|
|
210
|
+
workspace: target,
|
|
211
|
+
since: str(flags.since),
|
|
212
|
+
limit: num(flags.limit) ?? 50,
|
|
213
|
+
});
|
|
214
|
+
print(json, { workspace: target, sessions: refs }, [
|
|
215
|
+
`workspace ${target}`,
|
|
216
|
+
`sessions ${refs.length} across ${new Set(refs.map((r) => r.provider)).size} agents`,
|
|
217
|
+
'',
|
|
218
|
+
...refs.map((ref) => `${ref.provider.padEnd(11)} ${ref.id.slice(0, 8)} ${ref.updatedAt ?? '?'} ${oneLine(ref.title, 70)}`),
|
|
219
|
+
].join('\n'));
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 'turns': {
|
|
223
|
+
const session = await load(positional[0]);
|
|
224
|
+
const summaries = summarizeTurns(session);
|
|
225
|
+
print(json, summaries, [
|
|
226
|
+
`${summaries.length} 轮 · 共 ${session.turns.length} 个事件`,
|
|
227
|
+
'',
|
|
228
|
+
...summaries.flatMap((turn) => [
|
|
229
|
+
`${TURN_MARK[turn.status] ?? '·'} T${String(turn.no).padStart(2, ' ')} ${turn.startedAt?.slice(0, 19) ?? '?'}` +
|
|
230
|
+
`${turn.durationMs ? ` (${Math.round(turn.durationMs / 1000)}s)` : ''}` +
|
|
231
|
+
` 事件 ${turn.events[0]}–${turn.events[1]}` +
|
|
232
|
+
` 文件 ${turn.files.length} · 命令 ${turn.commands} · 失败 ${turn.errors}` +
|
|
233
|
+
`${turn.status === 'completed' ? '' : ` [${turn.status}${turn.nudgeCount > 1 ? ` ×${turn.nudgeCount}` : ''}]`}`,
|
|
234
|
+
...(turn.evidence.length ? [` ! ${turn.evidence.join(';')}`] : []),
|
|
235
|
+
` ▸ ${turn.prompt}`,
|
|
236
|
+
...(turn.outcome ? [` ◂ ${turn.outcome}`] : []),
|
|
237
|
+
]),
|
|
238
|
+
].join('\n'));
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 'turn': {
|
|
242
|
+
const session = await load(positional[0]);
|
|
243
|
+
const eventIndex = num(flags.event);
|
|
244
|
+
if (eventIndex !== undefined) {
|
|
245
|
+
const detail = await eventDetail(session, eventIndex);
|
|
246
|
+
const body = detail.fullText ?? detail.text ?? detail.toolResult ?? '';
|
|
247
|
+
print(json, detail, [
|
|
248
|
+
`#${detail.index} ${detail.kind}${detail.toolName ? `(${detail.toolName})` : ''} ${detail.timestamp ?? ''}` +
|
|
249
|
+
`${detail.truncated ? (detail.fullText ? ' [已从 steps/ 补全]' : ' [已截断]') : ''}`,
|
|
250
|
+
...(detail.toolArgs ? ['', '参数:', JSON.stringify(detail.toolArgs, null, 2)] : []),
|
|
251
|
+
...(body ? ['', '内容:', body] : []),
|
|
252
|
+
...(detail.truncationNote ? ['', `⚠️ ${detail.truncationNote}`] : []),
|
|
253
|
+
].join('\n'));
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
const detail = turnDetail(session, num(positional[1]) ?? 1);
|
|
257
|
+
print(json, detail, [
|
|
258
|
+
`T${detail.summary.no} ${detail.summary.startedAt ?? '?'} → ${detail.summary.endedAt ?? '?'}` +
|
|
259
|
+
` 事件 ${detail.summary.events[0]}–${detail.summary.events[1]}`,
|
|
260
|
+
`▸ ${detail.summary.prompt}`,
|
|
261
|
+
...(detail.summary.files.length ? ['', `改动:${detail.summary.files.join(', ')}`] : []),
|
|
262
|
+
'',
|
|
263
|
+
...detail.events.map((event) => {
|
|
264
|
+
const head = `#${event.index} ${event.kind}${event.toolName ? `(${event.toolName})` : ''}` +
|
|
265
|
+
`${event.truncated ? ' [截断]' : ''}${event.isError ? ' ✗' : ''}`;
|
|
266
|
+
return `${head} ${oneLine(event.text ?? event.toolResult ?? JSON.stringify(event.toolArgs), 160)}`;
|
|
267
|
+
}),
|
|
268
|
+
'',
|
|
269
|
+
`(用 --event <k> 展开单个事件的完整内容)`,
|
|
270
|
+
].join('\n'));
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case 'search': {
|
|
274
|
+
const query = positional.join(' ');
|
|
275
|
+
const hits = await searchSessions(query, {
|
|
276
|
+
workspace: str(flags.workspace),
|
|
277
|
+
since: str(flags.since),
|
|
278
|
+
limit: num(flags.limit),
|
|
279
|
+
provider: str(flags.provider),
|
|
280
|
+
kinds: kindsOf(flags.kind),
|
|
281
|
+
regex: flags.regex === true,
|
|
282
|
+
caseSensitive: flags.case === true,
|
|
283
|
+
context: num(flags.context),
|
|
284
|
+
maxPerSession: num(flags['max-hits']),
|
|
285
|
+
useIndex,
|
|
286
|
+
});
|
|
287
|
+
const total = hits.reduce((sum, hit) => sum + hit.totalMatches, 0);
|
|
288
|
+
print(json, hits, hits.length
|
|
289
|
+
? [
|
|
290
|
+
`${total} 处命中,分布在 ${hits.length} 个会话`,
|
|
291
|
+
...hits.flatMap((hit) => [
|
|
292
|
+
'',
|
|
293
|
+
`### ${hit.session.provider} ${hit.session.id.slice(0, 8)} ` +
|
|
294
|
+
`${hit.session.createdAt ?? '?'} → ${hit.session.updatedAt ?? '?'} (${hit.totalMatches} 命中)`,
|
|
295
|
+
` ${oneLine(hit.session.title, 80)}`,
|
|
296
|
+
...hit.matches.map((match) => ` #${match.index} ${match.kind}${match.toolName ? `(${match.toolName})` : ''} ${match.excerpt}`),
|
|
297
|
+
...(hit.totalMatches > hit.matches.length
|
|
298
|
+
? [` …另有 ${hit.totalMatches - hit.matches.length} 处(--max-hits 调大或 --json 查看全部)`]
|
|
299
|
+
: []),
|
|
300
|
+
]),
|
|
301
|
+
].join('\n')
|
|
302
|
+
: `无命中:${query}`);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case 'index': {
|
|
306
|
+
const { openStore } = await import('../src/store/db.js');
|
|
307
|
+
const { indexSession } = await import('../src/store/indexer.js');
|
|
308
|
+
const { deriveEdges } = await import('../src/store/edges.js');
|
|
309
|
+
const { readSession, sessionRow } = await import('../src/store/read.js');
|
|
310
|
+
const { listResolvedSessions } = await import('../src/resolver.js');
|
|
311
|
+
const db = await openStore();
|
|
312
|
+
const force = flags.force === true;
|
|
313
|
+
if (flags.all !== true) {
|
|
314
|
+
const target = positional[0];
|
|
315
|
+
if (!target)
|
|
316
|
+
throw new Error('missing <session-id> (or pass --all)');
|
|
317
|
+
const { resolveSession } = await import('../src/resolver.js');
|
|
318
|
+
const handle = await resolveSession(target);
|
|
319
|
+
if (!handle)
|
|
320
|
+
throw new Error(`session not found: ${target}`);
|
|
321
|
+
const result = await indexSession(db, handle, { force });
|
|
322
|
+
print(json, result, `${result.id} ${result.action} ${result.session.turns.length} 个事件`);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
// Backfill indexes L1+L2 first and derives edges in a second pass, so a
|
|
326
|
+
// reference to a session that had not been indexed yet still lands.
|
|
327
|
+
const started = Date.now();
|
|
328
|
+
const handles = await listResolvedSessions({
|
|
329
|
+
limit: num(flags.limit) ?? Number.POSITIVE_INFINITY,
|
|
330
|
+
scan: num(flags.scan) ?? Number.POSITIVE_INFINITY,
|
|
331
|
+
...(str(flags.since) ? { since: str(flags.since) } : {}),
|
|
332
|
+
...(str(flags.provider) ? { provider: str(flags.provider) } : {}),
|
|
333
|
+
});
|
|
334
|
+
const counts = {};
|
|
335
|
+
for (const handle of handles) {
|
|
336
|
+
const result = await indexSession(db, handle, { force, edges: false });
|
|
337
|
+
counts[result.action] = (counts[result.action] ?? 0) + 1;
|
|
338
|
+
}
|
|
339
|
+
let edges = 0;
|
|
340
|
+
for (const handle of handles) {
|
|
341
|
+
const id = `${handle.adapter.provider}:${handle.candidate.id}`;
|
|
342
|
+
const row = sessionRow(db, id);
|
|
343
|
+
if (row)
|
|
344
|
+
edges += deriveEdges(db, id, readSession(db, row));
|
|
345
|
+
}
|
|
346
|
+
const summary = {
|
|
347
|
+
sessions: handles.length,
|
|
348
|
+
actions: counts,
|
|
349
|
+
edges,
|
|
350
|
+
seconds: Number(((Date.now() - started) / 1000).toFixed(1)),
|
|
351
|
+
};
|
|
352
|
+
print(json, summary, `已索引 ${summary.sessions} 个会话,耗时 ${summary.seconds}s\n` +
|
|
353
|
+
Object.entries(counts)
|
|
354
|
+
.map(([action, n]) => ` ${action.padEnd(16)} ${n}`)
|
|
355
|
+
.join('\n') +
|
|
356
|
+
`\n 边证据 ${edges}`);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case 'graph':
|
|
360
|
+
case 'related': {
|
|
361
|
+
// An inbound edge is the same fact read from the other end.
|
|
362
|
+
const INVERSE = {
|
|
363
|
+
references: 'referenced_by',
|
|
364
|
+
handoff_from: 'handed_off_to',
|
|
365
|
+
forked_from: 'forked_into',
|
|
366
|
+
resumed_from: 'resumed_into',
|
|
367
|
+
sends_to: 'sent_from',
|
|
368
|
+
};
|
|
369
|
+
const { openStore } = await import('../src/store/db.js');
|
|
370
|
+
const { edgeEvidence, edgesOf } = await import('../src/store/edges.js');
|
|
371
|
+
const { findSessionRow } = await import('../src/store/read.js');
|
|
372
|
+
const target = positional[0];
|
|
373
|
+
if (!target)
|
|
374
|
+
throw new Error('missing <session-id>');
|
|
375
|
+
await load(target); // make sure it is indexed before we look it up
|
|
376
|
+
const db = await openStore();
|
|
377
|
+
const row = findSessionRow(db, target);
|
|
378
|
+
if (!row)
|
|
379
|
+
throw new Error(`session not found: ${target}`);
|
|
380
|
+
const edges = edgesOf(db, row.id).map((edge) => ({
|
|
381
|
+
...edge,
|
|
382
|
+
evidence: edgeEvidence(db, edge.from, edge.to, edge.relation),
|
|
383
|
+
}));
|
|
384
|
+
print(json, { session: row.id, edges }, edges.length
|
|
385
|
+
? [
|
|
386
|
+
`${row.id} ${oneLine(row.title ?? '', 60)}`,
|
|
387
|
+
'',
|
|
388
|
+
...edges.map((edge) => edge.direction === 'out'
|
|
389
|
+
? ` → ${edge.relation.padEnd(13)} ${edge.to} 证据 ${edge.evidenceCount} 次` +
|
|
390
|
+
`(${edge.evidence.map((item) => item.operation).join(' ')})`
|
|
391
|
+
: ` ← ${(INVERSE[edge.relation] ?? edge.relation).padEnd(13)} ${edge.from} 证据 ${edge.evidenceCount} 次` +
|
|
392
|
+
`(${edge.evidence.map((item) => item.operation).join(' ')})`),
|
|
393
|
+
'',
|
|
394
|
+
'> → 本会话查过对方;← 对方查过本会话。证据可下钻:1session turn <会话> <轮次>',
|
|
395
|
+
].join('\n')
|
|
396
|
+
: `${row.id} 尚无关系边(没有任何会话通过 1session 查过它,它也没查过别人)`);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
default:
|
|
400
|
+
console.log(USAGE);
|
|
401
|
+
if (command !== 'help' && command !== '--help')
|
|
402
|
+
process.exitCode = 1;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
main().catch((error) => {
|
|
406
|
+
console.error(`1session: ${error instanceof Error ? error.message : String(error)}`);
|
|
407
|
+
process.exitCode = 1;
|
|
408
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DigestFocus, WorkspaceDigest } from './types.js';
|
|
2
|
+
export interface AggregateOptions {
|
|
3
|
+
since?: string | Date;
|
|
4
|
+
/** Maximum number of sessions to open and interleave. */
|
|
5
|
+
limit?: number;
|
|
6
|
+
focus?: DigestFocus;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Pulls every session that ran in `workspacePath` — regardless of which agent
|
|
10
|
+
* produced it — and interleaves them into a single project storyline.
|
|
11
|
+
*/
|
|
12
|
+
export declare function aggregateWorkspaceSessions(workspacePath: string, options?: AggregateOptions): Promise<WorkspaceDigest>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { shellCommand } from './distiller.js';
|
|
3
|
+
import { fileWrites, resolveWritePath } from './writes.js';
|
|
4
|
+
import { findResolvedByWorkspace, parseSince } from './resolver.js';
|
|
5
|
+
import { canonicalizePath, isInside } from './util/paths.js';
|
|
6
|
+
import { oneLine } from './util/text.js';
|
|
7
|
+
const TIMELINE_CAP = 80;
|
|
8
|
+
function relativize(file, workspace) {
|
|
9
|
+
return isInside(workspace, file) ? path.relative(workspace, file) || '.' : file;
|
|
10
|
+
}
|
|
11
|
+
/** Keeps the events that tell the story: requests, edits, failures, conclusions. */
|
|
12
|
+
function significantEvents(session) {
|
|
13
|
+
const { provider, id } = session.ref;
|
|
14
|
+
const entries = [];
|
|
15
|
+
const lastAssistant = [...session.turns].reverse().find((t) => t.kind === 'assistant' && t.text?.trim());
|
|
16
|
+
for (const turn of session.turns) {
|
|
17
|
+
const base = { timestamp: turn.timestamp, provider, sessionId: id, kind: turn.kind };
|
|
18
|
+
if (turn.kind === 'user' && turn.text?.trim()) {
|
|
19
|
+
entries.push({ ...base, summary: `提出:${oneLine(turn.text, 180)}` });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const writes = fileWrites(turn);
|
|
23
|
+
if (writes.length) {
|
|
24
|
+
entries.push({
|
|
25
|
+
...base,
|
|
26
|
+
summary: `${turn.toolName} → ${writes.map((w) => path.basename(w.path)).join(', ')}`,
|
|
27
|
+
});
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (turn.kind === 'tool_result' && turn.isError) {
|
|
31
|
+
entries.push({ ...base, summary: `失败:${oneLine(turn.toolResult, 140)}` });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (turn === lastAssistant) {
|
|
35
|
+
entries.push({ ...base, summary: `收束:${oneLine(turn.text, 180)}` });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return entries;
|
|
39
|
+
}
|
|
40
|
+
function renderMarkdown(digest, focus, stats) {
|
|
41
|
+
const { workspace, sessions, collaboratingAgents, unifiedTimeline, fileAttribution } = digest;
|
|
42
|
+
const lines = [
|
|
43
|
+
`# ${path.basename(workspace)} · 跨智能体协作纪实`,
|
|
44
|
+
'',
|
|
45
|
+
`- 工作区:\`${workspace}\``,
|
|
46
|
+
`- 参与智能体:${collaboratingAgents.join(' / ') || '无'}`,
|
|
47
|
+
`- 会话:${sessions.length} 个,统一时间线 ${unifiedTimeline.length} 个节点,涉及 ${Object.keys(fileAttribution).length} 个文件`,
|
|
48
|
+
'',
|
|
49
|
+
'## 会话清单',
|
|
50
|
+
'',
|
|
51
|
+
];
|
|
52
|
+
for (const ref of sessions) {
|
|
53
|
+
const stat = stats.get(ref.id);
|
|
54
|
+
lines.push(`- \`${ref.provider}\` ${ref.id.slice(0, 8)} · ${ref.createdAt ?? '?'} → ${ref.updatedAt ?? '?'}` +
|
|
55
|
+
(stat ? ` · ${stat.turns} 轮 / ${stat.files} 文件 / ${stat.commands} 命令` : '') +
|
|
56
|
+
`\n ${ref.title ?? '(无标题)'}`);
|
|
57
|
+
}
|
|
58
|
+
lines.push('', '## 统一时间线', '');
|
|
59
|
+
const timeline = unifiedTimeline.slice(0, TIMELINE_CAP);
|
|
60
|
+
for (const entry of timeline) {
|
|
61
|
+
lines.push(`- \`${entry.timestamp ?? '?'}\` **${entry.provider}** ${entry.summary}`);
|
|
62
|
+
}
|
|
63
|
+
if (unifiedTimeline.length > timeline.length) {
|
|
64
|
+
lines.push(`- …另有 ${unifiedTimeline.length - timeline.length} 个节点`);
|
|
65
|
+
}
|
|
66
|
+
if (focus !== 'marketing') {
|
|
67
|
+
lines.push('', '## 文件归属(谁动了什么)', '');
|
|
68
|
+
const files = Object.entries(fileAttribution).sort((a, b) => b[1].length - a[1].length);
|
|
69
|
+
for (const [file, touches] of files.slice(0, 40)) {
|
|
70
|
+
const chain = [...new Set(touches.map((t) => t.provider))].join(' → ');
|
|
71
|
+
const inferred = touches.every((t) => t.provenance === 'derived') ? ' ~推断' : '';
|
|
72
|
+
lines.push(`- \`${file}\` — ${chain}(${touches.length} 次改动${inferred})`);
|
|
73
|
+
}
|
|
74
|
+
if (!files.length)
|
|
75
|
+
lines.push('- (无文件改动记录)');
|
|
76
|
+
if (files.length > 40)
|
|
77
|
+
lines.push(`- …另有 ${files.length - 40} 个文件`);
|
|
78
|
+
}
|
|
79
|
+
lines.push('');
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Pulls every session that ran in `workspacePath` — regardless of which agent
|
|
84
|
+
* produced it — and interleaves them into a single project storyline.
|
|
85
|
+
*/
|
|
86
|
+
export async function aggregateWorkspaceSessions(workspacePath, options = {}) {
|
|
87
|
+
const workspace = canonicalizePath(workspacePath);
|
|
88
|
+
const limit = options.limit ?? 10;
|
|
89
|
+
const focus = options.focus ?? 'review';
|
|
90
|
+
const resolved = await findResolvedByWorkspace(workspace, { since: options.since, limit });
|
|
91
|
+
// File mtimes only bound the search; the parsed timestamps decide what is in range.
|
|
92
|
+
const sinceMs = parseSince(options.since);
|
|
93
|
+
const sessions = [];
|
|
94
|
+
const timeline = [];
|
|
95
|
+
const fileAttribution = {};
|
|
96
|
+
const stats = new Map();
|
|
97
|
+
for (const handle of resolved) {
|
|
98
|
+
const session = await handle.adapter.parse(handle.candidate).catch(() => undefined);
|
|
99
|
+
if (!session)
|
|
100
|
+
continue;
|
|
101
|
+
if (sinceMs && Date.parse(session.ref.updatedAt ?? '') < sinceMs)
|
|
102
|
+
continue;
|
|
103
|
+
sessions.push(session.ref);
|
|
104
|
+
timeline.push(...significantEvents(session));
|
|
105
|
+
let commands = 0;
|
|
106
|
+
let files = 0;
|
|
107
|
+
for (const turn of session.turns) {
|
|
108
|
+
if (shellCommand(turn))
|
|
109
|
+
commands++;
|
|
110
|
+
for (const write of fileWrites(turn)) {
|
|
111
|
+
files++;
|
|
112
|
+
const key = relativize(resolveWritePath(write, session.ref.workspace), workspace);
|
|
113
|
+
(fileAttribution[key] ??= []).push({
|
|
114
|
+
provider: session.ref.provider,
|
|
115
|
+
sessionId: session.ref.id,
|
|
116
|
+
timestamp: turn.timestamp,
|
|
117
|
+
toolName: turn.toolName,
|
|
118
|
+
provenance: write.provenance,
|
|
119
|
+
...(write.host ? { host: write.host } : {}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
stats.set(session.ref.id, { turns: session.turns.length, commands, files });
|
|
124
|
+
}
|
|
125
|
+
timeline.sort((a, b) => Date.parse(a.timestamp ?? '') - Date.parse(b.timestamp ?? ''));
|
|
126
|
+
sessions.sort((a, b) => Date.parse(a.createdAt ?? '') - Date.parse(b.createdAt ?? ''));
|
|
127
|
+
const collaboratingAgents = [...new Set(sessions.map((s) => s.provider))];
|
|
128
|
+
const digest = {
|
|
129
|
+
workspace,
|
|
130
|
+
sessions,
|
|
131
|
+
collaboratingAgents,
|
|
132
|
+
unifiedTimeline: timeline,
|
|
133
|
+
fileAttribution,
|
|
134
|
+
};
|
|
135
|
+
return { ...digest, markdown: renderMarkdown(digest, focus, stats) };
|
|
136
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Pure "keep going" replies carry no instruction worth handing on. */
|
|
2
|
+
const NUDGE = /^(?:继续|再继续|contin|continue|go on|好的?|ok|okay|嗯+|再来|下一步|next|yes|是的|可以)[。.!!~]*$/i;
|
|
3
|
+
export function classifyUserTurn(text) {
|
|
4
|
+
const value = text.trim();
|
|
5
|
+
if (NUDGE.test(value))
|
|
6
|
+
return 'nudge';
|
|
7
|
+
// Long markdown reports are the user pasting an agent's own output back in.
|
|
8
|
+
if (value.length > 300 && (/(^|\n)#{1,4}\s/.test(value) || /🎉|阶段性|全面完成/.test(value)))
|
|
9
|
+
return 'paste';
|
|
10
|
+
return 'correction';
|
|
11
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DigestFocus, NormalizedSession, SessionDigest, TurnEvent } from './types.js';
|
|
2
|
+
/** Paths a tool call wrote to, including files written through the shell. */
|
|
3
|
+
export declare function editedFiles(turn: TurnEvent, workspace?: string): string[];
|
|
4
|
+
export declare function shellCommand(turn: TurnEvent): string | undefined;
|
|
5
|
+
export declare function distillSession(session: NormalizedSession, options?: {
|
|
6
|
+
focus?: DigestFocus;
|
|
7
|
+
}): SessionDigest;
|