@bakapiano/ccsm 0.22.7 → 0.22.8
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/CLAUDE.md +44 -63
- package/README.md +11 -14
- package/lib/cliActivity.js +11 -114
- package/lib/codexSeed.js +4 -61
- package/lib/config.js +62 -64
- package/lib/persistedSessions.js +68 -28
- package/lib/winPath.js +1 -1
- package/package.json +1 -1
- package/public/css/widgets.css +1 -379
- package/public/js/api.js +5 -27
- package/public/js/components/EntityFormModal.js +2 -2
- package/public/js/pages/ConfigurePage.js +37 -35
- package/public/js/pages/LaunchPage.js +8 -26
- package/public/js/pages/SessionsPage.js +1 -1
- package/public/js/util.js +1 -1
- package/server.js +110 -192
- package/lib/localCliSessions.js +0 -519
- package/public/js/components/AdoptModal.js +0 -261
package/CLAUDE.md
CHANGED
|
@@ -33,7 +33,6 @@ version router.
|
|
|
33
33
|
│ ccsm │
|
|
34
34
|
│ ├── /api/sessions /api/sessions/new │
|
|
35
35
|
│ ├── /api/sessions/:id/resume │
|
|
36
|
-
│ ├── /api/sessions/adopt │
|
|
37
36
|
│ ├── /ws/terminal/:id (PTY) │
|
|
38
37
|
│ ├── /api/version /api/upgrade │
|
|
39
38
|
│ ├── /api/heartbeat /api/health │
|
|
@@ -88,8 +87,8 @@ settings editable through the Configure page
|
|
|
88
87
|
(`~/.ccsm/config.json` on disk). Notable knobs:
|
|
89
88
|
|
|
90
89
|
- `port` (default `7777`) — preferred listen port. If taken, ccsm tries `+1..+9` then asks the OS for any free port.
|
|
91
|
-
- `
|
|
92
|
-
- `clis` — array of CLI definitions. Built-ins for `claude`, `codex`, `copilot`; users can add `other` CLIs with custom `command`, `args`, `
|
|
90
|
+
- `resumeMode` (default `latest`) — `latest` uses each CLI's `resumeLatestArgs`; `picker` uses `resumePickerArgs`.
|
|
91
|
+
- `clis` — array of CLI definitions. Built-ins for `claude`, `codex`, `copilot`; users can add `other` CLIs with custom `command`, `args`, `resumeLatestArgs`, `resumePickerArgs`, `shell` (direct/pwsh/cmd).
|
|
93
92
|
- `defaultCliId` — which CLI the Launch page pre-selects.
|
|
94
93
|
|
|
95
94
|
## ccsm:// protocol · "wake on click"
|
|
@@ -160,46 +159,35 @@ Environment overrides:
|
|
|
160
159
|
- `CCSM_NO_BROWSER=1` → set by the launcher when handling a `ccsm://` click or by `/api/upgrade` self-respawn; suppresses the server's auto-open browser.
|
|
161
160
|
- `CCSM_NO_DEV=1` → suppress dev-mode features (static serving, hot-reload SSE) even when running from a checkout.
|
|
162
161
|
|
|
163
|
-
## Sessions: persisted
|
|
162
|
+
## Sessions: persisted and resumed
|
|
164
163
|
|
|
165
164
|
There's **one source of truth**: `~/.ccsm/sessions.json`, managed by
|
|
166
165
|
`lib/persistedSessions.js`. Every session ccsm starts goes in there
|
|
167
166
|
with `{ id, cliId, cwd, workspace, title, folderId, repos,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
the CLI
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
must configure `newSessionIdArgs` + `resumeIdArgs` (or accept that
|
|
193
|
-
resume won't work).
|
|
194
|
-
|
|
195
|
-
For `adopt`-imported sessions the record is born with `cliSessionId`
|
|
196
|
-
already set (from disk scan), so resume uses `resumeIdArgs` directly.
|
|
197
|
-
|
|
198
|
-
**Adopt.** "Import existing session" on the Launch page lists
|
|
199
|
-
sessions found on disk (`/api/cli-sessions/:type`) and lets the user
|
|
200
|
-
add one to ccsm with `/api/sessions/adopt`. The created record is
|
|
201
|
-
born `status: 'exited'` with `cliSessionId` pre-set. Clicking it in
|
|
202
|
-
the sidebar runs the normal resume flow — which uses the captured id.
|
|
167
|
+
status, … }`. The persisted `id` is ccsm-owned and matches the PTY id;
|
|
168
|
+
it is not an upstream CLI session id.
|
|
169
|
+
|
|
170
|
+
**Folder-level resume.** ccsm does not persist upstream session ids and
|
|
171
|
+
does not seed CLI transcript files. Resume launches the configured CLI
|
|
172
|
+
at the record's `cwd` and appends one of two folder-level templates:
|
|
173
|
+
|
|
174
|
+
- `resumeMode: 'latest'` -> `cli.resumeLatestArgs`
|
|
175
|
+
- `resumeMode: 'picker'` -> `cli.resumePickerArgs`
|
|
176
|
+
|
|
177
|
+
Built-ins default to:
|
|
178
|
+
|
|
179
|
+
- Claude: latest `--continue`, picker `--resume`
|
|
180
|
+
- Codex: latest `resume --last`, picker `resume`
|
|
181
|
+
- Copilot: latest `--continue`, picker `--resume`
|
|
182
|
+
|
|
183
|
+
User-added `other` CLIs can leave those arrays empty if they do not
|
|
184
|
+
support resume, or set either template explicitly.
|
|
185
|
+
|
|
186
|
+
**Duplicate records.** New sessions are keyed by normalized
|
|
187
|
+
`cliId + cwd`. If the user launches the same CLI in the same folder
|
|
188
|
+
again, ccsm reuses the existing record instead of creating a second
|
|
189
|
+
sidebar entry. Workspace allocation treats every persisted session with
|
|
190
|
+
a `cwd` as occupying that workspace until the session record is deleted.
|
|
203
191
|
|
|
204
192
|
**Auto-resume.** SessionsPage doesn't show a "Resume" button. On
|
|
205
193
|
mount, if the active session's status isn't `running`, it calls
|
|
@@ -220,9 +208,7 @@ ccsm/
|
|
|
220
208
|
├── lib/
|
|
221
209
|
│ ├── persistedSessions.js # ~/.ccsm/sessions.json — source of truth
|
|
222
210
|
│ ├── folders.js # ~/.ccsm/folders.json — sidebar tree
|
|
223
|
-
│ ├──
|
|
224
|
-
│ ├── codexSeed.js # seed ~/.codex/sessions/.../rollout-*.jsonl
|
|
225
|
-
│ │ # so `codex resume <uuid>` works on launch 1
|
|
211
|
+
│ ├── codexSeed.js # Codex CODEX_HOME probe + bundled light theme install
|
|
226
212
|
│ ├── workspace.js # ws-N allocation under workDir, repo clones
|
|
227
213
|
│ ├── webTerminal.js # in-process PTY pool · node-pty + WebSocket
|
|
228
214
|
│ ├── jsonStore.js # shared keyed-JSON store factory
|
|
@@ -247,7 +233,7 @@ ccsm/
|
|
|
247
233
|
│ │ ├── App.js · Sidebar.js · PageTitleBar.js
|
|
248
234
|
│ │ ├── ServerStatus.js · Toast.js · OfflineBanner.js · DialogHost.js
|
|
249
235
|
│ │ ├── Card.js · Modal.js · Popover.js · Picker.js · EntityFormModal.js
|
|
250
|
-
│ │ ├── DirectoryPicker.js
|
|
236
|
+
│ │ ├── DirectoryPicker.js
|
|
251
237
|
│ │ ├── ProgressList.js · TerminalView.js · useDragSort.js
|
|
252
238
|
│ └── pages/
|
|
253
239
|
│ ├── SessionsPage.js · LaunchPage.js
|
|
@@ -260,7 +246,7 @@ ccsm/
|
|
|
260
246
|
|
|
261
247
|
~/.ccsm/ # or $CCSM_HOME
|
|
262
248
|
├── config.json # source of truth
|
|
263
|
-
├── sessions.json # persisted sessions (id,
|
|
249
|
+
├── sessions.json # persisted sessions (ccsm id, cliId, cwd, …)
|
|
264
250
|
├── folders.json # folder tree
|
|
265
251
|
├── server.log # detached-server stdout/stderr
|
|
266
252
|
├── .first-run-shown # marker so launcher only prints PWA hint once
|
|
@@ -286,7 +272,8 @@ everything the old path did.
|
|
|
286
272
|
|
|
287
273
|
**Workspace = folder holding multiple repo clones.** Each `ws-N` under
|
|
288
274
|
`workDir` contains a subdirectory per cloned repo. CLIs launch at the
|
|
289
|
-
|
|
275
|
+
single selected repo's directory; with zero or multiple repos selected,
|
|
276
|
+
they launch at the workspace root so selected repos are sibling folders.
|
|
290
277
|
|
|
291
278
|
**Workspace naming.** Auto-allocated names are `ws-1`, `ws-2`, …
|
|
292
279
|
(lowest free integer). Hand-named folders under `workDir` are still
|
|
@@ -311,12 +298,10 @@ allows `https://bakapiano.github.io` only — never `*`.
|
|
|
311
298
|
| GET | `/api/sessions` | list persisted sessions |
|
|
312
299
|
| PUT | `/api/sessions/:id` | rename / move to folder |
|
|
313
300
|
| DELETE | `/api/sessions/:id` | kill PTY + drop record |
|
|
314
|
-
| POST | `/api/sessions/:id/switch-cli` | change the persisted `cliId` for future resumes
|
|
301
|
+
| POST | `/api/sessions/:id/switch-cli` | change the persisted `cliId` for future resumes |
|
|
315
302
|
| POST | `/api/sessions/:id/stop` | kill the live PTY but keep the record; sets `manualStopped:true` so UI won't auto-resume |
|
|
316
303
|
| POST | `/api/sessions/new` | body `{cliId, cwd?, repos?, folderId?, title?}` — NDJSON stream (workspace · clone-progress · launched) |
|
|
317
|
-
| POST | `/api/sessions/:id/resume` | re-spawn at `cwd` with
|
|
318
|
-
| GET | `/api/cli-sessions/:type` | scan disk for unimported `claude`/`codex`/`copilot` sessions |
|
|
319
|
-
| POST | `/api/sessions/adopt` | body `{cliId, cliSessionId, cwd, title?, folderId?}` — create a `status:exited` record with `cliSessionId` pre-set |
|
|
304
|
+
| POST | `/api/sessions/:id/resume` | re-spawn at record `cwd` with the configured latest/picker resume args |
|
|
320
305
|
| GET | `/api/folders` · POST `/api/folders` · PUT/DELETE `/api/folders/:id` · POST `/api/folders/reorder` | folder CRUD |
|
|
321
306
|
| GET | `/api/workspaces` | workspaces under workDir with repo clone status + in-use flag |
|
|
322
307
|
| GET | `/api/browse` | directory browser for the Launch page workdir picker |
|
|
@@ -343,19 +328,15 @@ Browsers always send Origin on WS upgrades.
|
|
|
343
328
|
|
|
344
329
|
## Non-obvious gotchas
|
|
345
330
|
|
|
346
|
-
**
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
pre-assignment and fall back to `resumeArgs` (`--continue` / equivalent)
|
|
351
|
-
on relaunch — they just won't have a captured upstream id.
|
|
331
|
+
**Resume is cwd-scoped.** ccsm assumes the upstream CLI can find the
|
|
332
|
+
right conversation from the current working directory when handed its
|
|
333
|
+
latest/picker resume command. We deliberately do not persist or replay
|
|
334
|
+
upstream session UUIDs.
|
|
352
335
|
|
|
353
|
-
**
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
GET /api/sessions could see `running` with no live PTY, fooling the
|
|
358
|
-
sidebar's "skip resume if running" guard.
|
|
336
|
+
**Workspace reservation is record-scoped.** A stopped session still owns
|
|
337
|
+
its workspace until the session record is deleted. This keeps auto
|
|
338
|
+
allocation from reusing `ws-N` and later resuming an older session in a
|
|
339
|
+
folder that has been repurposed.
|
|
359
340
|
|
|
360
341
|
**Auto-resume dedup is module-level in api.js.** Sidebar.onClick and
|
|
361
342
|
SessionsPage's effect can both fire for the same exited session in the
|
|
@@ -521,7 +502,7 @@ Cross-platform-clean already:
|
|
|
521
502
|
- Router page (pure HTML/JS)
|
|
522
503
|
- `bin/ccsm.js` (pure node)
|
|
523
504
|
- `lib/webTerminal.js` (node-pty handles platform)
|
|
524
|
-
- `lib/persistedSessions.js`, `lib/folders.js`, `lib/config.js`, `lib/jsonStore.js`, `lib/
|
|
505
|
+
- `lib/persistedSessions.js`, `lib/folders.js`, `lib/config.js`, `lib/jsonStore.js`, `lib/workspace.js` (fs only)
|
|
525
506
|
- `server.js` Express + ws
|
|
526
507
|
|
|
527
508
|
Windows-specific (need ports for Mac/Linux):
|
|
@@ -538,6 +519,6 @@ When adding features, the natural extension points:
|
|
|
538
519
|
- **New REST routes**: `server.js` (keep under `/api/*`, use the `asyncH` wrapper, decide if it needs CORS by being in the allow-list).
|
|
539
520
|
- **Frontend page**: `public/js/pages/<Name>Page.js`, route in `App.js`, sidebar nav item in `Sidebar.js`, heading in `state.js`'s `TAB_HEADINGS`.
|
|
540
521
|
- **Persistent user data**: drop a JSON file under `~/.ccsm/` and use `lib/jsonStore.js`'s factory.
|
|
541
|
-
- **Different CLIs**: add a built-in to `DEFAULT_CLIS` in `lib/config.js`
|
|
522
|
+
- **Different CLIs**: add a built-in to `DEFAULT_CLIS` in `lib/config.js` with `resumeLatestArgs` / `resumePickerArgs`, and add an icon to `public/js/icons.js`.
|
|
542
523
|
- **A capability**: advertise via `/api/capabilities`. Frontend gates UI on `caps.<feature>`.
|
|
543
524
|
- **Bumping the frontend**: just `npm version <patch|minor|major>` + push. The GH Pages workflow publishes to `/<new-version>/` and the router redirects users to it.
|
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A single pane over every Claude / Codex / Copilot CLI session on your
|
|
4
4
|
machine. Each session runs inside the page (xterm.js + a PTY pool in
|
|
5
|
-
the local backend), gets recorded, and
|
|
6
|
-
|
|
5
|
+
the local backend), gets recorded by filesystem folder, and resumes in
|
|
6
|
+
that folder when you click it again.
|
|
7
7
|
|
|
8
8
|
[](https://bakapiano.github.io/ccsm/)
|
|
9
9
|
|
|
@@ -20,7 +20,6 @@ upstream conversation when you click it again.
|
|
|
20
20
|
│ ccsm (npm bin) │
|
|
21
21
|
│ ├── /api/sessions /api/sessions/new │
|
|
22
22
|
│ ├── /api/sessions/:id/resume │
|
|
23
|
-
│ ├── /api/sessions/adopt │
|
|
24
23
|
│ ├── /api/version /api/upgrade │
|
|
25
24
|
│ ├── /ws/terminal/:id (PTY) │
|
|
26
25
|
│ └── /api/health /api/heartbeat │
|
|
@@ -32,17 +31,15 @@ upstream conversation when you click it again.
|
|
|
32
31
|
- **Runs every CLI session in the page.** `claude`, `codex`, `copilot`
|
|
33
32
|
or any custom command, in an xterm.js panel. Switch sessions in the
|
|
34
33
|
sidebar; the PTY keeps running in the backend.
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
conversation comes back.
|
|
40
|
-
- **Import existing sessions.** Scans `~/.claude` / `~/.codex` /
|
|
41
|
-
`~/.copilot` and lets you adopt any session ccsm didn't start.
|
|
34
|
+
- **Folder-level resume.** ccsm stores the CLI and `cwd` for each
|
|
35
|
+
session. Click a stopped session later and ccsm launches the CLI in
|
|
36
|
+
that folder using either the configured "resume latest" command or
|
|
37
|
+
the CLI's resume picker.
|
|
42
38
|
- **Workspaces + clones.** "New session" picks an unused workspace
|
|
43
39
|
under your work-dir, clones selected repos with live `git clone
|
|
44
|
-
--progress` streamed to per-repo progress bars, opens a fresh CLI
|
|
45
|
-
|
|
40
|
+
--progress` streamed to per-repo progress bars, and opens a fresh CLI
|
|
41
|
+
in the single selected repo or at the workspace root for zero/multiple
|
|
42
|
+
repos. Or pick any existing folder via the file browser.
|
|
46
43
|
- **Folders.** Drag sessions into named folders for organisation.
|
|
47
44
|
- **In-app upgrade.** About page checks npm for newer versions of
|
|
48
45
|
ccsm and offers a one-click upgrade button. Backend self-restarts.
|
|
@@ -92,6 +89,7 @@ terminal needed.
|
|
|
92
89
|
| Port | `7777` (auto-bumps if taken) |
|
|
93
90
|
| Work dir | `~/ccsm-workspaces` (each subdirectory holds one or more repo clones) |
|
|
94
91
|
| Built-in CLIs | `claude`, `codex`, `copilot` — add your own via the **Configure** tab |
|
|
92
|
+
| Resume behavior | `latest` by default; switch to `picker` in **Configure** |
|
|
95
93
|
| Data dir | `~/.ccsm/` (override with `CCSM_HOME=<path>`) — survives upgrades and npx cache wipes |
|
|
96
94
|
|
|
97
95
|
All of the above are editable through the **Configure** tab.
|
|
@@ -108,7 +106,6 @@ ccsm/
|
|
|
108
106
|
├── lib/
|
|
109
107
|
│ ├── persistedSessions.js # ~/.ccsm/sessions.json — the source of truth
|
|
110
108
|
│ ├── folders.js # sidebar tree
|
|
111
|
-
│ ├── localCliSessions.js # scan ~/.claude · ~/.codex · ~/.copilot
|
|
112
109
|
│ ├── workspace.js # ws-N allocation + repo clones
|
|
113
110
|
│ ├── webTerminal.js # node-pty pool · WebSocket bridge
|
|
114
111
|
│ ├── jsonStore.js · config.js
|
|
@@ -186,4 +183,4 @@ bounces you back through the router automatically.
|
|
|
186
183
|
- Frontend: cross-platform (pure web).
|
|
187
184
|
|
|
188
185
|
See [CLAUDE.md](CLAUDE.md) for design decisions and the non-obvious
|
|
189
|
-
gotchas baked into the launcher
|
|
186
|
+
gotchas baked into the launcher, session lifecycle, and workspace code.
|
package/lib/cliActivity.js
CHANGED
|
@@ -1,134 +1,31 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Detect whether
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// moved within WORKING_WINDOW_MS, idle.
|
|
8
|
-
//
|
|
9
|
-
// Transcript paths per CLI:
|
|
10
|
-
// claude → ~/.claude/projects/<slug>/<cliSessionId>.jsonl
|
|
11
|
-
// codex → <CODEX_HOME>/sessions/YYYY/MM/DD/rollout-*-<id>.jsonl
|
|
12
|
-
// copilot → ~/.copilot/session-state/<cliSessionId>/
|
|
13
|
-
//
|
|
14
|
-
// Resolution is cached forever per ccsm session id — once we've found
|
|
15
|
-
// the file, subsequent probes are a single fs.stat().
|
|
3
|
+
// Detect whether a running CLI is actively producing terminal output.
|
|
4
|
+
// Older versions also looked up transcript mtimes by upstream session id;
|
|
5
|
+
// folder-level resume no longer persists those ids, so PTY output is the
|
|
6
|
+
// only session-local signal ccsm owns.
|
|
16
7
|
|
|
17
|
-
const fs = require('node:fs/promises');
|
|
18
|
-
const path = require('node:path');
|
|
19
|
-
const os = require('node:os');
|
|
20
|
-
|
|
21
|
-
// 8s window is comfortably above the 5s frontend poll cadence — if a CLI
|
|
22
|
-
// wrote anything within the last 8s we still call it working when the
|
|
23
|
-
// next refresh lands.
|
|
24
8
|
const WORKING_WINDOW_MS = 8000;
|
|
25
9
|
|
|
26
|
-
// sessionId
|
|
10
|
+
// sessionId -> { lastOutputAt }
|
|
27
11
|
const state = new Map();
|
|
28
12
|
|
|
29
|
-
async function
|
|
30
|
-
try { await fs.access(p); return true; }
|
|
31
|
-
catch { return false; }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function resolveClaude(id) {
|
|
35
|
-
const root = path.join(os.homedir(), '.claude', 'projects');
|
|
36
|
-
let dirs;
|
|
37
|
-
try { dirs = await fs.readdir(root); } catch { return null; }
|
|
38
|
-
for (const d of dirs) {
|
|
39
|
-
const p = path.join(root, d, `${id}.jsonl`);
|
|
40
|
-
if (await fileExists(p)) return p;
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function resolveCodex(id, cliCfg) {
|
|
46
|
-
let home = null;
|
|
47
|
-
try {
|
|
48
|
-
const { probeCodexHome } = require('./codexSeed');
|
|
49
|
-
home = await probeCodexHome({ command: cliCfg.command, shell: cliCfg.shell });
|
|
50
|
-
} catch { /* probe is best-effort */ }
|
|
51
|
-
if (!home) home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
52
|
-
const root = path.join(home, 'sessions');
|
|
53
|
-
const suffix = `-${id}.jsonl`;
|
|
54
|
-
async function walk(dir, depth) {
|
|
55
|
-
if (depth > 4) return null;
|
|
56
|
-
let entries;
|
|
57
|
-
try { entries = await fs.readdir(dir, { withFileTypes: true }); }
|
|
58
|
-
catch { return null; }
|
|
59
|
-
for (const e of entries) {
|
|
60
|
-
const p = path.join(dir, e.name);
|
|
61
|
-
if (e.isDirectory()) {
|
|
62
|
-
const r = await walk(p, depth + 1);
|
|
63
|
-
if (r) return r;
|
|
64
|
-
} else if (e.isFile() && e.name.endsWith(suffix)) {
|
|
65
|
-
return p;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
return walk(root, 0);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function resolveCopilot(id) {
|
|
74
|
-
const p = path.join(os.homedir(), '.copilot', 'session-state', id);
|
|
75
|
-
if (await fileExists(p)) return p;
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function resolveTranscript(record, cliCfg) {
|
|
80
|
-
if (!record.cliSessionId || !cliCfg) return null;
|
|
81
|
-
switch (cliCfg.type) {
|
|
82
|
-
case 'claude': return resolveClaude(record.cliSessionId);
|
|
83
|
-
case 'codex': return resolveCodex(record.cliSessionId, cliCfg);
|
|
84
|
-
case 'copilot': return resolveCopilot(record.cliSessionId);
|
|
85
|
-
default: return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Returns 'working' | 'idle' | 'unknown' for a single record.
|
|
90
|
-
async function probeActivity(record, cliCfg) {
|
|
13
|
+
async function probeActivity(record) {
|
|
91
14
|
let s = state.get(record.id);
|
|
92
15
|
if (!s) {
|
|
93
|
-
s = {
|
|
16
|
+
s = { lastOutputAt: 0 };
|
|
94
17
|
state.set(record.id, s);
|
|
95
18
|
}
|
|
96
|
-
// PTY output (CLI is streaming text — thinking spinners, token output,
|
|
97
|
-
// status lines) is the strongest signal that the CLI is working. It's
|
|
98
|
-
// ALSO the only signal we have when the transcript file isn't being
|
|
99
|
-
// updated — claude/codex buffer reasoning + tool results for tens of
|
|
100
|
-
// seconds before flushing a turn, so mtime alone reports "idle"
|
|
101
|
-
// through long thinking phases. Check PTY first; short-circuit if the
|
|
102
|
-
// CLI is clearly active, skipping the fs.stat below.
|
|
103
19
|
const now = Date.now();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!s.resolvedPath) {
|
|
108
|
-
s.resolvedPath = await resolveTranscript(record, cliCfg);
|
|
109
|
-
if (!s.resolvedPath) return 'unknown';
|
|
110
|
-
}
|
|
111
|
-
let mtimeMs;
|
|
112
|
-
try { mtimeMs = (await fs.stat(s.resolvedPath)).mtimeMs; }
|
|
113
|
-
catch {
|
|
114
|
-
// File disappeared (rollover, manual delete) — drop the cache so we
|
|
115
|
-
// re-resolve on the next probe.
|
|
116
|
-
s.resolvedPath = null;
|
|
117
|
-
return 'unknown';
|
|
118
|
-
}
|
|
119
|
-
if (mtimeMs !== s.lastMtimeMs) {
|
|
120
|
-
s.lastMtimeMs = mtimeMs;
|
|
121
|
-
s.lastChangedAt = now;
|
|
122
|
-
}
|
|
123
|
-
return (now - s.lastChangedAt) < WORKING_WINDOW_MS ? 'working' : 'idle';
|
|
20
|
+
return s.lastOutputAt && (now - s.lastOutputAt) < WORKING_WINDOW_MS
|
|
21
|
+
? 'working'
|
|
22
|
+
: 'idle';
|
|
124
23
|
}
|
|
125
24
|
|
|
126
|
-
// Called from server.js's spawnCliSession onData hook. Cheap (timestamp
|
|
127
|
-
// write); bound by how often the PTY emits, which is fine.
|
|
128
25
|
function noteOutput(sessionId) {
|
|
129
26
|
let s = state.get(sessionId);
|
|
130
27
|
if (!s) {
|
|
131
|
-
s = {
|
|
28
|
+
s = { lastOutputAt: 0 };
|
|
132
29
|
state.set(sessionId, s);
|
|
133
30
|
}
|
|
134
31
|
s.lastOutputAt = Date.now();
|
package/lib/codexSeed.js
CHANGED
|
@@ -1,37 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// surface is `resume <SESSION_ID>` against a file that already exists on
|
|
7
|
-
// disk. We pre-write that file with one `session_meta` line carrying the
|
|
8
|
-
// id + cwd ccsm pre-assigned, then spawn `codex resume <id>`. Codex picks
|
|
9
|
-
// up our seed and appends its actual conversation events to it.
|
|
10
|
-
//
|
|
11
|
-
// Path layout (matches codex's own scheme):
|
|
12
|
-
// ~/.codex/sessions/YYYY/MM/DD/rollout-<iso-ts>-<uuid>.jsonl
|
|
13
|
-
//
|
|
14
|
-
// Filename timestamp uses dashes-only (codex's convention), but it's
|
|
15
|
-
// purely cosmetic — codex looks up sessions by UUID, not filename.
|
|
16
|
-
//
|
|
17
|
-
// CODEX_HOME resolution. Some wrappers relocate CODEX_HOME to a
|
|
18
|
-
// non-default dir (e.g. %LOCALAPPDATA%\<wrapper>\codex-home) so the seed has
|
|
19
|
-
// to land there or `resume <id>` won't find it. We probe by running
|
|
3
|
+
// Codex light-theme helper. Some wrappers relocate CODEX_HOME to a
|
|
4
|
+
// non-default dir (e.g. %LOCALAPPDATA%\<wrapper>\codex-home), so the bundled
|
|
5
|
+
// ccsm-light theme has to be installed there. We probe by running
|
|
20
6
|
// `<cli.command> doctor` once per (command, shell) pair and parsing the
|
|
21
7
|
// "CODEX_HOME ... (dir)" line out of its output. Cached for the life of
|
|
22
8
|
// the process.
|
|
23
9
|
|
|
24
10
|
const fs = require('node:fs/promises');
|
|
25
11
|
const path = require('node:path');
|
|
26
|
-
const os = require('node:os');
|
|
27
12
|
const { execFile } = require('node:child_process');
|
|
28
13
|
const { spawnEnv } = require('./winPath');
|
|
29
14
|
|
|
30
|
-
function isoForFilename(d = new Date()) {
|
|
31
|
-
// 2026-05-25T15:39:11 → 2026-05-25T15-39-11 (codex strips ms + colons)
|
|
32
|
-
return d.toISOString().replace(/\.\d+Z$/, '').replace(/:/g, '-');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
15
|
// command+shell → CODEX_HOME (or null if probe failed / not detected).
|
|
36
16
|
// Module-scope so we probe at most once per (command, shell) per server.
|
|
37
17
|
const codexHomeCache = new Map();
|
|
@@ -121,43 +101,6 @@ async function probeCodexHome({ command, shell }) {
|
|
|
121
101
|
return home;
|
|
122
102
|
}
|
|
123
103
|
|
|
124
|
-
async function seedCodexSession({ id, cwd, cli }) {
|
|
125
|
-
if (!id || !cwd) throw new Error('seedCodexSession: id and cwd required');
|
|
126
|
-
// Resolution order:
|
|
127
|
-
// 1. `<cli.command> doctor` probe (handles wrappers that
|
|
128
|
-
// relocate CODEX_HOME)
|
|
129
|
-
// 2. process.env.CODEX_HOME (global override)
|
|
130
|
-
// 3. ~/.codex (codex's own default)
|
|
131
|
-
let home = null;
|
|
132
|
-
if (cli?.command) {
|
|
133
|
-
try { home = await probeCodexHome({ command: cli.command, shell: cli.shell }); }
|
|
134
|
-
catch (_) { /* probe is best-effort */ }
|
|
135
|
-
}
|
|
136
|
-
if (!home) home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
137
|
-
|
|
138
|
-
const now = new Date();
|
|
139
|
-
const yyyy = String(now.getUTCFullYear());
|
|
140
|
-
const mm = String(now.getUTCMonth() + 1).padStart(2, '0');
|
|
141
|
-
const dd = String(now.getUTCDate()).padStart(2, '0');
|
|
142
|
-
const dir = path.join(home, 'sessions', yyyy, mm, dd);
|
|
143
|
-
await fs.mkdir(dir, { recursive: true });
|
|
144
|
-
const file = path.join(dir, `rollout-${isoForFilename(now)}-${id}.jsonl`);
|
|
145
|
-
const meta = {
|
|
146
|
-
timestamp: now.toISOString(),
|
|
147
|
-
type: 'session_meta',
|
|
148
|
-
payload: {
|
|
149
|
-
id,
|
|
150
|
-
timestamp: now.toISOString(),
|
|
151
|
-
cwd,
|
|
152
|
-
originator: 'ccsm',
|
|
153
|
-
cli_version: '0.0.0',
|
|
154
|
-
source: 'ccsm-seed',
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
await fs.writeFile(file, JSON.stringify(meta) + '\n', 'utf8');
|
|
158
|
-
return file;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
104
|
// Copy ccsm's bundled light codex syntax theme into the codex home's themes/
|
|
162
105
|
// dir so `-c tui.theme=ccsm-light` resolves. This theme carries light
|
|
163
106
|
// markup.inserted/deleted backgrounds, which at true-color level override
|
|
@@ -179,5 +122,5 @@ async function ensureCodexLightTheme(home) {
|
|
|
179
122
|
} catch { return false; }
|
|
180
123
|
}
|
|
181
124
|
|
|
182
|
-
module.exports = {
|
|
125
|
+
module.exports = { probeCodexHome, parseCodexHomeFromDoctor, ensureCodexLightTheme };
|
|
183
126
|
|