windows-development-sandbox 1.1.0 → 1.4.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/.sandbox-config.example.json +2 -1
- package/README.md +3 -2
- package/config.example.json +39 -5
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +3 -1
- package/dist/cli/boot-progress.d.ts +22 -0
- package/dist/cli/boot-progress.js +59 -0
- package/dist/cli/commands/accounts.js +2 -2
- package/dist/cli/commands/agent-commands.js +1 -1
- package/dist/cli/commands/context.js +2 -1
- package/dist/cli/commands/data/clean.js +6 -1
- package/dist/cli/commands/data/collect.js +3 -3
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch.d.ts +5 -2
- package/dist/cli/commands/launch.js +113 -25
- package/dist/cli/commands/maintenance.d.ts +2 -0
- package/dist/cli/commands/maintenance.js +70 -4
- package/dist/cli/commands/project.d.ts +10 -6
- package/dist/cli/commands/project.js +42 -3
- package/dist/cli/commands/restart.js +17 -7
- package/dist/cli/commands/sessions.js +5 -4
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/manifest.js +6 -1
- package/dist/cli/menu/apps.d.ts +21 -0
- package/dist/cli/menu/apps.js +122 -0
- package/dist/cli/menu/command-form.d.ts +28 -0
- package/dist/cli/menu/command-form.js +219 -0
- package/dist/cli/menu/index.d.ts +26 -0
- package/dist/cli/menu/index.js +180 -0
- package/dist/cli/menu/settings/categories.d.ts +9 -0
- package/dist/cli/menu/settings/categories.js +28 -0
- package/dist/cli/menu/settings/config-file.d.ts +40 -0
- package/dist/cli/menu/settings/config-file.js +71 -0
- package/dist/cli/menu/settings/editors.d.ts +30 -0
- package/dist/cli/menu/settings/editors.js +305 -0
- package/dist/cli/menu/settings/index.d.ts +32 -0
- package/dist/cli/menu/settings/index.js +136 -0
- package/dist/cli/menu/settings/schema-node.d.ts +62 -0
- package/dist/cli/menu/settings/schema-node.js +104 -0
- package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
- package/dist/cli/menu/several.js +102 -0
- package/dist/cli/progress.d.ts +15 -0
- package/dist/cli/progress.js +52 -0
- package/dist/cli/tui/app.d.ts +39 -0
- package/dist/cli/tui/app.js +52 -0
- package/dist/cli/tui/dialogs.d.ts +57 -0
- package/dist/cli/tui/dialogs.js +162 -0
- package/dist/cli/tui/frame.d.ts +18 -0
- package/dist/cli/tui/frame.js +27 -0
- package/dist/cli/tui/index.d.ts +7 -0
- package/dist/cli/tui/index.js +8 -0
- package/dist/cli/tui/list-screen.d.ts +101 -0
- package/dist/cli/tui/list-screen.js +299 -0
- package/dist/cli/tui/terminal.d.ts +37 -0
- package/dist/cli/tui/terminal.js +89 -0
- package/dist/cli/tui/text.d.ts +20 -0
- package/dist/cli/tui/text.js +105 -0
- package/dist/cli/tui/theme.d.ts +45 -0
- package/dist/cli/tui/theme.js +30 -0
- package/dist/core/config.js +10 -7
- package/dist/core/local-config.d.ts +31 -1
- package/dist/core/local-config.js +72 -7
- package/dist/core/project-policy.d.ts +50 -3
- package/dist/core/project-policy.js +90 -4
- package/dist/core/schema/apps.d.ts +40 -0
- package/dist/core/schema/apps.js +27 -0
- package/dist/core/schema/config.d.ts +69 -3
- package/dist/core/schema/config.js +18 -6
- package/dist/core/schema/profile.d.ts +5 -0
- package/dist/core/schema/profile.js +4 -2
- package/dist/core/schema/projects.d.ts +1 -0
- package/dist/core/schema/projects.js +1 -0
- package/dist/core/schema/sandbox.d.ts +29 -1
- package/dist/core/schema/sandbox.js +29 -1
- package/dist/core/schema/toolchains.d.ts +3 -0
- package/dist/core/schema/toolchains.js +6 -1
- package/dist/core/types.d.ts +24 -1
- package/dist/core/util.d.ts +2 -0
- package/dist/core/util.js +2 -0
- package/dist/core/workspace.d.ts +21 -4
- package/dist/core/workspace.js +99 -21
- package/dist/guest/apps/index.d.ts +10 -0
- package/dist/guest/apps/index.js +206 -0
- package/dist/guest/apps/resolvers.d.ts +8 -0
- package/dist/guest/apps/resolvers.js +90 -0
- package/dist/guest/audit-parse.d.ts +7 -0
- package/dist/guest/audit-parse.js +24 -0
- package/dist/guest/audit.d.ts +4 -0
- package/dist/guest/audit.js +47 -0
- package/dist/guest/daemon/relays.d.ts +1 -0
- package/dist/guest/daemon/relays.js +18 -0
- package/dist/guest/daemon.js +4 -0
- package/dist/guest/env.d.ts +7 -2
- package/dist/guest/env.js +17 -3
- package/dist/guest/helpers.d.ts +1 -1
- package/dist/guest/main.js +20 -3
- package/dist/guest/netproxy.d.ts +1 -0
- package/dist/guest/netproxy.js +100 -0
- package/dist/guest/ptyhost.js +14 -4
- package/dist/guest/setup/claude.d.ts +0 -4
- package/dist/guest/setup/claude.js +48 -2
- package/dist/guest/setup/network.d.ts +9 -0
- package/dist/guest/setup/network.js +97 -4
- package/dist/guest/setup/user.js +4 -0
- package/dist/guest/toolchains/index.js +29 -3
- package/dist/guest/toolchains/native/mesa.js +13 -3
- package/dist/guest/toolchains/plan.d.ts +3 -1
- package/dist/guest/toolchains/plan.js +53 -9
- package/dist/guest/toolchains/python.js +26 -2
- package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
- package/dist/guest/toolchains/vscode-profile.js +69 -0
- package/dist/guest/toolchains/vscode.d.ts +17 -0
- package/dist/guest/toolchains/vscode.js +87 -1
- package/dist/host/attach.js +2 -1
- package/dist/host/audit/collect.d.ts +34 -0
- package/dist/host/audit/collect.js +126 -0
- package/dist/host/audit/log.d.ts +46 -0
- package/dist/host/audit/log.js +143 -0
- package/dist/host/channel.d.ts +2 -2
- package/dist/host/channel.js +3 -2
- package/dist/host/generate.d.ts +7 -1
- package/dist/host/generate.js +23 -6
- package/dist/host/join.d.ts +7 -0
- package/dist/host/join.js +23 -9
- package/dist/host/launch/mappings.d.ts +7 -2
- package/dist/host/launch/mappings.js +68 -18
- package/dist/host/launch/projects.d.ts +2 -0
- package/dist/host/launch/projects.js +11 -5
- package/dist/host/launch/stage.js +1 -1
- package/dist/host/motw.d.ts +17 -0
- package/dist/host/motw.js +74 -0
- package/dist/host/platform.js +3 -0
- package/dist/host/port-forward.d.ts +8 -0
- package/dist/host/port-forward.js +37 -0
- package/dist/host/projects.d.ts +25 -2
- package/dist/host/projects.js +54 -5
- package/dist/host/repo-guard.d.ts +10 -3
- package/dist/host/repo-guard.js +142 -14
- package/dist/host/stops.d.ts +67 -0
- package/dist/host/stops.js +126 -0
- package/dist/host/terminal.d.ts +8 -0
- package/dist/host/terminal.js +45 -1
- package/dist/host/workspace-view/index.d.ts +20 -0
- package/dist/host/workspace-view/index.js +53 -0
- package/dist/host/workspace-view/rules.d.ts +26 -0
- package/dist/host/workspace-view/rules.js +93 -0
- package/dist/host/workspace-view/view.d.ts +50 -0
- package/dist/host/workspace-view/view.js +281 -0
- package/dist/runtime/daemon.js +1387 -558
- package/dist/runtime/db.js +199 -14
- package/dist/runtime/main.js +3082 -631
- package/dist/runtime/netproxy.js +25948 -0
- package/dist/runtime/view.js +531 -148
- package/dist/selftest/audit.test.d.ts +2 -0
- package/dist/selftest/audit.test.js +57 -0
- package/dist/selftest/channel.test.js +45 -2
- package/dist/selftest/cli.test.js +47 -17
- package/dist/selftest/fixtures.d.ts +47 -1
- package/dist/selftest/fixtures.js +3 -1
- package/dist/selftest/generate.test.js +7 -2
- package/dist/selftest/index.js +6 -2
- package/dist/selftest/live-network.test.d.ts +2 -0
- package/dist/selftest/live-network.test.js +63 -0
- package/dist/selftest/live-terminal.test.d.ts +2 -0
- package/dist/selftest/live-terminal.test.js +169 -0
- package/dist/selftest/local-config.test.js +42 -4
- package/dist/selftest/mirror.test.js +36 -7
- package/dist/selftest/network.test.js +12 -0
- package/dist/selftest/projects.test.js +77 -8
- package/dist/selftest/repo-guard.test.js +59 -1
- package/dist/selftest/sync.test.js +50 -10
- package/dist/selftest/toolchains.test.js +30 -0
- package/dist/selftest/workspace-view.test.d.ts +2 -0
- package/dist/selftest/workspace-view.test.js +89 -0
- package/dist/shared/apps.d.ts +49 -0
- package/dist/shared/apps.js +114 -0
- package/dist/shared/console-input.d.ts +11 -0
- package/dist/shared/console-input.js +61 -0
- package/dist/shared/download.d.ts +3 -0
- package/dist/shared/download.js +25 -2
- package/dist/shared/guest-paths.d.ts +27 -0
- package/dist/shared/guest-paths.js +68 -0
- package/dist/shared/netpolicy.d.ts +27 -0
- package/dist/shared/netpolicy.js +51 -0
- package/dist/shared/projects.d.ts +27 -4
- package/dist/shared/projects.js +72 -13
- package/dist/shared/pty-client.d.ts +26 -4
- package/dist/shared/pty-client.js +204 -25
- package/dist/shared/rundir.d.ts +2 -0
- package/dist/shared/rundir.js +2 -0
- package/dist/shared/schema.d.ts +34 -0
- package/dist/shared/schema.js +14 -0
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/sync/claude-mem.js +3 -2
- package/dist/sync/history.d.ts +6 -0
- package/dist/sync/history.js +26 -11
- package/dist/sync/watcher.js +78 -6
- package/package.json +2 -1
- package/sandbox/cargoshim.cs +56 -0
- package/sandbox/cpucap.cs +33 -0
- package/sandbox/cpucap.ps1 +4 -35
- package/sandbox/logon.cmd +6 -0
- package/sandbox/memlog.ps1 +34 -0
- package/sandbox/pagefile.cs +31 -0
- package/sandbox/pagefile.ps1 +21 -0
- package/sandbox/ptyhost.cs +162 -7
- package/schemas/config.schema.json +318 -3
- package/schemas/sandbox-config.schema.json +129 -14
- package/dist/cli/menu-multi.js +0 -82
- package/dist/cli/menu.d.ts +0 -14
- package/dist/cli/menu.js +0 -290
- package/dist/cli/settings/edit.d.ts +0 -13
- package/dist/cli/settings/edit.js +0 -19
- package/dist/cli/settings/index.d.ts +0 -8
- package/dist/cli/settings/index.js +0 -131
- package/dist/cli/settings/prompts.d.ts +0 -7
- package/dist/cli/settings/prompts.js +0 -48
- package/dist/cli/settings/schema.d.ts +0 -33
- package/dist/cli/settings/schema.js +0 -29
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Client side of the ptyhost protocol (see sandbox/ptyhost.cs). Used by the window inside the sandbox and by
|
|
2
2
|
// `--terminal` / `attach` on the host, so it only depends on Node built-ins.
|
|
3
3
|
import net from 'node:net';
|
|
4
|
-
|
|
4
|
+
import { enableVtInput } from './console-input.js';
|
|
5
|
+
export const FrameType = { Hello: 1, Input: 2, Resize: 3, Restart: 4, Ping: 5, Output: 16, Exit: 17, Error: 18, Restarting: 19, Status: 20, Console: 21 };
|
|
5
6
|
/** HELLO flags: control-only connections get no output and never size the console; a query only asks for STATUS. */
|
|
6
7
|
export const HelloFlag = { ControlOnly: 1, Query: 2 };
|
|
7
8
|
/** Viewers ping this often; ptyhost counts a viewer as gone after 15 s of silence (e.g. its terminal tab was closed). */
|
|
8
9
|
export const PING_INTERVAL_MS = 5000;
|
|
10
|
+
/** Turns off every mouse tracking mode a program may have turned on in your terminal (normal, button, any, UTF-8, SGR, urxvt). */
|
|
11
|
+
export const MOUSE_OFF = '\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1015l';
|
|
9
12
|
/** Ctrl+] — the same detach key telnet uses; Claude Code does not bind it. */
|
|
10
13
|
export const DETACH_KEY = 0x1d;
|
|
11
14
|
export const encodeFrame = (type, payload = Buffer.alloc(0)) => {
|
|
@@ -47,23 +50,140 @@ export class FrameReader {
|
|
|
47
50
|
return frames;
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
/** OSC commands that may reach your terminal: window title (0, 1, 2), links (8), progress (9;4), shell marks (133). */
|
|
54
|
+
const KEPT_OSC = (body) => {
|
|
55
|
+
const number = /^(\d{1,4})(;|$)/.exec(body);
|
|
56
|
+
if (!number)
|
|
57
|
+
return false;
|
|
58
|
+
const n = Number(number[1]);
|
|
59
|
+
return n <= 2 || n === 8 || n === 133 || (n === 9 && body.startsWith(number[1] + ';4;'));
|
|
60
|
+
};
|
|
61
|
+
/** Longest OSC kept (a title or a link); anything longer is dropped whole. */
|
|
62
|
+
const MAX_KEPT_STRING = 4096;
|
|
50
63
|
/**
|
|
51
|
-
* Removes
|
|
52
|
-
*
|
|
53
|
-
*
|
|
64
|
+
* Removes the terminal "strings" an untrusted program could abuse from the sandbox's output before it reaches your
|
|
65
|
+
* terminal: clipboard writes (OSC 52), terminal macros (ConEmu's OSC 9), DCS / APC / PM / SOS, and their C1 forms.
|
|
66
|
+
* Titles, links, progress and colours pass. A byte-level state machine, so no split across chunks, leading zero
|
|
67
|
+
* (ESC]052) or huge payload slips through: a dropped string stays dropped until its terminator, however long it is.
|
|
54
68
|
*/
|
|
55
69
|
export class OutputFilter {
|
|
56
|
-
|
|
70
|
+
/** text: normal output; esc: after ESC; c2: after a 0xC2 lead byte; string: inside an OSC/DCS/...; stringEsc / stringC2: a possible terminator inside one. */
|
|
71
|
+
state = 'text';
|
|
72
|
+
/** The string being read: an OSC worth keeping is collected until its terminator; null = being dropped. */
|
|
73
|
+
body = null;
|
|
57
74
|
push(chunk) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
const out = Buffer.allocUnsafe(chunk.length + MAX_KEPT_STRING + 8);
|
|
76
|
+
let n = 0;
|
|
77
|
+
const put = (...bytes) => { for (const b of bytes)
|
|
78
|
+
out[n++] = b; };
|
|
79
|
+
/** After ESC (or the C1 form): ] opens an OSC (maybe kept), P X ^ _ open DCS / SOS / PM / APC (dropped). */
|
|
80
|
+
const opens = (b, c1) => {
|
|
81
|
+
if (b === (c1 ? 0x9d : 0x5d)) {
|
|
82
|
+
this.body = c1 ? null : [];
|
|
83
|
+
this.state = 'string';
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if ((c1 ? [0x90, 0x98, 0x9e, 0x9f] : [0x50, 0x58, 0x5e, 0x5f]).includes(b)) {
|
|
87
|
+
this.body = null;
|
|
88
|
+
this.state = 'string';
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
};
|
|
93
|
+
const endString = (terminator) => {
|
|
94
|
+
const body = this.body && Buffer.from(this.body).toString('latin1');
|
|
95
|
+
if (body !== null && KEPT_OSC(body)) {
|
|
96
|
+
put(0x1b, 0x5d);
|
|
97
|
+
out.write(body, n, 'latin1');
|
|
98
|
+
n += body.length;
|
|
99
|
+
put(...terminator);
|
|
100
|
+
}
|
|
101
|
+
this.body = null;
|
|
102
|
+
this.state = 'text';
|
|
103
|
+
};
|
|
104
|
+
const collect = (b) => { if (this.body && this.body.push(b) > MAX_KEPT_STRING)
|
|
105
|
+
this.body = null; };
|
|
106
|
+
for (const b of chunk) {
|
|
107
|
+
switch (this.state) {
|
|
108
|
+
case 'text':
|
|
109
|
+
if (b === 0x1b)
|
|
110
|
+
this.state = 'esc';
|
|
111
|
+
else if (b === 0xc2)
|
|
112
|
+
this.state = 'c2';
|
|
113
|
+
else
|
|
114
|
+
put(b);
|
|
115
|
+
break;
|
|
116
|
+
case 'esc':
|
|
117
|
+
if (opens(b, false))
|
|
118
|
+
break;
|
|
119
|
+
if (b !== 0x1b) {
|
|
120
|
+
put(0x1b, b);
|
|
121
|
+
this.state = 'text';
|
|
122
|
+
}
|
|
123
|
+
else
|
|
124
|
+
put(0x1b);
|
|
125
|
+
break;
|
|
126
|
+
case 'c2':
|
|
127
|
+
if (opens(b, true))
|
|
128
|
+
break;
|
|
129
|
+
if (b === 0x1b) {
|
|
130
|
+
put(0xc2);
|
|
131
|
+
this.state = 'esc';
|
|
132
|
+
}
|
|
133
|
+
else if (b === 0xc2)
|
|
134
|
+
put(0xc2);
|
|
135
|
+
else {
|
|
136
|
+
put(0xc2, b);
|
|
137
|
+
this.state = 'text';
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
case 'string':
|
|
141
|
+
if (b === 0x07)
|
|
142
|
+
endString([0x07]);
|
|
143
|
+
else if (b === 0x1b)
|
|
144
|
+
this.state = 'stringEsc';
|
|
145
|
+
else if (b === 0xc2)
|
|
146
|
+
this.state = 'stringC2';
|
|
147
|
+
else if (b === 0x18 || b === 0x1a) {
|
|
148
|
+
this.body = null;
|
|
149
|
+
this.state = 'text';
|
|
150
|
+
}
|
|
151
|
+
else
|
|
152
|
+
collect(b);
|
|
153
|
+
break;
|
|
154
|
+
case 'stringC2':
|
|
155
|
+
// U+009C is the C1 string terminator; any other character is part of the string.
|
|
156
|
+
if (b === 0x9c)
|
|
157
|
+
endString([0x1b, 0x5c]);
|
|
158
|
+
else {
|
|
159
|
+
collect(0xc2);
|
|
160
|
+
this.state = 'string';
|
|
161
|
+
if (b === 0x07)
|
|
162
|
+
endString([0x07]);
|
|
163
|
+
else if (b === 0x1b)
|
|
164
|
+
this.state = 'stringEsc';
|
|
165
|
+
else if (b !== 0xc2)
|
|
166
|
+
collect(b);
|
|
167
|
+
else
|
|
168
|
+
this.state = 'stringC2';
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
case 'stringEsc':
|
|
172
|
+
if (b === 0x5c) {
|
|
173
|
+
endString([0x1b, 0x5c]);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
// ESC inside a string ends it (dropped) and starts a new sequence.
|
|
177
|
+
this.body = null;
|
|
178
|
+
this.state = 'esc';
|
|
179
|
+
if (!opens(b, false) && b !== 0x1b) {
|
|
180
|
+
put(0x1b, b);
|
|
181
|
+
this.state = 'text';
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
65
185
|
}
|
|
66
|
-
return
|
|
186
|
+
return out.subarray(0, n);
|
|
67
187
|
}
|
|
68
188
|
}
|
|
69
189
|
const connectOnce = (host, port, timeoutMs) => {
|
|
@@ -138,6 +258,41 @@ export const fixWindowsKeys = (chunk) => {
|
|
|
138
258
|
return chunk;
|
|
139
259
|
return Buffer.from(chunk.map((b) => (b === 0x08 ? 0x7f : b === 0x7f ? 0x08 : b)));
|
|
140
260
|
};
|
|
261
|
+
/** How long an unfinished escape sequence is held back waiting for the rest (a lone Esc key press waits this long). */
|
|
262
|
+
export const JOIN_MS = 30;
|
|
263
|
+
/** Longest sequence held back; anything longer is not a key, mouse report or terminal reply, and is sent as it is. */
|
|
264
|
+
const MAX_HELD = 64;
|
|
265
|
+
/**
|
|
266
|
+
* Keeps each escape sequence typed into your terminal (keys, mouse reports, the terminal's replies) in one INPUT frame.
|
|
267
|
+
* The console can hand one over in pieces; ConPTY reads a lone "ESC[" as Alt+[ and the rest as typed text, so a split
|
|
268
|
+
* Right arrow (ESC[C) left a stray "C" in the agent's prompt. An unfinished sequence at the end of a chunk is held
|
|
269
|
+
* until the rest arrives (or JOIN_MS passes).
|
|
270
|
+
*/
|
|
271
|
+
export class InputJoiner {
|
|
272
|
+
held = Buffer.alloc(0);
|
|
273
|
+
/** The bytes to send now; an unfinished sequence at the end is kept for the next chunk. */
|
|
274
|
+
push(chunk) {
|
|
275
|
+
const data = this.held.length ? Buffer.concat([this.held, chunk]) : chunk;
|
|
276
|
+
const cut = unfinishedSequence(data);
|
|
277
|
+
this.held = Buffer.from(data.subarray(cut));
|
|
278
|
+
return data.subarray(0, cut);
|
|
279
|
+
}
|
|
280
|
+
holding() { return this.held.length > 0; }
|
|
281
|
+
flush() { const out = this.held; this.held = Buffer.alloc(0); return out; }
|
|
282
|
+
}
|
|
283
|
+
/** Where an escape sequence that has not ended yet starts at the end of `data` (data.length if there is none). */
|
|
284
|
+
const unfinishedSequence = (data) => {
|
|
285
|
+
const start = data.lastIndexOf(0x1b, -1);
|
|
286
|
+
if (start === -1 || data.length - start > MAX_HELD)
|
|
287
|
+
return data.length;
|
|
288
|
+
const rest = data.subarray(start + 1);
|
|
289
|
+
if (rest.length === 0)
|
|
290
|
+
return start;
|
|
291
|
+
// ESC [ or ESC O (SS3) followed only by parameter / intermediate bytes: the final byte has not arrived yet.
|
|
292
|
+
if (rest[0] === 0x5b || rest[0] === 0x4f)
|
|
293
|
+
return rest.subarray(1).every((b) => b >= 0x20 && b <= 0x3f) && (rest[0] === 0x5b || rest.length === 1) ? start : data.length;
|
|
294
|
+
return data.length;
|
|
295
|
+
};
|
|
141
296
|
/**
|
|
142
297
|
* Bridges this process's terminal to the remote program until it exits, the connection drops, or the user presses
|
|
143
298
|
* the detach key. Restores the terminal before resolving.
|
|
@@ -147,13 +302,15 @@ export const runTerminalClient = (socket, token, options = {}) => {
|
|
|
147
302
|
const stdout = process.stdout;
|
|
148
303
|
// getWindowSize asks the console every time; columns/rows only change when Node noticed a resize.
|
|
149
304
|
const size = () => { const [c, r] = stdout.isTTY ? stdout.getWindowSize() : [0, 0]; return [c || 120, r || 30]; };
|
|
150
|
-
|
|
305
|
+
// Until the console passes the terminal's input through as it is (enableVtInput), Node turns Backspace into ^H.
|
|
306
|
+
let swapKeys = process.platform === 'win32' && stdin.isTTY;
|
|
151
307
|
return new Promise((resolve) => {
|
|
152
308
|
let done = false;
|
|
153
309
|
const reader = new FrameReader();
|
|
154
310
|
const filter = new OutputFilter();
|
|
155
311
|
let sent = size();
|
|
156
312
|
let resizeTimer;
|
|
313
|
+
let vtInput;
|
|
157
314
|
const finish = (result) => {
|
|
158
315
|
if (done)
|
|
159
316
|
return;
|
|
@@ -163,29 +320,45 @@ export const runTerminalClient = (socket, token, options = {}) => {
|
|
|
163
320
|
clearTimeout(resizeTimer);
|
|
164
321
|
clearInterval(heartbeat);
|
|
165
322
|
clearInterval(sizeWatch);
|
|
166
|
-
|
|
167
|
-
stdin.setRawMode(false);
|
|
168
|
-
stdin.pause();
|
|
323
|
+
clearTimeout(joinTimer);
|
|
169
324
|
socket.destroy();
|
|
170
|
-
|
|
171
|
-
|
|
325
|
+
// The agent's mouse tracking ends with it: your terminal's wheel scrolls again (and clicks select text).
|
|
326
|
+
stdout.write('\x1b[0m\x1b[?25h' + MOUSE_OFF + '\r\n');
|
|
327
|
+
// Wait for a console-mode change still in progress, then give the console back in its normal mode.
|
|
328
|
+
void (vtInput?.stop() ?? Promise.resolve()).then(() => {
|
|
329
|
+
if (stdin.isTTY)
|
|
330
|
+
stdin.setRawMode(false);
|
|
331
|
+
stdin.pause();
|
|
332
|
+
resolve(result);
|
|
333
|
+
});
|
|
172
334
|
};
|
|
335
|
+
const joiner = new InputJoiner();
|
|
336
|
+
let joinTimer;
|
|
337
|
+
const sendInput = (data) => { if (data.length && !done)
|
|
338
|
+
socket.write(encodeFrame(FrameType.Input, swapKeys ? fixWindowsKeys(data) : data)); };
|
|
173
339
|
const onInput = (chunk) => {
|
|
174
340
|
if (options.detachKey !== undefined && chunk.includes(options.detachKey)) {
|
|
175
341
|
finish({ reason: 'detach' });
|
|
176
342
|
return;
|
|
177
343
|
}
|
|
178
|
-
|
|
344
|
+
clearTimeout(joinTimer);
|
|
345
|
+
sendInput(joiner.push(chunk));
|
|
346
|
+
// A lone Esc key press is a "sequence" that never completes: send it after a moment.
|
|
347
|
+
if (joiner.holding())
|
|
348
|
+
joinTimer = setTimeout(() => sendInput(joiner.flush()), JOIN_MS);
|
|
179
349
|
};
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// then let ptyhost repaint everything once at the new size.
|
|
350
|
+
// Windows' own ConPTY repaints the whole screen on every resize; dragging a window edge fires dozens of them, and
|
|
351
|
+
// repaints racing each other left the screen garbled. Wait until the size settles, clear what this terminal
|
|
352
|
+
// reflowed on its own, then let ptyhost repaint everything once at the new size. With conpty.dll (passthrough)
|
|
353
|
+
// nothing is repainted: this terminal's own reflow and scrollback stay, and the program redraws what it needs.
|
|
354
|
+
let passthrough = false;
|
|
183
355
|
const sendSize = () => {
|
|
184
356
|
const [c, r] = size();
|
|
185
357
|
if (c === sent[0] && r === sent[1])
|
|
186
358
|
return;
|
|
187
359
|
sent = [c, r];
|
|
188
|
-
|
|
360
|
+
if (!passthrough)
|
|
361
|
+
stdout.write('\x1b[H\x1b[J');
|
|
189
362
|
socket.write(resizeFrame(c, r));
|
|
190
363
|
};
|
|
191
364
|
const onResize = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { resizeTimer = undefined; sendSize(); }, 120); };
|
|
@@ -205,8 +378,10 @@ export const runTerminalClient = (socket, token, options = {}) => {
|
|
|
205
378
|
for (const frame of frames) {
|
|
206
379
|
if (frame.type === FrameType.Output)
|
|
207
380
|
stdout.write(filter.push(frame.payload));
|
|
381
|
+
else if (frame.type === FrameType.Console)
|
|
382
|
+
passthrough = frame.payload.toString('latin1') === 'conpty.dll';
|
|
208
383
|
else if (frame.type === FrameType.Restarting)
|
|
209
|
-
stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
384
|
+
stdout.write(MOUSE_OFF + '\x1b[2J\x1b[3J\x1b[H');
|
|
210
385
|
else if (frame.type === FrameType.Exit)
|
|
211
386
|
finish({ reason: 'exit', exitCode: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
|
|
212
387
|
else if (frame.type === FrameType.Error)
|
|
@@ -220,6 +395,10 @@ export const runTerminalClient = (socket, token, options = {}) => {
|
|
|
220
395
|
socket.write(helloFrame(token, c, r));
|
|
221
396
|
if (stdin.isTTY)
|
|
222
397
|
stdin.setRawMode(true);
|
|
398
|
+
// After setRawMode (which sets the console mode): mouse reports, focus and keys then arrive as the terminal sent them.
|
|
399
|
+
vtInput = enableVtInput();
|
|
400
|
+
void vtInput.ready.then((on) => { if (on)
|
|
401
|
+
swapKeys = false; });
|
|
223
402
|
stdin.on('data', onInput);
|
|
224
403
|
stdin.resume();
|
|
225
404
|
stdout.on('resize', onResize);
|
package/dist/shared/rundir.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare const runPaths: (dataDir: string) => {
|
|
|
11
11
|
responses: string;
|
|
12
12
|
sessions: string;
|
|
13
13
|
projects: string;
|
|
14
|
+
/** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
|
|
15
|
+
audit: string;
|
|
14
16
|
request: (id: string) => string;
|
|
15
17
|
response: (id: string) => string;
|
|
16
18
|
session: (id: string) => string;
|
package/dist/shared/rundir.js
CHANGED
|
@@ -19,6 +19,8 @@ export const runPaths = (dataDir) => {
|
|
|
19
19
|
responses: path.join(run, 'responses'),
|
|
20
20
|
sessions: path.join(run, 'sessions'),
|
|
21
21
|
projects: path.join(run, 'projects'),
|
|
22
|
+
/** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
|
|
23
|
+
audit: path.join(run, 'audit'),
|
|
22
24
|
request: file('requests'),
|
|
23
25
|
response: file('responses'),
|
|
24
26
|
session: file('sessions'),
|
package/dist/shared/schema.d.ts
CHANGED
|
@@ -5,6 +5,18 @@ export declare const AttachEndpoint: z.ZodObject<{
|
|
|
5
5
|
port: z.ZodNumber;
|
|
6
6
|
token: z.ZodString;
|
|
7
7
|
}, z.core.$strict>;
|
|
8
|
+
/** The boot's progress in a shape the host can show: step n of m, what is being set up now, how many are done. */
|
|
9
|
+
export declare const BootProgress: z.ZodObject<{
|
|
10
|
+
step: z.ZodNumber;
|
|
11
|
+
steps: z.ZodNumber;
|
|
12
|
+
title: z.ZodString;
|
|
13
|
+
active: z.ZodArray<z.ZodObject<{
|
|
14
|
+
name: z.ZodString;
|
|
15
|
+
percent: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
}, z.core.$strict>>;
|
|
17
|
+
done: z.ZodNumber;
|
|
18
|
+
total: z.ZodNumber;
|
|
19
|
+
}, z.core.$strict>;
|
|
8
20
|
/** C:\AgentData\run\sandbox.json: boot progress of the sandbox as a whole. */
|
|
9
21
|
export declare const SandboxStatus: z.ZodObject<{
|
|
10
22
|
launchId: z.ZodString;
|
|
@@ -19,6 +31,18 @@ export declare const SandboxStatus: z.ZodObject<{
|
|
|
19
31
|
workspaceHost: z.ZodString;
|
|
20
32
|
guestWorkspace: z.ZodString;
|
|
21
33
|
headless: z.ZodBoolean;
|
|
34
|
+
addresses: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
35
|
+
progress: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
36
|
+
step: z.ZodNumber;
|
|
37
|
+
steps: z.ZodNumber;
|
|
38
|
+
title: z.ZodString;
|
|
39
|
+
active: z.ZodArray<z.ZodObject<{
|
|
40
|
+
name: z.ZodString;
|
|
41
|
+
percent: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
}, z.core.$strict>>;
|
|
43
|
+
done: z.ZodNumber;
|
|
44
|
+
total: z.ZodNumber;
|
|
45
|
+
}, z.core.$strict>>>;
|
|
22
46
|
}, z.core.$strip>;
|
|
23
47
|
/**
|
|
24
48
|
* A project the host registered after boot (sandbox claude from another folder): everything the guest needs to
|
|
@@ -118,6 +142,9 @@ export declare const ProjectSpec: z.ZodObject<{
|
|
|
118
142
|
}, z.core.$strict>;
|
|
119
143
|
vscode: z.ZodObject<{
|
|
120
144
|
enabled: z.ZodBoolean;
|
|
145
|
+
setup: z.ZodBoolean;
|
|
146
|
+
recommended: z.ZodBoolean;
|
|
147
|
+
extensions: z.ZodArray<z.ZodString>;
|
|
121
148
|
}, z.core.$strict>;
|
|
122
149
|
environment: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
123
150
|
}, z.core.$strict>;
|
|
@@ -259,6 +286,9 @@ export declare const GuestRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
259
286
|
}, z.core.$strict>;
|
|
260
287
|
vscode: z.ZodObject<{
|
|
261
288
|
enabled: z.ZodBoolean;
|
|
289
|
+
setup: z.ZodBoolean;
|
|
290
|
+
recommended: z.ZodBoolean;
|
|
291
|
+
extensions: z.ZodArray<z.ZodString>;
|
|
262
292
|
}, z.core.$strict>;
|
|
263
293
|
environment: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
264
294
|
}, z.core.$strict>;
|
|
@@ -408,6 +438,9 @@ export declare const GuestRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
408
438
|
}, z.core.$strict>;
|
|
409
439
|
vscode: z.ZodObject<{
|
|
410
440
|
enabled: z.ZodBoolean;
|
|
441
|
+
setup: z.ZodBoolean;
|
|
442
|
+
recommended: z.ZodBoolean;
|
|
443
|
+
extensions: z.ZodArray<z.ZodString>;
|
|
411
444
|
}, z.core.$strict>;
|
|
412
445
|
environment: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
413
446
|
}, z.core.$strict>;
|
|
@@ -522,6 +555,7 @@ export type ProjectSpecT = z.output<typeof ProjectSpec>;
|
|
|
522
555
|
export type ProjectStatusT = z.output<typeof ProjectStatus>;
|
|
523
556
|
export type AttachEndpointT = z.output<typeof AttachEndpoint>;
|
|
524
557
|
export type SandboxStatusT = z.output<typeof SandboxStatus>;
|
|
558
|
+
export type BootProgressT = z.output<typeof BootProgress>;
|
|
525
559
|
export type GuestRequestT = z.output<typeof GuestRequest>;
|
|
526
560
|
export type GuestResponseT = z.output<typeof GuestResponse>;
|
|
527
561
|
export type SessionStatusT = z.output<typeof SessionStatus>;
|
package/dist/shared/schema.js
CHANGED
|
@@ -10,6 +10,16 @@ export const AttachEndpoint = z.strictObject({
|
|
|
10
10
|
port: z.number().int().min(1024).max(65535),
|
|
11
11
|
token: Text(256),
|
|
12
12
|
});
|
|
13
|
+
/** The boot's progress in a shape the host can show: step n of m, what is being set up now, how many are done. */
|
|
14
|
+
export const BootProgress = z.strictObject({
|
|
15
|
+
step: z.number().int().min(1).max(32),
|
|
16
|
+
steps: z.number().int().min(1).max(32),
|
|
17
|
+
title: Text(120),
|
|
18
|
+
/** Being set up now; percent while it downloads (when the size is known). */
|
|
19
|
+
active: z.array(z.strictObject({ name: Text(64), percent: z.number().int().min(0).max(100).optional() })).max(8),
|
|
20
|
+
done: z.number().int().min(0).max(1000),
|
|
21
|
+
total: z.number().int().min(0).max(1000),
|
|
22
|
+
});
|
|
13
23
|
/** C:\AgentData\run\sandbox.json: boot progress of the sandbox as a whole. */
|
|
14
24
|
export const SandboxStatus = z.object({
|
|
15
25
|
launchId: Text(64),
|
|
@@ -19,6 +29,10 @@ export const SandboxStatus = z.object({
|
|
|
19
29
|
workspaceHost: Text(),
|
|
20
30
|
guestWorkspace: Text(),
|
|
21
31
|
headless: z.boolean(),
|
|
32
|
+
/** The sandbox's IPv4 addresses (for network.forwardPorts; the host only uses ones on its Hyper-V network). */
|
|
33
|
+
addresses: z.array(z.string().max(45)).max(8).optional(),
|
|
34
|
+
/** A malformed progress is dropped, never the status (the phase still counts). */
|
|
35
|
+
progress: BootProgress.optional().catch(undefined),
|
|
22
36
|
});
|
|
23
37
|
const Version = z.strictObject({ spec: Text(128), source: Text(256) });
|
|
24
38
|
const DatabasePlan = z.strictObject({
|
package/dist/shared/system.d.ts
CHANGED
package/dist/shared/system.js
CHANGED
package/dist/sync/claude-mem.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// claude-mem data (~/.claude-mem) between this PC and the sandbox's persistent store.
|
|
2
2
|
//
|
|
3
3
|
// IN (this PC -> sandbox): the whole folder is mirrored; nothing flows to your PC in that direction.
|
|
4
|
-
// OUT (sandbox -> this PC): ONLY the SQLite database files come back. Never settings, never the Chroma vector store:
|
|
4
|
+
// OUT (sandbox -> this PC), only with claudeProfile.syncClaudeMemBack: ONLY the SQLite database files come back. Never settings, never the Chroma vector store:
|
|
5
5
|
// Chroma keeps Python pickle files, and a crafted pickle runs code when loaded, so copying it back would let a
|
|
6
6
|
// compromised sandbox execute code on your PC. claude-mem rebuilds its vector index from the database by itself.
|
|
7
7
|
import fs from 'node:fs';
|
|
@@ -10,7 +10,8 @@ import _ from 'lodash';
|
|
|
10
10
|
import { dirExists, homeDir, isLink } from '../core/util.js';
|
|
11
11
|
import { safeRemove } from '../shared/safe-remove.js';
|
|
12
12
|
import { assertTestWritable } from '../host/test-guard.js';
|
|
13
|
-
|
|
13
|
+
// Never in: its settings (provider API keys), .env files and backups; nor run-time files that mean nothing elsewhere.
|
|
14
|
+
const SKIP_IN = /(^logs$|^backups?$|^settings\.json$|^\.env|\.pid$|\.port$|\.lock$|^worker.*\.json$|\.incoming$|\.conflict-\d+$)/i;
|
|
14
15
|
/** The only files that may come back from the sandbox. */
|
|
15
16
|
const DATABASE = /^claude-mem\.db(-wal|-shm)?$/i;
|
|
16
17
|
const MAX_DB_BYTES = 2 * 1024 * 1024 * 1024;
|
package/dist/sync/history.d.ts
CHANGED
|
@@ -7,6 +7,11 @@ declare const SyncStateSchema: z.ZodObject<{
|
|
|
7
7
|
guestWorkspace: z.ZodString;
|
|
8
8
|
syncHistory: z.ZodOptional<z.ZodBoolean>;
|
|
9
9
|
syncClaudeMem: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
syncClaudeMemBack: z.ZodOptional<z.ZodBoolean>;
|
|
11
|
+
historyScope: z.ZodOptional<z.ZodEnum<{
|
|
12
|
+
project: "project";
|
|
13
|
+
all: "all";
|
|
14
|
+
}>>;
|
|
10
15
|
memHostStamp: z.ZodOptional<z.ZodNumber>;
|
|
11
16
|
memGuestStamp: z.ZodOptional<z.ZodNumber>;
|
|
12
17
|
projects: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -28,6 +33,7 @@ export interface SyncPaths extends WorkspacePair {
|
|
|
28
33
|
/** Brings this PC's history into the sandbox state (run before the sandbox starts). Returns files copied. */
|
|
29
34
|
export declare const syncHistoryIn: (p: SyncPaths, options?: {
|
|
30
35
|
promptHistory?: boolean;
|
|
36
|
+
scope?: "project" | "all";
|
|
31
37
|
}) => number;
|
|
32
38
|
/** Copies new sandbox history back to this PC (safe while the sandbox runs: transcripts are append-only). */
|
|
33
39
|
export declare const syncHistoryOut: (p: SyncPaths) => number;
|
package/dist/sync/history.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Safety (the sandbox side is untrusted):
|
|
9
9
|
// - links are never followed or written through, on either side;
|
|
10
|
-
// - what comes BACK is limited to transcripts (.jsonl)
|
|
11
|
-
// size, and never Claude's memory folders.
|
|
10
|
+
// - what comes BACK is limited to the sandbox's own projects: their transcripts (.jsonl) and prompt-history lines,
|
|
11
|
+
// plus to-do lists (.json), each capped in size, and never Claude's memory folders. A sandbox cannot plant a
|
|
12
|
+
// conversation for another project on this PC (claude --resume there would load it);
|
|
13
|
+
// - what goes IN is, by default (historyScope "project"), only the sandbox's own projects: other projects'
|
|
14
|
+
// conversations, file backups and prompts are not the agent's business.
|
|
12
15
|
import fs from 'node:fs';
|
|
13
16
|
import path from 'node:path';
|
|
14
17
|
import _ from 'lodash';
|
|
@@ -25,7 +28,7 @@ const MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
|
25
28
|
const AbsolutePath = z.string().max(1024).refine((p) => path.isAbsolute(p), 'must be an absolute path');
|
|
26
29
|
const SyncStateSchema = z.object({
|
|
27
30
|
hostClaudeDir: AbsolutePath, hostWorkspace: AbsolutePath, guestWorkspace: z.string().max(1024),
|
|
28
|
-
syncHistory: z.boolean().optional(), syncClaudeMem: z.boolean().optional(),
|
|
31
|
+
syncHistory: z.boolean().optional(), syncClaudeMem: z.boolean().optional(), syncClaudeMemBack: z.boolean().optional(), historyScope: z.enum(['project', 'all']).optional(),
|
|
29
32
|
/** Newest claude-mem database time on each side right after the last sync (detects edits on both sides). */
|
|
30
33
|
memHostStamp: z.number().optional(), memGuestStamp: z.number().optional(),
|
|
31
34
|
/** Projects registered while the sandbox ran (their history goes back to their own folders). */
|
|
@@ -91,6 +94,16 @@ const isJsonObject = (line) => { try {
|
|
|
91
94
|
catch {
|
|
92
95
|
return false;
|
|
93
96
|
} };
|
|
97
|
+
/** A prompt-history line whose project is one of `workspaces` (on this PC's side). */
|
|
98
|
+
const inProjects = (line, workspaces) => {
|
|
99
|
+
try {
|
|
100
|
+
const project = JSON.parse(line).project;
|
|
101
|
+
return typeof project === 'string' && workspaces.some((w) => path.resolve(w).toLowerCase() === path.resolve(project).toLowerCase());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
94
107
|
const lineTime = (line) => { try {
|
|
95
108
|
return Number(JSON.parse(line).timestamp) || 0;
|
|
96
109
|
}
|
|
@@ -102,22 +115,24 @@ const pairsOf = (p) => _.sortBy([{ hostWorkspace: p.hostWorkspace, guestWorkspac
|
|
|
102
115
|
const translateAll = (text, pairs) => pairs.reduce((out, [from, to]) => translatePaths(out, from, to), text);
|
|
103
116
|
/** Brings this PC's history into the sandbox state (run before the sandbox starts). Returns files copied. */
|
|
104
117
|
export const syncHistoryIn = (p, options = {}) => {
|
|
118
|
+
const all = options.scope === 'all';
|
|
105
119
|
assertTestWritable(p.guestClaudeDir);
|
|
106
120
|
const pairs = pairsOf(p);
|
|
107
121
|
const byHost = new Map(pairs.map((x) => [projectFolderName(x.hostWorkspace), projectFolderName(x.guestWorkspace)]));
|
|
108
122
|
const guestNames = new Set(pairs.map((x) => projectFolderName(x.guestWorkspace)));
|
|
109
123
|
const transform = (t) => translateAll(t, pairs.map((x) => [x.hostWorkspace, x.guestWorkspace]));
|
|
110
124
|
const hostProjects = path.join(p.hostClaudeDir, 'projects');
|
|
111
|
-
const names = dirExists(hostProjects) ? fs.readdirSync(hostProjects).filter((name) => isSafeFileName(name) && (!guestNames.has(name) || byHost.has(name))) : [];
|
|
125
|
+
const names = dirExists(hostProjects) ? fs.readdirSync(hostProjects).filter((name) => isSafeFileName(name) && (all ? !guestNames.has(name) || byHost.has(name) : byHost.has(name))) : [];
|
|
112
126
|
const projects = _.sumBy(names, (name) => byHost.has(name)
|
|
113
127
|
? copyNewer(path.join(hostProjects, name), path.join(p.guestClaudeDir, 'projects', byHost.get(name)), { transform })
|
|
114
128
|
: copyNewer(path.join(hostProjects, name), path.join(p.guestClaudeDir, 'projects', name)));
|
|
115
|
-
|
|
129
|
+
// To-do lists, plans and file backups are kept by session, not by project: only with historyScope "all".
|
|
130
|
+
const sessions = all ? _.sumBy(SESSION_DIRS, (dir) => copyNewer(path.join(p.hostClaudeDir, dir), path.join(p.guestClaudeDir, dir))) : 0;
|
|
116
131
|
if (options.promptHistory === false)
|
|
117
132
|
return projects + sessions;
|
|
118
133
|
// Prompt history: rebuild the sandbox copy from this PC's file plus anything only the sandbox has.
|
|
119
134
|
const target = path.join(p.guestClaudeDir, 'history.jsonl');
|
|
120
|
-
const hostLines = readLines(path.join(p.hostClaudeDir, 'history.jsonl')).map(transform);
|
|
135
|
+
const hostLines = readLines(path.join(p.hostClaudeDir, 'history.jsonl')).filter((l) => all || inProjects(l, pairs.map((x) => x.hostWorkspace))).map(transform);
|
|
121
136
|
const known = new Set(hostLines);
|
|
122
137
|
const merged = _.sortBy([...hostLines, ...readLines(target).filter((l) => !known.has(l))], lineTime);
|
|
123
138
|
if (merged.length) {
|
|
@@ -136,15 +151,15 @@ export const syncHistoryOut = (p) => {
|
|
|
136
151
|
const transform = (t) => translateAll(t, pairs);
|
|
137
152
|
const guestProjectsDir = path.join(p.guestClaudeDir, 'projects');
|
|
138
153
|
const names = dirExists(guestProjectsDir) && !isLink(guestProjectsDir) ? fs.readdirSync(guestProjectsDir).filter(isSafeFileName) : [];
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
: copyNewer(path.join(guestProjectsDir, name), path.join(p.hostClaudeDir, 'projects', name), { accept: transcript, skipDirs: NO_MEMORY }));
|
|
154
|
+
// Only this sandbox's projects: a transcript under any other folder name would be planted into that project on this PC.
|
|
155
|
+
const projects = _.sumBy(names.filter((name) => byGuest.has(name)), (name) => copyNewer(path.join(guestProjectsDir, name), path.join(p.hostClaudeDir, 'projects', byGuest.get(name)), { accept: transcript, skipDirs: NO_MEMORY, transform }));
|
|
142
156
|
// Only to-do lists come back besides transcripts; plans and file backups stay in the sandbox.
|
|
143
157
|
const sessions = copyNewer(path.join(p.guestClaudeDir, 'todos'), path.join(p.hostClaudeDir, 'todos'), { accept: json });
|
|
144
158
|
const hostFile = path.join(p.hostClaudeDir, 'history.jsonl');
|
|
145
159
|
const known = new Set(readLines(hostFile));
|
|
146
|
-
// Only prompt-history entries (one JSON object per line)
|
|
147
|
-
const
|
|
160
|
+
// Only prompt-history entries (one JSON object per line) of this sandbox's projects come back.
|
|
161
|
+
const hostWorkspaces = pairsOf(p).map((x) => x.hostWorkspace);
|
|
162
|
+
const missing = readLines(path.join(p.guestClaudeDir, 'history.jsonl')).filter(isJsonObject).map(transform).filter((l) => inProjects(l, hostWorkspaces) && !known.has(l));
|
|
148
163
|
if (missing.length) {
|
|
149
164
|
ensureDir(p.hostClaudeDir);
|
|
150
165
|
fs.appendFileSync(hostFile, missing.join('\n') + '\n', 'utf8');
|