claude-code-session-manager 0.81.0 → 0.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentLibrary-psZYVM2w.js → AgentLibrary-B-OM-4gk.js} +1 -1
- package/dist/assets/{DataModel-BMied5pg.js → DataModel-BC6ppcBr.js} +1 -1
- package/dist/assets/{History-BFC0oaKc.js → History-D3Qw2BWn.js} +1 -1
- package/dist/assets/{Hooks-CTLfO9G8.js → Hooks-OxBQ0nIc.js} +1 -1
- package/dist/assets/{HostBilko-D4I0Cpwn.js → HostBilko-BNFC6bNZ.js} +1 -1
- package/dist/assets/{Library-BTzS8KsS.js → Library-DAMyrUtY.js} +1 -1
- package/dist/assets/{ListDetail-CuRmT008.js → ListDetail-BwywrpvE.js} +1 -1
- package/dist/assets/{MarkdownEditor-BgQGtvWo.js → MarkdownEditor-HEwjN1bp.js} +1 -1
- package/dist/assets/{McpServers-DiVQUK57.js → McpServers-D1VZF7Ix.js} +1 -1
- package/dist/assets/{Memory-DSu15JmL.js → Memory-CgYJ_QqD.js} +1 -1
- package/dist/assets/{Panel-CD5wxGSR.js → Panel-dTNWC59Q.js} +1 -1
- package/dist/assets/{Permissions-C_tAE6yJ.js → Permissions-qI_NKi1b.js} +1 -1
- package/dist/assets/{Plugins-kwI7W-eK.js → Plugins-DZ0_CHiF.js} +2 -2
- package/dist/assets/{ProvenanceBadge-CQceOgsH.js → ProvenanceBadge-B2i_uxCu.js} +1 -1
- package/dist/assets/{SaveBar-BDk5e3Pp.js → SaveBar-4tNMCjUd.js} +1 -1
- package/dist/assets/{Scheduler-DRnvEYzv.js → Scheduler-CUen18Gc.js} +1 -1
- package/dist/assets/{ScopeSwitcher-CeifaOlq.js → ScopeSwitcher-Co3tkLTT.js} +1 -1
- package/dist/assets/{Settings-BWQ1Utop.js → Settings-DXVgKLUx.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-CW6e1SW1.js → SkillReferenceGraph-BKcHtMPc.js} +1 -1
- package/dist/assets/{Skills-BLwFB0E4.js → Skills-DHb4INtF.js} +1 -1
- package/dist/assets/{SystemPrompt-DC4ZTArJ.js → SystemPrompt-BjqFOHSk.js} +1 -1
- package/dist/assets/{TagLibrary-pMeGfjUb.js → TagLibrary-xLP5dJvh.js} +1 -1
- package/dist/assets/{TiptapBody-BHFid2pZ.js → TiptapBody-T-ULTjK4.js} +1 -1
- package/dist/assets/{Toggle-D9eYoZh4.js → Toggle-B-5M2hGe.js} +1 -1
- package/dist/assets/{index-CuyM9vAP.js → index-CzAxC432.js} +3 -3
- package/dist/assets/{settingsSchema-DrxC67uZ.js → settingsSchema-C_nsemcF.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/main/__tests__/machineProfile.test.cjs +134 -0
- package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +85 -0
- package/src/main/__tests__/telemetryClient.test.cjs +744 -0
- package/src/main/__tests__/telemetrySettings.test.cjs +178 -0
- package/src/main/lib/machineProfile.cjs +144 -0
- package/src/main/lib/telemetryClient.cjs +622 -0
- package/src/main/lib/telemetrySettings.cjs +171 -0
- package/src/main/scheduler.cjs +27 -3
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* telemetrySettings.test.cjs — unit tests for the telemetry consent/config store.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/telemetrySettings.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const fsp = require('node:fs/promises');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
const tmpDirs = [];
|
|
15
|
+
let originalHome;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
originalHome = process.env.HOME;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
if (originalHome !== undefined) process.env.HOME = originalHome;
|
|
23
|
+
delete process.env.SM_TELEMETRY;
|
|
24
|
+
delete process.env.SM_TELEMETRY_ENDPOINT;
|
|
25
|
+
while (tmpDirs.length) {
|
|
26
|
+
const d = tmpDirs.pop();
|
|
27
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function freshModule(home) {
|
|
32
|
+
process.env.HOME = home;
|
|
33
|
+
const modPath = require.resolve('../lib/telemetrySettings.cjs');
|
|
34
|
+
delete require.cache[modPath];
|
|
35
|
+
delete require.cache[require.resolve('../config.cjs')];
|
|
36
|
+
return require('../lib/telemetrySettings.cjs');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function mkHome() {
|
|
40
|
+
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-telemetry-home-'));
|
|
41
|
+
tmpDirs.push(dir);
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('DEFAULTS carries no identity-shaped key', async () => {
|
|
46
|
+
const home = await mkHome();
|
|
47
|
+
const telemetrySettings = await freshModule(home);
|
|
48
|
+
const keys = Object.keys(telemetrySettings.DEFAULTS);
|
|
49
|
+
const identityLike = keys.filter((k) => /email|user|name|host|account/i.test(k));
|
|
50
|
+
expect(identityLike).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('isValid rejects a config carrying an unknown extra identity-shaped key', async () => {
|
|
54
|
+
const home = await mkHome();
|
|
55
|
+
const telemetrySettings = await freshModule(home);
|
|
56
|
+
const base = { ...telemetrySettings.DEFAULTS, installId: 'abc' };
|
|
57
|
+
expect(telemetrySettings.isValid(base)).toBe(true);
|
|
58
|
+
const withExtra = { ...base, identifyEmail: 'someone@example.com' };
|
|
59
|
+
expect(telemetrySettings.isValid(withExtra)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('load() mints installId once via crypto.randomUUID and persists it', async () => {
|
|
63
|
+
const home = await mkHome();
|
|
64
|
+
const telemetrySettings = await freshModule(home);
|
|
65
|
+
|
|
66
|
+
const first = await telemetrySettings.load();
|
|
67
|
+
expect(typeof first.installId).toBe('string');
|
|
68
|
+
expect(first.installId.length).toBeGreaterThan(0);
|
|
69
|
+
|
|
70
|
+
const second = await telemetrySettings.load();
|
|
71
|
+
expect(second.installId).toBe(first.installId);
|
|
72
|
+
|
|
73
|
+
const raw = JSON.parse(fs.readFileSync(telemetrySettings.storePath(), 'utf8'));
|
|
74
|
+
expect(raw.installId).toBe(first.installId);
|
|
75
|
+
|
|
76
|
+
const freshRead = await freshModule(home);
|
|
77
|
+
const third = await freshRead.load();
|
|
78
|
+
expect(third.installId).toBe(first.installId);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('isEnabled: hard kill switch SM_TELEMETRY=0 overrides persisted enabled:true', async () => {
|
|
82
|
+
const home = await mkHome();
|
|
83
|
+
const telemetrySettings = await freshModule(home);
|
|
84
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', enabled: true };
|
|
85
|
+
process.env.SM_TELEMETRY = '0';
|
|
86
|
+
expect(telemetrySettings.isEnabled(cfg)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('isEnabled: returns false when persisted enabled is false', async () => {
|
|
90
|
+
const home = await mkHome();
|
|
91
|
+
const telemetrySettings = await freshModule(home);
|
|
92
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', enabled: false };
|
|
93
|
+
expect(telemetrySettings.isEnabled(cfg)).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('isEnabled: returns true when persisted enabled is true and no kill switch', async () => {
|
|
97
|
+
const home = await mkHome();
|
|
98
|
+
const telemetrySettings = await freshModule(home);
|
|
99
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', enabled: true };
|
|
100
|
+
expect(telemetrySettings.isEnabled(cfg)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('resolveEndpoint prefers SM_TELEMETRY_ENDPOINT over persisted endpoint', async () => {
|
|
104
|
+
const home = await mkHome();
|
|
105
|
+
const telemetrySettings = await freshModule(home);
|
|
106
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', endpoint: 'https://persisted.example' };
|
|
107
|
+
process.env.SM_TELEMETRY_ENDPOINT = 'https://env-override.example';
|
|
108
|
+
expect(telemetrySettings.resolveEndpoint(cfg)).toBe('https://env-override.example');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('resolveEndpoint falls back to persisted endpoint when no env override', async () => {
|
|
112
|
+
const home = await mkHome();
|
|
113
|
+
const telemetrySettings = await freshModule(home);
|
|
114
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', endpoint: 'https://persisted.example' };
|
|
115
|
+
expect(telemetrySettings.resolveEndpoint(cfg)).toBe('https://persisted.example');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('isDailyFlushDue: true when lastDailyFlushAt is null', async () => {
|
|
119
|
+
const home = await mkHome();
|
|
120
|
+
const telemetrySettings = await freshModule(home);
|
|
121
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastDailyFlushAt: null };
|
|
122
|
+
expect(telemetrySettings.isDailyFlushDue(cfg, Date.now())).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('isDailyFlushDue: false when lastDailyFlushAt is 23h before now', async () => {
|
|
126
|
+
const home = await mkHome();
|
|
127
|
+
const telemetrySettings = await freshModule(home);
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastDailyFlushAt: new Date(now - 23 * 60 * 60 * 1000).toISOString() };
|
|
130
|
+
expect(telemetrySettings.isDailyFlushDue(cfg, now)).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('isDailyFlushDue: true when lastDailyFlushAt is 25h before now', async () => {
|
|
134
|
+
const home = await mkHome();
|
|
135
|
+
const telemetrySettings = await freshModule(home);
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastDailyFlushAt: new Date(now - 25 * 60 * 60 * 1000).toISOString() };
|
|
138
|
+
expect(telemetrySettings.isDailyFlushDue(cfg, now)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('isMachineReportDue: true when app version changed', async () => {
|
|
142
|
+
const home = await mkHome();
|
|
143
|
+
const telemetrySettings = await freshModule(home);
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const cfg = {
|
|
146
|
+
...telemetrySettings.DEFAULTS,
|
|
147
|
+
installId: 'x',
|
|
148
|
+
lastMachineReportVersion: '0.80.0',
|
|
149
|
+
lastMachineReportAt: new Date(now).toISOString(),
|
|
150
|
+
};
|
|
151
|
+
expect(telemetrySettings.isMachineReportDue(cfg, { now, appVersion: '0.81.0' })).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('isMachineReportDue: true when lastMachineReportAt is null (same version)', async () => {
|
|
155
|
+
const home = await mkHome();
|
|
156
|
+
const telemetrySettings = await freshModule(home);
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastMachineReportVersion: '0.81.0', lastMachineReportAt: null };
|
|
159
|
+
expect(telemetrySettings.isMachineReportDue(cfg, { now, appVersion: '0.81.0' })).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('isMachineReportDue: true when lastMachineReportAt is more than 30 days before now (same version)', async () => {
|
|
163
|
+
const home = await mkHome();
|
|
164
|
+
const telemetrySettings = await freshModule(home);
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const thirtyOneDaysAgo = new Date(now - 31 * 24 * 60 * 60 * 1000).toISOString();
|
|
167
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastMachineReportVersion: '0.81.0', lastMachineReportAt: thirtyOneDaysAgo };
|
|
168
|
+
expect(telemetrySettings.isMachineReportDue(cfg, { now, appVersion: '0.81.0' })).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('isMachineReportDue: false when same version and reported less than 30 days ago', async () => {
|
|
172
|
+
const home = await mkHome();
|
|
173
|
+
const telemetrySettings = await freshModule(home);
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000).toISOString();
|
|
176
|
+
const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastMachineReportVersion: '0.81.0', lastMachineReportAt: oneDayAgo };
|
|
177
|
+
expect(telemetrySettings.isMachineReportDue(cfg, { now, appVersion: '0.81.0' })).toBe(false);
|
|
178
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* machineProfile — builds the "once per machine" telemetry payload: which
|
|
3
|
+
* build (appVersion) this install id is running, plus basic hardware/OS specs.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately anonymous: no hostname, no OS username, no homedir, no MAC
|
|
6
|
+
* address, no hardware serial, no absolute filesystem path, no IANA timezone
|
|
7
|
+
* name (offset only). machineDigest is a coarse hash of stable spec fields —
|
|
8
|
+
* a hardware-CLASS fingerprint, never a device id — so individual records
|
|
9
|
+
* can be joined back to a machine class without repeating the whole profile.
|
|
10
|
+
*
|
|
11
|
+
* Pure data plumbing: no network code, no call sites. Later PRDs consume it.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const crypto = require('node:crypto');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const claudeBin = require('./claudeBin.cjs');
|
|
19
|
+
|
|
20
|
+
function resolveElectronApp() {
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line global-require
|
|
23
|
+
const electron = require('electron');
|
|
24
|
+
if (electron && typeof electron === 'object' && typeof electron.app === 'object') {
|
|
25
|
+
return electron.app;
|
|
26
|
+
}
|
|
27
|
+
} catch { /* no electron runtime (plain vitest, or electron require resolved to a path string) */ }
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolvePackageVersion() {
|
|
32
|
+
try {
|
|
33
|
+
// src/main/lib/machineProfile.cjs -> src/main/lib -> src/main -> src -> repo root
|
|
34
|
+
const pkg = require(path.join(__dirname, '..', '..', '..', 'package.json'));
|
|
35
|
+
return (pkg && typeof pkg.version === 'string') ? pkg.version : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 'npx' when the app path lives inside an npm/npx cache dir, 'dev' when
|
|
43
|
+
* SM_DEV is set, else 'unknown'. Injectable so tests never depend on real
|
|
44
|
+
* process state.
|
|
45
|
+
*/
|
|
46
|
+
function resolveInstallChannel({ appPath = null, devFlag = !!process.env.SM_DEV } = {}) {
|
|
47
|
+
if (devFlag) return 'dev';
|
|
48
|
+
if (typeof appPath === 'string' && /[\\/](\.npm|_npx|npm-cache|npx-cache)[\\/]/i.test(appPath)) {
|
|
49
|
+
return 'npx';
|
|
50
|
+
}
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** sha256 of the concatenated stable spec fields, truncated to 12 hex chars. */
|
|
55
|
+
function computeMachineDigest(specs) {
|
|
56
|
+
const material = [
|
|
57
|
+
specs.platform,
|
|
58
|
+
specs.osRelease,
|
|
59
|
+
specs.arch,
|
|
60
|
+
specs.cpuModel || '',
|
|
61
|
+
String(specs.cpuCount),
|
|
62
|
+
String(specs.totalMemMb),
|
|
63
|
+
].join('|');
|
|
64
|
+
return crypto.createHash('sha256').update(material).digest('hex').slice(0, 12);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reads basic, non-identifying specs from node:os. Null-safe when os.cpus() is empty. */
|
|
68
|
+
function readSpecs(osModule) {
|
|
69
|
+
const cpus = osModule.cpus() || [];
|
|
70
|
+
const cpu0 = cpus[0] || null;
|
|
71
|
+
return {
|
|
72
|
+
platform: osModule.platform(),
|
|
73
|
+
osRelease: osModule.release(),
|
|
74
|
+
arch: osModule.arch(),
|
|
75
|
+
cpuModel: cpu0 && typeof cpu0.model === 'string' ? cpu0.model.trim() : null,
|
|
76
|
+
cpuSpeedMhz: cpu0 && typeof cpu0.speed === 'number' ? cpu0.speed : null,
|
|
77
|
+
cpuCount: cpus.length,
|
|
78
|
+
totalMemMb: Math.round(osModule.totalmem() / 1048576),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Builds the machine profile. All external dependencies are injectable so
|
|
84
|
+
* this is fully testable under plain vitest with no Electron runtime and no
|
|
85
|
+
* real Claude CLI.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} [opts]
|
|
88
|
+
* @param {object} [opts.osModule] - defaults to node:os
|
|
89
|
+
* @param {function} [opts.probeClaudeVersion] - defaults to claudeBin.probeClaudeVersion; must resolve to a string or null and never block startup
|
|
90
|
+
* @param {object|null} [opts.electronApp] - override for tests; when omitted, 'electron' is required lazily
|
|
91
|
+
* @param {string} [opts.firstSeenAt] - ISO timestamp; defaults to now (callers that track a persisted first-seen value should pass it)
|
|
92
|
+
*/
|
|
93
|
+
async function buildMachineProfile(opts = {}) {
|
|
94
|
+
const osModule = opts.osModule || os;
|
|
95
|
+
const probeClaudeVersion = opts.probeClaudeVersion || claudeBin.probeClaudeVersion;
|
|
96
|
+
const electronApp = Object.prototype.hasOwnProperty.call(opts, 'electronApp')
|
|
97
|
+
? opts.electronApp
|
|
98
|
+
: resolveElectronApp();
|
|
99
|
+
|
|
100
|
+
let appVersion = null;
|
|
101
|
+
let appPath = null;
|
|
102
|
+
if (electronApp) {
|
|
103
|
+
try { appVersion = electronApp.getVersion(); } catch { /* not ready / not a real app object */ }
|
|
104
|
+
try { appPath = electronApp.getAppPath(); } catch { /* not ready / not a real app object */ }
|
|
105
|
+
}
|
|
106
|
+
if (!appVersion) appVersion = resolvePackageVersion();
|
|
107
|
+
|
|
108
|
+
const specs = readSpecs(osModule);
|
|
109
|
+
const machineDigest = computeMachineDigest(specs);
|
|
110
|
+
|
|
111
|
+
let claudeCliVersion = null;
|
|
112
|
+
try {
|
|
113
|
+
claudeCliVersion = await probeClaudeVersion();
|
|
114
|
+
} catch {
|
|
115
|
+
claudeCliVersion = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
appVersion,
|
|
120
|
+
installChannel: resolveInstallChannel({ appPath, devFlag: !!process.env.SM_DEV }),
|
|
121
|
+
machineDigest,
|
|
122
|
+
platform: specs.platform,
|
|
123
|
+
osRelease: specs.osRelease,
|
|
124
|
+
arch: specs.arch,
|
|
125
|
+
cpuModel: specs.cpuModel,
|
|
126
|
+
cpuCount: specs.cpuCount,
|
|
127
|
+
cpuSpeedMhz: specs.cpuSpeedMhz,
|
|
128
|
+
totalMemMb: specs.totalMemMb,
|
|
129
|
+
nodeVersion: process.versions.node || null,
|
|
130
|
+
electronVersion: process.versions.electron || null,
|
|
131
|
+
chromeVersion: process.versions.chrome || null,
|
|
132
|
+
v8Version: process.versions.v8 || null,
|
|
133
|
+
claudeCliVersion,
|
|
134
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale || null,
|
|
135
|
+
timezoneOffsetMinutes: new Date().getTimezoneOffset(),
|
|
136
|
+
firstSeenAt: opts.firstSeenAt || new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
buildMachineProfile,
|
|
142
|
+
computeMachineDigest,
|
|
143
|
+
resolveInstallChannel,
|
|
144
|
+
};
|