@bli-cockpit/cli 0.2.259 → 0.2.261

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.
@@ -0,0 +1,332 @@
1
+ /**
2
+ * BLI-23208: the Windows recorder itself, `record.ps1` and the C# it compiles. Split from
3
+ * `notes-record-capture-win32.ts` (which launches it) so each file stays readable; the design notes live there.
4
+ * Pure text: nothing here runs on import.
5
+ */
6
+ /**
7
+ * The C# recorder. C# 5 and APIs of .NET Framework 4.x AND .NET 6+ only, so it
8
+ * compiles under Windows PowerShell 5.1 (CodeDom) and PowerShell 7 (Roslyn).
9
+ * Unmanaged WAVEHDRs and buffers, polled for WHDR_DONE: no delegate callback
10
+ * that the GC could collect. Exit codes: 0 stopped, 2 no microphone, 3 mic
11
+ * denied, 4 recorder failed.
12
+ */
13
+ const RECORDER_CSHARP = String.raw `using System;
14
+ using System.Diagnostics;
15
+ using System.IO;
16
+ using System.Runtime.InteropServices;
17
+ using System.Text;
18
+ using System.Threading;
19
+ using Microsoft.Win32;
20
+
21
+ public sealed class BliRecorder
22
+ {
23
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
24
+ public struct WaveFormatEx
25
+ {
26
+ public ushort wFormatTag;
27
+ public ushort nChannels;
28
+ public uint nSamplesPerSec;
29
+ public uint nAvgBytesPerSec;
30
+ public ushort nBlockAlign;
31
+ public ushort wBitsPerSample;
32
+ public ushort cbSize;
33
+ }
34
+
35
+ [StructLayout(LayoutKind.Sequential)]
36
+ public struct WaveHdr
37
+ {
38
+ public IntPtr lpData;
39
+ public uint dwBufferLength;
40
+ public uint dwBytesRecorded;
41
+ public IntPtr dwUser;
42
+ public uint dwFlags;
43
+ public uint dwLoops;
44
+ public IntPtr lpNext;
45
+ public IntPtr reserved;
46
+ }
47
+
48
+ [DllImport("winmm.dll")] static extern uint waveInGetNumDevs();
49
+ [DllImport("winmm.dll")] static extern uint waveInOpen(out IntPtr hwi, uint deviceId, ref WaveFormatEx fmt, IntPtr callback, IntPtr instance, uint flags);
50
+ [DllImport("winmm.dll")] static extern uint waveInPrepareHeader(IntPtr hwi, IntPtr hdr, uint size);
51
+ [DllImport("winmm.dll")] static extern uint waveInUnprepareHeader(IntPtr hwi, IntPtr hdr, uint size);
52
+ [DllImport("winmm.dll")] static extern uint waveInAddBuffer(IntPtr hwi, IntPtr hdr, uint size);
53
+ [DllImport("winmm.dll")] static extern uint waveInStart(IntPtr hwi);
54
+ [DllImport("winmm.dll")] static extern uint waveInReset(IntPtr hwi);
55
+ [DllImport("winmm.dll")] static extern uint waveInClose(IntPtr hwi);
56
+
57
+ const uint WaveMapper = 0xFFFFFFFF;
58
+ const uint WhdrDone = 1;
59
+ const int BufferCount = 8;
60
+ const int BufferBytes = 3200;
61
+ const string Consent = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\CapabilityAccessManager\\ConsentStore\\microphone";
62
+
63
+ readonly string dir;
64
+ bool ready;
65
+ long total;
66
+ int buffers;
67
+ FileStream file;
68
+ byte[] scratch = new byte[BufferBytes];
69
+
70
+ BliRecorder(string sessionDir) { dir = sessionDir; }
71
+
72
+ public static int Run(string sessionDir, string host)
73
+ {
74
+ BliRecorder recorder = new BliRecorder(sessionDir);
75
+ try
76
+ {
77
+ recorder.Log("start host=" + host + " pid=" + Process.GetCurrentProcess().Id + " session_id=" + Process.GetCurrentProcess().SessionId);
78
+ return recorder.Record();
79
+ }
80
+ catch (Exception e)
81
+ {
82
+ recorder.Log("exception type=" + e.GetType().Name);
83
+ if (!recorder.ready) recorder.Fail("recorder_failed");
84
+ return 4;
85
+ }
86
+ }
87
+
88
+ void Log(string line)
89
+ {
90
+ try { File.AppendAllText(Path.Combine(dir, "recorder.log"), DateTime.UtcNow.ToString("o") + " " + line + "\r\n"); }
91
+ catch (Exception) { /* best effort: a log line that cannot be written must not stop the recording */ }
92
+ }
93
+
94
+ void Fail(string reason)
95
+ {
96
+ try { File.WriteAllText(Path.Combine(dir, "failed.txt"), reason); }
97
+ catch (Exception) { /* the CLI then reports recorder_exited; the Log line below still says why */ }
98
+ Log("failed reason=" + reason);
99
+ }
100
+
101
+ static string ReadConsent(RegistryHive hive, string subKey)
102
+ {
103
+ try
104
+ {
105
+ using (RegistryKey root = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64))
106
+ using (RegistryKey key = root.OpenSubKey(subKey))
107
+ {
108
+ if (key == null) return "absent";
109
+ object value = key.GetValue("Value");
110
+ return value == null ? "absent" : value.ToString();
111
+ }
112
+ }
113
+ catch (Exception) { return "unreadable"; }
114
+ }
115
+
116
+ static bool Denied(out string summary)
117
+ {
118
+ string machine = ReadConsent(RegistryHive.LocalMachine, Consent);
119
+ string user = ReadConsent(RegistryHive.CurrentUser, Consent);
120
+ string desktop = ReadConsent(RegistryHive.CurrentUser, Consent + "\\NonPackaged");
121
+ summary = "machine=" + machine + " user=" + user + " desktop_apps=" + desktop;
122
+ return IsDeny(machine) || IsDeny(user) || IsDeny(desktop);
123
+ }
124
+
125
+ static bool IsDeny(string value) { return string.Equals(value, "Deny", StringComparison.OrdinalIgnoreCase); }
126
+
127
+ static byte[] Header(uint dataBytes)
128
+ {
129
+ using (MemoryStream memory = new MemoryStream())
130
+ using (BinaryWriter writer = new BinaryWriter(memory))
131
+ {
132
+ writer.Write(Encoding.ASCII.GetBytes("RIFF"));
133
+ writer.Write((uint)(36 + dataBytes));
134
+ writer.Write(Encoding.ASCII.GetBytes("WAVEfmt "));
135
+ writer.Write((uint)16);
136
+ writer.Write((ushort)1);
137
+ writer.Write((ushort)1);
138
+ writer.Write((uint)16000);
139
+ writer.Write((uint)32000);
140
+ writer.Write((ushort)2);
141
+ writer.Write((ushort)16);
142
+ writer.Write(Encoding.ASCII.GetBytes("data"));
143
+ writer.Write(dataBytes);
144
+ writer.Flush();
145
+ return memory.ToArray();
146
+ }
147
+ }
148
+
149
+ void Check(string what, uint rc)
150
+ {
151
+ if (rc == 0) return;
152
+ Log(what + " rc=" + rc);
153
+ throw new InvalidOperationException(what);
154
+ }
155
+
156
+ void Append(IntPtr hdr, IntPtr data, int bytesOffset)
157
+ {
158
+ int got = Marshal.ReadInt32(hdr, bytesOffset);
159
+ if (got > BufferBytes) got = BufferBytes;
160
+ if (got <= 0) return;
161
+ Marshal.Copy(data, scratch, 0, got);
162
+ file.Write(scratch, 0, got);
163
+ file.Flush();
164
+ total += got;
165
+ buffers++;
166
+ if (!ready)
167
+ {
168
+ File.WriteAllText(Path.Combine(dir, "ready"), "ok");
169
+ ready = true;
170
+ Log("ready first_buffer_bytes=" + got);
171
+ }
172
+ else if (buffers % 600 == 0)
173
+ {
174
+ Log("progress buffers=" + buffers + " bytes=" + total);
175
+ }
176
+ }
177
+
178
+ int Record()
179
+ {
180
+ string summary;
181
+ bool denied = Denied(out summary);
182
+ Log("privacy " + summary);
183
+ if (denied) { Fail("mic_denied"); return 3; }
184
+
185
+ uint devices = waveInGetNumDevs();
186
+ Log("devices=" + devices);
187
+ if (devices == 0) { Fail("no_microphone"); return 2; }
188
+
189
+ WaveFormatEx format = new WaveFormatEx();
190
+ format.wFormatTag = 1;
191
+ format.nChannels = 1;
192
+ format.nSamplesPerSec = 16000;
193
+ format.nAvgBytesPerSec = 32000;
194
+ format.nBlockAlign = 2;
195
+ format.wBitsPerSample = 16;
196
+ format.cbSize = 0;
197
+ IntPtr hwi;
198
+ uint rc = waveInOpen(out hwi, WaveMapper, ref format, IntPtr.Zero, IntPtr.Zero, 0);
199
+ Log("open rc=" + rc + " format=pcm,1ch,16000hz,16bit");
200
+ if (rc != 0)
201
+ {
202
+ bool none = rc == 2 || rc == 6;
203
+ Fail(none ? "no_microphone" : "recorder_failed");
204
+ return none ? 2 : 4;
205
+ }
206
+
207
+ int headerSize = Marshal.SizeOf(typeof(WaveHdr));
208
+ int flagsOffset = (int)Marshal.OffsetOf(typeof(WaveHdr), "dwFlags");
209
+ int bytesOffset = (int)Marshal.OffsetOf(typeof(WaveHdr), "dwBytesRecorded");
210
+ IntPtr[] headers = new IntPtr[BufferCount];
211
+ IntPtr[] data = new IntPtr[BufferCount];
212
+ bool[] prepared = new bool[BufferCount];
213
+ bool started = false;
214
+ string endedBy = "stop";
215
+ try
216
+ {
217
+ file = new FileStream(Path.Combine(dir, "audio.wav"), FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite, 4096);
218
+ byte[] head = Header(0);
219
+ file.Write(head, 0, head.Length);
220
+ file.Flush();
221
+
222
+ for (int i = 0; i < BufferCount; i++)
223
+ {
224
+ data[i] = Marshal.AllocHGlobal(BufferBytes);
225
+ headers[i] = Marshal.AllocHGlobal(headerSize);
226
+ WaveHdr hdr = new WaveHdr();
227
+ hdr.lpData = data[i];
228
+ hdr.dwBufferLength = BufferBytes;
229
+ Marshal.StructureToPtr(hdr, headers[i], false);
230
+ Check("prepare", waveInPrepareHeader(hwi, headers[i], (uint)headerSize));
231
+ prepared[i] = true;
232
+ Check("addbuffer", waveInAddBuffer(hwi, headers[i], (uint)headerSize));
233
+ }
234
+ Check("start", waveInStart(hwi));
235
+ started = true;
236
+ Log("recording buffers=" + BufferCount + " buffer_bytes=" + BufferBytes);
237
+
238
+ string stopPath = Path.Combine(dir, "stop");
239
+ Stopwatch clock = Stopwatch.StartNew();
240
+ long lastPrivacy = 0;
241
+ int next = 0;
242
+ while (true)
243
+ {
244
+ if (File.Exists(stopPath)) break;
245
+ if (clock.ElapsedMilliseconds - lastPrivacy >= 1000)
246
+ {
247
+ lastPrivacy = clock.ElapsedMilliseconds;
248
+ string now;
249
+ if (Denied(out now)) { Log("privacy " + now); endedBy = "mic_denied"; break; }
250
+ }
251
+ uint flags = (uint)Marshal.ReadInt32(headers[next], flagsOffset);
252
+ if ((flags & WhdrDone) == 0) { Thread.Sleep(10); continue; }
253
+ Append(headers[next], data[next], bytesOffset);
254
+ Check("addbuffer", waveInAddBuffer(hwi, headers[next], (uint)headerSize));
255
+ next = (next + 1) % BufferCount;
256
+ }
257
+
258
+ // Reset hands every queued buffer back (the oldest partly filled): keep what they hold, oldest first.
259
+ waveInReset(hwi);
260
+ started = false;
261
+ for (int i = 0; i < BufferCount; i++)
262
+ {
263
+ int index = (next + i) % BufferCount;
264
+ uint flags = (uint)Marshal.ReadInt32(headers[index], flagsOffset);
265
+ if ((flags & WhdrDone) != 0) Append(headers[index], data[index], bytesOffset);
266
+ }
267
+ }
268
+ finally
269
+ {
270
+ if (started) waveInReset(hwi);
271
+ for (int i = 0; i < BufferCount; i++)
272
+ {
273
+ if (prepared[i]) waveInUnprepareHeader(hwi, headers[i], (uint)headerSize);
274
+ if (headers[i] != IntPtr.Zero) Marshal.FreeHGlobal(headers[i]);
275
+ if (data[i] != IntPtr.Zero) Marshal.FreeHGlobal(data[i]);
276
+ }
277
+ waveInClose(hwi);
278
+ if (file != null)
279
+ {
280
+ try
281
+ {
282
+ uint size = total > 0xFFFFFFFFL - 36 ? 0xFFFFFFFFu - 36 : (uint)total;
283
+ file.Seek(0, SeekOrigin.Begin);
284
+ byte[] sized = Header(size);
285
+ file.Write(sized, 0, sized.Length);
286
+ file.Flush();
287
+ }
288
+ catch (Exception e) { Log("size_rewrite_failed type=" + e.GetType().Name); }
289
+ file.Close();
290
+ }
291
+ }
292
+
293
+ Log("closed ended_by=" + endedBy + " buffers=" + buffers + " bytes=" + total);
294
+ if (endedBy == "mic_denied") { Fail("mic_denied"); return 3; }
295
+ File.WriteAllText(Path.Combine(dir, "done"), "ok");
296
+ return 0;
297
+ }
298
+ }
299
+ `;
300
+ /**
301
+ * The recorder script, `record.ps1`. PURE ASCII (asserted by a test): Windows
302
+ * PowerShell 5.1 reads a BOM-less file as ANSI. `$args[0]` is the session
303
+ * directory. Reasons it writes to `failed.txt`: mic_denied, no_microphone,
304
+ * recorder_failed. Everything is written through [IO.File], never Out-File.
305
+ */
306
+ export const RECORDER_SCRIPT_WIN32 = `param([Parameter(Position = 0)][string]$Dir)
307
+ $ErrorActionPreference = 'Stop'
308
+ if (-not $Dir) { exit 4 }
309
+ $source = @'
310
+ ${RECORDER_CSHARP.replace(/\r?\n$/u, "")}
311
+ '@
312
+ function Write-RecorderLog([string]$Line) {
313
+ try { [IO.File]::AppendAllText((Join-Path $Dir 'recorder.log'), $Line + [Environment]::NewLine) } catch { <# best effort: a log line that cannot be written must not stop the recorder #> }
314
+ }
315
+ try {
316
+ Add-Type -TypeDefinition $source -Language CSharp
317
+ } catch {
318
+ Write-RecorderLog ('add_type_failed type=' + $_.Exception.GetType().Name)
319
+ try { [IO.File]::WriteAllText((Join-Path $Dir 'failed.txt'), 'recorder_failed') } catch { <# nothing else to tell: the CLI then reports recorder_exited, and recorder.log says why #> }
320
+ exit 4
321
+ }
322
+ $code = 4
323
+ try {
324
+ $code = [BliRecorder]::Run($Dir, ('powershell-' + $PSVersionTable.PSVersion.ToString()))
325
+ } catch {
326
+ Write-RecorderLog ('run_exception type=' + $_.Exception.GetType().Name)
327
+ if (-not [IO.File]::Exists((Join-Path $Dir 'ready'))) {
328
+ try { [IO.File]::WriteAllText((Join-Path $Dir 'failed.txt'), 'recorder_failed') } catch { <# nothing else to tell: the CLI then reports recorder_exited, and recorder.log says why #> }
329
+ }
330
+ }
331
+ exit $code
332
+ `.replace(/\r?\n/gu, "\r\n");
@@ -14,10 +14,14 @@
14
14
  *
