@1agents/session-reader 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +347 -0
- package/dist/bin/1session.d.ts +2 -0
- package/dist/bin/1session.js +408 -0
- package/dist/src/aggregator.d.ts +12 -0
- package/dist/src/aggregator.js +136 -0
- package/dist/src/classify.d.ts +2 -0
- package/dist/src/classify.js +11 -0
- package/dist/src/distiller.d.ts +7 -0
- package/dist/src/distiller.js +122 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +17 -0
- package/dist/src/ledger.d.ts +19 -0
- package/dist/src/ledger.js +192 -0
- package/dist/src/overview.d.ts +6 -0
- package/dist/src/overview.js +324 -0
- package/dist/src/parsers/antigravity.d.ts +4 -0
- package/dist/src/parsers/antigravity.js +297 -0
- package/dist/src/parsers/claude.d.ts +2 -0
- package/dist/src/parsers/claude.js +214 -0
- package/dist/src/parsers/codex.d.ts +2 -0
- package/dist/src/parsers/codex.js +316 -0
- package/dist/src/parsers/provider.d.ts +24 -0
- package/dist/src/parsers/provider.js +5 -0
- package/dist/src/resolver.d.ts +42 -0
- package/dist/src/resolver.js +145 -0
- package/dist/src/search.d.ts +59 -0
- package/dist/src/search.js +257 -0
- package/dist/src/store/db.d.ts +10 -0
- package/dist/src/store/db.js +89 -0
- package/dist/src/store/edges.d.ts +46 -0
- package/dist/src/store/edges.js +153 -0
- package/dist/src/store/facts.d.ts +8 -0
- package/dist/src/store/facts.js +39 -0
- package/dist/src/store/indexer.d.ts +40 -0
- package/dist/src/store/indexer.js +62 -0
- package/dist/src/store/read.d.ts +29 -0
- package/dist/src/store/read.js +70 -0
- package/dist/src/store/rows.d.ts +35 -0
- package/dist/src/store/rows.js +55 -0
- package/dist/src/store/schema.d.ts +19 -0
- package/dist/src/store/schema.js +142 -0
- package/dist/src/store/write.d.ts +22 -0
- package/dist/src/store/write.js +72 -0
- package/dist/src/turns.d.ts +14 -0
- package/dist/src/turns.js +156 -0
- package/dist/src/types.d.ts +294 -0
- package/dist/src/types.js +13 -0
- package/dist/src/util/jsonl.d.ts +6 -0
- package/dist/src/util/jsonl.js +32 -0
- package/dist/src/util/paths.d.ts +15 -0
- package/dist/src/util/paths.js +69 -0
- package/dist/src/util/text.d.ts +7 -0
- package/dist/src/util/text.js +48 -0
- package/dist/src/writes.d.ts +35 -0
- package/dist/src/writes.js +204 -0
- package/package.json +58 -0
|
@@ -0,0 +1,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
|
+
}
|