@1agents/session-reader 0.5.1 → 0.6.2
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 +61 -12
- package/dist/bin/1session.js +4 -3
- package/dist/src/parsers/dsh.d.ts +2 -0
- package/dist/src/parsers/dsh.js +298 -0
- package/dist/src/parsers/grok.d.ts +7 -0
- package/dist/src/parsers/grok.js +554 -0
- package/dist/src/parsers/provider.d.ts +7 -0
- package/dist/src/resolver.js +19 -6
- package/dist/src/serve/http.js +13 -1
- package/dist/src/serve/node.js +14 -6
- package/dist/src/skill.d.ts +3 -3
- package/dist/src/skill.js +12 -2
- package/dist/src/store/nul.d.ts +12 -0
- package/dist/src/store/nul.js +52 -0
- package/dist/src/store/read.js +4 -3
- package/dist/src/store/schema.d.ts +3 -3
- package/dist/src/store/schema.js +3 -3
- package/dist/src/store/write.js +3 -2
- package/dist/src/types.d.ts +1 -1
- package/dist/src/util/text.js +3 -0
- package/dist/src/util/zstd.d.ts +14 -0
- package/dist/src/util/zstd.js +129 -0
- package/dist/src/writes.js +19 -2
- package/package.json +5 -3
- package/skills/1session/SKILL.md +48 -9
- package/skills/1session/evals/evals.json +43 -1
- package/skills/1session/references/cli.md +1 -1
- package/skills/1session/references/install.md +195 -0
package/dist/src/skill.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** The bundled skill's directory name, used as the entry name in every agent. */
|
|
2
2
|
export declare const SKILL_NAME = "1session";
|
|
3
|
-
export type SkillAgent = 'claude' | 'codex' | 'antigravity';
|
|
3
|
+
export type SkillAgent = 'claude' | 'codex' | 'antigravity' | 'grok' | 'dsh';
|
|
4
4
|
export interface AgentTarget {
|
|
5
5
|
agent: SkillAgent;
|
|
6
6
|
/** Where this agent loads user skills from. */
|
|
@@ -9,8 +9,8 @@ export interface AgentTarget {
|
|
|
9
9
|
homeDir: string;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* so one bundled skill can serve all of them unchanged.
|
|
12
|
+
* Every one of them loads `<dir>/<name>/SKILL.md` with the same YAML
|
|
13
|
+
* frontmatter, so one bundled skill can serve all of them unchanged.
|
|
14
14
|
*/
|
|
15
15
|
export declare function agentTargets(home?: string): AgentTarget[];
|
|
16
16
|
/**
|
package/dist/src/skill.js
CHANGED
|
@@ -6,8 +6,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
/** The bundled skill's directory name, used as the entry name in every agent. */
|
|
7
7
|
export const SKILL_NAME = '1session';
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* so one bundled skill can serve all of them unchanged.
|
|
9
|
+
* Every one of them loads `<dir>/<name>/SKILL.md` with the same YAML
|
|
10
|
+
* frontmatter, so one bundled skill can serve all of them unchanged.
|
|
11
11
|
*/
|
|
12
12
|
export function agentTargets(home = os.homedir()) {
|
|
13
13
|
return [
|
|
@@ -27,6 +27,16 @@ export function agentTargets(home = os.homedir()) {
|
|
|
27
27
|
homeDir: path.join(home, '.gemini', 'antigravity'),
|
|
28
28
|
skillsDir: path.join(home, '.gemini', 'antigravity', 'skills'),
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
agent: 'grok',
|
|
32
|
+
homeDir: path.join(home, '.grok'),
|
|
33
|
+
skillsDir: path.join(home, '.grok', 'skills'),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
agent: 'dsh',
|
|
37
|
+
homeDir: path.join(home, '.dsh'),
|
|
38
|
+
skillsDir: path.join(home, '.dsh', 'skills'),
|
|
39
|
+
},
|
|
30
40
|
];
|
|
31
41
|
}
|
|
32
42
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 写进 SQLite 的 TEXT 列之前调用。
|
|
3
|
+
*
|
|
4
|
+
* 绝大多数内容两个字符都不含,直接原样返回——所以常态是零拷贝。
|
|
5
|
+
*/
|
|
6
|
+
export declare function encodeText(value: string): string;
|
|
7
|
+
export declare function encodeText(value: string | null | undefined): string | null;
|
|
8
|
+
/** 从 SQLite 的 TEXT 列读出来之后调用。 */
|
|
9
|
+
export declare function decodeText(value: string): string;
|
|
10
|
+
export declare function decodeText(value: string | null | undefined): string | null;
|
|
11
|
+
/** 测试与诊断用:这段文本经过 SQLite 的 TEXT 列会不会被截断。 */
|
|
12
|
+
export declare function wouldTruncate(value: string): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite 的 TEXT 列在第一个 NUL 处截断——不报错,安静地丢掉后面全部内容。
|
|
3
|
+
*
|
|
4
|
+
* 实测:一个 antigravity 会话里 `wsl -l -v` 的 UTF-16 输出被当 UTF-8 读,产生
|
|
5
|
+
* 交错的 NUL;502 字符的 tool_result 存进索引再读出来只剩 342,后面 160 个
|
|
6
|
+
* 字符凭空消失。round-trip 测试因此长期飘红,而失败信息指向的是内容本身,
|
|
7
|
+
* 很难看出是存储层干的。
|
|
8
|
+
*
|
|
9
|
+
* 改存 BLOB 能保真,但 `text` / `tool_result` 上有 SQL 搜索(`LIKE` 对 BLOB
|
|
10
|
+
* 不工作),所以改成**写入时转义、读取时还原**。
|
|
11
|
+
*
|
|
12
|
+
* 引导符用 U+FFFF:Unicode 明确规定的 noncharacter,不会出现在有效文本里;
|
|
13
|
+
* 万一真出现也会被双写,所以还原无歧义。
|
|
14
|
+
*
|
|
15
|
+
* 两个常量用 `String.fromCharCode` 而不是字面量,免得源文件里真带上这些
|
|
16
|
+
* 字符——它们在编辑器、diff、终端里都是隐形的。
|
|
17
|
+
*/
|
|
18
|
+
const NUL = String.fromCharCode(0x00);
|
|
19
|
+
const LEAD = String.fromCharCode(0xffff);
|
|
20
|
+
const ESCAPED_NUL = `${LEAD}0`;
|
|
21
|
+
const ESCAPED_LEAD = `${LEAD}${LEAD}`;
|
|
22
|
+
export function encodeText(value) {
|
|
23
|
+
if (value === null || value === undefined)
|
|
24
|
+
return null;
|
|
25
|
+
if (!value.includes(NUL) && !value.includes(LEAD))
|
|
26
|
+
return value;
|
|
27
|
+
// 顺序要紧:先把引导符自己双写,再拿它去转义 NUL。反过来会把刚写出的
|
|
28
|
+
// 转义序列又转义一遍。
|
|
29
|
+
return value.split(LEAD).join(ESCAPED_LEAD).split(NUL).join(ESCAPED_NUL);
|
|
30
|
+
}
|
|
31
|
+
export function decodeText(value) {
|
|
32
|
+
if (value === null || value === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
if (!value.includes(LEAD))
|
|
35
|
+
return value;
|
|
36
|
+
let out = '';
|
|
37
|
+
for (let i = 0; i < value.length; i++) {
|
|
38
|
+
if (value[i] !== LEAD) {
|
|
39
|
+
out += value[i];
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const next = value[++i];
|
|
43
|
+
// 双写还原成引导符本身;LEAD+'0' 还原成 NUL;落单的引导符原样留着,
|
|
44
|
+
// 宁可多留一个字符,也不要把不认识的序列吞掉。
|
|
45
|
+
out += next === LEAD ? LEAD : next === '0' ? NUL : LEAD + (next ?? '');
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** 测试与诊断用:这段文本经过 SQLite 的 TEXT 列会不会被截断。 */
|
|
50
|
+
export function wouldTruncate(value) {
|
|
51
|
+
return value.includes(NUL);
|
|
52
|
+
}
|
package/dist/src/store/read.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeText } from './nul.js';
|
|
1
2
|
import { turnStartsFrom } from '../turns.js';
|
|
2
3
|
import { emptyProviderStats, } from '../types.js';
|
|
3
4
|
export function sessionRow(db, id) {
|
|
@@ -70,7 +71,7 @@ export function refOf(row) {
|
|
|
70
71
|
id: row.native_id,
|
|
71
72
|
provider: row.provider,
|
|
72
73
|
path: row.source_path,
|
|
73
|
-
...(row.title === null ? {} : { title: row.title }),
|
|
74
|
+
...(row.title === null ? {} : { title: decodeText(row.title) }),
|
|
74
75
|
...(row.workspace === null ? {} : { workspace: row.workspace }),
|
|
75
76
|
...(row.started_at === null ? {} : { createdAt: row.started_at }),
|
|
76
77
|
...(row.ended_at === null ? {} : { updatedAt: row.ended_at }),
|
|
@@ -89,12 +90,12 @@ function eventsOf(db, id, nativeId) {
|
|
|
89
90
|
id: `${nativeId}#${row.idx}`,
|
|
90
91
|
index: row.idx,
|
|
91
92
|
kind: row.kind,
|
|
92
|
-
...(row.text === null ? {} : { text: row.text }),
|
|
93
|
+
...(row.text === null ? {} : { text: decodeText(row.text) }),
|
|
93
94
|
...(row.tool_name === null ? {} : { toolName: row.tool_name }),
|
|
94
95
|
...(row.tool_args_json === null
|
|
95
96
|
? {}
|
|
96
97
|
: { toolArgs: JSON.parse(row.tool_args_json) }),
|
|
97
|
-
...(row.tool_result === null ? {} : { toolResult: row.tool_result }),
|
|
98
|
+
...(row.tool_result === null ? {} : { toolResult: decodeText(row.tool_result) }),
|
|
98
99
|
...(row.is_error === null ? {} : { isError: row.is_error === 1 }),
|
|
99
100
|
...(row.ts === null ? {} : { timestamp: row.ts }),
|
|
100
101
|
...(row.source_index === null ? {} : { sourceIndex: row.source_index }),
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* the behaviour — never derive them from a git hash, `dist` ships without git.
|
|
8
8
|
*/
|
|
9
9
|
/** DDL layout. A bump drops and rebuilds the whole database. */
|
|
10
|
-
export declare const SCHEMA_VERSION =
|
|
10
|
+
export declare const SCHEMA_VERSION = 2;
|
|
11
11
|
/** L1 semantics — anything in `parsers/` that changes normalized events. */
|
|
12
|
-
export declare const PARSER_VERSION =
|
|
12
|
+
export declare const PARSER_VERSION = 3;
|
|
13
13
|
/** L2 rules — `writes.ts` / `ledger.ts`. Re-derives facts from stored events. */
|
|
14
|
-
export declare const EXTRACTOR_VERSION =
|
|
14
|
+
export declare const EXTRACTOR_VERSION = 2;
|
|
15
15
|
/** L3 rules — `store/edges.ts`. Re-derives edges from stored events. */
|
|
16
16
|
export declare const EDGE_VERSION = 1;
|
|
17
17
|
export declare const DDL = "\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY, -- canonical \"provider:native_id\"\n provider TEXT NOT NULL,\n native_id TEXT NOT NULL,\n source_path TEXT NOT NULL,\n workspace TEXT,\n title TEXT,\n started_at TEXT,\n ended_at TEXT,\n event_count INTEGER NOT NULL DEFAULT 0,\n turn_count INTEGER NOT NULL DEFAULT 0,\n source_size INTEGER NOT NULL DEFAULT 0,\n source_mtime_ms INTEGER NOT NULL DEFAULT 0,\n head_hash TEXT,\n aux_fingerprint TEXT,\n indexed_bytes INTEGER NOT NULL DEFAULT 0, -- reserved for resumable parse\n parser_version INTEGER NOT NULL DEFAULT 0,\n extractor_version INTEGER NOT NULL DEFAULT 0,\n edge_version INTEGER NOT NULL DEFAULT 0,\n indexed_at TEXT,\n artifacts_json TEXT,\n stats_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS events (\n session_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n kind TEXT NOT NULL,\n text TEXT,\n tool_name TEXT,\n tool_args_json TEXT,\n tool_result TEXT,\n is_error INTEGER,\n ts TEXT,\n source_index INTEGER,\n exit_code INTEGER,\n pid TEXT,\n duration_ms INTEGER,\n provider_truncated INTEGER, -- the provider's own \"shortened copy\" flag\n PRIMARY KEY (session_id, idx)\n) WITHOUT ROWID;\n\nCREATE TABLE IF NOT EXISTS file_ops (\n session_id TEXT NOT NULL,\n path TEXT NOT NULL,\n host TEXT,\n operation TEXT NOT NULL,\n turn INTEGER NOT NULL,\n event_index INTEGER NOT NULL,\n ts TEXT,\n provenance TEXT NOT NULL,\n extractor TEXT NOT NULL,\n file_group TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS commands (\n session_id TEXT NOT NULL,\n event_index INTEGER NOT NULL,\n turn INTEGER NOT NULL,\n command TEXT NOT NULL,\n host TEXT,\n cwd TEXT,\n exit_code INTEGER,\n duration_ms INTEGER,\n pid TEXT,\n stderr TEXT,\n ts TEXT,\n provenance TEXT NOT NULL,\n extractor TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS jobs (\n session_id TEXT NOT NULL,\n job_id TEXT NOT NULL,\n command TEXT,\n pid TEXT,\n host TEXT,\n log TEXT,\n started_at TEXT,\n discovered_from INTEGER,\n status TEXT NOT NULL,\n provenance TEXT NOT NULL,\n extractor TEXT NOT NULL,\n evidence_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS session_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n from_session TEXT NOT NULL,\n to_session TEXT NOT NULL,\n relation TEXT NOT NULL,\n first_seen_at TEXT,\n last_seen_at TEXT,\n evidence_count INTEGER NOT NULL DEFAULT 0,\n UNIQUE (from_session, to_session, relation)\n);\n\nCREATE TABLE IF NOT EXISTS edge_evidence (\n edge_id INTEGER NOT NULL,\n source_session TEXT NOT NULL,\n event_index INTEGER NOT NULL,\n operation TEXT NOT NULL,\n command TEXT,\n ts TEXT,\n provenance TEXT NOT NULL,\n extractor TEXT NOT NULL,\n UNIQUE (edge_id, source_session, event_index)\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_kind ON events (session_id, kind);\nCREATE INDEX IF NOT EXISTS idx_fileops_path ON file_ops (path);\nCREATE INDEX IF NOT EXISTS idx_fileops_session ON file_ops (session_id);\nCREATE INDEX IF NOT EXISTS idx_commands_session ON commands (session_id, exit_code);\nCREATE INDEX IF NOT EXISTS idx_jobs_session ON jobs (session_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_ws ON sessions (workspace, ended_at);\nCREATE INDEX IF NOT EXISTS idx_sessions_native ON sessions (native_id);\nCREATE INDEX IF NOT EXISTS idx_edges_to ON session_edges (to_session);\n";
|
package/dist/src/store/schema.js
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* the behaviour — never derive them from a git hash, `dist` ships without git.
|
|
8
8
|
*/
|
|
9
9
|
/** DDL layout. A bump drops and rebuilds the whole database. */
|
|
10
|
-
export const SCHEMA_VERSION =
|
|
10
|
+
export const SCHEMA_VERSION = 2;
|
|
11
11
|
/** L1 semantics — anything in `parsers/` that changes normalized events. */
|
|
12
|
-
export const PARSER_VERSION =
|
|
12
|
+
export const PARSER_VERSION = 3;
|
|
13
13
|
/** L2 rules — `writes.ts` / `ledger.ts`. Re-derives facts from stored events. */
|
|
14
|
-
export const EXTRACTOR_VERSION =
|
|
14
|
+
export const EXTRACTOR_VERSION = 2;
|
|
15
15
|
/** L3 rules — `store/edges.ts`. Re-derives edges from stored events. */
|
|
16
16
|
export const EDGE_VERSION = 1;
|
|
17
17
|
export const DDL = `
|
package/dist/src/store/write.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encodeText } from './nul.js';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import crypto from 'node:crypto';
|
|
3
4
|
import { PARSER_VERSION } from './schema.js';
|
|
@@ -51,7 +52,7 @@ export function writeSession(db, session, fingerprint, turnCount) {
|
|
|
51
52
|
id, provider, native_id, source_path, workspace, title, started_at, ended_at,
|
|
52
53
|
event_count, turn_count, source_size, source_mtime_ms, head_hash, aux_fingerprint,
|
|
53
54
|
parser_version, extractor_version, edge_version, indexed_at, artifacts_json, stats_json
|
|
54
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, ?)`).run(id, session.ref.provider, session.ref.id, session.ref.path, session.ref.workspace ?? null, session.ref.title
|
|
55
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, ?)`).run(id, session.ref.provider, session.ref.id, session.ref.path, session.ref.workspace ?? null, encodeText(session.ref.title), session.ref.createdAt ?? null, session.ref.updatedAt ?? null, session.turns.length, turnCount, fingerprint.sourceSize, fingerprint.sourceMtimeMs, fingerprint.headHash, fingerprint.auxFingerprint ?? null, PARSER_VERSION, new Date().toISOString(), JSON.stringify(session.artifacts), JSON.stringify(session.stats));
|
|
55
56
|
const insert = db.prepare(`INSERT INTO events (
|
|
56
57
|
session_id, idx, kind, text, tool_name, tool_args_json, tool_result, is_error,
|
|
57
58
|
ts, source_index, exit_code, pid, duration_ms, provider_truncated
|
|
@@ -60,7 +61,7 @@ export function writeSession(db, session, fingerprint, turnCount) {
|
|
|
60
61
|
// 0.08% and silently dropped search hits from long build logs — exactly
|
|
61
62
|
// the kind of "looks complete, isn't" answer this store exists to remove.
|
|
62
63
|
for (const event of session.turns) {
|
|
63
|
-
insert.run(id, event.index, event.kind, event.text
|
|
64
|
+
insert.run(id, event.index, event.kind, encodeText(event.text), event.toolName ?? null, event.toolArgs ? JSON.stringify(event.toolArgs) : null, encodeText(event.toolResult), event.isError === undefined ? null : event.isError ? 1 : 0, event.timestamp ?? null, event.sourceIndex ?? null, event.exitCode ?? null, event.processId ?? null, event.durationMs ?? null, event.truncated === undefined ? null : event.truncated ? 1 : 0);
|
|
64
65
|
}
|
|
65
66
|
db.exec('COMMIT');
|
|
66
67
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Canonical domain types shared by every provider parser. */
|
|
2
|
-
export type AgentProvider = 'antigravity' | 'claude' | 'codex' | 'cursor' | 'unknown';
|
|
2
|
+
export type AgentProvider = 'antigravity' | 'claude' | 'codex' | 'dsh' | 'grok' | 'cursor' | 'unknown';
|
|
3
3
|
/** Cheap metadata about a discovered session, obtained without a full parse. */
|
|
4
4
|
export interface SessionRef {
|
|
5
5
|
id: string;
|
package/dist/src/util/text.js
CHANGED
|
@@ -43,6 +43,9 @@ export function looksLikeInstructions(text) {
|
|
|
43
43
|
/<user_instructions>/.test(head) ||
|
|
44
44
|
/<environment_context>/.test(head) ||
|
|
45
45
|
/^<permissions instructions>/.test(head) ||
|
|
46
|
+
// Grok opens every session with an ambient `<user_info>/<git_status>/<rules>`
|
|
47
|
+
// block; the real prompt arrives as its own message right after it.
|
|
48
|
+
/^<user_info>/.test(head) ||
|
|
46
49
|
/^<recommended_plugins>/.test(head) ||
|
|
47
50
|
/^Caveat: The messages below were generated/.test(head));
|
|
48
51
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Every complete frame in the buffer, in order. */
|
|
2
|
+
export declare function frameRanges(buf: Buffer): [number, number][];
|
|
3
|
+
/** Concatenated payload of every frame that decompresses. */
|
|
4
|
+
export declare function decodeZstd(buf: Buffer): string;
|
|
5
|
+
export interface ZstdJsonlOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Read only this many bytes from the head of the file. Frames are
|
|
8
|
+
* self-delimiting, so a prefix decodes to a prefix of the session — which is
|
|
9
|
+
* all `scanRef` ever needs.
|
|
10
|
+
*/
|
|
11
|
+
headBytes?: number;
|
|
12
|
+
}
|
|
13
|
+
/** Streams a zstd-framed `.jsonl`, silently skipping malformed lines. */
|
|
14
|
+
export declare function readZstdJsonl(file: string, options?: ZstdJsonlOptions): AsyncGenerator<Record<string, unknown>>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import zlib from 'node:zlib';
|
|
3
|
+
/**
|
|
4
|
+
* Reading zstd-framed JSONL.
|
|
5
|
+
*
|
|
6
|
+
* Agents that compress their transcripts append one frame per flush, so a
|
|
7
|
+
* session file is a *concatenation* of complete frames. Node's zstd APIs —
|
|
8
|
+
* both the sync call and the stream — stop after the first one, which silently
|
|
9
|
+
* yields a session with a single line. The walker below therefore finds every
|
|
10
|
+
* frame boundary from the frame headers alone (no decompression), so the whole
|
|
11
|
+
* file is read and a truncated tail frame is dropped instead of poisoning it.
|
|
12
|
+
*/
|
|
13
|
+
/** zstd reached `node:zlib` in Node 22.15 / 23.8; `engines` asks for at least that. */
|
|
14
|
+
const SUPPORTED = typeof zlib.zstdDecompressSync === 'function';
|
|
15
|
+
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
16
|
+
const FRAME_MAGIC = 0xfd2fb528;
|
|
17
|
+
const SKIPPABLE_LOW = 0x184d2a50;
|
|
18
|
+
const SKIPPABLE_HIGH = 0x184d2a5f;
|
|
19
|
+
const DICTIONARY_ID_BYTES = [0, 1, 2, 4];
|
|
20
|
+
const CONTENT_SIZE_BYTES = [0, 2, 4, 8];
|
|
21
|
+
/**
|
|
22
|
+
* Offset just past the frame starting at `at`, or nothing when the bytes run
|
|
23
|
+
* out or the header is not one we understand. RFC 8878 §3.1: a frame is a
|
|
24
|
+
* header followed by blocks, each block announcing its own size, so the end
|
|
25
|
+
* can be found without inflating anything.
|
|
26
|
+
*/
|
|
27
|
+
function frameEnd(buf, at) {
|
|
28
|
+
let i = at + 4;
|
|
29
|
+
if (i >= buf.length)
|
|
30
|
+
return undefined;
|
|
31
|
+
const descriptor = buf[i++];
|
|
32
|
+
const contentSizeFlag = descriptor >> 6;
|
|
33
|
+
const singleSegment = (descriptor >> 5) & 1;
|
|
34
|
+
const hasChecksum = (descriptor >> 2) & 1;
|
|
35
|
+
i += singleSegment ? 0 : 1; // Window_Descriptor
|
|
36
|
+
i += DICTIONARY_ID_BYTES[descriptor & 3];
|
|
37
|
+
// The one asymmetric case: flag 0 means "1 byte" for a single-segment frame
|
|
38
|
+
// and "absent" otherwise.
|
|
39
|
+
i += contentSizeFlag === 0 ? singleSegment : CONTENT_SIZE_BYTES[contentSizeFlag];
|
|
40
|
+
for (;;) {
|
|
41
|
+
if (i + 3 > buf.length)
|
|
42
|
+
return undefined;
|
|
43
|
+
const header = buf[i] | (buf[i + 1] << 8) | (buf[i + 2] << 16);
|
|
44
|
+
i += 3;
|
|
45
|
+
const blockType = (header >> 1) & 3;
|
|
46
|
+
if (blockType === 3)
|
|
47
|
+
return undefined; // reserved — we are off the rails
|
|
48
|
+
i += blockType === 1 ? 1 : header >>> 3; // RLE stores a single byte
|
|
49
|
+
if (i > buf.length)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (header & 1)
|
|
52
|
+
break; // Last_Block
|
|
53
|
+
}
|
|
54
|
+
return i + (hasChecksum ? 4 : 0) <= buf.length ? i + (hasChecksum ? 4 : 0) : undefined;
|
|
55
|
+
}
|
|
56
|
+
/** Every complete frame in the buffer, in order. */
|
|
57
|
+
export function frameRanges(buf) {
|
|
58
|
+
const ranges = [];
|
|
59
|
+
let at = 0;
|
|
60
|
+
while (at + 4 <= buf.length) {
|
|
61
|
+
const magic = buf.readUInt32LE(at);
|
|
62
|
+
if (magic >= SKIPPABLE_LOW && magic <= SKIPPABLE_HIGH) {
|
|
63
|
+
if (at + 8 > buf.length)
|
|
64
|
+
break;
|
|
65
|
+
at += 8 + buf.readUInt32LE(at + 4);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const end = magic === FRAME_MAGIC ? frameEnd(buf, at) : undefined;
|
|
69
|
+
if (end === undefined) {
|
|
70
|
+
// Either not a frame start or a truncated one; resynchronize on the next
|
|
71
|
+
// magic rather than giving up on the rest of the file.
|
|
72
|
+
const next = buf.indexOf(MAGIC, at + 1);
|
|
73
|
+
if (next < 0)
|
|
74
|
+
break;
|
|
75
|
+
at = next;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
ranges.push([at, end]);
|
|
79
|
+
at = end;
|
|
80
|
+
}
|
|
81
|
+
return ranges;
|
|
82
|
+
}
|
|
83
|
+
/** Concatenated payload of every frame that decompresses. */
|
|
84
|
+
export function decodeZstd(buf) {
|
|
85
|
+
// Saying so is the point: a silent empty session would read as "that agent
|
|
86
|
+
// has no history", which is exactly the wrong thing to believe.
|
|
87
|
+
if (!SUPPORTED) {
|
|
88
|
+
throw new Error(`reading zstd-compressed sessions needs node:zlib zstd support (Node >= 22.15); this is ${process.version}`);
|
|
89
|
+
}
|
|
90
|
+
const parts = [];
|
|
91
|
+
for (const [start, end] of frameRanges(buf)) {
|
|
92
|
+
try {
|
|
93
|
+
parts.push(zlib.zstdDecompressSync(buf.subarray(start, end)));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* a frame written mid-flush is not worth failing the whole session for */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Buffer.concat(parts).toString('utf8');
|
|
100
|
+
}
|
|
101
|
+
/** Streams a zstd-framed `.jsonl`, silently skipping malformed lines. */
|
|
102
|
+
export async function* readZstdJsonl(file, options = {}) {
|
|
103
|
+
const handle = await fs.open(file, 'r');
|
|
104
|
+
let buf;
|
|
105
|
+
try {
|
|
106
|
+
const size = (await handle.stat()).size;
|
|
107
|
+
const length = Math.min(options.headBytes ?? size, size);
|
|
108
|
+
buf = Buffer.alloc(length);
|
|
109
|
+
const { bytesRead } = await handle.read(buf, 0, length, 0);
|
|
110
|
+
buf = buf.subarray(0, bytesRead);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await handle.close();
|
|
114
|
+
}
|
|
115
|
+
for (const line of decodeZstd(buf).split('\n')) {
|
|
116
|
+
const trimmed = line.trim();
|
|
117
|
+
if (!trimmed)
|
|
118
|
+
continue;
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(trimmed);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (parsed && typeof parsed === 'object')
|
|
127
|
+
yield parsed;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/dist/src/writes.js
CHANGED
|
@@ -11,9 +11,26 @@ const EDIT_TOOLS = new Set([
|
|
|
11
11
|
'create_file',
|
|
12
12
|
'apply_patch',
|
|
13
13
|
'str_replace_editor',
|
|
14
|
+
'search_replace',
|
|
15
|
+
]);
|
|
16
|
+
const FILE_ARGS = [
|
|
17
|
+
'file_path',
|
|
18
|
+
'filePath',
|
|
19
|
+
'notebook_path',
|
|
20
|
+
'target_file',
|
|
21
|
+
'TargetFile',
|
|
22
|
+
'AbsolutePath',
|
|
23
|
+
'path',
|
|
24
|
+
];
|
|
25
|
+
const SHELL_TOOLS = new Set([
|
|
26
|
+
'bash',
|
|
27
|
+
'run_command',
|
|
28
|
+
'run_terminal_command',
|
|
29
|
+
'shell',
|
|
30
|
+
'exec',
|
|
31
|
+
'local_shell',
|
|
32
|
+
'execute_command',
|
|
14
33
|
]);
|
|
15
|
-
const FILE_ARGS = ['file_path', 'filePath', 'notebook_path', 'TargetFile', 'AbsolutePath', 'path'];
|
|
16
|
-
const SHELL_TOOLS = new Set(['bash', 'run_command', 'shell', 'exec', 'local_shell', 'execute_command']);
|
|
17
34
|
const COMMAND_ARGS = ['command', 'CommandLine', 'cmd', 'input'];
|
|
18
35
|
const PATCH_FILE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
|
|
19
36
|
/** Extensions we accept on a bare token. Without this, `a.name` looks like a file. */
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agents/session-reader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Read Plane: cross-agent session discovery, turn inspection, workspace aggregation and distillation from raw local session files.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"codex",
|
|
8
8
|
"antigravity",
|
|
9
|
+
"grok",
|
|
10
|
+
"deepseek",
|
|
9
11
|
"session",
|
|
10
12
|
"transcript",
|
|
11
13
|
"jsonl",
|
|
@@ -49,7 +51,7 @@
|
|
|
49
51
|
"1session": "tsx bin/1session.ts"
|
|
50
52
|
},
|
|
51
53
|
"engines": {
|
|
52
|
-
"node": ">=22.
|
|
54
|
+
"node": ">=22.15"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
55
57
|
"@types/node": "^22.10.2",
|
|
@@ -58,6 +60,6 @@
|
|
|
58
60
|
},
|
|
59
61
|
"dependencies": {
|
|
60
62
|
"@1agents/dreammate-network": "^0.3.0",
|
|
61
|
-
"@1agents/dreammate-node": "^0.
|
|
63
|
+
"@1agents/dreammate-node": "^0.4.0"
|
|
62
64
|
}
|
|
63
65
|
}
|
package/skills/1session/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: 1session
|
|
3
|
-
description: Search and read the user's past AI coding sessions across Claude Code, Codex and
|
|
3
|
+
description: Search and read the user's past AI coding sessions across Claude Code, Codex, Antigravity, Grok and DeepSeek Harness (dsh) from their raw local session files, using the `1session` CLI. Use this whenever the user refers to work they did in an earlier session rather than in this conversation — "上次/之前/昨天我们改了什么", "那个报错后来怎么解决的", "我在哪个会话里提过 X", "这个功能是哪一轮加的", "codex 那边做到哪了", "grok/dsh 那边呢", "跨项目找一下", "整理一下最近几天的会话/写个周报". Also reach for it proactively, before asking the user to re-explain context they have obviously already established with some agent on this machine — the answer is usually already on disk. Read-only: it never modifies or resumes a session. This skill also covers getting itself running and spreading to other agents: when the `1session` CLI is missing it falls back to `npx` on the spot, and it knows how to install the package globally and push the skill into all five agents at once — so use it too when the user asks to install, upgrade, uninstall, share or distribute 1session / session-reader, or reports that the `1session` command is not found.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 1session — the cross-agent Read Plane
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Five agents write sessions to this machine in five different formats. `1session`
|
|
9
9
|
normalizes all of them and answers questions about what actually happened.
|
|
10
10
|
|
|
11
11
|
| Provider | On disk | Covered |
|
|
@@ -13,19 +13,58 @@ normalizes all of them and answers questions about what actually happened.
|
|
|
13
13
|
| `claude` | `~/.claude/projects/<slug>/<id>.jsonl` | prompts, tools, commands, files, tokens, git branch |
|
|
14
14
|
| `codex` | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` | same, plus structured `exit_code` / `stderr` / `pid` |
|
|
15
15
|
| `antigravity` | `~/.gemini/antigravity/brain/<uuid>/.../transcript.jsonl` | same, plus plan/walkthrough artifacts |
|
|
16
|
+
| `grok` | `~/.grok/sessions/<encoded cwd>/<uuid>/chat_history.jsonl` | same, plus tool durations/outcomes, background-task receipts, goal plans |
|
|
17
|
+
| `dsh` | `~/.dsh/sessions/<slug>/session-<uuid>/session.v2.jsonl.zstd` | same, plus native turn/step boundaries and per-message usage |
|
|
16
18
|
|
|
17
19
|
Everything is derived from the raw files at read time. Nothing is written back to
|
|
18
20
|
them, no daemon is involved, and no session is ever resumed or modified.
|
|
19
21
|
|
|
20
|
-
##
|
|
22
|
+
## Bootstrap: get the CLI, then answer the question
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
This skill travels on its own. Someone may have dropped `SKILL.md` into an agent
|
|
25
|
+
on a machine where the `1session` CLI does not exist, so the first call is a
|
|
26
|
+
probe rather than an assumption:
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
```bash
|
|
29
|
+
1session help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If that fails, **do not stop to install before answering.** `npx` runs the same
|
|
33
|
+
CLI with nothing installed:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y @1agents/session-reader@latest list --global --limit 10
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Substitute `npx -y @1agents/session-reader@latest` for `1session` everywhere
|
|
40
|
+
below, answer the question the user actually asked, and offer the permanent
|
|
41
|
+
install once, afterwards. Someone asking where last week's bug got fixed wants
|
|
42
|
+
the bug, not a setup errand.
|
|
43
|
+
|
|
44
|
+
The permanent install, when they want it:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm i -g @1agents/session-reader # requires Node >= 22.15
|
|
48
|
+
1session skill install # put this skill into every agent on the machine
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Node below 22.15 is a hard stop rather than a warning — the reader needs
|
|
52
|
+
`node:sqlite` for the index and `node:zlib`'s zstd to read dsh's compressed
|
|
53
|
+
sessions, and `npx` does not rescue an old runtime either. Check `node -v`, say
|
|
54
|
+
plainly that Node needs upgrading, and don't improvise around it.
|
|
55
|
+
|
|
56
|
+
That second command is what makes this spread: one run installs the skill into
|
|
57
|
+
Claude Code, Codex, Antigravity, Grok and dsh at once, so whichever agent the
|
|
58
|
+
user opens next already knows their history is readable. Run it after a global
|
|
59
|
+
install, **not** through `npx` — npx installs the package into a cache directory
|
|
60
|
+
that npm later garbage-collects, and the skill links would dangle with it.
|
|
61
|
+
|
|
62
|
+
`references/install.md` has the rest: PATH and permission failures, link vs copy,
|
|
63
|
+
upgrading, uninstalling, and what to hand someone who wants this on their own
|
|
64
|
+
machine. Read it when an install misbehaves or the user asks how to share this.
|
|
65
|
+
|
|
66
|
+
The first real run parses every session (~10s for a few hundred); an index makes
|
|
67
|
+
each call after that sub-second. A slow first call is that build, not a hang.
|
|
29
68
|
|
|
30
69
|
## Pick the command from the question
|
|
31
70
|
|
|
@@ -42,6 +42,48 @@
|
|
|
42
42
|
"Gives a drill-down handle — a turn/event number or an exact command the user can run to see the evidence",
|
|
43
43
|
"Reports how it was verified (tests green / the commit) rather than only describing the change"
|
|
44
44
|
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": 3,
|
|
48
|
+
"name": "missing-cli-still-answers",
|
|
49
|
+
"prompt": "上周我在这台机器上调一个超时的问题,后来是怎么解决的?(顺便说一句,我这台机器上好像没装过什么 1session,`1session` 这个命令敲下去是 command not found)",
|
|
50
|
+
"expected_output": "Recognizes the CLI is absent, immediately falls back to `npx -y @1agents/session-reader@latest ...` to actually answer the timeout question, and only then mentions the permanent install as a one-time follow-up — rather than stopping to install first or declaring it cannot help.",
|
|
51
|
+
"files": [],
|
|
52
|
+
"assertions": [
|
|
53
|
+
"Falls back to npx (`npx -y @1agents/session-reader@latest ...`) instead of stopping at the missing command",
|
|
54
|
+
"Actually attempts to answer the user's timeout question rather than turning the turn into a setup errand",
|
|
55
|
+
"Mentions the permanent install (`npm i -g @1agents/session-reader`) as an optional follow-up, not as a precondition",
|
|
56
|
+
"Does not run a global npm install without the user agreeing to it first",
|
|
57
|
+
"Does not claim the question is unanswerable because the CLI is missing"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": 4,
|
|
62
|
+
"name": "distribute-to-a-colleague",
|
|
63
|
+
"prompt": "同事看我能翻出以前会话的记录,也想在他 mac 上用。我要发给他什么?他那边只装了 claude code 和 codex,另外他 node 好像还是 20。",
|
|
64
|
+
"expected_output": "Gives the two-line install (npm i -g, then `1session skill install`), explains that the second line is what puts the skill into Claude Code and Codex, and flags that Node 20 is below the 22.15 floor so he must upgrade Node first — npx will not work around it either.",
|
|
65
|
+
"files": [],
|
|
66
|
+
"assertions": [
|
|
67
|
+
"Gives `npm i -g @1agents/session-reader` as the install command",
|
|
68
|
+
"Includes `1session skill install` and explains it is what puts the skill into the colleague's agents, not just the CLI",
|
|
69
|
+
"Flags Node 20 as below the >= 22.15 requirement and says it must be upgraded",
|
|
70
|
+
"Does not suggest npx as a way around the old Node version (it runs on the same runtime and fails identically)",
|
|
71
|
+
"Does not invent an install path that does not exist (no curl|sh script, no brew formula, no manual git clone as the primary route)"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": 5,
|
|
76
|
+
"name": "npx-skill-install-trap",
|
|
77
|
+
"prompt": "我不想全局装东西,能不能直接用 npx 把这个 skill 装到我的 claude 和 codex 里就好?",
|
|
78
|
+
"expected_output": "Warns that `skill install` in its default link mode would point at the npx cache directory, which npm garbage-collects — the skill would silently vanish later. Offers `--copy` as the npx-compatible route, or a global install as the robust one.",
|
|
79
|
+
"files": [],
|
|
80
|
+
"assertions": [
|
|
81
|
+
"Warns that a link-mode install via npx points into the npx cache (~/.npm/_npx/...) which npm later prunes",
|
|
82
|
+
"Names the consequence concretely: the skill silently disappears from the agents afterwards",
|
|
83
|
+
"Offers `--copy` as the way to make an npx-based install survive, and/or a global install as the robust alternative",
|
|
84
|
+
"Mentions `--agent claude,codex` or otherwise respects that the user only wants those two",
|
|
85
|
+
"Does not simply tell the user to run `npx ... skill install` with no caveat"
|
|
86
|
+
]
|
|
45
87
|
}
|
|
46
88
|
]
|
|
47
|
-
}
|
|
89
|
+
}
|
|
@@ -26,7 +26,7 @@ session file (size + mtime + head hash) and reparses only changed bytes, so
|
|
|
26
26
|
|
|
27
27
|
```
|
|
28
28
|
1session list [--limit n] [--scope <path>|cwd|global] [--global]
|
|
29
|
-
[--provider claude|codex|antigravity] [--since 24h] [--json]
|
|
29
|
+
[--provider claude|codex|antigravity|grok|dsh] [--since 24h] [--json]
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Most recently updated sessions first. Columns: provider, 8-char id, updated-at,
|