claude-code-session-manager 0.91.1 → 0.92.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/dist/assets/{AgentLibrary-Chmfxih8.js → AgentLibrary-Ci6u03bq.js} +1 -1
- package/dist/assets/{DataModel-Cp38ePS_.js → DataModel-BIqcjsv2.js} +1 -1
- package/dist/assets/{History-SMWV7lD6.js → History-BOR_fJNr.js} +1 -1
- package/dist/assets/{Hooks-JUWiyu-r.js → Hooks-BtD436iL.js} +1 -1
- package/dist/assets/{HostBilko-DFTixTqy.js → HostBilko-CMe5cH3H.js} +1 -1
- package/dist/assets/{Library-zUu71gsR.js → Library-DeKdY-tD.js} +1 -1
- package/dist/assets/{ListDetail-BK_PH4lf.js → ListDetail-CtVjrHKB.js} +1 -1
- package/dist/assets/{MarkdownEditor-ITJDD0Mr.js → MarkdownEditor-CbzC-Oqf.js} +1 -1
- package/dist/assets/{McpServers-CWwrfGkx.js → McpServers-DKN75Shn.js} +1 -1
- package/dist/assets/{Memory-DgHttor9.js → Memory-B68AJjg2.js} +1 -1
- package/dist/assets/{Panel-C2Cb7TS4.js → Panel-C2YTW-qP.js} +1 -1
- package/dist/assets/{Permissions-hd8OBsj_.js → Permissions-Cr0RRvNy.js} +1 -1
- package/dist/assets/{Plugins-BYa-ClGK.js → Plugins-DbZkJs5l.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Dmhz8ZOB.js → ProvenanceBadge-kJ__BOQv.js} +1 -1
- package/dist/assets/{SaveBar-1NvfbO9r.js → SaveBar-87ZJfscX.js} +1 -1
- package/dist/assets/{Scheduler-SYgINPCB.js → Scheduler-DdEaxku8.js} +1 -1
- package/dist/assets/{ScopeSwitcher-MBmWNzFk.js → ScopeSwitcher-CKzpqjJ_.js} +1 -1
- package/dist/assets/{Settings-C1WOHoYb.js → Settings-CjywVsNB.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-Cuse__ul.js → SkillReferenceGraph-D_-wg4r9.js} +1 -1
- package/dist/assets/{Skills-CQmLYgNh.js → Skills-Cd_yPO_l.js} +1 -1
- package/dist/assets/{SystemPrompt-DkhAYRXL.js → SystemPrompt-2z6eGPSF.js} +1 -1
- package/dist/assets/{TagLibrary-DGxI_3m3.js → TagLibrary-DcaWQSp4.js} +1 -1
- package/dist/assets/{TiptapBody-BY-fFjwh.js → TiptapBody-DYXIp8aG.js} +1 -1
- package/dist/assets/{Toggle--nwLR-_8.js → Toggle-CDXR5l3m.js} +1 -1
- package/dist/assets/{index-CRPJslSA.css → index-D3P_jldk.css} +1 -1
- package/dist/assets/{index-4dEn1t8J.js → index-D5H_H5wC.js} +329 -329
- package/dist/assets/{settingsSchema-DiqEpSBA.js → settingsSchema-JRDfjgHb.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +52 -3
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +105 -0
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +27 -4
- package/src/main/config.cjs +5 -0
- package/src/main/docEdit.cjs +3 -2
- package/src/main/index.cjs +2 -0
- package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +58 -2
- package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +79 -0
- package/src/main/lib/__tests__/updateCheck.test.cjs +63 -0
- package/src/main/lib/epicSpawnCwd.cjs +22 -13
- package/src/main/lib/epicSpawnPlan.cjs +27 -12
- package/src/main/lib/epicTranscriptPath.cjs +3 -2
- package/src/main/lib/epicWorktreeMerge.cjs +4 -4
- package/src/main/lib/updateCheck.cjs +102 -0
- package/src/main/pty.cjs +32 -6
- package/src/preload/api.d.ts +2 -0
- package/src/preload/index.cjs +1 -0
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* conversation found"), the flag-swap retry then hit `--session-id` ("already in use"), and both
|
|
9
9
|
* doors were locked.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* at the SAME recorded path (encoding unchanged), so the CLI sees the transcript again
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Three-way decision: (1) re-attach — resolveEpicSpawnCwd({restore:true}) re-attaches the Epic's
|
|
12
|
+
* own branch at the SAME recorded path (encoding unchanged), so the CLI sees the transcript again;
|
|
13
|
+
* (2) project-cwd fallback — when that spawn cwd cannot see the transcript but the project cwd can
|
|
14
|
+
* (transcript filed under encodeCwd(project cwd)), spawn from the project cwd; nothing forks since
|
|
15
|
+
* it writes to the directory it already occupies; (3) refuse BEFORE spawning with a message stating
|
|
16
|
+
* the real cause, never claim a flag that cannot work. Reachable = "some existing transcript lives
|
|
17
|
+
* under encodeCwd(candidate cwd)" and the candidate is a real directory.
|
|
16
18
|
*
|
|
17
19
|
* Also refuses requests routed to a closed (`completed`) Epic, and keeps a per-session circuit
|
|
18
20
|
* breaker: once a session is found unreachable, later requests are refused after ONE stat (is the
|
|
@@ -95,19 +97,32 @@ function planEpicSpawn({ cwd, claudeSessionId, fallbackResume = false, deps = {}
|
|
|
95
97
|
useResume = !!resolved.existsAnywhere;
|
|
96
98
|
} catch { /* keep the caller-supplied flag */ }
|
|
97
99
|
|
|
98
|
-
|
|
100
|
+
let execCwd = (deps.resolveSpawnCwd || resolveEpicSpawnCwd)({ cwd, claudeSessionId, deps: { restore: true, readActiveIndex: deps.readActiveIndex, statSync: deps.statSync, restoreWorktree: deps.restoreWorktree } });
|
|
99
101
|
|
|
100
102
|
if (resolved && resolved.existsAnywhere) {
|
|
101
103
|
const projectsDir = path.join(deps.homeDir || os.homedir(), '.claude', 'projects');
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
104
|
+
// O(candidates × existingPaths); both are tiny. Candidate order: chosen execCwd, then project cwd.
|
|
105
|
+
const findReachable = (dir) => {
|
|
106
|
+
if (!isDir(dir, statSync)) return null;
|
|
107
|
+
const home = path.join(projectsDir, encodeCwd(dir));
|
|
108
|
+
return resolved.existingPaths.find((p) => path.dirname(p) === home) || null;
|
|
109
|
+
};
|
|
110
|
+
let hit = findReachable(execCwd);
|
|
111
|
+
if (!hit && execCwd !== cwd) {
|
|
112
|
+
// The transcript may already live under the project encoding; spawning there writes back to
|
|
113
|
+
// the directory it occupies, so nothing forks.
|
|
114
|
+
hit = findReachable(cwd);
|
|
115
|
+
if (hit) execCwd = cwd;
|
|
116
|
+
}
|
|
117
|
+
if (!hit) {
|
|
105
118
|
const missing = brokenDir || execCwd;
|
|
119
|
+
const wt = epic?.worktree;
|
|
120
|
+
const recovery = wt?.status === 'merged'
|
|
121
|
+
? `That checkout was removed when the Epic was merged to main and its ${wt.branch || 'sm-epic/*'} branch was deleted, so it cannot be recreated. Start a new Epic.`
|
|
122
|
+
: `The checkout is gone and could not be re-attached (it may have been swept from its temp/state directory). Recreate it (git worktree add "${missing}" ${wt?.branch || '<sm-epic branch>'}) or start a new Epic.`;
|
|
106
123
|
const message =
|
|
107
124
|
`Session ${claudeSessionId} cannot be resumed: its transcript is at ${resolved.existingPaths[0]}, ` +
|
|
108
|
-
`which the CLI only finds when run from ${missing}
|
|
109
|
-
`re-attached (likely swept from /tmp). Recreate it (git worktree add "${missing}" ${epic?.worktree?.branch || '<sm-epic branch>'}) ` +
|
|
110
|
-
`or start a new Epic.`;
|
|
125
|
+
`which the CLI only finds when run from ${missing}. ${recovery}`;
|
|
111
126
|
unreachable.set(claudeSessionId, { dir: missing, message });
|
|
112
127
|
return { ok: false, code: 'session_unreachable', message };
|
|
113
128
|
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* transcript actually live?".
|
|
6
6
|
*
|
|
7
7
|
* The CLI writes `~/.claude/projects/<encodeCwd(spawn cwd)>/<id>.jsonl`, and an
|
|
8
|
-
* Epic's spawn cwd is its isolated worktree dir (chatRunner.cjs
|
|
9
|
-
*
|
|
8
|
+
* Epic's spawn cwd is normally its isolated worktree dir (chatRunner.cjs and
|
|
9
|
+
* pty.cjs via epicSpawnPlan.cjs), falling back to the project cwd when the
|
|
10
|
+
* worktree is merged/gone. Turn 1 can still land under the
|
|
10
11
|
* project encoding (the worktree mint is fire-and-forget), and the worktree dir
|
|
11
12
|
* itself is routinely swept from /tmp while its transcript survives under
|
|
12
13
|
* ~/.claude/projects. So candidates are built from the recorded `worktree.dir`
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* On success the worktree checkout is torn down (cleanupEpicWorktree) since
|
|
18
18
|
* its content now safely lives on the main tree. On a real conflict, the
|
|
19
19
|
* branch and worktree directory are left intact — never silently discarded
|
|
20
|
-
* — so a human can still open a Terminal into `worktree.dir` (
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* — so a human can still open a Terminal into `worktree.dir` (the spawn
|
|
21
|
+
* resolution in epicSpawnCwd.cjs is status-aware: `needs_merge_resolution`
|
|
22
|
+
* with an existing dir still lands there, while `merged`/`disabled` skip the
|
|
23
|
+
* re-attach and fall back to the project cwd) and resolve it manually before
|
|
24
24
|
* re-running this same action.
|
|
25
25
|
*/
|
|
26
26
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* updateCheck.cjs — bounded, offline-safe, opt-out "is a newer build published?" probe.
|
|
5
|
+
*
|
|
6
|
+
* getUpdateStatus() → { current, latest, behind }. Never throws/rejects; any failure
|
|
7
|
+
* (offline, DNS, non-200, bad JSON, timeout) → { current, latest: null, behind: false }.
|
|
8
|
+
* The request is a bare GET (no headers/cookies/query/body) so it carries nothing that
|
|
9
|
+
* identifies the user or machine. Disabled by SM_UPDATE_CHECK=0 or E2E mode (SM_E2E=1).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const https = require('node:https');
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
17
|
+
const REQUEST_TIMEOUT_MS = 3000;
|
|
18
|
+
const CACHE_FILE = 'update-check.json';
|
|
19
|
+
|
|
20
|
+
/** Plain GET → parsed JSON. Rejects on non-200, bad JSON, or timeout. */
|
|
21
|
+
function httpGetJson(url, timeoutMs) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const req = https.get(url, { timeout: timeoutMs }, (res) => {
|
|
24
|
+
if (res.statusCode !== 200) {
|
|
25
|
+
res.resume();
|
|
26
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let body = '';
|
|
30
|
+
res.setEncoding('utf8');
|
|
31
|
+
res.on('data', (c) => {
|
|
32
|
+
body += c;
|
|
33
|
+
if (body.length > 65536) req.destroy(new Error('response too large'));
|
|
34
|
+
});
|
|
35
|
+
res.on('end', () => {
|
|
36
|
+
try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
|
|
37
|
+
});
|
|
38
|
+
res.on('error', reject);
|
|
39
|
+
});
|
|
40
|
+
req.on('timeout', () => req.destroy(new Error('timeout')));
|
|
41
|
+
req.on('error', reject);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseTriple(v) {
|
|
46
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(v || ''));
|
|
47
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True iff `current` is strictly older than `latest` (numeric major.minor.patch). */
|
|
51
|
+
function isBehind(current, latest) {
|
|
52
|
+
const a = parseTriple(current);
|
|
53
|
+
const b = parseTriple(latest);
|
|
54
|
+
if (!a || !b) return false;
|
|
55
|
+
for (let i = 0; i < 3; i++) {
|
|
56
|
+
if (a[i] !== b[i]) return a[i] < b[i];
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readCache(file, now) {
|
|
62
|
+
try {
|
|
63
|
+
const c = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
64
|
+
if (c && typeof c.latest === 'string' && typeof c.checkedAt === 'number'
|
|
65
|
+
&& now - c.checkedAt >= 0 && now - c.checkedAt < CACHE_TTL_MS) return c.latest;
|
|
66
|
+
} catch { /* missing/corrupt cache = miss */ }
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function defaultPackageName() {
|
|
71
|
+
return require(path.join(__dirname, '..', '..', '..', 'package.json')).name;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function getUpdateStatus({ deps } = {}) {
|
|
75
|
+
const d = deps || {};
|
|
76
|
+
let current = '';
|
|
77
|
+
try {
|
|
78
|
+
const app = d.app || require('electron').app;
|
|
79
|
+
current = app.getVersion();
|
|
80
|
+
const env = d.env || process.env;
|
|
81
|
+
if (env.SM_UPDATE_CHECK === '0' || env.SM_E2E === '1') return { current, latest: null, behind: false };
|
|
82
|
+
|
|
83
|
+
const now = (d.now || Date.now)();
|
|
84
|
+
const file = path.join(app.getPath('userData'), CACHE_FILE);
|
|
85
|
+
let latest = readCache(file, now);
|
|
86
|
+
if (latest == null) {
|
|
87
|
+
const name = (d.packageName || defaultPackageName)();
|
|
88
|
+
const url = `https://registry.npmjs.org/-/package/${name}/dist-tags`;
|
|
89
|
+
const tags = await (d.fetchJson || httpGetJson)(url, REQUEST_TIMEOUT_MS);
|
|
90
|
+
latest = tags && typeof tags.latest === 'string' ? tags.latest : null;
|
|
91
|
+
if (latest == null) return { current, latest: null, behind: false };
|
|
92
|
+
try {
|
|
93
|
+
await (d.writeJson || require('../config.cjs').writeJson)(file, { checkedAt: now, latest });
|
|
94
|
+
} catch { /* cache is best-effort */ }
|
|
95
|
+
}
|
|
96
|
+
return { current, latest, behind: isBehind(current, latest) };
|
|
97
|
+
} catch {
|
|
98
|
+
return { current, latest: null, behind: false };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { getUpdateStatus, isBehind };
|
package/src/main/pty.cjs
CHANGED
|
@@ -22,6 +22,7 @@ const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
|
|
|
22
22
|
const { checkInsideHome } = require('./lib/insideHome.cjs');
|
|
23
23
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
24
24
|
const opsErrorLog = require('./lib/opsErrorLog.cjs');
|
|
25
|
+
const { planEpicSpawn } = require('./lib/epicSpawnPlan.cjs');
|
|
25
26
|
const { resolveEpicSpawnCwd } = require('./lib/epicSpawnCwd.cjs');
|
|
26
27
|
const telemetryCounters = require('./lib/telemetryCounters.cjs');
|
|
27
28
|
|
|
@@ -176,12 +177,37 @@ class PtyManager {
|
|
|
176
177
|
|
|
177
178
|
const shell = process.env.SHELL || '/bin/bash';
|
|
178
179
|
console.log('[pty] spawning shell', shell);
|
|
179
|
-
// Tab ID = claudeSessionId (see CLAUDE.md),
|
|
180
|
-
// this
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
180
|
+
// Tab ID = claudeSessionId (see CLAUDE.md), and Chat/Terminal are two views over ONE session,
|
|
181
|
+
// so this asks the same decision function chatRunner.cjs does (epicSpawnPlan.cjs). `cwd`
|
|
182
|
+
// itself is NEVER repointed; only this actual PTY spawn option is (epicSpawnCwd.cjs's
|
|
183
|
+
// ops-root hazard). A refused plan is surfaced in-tab rather than spawning into a bad cwd.
|
|
184
|
+
// Split: Chat and Terminal share this one verdict function, but the Terminal additionally
|
|
185
|
+
// downgrades `epic_closed` to a plain shell — it opens `$SHELL -il` (no --resume, no
|
|
186
|
+
// transcript write), not a chat turn. That path uses the read-side resolver WITHOUT
|
|
187
|
+
// `deps.restore`, so opening a closed Epic's terminal never re-attaches a checkout.
|
|
188
|
+
const plan = planEpicSpawn({ cwd, claudeSessionId: tabId });
|
|
189
|
+
let spawnCwd;
|
|
190
|
+
if (plan.ok) {
|
|
191
|
+
spawnCwd = plan.execCwd;
|
|
192
|
+
} else if (plan.code === 'epic_closed') {
|
|
193
|
+
spawnCwd = resolveEpicSpawnCwd({ cwd, claudeSessionId: tabId });
|
|
194
|
+
sendIfAlive(
|
|
195
|
+
this.window,
|
|
196
|
+
`pty:data:${tabId}`,
|
|
197
|
+
`\r\nThis Epic is completed \u2014 opening a read-only shell in ${spawnCwd}. Start a new Epic for new work.\r\n`,
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
opsErrorLog.appendError({
|
|
201
|
+
cwd,
|
|
202
|
+
scope: 'pty',
|
|
203
|
+
tabId,
|
|
204
|
+
tags: ['spawn-refused', plan.code],
|
|
205
|
+
message: plan.message,
|
|
206
|
+
});
|
|
207
|
+
sendIfAlive(this.window, `pty:data:${tabId}`, `\r\n${plan.message}\r\n`);
|
|
208
|
+
setImmediate(() => sendIfAlive(this.window, `pty:exit:${tabId}`, { exitCode: 1, signal: undefined }));
|
|
209
|
+
return { pid: null, cwd, reattached: false, error: plan.message };
|
|
210
|
+
}
|
|
185
211
|
if (spawnCwd !== cwd) {
|
|
186
212
|
console.log(`[pty] tabId=${tabId} isolated in worktree ${spawnCwd}`);
|
|
187
213
|
}
|
package/src/preload/api.d.ts
CHANGED
|
@@ -1541,6 +1541,8 @@ export interface ChatExternalSendEvent {
|
|
|
1541
1541
|
export interface SessionManagerAPI {
|
|
1542
1542
|
app: {
|
|
1543
1543
|
version: () => Promise<string>;
|
|
1544
|
+
/** Published-`latest` probe (cached 6h, opt-out SM_UPDATE_CHECK=0). Never rejects; offline → latest null. */
|
|
1545
|
+
updateStatus: () => Promise<{ current: string; latest: string | null; behind: boolean }>;
|
|
1544
1546
|
homeDir: () => Promise<string>;
|
|
1545
1547
|
cwd: () => Promise<string>;
|
|
1546
1548
|
engageRulesPath: () => Promise<string | null>;
|
package/src/preload/index.cjs
CHANGED
|
@@ -3,6 +3,7 @@ const { contextBridge, ipcRenderer } = require('electron');
|
|
|
3
3
|
contextBridge.exposeInMainWorld('api', {
|
|
4
4
|
app: {
|
|
5
5
|
version: () => ipcRenderer.invoke('app:version'),
|
|
6
|
+
updateStatus: () => ipcRenderer.invoke('app:update-status'),
|
|
6
7
|
homeDir: () => ipcRenderer.invoke('app:home-dir'),
|
|
7
8
|
cwd: () => ipcRenderer.invoke('app:cwd'),
|
|
8
9
|
engageRulesPath: () => ipcRenderer.invoke('app:engage-rules-path'),
|