@1agents/session-reader 0.5.1 → 0.6.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/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
- * All three agents load `<dir>/<name>/SKILL.md` with the same YAML frontmatter,
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
  /**
@@ -9,9 +9,9 @@
9
9
  /** DDL layout. A bump drops and rebuilds the whole database. */
10
10
  export declare const SCHEMA_VERSION = 1;
11
11
  /** L1 semantics — anything in `parsers/` that changes normalized events. */
12
- export declare const PARSER_VERSION = 2;
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 = 1;
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";
@@ -9,9 +9,9 @@
9
9
  /** DDL layout. A bump drops and rebuilds the whole database. */
10
10
  export const SCHEMA_VERSION = 1;
11
11
  /** L1 semantics — anything in `parsers/` that changes normalized events. */
12
- export const PARSER_VERSION = 2;
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 = 1;
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 = `
@@ -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;
@@ -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
+ }
@@ -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.5.1",
3
+ "version": "0.6.1",
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.5"
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.2.0"
63
+ "@1agents/dreammate-node": "^0.3.0"
62
64
  }
63
65
  }
@@ -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 Antigravity 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 那边做到哪了", "跨项目找一下", "整理一下最近几天的会话/写个周报". 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.
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.
4
4
  ---
5
5
 
6
6
  # 1session — the cross-agent Read Plane
7
7
 
8
- Three agents write sessions to this machine in three different formats. `1session`
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,6 +13,8 @@ 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.
@@ -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,