claude-code-session-manager 0.35.2 → 0.35.4

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 (39) hide show
  1. package/dist/assets/{TiptapBody-CIEmzy3k.js → TiptapBody-CVFG9ufh.js} +1 -1
  2. package/dist/assets/index-DASEOqMP.js +3558 -0
  3. package/dist/assets/index-DZgKJkf3.css +32 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +3 -2
  6. package/plugins/session-manager-dev/.claude-plugin/plugin.json +3 -1
  7. package/plugins/session-manager-dev/skills/develop/SKILL.md +12 -0
  8. package/plugins/session-manager-dev/skills/develop/standards.md +1 -0
  9. package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +48 -35
  10. package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +83 -0
  11. package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +127 -0
  12. package/plugins/session-manager-dev/skills/my-feedback/SKILL.md +20 -12
  13. package/plugins/session-manager-dev/skills/optimize-kpi/SKILL.md +4 -3
  14. package/plugins/session-manager-dev/skills/process-feedback/SKILL.md +33 -12
  15. package/src/main/__tests__/adminServer.test.cjs +175 -0
  16. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +137 -0
  17. package/src/main/__tests__/mcpStatus.test.cjs +61 -0
  18. package/src/main/__tests__/runVerify.test.cjs +74 -4
  19. package/src/main/__tests__/scheduler-autofix-select.test.cjs +50 -2
  20. package/src/main/__tests__/scheduler-autopromote.test.cjs +19 -1
  21. package/src/main/adminServer.cjs +157 -0
  22. package/src/main/browserCapture.cjs +714 -0
  23. package/src/main/browserView.cjs +613 -0
  24. package/src/main/chatRunner.cjs +60 -0
  25. package/src/main/config.cjs +37 -1
  26. package/src/main/historyAggregator.cjs +92 -6
  27. package/src/main/index.cjs +37 -3
  28. package/src/main/ipcSchemas.cjs +106 -0
  29. package/src/main/lib/schedulerConfig.cjs +6 -2
  30. package/src/main/mcpStatus.cjs +93 -0
  31. package/src/main/runVerify.cjs +14 -1
  32. package/src/main/scheduler.cjs +159 -31
  33. package/src/main/sessionsStore.cjs +4 -3
  34. package/src/preload/api.d.ts +154 -5
  35. package/src/preload/browserViewPreload.cjs +67 -0
  36. package/src/preload/index.cjs +63 -5
  37. package/dist/assets/index-2Om-ouz6.js +0 -3534
  38. package/dist/assets/index-DeIJ8SM5.css +0 -32
  39. package/src/main/docEditor.cjs +0 -92
@@ -8,9 +8,10 @@
8
8
 
9
9
  const { test } = require('node:test');
10
10
  const assert = require('node:assert/strict');
11
- const { selectAutoFixTargets } = require('../scheduler.cjs');
11
+ const { selectAutoFixTargets, isUnresolvableNeedsReview } = require('../scheduler.cjs');
12
12
 
13
13
  const noSiblingOnDisk = () => false;
14
+ const noRunDir = () => null;
14
15
 