15
15
  * Nothing leaves the machine while recording, and there is no auto-stop. The
16
16
  * original file is never deleted or changed by the trim. Apple Silicon macOS
17
- * only for now: Windows says so by name (a follow-up ticket adds it).
17
+ * and Windows (BLI-23208) record; any other platform refuses by name. Both
18
+ * platforms share one session directory, marker files, JSON answers and WAV
19
+ * format, so the trim, upload and recovery below are the same code.
18
20
  *
19
- * Capture is `notes-record-capture.ts`; the speech trim is `notes-record-wav.ts`.
21
+ * Capture is `notes-record-capture.ts` (macOS) and
22
+ * `notes-record-capture-win32.ts` (Windows); the speech trim is `notes-record-wav.ts`.
20
23
  */
24
+ import path from "node:path";
21
25
  import { setTimeout as delay } from "node:timers/promises";
22
26
  import { writeLine } from "./cli-io.js";
23
27
  import { audioPath, fileExists, fileSize, launchRecorder, listSessions, newSessionId, prepareSessionDir, processAlive, readReason, recordingsRoot, requestStop, sessionDir, trimmedPath, writeSession, RECORDING_SAMPLE_RATE, } from "./notes-record-capture.js";
@@ -31,7 +35,7 @@ export function defaultRecordDeps(io, upload) {
31
35
  platform: process.platform,
32
36
  now: () => new Date(),
33
37
  sleep: io.sleep ?? ((ms) => delay(ms)),
34
- launch: launchRecorder,
38
+ launch: (dir) => launchRecorder(dir, process.platform),
35
39
  alive: processAlive,
36
40
  kill: (pid) => { try {
37
41
  process.kill(pid, "SIGTERM");
@@ -45,12 +49,19 @@ const stateOf = (session, deps) => {
45
49
  return deps.alive(session.pid) ? "recording" : "crashed";
46
50
  return session.endedBy === "cancel" ? "cancelled" : "stopped";
47
51
  };
48
- const MIC_DENIED = "The microphone is blocked for this terminal. Open System Settings > Privacy & Security > Microphone, switch your terminal app on, then run `cockpit notes record start` again.";
49
- const REFUSALS = {
50
- mic_denied: MIC_DENIED,
51
- mic_permission_unanswered: "macOS asked for microphone access and nobody answered. Run `cockpit notes record start` again and choose Allow.",
52
- recorder_failed: "The recorder could not open the microphone. Check that a microphone is connected and not in use, then try again.",
53
- };
52
+ const MIC_DENIED_DARWIN = "The microphone is blocked for this terminal. Open System Settings > Privacy & Security > Microphone, switch your terminal app on, then run `cockpit notes record start` again.";
53
+ const MIC_DENIED_WIN32 = "The microphone is blocked. Open Settings > Privacy & security > Microphone, turn on Microphone access and Let desktop apps access your microphone, then run `cockpit notes record start` again.";
54
+ /** The sentence for a recorder reason, per platform: the macOS and Windows microphone switches live in different places. */
55
+ function refusalSentence(platform, reason) {
56
+ switch (reason) {
57
+ case "mic_denied": return platform === "win32" ? MIC_DENIED_WIN32 : MIC_DENIED_DARWIN;
58
+ case "mic_permission_unanswered": return "macOS asked for microphone access and nobody answered. Run `cockpit notes record start` again and choose Allow.";
59
+ case "no_microphone": return "No microphone was found. Connect or enable a microphone, then run `cockpit notes record start` again.";
60
+ case "recorder_failed": return "The recorder could not open the microphone. Check that a microphone is connected and not in use, then try again.";
61
+ default: return "The recorder stopped before it started.";
62
+ }
63
+ }
64
+ const RECORDER_NAME = { darwin: "macOS recorder (osascript)", win32: "Windows recorder (PowerShell)" };
54
65
  function say(io, command, human, json, ok = true) {
55
66
  writeLine(io.stdout, command.json ? JSON.stringify({ ok, ...json }) : human);
56
67
  return ok ? 0 : 1;
@@ -74,9 +85,8 @@ const megabytes = (bytes) => (bytes === null ? "no file yet" : `${(bytes / 1_000
74
85
  const audioSeconds = (bytes) => (bytes === null ? 0 : Math.max(0, bytes - 44) / (RECORDING_SAMPLE_RATE * 2));
75
86
  export async function runNotesRecord(command, io, deps) {
76
87
  const verb = command.recordAction ?? "status";
77
- if (deps.platform !== "darwin") {
78
- const where = deps.platform === "win32" ? "Windows" : deps.platform;
79
- return refuse(io, command, "platform_not_supported", `Recording a meeting is not supported yet on ${where}. It works on Apple Silicon Macs today; Windows is a follow-up.`);
88
+ if (deps.platform !== "darwin" && deps.platform !== "win32") {
89
+ return refuse(io, command, "platform_not_supported", `Recording a meeting is not supported on ${deps.platform}. It works on Apple Silicon Macs and on Windows.`);
80
90
  }
81
91
  const root = recordingsRoot(command.homeDir);
82
92
  switch (verb) {
@@ -93,10 +103,10 @@ async function startRecording(command, io, deps, root) {
93
103
  return refuse(io, command, "already_recording", `A recording is already running (started ${running.startedAt}). Run \`cockpit notes record stop\` to finish it or \`cockpit notes record cancel\` to keep the file and stop.`);
94
104
  }
95
105
  const id = newSessionId(deps.now());
96
- const dir = await prepareSessionDir(root, id);
106
+ const dir = await prepareSessionDir(root, id, deps.platform);
97
107
  const pid = deps.launch(dir);
98
108
  if (pid === null) {
99
- return refuse(io, command, "recorder_not_launched", "Could not start the macOS recorder (osascript). Nothing was recorded.");
109
+ return refuse(io, command, "recorder_not_launched", `Could not start the ${RECORDER_NAME[deps.platform] ?? "recorder"}. Nothing was recorded.`);
100
110
  }
101
111
  const session = { id, startedAt: deps.now().toISOString(), pid };
102
112
  await writeSession(root, session);
@@ -106,7 +116,7 @@ async function startRecording(command, io, deps, root) {
106
116
  const failed = await readReason(root, id);
107
117
  if (failed) {
108
118
  await writeSession(root, { ...session, endedAt: deps.now().toISOString(), endedBy: "cancel" });
109
- return refuse(io, command, failed, REFUSALS[failed] ?? "The recorder stopped before it started.");
119
+ return refuse(io, command, failed, refusalSentence(deps.platform, failed));
110
120
  }
111
121
  if (await fileExists(`${sessionDir(root, id)}/ready`)) {
112
122
  writeLine(io.stderr, `${TAG} recording ${JSON.stringify({ session: id, reason: "mic_open" })}`);
@@ -114,7 +124,8 @@ async function startRecording(command, io, deps, root) {
114
124
  }
115
125
  if (!deps.alive(pid)) {
116
126
  await writeSession(root, { ...session, endedAt: deps.now().toISOString(), endedBy: "cancel" });
117
- return refuse(io, command, "recorder_exited", "The recorder exited before the microphone opened. Nothing was recorded.");
127
+ const log = deps.platform === "win32" ? ` Details: ${path.join(sessionDir(root, id), "recorder.log")}` : "";
128
+ return refuse(io, command, "recorder_exited", `The recorder exited before the microphone opened. Nothing was recorded.${log}`);
118
129
  }
119
130
  await deps.sleep(POLL_MS);
120
131
  }
@@ -37,7 +37,7 @@
37
37
  * (it runs on a shared server-side key, not a `notes_accounts` row), read
38
38
  * off its `granola_sync_runs` ledger.
39
39
  * - `notes-app.ts` — BLI-23207: `app install|uninstall|status`, the BLI
40
- * Notetaker menu-bar app on this Mac (its LaunchAgent: `notes-app-plist.ts`).
40
+ * Notetaker app on this Mac or Windows PC (Mac LaunchAgent: `notes-app-plist.ts`; Windows: `notes-app-windows.ts`).
41
41
  * - `notes-transcription-stats.ts` — BLI-18003: what meeting transcription
42
42
  * cost and did per provider for a month, and the spend against the cap.
43
43
  *
@@ -105,10 +105,12 @@ export async function runNotes(command, io) {
105
105
  });
106
106
  }));
107
107
  }
108
- // BLI-23207: the menu-bar app is installed on this Mac; only install and status ask Tower (for the newest release).
108
+ // BLI-23207/23208: the menu-bar / notification-area app is installed on this Mac or Windows PC; only install and status ask Tower (for the newest release).
109
109
  if (command.action === "app") {
110
110
  return runNotesApp(command, io, {
111
111
  platform: process.platform,
112
+ arch: process.arch,
113
+ env: io.env,
112
114
  homeDir: command.homeDir ?? os.homedir(),
113
115
  uid: process.getuid?.() ?? 0,
114
116
  exec: io.exec ?? defaultExec(),
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
21
21
  }
22
22
 
23
23
  if (command === "--version" || command === "-V" || command === "version") {
24
- writeLine(io?.stdout ?? process.stdout, "0.2.259");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.261");
25
25
  return 0;
26
26
  }
27
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.259",
3
+ "version": "0.2.261",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@bli-cockpit/memory-mcp": "0.1.77",
31
- "@bli-cockpit/mcp": "0.1.159",
31
+ "@bli-cockpit/mcp": "0.1.160",
32
32
  "@bli-cockpit/telemetry-core": "0.1.84"
33
33
  },
34
34
  "repository": {