ccprofiles-core 0.5.1 → 0.7.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/README.md +15 -0
- package/dist/adopt.d.ts +1 -1
- package/dist/adopt.js +24 -3
- package/dist/apply.d.ts +4 -1
- package/dist/apply.js +43 -13
- package/dist/assets.d.ts +10 -2
- package/dist/assets.js +15 -5
- package/dist/codex.d.ts +4 -1
- package/dist/codex.js +10 -3
- package/dist/devices.js +1 -1
- package/dist/discovery.d.ts +4 -0
- package/dist/discovery.js +19 -0
- package/dist/fsutil.d.ts +3 -1
- package/dist/fsutil.js +14 -6
- package/dist/handoff.d.ts +21 -0
- package/dist/handoff.js +38 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.js +74 -5
- package/dist/platform.js +8 -1
- package/dist/plugins.d.ts +21 -0
- package/dist/plugins.js +56 -0
- package/dist/provider.d.ts +10 -0
- package/dist/provider.js +30 -0
- package/dist/rcblock.js +2 -0
- package/dist/secrets.js +5 -5
- package/dist/sessions.d.ts +35 -0
- package/dist/sessions.js +291 -67
- package/package.json +12 -2
package/dist/sessions.js
CHANGED
|
@@ -1,11 +1,54 @@
|
|
|
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;
|
|
6
|
+
const claudeSessionCache = new Map();
|
|
7
|
+
const codexSessionCache = new Map();
|
|
8
|
+
/** Per-file cap: a transcript above this size is summarized without reading its content. */
|
|
9
|
+
const MAX_FILE_SCAN_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
/** Per-request cap: once this many bytes have been read from disk in one scanSessions() call,
|
|
11
|
+
* remaining (uncached) files are summarized without reading their content either. */
|
|
12
|
+
const MAX_TOTAL_SCAN_BYTES = 50 * 1024 * 1024;
|
|
13
|
+
let scanCacheHits = 0;
|
|
14
|
+
let scanCacheMisses = 0;
|
|
15
|
+
/** Test/observability hook: cumulative cache hit/miss counts across all scanSessions() calls
|
|
16
|
+
* in this process. Not reset automatically — callers that need deltas should diff two reads. */
|
|
17
|
+
export function sessionScanCacheStats() {
|
|
18
|
+
return { hits: scanCacheHits, misses: scanCacheMisses };
|
|
19
|
+
}
|
|
4
20
|
/** Best-effort decode of Claude Code's project dir name (cwd from a record is preferred). */
|
|
5
21
|
export function decodeProjectDir(name) {
|
|
6
22
|
return name.replace(/^-/, '/').replace(/-/g, '/');
|
|
7
23
|
}
|
|
8
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Shared mtime+size cache lookup for the two session-file formats. `parseFull` runs only on a
|
|
26
|
+
* cache miss with a fresh read; `makeTruncated` builds a summary-only result when the file is
|
|
27
|
+
* over the per-file cap or the per-request byte budget is exhausted. A size-cap truncation is
|
|
28
|
+
* deterministic (depends only on the file) and gets cached; a budget-cap truncation depends on
|
|
29
|
+
* request ordering, so it is *not* cached — a later request with a fresh budget may read it fully.
|
|
30
|
+
*/
|
|
31
|
+
async function cachedParse(file, cache, budget, parseFull, makeTruncated) {
|
|
32
|
+
let st;
|
|
33
|
+
try {
|
|
34
|
+
st = await stat(file);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const cached = cache.get(file);
|
|
40
|
+
if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
|
|
41
|
+
scanCacheHits++;
|
|
42
|
+
return cached.result;
|
|
43
|
+
}
|
|
44
|
+
scanCacheMisses++;
|
|
45
|
+
if (st.size > MAX_FILE_SCAN_BYTES) {
|
|
46
|
+
const result = makeTruncated(st);
|
|
47
|
+
cache.set(file, { mtimeMs: st.mtimeMs, size: st.size, result });
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
if (st.size > budget.bytesLeft)
|
|
51
|
+
return makeTruncated(st); // not cached — budget is per-request
|
|
9
52
|
let raw;
|
|
10
53
|
try {
|
|
11
54
|
raw = await readFile(file, 'utf8');
|
|
@@ -13,36 +56,43 @@ async function parseClaudeSession(file) {
|
|
|
13
56
|
catch {
|
|
14
57
|
return null;
|
|
15
58
|
}
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
rec = JSON.parse(line);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (cwd === null && typeof rec.cwd === 'string')
|
|
28
|
-
cwd = rec.cwd;
|
|
29
|
-
if (gitBranch === null && typeof rec.gitBranch === 'string')
|
|
30
|
-
gitBranch = rec.gitBranch;
|
|
31
|
-
if (firstPrompt === null && rec.type === 'user') {
|
|
32
|
-
const c = rec.message?.content;
|
|
33
|
-
const text = typeof c === 'string' ? c : Array.isArray(c) ? c.find((x) => x?.type === 'text')?.text : null;
|
|
34
|
-
if (typeof text === 'string' && text.trim() && !text.trimStart().startsWith('<'))
|
|
35
|
-
firstPrompt = text.trim().slice(0, 200);
|
|
36
|
-
}
|
|
37
|
-
if (model === null && typeof rec.message?.model === 'string')
|
|
38
|
-
model = rec.message.model;
|
|
39
|
-
if (firstPrompt && model && gitBranch && cwd)
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
59
|
+
budget.bytesLeft -= st.size;
|
|
60
|
+
const result = parseFull(raw, st);
|
|
61
|
+
cache.set(file, { mtimeMs: st.mtimeMs, size: st.size, result });
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
async function parseClaudeSession(file, budget) {
|
|
42
65
|
const id = file.split(/[\\/]/).pop().replace(/\.jsonl$/, '');
|
|
43
|
-
return
|
|
66
|
+
return cachedParse(file, claudeSessionCache, budget, (raw, st) => {
|
|
67
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
68
|
+
let firstPrompt = null, gitBranch = null, model = null, cwd = null;
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
let rec;
|
|
71
|
+
try {
|
|
72
|
+
rec = JSON.parse(line);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (cwd === null && typeof rec.cwd === 'string')
|
|
78
|
+
cwd = rec.cwd;
|
|
79
|
+
if (gitBranch === null && typeof rec.gitBranch === 'string')
|
|
80
|
+
gitBranch = rec.gitBranch;
|
|
81
|
+
if (firstPrompt === null && rec.type === 'user') {
|
|
82
|
+
const c = rec.message?.content;
|
|
83
|
+
const text = typeof c === 'string' ? c : Array.isArray(c) ? c.find((x) => x?.type === 'text')?.text : null;
|
|
84
|
+
if (typeof text === 'string' && text.trim() && !text.trimStart().startsWith('<'))
|
|
85
|
+
firstPrompt = text.trim().slice(0, 200);
|
|
86
|
+
}
|
|
87
|
+
if (model === null && typeof rec.message?.model === 'string')
|
|
88
|
+
model = rec.message.model;
|
|
89
|
+
if (firstPrompt && model && gitBranch && cwd)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
return { meta: { id, mtime: st.mtimeMs, messageCount: lines.length, firstPrompt, gitBranch, model, sizeBytes: st.size }, cwd };
|
|
93
|
+
}, st => ({ meta: { id, mtime: st.mtimeMs, messageCount: 0, firstPrompt: null, gitBranch: null, model: null, sizeBytes: st.size, truncated: true }, cwd: null }));
|
|
44
94
|
}
|
|
45
|
-
async function scanProjectsDir(projectsDir, scope) {
|
|
95
|
+
async function scanProjectsDir(projectsDir, scope, budget) {
|
|
46
96
|
if (!existsSync(projectsDir))
|
|
47
97
|
return [];
|
|
48
98
|
const out = [];
|
|
@@ -69,7 +119,7 @@ async function scanProjectsDir(projectsDir, scope) {
|
|
|
69
119
|
for (const f of files) {
|
|
70
120
|
if (!f.endsWith('.jsonl'))
|
|
71
121
|
continue;
|
|
72
|
-
const parsed = await parseClaudeSession(join(pdir, f));
|
|
122
|
+
const parsed = await parseClaudeSession(join(pdir, f), budget);
|
|
73
123
|
if (!parsed)
|
|
74
124
|
continue;
|
|
75
125
|
sessions.push(parsed.meta);
|
|
@@ -88,10 +138,40 @@ function messageText(content) {
|
|
|
88
138
|
return content;
|
|
89
139
|
if (!Array.isArray(content))
|
|
90
140
|
return null;
|
|
91
|
-
const
|
|
92
|
-
|
|
141
|
+
const texts = content
|
|
142
|
+
.filter((x) => x?.type === 'input_text' || x?.type === 'output_text' || x?.type === 'text')
|
|
143
|
+
.map((x) => x.text)
|
|
144
|
+
.filter((x) => typeof x === 'string');
|
|
145
|
+
return texts.length ? texts.join('\n\n') : null;
|
|
146
|
+
}
|
|
147
|
+
function limited(text) {
|
|
148
|
+
return text.length <= MAX_ENTRY_CHARS ? text : `${text.slice(0, MAX_ENTRY_CHARS)}\n\n[entry truncated for display]`;
|
|
149
|
+
}
|
|
150
|
+
function toolText(value) {
|
|
151
|
+
if (typeof value === 'string')
|
|
152
|
+
return value;
|
|
153
|
+
try {
|
|
154
|
+
return JSON.stringify(value, null, 2);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return String(value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function pushEntry(messages, role, text, label, timestamp) {
|
|
161
|
+
const clean = text?.trim();
|
|
162
|
+
if (!clean)
|
|
163
|
+
return;
|
|
164
|
+
if (role === 'user' && /^<(system-reminder|local-command-caveat|command-name)>/i.test(clean))
|
|
165
|
+
return;
|
|
166
|
+
const previous = messages.at(-1);
|
|
167
|
+
if (previous?.role === role && previous.text === clean)
|
|
168
|
+
return;
|
|
169
|
+
messages.push({
|
|
170
|
+
id: String(messages.length + 1), role, text: limited(clean), label,
|
|
171
|
+
timestamp: typeof timestamp === 'string' ? timestamp : null,
|
|
172
|
+
});
|
|
93
173
|
}
|
|
94
|
-
async function
|
|
174
|
+
async function parseClaudeTranscript(file, scope) {
|
|
95
175
|
let raw;
|
|
96
176
|
try {
|
|
97
177
|
raw = await readFile(file, 'utf8');
|
|
@@ -99,11 +179,12 @@ async function parseCodexSession(file) {
|
|
|
99
179
|
catch {
|
|
100
180
|
return null;
|
|
101
181
|
}
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
|
|
182
|
+
const id = file.split(/[\\/]/).pop().replace(/\.jsonl$/, '');
|
|
183
|
+
const messages = [];
|
|
184
|
+
let project = '(unknown project)';
|
|
185
|
+
for (const line of raw.split('\n')) {
|
|
186
|
+
if (!line.trim())
|
|
187
|
+
continue;
|
|
107
188
|
let rec;
|
|
108
189
|
try {
|
|
109
190
|
rec = JSON.parse(line);
|
|
@@ -111,6 +192,51 @@ async function parseCodexSession(file) {
|
|
|
111
192
|
catch {
|
|
112
193
|
continue;
|
|
113
194
|
}
|
|
195
|
+
if (project === '(unknown project)' && typeof rec.cwd === 'string')
|
|
196
|
+
project = rec.cwd;
|
|
197
|
+
const content = rec.message?.content;
|
|
198
|
+
if (rec.type === 'user') {
|
|
199
|
+
pushEntry(messages, 'user', messageText(content), null, rec.timestamp);
|
|
200
|
+
if (Array.isArray(content))
|
|
201
|
+
for (const block of content) {
|
|
202
|
+
if (block?.type === 'tool_result')
|
|
203
|
+
pushEntry(messages, 'tool', messageText(block.content) ?? toolText(block.content), 'Tool result', rec.timestamp);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (rec.type === 'assistant') {
|
|
207
|
+
pushEntry(messages, 'assistant', messageText(content), null, rec.timestamp);
|
|
208
|
+
if (Array.isArray(content))
|
|
209
|
+
for (const block of content) {
|
|
210
|
+
if (block?.type === 'tool_use')
|
|
211
|
+
pushEntry(messages, 'tool', toolText(block.input), typeof block.name === 'string' ? block.name : 'Tool call', rec.timestamp);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { id, agent: 'claude', scope, project, messages };
|
|
216
|
+
}
|
|
217
|
+
async function parseCodexTranscript(file, scope) {
|
|
218
|
+
let raw;
|
|
219
|
+
try {
|
|
220
|
+
raw = await readFile(file, 'utf8');
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const records = [];
|
|
226
|
+
for (const line of raw.split('\n')) {
|
|
227
|
+
if (!line.trim())
|
|
228
|
+
continue;
|
|
229
|
+
try {
|
|
230
|
+
records.push(JSON.parse(line));
|
|
231
|
+
}
|
|
232
|
+
catch { /* malformed line */ }
|
|
233
|
+
}
|
|
234
|
+
const filename = file.split(/[\\/]/).pop().replace(/\.jsonl$/, '');
|
|
235
|
+
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;
|
|
236
|
+
let project = '(unknown project)';
|
|
237
|
+
const hasResponseMessages = records.some(rec => rec?.type === 'response_item' && rec?.payload?.type === 'message');
|
|
238
|
+
const messages = [];
|
|
239
|
+
for (const rec of records) {
|
|
114
240
|
const payload = rec?.payload;
|
|
115
241
|
if (rec?.type === 'session_meta') {
|
|
116
242
|
if (typeof payload?.id === 'string')
|
|
@@ -118,33 +244,77 @@ async function parseCodexSession(file) {
|
|
|
118
244
|
else if (typeof payload?.session_id === 'string')
|
|
119
245
|
id = payload.session_id;
|
|
120
246
|
if (typeof payload?.cwd === 'string')
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
messageCount++;
|
|
134
|
-
if (firstPrompt === null && payload.role === 'user') {
|
|
135
|
-
const text = messageText(payload.content);
|
|
136
|
-
if (text?.trim() && !text.trimStart().startsWith('<'))
|
|
137
|
-
firstPrompt = text.trim().slice(0, 200);
|
|
138
|
-
}
|
|
247
|
+
project = payload.cwd;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (project === '(unknown project)' && rec?.type === 'turn_context' && typeof payload?.cwd === 'string')
|
|
251
|
+
project = payload.cwd;
|
|
252
|
+
if (rec?.type === 'response_item') {
|
|
253
|
+
if (payload?.type === 'message' && (payload.role === 'user' || payload.role === 'assistant'))
|
|
254
|
+
pushEntry(messages, payload.role, messageText(payload.content), null, rec.timestamp);
|
|
255
|
+
else if (payload?.type === 'function_call' || payload?.type === 'custom_tool_call' || payload?.type === 'local_shell_call')
|
|
256
|
+
pushEntry(messages, 'tool', toolText(payload.arguments ?? payload.input ?? payload), payload.name ?? payload.type, rec.timestamp);
|
|
257
|
+
else if (payload?.type === 'function_call_output' || payload?.type === 'custom_tool_call_output')
|
|
258
|
+
pushEntry(messages, 'tool', toolText(payload.output), 'Tool result', rec.timestamp);
|
|
139
259
|
}
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
260
|
+
else if (!hasResponseMessages && rec?.type === 'event_msg') {
|
|
261
|
+
if (payload?.type === 'user_message')
|
|
262
|
+
pushEntry(messages, 'user', payload.message, null, rec.timestamp);
|
|
263
|
+
else if (payload?.type === 'agent_message')
|
|
264
|
+
pushEntry(messages, 'assistant', payload.message, null, rec.timestamp);
|
|
143
265
|
}
|
|
144
266
|
}
|
|
267
|
+
return { id, agent: 'codex', scope, project, messages };
|
|
268
|
+
}
|
|
269
|
+
async function parseCodexSession(file, budget) {
|
|
145
270
|
const filename = file.split(/[\\/]/).pop().replace(/\.jsonl$/, '');
|
|
146
|
-
|
|
147
|
-
return
|
|
271
|
+
const fallbackId = 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;
|
|
272
|
+
return cachedParse(file, codexSessionCache, budget, (raw, st) => {
|
|
273
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
274
|
+
let id = null, firstPrompt = null, gitBranch = null;
|
|
275
|
+
let model = null, cwd = null, messageCount = 0;
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
let rec;
|
|
278
|
+
try {
|
|
279
|
+
rec = JSON.parse(line);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const payload = rec?.payload;
|
|
285
|
+
if (rec?.type === 'session_meta') {
|
|
286
|
+
if (typeof payload?.id === 'string')
|
|
287
|
+
id = payload.id;
|
|
288
|
+
else if (typeof payload?.session_id === 'string')
|
|
289
|
+
id = payload.session_id;
|
|
290
|
+
if (typeof payload?.cwd === 'string')
|
|
291
|
+
cwd = payload.cwd;
|
|
292
|
+
if (typeof payload?.git?.branch === 'string')
|
|
293
|
+
gitBranch = payload.git.branch;
|
|
294
|
+
}
|
|
295
|
+
if (rec?.type === 'turn_context') {
|
|
296
|
+
if (cwd === null && typeof payload?.cwd === 'string')
|
|
297
|
+
cwd = payload.cwd;
|
|
298
|
+
if (model === null && typeof payload?.model === 'string')
|
|
299
|
+
model = payload.model;
|
|
300
|
+
}
|
|
301
|
+
if (rec?.type === 'response_item' && payload?.type === 'message'
|
|
302
|
+
&& (payload?.role === 'user' || payload?.role === 'assistant')) {
|
|
303
|
+
messageCount++;
|
|
304
|
+
if (firstPrompt === null && payload.role === 'user') {
|
|
305
|
+
const text = messageText(payload.content);
|
|
306
|
+
if (text?.trim() && !text.trimStart().startsWith('<'))
|
|
307
|
+
firstPrompt = text.trim().slice(0, 200);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (firstPrompt === null && rec?.type === 'event_msg' && payload?.type === 'user_message'
|
|
311
|
+
&& typeof payload?.message === 'string' && payload.message.trim()) {
|
|
312
|
+
firstPrompt = payload.message.trim().slice(0, 200);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
id ??= fallbackId;
|
|
316
|
+
return { meta: { id, mtime: st.mtimeMs, messageCount: messageCount || lines.length, firstPrompt, gitBranch, model, sizeBytes: st.size }, cwd };
|
|
317
|
+
}, st => ({ meta: { id: fallbackId, mtime: st.mtimeMs, messageCount: 0, firstPrompt: null, gitBranch: null, model: null, sizeBytes: st.size, truncated: true }, cwd: null }));
|
|
148
318
|
}
|
|
149
319
|
async function codexSessionFiles(dir) {
|
|
150
320
|
let entries;
|
|
@@ -164,10 +334,30 @@ async function codexSessionFiles(dir) {
|
|
|
164
334
|
}
|
|
165
335
|
return out;
|
|
166
336
|
}
|
|
167
|
-
async function
|
|
337
|
+
async function claudeSessionFile(projectsDir, id) {
|
|
338
|
+
let projects;
|
|
339
|
+
try {
|
|
340
|
+
projects = await readdir(projectsDir, { withFileTypes: true });
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
for (const project of projects) {
|
|
346
|
+
if (!project.isDirectory())
|
|
347
|
+
continue;
|
|
348
|
+
const file = join(projectsDir, project.name, `${id}.jsonl`);
|
|
349
|
+
try {
|
|
350
|
+
if ((await stat(file)).isFile())
|
|
351
|
+
return file;
|
|
352
|
+
}
|
|
353
|
+
catch { /* keep looking */ }
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
async function scanCodexSessionsDir(sessionsDir, scope, budget) {
|
|
168
358
|
const byProject = new Map();
|
|
169
359
|
for (const file of await codexSessionFiles(sessionsDir)) {
|
|
170
|
-
const parsed = await parseCodexSession(file);
|
|
360
|
+
const parsed = await parseCodexSession(file, budget);
|
|
171
361
|
if (!parsed)
|
|
172
362
|
continue;
|
|
173
363
|
const project = parsed.cwd ?? '(unknown project)';
|
|
@@ -181,9 +371,10 @@ async function scanCodexSessionsDir(sessionsDir, scope) {
|
|
|
181
371
|
}));
|
|
182
372
|
}
|
|
183
373
|
export async function scanSessions(opts) {
|
|
374
|
+
const budget = { bytesLeft: MAX_TOTAL_SCAN_BYTES };
|
|
184
375
|
const out = [];
|
|
185
|
-
out.push(...await scanProjectsDir(join(opts.sharedRoot, 'projects'), 'shared'));
|
|
186
|
-
out.push(...await scanCodexSessionsDir(join(opts.sharedRoot, 'sessions'), 'shared'));
|
|
376
|
+
out.push(...await scanProjectsDir(join(opts.sharedRoot, 'projects'), 'shared', budget));
|
|
377
|
+
out.push(...await scanCodexSessionsDir(join(opts.sharedRoot, 'sessions'), 'shared', budget));
|
|
187
378
|
for (const p of opts.profiles) {
|
|
188
379
|
const agent = p.agent ?? 'claude';
|
|
189
380
|
const sessionDir = join(p.dir, agent === 'codex' ? 'sessions' : 'projects');
|
|
@@ -195,8 +386,41 @@ export async function scanSessions(opts) {
|
|
|
195
386
|
continue;
|
|
196
387
|
}
|
|
197
388
|
out.push(...agent === 'codex'
|
|
198
|
-
? await scanCodexSessionsDir(sessionDir, p.name)
|
|
199
|
-
: await scanProjectsDir(sessionDir, p.name));
|
|
389
|
+
? await scanCodexSessionsDir(sessionDir, p.name, budget)
|
|
390
|
+
: await scanProjectsDir(sessionDir, p.name, budget));
|
|
200
391
|
}
|
|
201
392
|
return out;
|
|
202
393
|
}
|
|
394
|
+
/** Read visible conversation and tool activity for one known managed session. */
|
|
395
|
+
export async function readSessionTranscript(opts) {
|
|
396
|
+
if (!SAFE_SESSION_ID.test(opts.id))
|
|
397
|
+
return null;
|
|
398
|
+
let root;
|
|
399
|
+
if (opts.scope === 'shared')
|
|
400
|
+
root = join(opts.sharedRoot, opts.agent === 'codex' ? 'sessions' : 'projects');
|
|
401
|
+
else {
|
|
402
|
+
const profile = opts.profiles.find(p => p.name === opts.scope && (p.agent ?? 'claude') === opts.agent);
|
|
403
|
+
if (!profile)
|
|
404
|
+
return null;
|
|
405
|
+
root = join(profile.dir, opts.agent === 'codex' ? 'sessions' : 'projects');
|
|
406
|
+
try {
|
|
407
|
+
if ((await lstat(root)).isSymbolicLink())
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (opts.agent === 'claude') {
|
|
415
|
+
const file = await claudeSessionFile(root, opts.id);
|
|
416
|
+
return file ? parseClaudeTranscript(file, opts.scope) : null;
|
|
417
|
+
}
|
|
418
|
+
const files = await codexSessionFiles(root);
|
|
419
|
+
files.sort((a, b) => Number(!a.includes(opts.id)) - Number(!b.includes(opts.id)));
|
|
420
|
+
for (const file of files) {
|
|
421
|
+
const transcript = await parseCodexTranscript(file, opts.scope);
|
|
422
|
+
if (transcript?.id === opts.id)
|
|
423
|
+
return transcript;
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccprofiles-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
8
14
|
"files": [
|
|
9
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
10
17
|
],
|
|
11
18
|
"license": "MIT",
|
|
12
19
|
"funding": {
|
|
@@ -34,5 +41,8 @@
|
|
|
34
41
|
"smol-toml": "^1.7.0",
|
|
35
42
|
"yaml": "^2.5.0",
|
|
36
43
|
"zod": "^3.23.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"prepack": "cd ../.. && npm run build"
|
|
37
47
|
}
|
|
38
48
|
}
|