claude-code-session-manager 0.81.0 → 0.82.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.
Files changed (34) hide show
  1. package/dist/assets/{AgentLibrary-psZYVM2w.js → AgentLibrary-pwlkAFb3.js} +1 -1
  2. package/dist/assets/{DataModel-BMied5pg.js → DataModel-BZK9PXFD.js} +1 -1
  3. package/dist/assets/{History-BFC0oaKc.js → History-BVRjxJjS.js} +1 -1
  4. package/dist/assets/{Hooks-CTLfO9G8.js → Hooks-CNuwHeGx.js} +1 -1
  5. package/dist/assets/{HostBilko-D4I0Cpwn.js → HostBilko-cjwNodhV.js} +1 -1
  6. package/dist/assets/{Library-BTzS8KsS.js → Library-YPNm9W92.js} +1 -1
  7. package/dist/assets/{ListDetail-CuRmT008.js → ListDetail-CY4GM1Om.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-BgQGtvWo.js → MarkdownEditor-BF4y2Jiz.js} +1 -1
  9. package/dist/assets/{McpServers-DiVQUK57.js → McpServers-CmBdWtX_.js} +1 -1
  10. package/dist/assets/{Memory-DSu15JmL.js → Memory-CvkIXNl1.js} +1 -1
  11. package/dist/assets/{Panel-CD5wxGSR.js → Panel-D93o-sxe.js} +1 -1
  12. package/dist/assets/{Permissions-C_tAE6yJ.js → Permissions-DQipg16I.js} +1 -1
  13. package/dist/assets/{Plugins-kwI7W-eK.js → Plugins-B6NwzPfK.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-CQceOgsH.js → ProvenanceBadge-DACVJhrB.js} +1 -1
  15. package/dist/assets/{SaveBar-BDk5e3Pp.js → SaveBar-Cg4lbChb.js} +1 -1
  16. package/dist/assets/{Scheduler-DRnvEYzv.js → Scheduler-Dr5ZcBLe.js} +1 -1
  17. package/dist/assets/{ScopeSwitcher-CeifaOlq.js → ScopeSwitcher-C-locvy0.js} +1 -1
  18. package/dist/assets/{Settings-BWQ1Utop.js → Settings-BVrAle90.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-CW6e1SW1.js → SkillReferenceGraph-DDzuYgSK.js} +1 -1
  20. package/dist/assets/{Skills-BLwFB0E4.js → Skills-DFvhiOAQ.js} +1 -1
  21. package/dist/assets/{SystemPrompt-DC4ZTArJ.js → SystemPrompt-8PiTyUyL.js} +1 -1
  22. package/dist/assets/{TagLibrary-pMeGfjUb.js → TagLibrary-DruUYaAc.js} +1 -1
  23. package/dist/assets/{TiptapBody-BHFid2pZ.js → TiptapBody-Dr4a--42.js} +1 -1
  24. package/dist/assets/{Toggle-D9eYoZh4.js → Toggle-B_EH2TFb.js} +1 -1
  25. package/dist/assets/{index-CuyM9vAP.js → index-DApB4DHS.js} +3 -3
  26. package/dist/assets/{settingsSchema-DrxC67uZ.js → settingsSchema-BKa-xk8g.js} +1 -1
  27. package/dist/index.html +1 -1
  28. package/package.json +1 -1
  29. package/src/main/__tests__/machineProfile.test.cjs +134 -0
  30. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +85 -0
  31. package/src/main/__tests__/telemetrySettings.test.cjs +178 -0
  32. package/src/main/lib/machineProfile.cjs +144 -0
  33. package/src/main/lib/telemetrySettings.cjs +171 -0
  34. package/src/main/scheduler.cjs +27 -3
