@bakapiano/ccsm 0.22.6 → 0.22.7

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 (58) hide show
  1. package/CLAUDE.md +538 -538
  2. package/README.md +189 -189
  3. package/bin/ccsm.js +235 -235
  4. package/lib/cliActivity.js +139 -139
  5. package/lib/codexSeed.js +183 -183
  6. package/lib/config.js +279 -274
  7. package/lib/devices.js +229 -229
  8. package/lib/folders.js +124 -124
  9. package/lib/localCliSessions.js +519 -519
  10. package/lib/persistedSessions.js +129 -129
  11. package/lib/tunnel.js +621 -621
  12. package/lib/webTerminal.js +225 -225
  13. package/lib/workspace.js +233 -233
  14. package/package.json +57 -57
  15. package/public/css/base.css +99 -99
  16. package/public/css/cards.css +183 -183
  17. package/public/css/feedback.css +504 -504
  18. package/public/css/forms.css +453 -453
  19. package/public/css/layout.css +154 -154
  20. package/public/css/modal.css +190 -190
  21. package/public/css/responsive.css +176 -176
  22. package/public/css/sidebar.css +707 -707
  23. package/public/css/terminals.css +546 -546
  24. package/public/css/tokens.css +81 -81
  25. package/public/css/wco.css +196 -196
  26. package/public/css/widgets.css +2725 -2725
  27. package/public/index.html +152 -152
  28. package/public/js/api.js +371 -371
  29. package/public/js/backend.js +149 -149
  30. package/public/js/components/App.js +73 -73
  31. package/public/js/components/DirectoryPicker.js +203 -203
  32. package/public/js/components/EntityFormModal.js +153 -153
  33. package/public/js/components/Modal.js +57 -57
  34. package/public/js/components/OfflineBanner.js +67 -67
  35. package/public/js/components/PageTitleBar.js +13 -13
  36. package/public/js/components/PendingApprovalOverlay.js +128 -128
  37. package/public/js/components/Picker.js +179 -179
  38. package/public/js/components/Popover.js +55 -55
  39. package/public/js/components/RestartOverlay.js +36 -36
  40. package/public/js/components/Sidebar.js +380 -380
  41. package/public/js/components/TerminalInstance.js +28 -0
  42. package/public/js/components/useDragSort.js +67 -67
  43. package/public/js/dialog.js +67 -67
  44. package/public/js/icons.js +212 -212
  45. package/public/js/main.js +296 -296
  46. package/public/js/pages/AboutPage.js +90 -90
  47. package/public/js/pages/ConfigurePage.js +728 -713
  48. package/public/js/pages/LaunchPage.js +421 -421
  49. package/public/js/pages/RemotePage.js +743 -743
  50. package/public/js/pages/SessionsPage.js +53 -53
  51. package/public/js/state.js +335 -335
  52. package/scripts/dev.js +149 -149
  53. package/scripts/install.js +153 -153
  54. package/scripts/restart-helper.js +96 -96
  55. package/scripts/upgrade-helper.js +687 -687
  56. package/server.js +1820 -1807
  57. package/public/manifest.webmanifest +0 -25
  58. package/public/setup/index.html +0 -567
@@ -1,126 +1,126 @@
1
- 'use strict';
2
-
3
- // ccsm in-process PTY pool. Used by the "web terminal" launch path:
4
- // claude (or any cmd) runs as a child of ccsm and its stdio is bridged
5
- // to one or more WebSocket clients via xterm.js in the browser.
6
- //
7
- // Lifecycle: a PTY entry is created by spawn(), broadcasts every output
8
- // chunk to all attached sockets, keeps a rolling history ring so a fresh
9
- // connection can replay recent output. attach() wires a websocket to an
10
- // entry, kill() ends a PTY explicitly, list() returns metadata for UI.
11
- //
12
- // node-pty is optional (Windows native binary). If it failed to load,
13
- // `available` is false and spawn() throws — server.js gates the
14
- // /api/sessions/web route on this flag so install failures degrade
15
- // gracefully to wt-only mode.
16
-
17
- const path = require('node:path');
18
-
19
- let pty = null;
20
- let loadError = null;
21
- try {
22
- pty = require('node-pty');
23
- } catch (e) {
24
- loadError = e;
25
- }
26
-
27
- const HISTORY_BYTES = 256 * 1024;
28
-
29
- // Map<id, { id, pty, history, sockets:Set<ws>, meta }>
30
- const sessions = new Map();
31
-
32
- function genId() {
33
- return 'web-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
34
- }
35
-
36
-
37
-
38
- // Spawn a new PTY. `command` and `args` are passed straight to node-pty.
39
- // `meta` is whatever the caller wants surfaced to the UI (title, cwd, etc).
40
- // `id` lets the caller dictate the session id (so persistedSessions can
41
- // keep PTY id == record id); defaults to an auto-generated one.
42
- // `onData` / `onExit` are optional callbacks fired alongside the built-in
43
- // history-recording + socket-broadcast, so persistedSessions can mark
44
- // status/lastActiveAt without us having to drill the dependency in here.
45
- // Throws if node-pty isn't available.
46
- function spawn({ command, args = [], cwd, env, cols = 120, rows = 30, meta = {}, id, onData, onExit }) {
47
- if (!pty) {
48
- const err = new Error('node-pty is not available · ' + (loadError && loadError.message || 'unknown'));
49
- err.code = 'PTY_UNAVAILABLE';
50
- throw err;
51
- }
52
- const entryId = id || genId();
53
- const ptyOpts = {
54
- name: 'xterm-256color',
55
- cols, rows,
56
- cwd: cwd ? path.resolve(cwd) : process.cwd(),
57
- // Advertise 24-bit colour. xterm.js renders truecolor, and CLIs like
58
- // claude paint their diffs with 24-bit DARK red/green backgrounds +
59
- // white text when COLORTERM says so. Without it they downgrade to the
60
- // bright ANSI-16 red/green backgrounds, which wash the white foreground
61
- // out (the "white text gets buried" report). VSCode sets the same on its
62
- // terminal PTYs; our ANSI-16 palette already matches VSCode's verbatim.
63
- env: { ...process.env, ...(env || {}), COLORTERM: 'truecolor' },
64
- };
65
- if (process.platform === 'win32') {
66
- ptyOpts.useConpty = true;
67
- ptyOpts.useConptyDll = true;
68
- }
69
- const proc = pty.spawn(command, args, ptyOpts);
70
- const entry = {
71
- id: entryId,
72
- pty: proc,
73
- history: '',
74
- sockets: new Set(),
75
- meta: { ...meta, startedAt: Date.now(), command, args, cwd: cwd || process.cwd(), pid: proc.pid },
76
- exitCode: null,
77
- exitedAt: null,
78
- onDataExtra: onData,
79
- onExitExtra: onExit,
80
- };
81
- proc.onData((data) => {
82
- entry.history = (entry.history + data);
83
- if (entry.history.length > HISTORY_BYTES) {
84
- entry.history = entry.history.slice(-HISTORY_BYTES);
85
- }
86
- const frame = JSON.stringify({ type: 'output', data });
87
- for (const ws of entry.sockets) {
88
- try { ws.send(frame); } catch {}
89
- }
90
- if (entry.onDataExtra) { try { entry.onDataExtra(data); } catch {} }
91
- });
92
- proc.onExit(({ exitCode, signal }) => {
93
- // If a respawn replaced us in the pool (same entryId, new entry
94
- // object), do not touch persistedSessions or schedule a delete —
95
- // those belong to the new entry now. Without this guard, a slow-
96
- // dying old PTY would fire markExited on the same sessionId and
97
- // clobber the new spawn's markRunning state.
98
- if (sessions.get(entryId) !== entry) return;
99
- entry.exitCode = exitCode;
100
- entry.exitedAt = Date.now();
101
- const frame = JSON.stringify({ type: 'exit', code: exitCode, signal });
102
- for (const ws of entry.sockets) {
103
- try { ws.send(frame); } catch {}
104
- }
105
- if (entry.onExitExtra) { try { entry.onExitExtra({ exitCode, signal }); } catch {} }
106
- setTimeout(() => {
107
- if (sessions.get(entryId) === entry) sessions.delete(entryId);
108
- }, 30_000);
109
- });
110
- // If a previous entry exists under the same id (respawn), kill its
111
- // pty so we don't have zombie claude.exe processes hanging on. The
112
- // onExit guard above ensures its callback no-ops once we've taken
113
- // over the slot.
114
- const prev = sessions.get(entryId);
115
- if (prev && !prev.exitedAt) {
116
- try { prev.pty.kill(); } catch {}
117
- }
118
- sessions.set(entryId, entry);
119
- return entry;
120
- }
121
-
122
- // Strip ANSI sequences from history that would cause spurious
123
- // terminal-to-host responses if a fresh xterm.js re-parses the replay.
1
+ 'use strict';
2
+
3
+ // ccsm in-process PTY pool. Used by the "web terminal" launch path:
4
+ // claude (or any cmd) runs as a child of ccsm and its stdio is bridged
5
+ // to one or more WebSocket clients via xterm.js in the browser.
6
+ //
7
+ // Lifecycle: a PTY entry is created by spawn(), broadcasts every output
8
+ // chunk to all attached sockets, keeps a rolling history ring so a fresh
9
+ // connection can replay recent output. attach() wires a websocket to an
10
+ // entry, kill() ends a PTY explicitly, list() returns metadata for UI.
11
+ //
12
+ // node-pty is optional (Windows native binary). If it failed to load,
13
+ // `available` is false and spawn() throws — server.js gates the
14
+ // /api/sessions/web route on this flag so install failures degrade
15
+ // gracefully to wt-only mode.
16
+
17
+ const path = require('node:path');
18
+
19
+ let pty = null;
20
+ let loadError = null;
21
+ try {
22
+ pty = require('node-pty');
23
+ } catch (e) {
24
+ loadError = e;
25
+ }
26
+
27
+ const HISTORY_BYTES = 256 * 1024;
28
+
29
+ // Map<id, { id, pty, history, sockets:Set<ws>, meta }>
30
+ const sessions = new Map();
31
+
32
+ function genId() {
33
+ return 'web-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
34
+ }
35
+
36
+
37
+
38
+ // Spawn a new PTY. `command` and `args` are passed straight to node-pty.
39
+ // `meta` is whatever the caller wants surfaced to the UI (title, cwd, etc).
40
+ // `id` lets the caller dictate the session id (so persistedSessions can
41
+ // keep PTY id == record id); defaults to an auto-generated one.
42
+ // `onData` / `onExit` are optional callbacks fired alongside the built-in
43
+ // history-recording + socket-broadcast, so persistedSessions can mark
44
+ // status/lastActiveAt without us having to drill the dependency in here.
45
+ // Throws if node-pty isn't available.
46
+ function spawn({ command, args = [], cwd, env, cols = 120, rows = 30, meta = {}, id, onData, onExit }) {
47
+ if (!pty) {
48
+ const err = new Error('node-pty is not available · ' + (loadError && loadError.message || 'unknown'));
49
+ err.code = 'PTY_UNAVAILABLE';
50
+ throw err;
51
+ }
52
+ const entryId = id || genId();
53
+ const ptyOpts = {
54
+ name: 'xterm-256color',
55
+ cols, rows,
56
+ cwd: cwd ? path.resolve(cwd) : process.cwd(),
57
+ // Advertise 24-bit colour. xterm.js renders truecolor, and CLIs like
58
+ // claude paint their diffs with 24-bit DARK red/green backgrounds +
59
+ // white text when COLORTERM says so. Without it they downgrade to the
60
+ // bright ANSI-16 red/green backgrounds, which wash the white foreground
61
+ // out (the "white text gets buried" report). VSCode sets the same on its
62
+ // terminal PTYs; our ANSI-16 palette already matches VSCode's verbatim.
63
+ env: { ...process.env, ...(env || {}), COLORTERM: 'truecolor' },
64
+ };
65
+ if (process.platform === 'win32') {
66
+ ptyOpts.useConpty = true;
67
+ ptyOpts.useConptyDll = true;
68
+ }
69
+ const proc = pty.spawn(command, args, ptyOpts);
70
+ const entry = {
71
+ id: entryId,
72
+ pty: proc,
73
+ history: '',
74
+ sockets: new Set(),
75
+ meta: { ...meta, startedAt: Date.now(), command, args, cwd: cwd || process.cwd(), pid: proc.pid },
76
+ exitCode: null,
77
+ exitedAt: null,
78
+ onDataExtra: onData,
79
+ onExitExtra: onExit,
80
+ };
81
+ proc.onData((data) => {
82
+ entry.history = (entry.history + data);
83
+ if (entry.history.length > HISTORY_BYTES) {
84
+ entry.history = entry.history.slice(-HISTORY_BYTES);
85
+ }
86
+ const frame = JSON.stringify({ type: 'output', data });
87
+ for (const ws of entry.sockets) {
88
+ try { ws.send(frame); } catch {}
89
+ }
90
+ if (entry.onDataExtra) { try { entry.onDataExtra(data); } catch {} }
91
+ });
92
+ proc.onExit(({ exitCode, signal }) => {
93
+ // If a respawn replaced us in the pool (same entryId, new entry
94
+ // object), do not touch persistedSessions or schedule a delete —
95
+ // those belong to the new entry now. Without this guard, a slow-
96
+ // dying old PTY would fire markExited on the same sessionId and
97
+ // clobber the new spawn's markRunning state.
98
+ if (sessions.get(entryId) !== entry) return;
99
+ entry.exitCode = exitCode;
100
+ entry.exitedAt = Date.now();
101
+ const frame = JSON.stringify({ type: 'exit', code: exitCode, signal });
102
+ for (const ws of entry.sockets) {
103
+ try { ws.send(frame); } catch {}
104
+ }
105
+ if (entry.onExitExtra) { try { entry.onExitExtra({ exitCode, signal }); } catch {} }
106
+ setTimeout(() => {
107
+ if (sessions.get(entryId) === entry) sessions.delete(entryId);
108
+ }, 30_000);
109
+ });
110
+ // If a previous entry exists under the same id (respawn), kill its
111
+ // pty so we don't have zombie claude.exe processes hanging on. The
112
+ // onExit guard above ensures its callback no-ops once we've taken
113
+ // over the slot.
114
+ const prev = sessions.get(entryId);
115
+ if (prev && !prev.exitedAt) {
116
+ try { prev.pty.kill(); } catch {}
117
+ }
118
+ sessions.set(entryId, entry);
119
+ return entry;
120
+ }
121
+
122
+ // Strip ANSI sequences from history that would cause spurious
123
+ // terminal-to-host responses if a fresh xterm.js re-parses the replay.
124
124
  // Specifically: device-attribute / device-status queries (CSI c, CSI 0c,
