@optimuslabs/harness-map-staging 1.5.16

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.
Files changed (88) hide show
  1. package/README.md +46 -0
  2. package/dist/apply_deferred_vscdb.js +38 -0
  3. package/dist/bootstrap_constants.js +15 -0
  4. package/dist/cli/bash_script_generator.js +95 -0
  5. package/dist/cli.js +144 -0
  6. package/dist/cli_invocation_match.js +30 -0
  7. package/dist/compliance_check_runner.js +57 -0
  8. package/dist/compliance_prompt_gate.js +396 -0
  9. package/dist/dialog_prefs_cli.js +109 -0
  10. package/dist/endpoint_client/compress_payload.js +25 -0
  11. package/dist/endpoint_client/http_transport.js +90 -0
  12. package/dist/endpoint_client/index.js +3 -0
  13. package/dist/endpoint_client/registry_api.js +41 -0
  14. package/dist/endpoint_client/startup_api.js +44 -0
  15. package/dist/endpoint_client/types.js +4 -0
  16. package/dist/execute_trusted_restarts.js +60 -0
  17. package/dist/log_config_files/auth/auth_flow.js +22 -0
  18. package/dist/log_config_files/auth/auth_key_store.js +14 -0
  19. package/dist/log_config_files/collection/claude_desktop_extensions_collector.js +248 -0
  20. package/dist/log_config_files/collection/claude_known_projects_collector.js +42 -0
  21. package/dist/log_config_files/collection/claude_token_usage_collector.js +203 -0
  22. package/dist/log_config_files/collection/codex_token_usage_collector.js +342 -0
  23. package/dist/log_config_files/collection/config_collector.js +245 -0
  24. package/dist/log_config_files/collection/copilot_token_usage_collector.js +167 -0
  25. package/dist/log_config_files/collection/cowork_desktop_version_collector.js +60 -0
  26. package/dist/log_config_files/collection/cowork_session_whitelist.js +89 -0
  27. package/dist/log_config_files/collection/cursor_project_mcp_collector.js +230 -0
  28. package/dist/log_config_files/collection/cursor_project_workspace_activity.js +73 -0
  29. package/dist/log_config_files/collection/cursor_token_usage_collector.js +192 -0
  30. package/dist/log_config_files/collection/directory_collector.js +126 -0
  31. package/dist/log_config_files/collection/enrichment_helpers.js +53 -0
  32. package/dist/log_config_files/collection/ensure_cursor_user_settings_snapshot.js +50 -0
  33. package/dist/log_config_files/collection/file_type_rules.js +47 -0
  34. package/dist/log_config_files/collection/grok_cli_version_collector.js +53 -0
  35. package/dist/log_config_files/collection/hermes_token_usage_collector.js +241 -0
  36. package/dist/log_config_files/collection/mcp_tool_collector.js +106 -0
  37. package/dist/log_config_files/collection/metadata_merge.js +43 -0
  38. package/dist/log_config_files/collection/openclaw_helpers.js +55 -0
  39. package/dist/log_config_files/collection/openclaw_token_usage_collector.js +223 -0
  40. package/dist/log_config_files/collection/opencode_token_usage_collector.js +177 -0
  41. package/dist/log_config_files/collection/pi_token_usage_collector.js +239 -0
  42. package/dist/log_config_files/collection/plugin_collector.js +223 -0
  43. package/dist/log_config_files/collection/plugin_version_helpers.js +37 -0
  44. package/dist/log_config_files/collection/skills_cli_collector.js +162 -0
  45. package/dist/log_config_files/index.js +19 -0
  46. package/dist/log_config_files/paths/path_constants_helpers.js +71 -0
  47. package/dist/log_config_files/paths/pattern_resolver.js +326 -0
  48. package/dist/log_config_files/readers/composer_shadow_merge.js +67 -0
  49. package/dist/log_config_files/readers/cursor_shadow_merge_ruleset.js +46 -0
  50. package/dist/log_config_files/readers/file_readers.js +196 -0
  51. package/dist/log_config_files/readers/vscdb_config_builder.js +148 -0
  52. package/dist/log_config_files/readers/vscdb_reactive_storage.js +62 -0
  53. package/dist/log_config_files/readers/vscdb_reader.js +337 -0
  54. package/dist/log_config_files/runtime/client_event_reporter.js +347 -0
  55. package/dist/log_config_files/runtime/compliance_check.js +1013 -0
  56. package/dist/log_config_files/runtime/compliance_session_log.js +378 -0
  57. package/dist/log_config_files/runtime/dialog_preferences.js +31 -0
  58. package/dist/log_config_files/runtime/hardware_uuid.js +36 -0
  59. package/dist/log_config_files/runtime/hook_logger.js +267 -0
  60. package/dist/log_config_files/runtime/hook_type_for_request.js +30 -0
  61. package/dist/log_config_files/runtime/log_metadata.js +122 -0
  62. package/dist/log_config_files/runtime/main_runner.js +376 -0
  63. package/dist/log_config_files/runtime/management_storage.js +144 -0
  64. package/dist/log_config_files/runtime/ops_target_path.js +18 -0
  65. package/dist/log_config_files/runtime/remediation_apply_tracking.js +152 -0
  66. package/dist/log_config_files/runtime/remediation_config_path.js +140 -0
  67. package/dist/log_config_files/runtime/remediation_sync.js +1705 -0
  68. package/dist/log_config_files/runtime/secret_regex_scan.js +126 -0
  69. package/dist/log_config_files/runtime/sqlite_binary.js +124 -0
  70. package/dist/log_config_files/runtime/trusted_restarts.js +113 -0
  71. package/dist/log_config_files/runtime/workspace_repo.js +170 -0
  72. package/dist/log_config_files/runtime/worktree_absent.js +50 -0
  73. package/dist/log_config_files/runtime/worktree_scanner.js +137 -0
  74. package/dist/log_config_files/sender/batch_sender.js +283 -0
  75. package/dist/log_config_files/sender/endpoint_config.js +24 -0
  76. package/dist/log_config_files/sender/signing.js +1 -0
  77. package/dist/log_sensitive_paths_audit.js +89 -0
  78. package/dist/log_uuid/auth_key_store.js +37 -0
  79. package/dist/log_uuid/hardware_uuid.js +35 -0
  80. package/dist/log_uuid/index.js +11 -0
  81. package/dist/log_uuid/log_uuid_helper.js +30 -0
  82. package/dist/log_uuid/startup_sender.js +131 -0
  83. package/dist/log_uuid/user_profile.js +192 -0
  84. package/dist/remediation_change_preview.js +182 -0
  85. package/dist/tofu.js +22 -0
  86. package/dist/tofu_environment.js +36 -0
  87. package/dist/types/config_file_types.js +1 -0
  88. package/package.json +67 -0
