@bman654/clodex 2.13.0 → 2.14.0
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/README.md +6 -2
- package/dist/{chunk-ZM2KYLPH.js → chunk-3NCDMS7N.js} +468 -2
- package/dist/chunk-3NCDMS7N.js.map +1 -0
- package/dist/claude-wrapper.js +276 -7
- package/dist/claude-wrapper.js.map +1 -1
- package/dist/cli.js +555 -488
- package/dist/cli.js.map +1 -1
- package/docs/background-agents.md +35 -0
- package/docs/windows-setup.md +203 -168
- package/launcher/clodex-claude-launcher.cs +311 -0
- package/package.json +2 -1
- package/dist/chunk-ZM2KYLPH.js.map +0 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// clodex-claude-launcher.cs - the Windows executable behind `clodex install-vscode-launcher`.
|
|
2
|
+
//
|
|
3
|
+
// The Claude Code VS Code extension spawns its `claudeCode.claudeProcessWrapper` as a bare
|
|
4
|
+
// executable (no shell) with the bundled claude.exe as the first argument. npm gives clodex-claude
|
|
5
|
+
// three script shims on Windows (extensionless, .cmd, .ps1) and no executable; the spawn rejects
|
|
6
|
+
// them with `spawn EINVAL`. This program is
|
|
7
|
+
// the .exe the extension can spawn: it runs `node.exe dist/claude-wrapper.js` with the extension's
|
|
8
|
+
// arguments passed through with their values intact, and keeps the whole Node -> claude tree tied to its own
|
|
9
|
+
// lifetime so cancelling a chat leaves no orphan processes.
|
|
10
|
+
//
|
|
11
|
+
// `clodex install-vscode-launcher` bakes two absolute paths into the placeholders below and compiles
|
|
12
|
+
// this file with the C# compiler that ships inside the .NET Framework on every Windows 10/11
|
|
13
|
+
// machine (csc.exe under %WINDIR%\Microsoft.NET\Framework64\v4.0.30319). That compiler stops at
|
|
14
|
+
// C# 5, so this file deliberately uses no string interpolation, no `?.`, no expression-bodied
|
|
15
|
+
// members and no `nameof`.
|
|
16
|
+
//
|
|
17
|
+
// Argument forwarding does NOT go through `string[] args`: .NET Framework has no ArgumentList, and
|
|
18
|
+
// hand-written re-quoting under the C runtime's rules is error-prone for JSON, empty strings and
|
|
19
|
+
// trailing backslashes. Instead the raw command line from GetCommandLineW has its first token (this
|
|
20
|
+
// executable's own path, quoted or not) removed and the remainder is appended to the child's
|
|
21
|
+
// command line untouched. Node then parses it with the same C-runtime rules the extension wrote
|
|
22
|
+
// it for, so the argument values Node sees - Unicode included - are the ones the extension sent.
|
|
23
|
+
|
|
24
|
+
using System;
|
|
25
|
+
using System.IO;
|
|
26
|
+
using System.Runtime.InteropServices;
|
|
27
|
+
using System.Text;
|
|
28
|
+
|
|
29
|
+
internal static class ClodexClaudeLauncher
|
|
30
|
+
{
|
|
31
|
+
// Both constants are replaced by `clodex install-vscode-launcher` before compilation.
|
|
32
|
+
private const string NodePath = "__CLODEX_NODE_PATH__";
|
|
33
|
+
private const string WrapperScriptPath = "__CLODEX_WRAPPER_SCRIPT_PATH__";
|
|
34
|
+
|
|
35
|
+
private const uint CREATE_SUSPENDED = 0x00000004;
|
|
36
|
+
private const uint STARTF_USESTDHANDLES = 0x00000100;
|
|
37
|
+
private const uint INFINITE = 0xFFFFFFFF;
|
|
38
|
+
private const int STD_INPUT_HANDLE = -10;
|
|
39
|
+
private const int STD_OUTPUT_HANDLE = -11;
|
|
40
|
+
private const int STD_ERROR_HANDLE = -12;
|
|
41
|
+
private const int JobObjectExtendedLimitInformation = 9;
|
|
42
|
+
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
|
43
|
+
private const uint CTRL_C_EVENT = 0;
|
|
44
|
+
private const uint CTRL_BREAK_EVENT = 1;
|
|
45
|
+
private const uint WAIT_OBJECT_0 = 0;
|
|
46
|
+
private const uint RESUME_FAILED = 0xFFFFFFFF;
|
|
47
|
+
|
|
48
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
49
|
+
private struct STARTUPINFO
|
|
50
|
+
{
|
|
51
|
+
public uint cb;
|
|
52
|
+
public string lpReserved;
|
|
53
|
+
public string lpDesktop;
|
|
54
|
+
public string lpTitle;
|
|
55
|
+
public uint dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute;
|
|
56
|
+
public uint dwFlags;
|
|
57
|
+
public ushort wShowWindow, cbReserved2;
|
|
58
|
+
public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
62
|
+
private struct PROCESS_INFORMATION
|
|
63
|
+
{
|
|
64
|
+
public IntPtr hProcess, hThread;
|
|
65
|
+
public uint dwProcessId, dwThreadId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
69
|
+
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
|
70
|
+
{
|
|
71
|
+
public long PerProcessUserTimeLimit, PerJobUserTimeLimit;
|
|
72
|
+
public uint LimitFlags;
|
|
73
|
+
public UIntPtr MinimumWorkingSetSize, MaximumWorkingSetSize;
|
|
74
|
+
public uint ActiveProcessLimit;
|
|
75
|
+
public UIntPtr Affinity;
|
|
76
|
+
public uint PriorityClass, SchedulingClass;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
80
|
+
private struct IO_COUNTERS
|
|
81
|
+
{
|
|
82
|
+
public ulong ReadOperationCount, WriteOperationCount, OtherOperationCount;
|
|
83
|
+
public ulong ReadTransferCount, WriteTransferCount, OtherTransferCount;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
87
|
+
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|
88
|
+
{
|
|
89
|
+
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
|
90
|
+
public IO_COUNTERS IoInfo;
|
|
91
|
+
public UIntPtr ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private delegate bool ConsoleCtrlHandler(uint ctrlType);
|
|
95
|
+
|
|
96
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
|
97
|
+
private static extern IntPtr GetCommandLineW();
|
|
98
|
+
|
|
99
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
100
|
+
private static extern bool CreateProcessW(
|
|
101
|
+
string lpApplicationName, StringBuilder lpCommandLine, IntPtr lpProcessAttributes,
|
|
102
|
+
IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment,
|
|
103
|
+
string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
|
104
|
+
|
|
105
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
106
|
+
private static extern IntPtr CreateJobObjectW(IntPtr lpJobAttributes, string lpName);
|
|
107
|
+
|
|
108
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
109
|
+
private static extern bool SetInformationJobObject(
|
|
110
|
+
IntPtr hJob, int infoClass, ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION info, uint infoLength);
|
|
111
|
+
|
|
112
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
113
|
+
private static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
|
|
114
|
+
|
|
115
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
116
|
+
private static extern uint ResumeThread(IntPtr hThread);
|
|
117
|
+
|
|
118
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
119
|
+
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
|
120
|
+
|
|
121
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
122
|
+
private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
|
123
|
+
|
|
124
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
125
|
+
private static extern bool CloseHandle(IntPtr hObject);
|
|
126
|
+
|
|
127
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
128
|
+
private static extern IntPtr GetStdHandle(int nStdHandle);
|
|
129
|
+
|
|
130
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
131
|
+
private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
|
|
132
|
+
|
|
133
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
134
|
+
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandler handler, bool add);
|
|
135
|
+
|
|
136
|
+
// Held in a static so the garbage collector never reclaims the delegate the console still calls.
|
|
137
|
+
private static readonly ConsoleCtrlHandler CtrlHandler = new ConsoleCtrlHandler(OnConsoleCtrl);
|
|
138
|
+
|
|
139
|
+
/// <summary>
|
|
140
|
+
/// Everything after the first token of a Windows command line. The token boundary follows the
|
|
141
|
+
/// C runtime's rule for argv[0]: a leading quote ends at the next quote with no escape
|
|
142
|
+
/// processing; otherwise the token ends at the first space or tab. Leading whitespace after
|
|
143
|
+
/// the token is dropped; the rest - every quote, backslash and empty "" - is returned verbatim.
|
|
144
|
+
/// </summary>
|
|
145
|
+
internal static string ArgumentsAfterFirstToken(string commandLine)
|
|
146
|
+
{
|
|
147
|
+
int length = commandLine.Length;
|
|
148
|
+
int index = 0;
|
|
149
|
+
if (length > 0 && commandLine[0] == '"')
|
|
150
|
+
{
|
|
151
|
+
index = 1;
|
|
152
|
+
while (index < length && commandLine[index] != '"') index++;
|
|
153
|
+
if (index < length) index++;
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
{
|
|
157
|
+
while (index < length && commandLine[index] != ' ' && commandLine[index] != '\t') index++;
|
|
158
|
+
}
|
|
159
|
+
while (index < length && (commandLine[index] == ' ' || commandLine[index] == '\t')) index++;
|
|
160
|
+
return commandLine.Substring(index);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// <summary>The child command line: node and the wrapper script quoted, then the caller's arguments.</summary>
|
|
164
|
+
internal static string BuildChildCommandLine(string node, string script, string forwardedArguments)
|
|
165
|
+
{
|
|
166
|
+
string head = "\"" + node + "\" \"" + script + "\"";
|
|
167
|
+
return forwardedArguments.Length == 0 ? head : head + " " + forwardedArguments;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// <summary>The baked node.exe if it still exists, else the first node.exe on PATH, else null.</summary>
|
|
171
|
+
private static string ResolveNode()
|
|
172
|
+
{
|
|
173
|
+
if (File.Exists(NodePath)) return NodePath;
|
|
174
|
+
string path = Environment.GetEnvironmentVariable("PATH") ?? "";
|
|
175
|
+
foreach (string entry in path.Split(';'))
|
|
176
|
+
{
|
|
177
|
+
string directory = entry.Trim().Trim('"');
|
|
178
|
+
if (directory.Length == 0) continue;
|
|
179
|
+
try
|
|
180
|
+
{
|
|
181
|
+
string candidate = Path.Combine(directory, "node.exe");
|
|
182
|
+
if (File.Exists(candidate)) return candidate;
|
|
183
|
+
}
|
|
184
|
+
catch (ArgumentException)
|
|
185
|
+
{
|
|
186
|
+
// An unusable PATH entry (illegal characters) is skipped, not fatal.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private static bool OnConsoleCtrl(uint ctrlType)
|
|
193
|
+
{
|
|
194
|
+
// Ctrl+C / Ctrl+Break reach the child through the shared console; the launcher only keeps
|
|
195
|
+
// waiting so it can report the child's real exit code. Close/logoff/shutdown keep the default
|
|
196
|
+
// handling - the job object takes the child down with this process.
|
|
197
|
+
return ctrlType == CTRL_C_EVENT || ctrlType == CTRL_BREAK_EVENT;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private static void Warn(string message)
|
|
201
|
+
{
|
|
202
|
+
Console.Error.WriteLine("clodex-claude.exe: warning: " + message);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private static int Fail(string message, int exitCode)
|
|
206
|
+
{
|
|
207
|
+
Console.Error.WriteLine("clodex-claude.exe: " + message);
|
|
208
|
+
return exitCode;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private static string LastError()
|
|
212
|
+
{
|
|
213
|
+
return "Windows error " + Marshal.GetLastWin32Error();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// <summary>
|
|
217
|
+
/// A kill-on-close job, or IntPtr.Zero after one warning. Created before the child runs, so a
|
|
218
|
+
/// launcher killed by the extension (cancel, window reload, editor exit) takes Node and the
|
|
219
|
+
/// claude grandchild with it. The handle is deliberately never closed once a child is inside -
|
|
220
|
+
/// the OS closes it when this process ends, which is exactly the moment the tree must die.
|
|
221
|
+
/// </summary>
|
|
222
|
+
private static IntPtr CreateKillOnCloseJob()
|
|
223
|
+
{
|
|
224
|
+
IntPtr job = CreateJobObjectW(IntPtr.Zero, null);
|
|
225
|
+
if (job == IntPtr.Zero)
|
|
226
|
+
{
|
|
227
|
+
Warn("could not create a job object (" + LastError() + "); cancellation cleanup is not guaranteed");
|
|
228
|
+
return IntPtr.Zero;
|
|
229
|
+
}
|
|
230
|
+
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
|
|
231
|
+
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
232
|
+
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, ref limits,
|
|
233
|
+
(uint)Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION))))
|
|
234
|
+
{
|
|
235
|
+
Warn("could not configure the job object (" + LastError() + "); cancellation cleanup is not guaranteed");
|
|
236
|
+
CloseHandle(job);
|
|
237
|
+
return IntPtr.Zero;
|
|
238
|
+
}
|
|
239
|
+
return job;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private static int Main()
|
|
243
|
+
{
|
|
244
|
+
if (!File.Exists(WrapperScriptPath))
|
|
245
|
+
{
|
|
246
|
+
return Fail("clodex's claude-wrapper.js is no longer at " + WrapperScriptPath
|
|
247
|
+
+ "; re-run `clodex install-vscode-launcher`", 127);
|
|
248
|
+
}
|
|
249
|
+
string node = ResolveNode();
|
|
250
|
+
if (node == null)
|
|
251
|
+
{
|
|
252
|
+
return Fail("node.exe is no longer at " + NodePath
|
|
253
|
+
+ " and none was found on PATH; re-run `clodex install-vscode-launcher`", 127);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
string forwarded = ArgumentsAfterFirstToken(Marshal.PtrToStringUni(GetCommandLineW()));
|
|
257
|
+
StringBuilder commandLine = new StringBuilder(BuildChildCommandLine(node, WrapperScriptPath, forwarded));
|
|
258
|
+
|
|
259
|
+
IntPtr job = CreateKillOnCloseJob();
|
|
260
|
+
|
|
261
|
+
STARTUPINFO startup = new STARTUPINFO();
|
|
262
|
+
startup.cb = (uint)Marshal.SizeOf(typeof(STARTUPINFO));
|
|
263
|
+
startup.dwFlags = STARTF_USESTDHANDLES;
|
|
264
|
+
startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
265
|
+
startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
266
|
+
startup.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
|
267
|
+
|
|
268
|
+
SetConsoleCtrlHandler(CtrlHandler, true);
|
|
269
|
+
|
|
270
|
+
// Environment and working directory are inherited (null). The child starts suspended so it
|
|
271
|
+
// is inside the job before it can spawn anything of its own.
|
|
272
|
+
PROCESS_INFORMATION child;
|
|
273
|
+
bool created = CreateProcessW(node, commandLine, IntPtr.Zero, IntPtr.Zero, true, CREATE_SUSPENDED,
|
|
274
|
+
IntPtr.Zero, null, ref startup, out child);
|
|
275
|
+
if (!created)
|
|
276
|
+
{
|
|
277
|
+
return Fail("could not start " + node + " (" + LastError() + ")", 127);
|
|
278
|
+
}
|
|
279
|
+
if (job != IntPtr.Zero && !AssignProcessToJobObject(job, child.hProcess))
|
|
280
|
+
{
|
|
281
|
+
Warn("could not attach node to the job object (" + LastError() + "); cancellation cleanup is not guaranteed");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
uint resumed = ResumeThread(child.hThread);
|
|
285
|
+
string resumeError = resumed == RESUME_FAILED ? LastError() : null;
|
|
286
|
+
CloseHandle(child.hThread);
|
|
287
|
+
if (resumeError != null)
|
|
288
|
+
{
|
|
289
|
+
TerminateProcess(child.hProcess, 1);
|
|
290
|
+
CloseHandle(child.hProcess);
|
|
291
|
+
return Fail("could not resume node after starting it (" + resumeError + ")", 1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (WaitForSingleObject(child.hProcess, INFINITE) != WAIT_OBJECT_0)
|
|
295
|
+
{
|
|
296
|
+
string reason = LastError();
|
|
297
|
+
TerminateProcess(child.hProcess, 1);
|
|
298
|
+
CloseHandle(child.hProcess);
|
|
299
|
+
return Fail("waiting for node failed (" + reason + "); node was terminated", 1);
|
|
300
|
+
}
|
|
301
|
+
uint exitCode;
|
|
302
|
+
bool haveExitCode = GetExitCodeProcess(child.hProcess, out exitCode);
|
|
303
|
+
string exitCodeError = haveExitCode ? null : LastError();
|
|
304
|
+
CloseHandle(child.hProcess);
|
|
305
|
+
if (!haveExitCode)
|
|
306
|
+
{
|
|
307
|
+
return Fail("could not read node's exit code (" + exitCodeError + ")", 1);
|
|
308
|
+
}
|
|
309
|
+
return unchecked((int)exitCode);
|
|
310
|
+
}
|
|
311
|
+
}
|
package/package.json
CHANGED