ccprofiles-core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sessions.d.ts +26 -0
- package/dist/sessions.js +182 -2
- package/package.json +5 -1
package/dist/sessions.d.ts
CHANGED
|
@@ -13,6 +13,20 @@ export interface ProjectSessions {
|
|
|
13
13
|
project: string;
|
|
14
14
|
sessions: SessionMeta[];
|
|
15
15
|
}
|
|
16
|
+
export interface TranscriptEntry {
|
|
17
|
+
id: string;
|
|
18
|
+
role: 'user' | 'assistant' | 'tool';
|
|
19
|
+
text: string;
|
|
20
|
+
label: string | null;
|
|
21
|
+
timestamp: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface SessionTranscript {
|
|
24
|
+
id: string;
|
|
25
|
+
agent: 'claude' | 'codex';
|
|
26
|
+
scope: string;
|
|
27
|
+
project: string;
|
|
28
|
+
messages: TranscriptEntry[];
|
|
29
|
+
}
|
|
16
30
|
/** Best-effort decode of Claude Code's project dir name (cwd from a record is preferred). */
|
|
17
31
|
export declare function decodeProjectDir(name: string): string;
|
|
18
32
|
export declare function scanSessions(opts: {
|
|
@@ -23,3 +37,15 @@ export declare function scanSessions(opts: {
|
|
|
23
37
|
agent?: 'claude' | 'codex';
|
|
24
38
|
}[];
|
|
25
39
|
}): Promise<ProjectSessions[]>;
|
|
40
|
+
/** Read visible conversation and tool activity for one known managed session. */
|
|
41
|
+
export declare function readSessionTranscript(opts: {
|
|
42
|
+
sharedRoot: string;
|
|
43
|
+
profiles: {
|
|
44
|
+
name: string;
|
|
45
|
+
dir: string;
|
|
46
|
+
agent?: 'claude' | 'codex';
|
|
47
|
+
}[];
|
|
48
|
+
agent: 'claude' | 'codex';
|
|
49
|
+
scope: string;
|
|
50
|
+
id: string;
|
|
51
|
+
}): Promise<SessionTranscript | null>;
|
package/dist/sessions.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readdir, readFile, stat, lstat } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
const SAFE_SESSION_ID = /^[A-Za-z0-9_-]{1,160}$/;
|
|
5
|
+
const MAX_ENTRY_CHARS = 100_000;
|
|
4
6
|
/** Best-effort decode of Claude Code's project dir name (cwd from a record is preferred). */
|
|
5
7
|
export function decodeProjectDir(name) {
|
|
6
8
|
return name.replace(/^-/, '/').replace(/-/g, '/');
|
|
@@ -88,8 +90,133 @@ function messageText(content) {
|
|
|
88
90
|
return content;
|
|
89
91
|
if (!Array.isArray(content))
|
|
90
92
|
return null;
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
+
const texts = content
|
|
94
|
+
.filter((x) => x?.type === 'input_text' || x?.type === 'output_text' || x?.type === 'text')
|
|
95
|
+
.map((x) => x.text)
|
|
96
|
+
.filter((x) => typeof x === 'string');
|
|
97
|
+
return texts.length ? texts.join('\n\n') : null;
|
|
98
|
+
}
|
|
99
|
+
function limited(text) {
|
|
100
|
+
return text.length <= MAX_ENTRY_CHARS ? text : `${text.slice(0, MAX_ENTRY_CHARS)}\n\n[entry truncated for display]`;
|
|
101
|
+
}
|
|
102
|
+
function toolText(value) {
|
|
103
|
+
if (typeof value === 'string')
|
|
104
|
+
return value;
|
|
105
|
+
try {
|
|
106
|
+
return JSON.stringify(value, null, 2);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return String(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function pushEntry(messages, role, text, label, timestamp) {
|
|
113
|
+
const clean = text?.trim();
|
|
114
|
+
if (!clean)
|
|
115
|
+
return;
|
|
116
|
+
if (role === 'user' && /^<(system-reminder|local-command-caveat|command-name)>/i.test(clean))
|
|
117
|
+
return;
|
|
118
|
+
const previous = messages.at(-1);
|
|
119
|
+
if (previous?.role === role && previous.text === clean)
|
|
120
|
+
return;
|
|
121
|
+
messages.push({
|
|
122
|
+
id: String(messages.length + 1), role, text: limited(clean), label,
|
|
123
|
+
timestamp: typeof timestamp === 'string' ? timestamp : null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function parseClaudeTranscript(file, scope) {
|
|
127
|
+
let raw;
|
|
128
|
+
try {
|
|
129
|
+
raw = await readFile(file, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const id = file.split(/[\\/]/).pop().replace(/\.jsonl$/, '');
|
|
135
|
+
const messages = [];
|
|
136
|
+
let project = '(unknown project)';
|
|
137
|
+
for (const line of raw.split('\n')) {
|
|
138
|
+
if (!line.trim())
|
|
139
|
+
continue;
|
|
140
|
+
let rec;
|
|
141
|
+
try {
|
|
142
|
+
rec = JSON.parse(line);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (project === '(unknown project)' && typeof rec.cwd === 'string')
|
|
148
|
+
project = rec.cwd;
|
|
149
|
+
const content = rec.message?.content;
|
|
150
|
+
if (rec.type === 'user') {
|
|
151
|
+
pushEntry(messages, 'user', messageText(content), null, rec.timestamp);
|
|
152
|
+
if (Array.isArray(content))
|
|
153
|
+
for (const block of content) {
|
|
154
|
+
if (block?.type === 'tool_result')
|
|
155
|
+
pushEntry(messages, 'tool', messageText(block.content) ?? toolText(block.content), 'Tool result', rec.timestamp);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (rec.type === 'assistant') {
|
|
159
|
+
pushEntry(messages, 'assistant', messageText(content), null, rec.timestamp);
|
|
160
|
+
if (Array.isArray(content))
|
|
161
|
+
for (const block of content) {
|
|
162
|
+
if (block?.type === 'tool_use')
|
|
163
|
+
pushEntry(messages, 'tool', toolText(block.input), typeof block.name === 'string' ? block.name : 'Tool call', rec.timestamp);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { id, agent: 'claude', scope, project, messages };
|
|
168
|
+
}
|
|
169
|
+
async function parseCodexTranscript(file, scope) {
|
|
170
|
+
let raw;
|
|
171
|
+
try {
|
|
172
|
+
raw = await readFile(file, 'utf8');
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const records = [];
|
|
178
|
+
for (const line of raw.split('\n')) {
|
|
179
|
+
if (!line.trim())
|
|
180
|
+
continue;
|
|
181
|
+
try {
|
|
182
|
+
records.push(JSON.parse(line));
|
|
183
|
+
}
|
|
184
|
+
catch { /* malformed line */ }
|
|
185
|
+
}
|
|
186
|
+
const filename = file.split(/[\\/]/).pop().replace(/\.jsonl$/, '');
|
|
187
|
+
let id = filename.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0] ?? filename;
|
|
188
|
+
let project = '(unknown project)';
|
|
189
|
+
const hasResponseMessages = records.some(rec => rec?.type === 'response_item' && rec?.payload?.type === 'message');
|
|
190
|
+
const messages = [];
|
|
191
|
+
for (const rec of records) {
|
|
192
|
+
const payload = rec?.payload;
|
|
193
|
+
if (rec?.type === 'session_meta') {
|
|
194
|
+
if (typeof payload?.id === 'string')
|
|
195
|
+
id = payload.id;
|
|
196
|
+
else if (typeof payload?.session_id === 'string')
|
|
197
|
+
id = payload.session_id;
|
|
198
|
+
if (typeof payload?.cwd === 'string')
|
|
199
|
+
project = payload.cwd;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (project === '(unknown project)' && rec?.type === 'turn_context' && typeof payload?.cwd === 'string')
|
|
203
|
+
project = payload.cwd;
|
|
204
|
+
if (rec?.type === 'response_item') {
|
|
205
|
+
if (payload?.type === 'message' && (payload.role === 'user' || payload.role === 'assistant'))
|
|
206
|
+
pushEntry(messages, payload.role, messageText(payload.content), null, rec.timestamp);
|
|
207
|
+
else if (payload?.type === 'function_call' || payload?.type === 'custom_tool_call' || payload?.type === 'local_shell_call')
|
|
208
|
+
pushEntry(messages, 'tool', toolText(payload.arguments ?? payload.input ?? payload), payload.name ?? payload.type, rec.timestamp);
|
|
209
|
+
else if (payload?.type === 'function_call_output' || payload?.type === 'custom_tool_call_output')
|
|
210
|
+
pushEntry(messages, 'tool', toolText(payload.output), 'Tool result', rec.timestamp);
|
|
211
|
+
}
|
|
212
|
+
else if (!hasResponseMessages && rec?.type === 'event_msg') {
|
|
213
|
+
if (payload?.type === 'user_message')
|
|
214
|
+
pushEntry(messages, 'user', payload.message, null, rec.timestamp);
|
|
215
|
+
else if (payload?.type === 'agent_message')
|
|
216
|
+
pushEntry(messages, 'assistant', payload.message, null, rec.timestamp);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { id, agent: 'codex', scope, project, messages };
|
|
93
220
|
}
|
|
94
221
|
async function parseCodexSession(file) {
|
|
95
222
|
let raw;
|
|
@@ -164,6 +291,26 @@ async function codexSessionFiles(dir) {
|
|
|
164
291
|
}
|
|
165
292
|
return out;
|
|
166
293
|
}
|
|
294
|
+
async function claudeSessionFile(projectsDir, id) {
|
|
295
|
+
let projects;
|
|
296
|
+
try {
|
|
297
|
+
projects = await readdir(projectsDir, { withFileTypes: true });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
for (const project of projects) {
|
|
303
|
+
if (!project.isDirectory())
|
|
304
|
+
continue;
|
|
305
|
+
const file = join(projectsDir, project.name, `${id}.jsonl`);
|
|
306
|
+
try {
|
|
307
|
+
if ((await stat(file)).isFile())
|
|
308
|
+
return file;
|
|
309
|
+
}
|
|
310
|
+
catch { /* keep looking */ }
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
167
314
|
async function scanCodexSessionsDir(sessionsDir, scope) {
|
|
168
315
|
const byProject = new Map();
|
|
169
316
|
for (const file of await codexSessionFiles(sessionsDir)) {
|
|
@@ -200,3 +347,36 @@ export async function scanSessions(opts) {
|
|
|
200
347
|
}
|
|
201
348
|
return out;
|
|
202
349
|
}
|
|
350
|
+
/** Read visible conversation and tool activity for one known managed session. */
|
|
351
|
+
export async function readSessionTranscript(opts) {
|
|
352
|
+
if (!SAFE_SESSION_ID.test(opts.id))
|
|
353
|
+
return null;
|
|
354
|
+
let root;
|
|
355
|
+
if (opts.scope === 'shared')
|
|
356
|
+
root = join(opts.sharedRoot, opts.agent === 'codex' ? 'sessions' : 'projects');
|
|
357
|
+
else {
|
|
358
|
+
const profile = opts.profiles.find(p => p.name === opts.scope && (p.agent ?? 'claude') === opts.agent);
|
|
359
|
+
if (!profile)
|
|
360
|
+
return null;
|
|
361
|
+
root = join(profile.dir, opts.agent === 'codex' ? 'sessions' : 'projects');
|
|
362
|
+
try {
|
|
363
|
+
if ((await lstat(root)).isSymbolicLink())
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (opts.agent === 'claude') {
|
|
371
|
+
const file = await claudeSessionFile(root, opts.id);
|
|
372
|
+
return file ? parseClaudeTranscript(file, opts.scope) : null;
|
|
373
|
+
}
|
|
374
|
+
const files = await codexSessionFiles(root);
|
|
375
|
+
files.sort((a, b) => Number(!a.includes(opts.id)) - Number(!b.includes(opts.id)));
|
|
376
|
+
for (const file of files) {
|
|
377
|
+
const transcript = await parseCodexTranscript(file, opts.scope);
|
|
378
|
+
if (transcript?.id === opts.id)
|
|
379
|
+
return transcript;
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccprofiles-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Core library for ccprofiles — discovery, manifest, secrets, apply, and LAN sync for Claude Code multi-account setups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"dist"
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
+
"funding": {
|
|
13
|
+
"type": "individual",
|
|
14
|
+
"url": "https://ko-fi.com/teukufadh"
|
|
15
|
+
},
|
|
12
16
|
"repository": {
|
|
13
17
|
"type": "git",
|
|
14
18
|
"url": "git+https://github.com/tmfadhlul/claude-code-profile.git",
|