@@ -0,0 +1,44 @@
1
+ import { executeBody } from './http_transport.js';
2
+ import { gzipPayload } from './compress_payload.js';
3
+ export const postStartupPayload = async (endpointUrl, body, timeoutMs = 5000) => {
4
+ const payload = gzipPayload(JSON.stringify(body));
5
+ const { statusCode, statusMessage, body: responseBody } = await executeBody(endpointUrl, 'POST', payload, timeoutMs);
6
+ if (statusCode >= 400) {
7
+ const msg = statusCode === 413
8
+ ? `413 Request Entity Too Large: ${responseBody.substring(0, 200)}`
9
+ : `HTTP ${statusCode} ${statusMessage}: ${responseBody.substring(0, 200)}`;
10
+ throw new Error(msg);
11
+ }
12
+ if (!responseBody)
13
+ return { status: 'error', message: 'Empty response from endpoint' };
14
+ try {
15
+ return JSON.parse(responseBody);
16
+ }
17
+ catch (error) {
18
+ throw new Error(`Failed to parse endpoint response (${error.message}): ${responseBody}`);
19
+ }
20
+ };
21
+ export const patchPayload = async (endpointUrl, body, timeoutMs = 10000) => {
22
+ const payload = gzipPayload(JSON.stringify(body));
23
+ const { body: responseBody } = await executeBody(endpointUrl, 'PATCH', payload, timeoutMs);
24
+ if (!responseBody)
25
+ return { status: 'error', error: 'empty_response' };
26
+ try {
27
+ return JSON.parse(responseBody);
28
+ }
29
+ catch {
30
+ throw new Error(`Invalid JSON: ${responseBody.slice(0, 200)}`);
31
+ }
32
+ };
33
+ export const classifyEndpointResponse = (response) => {
34
+ if (response.status === 'key_issued' && response.key) {
35
+ return { branch: 'key_issued', message: 'Server issued a new symmetric key; store it for future signatures.', key: response.key, keyId: response.key_id };
36
+ }
37
+ if (response.status === 'accepted' && response.signature_valid) {
38
+ return { branch: 'accepted', message: 'Startup payload accepted and signature verified.' };
39
+ }
40
+ if (response.status === 'error' && response.error) {
41
+ return { branch: 'error', message: response.error };
42
+ }
43
+ return { branch: 'error', message: response.message || 'Endpoint returned an unexpected response.' };
44
+ };
@@ -0,0 +1,4 @@
1
+ /** Path suffix for file collection patterns (backend: api/file-path-registry/file-patterns/). */
2
+ export const FILE_PATH_REGISTRY_FILE_PATTERNS_PATH = '/api/file-path-registry/file-patterns/';
3
+ /** Path suffix for sensitive paths audit candidates (backend: api/file-path-registry/sensitive-paths-audit-candidates/). */
4
+ export const FILE_PATH_REGISTRY_SENSITIVE_PATHS_PATH = '/api/file-path-registry/sensitive-paths-audit-candidates/';
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI: read one JSON line from stdin (same shape as compliance_prompt_gate stdout for __optimus_autofix),
4
+ * parse restart_commands[], allowlist each in TS, spawn detached. Invoked by optimus-compliance-check.sh only.
5
+ * stderr stays quiet; failures logged via hookRunLog.
6
+ */
7
+ import { readFileSync } from 'node:fs';
8
+ import { isThisCliModule } from './cli_invocation_match.js';
9
+ import { appendComplianceRunnerLine, hookRunLog } from './log_config_files/runtime/hook_logger.js';
10
+ import { setActiveComplianceLogPath } from './log_config_files/runtime/compliance_session_log.js';
11
+ import { executeTrustedRestartCommands } from './log_config_files/runtime/trusted_restarts.js';
12
+ /** Invoked by dist entrypoint or by `npx --package=@optimuslabs/harness-map-staging@latest execute-trusted-restarts-staging` (cli.js dispatches here). */
13
+ export function runExecuteTrustedRestartsFromStdin() {
14
+ const complianceLogPath = process.env.OPTIMUS_COMPLIANCE_LOG?.trim();
15
+ if (complianceLogPath) {
16
+ setActiveComplianceLogPath(complianceLogPath);
17
+ }
18
+ let raw;
19
+ try {
20
+ raw = readFileSync(0, 'utf8');
21
+ }
22
+ catch {
23
+ const msg = 'execute_trusted_restarts: could not read stdin';
24
+ hookRunLog(msg);
25
+ appendComplianceRunnerLine('RESTART', msg);
26
+ return;
27
+ }
28
+ const first = raw.trim().split(/\r?\n/)[0]?.trim() ?? '';
29
+ if (!first) {
30
+ appendComplianceRunnerLine('RESTART', 'empty stdin — no restart_commands');
31
+ return;
32
+ }
33
+ let j;
34
+ try {
35
+ j = JSON.parse(first);
36
+ }
37
+ catch {
38
+ const msg = 'execute_trusted_restarts: stdin is not valid JSON';
39
+ hookRunLog(msg);
40
+ appendComplianceRunnerLine('RESTART', msg);
41
+ return;
42
+ }
43
+ const cmds = j.restart_commands;
44
+ if (!Array.isArray(cmds)) {
45
+ appendComplianceRunnerLine('RESTART', 'JSON missing restart_commands array — nothing to run');
46
+ return;
47
+ }
48
+ const strings = cmds.filter((c) => typeof c === 'string');
49
+ appendComplianceRunnerLine('RESTART', `stdin ok restart_commands=${strings.length}`);
50
+ executeTrustedRestartCommands(strings);
51
+ }
52
+ if (isThisCliModule(process.argv[1], import.meta.url)) {
53
+ try {
54
+ runExecuteTrustedRestartsFromStdin();
55
+ }
56
+ catch (e) {
57
+ hookRunLog(`execute_trusted_restarts: ${e instanceof Error ? e.message : String(e)}`);
58
+ }
59
+ process.exit(0);
60
+ }
@@ -0,0 +1,22 @@
1
+ import path from 'node:path';
2
+ import { postStartupPayload } from '../../endpoint_client/index.js';
3
+ import { ensureAuthentication as ensureTofuAuthentication } from '../../tofu.js';
4
+ import { loadEndpointBase } from '../sender/endpoint_config.js';
5
+ import { OPTIMUSLABS_MANAGEMENT_REL } from '../../bootstrap_constants.js';
6
+ import { hookRunLog } from '../runtime/hook_logger.js';
7
+ const AUTH_KEY_RELATIVE_PATH = path.join(OPTIMUSLABS_MANAGEMENT_REL, 'auth', 'auth_key.txt');
8
+ /** Ensure authentication — verify stored key or request new one via handshake. */
9
+ async function ensureAuthentication(hardwareUuid, endpointBase) {
10
+ const key = await ensureTofuAuthentication({
11
+ hardwareUuid,
12
+ endpointBase: endpointBase ?? loadEndpointBase(),
13
+ authRelativePath: AUTH_KEY_RELATIVE_PATH,
14
+ postJson: (endpointUrl, body) => postStartupPayload(endpointUrl, body),
15
+ onLog: (message) => {
16
+ if (message.startsWith('auth:'))
17
+ hookRunLog(message);
18
+ },
19
+ });
20
+ return key;
21
+ }
22
+ export { ensureAuthentication };
@@ -0,0 +1,14 @@
1
+ import path from 'node:path';
2
+ import { getAuthKeyPath as getSharedAuthKeyPath, readStoredAuthKey as readSharedStoredAuthKey, } from '../../tofu.js';
3
+ import { OPTIMUSLABS_MANAGEMENT_REL } from '../../bootstrap_constants.js';
4
+ const AUTH_KEY_RELATIVE_PATH = path.join(OPTIMUSLABS_MANAGEMENT_REL, 'auth', 'auth_key.txt');
5
+ const AUTH_KEY_OPTIONS = { authRelativePath: AUTH_KEY_RELATIVE_PATH };
6
+ /** Return absolute path to auth_key.txt, or null if home dir is unavailable. */
7
+ function getAuthKeyPath() {
8
+ return getSharedAuthKeyPath(AUTH_KEY_OPTIONS);
9
+ }
10
+ /** Read and parse the stored auth key, or return null if absent/unreadable. */
11
+ function readStoredAuthKey() {
12
+ return readSharedStoredAuthKey(AUTH_KEY_OPTIONS);
13
+ }
14
+ export { getAuthKeyPath, readStoredAuthKey };
@@ -0,0 +1,248 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { readJSONFile } from '../readers/file_readers.js';
5
+ function claudeAppSupportDir(home) {
6
+ if (process.platform === 'win32') {
7
+ const appData = process.env.APPDATA?.trim();
8
+ if (!appData)
9
+ return '';
10
+ return join(appData, 'Claude');
11
+ }
12
+ return join(home, 'Library', 'Application Support', 'Claude');
13
+ }
14
+ function claudeDesktopExtensionsDir(home) {
15
+ const base = claudeAppSupportDir(home);
16
+ return base ? join(base, 'Claude Extensions') : '';
17
+ }
18
+ function extensionsInstallationsPath(home) {
19
+ const base = claudeAppSupportDir(home);
20
+ return base ? join(base, 'extensions-installations.json') : '';
21
+ }
22
+ function claudeExtensionSettingsDir(home) {
23
+ const base = claudeAppSupportDir(home);
24
+ return base ? join(base, 'Claude Extensions Settings') : '';
25
+ }
26
+ /** Per-connector toggle: Claude Extensions Settings/<extension-id>.json → { isEnabled: boolean }. */
27
+ function readClaudeDesktopExtensionEnableSettings(home) {
28
+ const settingsDir = claudeExtensionSettingsDir(home);
29
+ if (!settingsDir || !existsSync(settingsDir))
30
+ return new Map();
31
+ const out = new Map();
32
+ let entries;
33
+ try {
34
+ entries = readdirSync(settingsDir, { withFileTypes: true }).filter((d) => d.isFile());
35
+ }
36
+ catch {
37
+ return out;
38
+ }
39
+ for (const dirent of entries) {
40
+ if (!dirent.name.endsWith('.json'))
41
+ continue;
42
+ const extId = dirent.name.slice(0, -'.json'.length);
43
+ if (!extId)
44
+ continue;
45
+ const raw = readJSONFile(join(settingsDir, dirent.name));
46
+ if (!raw || typeof raw !== 'object')
47
+ continue;
48
+ const isEnabled = raw.isEnabled;
49
+ if (typeof isEnabled === 'boolean') {
50
+ out.set(extId, isEnabled);
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+ function applyExtensionEnableSettingsToRawContent(rawContent, settings) {
56
+ if (settings.size === 0)
57
+ return 0;
58
+ const extensions = rawContent.extensions;
59
+ if (!extensions || typeof extensions !== 'object')
60
+ return 0;
61
+ let applied = 0;
62
+ for (const [extId, enabled] of settings) {
63
+ const ext = extensions[extId];
64
+ if (!ext || typeof ext !== 'object')
65
+ continue;
66
+ ext.isEnabled = enabled;
67
+ applied += 1;
68
+ }
69
+ return applied;
70
+ }
71
+ /**
72
+ * Upload each Claude Extensions Settings/*.json as its own config file (raw on-disk JSON).
73
+ * Do not merge isEnabled into extensions-installations.json — that mutates the payload
74
+ * vs the source file and creates spurious inventory audit diffs.
75
+ */
76
+ function collectClaudeDesktopExtensionSettingsFiles(home = homedir()) {
77
+ const settingsDir = claudeExtensionSettingsDir(home);
78
+ if (!settingsDir || !existsSync(settingsDir))
79
+ return [];
80
+ let entries;
81
+ try {
82
+ entries = readdirSync(settingsDir, { withFileTypes: true }).filter((d) => d.isFile());
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ const results = [];
88
+ for (const dirent of entries) {
89
+ if (!dirent.name.endsWith('.json'))
90
+ continue;
91
+ const filePath = join(settingsDir, dirent.name);
92
+ const raw = readJSONFile(filePath);
93
+ if (!raw || typeof raw !== 'object')
94
+ continue;
95
+ results.push({
96
+ file_type: 'claude_extension_settings',
97
+ file_path: filePath,
98
+ raw_content: raw,
99
+ });
100
+ }
101
+ return results;
102
+ }
103
+ /**
104
+ * @deprecated Do not call during collection — mutates extensions-installations upload payloads.
105
+ * isEnabled is resolved server-side from claude_extension_settings uploads.
106
+ */
107
+ function mergeClaudeDesktopExtensionEnableSettings(configFiles, home = homedir()) {
108
+ const settings = readClaudeDesktopExtensionEnableSettings(home);
109
+ if (settings.size === 0) {
110
+ return { applied: 0, settingsCount: 0 };
111
+ }
112
+ let applied = 0;
113
+ for (const entry of configFiles) {
114
+ if (entry.file_type !== 'claude_extensions_installations')
115
+ continue;
116
+ if (!entry.raw_content || typeof entry.raw_content !== 'object')
117
+ continue;
118
+ const extensions = entry.raw_content.extensions;
119
+ if (!extensions || typeof extensions !== 'object')
120
+ continue;
121
+ applied += applyExtensionEnableSettingsToRawContent(entry.raw_content, settings);
122
+ }
123
+ return { applied, settingsCount: settings.size };
124
+ }
125
+ function normalizePathForMatch(path) {
126
+ return path.replace(/\\/g, '/');
127
+ }
128
+ function isExtensionsInstallationsUpload(entry) {
129
+ if (entry.file_type !== 'claude_extensions_installations')
130
+ return false;
131
+ return normalizePathForMatch(entry.file_path).includes('extensions-installations.json');
132
+ }
133
+ function registryExtensionIds(home) {
134
+ const path = extensionsInstallationsPath(home);
135
+ if (!path || !existsSync(path))
136
+ return new Set();
137
+ const raw = readJSONFile(path);
138
+ if (!raw || typeof raw !== 'object')
139
+ return new Set();
140
+ const extensions = raw.extensions;
141
+ if (!extensions || typeof extensions !== 'object')
142
+ return new Set();
143
+ return new Set(Object.keys(extensions));
144
+ }
145
+ function manifestHasMcpConfig(manifest) {
146
+ const server = manifest.server;
147
+ if (!server || typeof server !== 'object')
148
+ return false;
149
+ const mcpConfig = server.mcp_config;
150
+ return mcpConfig !== null && typeof mcpConfig === 'object';
151
+ }
152
+ function collectDiskOnlyClaudeDesktopExtensions(home, skipIds) {
153
+ const extensionsDir = claudeDesktopExtensionsDir(home);
154
+ if (!extensionsDir || !existsSync(extensionsDir))
155
+ return {};
156
+ const out = {};
157
+ let dirs;
158
+ try {
159
+ dirs = readdirSync(extensionsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
160
+ }
161
+ catch {
162
+ return out;
163
+ }
164
+ for (const dirent of dirs) {
165
+ const extId = dirent.name;
166
+ if (skipIds.has(extId))
167
+ continue;
168
+ const manifestPath = join(extensionsDir, extId, 'manifest.json');
169
+ if (!existsSync(manifestPath))
170
+ continue;
171
+ const manifest = readJSONFile(manifestPath);
172
+ if (!manifest || typeof manifest !== 'object')
173
+ continue;
174
+ const manifestObj = manifest;
175
+ if (!manifestHasMcpConfig(manifestObj))
176
+ continue;
177
+ const version = typeof manifestObj.version === 'string' || typeof manifestObj.version === 'number'
178
+ ? String(manifestObj.version)
179
+ : undefined;
180
+ out[extId] = {
181
+ id: extId,
182
+ version,
183
+ manifest: manifestObj,
184
+ source: 'claude_extensions_directory',
185
+ };
186
+ }
187
+ return out;
188
+ }
189
+ /**
190
+ * Merge disk-only Desktop extensions into the extensions-installations.json upload
191
+ * so MCP inventory uses the same ingest path as registry-listed extensions (e.g. iMessage).
192
+ */
193
+ function enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, home = homedir()) {
194
+ const skipIds = new Set();
195
+ for (const id of registryExtensionIds(home))
196
+ skipIds.add(id);
197
+ const registryUpload = configFiles.find(isExtensionsInstallationsUpload);
198
+ if (registryUpload?.raw_content && typeof registryUpload.raw_content === 'object') {
199
+ const extensions = registryUpload.raw_content.extensions;
200
+ if (extensions && typeof extensions === 'object') {
201
+ for (const id of Object.keys(extensions))
202
+ skipIds.add(id);
203
+ }
204
+ }
205
+ const diskOnly = collectDiskOnlyClaudeDesktopExtensions(home, skipIds);
206
+ const diskOnlyIds = Object.keys(diskOnly);
207
+ if (diskOnlyIds.length === 0) {
208
+ return { mergedCount: 0, hadRegistryUpload: Boolean(registryUpload) };
209
+ }
210
+ if (registryUpload) {
211
+ const raw = registryUpload.raw_content;
212
+ if (!raw.extensions || typeof raw.extensions !== 'object')
213
+ raw.extensions = {};
214
+ Object.assign(raw.extensions, diskOnly);
215
+ return { mergedCount: diskOnlyIds.length, hadRegistryUpload: true };
216
+ }
217
+ const registryPath = extensionsInstallationsPath(home);
218
+ if (!registryPath)
219
+ return { mergedCount: 0, hadRegistryUpload: false };
220
+ configFiles.push({
221
+ file_type: 'claude_extensions_installations',
222
+ file_path: registryPath,
223
+ raw_content: { extensions: diskOnly },
224
+ });
225
+ return { mergedCount: diskOnlyIds.length, hadRegistryUpload: false };
226
+ }
227
+ /**
228
+ * Fallback when extensions-installations.json is not part of the upload batch: one synthetic
229
+ * claude_extensions_installations payload per disk-only manifest path.
230
+ */
231
+ function collectClaudeDesktopExtensionManifests(home = homedir()) {
232
+ const extensionsDir = claudeDesktopExtensionsDir(home);
233
+ if (!extensionsDir || !existsSync(extensionsDir))
234
+ return [];
235
+ const inRegistry = registryExtensionIds(home);
236
+ const results = [];
237
+ const diskOnly = collectDiskOnlyClaudeDesktopExtensions(home, inRegistry);
238
+ for (const [extId, entry] of Object.entries(diskOnly)) {
239
+ const manifestPath = join(extensionsDir, extId, 'manifest.json');
240
+ results.push({
241
+ file_type: 'claude_extensions_installations',
242
+ file_path: manifestPath,
243
+ raw_content: { extensions: { [extId]: entry } },
244
+ });
245
+ }
246
+ return results;
247
+ }
248
+ export { claudeAppSupportDir, claudeDesktopExtensionsDir, claudeExtensionSettingsDir, collectClaudeDesktopExtensionSettingsFiles, collectClaudeDesktopExtensionManifests, collectDiskOnlyClaudeDesktopExtensions, enrichClaudeDesktopExtensionsInstallationsUpload, mergeClaudeDesktopExtensionEnableSettings, readClaudeDesktopExtensionEnableSettings, };
@@ -0,0 +1,42 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { readJSONFile } from '../readers/file_readers.js';
5
+ /**
6
+ * Collect project-level `.mcp.json` for each path listed under `projects` in ~/.claude.json.
7
+ * Claude Code records known project directories there; MCP config lives at `<project>/.mcp.json`.
8
+ */
9
+ function collectMcpFromClaudeJsonProjects(home = homedir()) {
10
+ const claudeJsonPath = join(home, '.claude.json');
11
+ if (!existsSync(claudeJsonPath))
12
+ return [];
13
+ const raw = readJSONFile(claudeJsonPath);
14
+ if (!raw || typeof raw !== 'object')
15
+ return [];
16
+ const projects = raw.projects;
17
+ if (!projects || typeof projects !== 'object')
18
+ return [];
19
+ const seenPaths = new Set();
20
+ const results = [];
21
+ for (const projectPath of Object.keys(projects)) {
22
+ if (!projectPath || typeof projectPath !== 'string')
23
+ continue;
24
+ const trimmed = projectPath.trim();
25
+ if (!trimmed)
26
+ continue;
27
+ const mcpPath = join(trimmed, '.mcp.json');
28
+ if (seenPaths.has(mcpPath) || !existsSync(mcpPath))
29
+ continue;
30
+ const content = readJSONFile(mcpPath);
31
+ if (content === null)
32
+ continue;
33
+ seenPaths.add(mcpPath);
34
+ results.push({
35
+ file_type: 'claude_plugin_mcp',
36
+ file_path: mcpPath,
37
+ raw_content: typeof content === 'object' ? content : { content, source: 'file' },
38
+ });
39
+ }
40
+ return results;
41
+ }
42
+ export { collectMcpFromClaudeJsonProjects };
@@ -0,0 +1,203 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ /**
5
+ * Token-usage / session / CLI-version aggregator for Claude Code.
6
+ *
7
+ * Claude Code writes one JSONL transcript per session under ~/.claude/projects/**. Each
8
+ * assistant line carries a `message.usage` block ({input,output,cache_*}_tokens), and every
9
+ * line carries the CLI `version` and `sessionId`. We aggregate the *numbers* (and version /
10
+ * model ids) on the endpoint and emit only those — never transcript content — so the backend
11
+ * gets token usage, session count, and a reliable CLI version without ingesting prompts.
12
+ *
13
+ * Bounded by mtime recency so a large transcript history (can be hundreds of MB) does not get
14
+ * fully re-parsed on every hook run; this yields "recent activity" usage and the current version.
15
+ */
16
+ const TOKEN_USAGE_RECENCY_DAYS = 30;
17
+ const TOKEN_USAGE_FILE_TYPE = 'claude_token_usage';
18
+ function emptyTotals() {
19
+ return {
20
+ input_tokens: 0,
21
+ output_tokens: 0,
22
+ cache_creation_input_tokens: 0,
23
+ cache_read_input_tokens: 0,
24
+ };
25
+ }
26
+ function addUsage(totals, usage) {
27
+ for (const key of Object.keys(totals)) {
28
+ const value = readTokenField(usage, key);
29
+ if (value > 0)
30
+ totals[key] += value;
31
+ }
32
+ }
33
+ /** Read one token field, preferring the nested 5m+1h cache-creation sum over the flat field. */
34
+ function readTokenField(usage, key) {
35
+ if (key === 'cache_creation_input_tokens') {
36
+ const nested = usage.cache_creation;
37
+ if (nested && typeof nested === 'object') {
38
+ const n = nested;
39
+ const has5m = typeof n.ephemeral_5m_input_tokens === 'number' && Number.isFinite(n.ephemeral_5m_input_tokens);
40
+ const has1h = typeof n.ephemeral_1h_input_tokens === 'number' && Number.isFinite(n.ephemeral_1h_input_tokens);
41
+ // Fall back to the flat field only when the nested block is truly absent — a
42
+ // legitimately-zero nested sum (both fields present as 0) must not be overridden by it.
43
+ if (has5m || has1h) {
44
+ return (has5m ? n.ephemeral_5m_input_tokens : 0) + (has1h ? n.ephemeral_1h_input_tokens : 0);
45
+ }
46
+ }
47
+ }
48
+ const value = usage[key];
49
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
50
+ }
51
+ /** Sum the token fields of one usage block (used for per-model attribution and dedup tiebreaks). */
52
+ function usageSum(usage) {
53
+ let sum = 0;
54
+ for (const key of ['input_tokens', 'output_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']) {
55
+ sum += readTokenField(usage, key);
56
+ }
57
+ return sum;
58
+ }
59
+ /** Recursively collect *.jsonl files under a directory whose mtime is within the recency window. */
60
+ function recentJsonlFiles(dir, cutoffMs) {
61
+ const out = [];
62
+ let entries;
63
+ try {
64
+ entries = readdirSync(dir, { withFileTypes: true });
65
+ }
66
+ catch {
67
+ return out;
68
+ }
69
+ for (const entry of entries) {
70
+ const full = join(dir, entry.name);
71
+ if (entry.isDirectory()) {
72
+ out.push(...recentJsonlFiles(full, cutoffMs));
73
+ }
74
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
75
+ try {
76
+ if (statSync(full).mtimeMs >= cutoffMs)
77
+ out.push(full);
78
+ }
79
+ catch {
80
+ /* unreadable file — skip */
81
+ }
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+ /**
87
+ * Aggregate token usage / session count / version across recent Claude session transcripts.
88
+ * Returns a single-element array (one per-machine aggregate) or [] when there is no recent data.
89
+ */
90
+ function collectClaudeTokenUsage(home = homedir()) {
91
+ const projectsDir = join(home, '.claude', 'projects');
92
+ if (!existsSync(projectsDir))
93
+ return [];
94
+ const cutoffMs = Date.now() - TOKEN_USAGE_RECENCY_DAYS * 24 * 60 * 60 * 1000;
95
+ const files = recentJsonlFiles(projectsDir, cutoffMs);
96
+ if (files.length === 0)
97
+ return [];
98
+ const totals = emptyTotals();
99
+ const sessions = new Set();
100
+ const models = new Set();
101
+ const modelTokenSplit = {};
102
+ let version = '';
103
+ let versionAt = '';
104
+ let sawData = false;
105
+ // Claude Code writes one JSONL line per streaming snapshot of the same assistant message,
106
+ // so message.id/requestId repeat with a growing usage total; only the final snapshot is
107
+ // the true total. Dedup by (message.id, requestId), taking the per-field max across all
108
+ // snapshots for that key (order-independent — robust to out-of-order lines, and safe even
109
+ // if a snapshot has a genuinely-lower or reset value in one field while another snapshot in
110
+ // the same group peaks a different field). Entries without a message.id can't be deduped and
111
+ // are accumulated directly.
112
+ const dedupedUsage = new Map();
113
+ for (const file of files) {
114
+ let content;
115
+ try {
116
+ content = readFileSync(file, 'utf8');
117
+ }
118
+ catch {
119
+ continue;
120
+ }
121
+ for (const line of content.split('\n')) {
122
+ const trimmed = line.trim();
123
+ if (!trimmed)
124
+ continue;
125
+ let entry;
126
+ try {
127
+ entry = JSON.parse(trimmed);
128
+ }
129
+ catch {
130
+ continue;
131
+ }
132
+ sawData = true;
133
+ const sessionId = entry.sessionId;
134
+ if (typeof sessionId === 'string' && sessionId)
135
+ sessions.add(sessionId);
136
+ // Keep the version from the most recent line (timestamps are ISO-8601, sort lexically).
137
+ const lineVersion = entry.version;
138
+ const lineAt = typeof entry.timestamp === 'string' ? entry.timestamp : '';
139
+ if (typeof lineVersion === 'string' && lineVersion && lineAt >= versionAt) {
140
+ version = lineVersion;
141
+ versionAt = lineAt;
142
+ }
143
+ const message = entry.message;
144
+ if (message && typeof message === 'object') {
145
+ const msg = message;
146
+ // Skip Claude Code's internal placeholder model ids (e.g. "<synthetic>").
147
+ const model = typeof msg.model === 'string' && msg.model && !msg.model.startsWith('<') ? msg.model : '';
148
+ if (model)
149
+ models.add(model);
150
+ if (msg.usage && typeof msg.usage === 'object') {
151
+ const usage = msg.usage;
152
+ const messageId = typeof msg.id === 'string' ? msg.id : '';
153
+ if (messageId) {
154
+ const requestId = typeof entry.requestId === 'string' ? entry.requestId : '';
155
+ const dedupeKey = `${messageId} ${requestId}`;
156
+ const existing = dedupedUsage.get(dedupeKey) ?? { totals: emptyTotals(), model: '' };
157
+ for (const key of Object.keys(existing.totals)) {
158
+ const value = readTokenField(usage, key);
159
+ if (value > existing.totals[key])
160
+ existing.totals[key] = value;
161
+ }
162
+ if (model)
163
+ existing.model = model;
164
+ dedupedUsage.set(dedupeKey, existing);
165
+ }
166
+ else {
167
+ addUsage(totals, usage);
168
+ const sum = usageSum(usage);
169
+ if (model && sum > 0)
170
+ modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ for (const { totals: merged, model } of dedupedUsage.values()) {
177
+ let sum = 0;
178
+ for (const key of Object.keys(totals)) {
179
+ totals[key] += merged[key];
180
+ sum += merged[key];
181
+ }
182
+ if (model && sum > 0)
183
+ modelTokenSplit[model] = (modelTokenSplit[model] ?? 0) + sum;
184
+ }
185
+ if (!sawData)
186
+ return [];
187
+ return [
188
+ {
189
+ file_type: TOKEN_USAGE_FILE_TYPE,
190
+ // Stable synthetic path → one aggregate row per machine.
191
+ file_path: projectsDir,
192
+ raw_content: {
193
+ version,
194
+ session_count: sessions.size,
195
+ models: [...models].sort(),
196
+ model_token_split: modelTokenSplit,
197
+ window_days: TOKEN_USAGE_RECENCY_DAYS,
198
+ totals,
199
+ },
200
+ },
201
+ ];
202
+ }
203
+ export { collectClaudeTokenUsage, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE };