packlight-cli 0.0.0-stage → 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/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +72 -2
- package/dist/adapters/claude-code/inventory.d.ts +48 -0
- package/dist/adapters/claude-code/inventory.js +449 -0
- package/dist/adapters/claude-code/logs.d.ts +54 -0
- package/dist/adapters/claude-code/logs.js +359 -0
- package/dist/adapters/codex/inventory.d.ts +17 -0
- package/dist/adapters/codex/inventory.js +130 -0
- package/dist/adapters/codex/logs.d.ts +22 -0
- package/dist/adapters/codex/logs.js +151 -0
- package/dist/adapters/codex/toml.d.ts +10 -0
- package/dist/adapters/codex/toml.js +76 -0
- package/dist/archive/apply.d.ts +62 -0
- package/dist/archive/apply.js +284 -0
- package/dist/archive/fsops.d.ts +24 -0
- package/dist/archive/fsops.js +95 -0
- package/dist/archive/journal.d.ts +41 -0
- package/dist/archive/journal.js +95 -0
- package/dist/archive/lock.d.ts +2 -0
- package/dist/archive/lock.js +32 -0
- package/dist/archive/manifest.d.ts +37 -0
- package/dist/archive/manifest.js +9 -0
- package/dist/archive/methods.d.ts +46 -0
- package/dist/archive/methods.js +239 -0
- package/dist/archive/paths.d.ts +18 -0
- package/dist/archive/paths.js +61 -0
- package/dist/archive/restore.d.ts +30 -0
- package/dist/archive/restore.js +84 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +329 -0
- package/dist/core/buckets.d.ts +3 -0
- package/dist/core/buckets.js +6 -0
- package/dist/core/frontmatter.d.ts +6 -0
- package/dist/core/frontmatter.js +31 -0
- package/dist/core/hash.d.ts +19 -0
- package/dist/core/hash.js +42 -0
- package/dist/core/messages.d.ts +16 -0
- package/dist/core/messages.js +17 -0
- package/dist/core/scan.d.ts +16 -0
- package/dist/core/scan.js +345 -0
- package/dist/core/summary.d.ts +4 -0
- package/dist/core/summary.js +63 -0
- package/dist/core/types.d.ts +185 -0
- package/dist/core/types.js +3 -0
- package/dist/report/client.d.ts +1 -0
- package/dist/report/client.js +676 -0
- package/dist/report/fix.d.ts +46 -0
- package/dist/report/fix.js +116 -0
- package/dist/report/logo.d.ts +7 -0
- package/dist/report/logo.js +23 -0
- package/dist/report/model.d.ts +103 -0
- package/dist/report/model.js +194 -0
- package/dist/report/render.d.ts +9 -0
- package/dist/report/render.js +45 -0
- package/dist/report/styles.d.ts +1 -0
- package/dist/report/styles.js +202 -0
- package/dist/report/suggest.d.ts +39 -0
- package/dist/report/suggest.js +88 -0
- package/package.json +56 -4
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BudgetObservation, SessionRecord, ToolListing } from '../../core/types.js';
|
|
2
|
+
/** Top-level line types seen in Claude Code logs up to 2.1.293 (eng A2). Anything else is counted as an unknown shape. */
|
|
3
|
+
export declare const KNOWN_LINE_TYPES: Set<string>;
|
|
4
|
+
export interface Use {
|
|
5
|
+
ts: string | null;
|
|
6
|
+
project: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface HookFiring {
|
|
9
|
+
event: string;
|
|
10
|
+
command: string;
|
|
11
|
+
ts: string | null;
|
|
12
|
+
project: string | null;
|
|
13
|
+
durationMs: number | null;
|
|
14
|
+
/** Output Claude Code recorded on the success line (used when no separate context line follows). */
|
|
15
|
+
ownChars: number;
|
|
16
|
+
/** Characters from additional-context/system-message lines linked to this firing. */
|
|
17
|
+
linkedChars: number;
|
|
18
|
+
/** Linked text was shared with other hooks on the same tool call and could not be split. */
|
|
19
|
+
shared: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface LogScan {
|
|
22
|
+
sessions: SessionRecord[];
|
|
23
|
+
skillUses: Map<string, Use[]>;
|
|
24
|
+
agentUses: Map<string, Use[]>;
|
|
25
|
+
mcpUses: Map<string, Use[]>;
|
|
26
|
+
hookFirings: HookFiring[];
|
|
27
|
+
/** Initial skill listings, oldest first. */
|
|
28
|
+
listings: BudgetObservation[];
|
|
29
|
+
/** One per main session file that logged a tool list, oldest first. */
|
|
30
|
+
toolListings: ToolListing[];
|
|
31
|
+
unattributedHookChars: number;
|
|
32
|
+
linesTotal: number;
|
|
33
|
+
linesUnknownShape: number;
|
|
34
|
+
/** Unknown-shape lines that looked like usage evidence (tool calls, hooks, listings). */
|
|
35
|
+
linesUnknownRelevant: number;
|
|
36
|
+
linesUnreadable: number;
|
|
37
|
+
unknownByVersion: Record<string, number>;
|
|
38
|
+
versions: string[];
|
|
39
|
+
}
|
|
40
|
+
export interface LogOptions {
|
|
41
|
+
/** Maps a working directory to its project root, or null when it belongs to no known project. */
|
|
42
|
+
projectOf: (cwd: string) => string | null;
|
|
43
|
+
since?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Every .jsonl under the projects folder; subagent transcripts belong to their parent session. */
|
|
46
|
+
export declare function logFiles(projectsDir: string): {
|
|
47
|
+
file: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
isSubagent: boolean;
|
|
50
|
+
}[];
|
|
51
|
+
export declare function scanLogs(projectsDir: string, opts: LogOptions): Promise<LogScan>;
|
|
52
|
+
/** A server name in the form tool names use: "plugin:x:y" and "My Server" become "plugin_x_y" and "My_Server". */
|
|
53
|
+
export declare const toolKey: (name: string) => string;
|
|
54
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { createReadStream, readdirSync } from 'node:fs';
|
|
2
|
+
import { basename, join, relative, sep } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
/** Top-level line types seen in Claude Code logs up to 2.1.293 (eng A2). Anything else is counted as an unknown shape. */
|
|
5
|
+
export const KNOWN_LINE_TYPES = new Set([
|
|
6
|
+
'agent-name', 'ai-title', 'artifact-autoreact-ledger', 'artifact-comment-monitor', 'assistant', 'atis-latch', 'attachment',
|
|
7
|
+
'bridge-session', 'cost-state', 'custom-title', 'dev-mods', 'failed', 'file-history-delta', 'file-history-snapshot',
|
|
8
|
+
'frame-link', 'last-prompt', 'launched', 'mode', 'pr-link', 'queue-operation', 'relocated', 'result', 'started',
|
|
9
|
+
'summary', 'system', 'user',
|
|
10
|
+
]);
|
|
11
|
+
const HOOK_ATTACHMENTS = new Set(['hook_success', 'hook_additional_context', 'hook_system_message']);
|
|
12
|
+
/** Text length of an attachment's content, which is a string or a list of strings/text blocks. */
|
|
13
|
+
function contentChars(c) {
|
|
14
|
+
if (typeof c === 'string')
|
|
15
|
+
return c.length;
|
|
16
|
+
if (Array.isArray(c))
|
|
17
|
+
return c.reduce((n, x) => n + (typeof x === 'string' ? x.length : typeof x?.text === 'string' ? x.text.length : 0), 0);
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
function push(map, key, use) {
|
|
21
|
+
const list = map.get(key);
|
|
22
|
+
if (list)
|
|
23
|
+
list.push(use);
|
|
24
|
+
else
|
|
25
|
+
map.set(key, [use]);
|
|
26
|
+
}
|
|
27
|
+
function listingSplit(content, names) {
|
|
28
|
+
const byLength = [...names].sort((a, b) => b.length - a.length);
|
|
29
|
+
const withDescription = [];
|
|
30
|
+
const dropped = [];
|
|
31
|
+
const entries = [];
|
|
32
|
+
let current = null;
|
|
33
|
+
for (const line of content.split('\n')) {
|
|
34
|
+
if (!line.startsWith('- ')) {
|
|
35
|
+
// A description that runs over several lines: its size belongs to the entry above.
|
|
36
|
+
if (current)
|
|
37
|
+
current.chars += line.length + 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const body = line.slice(2);
|
|
41
|
+
const name = byLength.find(n => body === n || body.startsWith(`${n}:`) || body.startsWith(`${n} (`));
|
|
42
|
+
if (!name) {
|
|
43
|
+
current = null;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const rest = body.slice(name.length).replace(/^ \([^)]*\)/, '');
|
|
47
|
+
const described = rest.startsWith(': ');
|
|
48
|
+
(described ? withDescription : dropped).push(name);
|
|
49
|
+
current = { name, chars: line.length + 1, described };
|
|
50
|
+
entries.push(current);
|
|
51
|
+
}
|
|
52
|
+
return { withDescription, dropped, entries };
|
|
53
|
+
}
|
|
54
|
+
/** Every .jsonl under the projects folder; subagent transcripts belong to their parent session. */
|
|
55
|
+
export function logFiles(projectsDir) {
|
|
56
|
+
const out = [];
|
|
57
|
+
const walk = (dir) => {
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
const p = join(dir, e.name);
|
|
67
|
+
if (e.isDirectory())
|
|
68
|
+
walk(p);
|
|
69
|
+
else if (e.name.endsWith('.jsonl')) {
|
|
70
|
+
const parts = relative(projectsDir, p).split(sep);
|
|
71
|
+
// <project>/<session>.jsonl, or <project>/<session>/subagents/<agent>.jsonl
|
|
72
|
+
const isSubagent = parts.length > 2;
|
|
73
|
+
out.push({ file: p, sessionId: isSubagent ? parts[1] : basename(e.name, '.jsonl'), isSubagent });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(projectsDir);
|
|
78
|
+
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
79
|
+
}
|
|
80
|
+
export async function scanLogs(projectsDir, opts) {
|
|
81
|
+
const scan = {
|
|
82
|
+
sessions: [], skillUses: new Map(), agentUses: new Map(), mcpUses: new Map(), hookFirings: [], listings: [], toolListings: [],
|
|
83
|
+
unattributedHookChars: 0, linesTotal: 0, linesUnknownShape: 0, linesUnknownRelevant: 0, linesUnreadable: 0,
|
|
84
|
+
unknownByVersion: {}, versions: [],
|
|
85
|
+
};
|
|
86
|
+
const versions = new Set();
|
|
87
|
+
const sessions = new Map();
|
|
88
|
+
for (const { file, sessionId, isSubagent } of logFiles(projectsDir)) {
|
|
89
|
+
let s = sessions.get(sessionId);
|
|
90
|
+
if (!s) {
|
|
91
|
+
s = { id: sessionId, file, projectRoot: null, start: null, end: null, lines: 0, unknownShapeLines: 0, unreadableLines: 0, versions: [], versionSet: new Set() };
|
|
92
|
+
sessions.set(sessionId, s);
|
|
93
|
+
}
|
|
94
|
+
if (!isSubagent)
|
|
95
|
+
s.file = file;
|
|
96
|
+
// Lines without a cwd inherit the last one seen in this file (eng A3).
|
|
97
|
+
let cwd = null;
|
|
98
|
+
let version = null;
|
|
99
|
+
// Hook success lines waiting for their context lines, keyed by tool call and hook name.
|
|
100
|
+
const pending = new Map();
|
|
101
|
+
// This session's tool list, built from its deltas (main session files only: subagents get their own lists).
|
|
102
|
+
let tools = null;
|
|
103
|
+
const toolLines = new Map();
|
|
104
|
+
const server = (key) => (tools.servers[key] ??= { tools: 0, chars: 0, instructionChars: 0 });
|
|
105
|
+
const rl = createInterface({ input: createReadStream(file, { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
106
|
+
for await (const line of rl) {
|
|
107
|
+
if (!line)
|
|
108
|
+
continue;
|
|
109
|
+
scan.linesTotal++;
|
|
110
|
+
s.lines++;
|
|
111
|
+
let e;
|
|
112
|
+
try {
|
|
113
|
+
e = JSON.parse(line);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
scan.linesUnreadable++;
|
|
117
|
+
s.unreadableLines++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!e || typeof e !== 'object') {
|
|
121
|
+
scan.linesUnreadable++;
|
|
122
|
+
s.unreadableLines++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (typeof e.cwd === 'string')
|
|
126
|
+
cwd = e.cwd;
|
|
127
|
+
if (typeof e.version === 'string') {
|
|
128
|
+
const v = e.version;
|
|
129
|
+
version = v;
|
|
130
|
+
versions.add(v);
|
|
131
|
+
s.versionSet.add(v);
|
|
132
|
+
}
|
|
133
|
+
const ts = typeof e.timestamp === 'string' ? e.timestamp : null;
|
|
134
|
+
if (ts && opts.since && ts < opts.since)
|
|
135
|
+
continue;
|
|
136
|
+
if (ts) {
|
|
137
|
+
if (!s.start || ts < s.start)
|
|
138
|
+
s.start = ts;
|
|
139
|
+
if (!s.end || ts > s.end)
|
|
140
|
+
s.end = ts;
|
|
141
|
+
}
|
|
142
|
+
const project = cwd ? opts.projectOf(cwd) : null;
|
|
143
|
+
if (project && !isSubagent && !s.projectRoot)
|
|
144
|
+
s.projectRoot = project;
|
|
145
|
+
const use = { ts, project };
|
|
146
|
+
// Session counts hold only unknown lines that looked like usage evidence; they drive "usage uncertain".
|
|
147
|
+
const unknown = (relevant) => {
|
|
148
|
+
scan.linesUnknownShape++;
|
|
149
|
+
if (relevant) {
|
|
150
|
+
s.unknownShapeLines++;
|
|
151
|
+
scan.linesUnknownRelevant++;
|
|
152
|
+
const v = version ?? 'unknown';
|
|
153
|
+
scan.unknownByVersion[v] = (scan.unknownByVersion[v] ?? 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const type = e.type;
|
|
157
|
+
if (typeof type !== 'string' || !KNOWN_LINE_TYPES.has(type)) {
|
|
158
|
+
unknown(/"tool_use"|"hook[A-Z_]|skill_listing/.test(line));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (type === 'assistant') {
|
|
162
|
+
const content = e.message?.content;
|
|
163
|
+
if (!Array.isArray(content)) {
|
|
164
|
+
if (line.includes('"tool_use"'))
|
|
165
|
+
unknown(true);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const b of content) {
|
|
169
|
+
if (b?.type !== 'tool_use')
|
|
170
|
+
continue;
|
|
171
|
+
if (typeof b.name !== 'string') {
|
|
172
|
+
unknown(true);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const input = b.input ?? {};
|
|
176
|
+
if (b.name === 'Skill') {
|
|
177
|
+
if (typeof input.skill === 'string')
|
|
178
|
+
push(scan.skillUses, input.skill.replace(/^\//, ''), use);
|
|
179
|
+
else
|
|
180
|
+
unknown(true);
|
|
181
|
+
}
|
|
182
|
+
else if (b.name === 'Agent' || b.name === 'Task') {
|
|
183
|
+
push(scan.agentUses, typeof input.subagent_type === 'string' ? input.subagent_type : 'general-purpose', use);
|
|
184
|
+
}
|
|
185
|
+
else if (b.name.startsWith('mcp__')) {
|
|
186
|
+
const server = b.name.split('__')[1];
|
|
187
|
+
if (server)
|
|
188
|
+
push(scan.mcpUses, server, use);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (type === 'user') {
|
|
193
|
+
if (!line.includes('<command-name>'))
|
|
194
|
+
continue;
|
|
195
|
+
const c = e.message?.content;
|
|
196
|
+
const text = typeof c === 'string' ? c : Array.isArray(c) ? c.map((x) => (typeof x?.text === 'string' ? x.text : '')).join('') : '';
|
|
197
|
+
const m = /<command-name>\/?([^<]+)<\/command-name>/.exec(text);
|
|
198
|
+
if (m?.[1])
|
|
199
|
+
push(scan.skillUses, m[1].trim(), use);
|
|
200
|
+
}
|
|
201
|
+
else if (type === 'attachment') {
|
|
202
|
+
const a = e.attachment;
|
|
203
|
+
if (!a || typeof a !== 'object')
|
|
204
|
+
continue;
|
|
205
|
+
const at = a.type;
|
|
206
|
+
if (typeof at === 'string' && at.startsWith('hook_') && !HOOK_ATTACHMENTS.has(at)) {
|
|
207
|
+
unknown(true);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (at === 'hook_success') {
|
|
211
|
+
if (typeof a.command !== 'string' || typeof a.hookEvent !== 'string') {
|
|
212
|
+
unknown(true);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const firing = {
|
|
216
|
+
event: a.hookEvent, command: a.command, ts, project,
|
|
217
|
+
durationMs: typeof a.durationMs === 'number' ? a.durationMs : null,
|
|
218
|
+
ownChars: contentChars(a.content) || (typeof a.stdout === 'string' ? a.stdout.length : 0),
|
|
219
|
+
linkedChars: 0, shared: false,
|
|
220
|
+
};
|
|
221
|
+
scan.hookFirings.push(firing);
|
|
222
|
+
const key = `${a.toolUseID ?? ''}|${a.hookName ?? ''}`;
|
|
223
|
+
const list = pending.get(key);
|
|
224
|
+
if (list)
|
|
225
|
+
list.push(firing);
|
|
226
|
+
else
|
|
227
|
+
pending.set(key, [firing]);
|
|
228
|
+
}
|
|
229
|
+
else if (at === 'hook_additional_context' || at === 'hook_system_message') {
|
|
230
|
+
const chars = contentChars(a.content);
|
|
231
|
+
const firings = pending.get(`${a.toolUseID ?? ''}|${a.hookName ?? ''}`);
|
|
232
|
+
if (!firings?.length) {
|
|
233
|
+
scan.unattributedHookChars += chars;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Only hooks that printed something can have produced the text.
|
|
237
|
+
const producers = firings.filter(f => f.ownChars > 0);
|
|
238
|
+
const targets = producers.length ? producers : firings;
|
|
239
|
+
if (targets.length === 1)
|
|
240
|
+
targets[0].linkedChars += chars;
|
|
241
|
+
else
|
|
242
|
+
for (const f of targets) {
|
|
243
|
+
f.linkedChars += chars / targets.length;
|
|
244
|
+
f.shared = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if ((at === 'deferred_tools_delta' || at === 'mcp_instructions_delta') && !isSubagent) {
|
|
248
|
+
tools ??= { sessionId, timestamp: ts, version, projectRoot: project, servers: {}, builtIn: { tools: 0, chars: 0 } };
|
|
249
|
+
const added = Array.isArray(a.addedNames) ? a.addedNames : [];
|
|
250
|
+
const removed = Array.isArray(a.removedNames) ? a.removedNames : [];
|
|
251
|
+
if (at === 'deferred_tools_delta') {
|
|
252
|
+
const lines = Array.isArray(a.addedLines) ? a.addedLines : [];
|
|
253
|
+
added.forEach((n, i) => {
|
|
254
|
+
if (typeof n !== 'string')
|
|
255
|
+
return;
|
|
256
|
+
const text = typeof lines[i] === 'string' ? lines[i] : n;
|
|
257
|
+
const key = n.startsWith('mcp__') ? n.split('__')[1] ?? null : null;
|
|
258
|
+
const prev = toolLines.get(n);
|
|
259
|
+
if (prev) {
|
|
260
|
+
if (prev.key) {
|
|
261
|
+
server(prev.key).tools--;
|
|
262
|
+
server(prev.key).chars -= prev.chars;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
tools.builtIn.tools--;
|
|
266
|
+
tools.builtIn.chars -= prev.chars;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
toolLines.set(n, { key, chars: text.length + 1 });
|
|
270
|
+
if (key) {
|
|
271
|
+
const s = server(key);
|
|
272
|
+
s.tools++;
|
|
273
|
+
s.chars += text.length + 1;
|
|
274
|
+
const short = n.split('__').slice(2).join('__');
|
|
275
|
+
if (short && (s.sample ??= []).length < 4 && !s.sample.includes(short))
|
|
276
|
+
s.sample.push(short);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
tools.builtIn.tools++;
|
|
280
|
+
tools.builtIn.chars += text.length + 1;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
for (const n of removed) {
|
|
284
|
+
const prev = typeof n === 'string' ? toolLines.get(n) : undefined;
|
|
285
|
+
if (!prev)
|
|
286
|
+
continue;
|
|
287
|
+
toolLines.delete(n);
|
|
288
|
+
if (prev.key) {
|
|
289
|
+
server(prev.key).tools--;
|
|
290
|
+
server(prev.key).chars -= prev.chars;
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
tools.builtIn.tools--;
|
|
294
|
+
tools.builtIn.chars -= prev.chars;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const blocks = Array.isArray(a.addedBlocks) ? a.addedBlocks : [];
|
|
300
|
+
added.forEach((n, i) => {
|
|
301
|
+
if (typeof n !== 'string')
|
|
302
|
+
return;
|
|
303
|
+
const block = typeof blocks[i] === 'string' ? blocks[i] : '';
|
|
304
|
+
const s = server(toolKey(n));
|
|
305
|
+
s.instructionChars = block.length;
|
|
306
|
+
// Connectors are named by id; their block's first line after the heading names the service.
|
|
307
|
+
// Only a short name before a colon ("Claude Docs: living docs…") counts; a sentence is instructions, not a name.
|
|
308
|
+
const name = /^[^\n]*\n([^:\n.]{1,32}):/.exec(block)?.[1]?.trim();
|
|
309
|
+
const label = name && name.split(/\s+/).length <= 4 && !/^(use|prefer|always|never|when|do|call)\b/i.test(name) ? name : undefined;
|
|
310
|
+
if (label)
|
|
311
|
+
s.label = label;
|
|
312
|
+
});
|
|
313
|
+
for (const n of removed)
|
|
314
|
+
if (typeof n === 'string' && tools.servers[toolKey(n)])
|
|
315
|
+
tools.servers[toolKey(n)].instructionChars = 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (at === 'skill_listing') {
|
|
319
|
+
if (!Array.isArray(a.names) || typeof a.content !== 'string') {
|
|
320
|
+
unknown(true);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (a.isInitial === false)
|
|
324
|
+
continue;
|
|
325
|
+
scan.listings.push({
|
|
326
|
+
sessionId, timestamp: ts, version, projectRoot: project, skillCount: typeof a.skillCount === 'number' ? a.skillCount : a.names.length,
|
|
327
|
+
listingChars: a.content.length, ...listingSplit(a.content, a.names),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (tools) {
|
|
333
|
+
for (const [k, v] of Object.entries(tools.servers))
|
|
334
|
+
if (v.tools <= 0 && v.instructionChars <= 0)
|
|
335
|
+
delete tools.servers[k];
|
|
336
|
+
scan.toolListings.push(tools);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
scan.toolListings.sort((a, b) => (a.timestamp ?? '').localeCompare(b.timestamp ?? ''));
|
|
340
|
+
for (const s of sessions.values()) {
|
|
341
|
+
const { versionSet, ...rest } = s;
|
|
342
|
+
scan.sessions.push({ ...rest, versions: [...versionSet].sort() });
|
|
343
|
+
}
|
|
344
|
+
scan.listings.sort((a, b) => (a.timestamp ?? '').localeCompare(b.timestamp ?? ''));
|
|
345
|
+
scan.versions = [...versions].sort(compareVersions);
|
|
346
|
+
return scan;
|
|
347
|
+
}
|
|
348
|
+
/** A server name in the form tool names use: "plugin:x:y" and "My Server" become "plugin_x_y" and "My_Server". */
|
|
349
|
+
export const toolKey = (name) => name.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
350
|
+
export function compareVersions(a, b) {
|
|
351
|
+
const pa = a.split('.').map(Number);
|
|
352
|
+
const pb = b.split('.').map(Number);
|
|
353
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
354
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
355
|
+
if (d)
|
|
356
|
+
return d;
|
|
357
|
+
}
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Item } from '../../core/types.js';
|
|
2
|
+
import { type TomlTable } from './toml.js';
|
|
3
|
+
export interface CodexPaths {
|
|
4
|
+
home: string;
|
|
5
|
+
root: string;
|
|
6
|
+
config: string;
|
|
7
|
+
skills: string;
|
|
8
|
+
pluginCache: string;
|
|
9
|
+
prompts: string;
|
|
10
|
+
sessions: string;
|
|
11
|
+
agentsMd: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function codexPaths(home: string): CodexPaths;
|
|
14
|
+
export declare function readCodexConfig(paths: CodexPaths): TomlTable[];
|
|
15
|
+
/** Project roots Codex knows: its [projects."<path>"] tables. */
|
|
16
|
+
export declare function codexProjects(paths: CodexPaths): string[];
|
|
17
|
+
export declare function collectCodexInventory(paths: CodexPaths, projectRoots: string[]): Item[];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// What a Codex setup holds: skills (personal, built-in, from plugins), plugins, MCP servers, custom prompts and
|
|
2
|
+
// AGENTS.md files. Read from ~/.codex (or $CODEX_HOME). Plugins and MCP servers are switched off in config.toml,
|
|
3
|
+
// which packlight does not edit: their removal is "manual" with the exact line to change.
|
|
4
|
+
import { existsSync, statSync } from 'node:fs';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
6
|
+
import { dirs, files, firstSeenOf, makeItem, readText } from '../claude-code/inventory.js';
|
|
7
|
+
import { frontmatter } from '../../core/frontmatter.js';
|
|
8
|
+
import { pathFingerprint, valueFingerprint } from '../../core/hash.js';
|
|
9
|
+
import { readToml } from './toml.js';
|
|
10
|
+
export function codexPaths(home) {
|
|
11
|
+
const root = process.env.CODEX_HOME && home === process.env.HOME ? process.env.CODEX_HOME : join(home, '.codex');
|
|
12
|
+
return {
|
|
13
|
+
home, root, config: join(root, 'config.toml'), skills: join(root, 'skills'), pluginCache: join(root, 'plugins', 'cache'),
|
|
14
|
+
prompts: join(root, 'prompts'), sessions: join(root, 'sessions'), agentsMd: join(root, 'AGENTS.md'),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const item = (i) => makeItem({ ...i, agent: 'codex' });
|
|
18
|
+
const tilde = (p, home) => (p.startsWith(home) ? `~${p.slice(home.length)}` : p);
|
|
19
|
+
/** A skill's line in Codex's skill listing: "- name: description (file: rN/…/SKILL.md)". */
|
|
20
|
+
const listingChars = (name, description) => `- ${name}: ${description} (file: r0/${name}/SKILL.md)\n`.length;
|
|
21
|
+
const newestDir = (p) => {
|
|
22
|
+
const ds = dirs(p).map(d => join(p, d));
|
|
23
|
+
ds.sort((a, b) => { try {
|
|
24
|
+
return statSync(b).mtimeMs - statSync(a).mtimeMs;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return 0;
|
|
28
|
+
} });
|
|
29
|
+
return ds[0] ?? null;
|
|
30
|
+
};
|
|
31
|
+
function skill(dir, name, opts) {
|
|
32
|
+
const file = join(dir, 'SKILL.md');
|
|
33
|
+
if (!existsSync(file))
|
|
34
|
+
return null;
|
|
35
|
+
const description = frontmatter(readText(file)).description ?? '';
|
|
36
|
+
return item({
|
|
37
|
+
kind: 'skill', name, source: opts.source, path: dir, enabled: opts.enabled, description,
|
|
38
|
+
standingChars: opts.enabled === false ? 0 : listingChars(name, description), fingerprint: pathFingerprint(dir),
|
|
39
|
+
...(opts.firstSeen ?? firstSeenOf(dir)), logKeys: [name], plugin: opts.plugin, removal: opts.removal,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function readCodexConfig(paths) {
|
|
43
|
+
return readToml(readText(paths.config));
|
|
44
|
+
}
|
|
45
|
+
/** Project roots Codex knows: its [projects."<path>"] tables. */
|
|
46
|
+
export function codexProjects(paths) {
|
|
47
|
+
return readCodexConfig(paths).filter(t => t.path[0] === 'projects' && t.path.length === 2).map(t => t.path[1]);
|
|
48
|
+
}
|
|
49
|
+
export function collectCodexInventory(paths, projectRoots) {
|
|
50
|
+
const items = [];
|
|
51
|
+
const config = readCodexConfig(paths);
|
|
52
|
+
const cfgFile = tilde(paths.config, paths.home);
|
|
53
|
+
// Personal skills move into packlight's archive like Claude Code's; built-in ones ship with Codex.
|
|
54
|
+
for (const d of dirs(paths.skills)) {
|
|
55
|
+
if (d.startsWith('.'))
|
|
56
|
+
continue;
|
|
57
|
+
const s = skill(join(paths.skills, d), d, { source: 'personal', removal: { method: 'move' } });
|
|
58
|
+
if (s)
|
|
59
|
+
items.push(s);
|
|
60
|
+
}
|
|
61
|
+
for (const d of dirs(join(paths.skills, '.system'))) {
|
|
62
|
+
const s = skill(join(paths.skills, '.system', d), d, { source: 'built into Codex', removal: { method: 'manual', where: 'built into Codex', suite: true } });
|
|
63
|
+
if (s)
|
|
64
|
+
items.push(s);
|
|
65
|
+
}
|
|
66
|
+
// Plugins: every cached plugin, newest version; on unless config.toml says enabled = false.
|
|
67
|
+
const pluginCfg = new Map(config.filter(t => t.path[0] === 'plugins' && t.path.length === 2).map(t => [t.path[1], t]));
|
|
68
|
+
for (const market of dirs(paths.pluginCache)) {
|
|
69
|
+
for (const name of dirs(join(paths.pluginCache, market))) {
|
|
70
|
+
const key = `${name}@${market}`;
|
|
71
|
+
const version = newestDir(join(paths.pluginCache, market, name));
|
|
72
|
+
if (!version)
|
|
73
|
+
continue;
|
|
74
|
+
const cfg = pluginCfg.get(key);
|
|
75
|
+
const enabled = cfg?.values.enabled !== false;
|
|
76
|
+
const where = cfg ? `${cfgFile}, under [plugins."${key}"]: set enabled = false` : `Codex › Plugins (or add [plugins."${key}"] enabled = false to ${cfgFile})`;
|
|
77
|
+
const removal = { method: 'manual', where };
|
|
78
|
+
const seen = firstSeenOf(version);
|
|
79
|
+
const parts = [];
|
|
80
|
+
for (const s of dirs(join(version, 'skills'))) {
|
|
81
|
+
const it = skill(join(version, 'skills', s), `${name}:${s}`, { source: `plugin:${name}`, enabled, plugin: key, removal, firstSeen: seen });
|
|
82
|
+
if (it)
|
|
83
|
+
parts.push(it);
|
|
84
|
+
}
|
|
85
|
+
const manifest = readText(join(version, '.codex-plugin', 'plugin.json'));
|
|
86
|
+
let description = '';
|
|
87
|
+
try {
|
|
88
|
+
description = String(JSON.parse(manifest).description ?? '');
|
|
89
|
+
}
|
|
90
|
+
catch { /* no manifest */ }
|
|
91
|
+
items.push(item({
|
|
92
|
+
kind: 'plugin', name, source: `marketplace:${market}`, path: version, enabled, description,
|
|
93
|
+
fingerprint: valueFingerprint({ key, version: basename(version), enabled }), ...seen, logKeys: [name], plugin: key,
|
|
94
|
+
members: parts.length ? { skill: parts.length } : {}, removal,
|
|
95
|
+
declarations: cfg ? [{ file: paths.config, scope: 'user', enabled }] : [],
|
|
96
|
+
}), ...parts);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// MCP servers: [mcp_servers.<name>]; enabled = false keeps them listed but off.
|
|
100
|
+
for (const t of config.filter(t => t.path[0] === 'mcp_servers' && t.path.length === 2)) {
|
|
101
|
+
const name = t.path[1];
|
|
102
|
+
const enabled = t.values.enabled !== false;
|
|
103
|
+
items.push(item({
|
|
104
|
+
kind: 'mcp', name, source: 'user config', path: paths.config, enabled,
|
|
105
|
+
description: typeof t.values.command === 'string' ? `${t.values.command}` : typeof t.values.url === 'string' ? `${t.values.url}` : '',
|
|
106
|
+
fingerprint: valueFingerprint(t.values), logKeys: [name.replace(/[^A-Za-z0-9_-]/g, '_')],
|
|
107
|
+
removal: { method: 'manual', where: `${cfgFile}, under [mcp_servers.${name}]: set enabled = false` },
|
|
108
|
+
declarations: [{ file: paths.config, scope: 'user', enabled }],
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
// Custom prompts (slash commands) live as markdown files.
|
|
112
|
+
for (const f of files(paths.prompts, '.md')) {
|
|
113
|
+
const path = join(paths.prompts, f);
|
|
114
|
+
const name = f.replace(/\.md$/, '');
|
|
115
|
+
const description = frontmatter(readText(path)).description ?? '';
|
|
116
|
+
items.push(item({ kind: 'command', name, source: 'personal', path, description, fingerprint: pathFingerprint(path), ...firstSeenOf(path), logKeys: [name], removal: { method: 'move' } }));
|
|
117
|
+
}
|
|
118
|
+
// AGENTS.md: the global one and each known project's; loaded in full, edited by hand.
|
|
119
|
+
const agents = [[paths.agentsMd, null], ...projectRoots.map(r => [join(r, 'AGENTS.md'), r])];
|
|
120
|
+
for (const [path, root] of agents) {
|
|
121
|
+
const text = readText(path);
|
|
122
|
+
if (!text.trim())
|
|
123
|
+
continue;
|
|
124
|
+
items.push(item({
|
|
125
|
+
kind: 'instructions', name: root ? `${basename(root)}/AGENTS.md` : 'AGENTS.md', source: root ? 'project' : 'personal', path, projectRoot: root,
|
|
126
|
+
standingChars: text.length, fingerprint: pathFingerprint(path), ...firstSeenOf(path), removal: { method: 'manual', where: tilde(path, paths.home) },
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
return items;
|
|
130
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CodexStart, SessionRecord } from '../../core/types.js';
|
|
2
|
+
import type { Use } from '../claude-code/logs.js';
|
|
3
|
+
export interface CodexLogScan {
|
|
4
|
+
sessions: SessionRecord[];
|
|
5
|
+
skillUses: Map<string, Use[]>;
|
|
6
|
+
mcpUses: Map<string, Use[]>;
|
|
7
|
+
/** One per session that logged a world_state, oldest first; firstRequestTokens holds that session's only. */
|
|
8
|
+
starts: CodexStart[];
|
|
9
|
+
linesTotal: number;
|
|
10
|
+
linesUnknownShape: number;
|
|
11
|
+
linesUnreadable: number;
|
|
12
|
+
unknownByVersion: Record<string, number>;
|
|
13
|
+
versions: string[];
|
|
14
|
+
}
|
|
15
|
+
/** Skills a tool call opened: Codex reads a skill by reading its SKILL.md. Plugin skills are "<plugin>:<skill>". */
|
|
16
|
+
export declare function skillsIn(text: string): string[];
|
|
17
|
+
/** MCP servers a tool call used: "mcp__<server>__<tool>" as the tool name, or called from Codex's code mode. */
|
|
18
|
+
export declare function mcpServersIn(name: unknown, input: string): string[];
|
|
19
|
+
export declare function scanCodexLogs(dir: string, opts: {
|
|
20
|
+
projectOf: (cwd: string) => string | null;
|
|
21
|
+
since?: string;
|
|
22
|
+
}): Promise<CodexLogScan>;
|