125
125
  // CSI >0c, CSI 5n, CSI 6n, …) and OSC 10/11 default color queries. The
126
126
  // original xterm already answered them, but on attach we replay everything;
@@ -134,110 +134,110 @@ function scrubReplayResponses(history) {
134
134
  // CSI [ ? Ps c (primary DA query)
135
135
  .replace(/\x1b\[[?>0-9]*c/g, '')
136
136
  // CSI [ Ps n (device status / cursor position queries)
137
- .replace(/\x1b\[[?>0-9;]*n/g, '');
138
- }
139
-
140
- // Wire a websocket to a session. Replays history immediately so the
141
- // client sees recent context; then forwards input/resize messages from
142
- // the client to the PTY and broadcast outputs back via onData above.
143
- function attach(id, ws) {
144
- const entry = sessions.get(id);
145
- if (!entry) {
146
- try { ws.close(4404, 'no such terminal'); } catch {}
147
- return;
148
- }
149
- // Latest-wins: a session has at most one live WebSocket at any
150
- // moment. The new attach displaces any existing ones — keeps PTY
151
- // resize semantics unambiguous (each client sends its own dimensions
152
- // for its own viewport; with two clients fighting, claude's TUI
153
- // re-renders for whichever sent last and the other side sees a
154
- // mis-sized layout). Close code 4001 + reason lets the displaced
155
- // client show a clearer message than the generic "[disconnected]".
156
- for (const other of entry.sockets) {
157
- try { other.close(4001, 'displaced by another client'); } catch {}
158
- }
137
+ .replace(/\x1b\[[?>0-9;]*n/g, '');
138
+ }
139
+
140
+ // Wire a websocket to a session. Replays history immediately so the
141
+ // client sees recent context; then forwards input/resize messages from
142
+ // the client to the PTY and broadcast outputs back via onData above.
143
+ function attach(id, ws) {
144
+ const entry = sessions.get(id);
145
+ if (!entry) {
146
+ try { ws.close(4404, 'no such terminal'); } catch {}
147
+ return;
148
+ }
149
+ // Latest-wins: a session has at most one live WebSocket at any
150
+ // moment. The new attach displaces any existing ones — keeps PTY
151
+ // resize semantics unambiguous (each client sends its own dimensions
152
+ // for its own viewport; with two clients fighting, claude's TUI
153
+ // re-renders for whichever sent last and the other side sees a
154
+ // mis-sized layout). Close code 4001 + reason lets the displaced
155
+ // client show a clearer message than the generic "[disconnected]".
156
+ for (const other of entry.sockets) {
157
+ try { other.close(4001, 'displaced by another client'); } catch {}
158
+ }
159
159
  entry.sockets.clear();
160
160
  entry.sockets.add(ws);
161
161
  if (entry.history) {
162
162
  try { ws.send(JSON.stringify({ type: 'output', data: scrubReplayResponses(entry.history), replay: true })); } catch {}
163
163
  }
164
- if (entry.exitedAt) {
165
- try { ws.send(JSON.stringify({ type: 'exit', code: entry.exitCode })); } catch {}
166
- } else {
167
- try { ws.send(JSON.stringify({ type: 'attached', meta: entry.meta })); } catch {}
168
- }
169
-
170
- ws.on('message', (msg) => {
171
- let event;
172
- try { event = JSON.parse(msg.toString()); } catch { return; }
173
- if (entry.exitedAt) return; // PTY is dead, ignore further input
174
- switch (event.type) {
175
- case 'input':
176
- if (typeof event.data === 'string') {
177
- if (process.env.CCSM_DEBUG_PASTE === '1') {
178
- const d = event.data;
179
- const hex = Buffer.from(d, 'utf8').toString('hex').match(/.{1,2}/g).join(' ');
180
- console.log(`[pty.write id=${id}] len=${d.length} hex=${hex.slice(0, 400)}${hex.length > 400 ? '...' : ''}`);
181
- }
182
- entry.pty.write(event.data);
183
- }
184
- break;
185
- case 'resize':
186
- if (Number(event.cols) > 0 && Number(event.rows) > 0) {
187
- try { entry.pty.resize(Number(event.cols), Number(event.rows)); } catch {}
188
- }
189
- break;
190
- case 'kill':
191
- kill(id);
192
- break;
193
- }
194
- });
195
-
196
- ws.on('close', () => {
197
- entry.sockets.delete(ws);
198
- });
199
- }
200
-
201
- function kill(id) {
202
- const entry = sessions.get(id);
203
- if (!entry || entry.exitedAt) return false;
204
- try { entry.pty.kill(); } catch {}
205
- return true;
206
- }
207
-
208
- // Public summary for the frontend. Don't leak the pty / sockets objects.
209
- function describe(entry) {
210
- return {
211
- id: entry.id,
212
- meta: entry.meta,
213
- attached: entry.sockets.size,
214
- exitedAt: entry.exitedAt,
215
- exitCode: entry.exitCode,
216
- };
217
- }
218
-
219
- function list() {
220
- return Array.from(sessions.values()).map(describe);
221
- }
222
-
223
- function get(id) {
224
- const e = sessions.get(id);
225
- return e ? describe(e) : null;
226
- }
227
-
228
- function killAll() {
229
- for (const e of sessions.values()) {
230
- try { e.pty.kill(); } catch {}
231
- }
232
- }
233
-
234
- module.exports = {
235
- available: !!pty,
236
- loadError,
237
- spawn,
238
- attach,
239
- kill,
240
- list,
241
- get,
242
- killAll,
243
- };
164
+ if (entry.exitedAt) {
165
+ try { ws.send(JSON.stringify({ type: 'exit', code: entry.exitCode })); } catch {}
166
+ } else {
167
+ try { ws.send(JSON.stringify({ type: 'attached', meta: entry.meta })); } catch {}
168
+ }
169
+
170
+ ws.on('message', (msg) => {
171
+ let event;
172
+ try { event = JSON.parse(msg.toString()); } catch { return; }
173
+ if (entry.exitedAt) return; // PTY is dead, ignore further input
174
+ switch (event.type) {
175
+ case 'input':
176
+ if (typeof event.data === 'string') {
177
+ if (process.env.CCSM_DEBUG_PASTE === '1') {
178
+ const d = event.data;
179
+ const hex = Buffer.from(d, 'utf8').toString('hex').match(/.{1,2}/g).join(' ');
180
+ console.log(`[pty.write id=${id}] len=${d.length} hex=${hex.slice(0, 400)}${hex.length > 400 ? '...' : ''}`);
181
+ }
182
+ entry.pty.write(event.data);
183
+ }
184
+ break;
185
+ case 'resize':
186
+ if (Number(event.cols) > 0 && Number(event.rows) > 0) {
187
+ try { entry.pty.resize(Number(event.cols), Number(event.rows)); } catch {}
188
+ }
189
+ break;
190
+ case 'kill':
191
+ kill(id);
192
+ break;
193
+ }
194
+ });
195
+
196
+ ws.on('close', () => {
197
+ entry.sockets.delete(ws);
198
+ });
199
+ }
200
+
201
+ function kill(id) {
202
+ const entry = sessions.get(id);
203
+ if (!entry || entry.exitedAt) return false;
204
+ try { entry.pty.kill(); } catch {}
205
+ return true;
206
+ }
207
+
208
+ // Public summary for the frontend. Don't leak the pty / sockets objects.
209
+ function describe(entry) {
210
+ return {
211
+ id: entry.id,
212
+ meta: entry.meta,
213
+ attached: entry.sockets.size,
214
+ exitedAt: entry.exitedAt,
215
+ exitCode: entry.exitCode,
216
+ };
217
+ }
218
+
219
+ function list() {
220
+ return Array.from(sessions.values()).map(describe);
221
+ }
222
+
223
+ function get(id) {
224
+ const e = sessions.get(id);
225
+ return e ? describe(e) : null;
226
+ }
227
+
228
+ function killAll() {
229
+ for (const e of sessions.values()) {
230
+ try { e.pty.kill(); } catch {}
231
+ }
232
+ }
233
+
234
+ module.exports = {
235
+ available: !!pty,
236
+ loadError,
237
+ spawn,
238
+ attach,
239
+ kill,
240
+ list,
241
+ get,
242
+ killAll,
243
+ };