copilot-usage-studio 0.2.0 → 0.2.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 +16 -1
- package/README.md +42 -13
- package/data/github-copilot-pricing.json +34 -6
- package/dist/copilot-usage-studio/browser/chunk-2SAR2Q6M.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-5JYQJM5G.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-7FKLWMFH.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-EWN3YOHT.js +1 -0
- package/dist/copilot-usage-studio/browser/{chunk-NXH37FKU.js → chunk-KS5W2HMH.js} +1 -1
- package/dist/copilot-usage-studio/browser/{chunk-TVSYR63W.js → chunk-OS2XPCVW.js} +2 -2
- package/dist/copilot-usage-studio/browser/{chunk-QQOFM3HF.js → chunk-U4H425LY.js} +1 -1
- package/dist/copilot-usage-studio/browser/chunk-VEKXIM47.js +1 -0
- package/dist/copilot-usage-studio/browser/chunk-WOIYAEKB.js +4 -0
- package/dist/copilot-usage-studio/browser/{chunk-J47KKACI.js → chunk-XHID4XVE.js} +1 -1
- package/dist/copilot-usage-studio/browser/index.html +2 -2
- package/dist/copilot-usage-studio/browser/main-OZIMQRXC.js +1 -0
- package/dist/copilot-usage-studio/browser/styles-7RDDQXQV.css +1 -0
- package/docs/local-deployment.md +67 -11
- package/docs/pricing.md +3 -3
- package/docs/scanner-api.md +28 -0
- package/lib/cli.mjs +15 -1
- package/lib/local-runtime.mjs +455 -19
- package/lib/scanner-worker.mjs +25 -0
- package/package.json +17 -1
- package/scripts/pricing-utils.mjs +5 -0
- package/scripts/scan-vscode-sessions.mjs +383 -1475
- package/scripts/scanner-customization-evidence.mjs +576 -0
- package/scripts/scanner-customization-inventory.mjs +1021 -0
- package/scripts/scanner-memory.mjs +229 -0
- package/scripts/scanner-session-parser.mjs +1237 -0
- package/scripts/scanner-traversal.mjs +203 -0
- package/scripts/scanner-workspace.mjs +209 -0
- package/dist/copilot-usage-studio/browser/chunk-CUKTUQ6W.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-EYAHOEVS.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-FXNLFSUB.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-HJNYITFH.js +0 -1
- package/dist/copilot-usage-studio/browser/chunk-KDAJN6DF.js +0 -4
- package/dist/copilot-usage-studio/browser/main-TL27ZSEQ.js +0 -1
- package/dist/copilot-usage-studio/browser/styles-GZOQ5QIH.css +0 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { basename, extname, relative, resolve, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const defaultMemoryFileLimit = 5000;
|
|
6
|
+
const defaultMemoryFileSizeLimit = 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
function decodeMemorySessionId(value) {
|
|
9
|
+
try {
|
|
10
|
+
const decoded = Buffer.from(String(value), 'base64').toString('utf8');
|
|
11
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(decoded)
|
|
12
|
+
? decoded
|
|
13
|
+
: '';
|
|
14
|
+
} catch {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function memoryTitle(content, file) {
|
|
20
|
+
const heading = String(content)
|
|
21
|
+
.split(/\r?\n/)
|
|
22
|
+
.map((line) => line.match(/^#{1,3}\s+(.+?)\s*#*$/)?.[1]?.trim())
|
|
23
|
+
.find(Boolean);
|
|
24
|
+
|
|
25
|
+
if (heading) {
|
|
26
|
+
return heading.slice(0, 160);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return basename(file, extname(file))
|
|
30
|
+
.replace(/[-_]+/g, ' ')
|
|
31
|
+
.replace(/\b\w/g, (character) => character.toUpperCase())
|
|
32
|
+
.slice(0, 160);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function memoryExcerpt(content) {
|
|
36
|
+
return String(content)
|
|
37
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
38
|
+
.replace(/[`*_>~-]+/g, ' ')
|
|
39
|
+
.replace(/\s+/g, ' ')
|
|
40
|
+
.trim()
|
|
41
|
+
.slice(0, 280);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeMemoryVirtualPath(value) {
|
|
45
|
+
const path = String(value ?? '').trim().replace(/\\/g, '/');
|
|
46
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function virtualPathForMemory(memory) {
|
|
50
|
+
const segments = String(memory.relativePath ?? '').split(/[\\/]+/).filter(Boolean);
|
|
51
|
+
|
|
52
|
+
if (memory.scope === 'session') {
|
|
53
|
+
return normalizeMemoryVirtualPath(`/memories/session/${segments.slice(1).join('/')}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return normalizeMemoryVirtualPath(`/memories/${segments.join('/')}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createMemoryScanner(context = {}) {
|
|
60
|
+
const diagnostics = () => context.diagnostics?.() ?? context.diagnostics ?? { warnings: [] };
|
|
61
|
+
const listFilesRecursive = (...args) => context.listFilesRecursive?.(...args) ?? [];
|
|
62
|
+
const listDebugLogFiles = (...args) => context.listDebugLogFiles?.(...args) ?? [];
|
|
63
|
+
const readJsonl = (...args) => context.readJsonl?.(...args) ?? [];
|
|
64
|
+
const safeJson = (...args) => context.safeJson?.(...args) ?? null;
|
|
65
|
+
const timestampForEvent = (...args) => context.timestampForEvent?.(...args) ?? '';
|
|
66
|
+
const llmTokenFields = (...args) => context.llmTokenFields?.(...args) ?? {
|
|
67
|
+
inputTokens: 0,
|
|
68
|
+
cachedInputTokens: 0,
|
|
69
|
+
outputTokens: 0,
|
|
70
|
+
};
|
|
71
|
+
const normalizeModel = (...args) => context.normalizeModel?.(...args) ?? '';
|
|
72
|
+
const memoryFileLimit = context.memoryFileLimit ?? defaultMemoryFileLimit;
|
|
73
|
+
const memoryFileSizeLimit = context.memoryFileSizeLimit ?? defaultMemoryFileSizeLimit;
|
|
74
|
+
|
|
75
|
+
function memoryFromFile(file, root, source, workspace) {
|
|
76
|
+
try {
|
|
77
|
+
const stats = statSync(file);
|
|
78
|
+
if (stats.size > memoryFileSizeLimit) {
|
|
79
|
+
diagnostics().skippedOversizedMemories += 1;
|
|
80
|
+
diagnostics().warnings.push(`${file}: memory skipped because it exceeds 1 MiB.`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const content = readFileSync(file, 'utf8');
|
|
85
|
+
const relativePath = relative(root, file);
|
|
86
|
+
const segments = relativePath.split(sep).filter(Boolean);
|
|
87
|
+
const sessionId = source === 'workspace' ? decodeMemorySessionId(segments[0]) : '';
|
|
88
|
+
const kind = basename(file).toLowerCase() === 'plan.md' ? 'plan' : 'memory';
|
|
89
|
+
const scope = source === 'global'
|
|
90
|
+
? 'global'
|
|
91
|
+
: segments[0]?.toLowerCase() === 'repo'
|
|
92
|
+
? 'repository'
|
|
93
|
+
: sessionId
|
|
94
|
+
? 'session'
|
|
95
|
+
: 'workspace';
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
id: createHash('sha256').update(resolve(file)).digest('hex').slice(0, 24),
|
|
99
|
+
kind,
|
|
100
|
+
scope,
|
|
101
|
+
title: memoryTitle(content, file),
|
|
102
|
+
excerpt: memoryExcerpt(content),
|
|
103
|
+
content,
|
|
104
|
+
workspace: source === 'global' ? '' : workspace,
|
|
105
|
+
sessionId,
|
|
106
|
+
sourcePath: resolve(file),
|
|
107
|
+
relativePath,
|
|
108
|
+
createdAt: stats.birthtimeMs > 0 ? stats.birthtime.toISOString() : '',
|
|
109
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
110
|
+
sizeBytes: stats.size,
|
|
111
|
+
characterCount: content.length,
|
|
112
|
+
lineCount: content ? content.split(/\r?\n/).length : 0,
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
diagnostics().skippedUnreadableMemories += 1;
|
|
116
|
+
diagnostics().warnings.push(`${file}: memory skipped: ${error.message}`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function memoriesFromRoot(root, source, workspace = '') {
|
|
122
|
+
if (!existsSync(root)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
diagnostics().scannedMemoryRoots += 1;
|
|
127
|
+
const memories = listFilesRecursive(
|
|
128
|
+
root,
|
|
129
|
+
(file) => extname(file).toLowerCase() === '.md',
|
|
130
|
+
memoryFileLimit,
|
|
131
|
+
{ label: 'memory', maxDepth: 8, maxDirs: 2500 },
|
|
132
|
+
)
|
|
133
|
+
.map((file) => memoryFromFile(file, root, source, workspace))
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
|
|
136
|
+
diagnostics().importedMemories += memories.length;
|
|
137
|
+
diagnostics().importedPlans += memories.filter((memory) => memory.kind === 'plan').length;
|
|
138
|
+
return memories;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function memoryRecallsFromDebugLog(sessionDir, workspace = '') {
|
|
142
|
+
const sessionId = basename(sessionDir);
|
|
143
|
+
const recalls = [];
|
|
144
|
+
|
|
145
|
+
for (const file of listDebugLogFiles(sessionDir)) {
|
|
146
|
+
const events = readJsonl(file);
|
|
147
|
+
let modelCallNumber = 0;
|
|
148
|
+
const modelCallNumbers = new Map();
|
|
149
|
+
|
|
150
|
+
events.forEach((event, index) => {
|
|
151
|
+
if (event.type === 'llm_request') {
|
|
152
|
+
modelCallNumber += 1;
|
|
153
|
+
modelCallNumbers.set(index, modelCallNumber);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
events.forEach((event, index) => {
|
|
158
|
+
if (event.type !== 'tool_call' || event.name !== 'memory') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const args = safeJson(event.attrs?.args) ?? {};
|
|
163
|
+
if (args.command !== 'view' || !args.path) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const nextModelIndex = events.findIndex(
|
|
168
|
+
(candidate, candidateIndex) => candidateIndex > index && candidate.type === 'llm_request',
|
|
169
|
+
);
|
|
170
|
+
const nextModel = nextModelIndex >= 0 ? events[nextModelIndex] : null;
|
|
171
|
+
const tokenFields = nextModel ? llmTokenFields(nextModel) : null;
|
|
172
|
+
const sourceLog = basename(file);
|
|
173
|
+
|
|
174
|
+
recalls.push({
|
|
175
|
+
id: createHash('sha256')
|
|
176
|
+
.update(`${resolve(file)}:${index}:${args.path}`)
|
|
177
|
+
.digest('hex')
|
|
178
|
+
.slice(0, 24),
|
|
179
|
+
sessionId,
|
|
180
|
+
workspace,
|
|
181
|
+
virtualPath: normalizeMemoryVirtualPath(args.path),
|
|
182
|
+
timestamp: timestampForEvent(event),
|
|
183
|
+
sourceLog,
|
|
184
|
+
returnedCharacterCount: String(event.attrs?.result ?? '').length,
|
|
185
|
+
...(nextModel
|
|
186
|
+
? {
|
|
187
|
+
followingModelCall: {
|
|
188
|
+
number: modelCallNumbers.get(nextModelIndex) ?? 0,
|
|
189
|
+
model: normalizeModel(nextModel.attrs?.model),
|
|
190
|
+
inputTokens: tokenFields.inputTokens,
|
|
191
|
+
cachedInputTokens: tokenFields.cachedInputTokens,
|
|
192
|
+
outputTokens: tokenFields.outputTokens,
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return recalls.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
memoriesFromRoot,
|
|
205
|
+
memoryRecallsFromDebugLog,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function attachMemoryRecalls(memories, sessions) {
|
|
210
|
+
const recalls = sessions.flatMap((session) => session.memoryRecalls ?? []);
|
|
211
|
+
|
|
212
|
+
return memories.map((memory) => {
|
|
213
|
+
const virtualPath = virtualPathForMemory(memory);
|
|
214
|
+
const matchingRecalls = recalls.filter((recall) => {
|
|
215
|
+
if (recall.virtualPath !== virtualPath) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (memory.scope === 'session') {
|
|
219
|
+
return memory.sessionId === recall.sessionId;
|
|
220
|
+
}
|
|
221
|
+
if (memory.scope === 'repository' || memory.scope === 'workspace') {
|
|
222
|
+
return memory.workspace === recall.workspace;
|
|
223
|
+
}
|
|
224
|
+
return memory.scope === 'global';
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return matchingRecalls.length ? { ...memory, recalls: matchingRecalls } : memory;
|
|
228
|
+
});
|
|
229
|
+
}
|