@1agents/session-reader 0.4.0 → 0.5.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 +77 -7
- package/dist/bin/1session.js +63 -22
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +3 -3
- package/dist/src/ledger.js +3 -6
- package/dist/src/search.d.ts +48 -0
- package/dist/src/search.js +136 -20
- package/dist/src/serve/http.d.ts +9 -1
- package/dist/src/serve/http.js +28 -5
- package/dist/src/serve/node.d.ts +16 -17
- package/dist/src/serve/node.js +27 -58
- package/dist/src/store/read.d.ts +8 -0
- package/dist/src/store/read.js +16 -0
- package/dist/src/turns.d.ts +36 -0
- package/dist/src/turns.js +87 -13
- package/package.json +3 -2
- package/skills/1session/SKILL.md +47 -6
- package/skills/1session/references/cli.md +39 -5
package/README.md
CHANGED
|
@@ -69,14 +69,14 @@ npm run build && node dist/bin/1session.js <command>
|
|
|
69
69
|
| `1session list [--limit n] [--scope <path>\|cwd\|global] [--provider name] [--since 24h] [--json]` | 按最近更新列出各智能体的会话(默认当前 pwd 子树,见 `--scope`) |
|
|
70
70
|
| `1session overview <session-id> [--json]` | **第 1 层**:统计卡片(轮次/文件/命令/提交/产物/上传/后台任务/token)+ 目标、口径修正、状态锚点、全量落盘 |
|
|
71
71
|
| `1session turns <session-id> [--json]` | **第 2 层**:逐轮概要——时间、耗时、事件区间、文件/命令/失败数、用户说了什么、agent 回了什么 |
|
|
72
|
-
| `1session turn <session-id> <n> [--event k] [--json]` | **第 3 层**:展开某一轮的全部事件;`--event`
|
|
72
|
+
| `1session turn <session-id> <n> [--event k\|a-b\|a,b,c] [--json]` | **第 3 层**:展开某一轮的全部事件;`--event` 定位工具调用,输出完整参数与**未截断**结果,支持 `491`、`491-493`、`491,495,502` |
|
|
73
73
|
| `1session digest <session-id> [--focus marketing\|review\|full] [--json]` | 单会话蒸馏:目标、改动文件、命令、关键节点 |
|
|
74
74
|
| `1session workspace [path] [--since 24h] [--limit n] [--digest] [--focus f] [--json]` | **按 pwd 跨智能体聚合**(默认 `.`);带 `--digest` 输出统一故事线 |
|
|
75
75
|
| `1session jobs <id> [--json]` | 异步作业账本:状态 + **证据** + pid / host / log |
|
|
76
76
|
| `1session commands <id> [--failed] [--host h] [--turn n] [--json]` | 命令账本:exit_code / 耗时 / cwd |
|
|
77
77
|
| `1session files <id> [--group project\|runtime\|log\|all] [--json]` | 文件账本,按项目 / 运行态 / 日志分组 |
|
|
78
78
|
| `1session errors <id> [--json]` | 失败命令,带 stderr 与"同前缀命令后续是否成功" |
|
|
79
|
-
| `1session search <query> [--scope <path>\|cwd\|global] [--since 24h] [--limit n] [--kind k1,k2] [--regex] [--case] [--context n] [--max-hits n] [--json]` |
|
|
79
|
+
| `1session search <query> [--scope <path>\|cwd\|global] [--since 24h] [--limit n] [--kind k1,k2] [--regex] [--case] [--context n] [--max-hits n] [--include-self] [--json]` | 跨会话全文检索:命中带 `T<轮次> · E<事件号>` 句柄 + 上下文片段(默认当前 pwd 子树,见 `--scope`) |
|
|
80
80
|
| `1session index [<id>] [--all] [--scope <path>\|cwd\|global] [--force] [--since 30d]` | 建立 / 刷新索引;`--all` 全库回填 |
|
|
81
81
|
| `1session graph <id> [--json]`(别名 `related`) | 会话之间的引用关系 + 每条边的证据 |
|
|
82
82
|
| `1session skill install\|status\|uninstall [--agent a,b] [--copy] [--force] [--dry-run]` | 把内置 skill 装进三家智能体的 skills 目录(见上) |
|
|
@@ -158,9 +158,14 @@ T 3 2026-09-11T13:03:29 (283s) 事件 29–45 文件 1 · 命令 7 · 失败 0
|
|
|
158
158
|
```
|
|
159
159
|
|
|
160
160
|
```bash
|
|
161
|
-
1session turn 5575981e 1 --event 11
|
|
161
|
+
1session turn 5575981e 1 --event 11 # 第 3 层:单次工具调用的完整输出
|
|
162
|
+
1session turn 5575981e 1 --event 11-13 # 连着看:调用 + 结果 + 助理的判断
|
|
162
163
|
```
|
|
163
164
|
|
|
165
|
+
`--event` 收 `11`、`11-13`、`11,15,22` 及其混合,一次最多 50 条;区间会按会话长度裁剪,
|
|
166
|
+
所以 `560-999` 就是「看到结尾」。读一次工具调用通常要连读它的结果和后面那条助理判断——
|
|
167
|
+
一个进程读完,而不是起三次。
|
|
168
|
+
|
|
164
169
|
第三层会自动补全被截断的内容:antigravity 的 `transcript.jsonl` 会把长输出截短(事件 #11 只存了 4092 字符),命令会回读 `.system_generated/steps/8/output.txt` 拿到完整的 6937 字节并标注 `[已从 steps/ 补全]`;补不回来时如实提示 `⚠️ steps/N/output.txt 不存在`。
|
|
165
170
|
|
|
166
171
|
跨会话检索(`--kind` 可过滤轮次类型,只看用户说了什么 / 只看工具调用):
|
|
@@ -171,12 +176,22 @@ T 3 2026-09-11T13:03:29 (283s) 事件 29–45 文件 1 · 命令 7 · 失败 0
|
|
|
171
176
|
|
|
172
177
|
```text
|
|
173
178
|
4 处命中,分布在 2 个会话
|
|
179
|
+
(已折叠 1 处本次检索自身留下的回声;--include-self 展开)
|
|
174
180
|
|
|
175
181
|
### antigravity 5575981e 2026-09-13T00:21:05Z → 2026-09-13T03:31:58Z (2 命中)
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
T7 · E318 user /xhs-tech-card 结合 docs/xhs_sol_h3_spark_output/index.html,第一次实战经验贴。
|
|
183
|
+
T9 · E441 user /mobile-xhs-publisher
|
|
184
|
+
↳ 1session turn 5575981e 7 --event 318
|
|
178
185
|
```
|
|
179
186
|
|
|
187
|
+
每条命中前缀就是**下钻句柄** `T<轮次> · E<事件号>`——顺序照着 `turn <id> <T> --event <E>` 排,
|
|
188
|
+
每个会话再附一条拼好的命令,从第 1 层到第 3 层不用再跑一趟 `turns` 人工比对事件区间。
|
|
189
|
+
|
|
190
|
+
**默认折叠本次检索自己留下的脚印**:调用方的会话是边跑边索引的,`1session search "X"` 这条
|
|
191
|
+
命令本身(以及它打印出来的东西)会立刻成为 `X` 的第一条命中。判据只有一条——五分钟内写下的
|
|
192
|
+
`1session` 调用及其结果;只有活着的那个 transcript 才可能有「此刻」的时间戳,所以历史会话一条
|
|
193
|
+
都不会被碰到。折叠了多少永远会打印出来,`--include-self` 可以全部展开。
|
|
194
|
+
|
|
180
195
|
输出示例(上午 Antigravity 定方案 → 下午 Claude Code 落地实现,自动交织成一条时间线):
|
|
181
196
|
|
|
182
197
|
```markdown
|
|
@@ -206,6 +221,7 @@ import {
|
|
|
206
221
|
summarizeTurns,
|
|
207
222
|
turnDetail,
|
|
208
223
|
eventDetail,
|
|
224
|
+
eventDetails,
|
|
209
225
|
} from '@1agents/session-reader';
|
|
210
226
|
|
|
211
227
|
const sessions = await findSessionsByWorkspace(process.cwd(), { since: '7d' });
|
|
@@ -217,8 +233,13 @@ const overview = buildOverview(session); // 第 1 层:overview.stats / ov
|
|
|
217
233
|
const turns = summarizeTurns(session); // 第 2 层
|
|
218
234
|
const detail = turnDetail(session, 3); // 第 3 层
|
|
219
235
|
const full = await eventDetail(session, 11); // 第 3 层:未截断的单次工具输出
|
|
236
|
+
const around = await eventDetails(session, '11-13'); // 相邻几条一起读
|
|
237
|
+
hits[0]?.matches[0]?.turn; // 命中所在轮次,与 match.index 凑成 T·E 句柄
|
|
220
238
|
```
|
|
221
239
|
|
|
240
|
+
`searchSessions` 另接 `selfSessionId` 显式指明调用方(默认取 `SESSION_READER_CALLER_SESSION`);
|
|
241
|
+
被判定为「调用方自己」的命中不会被删掉,而是带上 `self` / `suppressed` 标记返回,展不展示由调用方定。
|
|
242
|
+
|
|
222
243
|
`aggregateWorkspaceSessions` 返回 `WorkspaceDigest`:`sessions` / `collaboratingAgents` /
|
|
223
244
|
`unifiedTimeline`(按时间排序的跨智能体节点)/ `fileAttribution`(文件 → 谁在什么时候改的)/ `markdown`。
|
|
224
245
|
适合直接喂给下游做小红书笔记、PRD、周报与 changelog,也可零成本包成 DeepSeek Harness(Cordis)插件或 MCP server。
|
|
@@ -267,6 +288,20 @@ const full = await eventDetail(session, 11); // 第 3 层:未截断的单次
|
|
|
267
288
|
|
|
268
289
|
同样的原则贯穿每一处:**没有证据就不断言**。异步作业只在有完成回执时标 `completed`,否则一律 `unknown` 并写明依据;没有打印 exit code 的命令结果,`exitCode` 就是空,绝不补成 0。
|
|
269
290
|
|
|
291
|
+
### 三级溯源回答不了的那一问:现在还成不成立
|
|
292
|
+
|
|
293
|
+
`observed / derived / candidate` 回答的是**这个事实怎么来的**,不是**它现在还对不对**。
|
|
294
|
+
overview 的「末态」恰恰是最易腐的一段:
|
|
295
|
+
|
|
296
|
+
> 凡是关于**当前状态**的问题(凭据、环境变量、装了哪个版本、文件还在不在),会话只能告诉你
|
|
297
|
+
> 它**最后一次为真是什么时候**——去核实。转录记下的是说过什么和退出码,不是效果。
|
|
298
|
+
|
|
299
|
+
三种真实撞上过的情况:一条命令退出码为 0、无报错,但它读到的是空输入,写进配置的是个空值;
|
|
300
|
+
某一轮的结论说「现在存了三份」,其中一份是只活在当时那个 shell 里的临时函数,从没落盘;
|
|
301
|
+
「某某已配好」在当时是对的,三周后需要另外验证。会话忠实记录了意图和退出码,记不下效果——
|
|
302
|
+
这是转录这种载体的边界,不是提取规则的疏漏,所以本模块不打算用更聪明的规则去补它,
|
|
303
|
+
只把话说清楚。
|
|
304
|
+
|
|
270
305
|
## 轮次完成状态
|
|
271
306
|
|
|
272
307
|
第二层每一轮带一个**有证据的状态**——只说这一轮有没有收尾,不说做成了什么:
|
|
@@ -414,6 +449,16 @@ $ 1session graph ca8325e1
|
|
|
414
449
|
1session serve --host 100.x.x.x --token <t> # 暴露到 tailnet
|
|
415
450
|
```
|
|
416
451
|
|
|
452
|
+
启动时默认向本机 [node agent](https://github.com/scottzx/dreammate-node)(36908)报备,
|
|
453
|
+
`--no-report` 可关。**agent 没起时是静默 no-op**,不影响本服务——只是外部得靠约定端口
|
|
454
|
+
碰运气找它,而不是探一个 36908 就看见。报备会如实声明可达性:`--host` 是回环就报
|
|
455
|
+
`localhost`(外部发现得了但连不上),否则报 `network`。
|
|
456
|
+
|
|
457
|
+
端口 7777 是 [L0 协议](https://github.com/scottzx/dreammate-network) 的**约定端口**
|
|
458
|
+
(`DEFAULT_PORTS['session-registry']`),不是随手挑的:发现是 pull 的——
|
|
459
|
+
Control Plane 从 tailnet 拿到节点后,照着这张表探测 `/manifest` 与 `/health`。
|
|
460
|
+
换成别的端口就探测不到了,得由服务自己 `POST /nodes/register` 告知。
|
|
461
|
+
|
|
417
462
|
```
|
|
418
463
|
GET /manifest Node Manifest(dreammate-network node.schema.json)
|
|
419
464
|
GET /health
|
|
@@ -434,12 +479,37 @@ session://<node>/<runtime>/<session_id>
|
|
|
434
479
|
session://Scott-Mac.local/claude/87f7a60a-a86e-49c5-b711-e463156a5420
|
|
435
480
|
```
|
|
436
481
|
|
|
482
|
+
**跨机实测**(Windows 节点读 Mac 的会话,全程没有 Control Plane 参与):
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
scott-pc$ curl http://scott-mac.tailfb4720.ts.net:7777/v1/sessions?limit=3
|
|
486
|
+
{ "node": "scott-mac",
|
|
487
|
+
"sessions": [ { "uri": "session://scott-mac/claude/87f7a60a-…", … } ] }
|
|
488
|
+
```
|
|
489
|
+
|
|
437
490
|
**读取即落边。** 请求带 `X-Caller-Session: <调用方会话>` 时,读取当下就写入
|
|
438
491
|
`调用方 --references--> 目标`,与 CLI 的 `SESSION_READER_CALLER_SESSION` 是同一条路径。
|
|
439
492
|
两端都必须是本地已索引的会话,否则静默跳过——悬空边比没有边更糟。
|
|
440
493
|
|
|
441
|
-
|
|
442
|
-
`
|
|
494
|
+
**节点身份由 [`@1agents/dreammate-node`](https://github.com/scottzx/dreammate-node) 统一提供**
|
|
495
|
+
(优先 `tailscale status --json` 的 `Self`,缓存 60s):它是每台机器的公共事实,
|
|
496
|
+
本机所有服务读到同一份,不会各自生成 id 把一台机器裂成几个 Node。
|
|
497
|
+
|
|
498
|
+
```
|
|
499
|
+
node_id nigVtDS1s521CNTRL ← tailscale ID,重启不变
|
|
500
|
+
name scott-mac ← DNSName 前缀
|
|
501
|
+
type macos ← 由 tailscale 的 OS 映射
|
|
502
|
+
base_url http://scott-mac.tailfb4720.ts.net:7777/v1 ← MagicDNS,跨机可直接用
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
> ⚠️ 名字取 **DNSName** 而不是 HostName 是有原因的:iOS 设备的 HostName
|
|
506
|
+
> 全是 `localhost`(实测 11 个节点里只有 9 个唯一),几台手机接进来会产出
|
|
507
|
+
> 一模一样的 `session://localhost/yima/...`。DNSName 实测 11/11 唯一且可读。
|
|
508
|
+
|
|
509
|
+
没装 / 没登录 tailscale 时**静默回退**到本地身份(hostname + 首次生成的 uuid,
|
|
510
|
+
存在 `~/.1agents/node.json`)。回退身份的 name 不保证跨设备唯一,只适合单机自用。
|
|
511
|
+
`manifest.metadata.identity_source` 会如实报告身份来自 `tailscale` 还是 `local`。
|
|
512
|
+
`DREAMMATE_NODE_ID` / `DREAMMATE_NODE_NAME` 覆盖一切(容器或同机第二个实例用)。
|
|
443
513
|
|
|
444
514
|
**只读、且默认只监听 loopback。** 会话原文含源码、shell 历史和恰好滚过屏幕的密钥,
|
|
445
515
|
所以走出本机必须是一个刻意动作:显式 `--host`,并且最好配 `--token`(`Authorization: Bearer`)。
|
package/dist/bin/1session.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { aggregateWorkspaceSessions } from '../src/aggregator.js';
|
|
5
5
|
import { distillSession } from '../src/distiller.js';
|
|
6
6
|
import { buildOverview } from '../src/overview.js';
|
|
7
|
-
import {
|
|
7
|
+
import { eventDetails, summarizeTurns, turnDetail } from '../src/turns.js';
|
|
8
8
|
import { commandLedger, displayPath, errorLedger, fileLedger, jobLedger } from '../src/ledger.js';
|
|
9
9
|
import { listRecentSessions, loadSession } from '../src/resolver.js';
|
|
10
10
|
import { searchSessions } from '../src/search.js';
|
|
@@ -15,7 +15,8 @@ const USAGE = `1session — cross-agent session Read Plane
|
|
|
15
15
|
1session list [--limit <n>] [--scope <path>|cwd|global] [--provider <name>] [--since 24h] [--json]
|
|
16
16
|
1session overview <session-id> [--json] 第 1 层:会话概要
|
|
17
17
|
1session turns <session-id> [--json] 第 2 层:逐轮概要
|
|
18
|
-
1session turn <session-id> <n> [--event <k>] [--json]
|
|
18
|
+
1session turn <session-id> <n> [--event <k|a-b|a,b,c>] [--json]
|
|
19
|
+
第 3 层:单轮 / 单次或多次工具调用明细
|
|
19
20
|
1session jobs <session-id> [--json] 异步作业账本
|
|
20
21
|
1session commands <session-id> [--failed] [--host h] [--turn n] [--json]
|
|
21
22
|
1session files <session-id> [--group project|runtime|log|all] [--json]
|
|
@@ -24,13 +25,17 @@ const USAGE = `1session — cross-agent session Read Plane
|
|
|
24
25
|
1session workspace [path] [--since 24h] [--limit <n>] [--digest] [--focus <f>] [--json]
|
|
25
26
|
1session index [<session-id>] [--all] [--scope <path>|cwd|global] [--force] [--since 30d] 建立/刷新索引
|
|
26
27
|
1session graph <session-id> [--json] 会话之间的引用关系
|
|
27
|
-
1session serve [--port 7777] [--host 127.0.0.1] [--token <t>]
|
|
28
|
+
1session serve [--port 7777] [--host 127.0.0.1] [--token <t>] [--no-report]
|
|
28
29
|
起 HTTP Service,把本机会话接入 DreamMate Network
|
|
29
30
|
1session skill install|status|uninstall [--agent claude,codex,antigravity]
|
|
30
31
|
[--copy] [--force] [--dry-run] [--json] 装到三家智能体的 skills 目录
|
|
31
32
|
1session search <query> [--scope <path>|cwd|global] [--since 24h] [--limit n] [--provider name]
|
|
32
33
|
[--kind user,assistant,thinking,tool_call,tool_result]
|
|
33
|
-
[--regex] [--case] [--context n] [--max-hits n]
|
|
34
|
+
[--regex] [--case] [--context n] [--max-hits n]
|
|
35
|
+
[--include-self] [--json]
|
|
36
|
+
|
|
37
|
+
search 的每条命中带 T<轮次> · E<事件号>,可直接拼成 1session turn <id> <T> --event <E>。
|
|
38
|
+
默认折叠本次检索自身在活跃会话里留下的回声,--include-self 展开。
|
|
34
39
|
|
|
35
40
|
全局:--no-index 绕过索引直读源文件。索引位于 ~/.1agents/session-reader/index.db
|
|
36
41
|
list / search / index --all 默认只看当前 pwd 目录(含子目录)下的会话。
|
|
@@ -42,7 +47,7 @@ Providers: antigravity (~/.gemini/antigravity/brain), claude (~/.claude/projects
|
|
|
42
47
|
/** Flags that never take a value, so they cannot swallow a positional. */
|
|
43
48
|
const BOOLEAN_FLAGS = new Set([
|
|
44
49
|
'json', 'failed', 'digest', 'regex', 'case', 'all', 'force', 'no-index', 'global',
|
|
45
|
-
'copy', 'dry-run',
|
|
50
|
+
'copy', 'dry-run', 'no-report', 'include-self',
|
|
46
51
|
]);
|
|
47
52
|
function parseArgs(argv) {
|
|
48
53
|
const [command = 'help', ...rest] = argv;
|
|
@@ -90,6 +95,12 @@ const kindsOf = (value) => str(value)
|
|
|
90
95
|
?.split(',')
|
|
91
96
|
.map((kind) => kind.trim())
|
|
92
97
|
.filter(Boolean);
|
|
98
|
+
/**
|
|
99
|
+
* The drill-down handle a hit carries: `T9 · E221` reads straight into
|
|
100
|
+
* `1session turn <id> 9 --event 221`. Turn first, because that is the order
|
|
101
|
+
* the command wants it in.
|
|
102
|
+
*/
|
|
103
|
+
const handle = (turn, event) => `T${turn || '?'} · E${event}`;
|
|
93
104
|
const focusOf = (value) => {
|
|
94
105
|
const focus = str(value);
|
|
95
106
|
return focus === 'marketing' || focus === 'full' ? focus : 'review';
|
|
@@ -268,17 +279,24 @@ async function main() {
|
|
|
268
279
|
}
|
|
269
280
|
case 'turn': {
|
|
270
281
|
const session = await load(positional[0]);
|
|
271
|
-
const
|
|
272
|
-
if (
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
+
const spec = str(flags.event);
|
|
283
|
+
if (spec !== undefined) {
|
|
284
|
+
const details = await eventDetails(session, spec);
|
|
285
|
+
// A bare index keeps returning one object; a range or a list — which
|
|
286
|
+
// the caller had to ask for explicitly — returns the array.
|
|
287
|
+
const single = /^\s*\d+\s*$/.test(spec);
|
|
288
|
+
print(json, single ? details[0] : details, details
|
|
289
|
+
.map((detail) => {
|
|
290
|
+
const body = detail.fullText ?? detail.text ?? detail.toolResult ?? '';
|
|
291
|
+
return [
|
|
292
|
+
`#${detail.index} ${detail.kind}${detail.toolName ? `(${detail.toolName})` : ''} ${detail.timestamp ?? ''}` +
|
|
293
|
+
`${detail.truncated ? (detail.fullText ? ' [已从 steps/ 补全]' : ' [已截断]') : ''}`,
|
|
294
|
+
...(detail.toolArgs ? ['', '参数:', JSON.stringify(detail.toolArgs, null, 2)] : []),
|
|
295
|
+
...(body ? ['', '内容:', body] : []),
|
|
296
|
+
...(detail.truncationNote ? ['', `⚠️ ${detail.truncationNote}`] : []),
|
|
297
|
+
].join('\n');
|
|
298
|
+
})
|
|
299
|
+
.join('\n\n────────\n\n'));
|
|
282
300
|
break;
|
|
283
301
|
}
|
|
284
302
|
const detail = turnDetail(session, num(positional[1]) ?? 1);
|
|
@@ -294,13 +312,15 @@ async function main() {
|
|
|
294
312
|
return `${head} ${oneLine(event.text ?? event.toolResult ?? JSON.stringify(event.toolArgs), 160)}`;
|
|
295
313
|
}),
|
|
296
314
|
'',
|
|
297
|
-
|
|
315
|
+
`(--event <k> 展开单个事件的完整内容;--event ${detail.summary.events[0]}-` +
|
|
316
|
+
`${Math.min(detail.summary.events[0] + 2, detail.summary.events[1])} 或 --event a,b,c 一次读相邻几条)`,
|
|
298
317
|
].join('\n'));
|
|
299
318
|
break;
|
|
300
319
|
}
|
|
301
320
|
case 'search': {
|
|
302
321
|
const query = positional.join(' ');
|
|
303
|
-
const
|
|
322
|
+
const includeSelf = flags['include-self'] === true;
|
|
323
|
+
const found = await searchSessions(query, {
|
|
304
324
|
workspace: scopeWorkspace(flags),
|
|
305
325
|
since: str(flags.since),
|
|
306
326
|
limit: num(flags.limit),
|
|
@@ -312,22 +332,42 @@ async function main() {
|
|
|
312
332
|
maxPerSession: num(flags['max-hits']),
|
|
313
333
|
useIndex,
|
|
314
334
|
});
|
|
335
|
+
// The searcher's own live transcript is noise by default: the query is
|
|
336
|
+
// in it because this very command put it there.
|
|
337
|
+
const hits = includeSelf ? found : found.filter((hit) => !hit.self);
|
|
315
338
|
const total = hits.reduce((sum, hit) => sum + hit.totalMatches, 0);
|
|
339
|
+
const foldedSessions = found.length - hits.length;
|
|
340
|
+
const foldedMatches = found.reduce((sum, hit) => sum + (hit.suppressed ?? 0) + (hits.includes(hit) ? 0 : hit.totalMatches), 0);
|
|
341
|
+
const foldNote = !includeSelf && foldedMatches
|
|
342
|
+
? `(已折叠 ${foldedMatches} 处本次检索自身留下的回声` +
|
|
343
|
+
`${foldedSessions ? `,涉及 ${foldedSessions} 个会话` : ''};--include-self 展开)`
|
|
344
|
+
: undefined;
|
|
316
345
|
print(json, hits, hits.length
|
|
317
346
|
? [
|
|
318
347
|
`${total} 处命中,分布在 ${hits.length} 个会话`,
|
|
348
|
+
...(foldNote ? [foldNote] : []),
|
|
319
349
|
...hits.flatMap((hit) => [
|
|
320
350
|
'',
|
|
321
351
|
`### ${hit.session.provider} ${hit.session.id.slice(0, 8)} ` +
|
|
322
352
|
`${hit.session.createdAt ?? '?'} → ${hit.session.updatedAt ?? '?'} (${hit.totalMatches} 命中)`,
|
|
323
353
|
` ${oneLine(hit.session.title, 80)}`,
|
|
324
|
-
...hit.matches.map((match) => `
|
|
354
|
+
...hit.matches.map((match) => ` ${handle(match.turn, match.index)} ` +
|
|
355
|
+
`${match.kind}${match.toolName ? `(${match.toolName})` : ''} ${match.excerpt}`),
|
|
356
|
+
...(hit.matches[0]
|
|
357
|
+
? [
|
|
358
|
+
` ↳ 1session turn ${hit.session.id.slice(0, 8)} ` +
|
|
359
|
+
`${hit.matches[0].turn || '<轮次>'} --event ${hit.matches[0].index}`,
|
|
360
|
+
]
|
|
361
|
+
: []),
|
|
325
362
|
...(hit.totalMatches > hit.matches.length
|
|
326
363
|
? [` …另有 ${hit.totalMatches - hit.matches.length} 处(--max-hits 调大或 --json 查看全部)`]
|
|
327
364
|
: []),
|
|
365
|
+
...(!includeSelf && hit.suppressed
|
|
366
|
+
? [` …另折叠 ${hit.suppressed} 处本次检索自身的回声(--include-self 展开)`]
|
|
367
|
+
: []),
|
|
328
368
|
]),
|
|
329
369
|
].join('\n')
|
|
330
|
-
: `无命中:${query}
|
|
370
|
+
: [`无命中:${query}`, ...(foldNote ? [foldNote] : [])].join('\n'));
|
|
331
371
|
break;
|
|
332
372
|
}
|
|
333
373
|
case 'index': {
|
|
@@ -426,12 +466,13 @@ async function main() {
|
|
|
426
466
|
break;
|
|
427
467
|
}
|
|
428
468
|
case 'serve': {
|
|
429
|
-
const { serve } = await import('../src/serve/http.js');
|
|
469
|
+
const { serve, DEFAULT_PORT } = await import('../src/serve/http.js');
|
|
430
470
|
await serve({
|
|
431
|
-
port: num(flags.port) ??
|
|
471
|
+
port: num(flags.port) ?? DEFAULT_PORT,
|
|
432
472
|
...(str(flags.host) ? { host: str(flags.host) } : {}),
|
|
433
473
|
...(str(flags.token) ? { token: str(flags.token) } : {}),
|
|
434
474
|
...(str(flags['base-url']) ? { baseUrl: str(flags['base-url']) } : {}),
|
|
475
|
+
...(flags['no-report'] === true ? { report: false } : {}),
|
|
435
476
|
});
|
|
436
477
|
// The server owns the process from here; nothing after this resolves.
|
|
437
478
|
await new Promise(() => { });
|
package/dist/src/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ export { distillSession, editedFiles, shellCommand } from './distiller.js';
|
|
|
4
4
|
export { buildOverview } from './overview.js';
|
|
5
5
|
export { classifyUserTurn } from './classify.js';
|
|
6
6
|
export { commandLedger, displayPath, errorLedger, fileLedger, jobCounts, jobLedger, } from './ledger.js';
|
|
7
|
-
export { eventDetail, summarizeTurns, turnDetail, type EventDetail } from './turns.js';
|
|
7
|
+
export { eventDetail, eventDetails, parseEventSpec, summarizeTurns, turnDetail, turnNoAt, turnStarts, turnStartsFrom, type EventDetail, } from './turns.js';
|
|
8
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';
|
|
9
|
+
export { echoFolder, isCallerSession, mandatoryLiteral, planQuery, searchSessions, type Candidate, type QueryPlan, type SearchHit, type SearchMatch, type SearchOptions, } from './search.js';
|
|
10
10
|
export { aggregateWorkspaceSessions, type AggregateOptions } from './aggregator.js';
|
|
11
11
|
export type { ProviderAdapter, SessionCandidate } from './parsers/provider.js';
|
|
12
12
|
export { canonicalizePath, isInside, slugifyWorkspace } from './util/paths.js';
|
|
@@ -18,5 +18,5 @@ export { captureRuntimeEdge, deriveEdges, edgeEvidence, edgesOf, invocationsOf,
|
|
|
18
18
|
export { deriveFacts } from './store/facts.js';
|
|
19
19
|
export { EDGE_VERSION, EXTRACTOR_VERSION, PARSER_VERSION, SCHEMA_VERSION, } from './store/schema.js';
|
|
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
|
-
export { buildManifest, nodeIdentity,
|
|
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';
|
package/dist/src/index.js
CHANGED
|
@@ -3,9 +3,9 @@ export { distillSession, editedFiles, shellCommand } from './distiller.js';
|
|
|
3
3
|
export { buildOverview } from './overview.js';
|
|
4
4
|
export { classifyUserTurn } from './classify.js';
|
|
5
5
|
export { commandLedger, displayPath, errorLedger, fileLedger, jobCounts, jobLedger, } from './ledger.js';
|
|
6
|
-
export { eventDetail, summarizeTurns, turnDetail } from './turns.js';
|
|
6
|
+
export { eventDetail, eventDetails, parseEventSpec, summarizeTurns, turnDetail, turnNoAt, turnStarts, turnStartsFrom, } from './turns.js';
|
|
7
7
|
export { analyzableCommand, fileWrites, rawCommand, stripHeredocs, resolveWritePath, } from './writes.js';
|
|
8
|
-
export { mandatoryLiteral, planQuery, searchSessions, } from './search.js';
|
|
8
|
+
export { echoFolder, isCallerSession, mandatoryLiteral, planQuery, searchSessions, } from './search.js';
|
|
9
9
|
export { aggregateWorkspaceSessions } from './aggregator.js';
|
|
10
10
|
export { canonicalizePath, isInside, slugifyWorkspace } from './util/paths.js';
|
|
11
11
|
export { defaultDbPath, openStore, resetStoreCache } from './store/db.js';
|
|
@@ -16,5 +16,5 @@ export { captureRuntimeEdge, deriveEdges, edgeEvidence, edgesOf, invocationsOf,
|
|
|
16
16
|
export { deriveFacts } from './store/facts.js';
|
|
17
17
|
export { EDGE_VERSION, EXTRACTOR_VERSION, PARSER_VERSION, SCHEMA_VERSION, } from './store/schema.js';
|
|
18
18
|
export { SKILL_NAME, agentTargets, bundledSkillDir, describeState, installSkill, skillStatus, uninstallSkill, } from './skill.js';
|
|
19
|
-
export { buildManifest, nodeIdentity,
|
|
19
|
+
export { buildManifest, nodeIdentity, sessionUri, SESSION_CAPABILITIES, } from './serve/node.js';
|
|
20
20
|
export { createServer, serve } from './serve/http.js';
|
package/dist/src/ledger.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { isInside } from './util/paths.js';
|
|
3
3
|
import { oneLine } from './util/text.js';
|
|
4
4
|
import { analyzableCommand, fileWrites, rawCommand, resolveWritePath } from './writes.js';
|
|
5
|
-
import {
|
|
5
|
+
import { turnNoAt, turnStarts } from './turns.js';
|
|
6
6
|
const SSH_HOST = /\b[\w.-]+@((?:\d{1,3}(?:\.\d{1,3}){3})|(?:[\w-]+(?:\.[\w-]+)+))/;
|
|
7
7
|
const BACKGROUND = /\b(?:nohup|setsid|start_new_session\s*=\s*True|subprocess\.Popen|Popen\(|queue-[\w-]+\.py)\b/;
|
|
8
8
|
const LOG_PATH = /([\w./-]+\.log)\b/;
|
|
@@ -11,11 +11,8 @@ const LOG_FILE = /(?:\.log|\.download\.log)$|progress\.jsonl?$/i;
|
|
|
11
11
|
const TEMP_DIR = /^\/(?:private\/)?(?:tmp|var\/folders)\//;
|
|
12
12
|
/** Maps each event index to the turn it belongs to. */
|
|
13
13
|
function turnOf(session) {
|
|
14
|
-
const
|
|
15
|
-
return (index) =>
|
|
16
|
-
const found = ranges.findIndex(([start, end]) => index >= start && index <= end);
|
|
17
|
-
return found === -1 ? 0 : found + 1;
|
|
18
|
-
};
|
|
14
|
+
const starts = turnStarts(session);
|
|
15
|
+
return (index) => (index < 0 ? 0 : turnNoAt(starts, index));
|
|
19
16
|
}
|
|
20
17
|
/**
|
|
21
18
|
* Every shell command with whatever is known about how it went. Codex records
|
package/dist/src/search.d.ts
CHANGED
|
@@ -12,9 +12,20 @@ export interface SearchOptions extends ListOptions {
|
|
|
12
12
|
context?: number;
|
|
13
13
|
/** Matches recorded per session before scanning moves on. */
|
|
14
14
|
maxPerSession?: number;
|
|
15
|
+
/**
|
|
16
|
+
* The session doing the searching, so its own live transcript can be marked.
|
|
17
|
+
* Defaults to `SESSION_READER_CALLER_SESSION`; pass `''` to disable.
|
|
18
|
+
*/
|
|
19
|
+
selfSessionId?: string;
|
|
15
20
|
}
|
|
16
21
|
export interface SearchMatch {
|
|
17
22
|
index: number;
|
|
23
|
+
/**
|
|
24
|
+
* The turn this event belongs to — the other half of the drill-down handle,
|
|
25
|
+
* so `T<turn> · E<index>` maps straight onto `turn <id> <turn> --event <index>`.
|
|
26
|
+
* 0 when the session records no turn that contains the event.
|
|
27
|
+
*/
|
|
28
|
+
turn: number;
|
|
18
29
|
kind: TurnKind;
|
|
19
30
|
toolName?: string;
|
|
20
31
|
timestamp?: string;
|
|
@@ -24,6 +35,14 @@ export interface SearchHit {
|
|
|
24
35
|
session: SessionRef;
|
|
25
36
|
matches: SearchMatch[];
|
|
26
37
|
totalMatches: number;
|
|
38
|
+
/** Matches dropped because they were this very search echoing back. */
|
|
39
|
+
suppressed?: number;
|
|
40
|
+
/**
|
|
41
|
+
* The searcher's own session: either the injected caller id, or a session
|
|
42
|
+
* whose every match was the running invocation. Never removed from the
|
|
43
|
+
* result — callers hide it, so the count stays reportable.
|
|
44
|
+
*/
|
|
45
|
+
self?: boolean;
|
|
27
46
|
}
|
|
28
47
|
/**
|
|
29
48
|
* A substring that every string matching `source` must contain, or `undefined`
|
|
@@ -49,6 +68,35 @@ export interface QueryPlan {
|
|
|
49
68
|
* either way.
|
|
50
69
|
*/
|
|
51
70
|
export declare function planQuery(query: string, options?: SearchOptions): QueryPlan;
|
|
71
|
+
export interface Candidate {
|
|
72
|
+
index: number;
|
|
73
|
+
kind: TurnKind;
|
|
74
|
+
toolName?: string;
|
|
75
|
+
timestamp?: string;
|
|
76
|
+
body: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Folds the Read Plane's own footprint out of one session: an invocation
|
|
80
|
+
* running right now, and the result carrying what it printed.
|
|
81
|
+
*
|
|
82
|
+
* It deliberately does not require the invocation to carry *this* query. A
|
|
83
|
+
* `1session` call from a minute ago prints other sessions' content verbatim,
|
|
84
|
+
* so it matches queries it never mentioned — and whatever it echoed is still
|
|
85
|
+
* in the session it was quoting from, where the search finds it properly, with
|
|
86
|
+
* a handle that drills down to the real thing instead of to a screenful of
|
|
87
|
+
* this tool's output.
|
|
88
|
+
*
|
|
89
|
+
* Stateful because the second half is only knowable from the first, so it is
|
|
90
|
+
* built fresh per session and fed events in index order — which both search
|
|
91
|
+
* paths already produce.
|
|
92
|
+
*/
|
|
93
|
+
export declare function echoFolder(now: number): (candidate: Candidate) => boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Whether a session is the one running the search. Accepts the canonical
|
|
96
|
+
* `provider:native_id`, the bare native id, or a 6+ character prefix of it —
|
|
97
|
+
* the same spellings `resolveSession` takes.
|
|
98
|
+
*/
|
|
99
|
+
export declare function isCallerSession(ref: SessionRef, callerId: string | undefined): boolean;
|
|
52
100
|
/**
|
|
53
101
|
* Full-text search across sessions.
|
|
54
102
|
*
|