@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 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
- - `browserMode` (default `app`) — `app` finds Edge or Chrome and spawns it with `--app=<url> --user-data-dir=<DATA_DIR>/browser-profile`. `tab` opens the default browser. `none` skips opening.
92
- - `clis` — array of CLI definitions. Built-ins for `claude`, `codex`, `copilot`; users can add `other` CLIs with custom `command`, `args`, `resumeArgs`, `resumeIdArgs`, `shell` (direct/pwsh/cmd).
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, adopted, resumed
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
- cliSessionId, status, … }`. We don't enumerate `~/.claude/` or walk
169
- process trees anymore.
170
-
171
- **`cliSessionId` pre-assignment.** Every built-in CLI (claude, codex,
172
- copilot) gets a UUID generated by `crypto.randomUUID()` at spawn time
173
- and stamped into the persistedSessions record up front. How the UUID
174
- makes it into the CLI's own state depends on what the CLI exposes:
175
-
176
- - **claude / copilot** — pass it via `--session-id <uuid>` (their
177
- `newSessionIdArgs` template). The CLI creates its transcript file
178
- under that exact UUID.
179
- - **codex** — no equivalent flag exists. Instead, `lib/codexSeed.js`
180
- writes a fake rollout file at
181
- `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl` containing
182
- one `session_meta` line carrying the chosen id + spawn cwd, then we
183
- spawn `codex resume <uuid>` so the first launch *is* a resume
184
- against our seed. Codex appends real events to the same file from
185
- there.
186
-
187
- Either way, no polling, no race, no 5-minute timeout. **Resume** uses
188
- the CLI's `resumeIdArgs` template (`['--resume', '<id>']` for
189
- claude/copilot, `['resume', '<id>']` for codex) to reattach precisely.
190
- There is no fallback path every ccsm-launched session has a captured
191
- upstream id, so resume by id always applies. User-added "other" CLIs
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
- │ ├── localCliSessions.js # scan ~/.claude · ~/.codex · ~/.copilot
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 · AdoptModal.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, cliSessionId, …)
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
- workspace root so all selected repos are sibling folders.
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; current and target CLI must share `type` |
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 `cli.resumeIdArgs <id>` (fallback `resumeArgs`) |
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
- **Pre-assigned UUID needs either a flag OR a writable transcript dir.**
347
- `newSessionIdArgs` works two ways: native flag (claude, copilot via
348
- `--session-id`) or seeded transcript file (codex via `resume <id>` +
349
- `lib/codexSeed.js`). User-added "other" CLIs without either get no
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
- **Adopt is atomic.** `persistedSessions.create()` accepts `status` +
354
- `cliSessionId` so the adopt endpoint writes the record in a single
355
- file write rather than `create({running})` + `update({exited,
356
- cliSessionId})`. The two-write form had a window where a concurrent
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/localCliSessions.js`, `lib/workspace.js` (fs only)
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` (set `newSessionIdArgs` if the CLI accepts a pre-assigned UUID, `resumeIdArgs` for precise resume), an icon to `public/js/icons.js`, and (if the CLI persists transcripts on disk and you want adopt support) a list helper to `lib/localCliSessions.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 re-attaches to the exact
6
- upstream conversation when you click it again.
5
+ the local backend), gets recorded by filesystem folder, and resumes in
6
+ that folder when you click it again.
7
7
 
8
8
  [![open](https://img.shields.io/badge/open-bakapiano.github.io%2Fccsm-1a1815?style=flat-square)](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
- - **`--resume <uuid>` precision.** ccsm watches the upstream CLI's
36
- transcript dir after spawn and captures its session UUID. Click a
37
- stopped session later re-spawns with `--resume <uuid>` (or
38
- whatever `resumeIdArgs` template you set per-CLI) so the exact
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
- there. Or pick any existing folder via the file browser.
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 / session-watcher / lifecycle code.
186
+ gotchas baked into the launcher, session lifecycle, and workspace code.
@@ -1,134 +1,31 @@
1
1
  'use strict';
2
2
 
3
- // Detect whether each running CLI session is "working" (actively writing
4
- // to its transcript) or "idle" (waiting on user input). We poll the
5
- // transcript file's mtime on each /api/sessions request: if it moved
6
- // since the previous probe, the CLI is writing → working. If it hasn't
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 { resolvedPath, lastMtimeMs, lastChangedAt }
10
+ // sessionId -> { lastOutputAt }
27
11
  const state = new Map();
28
12
 
29
- async function fileExists(p) {
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 = { resolvedPath: null, lastMtimeMs: 0, lastChangedAt: 0, lastOutputAt: 0 };
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
- if (s.lastOutputAt && (now - s.lastOutputAt) < WORKING_WINDOW_MS) {
105
- return 'working';
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 = { resolvedPath: null, lastMtimeMs: 0, lastChangedAt: 0, lastOutputAt: 0 };
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
- // Seed a fake codex rollout file so `codex resume <uuid>` works from the
4
- // VERY FIRST launch the same trick claude/copilot's `--session-id` flag
5
- // gives us natively. codex has no equivalent flag; its only "set the id"
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 = { seedCodexSession, probeCodexHome, parseCodexHomeFromDoctor, ensureCodexLightTheme };
125
+ module.exports = { probeCodexHome, parseCodexHomeFromDoctor, ensureCodexLightTheme };
183
126