@1agents/session-reader 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +347 -0
  2. package/dist/bin/1session.d.ts +2 -0
  3. package/dist/bin/1session.js +408 -0
  4. package/dist/src/aggregator.d.ts +12 -0
  5. package/dist/src/aggregator.js +136 -0
  6. package/dist/src/classify.d.ts +2 -0
  7. package/dist/src/classify.js +11 -0
  8. package/dist/src/distiller.d.ts +7 -0
  9. package/dist/src/distiller.js +122 -0
  10. package/dist/src/index.d.ts +19 -0
  11. package/dist/src/index.js +17 -0
  12. package/dist/src/ledger.d.ts +19 -0
  13. package/dist/src/ledger.js +192 -0
  14. package/dist/src/overview.d.ts +6 -0
  15. package/dist/src/overview.js +324 -0
  16. package/dist/src/parsers/antigravity.d.ts +4 -0
  17. package/dist/src/parsers/antigravity.js +297 -0
  18. package/dist/src/parsers/claude.d.ts +2 -0
  19. package/dist/src/parsers/claude.js +214 -0
  20. package/dist/src/parsers/codex.d.ts +2 -0
  21. package/dist/src/parsers/codex.js +316 -0
  22. package/dist/src/parsers/provider.d.ts +24 -0
  23. package/dist/src/parsers/provider.js +5 -0
  24. package/dist/src/resolver.d.ts +42 -0
  25. package/dist/src/resolver.js +145 -0
  26. package/dist/src/search.d.ts +59 -0
  27. package/dist/src/search.js +257 -0
  28. package/dist/src/store/db.d.ts +10 -0
  29. package/dist/src/store/db.js +89 -0
  30. package/dist/src/store/edges.d.ts +46 -0
  31. package/dist/src/store/edges.js +153 -0
  32. package/dist/src/store/facts.d.ts +8 -0
  33. package/dist/src/store/facts.js +39 -0
  34. package/dist/src/store/indexer.d.ts +40 -0
  35. package/dist/src/store/indexer.js +62 -0
  36. package/dist/src/store/read.d.ts +29 -0
  37. package/dist/src/store/read.js +70 -0
  38. package/dist/src/store/rows.d.ts +35 -0
  39. package/dist/src/store/rows.js +55 -0
  40. package/dist/src/store/schema.d.ts +19 -0
  41. package/dist/src/store/schema.js +142 -0
  42. package/dist/src/store/write.d.ts +22 -0
  43. package/dist/src/store/write.js +72 -0
  44. package/dist/src/turns.d.ts +14 -0
  45. package/dist/src/turns.js +156 -0
  46. package/dist/src/types.d.ts +294 -0
  47. package/dist/src/types.js +13 -0
  48. package/dist/src/util/jsonl.d.ts +6 -0
  49. package/dist/src/util/jsonl.js +32 -0
  50. package/dist/src/util/paths.d.ts +15 -0
  51. package/dist/src/util/paths.js +69 -0
  52. package/dist/src/util/text.d.ts +7 -0
  53. package/dist/src/util/text.js +48 -0
  54. package/dist/src/writes.d.ts +35 -0
  55. package/dist/src/writes.js +204 -0
  56. package/package.json +58 -0
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ export function expandHome(p) {
6
+ if (p === '~')
7
+ return os.homedir();
8
+ if (p.startsWith('~/'))
9
+ return path.join(os.homedir(), p.slice(2));
10
+ return p;
11
+ }
12
+ /**
13
+ * Normalizes anything a session file may contain into one comparable absolute
14
+ * path: `~`, relative paths, `file://` URIs, percent-encoding and symlinks.
15
+ */
16
+ export function canonicalizePath(input) {
17
+ let p = (input ?? '').trim();
18
+ if (!p)
19
+ return '';
20
+ if (p.startsWith('file://')) {
21
+ try {
22
+ p = fileURLToPath(p);
23
+ }
24
+ catch {
25
+ p = decodeURIComponent(p.slice('file://'.length));
26
+ }
27
+ }
28
+ else if (/%[0-9A-Fa-f]{2}/.test(p)) {
29
+ try {
30
+ p = decodeURIComponent(p);
31
+ }
32
+ catch {
33
+ /* keep as-is */
34
+ }
35
+ }
36
+ p = path.resolve(expandHome(p));
37
+ try {
38
+ p = fs.realpathSync.native(p);
39
+ }
40
+ catch {
41
+ /* path may not exist any more; the lexical form is still comparable */
42
+ }
43
+ return p.length > 1 ? p.replace(/\/+$/, '') : p;
44
+ }
45
+ /** True when `child` is `parent` itself or lives under it. */
46
+ export function isInside(parent, child) {
47
+ if (!parent || !child)
48
+ return false;
49
+ if (parent === child)
50
+ return true;
51
+ return child.startsWith(parent.endsWith('/') ? parent : `${parent}/`);
52
+ }
53
+ /**
54
+ * Claude Code names its project directories by replacing every non-alphanumeric
55
+ * character of the cwd with `-` (lossy, so we only ever slugify forwards).
56
+ */
57
+ export function slugifyWorkspace(p) {
58
+ return p.replace(/[^a-zA-Z0-9]/g, '-');
59
+ }
60
+ /** Walks up from a file to the closest directory holding `.git`. */
61
+ export function findRepoRoot(from) {
62
+ let dir = path.dirname(canonicalizePath(from));
63
+ for (let i = 0; i < 40 && dir && dir !== '/'; i++) {
64
+ if (fs.existsSync(path.join(dir, '.git')))
65
+ return dir;
66
+ dir = path.dirname(dir);
67
+ }
68
+ return undefined;
69
+ }
@@ -0,0 +1,7 @@
1
+ /** Collapses whitespace and clips to `max` characters. */
2
+ export declare function oneLine(text: string | undefined, max?: number): string;
3
+ export declare function clip(text: string | undefined, max: number): string;
4
+ /** Strips the wrapper tags and ambient blocks agents inject around a real request. */
5
+ export declare function stripPromptEnvelope(text: string): string;
6
+ /** Boilerplate the agents prepend to the first turn — never a real request. */
7
+ export declare function looksLikeInstructions(text: string): boolean;
@@ -0,0 +1,48 @@
1
+ /** Collapses whitespace and clips to `max` characters. */
2
+ export function oneLine(text, max = 160) {
3
+ const flat = (text ?? '').replace(/\s+/g, ' ').trim();
4
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
5
+ }
6
+ export function clip(text, max) {
7
+ const value = (text ?? '').trim();
8
+ return value.length > max ? `${value.slice(0, max - 1)}…` : value;
9
+ }
10
+ /** Strips the wrapper tags and ambient blocks agents inject around a real request. */
11
+ export function stripPromptEnvelope(text) {
12
+ let out = text;
13
+ const request = out.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
14
+ if (request?.[1])
15
+ out = request[1];
16
+ // Codex answers to its own clarifying questions carry the reply in `answer`.
17
+ if (out.includes('<send_user_message_question_reply>')) {
18
+ const answers = [...out.matchAll(/"answer"\s*:\s*"((?:[^"\\]|\\.)*)"/g)].map((m) => (m[1] ?? '').replace(/\\"/g, '"').replace(/\\n/g, ' '));
19
+ if (answers.length)
20
+ return answers.join('\n').trim();
21
+ }
22
+ out = out
23
+ .replace(/<in-app-browser-context[\s\S]*?<\/in-app-browser-context>/g, '')
24
+ .replace(/<image\s[^>]*>[\s\S]*?<\/image>/g, '');
25
+ // Attachment/ambient preambles keep the actual prompt behind a `## My request:` heading.
26
+ const marker = out.lastIndexOf('## My request:');
27
+ if (marker !== -1)
28
+ out = out.slice(marker + '## My request:'.length);
29
+ out = out
30
+ .replace(/<ADDITIONAL_METADATA>[\s\S]*?<\/ADDITIONAL_METADATA>/g, '')
31
+ .replace(/<USER_SETTINGS_CHANGE>[\s\S]*?<\/USER_SETTINGS_CHANGE>/g, '')
32
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
33
+ .replace(/<environment_context>[\s\S]*?<\/environment_context>/g, '')
34
+ .replace(/<user_instructions>[\s\S]*?<\/user_instructions>/g, '')
35
+ .replace(/<INSTRUCTIONS>[\s\S]*?<\/INSTRUCTIONS>/g, '');
36
+ return out.trim();
37
+ }
38
+ /** Boilerplate the agents prepend to the first turn — never a real request. */
39
+ export function looksLikeInstructions(text) {
40
+ const head = text.slice(0, 400);
41
+ return (/^#\s*AGENTS\.md/i.test(head) ||
42
+ /<INSTRUCTIONS>/.test(head) ||
43
+ /<user_instructions>/.test(head) ||
44
+ /<environment_context>/.test(head) ||
45
+ /^<permissions instructions>/.test(head) ||
46
+ /^<recommended_plugins>/.test(head) ||
47
+ /^Caveat: The messages below were generated/.test(head));
48
+ }
@@ -0,0 +1,35 @@
1
+ import type { Provenance, TurnEvent } from './types.js';
2
+ export interface FileWrite {
3
+ path: string;
4
+ /** `observed` from a dedicated edit tool, `derived` from a shell command. */
5
+ provenance: Provenance;
6
+ /** The rule that produced it: `tool:Write`, `shell:redirect`, … */
7
+ extractor: string;
8
+ /** Set when the write happened inside `ssh user@host '…'`. */
9
+ host?: string;
10
+ event?: number;
11
+ }
12
+ /**
13
+ * Drops here-document bodies before scanning. Their content is data being
14
+ * written, not commands — and when the payload is source code, its arrow
15
+ * functions and string literals otherwise masquerade as redirects and hosts.
16
+ */
17
+ export declare function stripHeredocs(command: string): string;
18
+ /** The untruncated shell command a tool call carries, if it is a shell tool. */
19
+ export declare function rawCommand(turn: TurnEvent): string | undefined;
20
+ /**
21
+ * The command as it should be analysed: here-doc payloads removed, so that
22
+ * data being written never gets mistaken for commands being run.
23
+ */
24
+ export declare function analyzableCommand(turn: TurnEvent): string | undefined;
25
+ /**
26
+ * Files a single tool call wrote. Explicit writes come from dedicated edit
27
+ * tools; inferred ones are parsed out of shell commands (heuristic — agents
28
+ * that write through the shell would otherwise leave no trace at all).
29
+ */
30
+ export declare function fileWrites(turn: TurnEvent): FileWrite[];
31
+ /**
32
+ * Stable identity for a write: `host:path` when remote, otherwise an absolute
33
+ * local path (relative ones resolved against the session's workspace).
34
+ */
35
+ export declare function resolveWritePath(write: FileWrite, workspace?: string): string;
@@ -0,0 +1,204 @@
1
+ import path from 'node:path';
2
+ import { canonicalizePath } from './util/paths.js';
3
+ const EDIT_TOOLS = new Set([
4
+ 'write',
5
+ 'edit',
6
+ 'multiedit',
7
+ 'notebookedit',
8
+ 'write_to_file',
9
+ 'replace_file_content',
10
+ 'edit_file',
11
+ 'create_file',
12
+ 'apply_patch',
13
+ 'str_replace_editor',
14
+ ]);
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
+ const COMMAND_ARGS = ['command', 'CommandLine', 'cmd', 'input'];
18
+ const PATCH_FILE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
19
+ /** Extensions we accept on a bare token. Without this, `a.name` looks like a file. */
20
+ const KNOWN_EXT = /\.(?:ts|tsx|js|jsx|mjs|cjs|json|jsonl|md|markdown|txt|csv|tsv|ya?ml|toml|ini|conf|cfg|sh|bash|zsh|fish|py|rb|go|rs|java|c|h|cpp|hpp|sql|html|css|scss|sql|log|png|jpe?g|gif|webp|svg|pdf|zip|tar|gz|safetensors|pt|pth|bin|lock|env|gitignore|dockerfile|patch|diff)$/i;
21
+ /**
22
+ * Drops here-document bodies before scanning. Their content is data being
23
+ * written, not commands — and when the payload is source code, its arrow
24
+ * functions and string literals otherwise masquerade as redirects and hosts.
25
+ */
26
+ export function stripHeredocs(command) {
27
+ return command.replace(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm, (match) => match.slice(0, match.indexOf('\n') + 1));
28
+ }
29
+ /** `user@host` in an ssh/scp invocation — the bare `-o opt` forms carry no host. */
30
+ const SSH_HOST = /\b(?:ssh|scp|rsync)\b[^\n;|]*?\b[\w.-]+@([\w.-]+)/;
31
+ /** Shell constructs that create or overwrite a file, in order of specificity. */
32
+ const SHELL_PATTERNS = [
33
+ { via: 'tee', re: /\btee\s+(?:-a\s+)?(?:'([^']+)'|"([^"]+)"|([^\s'";|&)]+))/g },
34
+ { via: 'sed-i', re: /\bsed\b[^\n;|]*?\s-i(?:\.\w+)?\s[^\n;|]*?\s(?:'([^']+)'|"([^"]+)"|([^\s'";|&)]+))\s*(?=$|[;|&\n'"])/g },
35
+ { via: 'write-text', re: /(?:'([^']+)'|"([^"]+)")\s*\)?\s*\.write_text\(/g },
36
+ { via: 'open-w', re: /\bopen\(\s*(?:'([^']+)'|"([^"]+)")\s*,\s*['"][wa]/g },
37
+ // `=>` and `>=` are code, not redirection.
38
+ { via: 'redirect', re: /(?<![0-9&=<>])>>?\s*(?:'([^']+)'|"([^"]+)"|([^\s'";|&<>)]+))/g },
39
+ ];
40
+ function firstGroup(match) {
41
+ return match[1] ?? match[2] ?? match[3];
42
+ }
43
+ /** Resolves only what is already absolute; relative paths stay verbatim. */
44
+ function absolutize(candidate) {
45
+ const value = candidate.trim();
46
+ return path.isAbsolute(value) || value.startsWith('~') || value.startsWith('file://')
47
+ ? canonicalizePath(value)
48
+ : value;
49
+ }
50
+ /** Rejects device files, globs, flags, URLs and file descriptors. */
51
+ function looksWritable(candidate) {
52
+ if (!candidate || candidate.length < 2)
53
+ return false;
54
+ if (candidate.startsWith('-') || candidate.startsWith('$'))
55
+ return false;
56
+ if (/^\/dev\//.test(candidate) || candidate === '/dev/null')
57
+ return false;
58
+ if (/[*?]/.test(candidate))
59
+ return false;
60
+ if (/^\d+$/.test(candidate) || /^&/.test(candidate))
61
+ return false;
62
+ if (/^[a-z]+:\/\//i.test(candidate))
63
+ return false;
64
+ // Shell commands are often embedded in JSON/JS blobs; anything carrying
65
+ // structural punctuation is a fragment of the wrapper, not a real path.
66
+ if (/["`{}()<>,;|&$\n]/.test(candidate))
67
+ return false;
68
+ if (/\[|\]/.test(candidate))
69
+ return false;
70
+ // A bare token must carry a real file extension; `r.source` is a property.
71
+ return candidate.includes('/') || KNOWN_EXT.test(candidate);
72
+ }
73
+ /**
74
+ * Target of `cp`/`mv`/`install`/`scp`/`rsync` — always the last operand.
75
+ * `scp remote:src local_dst` writes locally, so transfer tools only ever take
76
+ * the host spelled on the target itself, never the one elsewhere in the line.
77
+ */
78
+ function copyTargets(command) {
79
+ const out = [];
80
+ for (const match of command.matchAll(/\b(cp|mv|install|scp|rsync)\b([^\n;|&]*)/g)) {
81
+ const via = match[1];
82
+ const operands = (match[2] ?? '')
83
+ .split(/\s+/)
84
+ .map((token) => token.replace(/^['"]+|['"]+$/g, ''))
85
+ .filter((token) => token && !token.startsWith('-'));
86
+ const target = operands.at(-1);
87
+ if (!target || operands.length < 2)
88
+ continue;
89
+ const transfer = via === 'scp' || via === 'rsync';
90
+ const remote = /^([\w.-]+)@([\w.-]+):(.+)$/.exec(target);
91
+ if (remote)
92
+ out.push({ path: remote[3], via, host: remote[2], hostFromTargetOnly: transfer });
93
+ else if (looksWritable(target))
94
+ out.push({ path: target, via, hostFromTargetOnly: transfer });
95
+ }
96
+ return out;
97
+ }
98
+ /** The untruncated shell command a tool call carries, if it is a shell tool. */
99
+ export function rawCommand(turn) {
100
+ if (turn.kind !== 'tool_call' || !SHELL_TOOLS.has((turn.toolName ?? '').toLowerCase()))
101
+ return undefined;
102
+ const args = turn.toolArgs ?? {};
103
+ for (const key of COMMAND_ARGS) {
104
+ const value = args[key];
105
+ if (typeof value === 'string' && value.trim())
106
+ return value;
107
+ if (Array.isArray(value) && value.length)
108
+ return value.join(' ');
109
+ }
110
+ return undefined;
111
+ }
112
+ /**
113
+ * The command as it should be analysed: here-doc payloads removed, so that
114
+ * data being written never gets mistaken for commands being run.
115
+ */
116
+ export function analyzableCommand(turn) {
117
+ const raw = rawCommand(turn);
118
+ return raw ? stripHeredocs(raw) : undefined;
119
+ }
120
+ /**
121
+ * Files a single tool call wrote. Explicit writes come from dedicated edit
122
+ * tools; inferred ones are parsed out of shell commands (heuristic — agents
123
+ * that write through the shell would otherwise leave no trace at all).
124
+ */
125
+ export function fileWrites(turn) {
126
+ if (turn.kind !== 'tool_call')
127
+ return [];
128
+ const found = [];
129
+ const toolName = (turn.toolName ?? '').toLowerCase();
130
+ const args = turn.toolArgs ?? {};
131
+ if (EDIT_TOOLS.has(toolName)) {
132
+ for (const key of FILE_ARGS) {
133
+ const value = args[key];
134
+ if (typeof value === 'string' && value.trim()) {
135
+ found.push({
136
+ path: absolutize(value),
137
+ provenance: 'observed',
138
+ extractor: `tool:${turn.toolName}`,
139
+ event: turn.index,
140
+ });
141
+ }
142
+ }
143
+ const patch = typeof args.input === 'string' ? args.input : '';
144
+ for (const match of patch.matchAll(PATCH_FILE)) {
145
+ if (match[1]) {
146
+ found.push({
147
+ path: absolutize(match[1].trim()),
148
+ provenance: 'observed',
149
+ extractor: 'patch:apply_patch',
150
+ event: turn.index,
151
+ });
152
+ }
153
+ }
154
+ }
155
+ const raw = rawCommand(turn);
156
+ if (raw) {
157
+ // Everything below reads the command, never the here-doc payload.
158
+ const command = stripHeredocs(raw);
159
+ const host = SSH_HOST.exec(command)?.[1];
160
+ const add = (candidate, via, viaHost) => {
161
+ if (!looksWritable(candidate))
162
+ return;
163
+ // Remote paths must not be resolved against the local filesystem, and
164
+ // relative ones belong to the session's workspace — not to our cwd.
165
+ found.push({
166
+ path: viaHost ? candidate : absolutize(candidate),
167
+ // A shell write is a real action we saw run, just not a typed field.
168
+ provenance: 'derived',
169
+ extractor: `shell:${via}`,
170
+ event: turn.index,
171
+ ...(viaHost ? { host: viaHost } : {}),
172
+ });
173
+ };
174
+ for (const { via, re } of SHELL_PATTERNS) {
175
+ for (const match of command.matchAll(re)) {
176
+ const candidate = firstGroup(match);
177
+ if (candidate)
178
+ add(candidate.trim(), via, host);
179
+ }
180
+ }
181
+ for (const target of copyTargets(command)) {
182
+ add(target.path, target.via, target.hostFromTargetOnly ? target.host : (target.host ?? host));
183
+ }
184
+ }
185
+ const seen = new Set();
186
+ return found.filter((write) => {
187
+ const key = `${write.host ?? ''}|${write.path}`;
188
+ if (seen.has(key))
189
+ return false;
190
+ seen.add(key);
191
+ return true;
192
+ });
193
+ }
194
+ /**
195
+ * Stable identity for a write: `host:path` when remote, otherwise an absolute
196
+ * local path (relative ones resolved against the session's workspace).
197
+ */
198
+ export function resolveWritePath(write, workspace) {
199
+ if (write.host)
200
+ return `${write.host}:${write.path}`;
201
+ if (path.isAbsolute(write.path))
202
+ return write.path;
203
+ return workspace ? path.resolve(workspace, write.path) : write.path;
204
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@1agents/session-reader",
3
+ "version": "0.1.1",
4
+ "description": "Read Plane: cross-agent session discovery, turn inspection, workspace aggregation and distillation from raw local session files.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "codex",
8
+ "antigravity",
9
+ "session",
10
+ "transcript",
11
+ "jsonl",
12
+ "cli"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/scottzx/session-reader.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/scottzx/session-reader/issues"
21
+ },
22
+ "homepage": "https://github.com/scottzx/session-reader#readme",
23
+ "type": "module",
24
+ "main": "./dist/src/index.js",
25
+ "types": "./dist/src/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/src/index.d.ts",
29
+ "import": "./dist/src/index.js"
30
+ }
31
+ },
32
+ "bin": {
33
+ "1session": "./dist/bin/1session.js"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.build.json",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit",
46
+ "test": "node --import tsx --test test/session-reader.test.ts",
47
+ "prepack": "npm run build",
48
+ "1session": "tsx bin/1session.ts"
49
+ },
50
+ "engines": {
51
+ "node": ">=22.5"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.10.2",
55
+ "tsx": "^4.19.2",
56
+ "typescript": "^5.7.2"
57
+ }
58
+ }