15
16
  function makeJob(overrides = {}) {
16
17
  return {
@@ -53,8 +54,55 @@ test('excludes a completed job', () => {
53
54
  assert.strictEqual(result.length, 0);
54
55
  });
55
56
 
56
- test('excludes a job missing runId', () => {
57
+ test('excludes a job missing runId when no run dir resolves', () => {
57
58
  const jobs = [makeJob({ runId: null })];
59
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk, resolveJobRunId: noRunDir });
60
+ assert.strictEqual(result.length, 0);
61
+ });
62
+
63
+ test('selects a job missing runId when a run dir resolves (gap 1: runId backfill)', () => {
64
+ const jobs = [makeJob({ runId: null })];
65
+ const result = selectAutoFixTargets(jobs, {
66
+ fixSlugExists: noSiblingOnDisk,
67
+ resolveJobRunId: () => '2026-06-16T10-00-00-000Z',
68
+ });
69
+ assert.strictEqual(result.length, 1);
70
+ });
71
+
72
+ test('isUnresolvableNeedsReview: needs_review + no runId + no run dir → true', () => {
73
+ const job = makeJob({ runId: null });
74
+ assert.strictEqual(isUnresolvableNeedsReview(job, { hasRunDir: false }), true);
75
+ });
76
+
77
+ test('isUnresolvableNeedsReview: needs_review + no runId + has run dir → false', () => {
78
+ const job = makeJob({ runId: null });
79
+ assert.strictEqual(isUnresolvableNeedsReview(job, { hasRunDir: true }), false);
80
+ });
81
+
82
+ test('isUnresolvableNeedsReview: needs_review + runId present → false', () => {
83
+ const job = makeJob();
84
+ assert.strictEqual(isUnresolvableNeedsReview(job, { hasRunDir: false }), false);
85
+ });
86
+
87
+ test('isUnresolvableNeedsReview: non needs_review status → false', () => {
88
+ const job = makeJob({ runId: null, status: 'failed' });
89
+ assert.strictEqual(isUnresolvableNeedsReview(job, { hasRunDir: false }), false);
90
+ });
91
+
92
+ test('autoFixOutcome no-plan with retries=0 is retried', () => {
93
+ const jobs = [makeJob({ autoFixAttempted: true, autoFixOutcome: 'no-plan', autoFixRetries: 0 })];
94
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
95
+ assert.strictEqual(result.length, 1);
96
+ });
97
+
98
+ test('autoFixOutcome no-plan with retries=1 is exhausted (not selected)', () => {
99
+ const jobs = [makeJob({ autoFixAttempted: true, autoFixOutcome: 'no-plan', autoFixRetries: 1 })];
100
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
101
+ assert.strictEqual(result.length, 0);
102
+ });
103
+
104
+ test('autoFixAttempted true with no outcome recorded stays excluded (existing 1-attempt cap)', () => {
105
+ const jobs = [makeJob({ autoFixAttempted: true })];
58
106
  const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
59
107
  assert.strictEqual(result.length, 0);
60
108
  });
@@ -8,7 +8,7 @@
8
8
 
9
9
  const { test } = require('node:test');
10
10
  const assert = require('node:assert/strict');
11
- const { isPromotableOriginal } = require('../scheduler.cjs');
11
+ const { isPromotableOriginal, healTargetForFix } = require('../scheduler.cjs');
12
12
 
13
13
  test('isPromotableOriginal: failed → true', () => {
14
14
  assert.strictEqual(isPromotableOriginal('failed'), true);
@@ -30,4 +30,22 @@ test('isPromotableOriginal: pending → false', () => {
30
30
  assert.strictEqual(isPromotableOriginal('pending'), false);
31
31
  });
32
32
 
33
+ test('healTargetForFix: matches the specific original by full numeric-prefixed slug, not just base', () => {
34
+ const jobs = [
35
+ { slug: '451-foo', status: 'needs_review' },
36
+ { slug: '453-foo', status: 'needs_review' },
37
+ ];
38
+ const target = healTargetForFix('451-fix-foo', jobs);
39
+ assert.strictEqual(target.slug, '451-foo');
40
+ });
41
+
42
+ test('healTargetForFix: returns null when no promotable original matches', () => {
43
+ const jobs = [
44
+ { slug: '451-foo', status: 'completed' },
45
+ { slug: '453-foo', status: 'needs_review' },
46
+ ];
47
+ const target = healTargetForFix('451-fix-foo', jobs);
48
+ assert.strictEqual(target, null);
49
+ });
50
+
33
51
  console.log('scheduler-autopromote tests: PASS');
@@ -0,0 +1,157 @@
1
+ /**
2
+ * adminServer.cjs — loopback-only HTTP admin API for the scheduler.
3
+ *
4
+ * Foundation for PRD 449 (an MCP server wraps this HTTP API). Exposes a
5
+ * narrow, token-authed surface so a same-machine script/tool can reset a
6
+ * stuck scheduler job or list jobs without hand-editing queue.json from
7
+ * outside the running app (which would race the in-process mutate() queue
8
+ * — see scheduler.cjs's serialized-mutation comment).
9
+ *
10
+ * Security posture:
11
+ * - Binds 127.0.0.1 only, OS-assigned ephemeral port. Never reachable
12
+ * off-box.
13
+ * - Bearer token, regenerated every app boot, written to
14
+ * ~/.claude/session-manager/admin-api.json with 0600 perms.
15
+ * - Token compared with crypto.timingSafeEqual to avoid a timing
16
+ * side-channel on the comparison itself.
17
+ * - Only two routes: reset-job (one narrow mutation) and jobs (read-only
18
+ * list). writePrd/pause/resume are intentionally NOT exposed here.
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const http = require('node:http');
24
+ const os = require('node:os');
25
+ const path = require('node:path');
26
+ const crypto = require('node:crypto');
27
+ const fsp = require('node:fs/promises');
28
+ const config = require('./config.cjs');
29
+
30
+ const TOKEN_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'admin-api.json');
31
+
32
+ function timingSafeEqualStrings(a, b) {
33
+ const bufA = Buffer.from(String(a ?? ''));
34
+ const bufB = Buffer.from(String(b ?? ''));
35
+ if (bufA.length !== bufB.length) {
36
+ // Compare against a same-length buffer anyway so the failure path still
37
+ // takes constant time relative to the (fixed) token length, rather than
38
+ // returning early on a length mismatch.
39
+ crypto.timingSafeEqual(bufA, bufA);
40
+ return false;
41
+ }
42
+ return crypto.timingSafeEqual(bufA, bufB);
43
+ }
44
+
45
+ function readBody(req, maxBytes = 1024 * 1024) {
46
+ return new Promise((resolve, reject) => {
47
+ let total = 0;
48
+ const chunks = [];
49
+ req.on('data', (chunk) => {
50
+ total += chunk.length;
51
+ if (total > maxBytes) {
52
+ reject(new Error('body too large'));
53
+ req.destroy();
54
+ return;
55
+ }
56
+ chunks.push(chunk);
57
+ });
58
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
59
+ req.on('error', reject);
60
+ });
61
+ }
62
+
63
+ function sendJson(res, status, obj) {
64
+ const body = JSON.stringify(obj);
65
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
66
+ res.end(body);
67
+ }
68
+
69
+ /**
70
+ * Create the admin HTTP server. `remote` is scheduler.cjs's remote object
71
+ * (injected, not required directly, so this module stays testable without
72
+ * booting Electron).
73
+ */
74
+ function createAdminServer(remote) {
75
+ let server = null;
76
+ let token = null;
77
+
78
+ async function ensureToken() {
79
+ token = crypto.randomBytes(32).toString('hex');
80
+ await config.writeJson(TOKEN_PATH, { port: null, token });
81
+ try { await fsp.chmod(TOKEN_PATH, 0o600); } catch { /* best-effort on platforms without POSIX perms */ }
82
+ return token;
83
+ }
84
+
85
+ async function persistPort(port) {
86
+ await config.writeJson(TOKEN_PATH, { port, token });
87
+ try { await fsp.chmod(TOKEN_PATH, 0o600); } catch { /* */ }
88
+ }
89
+
90
+ function authorized(req) {
91
+ const header = req.headers.authorization || '';
92
+ const match = /^Bearer (.+)$/.exec(header);
93
+ if (!match) return false;
94
+ return timingSafeEqualStrings(match[1], token);
95
+ }
96
+
97
+ async function handleRequest(req, res) {
98
+ if (!authorized(req)) {
99
+ sendJson(res, 401, { ok: false, error: 'unauthorized' });
100
+ return;
101
+ }
102
+ try {
103
+ if (req.method === 'GET' && req.url === '/admin/scheduler/jobs') {
104
+ const jobs = await remote.listJobs();
105
+ sendJson(res, 200, jobs);
106
+ return;
107
+ }
108
+ if (req.method === 'POST' && req.url === '/admin/scheduler/reset-job') {
109
+ const raw = await readBody(req);
110
+ let parsed;
111
+ try {
112
+ parsed = raw ? JSON.parse(raw) : {};
113
+ } catch {
114
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
115
+ return;
116
+ }
117
+ const slug = typeof parsed.slug === 'string' ? parsed.slug : null;
118
+ if (!slug) {
119
+ sendJson(res, 400, { ok: false, error: 'missing slug' });
120
+ return;
121
+ }
122
+ const result = await remote.resetJob(slug);
123
+ sendJson(res, 200, result);
124
+ return;
125
+ }
126
+ sendJson(res, 404, { ok: false, error: 'not found' });
127
+ } catch (e) {
128
+ sendJson(res, 500, { ok: false, error: e?.message ?? 'internal error' });
129
+ }
130
+ }
131
+
132
+ async function start() {
133
+ await ensureToken();
134
+ server = http.createServer((req, res) => {
135
+ handleRequest(req, res).catch(() => {
136
+ try { sendJson(res, 500, { ok: false, error: 'internal error' }); } catch { /* */ }
137
+ });
138
+ });
139
+ await new Promise((resolve, reject) => {
140
+ server.once('error', reject);
141
+ server.listen(0, '127.0.0.1', resolve);
142
+ });
143
+ const { port } = server.address();
144
+ await persistPort(port);
145
+ return { port, token };
146
+ }
147
+
148
+ async function stop() {
149
+ if (!server) return;
150
+ await new Promise((resolve) => server.close(() => resolve()));
151
+ server = null;
152
+ }
153
+
154
+ return { start, stop, get token() { return token; }, get server() { return server; } };
155
+ }
156
+
157
+ module.exports = { createAdminServer, TOKEN_PATH };