@@ -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
+ };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * telemetrySettings — persists product-telemetry consent/config + install identity.
3
+ *
4
+ * Storage: ~/.config/session-manager/telemetry.json
5
+ * Shape: {
6
+ * enabled: boolean, // ON by default — hard kill switch is SM_TELEMETRY=0
7
+ * installId: string, // crypto.randomUUID(), minted once, the only identity
8
+ * endpoint: string, // bilko.run ingest base URL
9
+ * noticeAckedAt: string|null, // ISO timestamp the first-run disclosure was shown
10
+ * lastMachineReportAt: string|null,
11
+ * lastMachineReportVersion: string,
12
+ * lastDailyFlushAt: string|null,
13
+ * schemaVersion: 1
14
+ * }
15
+ *
16
+ * Modeled directly on ../otelSettings.cjs — same atomic tmp+rename write via config.cjs,
17
+ * same schemaVersion + isValid() + frozen DEFAULTS shape. Deliberately has NO identity
18
+ * field beyond installId — no email, no username, no hostname. isValid() rejects any
19
+ * unknown key so a future accidental PII field can never silently round-trip through load/save.
20
+ *
21
+ * This module is pure data plumbing: no network code, no call sites. Later PRDs consume it.
22
+ */
23
+ 'use strict';
24
+
25
+ const fsp = require('node:fs/promises');
26
+ const path = require('node:path');
27
+ const os = require('node:os');
28
+ const crypto = require('node:crypto');
29
+ const config = require('../config.cjs');
30
+
31
+ const SCHEMA_VERSION = 1;
32
+ const DAY_MS = 24 * 60 * 60 * 1000;
33
+ const THIRTY_DAYS_MS = 30 * DAY_MS;
34
+
35
+ const DEFAULTS = Object.freeze({
36
+ enabled: true,
37
+ installId: '',
38
+ endpoint: 'https://bilko.run',
39
+ noticeAckedAt: null,
40
+ lastMachineReportAt: null,
41
+ lastMachineReportVersion: '',
42
+ lastDailyFlushAt: null,
43
+ schemaVersion: SCHEMA_VERSION,
44
+ });
45
+
46
+ const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
47
+
48
+ function storePath() {
49
+ return path.join(os.homedir(), '.config', 'session-manager', 'telemetry.json');
50
+ }
51
+
52
+ function isValid(cfg) {
53
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) return false;
54
+ for (const k of Object.keys(cfg)) {
55
+ if (!KNOWN_KEYS.has(k)) return false;
56
+ }
57
+ if (typeof cfg.enabled !== 'boolean') return false;
58
+ if (typeof cfg.installId !== 'string') return false;
59
+ if (typeof cfg.endpoint !== 'string' || !cfg.endpoint.trim()) return false;
60
+ if (cfg.noticeAckedAt !== null && typeof cfg.noticeAckedAt !== 'string') return false;
61
+ if (cfg.lastMachineReportAt !== null && typeof cfg.lastMachineReportAt !== 'string') return false;
62
+ if (typeof cfg.lastMachineReportVersion !== 'string') return false;
63
+ if (cfg.lastDailyFlushAt !== null && typeof cfg.lastDailyFlushAt !== 'string') return false;
64
+ if (cfg.schemaVersion !== SCHEMA_VERSION) return false;
65
+ return true;
66
+ }
67
+
68
+ function normalize(cfg) {
69
+ return {
70
+ enabled: !!cfg.enabled,
71
+ installId: typeof cfg.installId === 'string' ? cfg.installId : '',
72
+ endpoint: typeof cfg.endpoint === 'string' && cfg.endpoint.trim() ? cfg.endpoint.trim() : DEFAULTS.endpoint,
73
+ noticeAckedAt: typeof cfg.noticeAckedAt === 'string' ? cfg.noticeAckedAt : null,
74
+ lastMachineReportAt: typeof cfg.lastMachineReportAt === 'string' ? cfg.lastMachineReportAt : null,
75
+ lastMachineReportVersion: typeof cfg.lastMachineReportVersion === 'string' ? cfg.lastMachineReportVersion : '',
76
+ lastDailyFlushAt: typeof cfg.lastDailyFlushAt === 'string' ? cfg.lastDailyFlushAt : null,
77
+ schemaVersion: SCHEMA_VERSION,
78
+ };
79
+ }
80
+
81
+ let writeQueue = Promise.resolve();
82
+ async function save(cfg) {
83
+ if (!isValid(cfg)) throw new Error('Invalid telemetry config');
84
+ const next = normalize(cfg);
85
+ const run = async () => {
86
+ await config.writeTextAtomic(storePath(), JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
87
+ return next;
88
+ };
89
+ const tail = writeQueue.then(run, run);
90
+ writeQueue = tail.catch(() => {});
91
+ return tail;
92
+ }
93
+
94
+ async function readRaw() {
95
+ try {
96
+ const raw = await fsp.readFile(storePath(), 'utf8');
97
+ const data = JSON.parse(raw);
98
+ if (data && typeof data === 'object') {
99
+ return normalize({ ...DEFAULTS, ...data });
100
+ }
101
+ } catch (e) {
102
+ if (e.code !== 'ENOENT') {
103
+ console.warn('[telemetrySettings] load failed:', e.message);
104
+ }
105
+ }
106
+ return { ...DEFAULTS };
107
+ }
108
+
109
+ /**
110
+ * Loads the persisted config, minting and persisting installId on first read
111
+ * when absent. A second in-process load() and a fresh load() from disk both
112
+ * observe the same minted id, since the mint result is written to disk
113
+ * before this resolves.
114
+ */
115
+ async function load() {
116
+ const cfg = await readRaw();
117
+ if (!cfg.installId) {
118
+ return save({ ...cfg, installId: crypto.randomUUID() });
119
+ }
120
+ return cfg;
121
+ }
122
+
123
+ /** Hard kill switch (SM_TELEMETRY=0) always wins over the persisted value. */
124
+ function isEnabled(cfg) {
125
+ if (process.env.SM_TELEMETRY === '0') return false;
126
+ return !!(cfg && cfg.enabled);
127
+ }
128
+
129
+ /** Env override always wins over the persisted endpoint. */
130
+ function resolveEndpoint(cfg) {
131
+ if (process.env.SM_TELEMETRY_ENDPOINT) return process.env.SM_TELEMETRY_ENDPOINT;
132
+ return (cfg && typeof cfg.endpoint === 'string' && cfg.endpoint.trim()) ? cfg.endpoint : DEFAULTS.endpoint;
133
+ }
134
+
135
+ /** True when lastDailyFlushAt is null or more than 24h before `now`. `now` is injected, never read internally. */
136
+ function isDailyFlushDue(cfg, now) {
137
+ const last = cfg && cfg.lastDailyFlushAt;
138
+ if (!last) return true;
139
+ const lastMs = new Date(last).getTime();
140
+ if (Number.isNaN(lastMs)) return true;
141
+ return (now - lastMs) > DAY_MS;
142
+ }
143
+
144
+ /**
145
+ * True when the install has changed app version since its last report, OR
146
+ * lastMachineReportAt is null / more than 30 days before `now` (periodic
147
+ * liveness heartbeat — what turns the data into an active-install count
148
+ * rather than an ever-installed count). `now` is injected, never read internally.
149
+ */
150
+ function isMachineReportDue(cfg, { now, appVersion }) {
151
+ if (!cfg) return true;
152
+ if (cfg.lastMachineReportVersion !== appVersion) return true;
153
+ const last = cfg.lastMachineReportAt;
154
+ if (!last) return true;
155
+ const lastMs = new Date(last).getTime();
156
+ if (Number.isNaN(lastMs)) return true;
157
+ return (now - lastMs) > THIRTY_DAYS_MS;
158
+ }
159
+
160
+ module.exports = {
161
+ load,
162
+ save,
163
+ storePath,
164
+ isValid,
165
+ isEnabled,
166
+ resolveEndpoint,
167
+ isDailyFlushDue,
168
+ isMachineReportDue,
169
+ DEFAULTS,
170
+ SCHEMA_VERSION,
171
+ };
@@ -7410,6 +7410,27 @@ function isRescanCandidate(job) {
7410
7410
  return false;
7411
7411
  }
7412
7412
 
7413
+ /**
7414
+ * Cheap-guard for the 10-minute periodic reverify tick. MUST be expressed in
7415
+ * terms of isRescanCandidate — not a hand-written status test — because the
7416
+ * two drifted once already: the guard only looked for a `needs_review` row
7417
+ * while isRescanCandidate had grown a `failed`/isFailedUnverifiedShaped arm,
7418
+ * so reverifyNeedsReview's failed → needs_review looksDone branch was
7419
+ * unreachable from the periodic path on any queue holding zero needs_review
7420
+ * rows. A `failed` row with no run log sat wedged for five days with the
7421
+ * self-heal that exists to inspect it never firing (only an app restart, via
7422
+ * the unguarded boot call, could reach it). Reported 2026-09-10 from
7423
+ * social-signals-trader.
7424
+ *
7425
+ * Cost: one extra classifyRunOutcome() log stat per `failed` row, and only
7426
+ * for rows that reach isFailedUnverifiedShaped's log check — bounded by the
7427
+ * failed-row count, and strictly cheaper than firing the whole pass (which
7428
+ * does git fetches via computeLooksDone) on every tick.
7429
+ */
7430
+ function shouldRunPeriodicReverify(jobs) {
7431
+ return Array.isArray(jobs) && jobs.some((j) => isRescanCandidate(j));
7432
+ }
7433
+
7413
7434
  /**
7414
7435
  * Self-healing pass over needs_review jobs. The verifier runs in-process, so a
7415
7436
  * fix to runVerify.cjs only takes effect for jobs verified AFTER an app
@@ -8356,14 +8377,16 @@ async function init() {
8356
8377
  const s = readQueueSync();
8357
8378
  // Periodic self-heal: re-run the verifier over stale needs_review jobs so a
8358
8379
  // job whose work actually landed (committed in-window, no FAIL sentinel)
8359
- // auto-clears WITHOUT waiting for the next app restart. Cheap-guarded — the
8360
- // log scan only runs when something is actually flagged. Kill-switch:
8380
+ // auto-clears WITHOUT waiting for the next app restart. Cheap-guarded by
8381
+ // shouldRunPeriodicReverify, which reuses isRescanCandidate so the guard
8382
+ // and the candidate filter can never drift apart again (they did once —
8383
+ // see that function's comment). Kill-switch:
8361
8384
  // SM_REVERIFY_PERIODIC_DISABLE=1 (boot reverify above stays always-on).
8362
8385
  // reverifyNeedsReview's auto-fix loop is capped downstream by
8363
8386
  // MAX_CONCURRENT_INVESTIGATIONS (spawnInvestigation queues/early-returns
8364
8387
  // past it), so this interval firing cannot fan out investigations.
8365
8388
  if (process.env.SM_REVERIFY_PERIODIC_DISABLE !== '1') {
8366
- if (s.jobs.some((j) => j.status === 'needs_review')) {
8389
+ if (shouldRunPeriodicReverify(s.jobs)) {
8367
8390
  reverifyNeedsReview().catch(() => {});
8368
8391
  }
8369
8392
  // A quarantined row only ever promotes to 'pending' through
@@ -9146,6 +9169,7 @@ module.exports = {
9146
9169
  memoryLimitedBatchSize,
9147
9170
  availableForJobs,
9148
9171
  reverifyNeedsReview,
9172
+ shouldRunPeriodicReverify,
9149
9173
  isRescanCandidate,
9150
9174
  isFailedUnverifiedShaped,
9151
9175
  computeLooksDone,