@pulso/companion 0.4.3 → 0.4.4

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 (2) hide show
  1. package/dist/index.js +594 -45
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -346,7 +346,7 @@ var require_dist = __commonJS({
346
346
 
347
347
  // src/index.ts
348
348
  import WebSocket from "ws";
349
- import { exec as exec5, execSync as execSync3 } from "child_process";
349
+ import { exec as exec5, execSync as execSync3, spawn } from "child_process";
350
350
  import { createRequire } from "module";
351
351
  import { createInterface } from "readline/promises";
352
352
  import { stdin as input, stdout as output } from "process";
@@ -1835,6 +1835,57 @@ var LinuxAdapter = class {
1835
1835
  };
1836
1836
  }
1837
1837
  }
1838
+ async browserListProfiles() {
1839
+ const home = process.env.HOME || "";
1840
+ const browserPaths = [
1841
+ { browser: "Google Chrome", dir: `${home}/.config/google-chrome` },
1842
+ { browser: "Chromium", dir: `${home}/.config/chromium` },
1843
+ { browser: "Microsoft Edge", dir: `${home}/.config/microsoft-edge` },
1844
+ { browser: "Brave Browser", dir: `${home}/.config/BraveSoftware/Brave-Browser` }
1845
+ ];
1846
+ const profiles = [];
1847
+ for (const { browser, dir } of browserPaths) {
1848
+ if (!existsSync(dir)) continue;
1849
+ let entries = [];
1850
+ try {
1851
+ entries = readdirSync(dir);
1852
+ } catch {
1853
+ continue;
1854
+ }
1855
+ for (const entry of entries) {
1856
+ if (entry !== "Default" && !entry.startsWith("Profile ")) continue;
1857
+ const prefsPath = `${dir}/${entry}/Preferences`;
1858
+ if (!existsSync(prefsPath)) continue;
1859
+ try {
1860
+ const prefs = JSON.parse(readFileSync(prefsPath, "utf-8"));
1861
+ profiles.push({
1862
+ browser,
1863
+ profileDir: `${dir}/${entry}`,
1864
+ name: prefs.profile?.name || entry,
1865
+ email: prefs.account_info?.[0]?.email,
1866
+ isDefault: entry === "Default"
1867
+ });
1868
+ } catch {
1869
+ }
1870
+ }
1871
+ }
1872
+ const ffDir = `${home}/.mozilla/firefox`;
1873
+ if (existsSync(ffDir)) {
1874
+ try {
1875
+ for (const entry of readdirSync(ffDir)) {
1876
+ if (!existsSync(`${ffDir}/${entry}/prefs.js`)) continue;
1877
+ profiles.push({
1878
+ browser: "Firefox",
1879
+ profileDir: `${ffDir}/${entry}`,
1880
+ name: entry.replace(/^[a-z0-9]+\./, ""),
1881
+ isDefault: entry.includes("default")
1882
+ });
1883
+ }
1884
+ } catch {
1885
+ }
1886
+ }
1887
+ return { success: true, data: { profiles, total: profiles.length } };
1888
+ }
1838
1889
  /* ══════════════════════════════════════════════════════════
1839
1890
  * Productivity: Calendar
1840
1891
  *
@@ -3443,6 +3494,18 @@ function runSwift(code, timeout = 1e4) {
3443
3494
  child.stdin?.end();
3444
3495
  });
3445
3496
  }
3497
+ async function hasScreenRecordingPermission() {
3498
+ try {
3499
+ const out = await runSwift(`
3500
+ import Cocoa
3501
+ import CoreGraphics
3502
+ print(CGPreflightScreenCaptureAccess() ? "granted" : "denied")
3503
+ `, 6e3);
3504
+ return out.trim().toLowerCase() === "granted";
3505
+ } catch {
3506
+ return false;
3507
+ }
3508
+ }
3446
3509
  function safePath2(relative) {
3447
3510
  const full = resolve2(HOME2, relative);
3448
3511
  if (!full.startsWith(HOME2)) return null;
@@ -3651,6 +3714,14 @@ var MacOSAdapter = class {
3651
3714
  * Screenshots & Computer Use
3652
3715
  * ══════════════════════════════════════════════════════════ */
3653
3716
  async screenshot() {
3717
+ const allowed = await hasScreenRecordingPermission();
3718
+ if (!allowed) {
3719
+ return {
3720
+ image: "",
3721
+ format: "jpeg",
3722
+ note: "Screen Recording permission is not granted for this Companion binary. Enable it in System Settings -> Privacy & Security -> Screen Recording, then reopen Pulso Companion."
3723
+ };
3724
+ }
3654
3725
  const ts = Date.now();
3655
3726
  const pngPath = `/tmp/pulso-ss-${ts}.png`;
3656
3727
  const jpgPath = `/tmp/pulso-ss-${ts}.jpg`;
@@ -4065,6 +4136,89 @@ end tell`);
4065
4136
  return { success: false, error: `JS execution failed: ${err.message}` };
4066
4137
  }
4067
4138
  }
4139
+ async browserListProfiles() {
4140
+ const os = homedir2();
4141
+ const browserPaths = [
4142
+ { browser: "Google Chrome", dir: `${os}/Library/Application Support/Google/Chrome` },
4143
+ { browser: "Google Chrome Beta", dir: `${os}/Library/Application Support/Google/Chrome Beta` },
4144
+ { browser: "Google Chrome Dev", dir: `${os}/Library/Application Support/Google/Chrome Dev` },
4145
+ { browser: "Microsoft Edge", dir: `${os}/Library/Application Support/Microsoft Edge` },
4146
+ { browser: "Brave Browser", dir: `${os}/Library/Application Support/BraveSoftware/Brave-Browser` },
4147
+ { browser: "Opera", dir: `${os}/Library/Application Support/com.operasoftware.Opera` },
4148
+ { browser: "Vivaldi", dir: `${os}/Library/Application Support/Vivaldi` },
4149
+ { browser: "Arc", dir: `${os}/Library/Application Support/Arc/User Data` },
4150
+ { browser: "Chromium", dir: `${os}/Library/Application Support/Chromium` }
4151
+ ];
4152
+ const profiles = [];
4153
+ for (const { browser, dir } of browserPaths) {
4154
+ if (!existsSync2(dir)) continue;
4155
+ let dirs = [];
4156
+ try {
4157
+ dirs = readdirSync2(dir);
4158
+ } catch {
4159
+ continue;
4160
+ }
4161
+ for (const entry of dirs) {
4162
+ if (entry !== "Default" && !entry.startsWith("Profile ")) continue;
4163
+ const prefsPath = `${dir}/${entry}/Preferences`;
4164
+ if (!existsSync2(prefsPath)) continue;
4165
+ try {
4166
+ const raw = readFileSync2(prefsPath, "utf-8");
4167
+ const prefs = JSON.parse(raw);
4168
+ const name = prefs.profile?.name || entry;
4169
+ const email = prefs.account_info?.[0]?.email;
4170
+ profiles.push({
4171
+ browser,
4172
+ profileDir: `${dir}/${entry}`,
4173
+ name,
4174
+ email,
4175
+ isDefault: entry === "Default"
4176
+ });
4177
+ } catch {
4178
+ }
4179
+ }
4180
+ }
4181
+ const firefoxPaths = [
4182
+ `${os}/Library/Application Support/Firefox/Profiles`,
4183
+ `${os}/Library/Application Support/Firefox Developer Edition/Profiles`
4184
+ ];
4185
+ for (const ffDir of firefoxPaths) {
4186
+ if (!existsSync2(ffDir)) continue;
4187
+ try {
4188
+ const dirs = readdirSync2(ffDir);
4189
+ const browserName = ffDir.includes("Developer") ? "Firefox Developer Edition" : "Firefox";
4190
+ for (const entry of dirs) {
4191
+ const userJs = `${ffDir}/${entry}/user.js`;
4192
+ const prefsJs = `${ffDir}/${entry}/prefs.js`;
4193
+ if (!existsSync2(prefsJs) && !existsSync2(userJs)) continue;
4194
+ profiles.push({
4195
+ browser: browserName,
4196
+ profileDir: `${ffDir}/${entry}`,
4197
+ name: entry.replace(/^[a-z0-9]+\./, ""),
4198
+ // strip hash prefix
4199
+ isDefault: entry.includes("default")
4200
+ });
4201
+ }
4202
+ } catch {
4203
+ }
4204
+ }
4205
+ let running = [];
4206
+ try {
4207
+ const ps = await runShell2("ps aux | grep -E '(Chrome|Edge|Brave|Firefox|Opera|Vivaldi|Arc)' | grep -v grep | awk '{print $11}'");
4208
+ running = ps.split("\n").filter(Boolean);
4209
+ } catch {
4210
+ }
4211
+ return {
4212
+ success: true,
4213
+ data: {
4214
+ profiles: profiles.map((p) => ({
4215
+ ...p,
4216
+ isRunning: running.some((r) => r.toLowerCase().includes(p.browser.toLowerCase().replace(/ /g, "")))
4217
+ })),
4218
+ total: profiles.length
4219
+ }
4220
+ };
4221
+ }
4068
4222
  /* ══════════════════════════════════════════════════════════
4069
4223
  * Productivity: Calendar
4070
4224
  * ══════════════════════════════════════════════════════════ */
@@ -6099,6 +6253,47 @@ foreach ($w in $windows) {
6099
6253
  };
6100
6254
  }
6101
6255
  }
6256
+ async browserListProfiles() {
6257
+ try {
6258
+ const userProfile = process.env.LOCALAPPDATA || process.env.APPDATA || "";
6259
+ const browserPaths = [
6260
+ { browser: "Google Chrome", dir: `${userProfile}\\Google\\Chrome\\User Data` },
6261
+ { browser: "Microsoft Edge", dir: `${userProfile}\\Microsoft\\Edge\\User Data` },
6262
+ { browser: "Brave Browser", dir: `${userProfile}\\BraveSoftware\\Brave-Browser\\User Data` },
6263
+ { browser: "Vivaldi", dir: `${userProfile}\\Vivaldi\\User Data` },
6264
+ { browser: "Opera", dir: `${userProfile}\\Opera Software\\Opera Stable` }
6265
+ ];
6266
+ const profiles = [];
6267
+ for (const { browser, dir } of browserPaths) {
6268
+ if (!existsSync3(dir)) continue;
6269
+ let entries = [];
6270
+ try {
6271
+ entries = readdirSync3(dir);
6272
+ } catch {
6273
+ continue;
6274
+ }
6275
+ for (const entry of entries) {
6276
+ if (entry !== "Default" && !entry.startsWith("Profile ")) continue;
6277
+ const prefsPath = `${dir}\\${entry}\\Preferences`;
6278
+ if (!existsSync3(prefsPath)) continue;
6279
+ try {
6280
+ const prefs = JSON.parse(readFileSync3(prefsPath, "utf-8"));
6281
+ profiles.push({
6282
+ browser,
6283
+ profileDir: `${dir}\\${entry}`,
6284
+ name: prefs.profile?.name || entry,
6285
+ email: prefs.account_info?.[0]?.email,
6286
+ isDefault: entry === "Default"
6287
+ });
6288
+ } catch {
6289
+ }
6290
+ }
6291
+ }
6292
+ return { success: true, data: { profiles, total: profiles.length } };
6293
+ } catch (err) {
6294
+ return { success: false, error: err.message };
6295
+ }
6296
+ }
6102
6297
  /* ══════════════════════════════════════════════════════════
6103
6298
  * Productivity: Calendar
6104
6299
  *
@@ -8755,6 +8950,61 @@ var WAKE_WORD_LOCAL_STT_BUDGET_MS_RAW = process.env.PULSO_WAKE_WORD_LOCAL_STT_BU
8755
8950
  var WS_BASE = API_URL.replace("https://", "wss://").replace("http://", "ws://") + "/ws/companion";
8756
8951
  var HOME4 = homedir4();
8757
8952
  var RECONNECT_DELAY = 5e3;
8953
+ var COMPANION_LOCK_FILE = join5(CREDENTIALS_DIR, "companion.lock.json");
8954
+ function releaseCompanionLock() {
8955
+ try {
8956
+ if (!existsSync4(COMPANION_LOCK_FILE)) return;
8957
+ const raw = readFileSync4(COMPANION_LOCK_FILE, "utf-8");
8958
+ const parsed = JSON.parse(raw);
8959
+ if (parsed?.pid === process.pid) {
8960
+ unlinkSync5(COMPANION_LOCK_FILE);
8961
+ }
8962
+ } catch {
8963
+ }
8964
+ }
8965
+ function acquireCompanionLock() {
8966
+ try {
8967
+ if (!existsSync4(CREDENTIALS_DIR)) {
8968
+ mkdirSync3(CREDENTIALS_DIR, { recursive: true });
8969
+ }
8970
+ if (existsSync4(COMPANION_LOCK_FILE)) {
8971
+ try {
8972
+ const raw = readFileSync4(COMPANION_LOCK_FILE, "utf-8");
8973
+ const parsed = JSON.parse(raw);
8974
+ const existingPid = Number(parsed?.pid || 0);
8975
+ if (existingPid > 1 && existingPid !== process.pid) {
8976
+ try {
8977
+ process.kill(existingPid, 0);
8978
+ console.log("");
8979
+ console.log(" \u26A0\uFE0F Another Pulso Companion instance is already running.");
8980
+ console.log(` PID: ${existingPid}${parsed?.startedAt ? ` (${parsed.startedAt})` : ""}`);
8981
+ console.log(" Exiting this instance to avoid command collisions.\n");
8982
+ process.exit(0);
8983
+ } catch {
8984
+ }
8985
+ }
8986
+ } catch {
8987
+ }
8988
+ }
8989
+ writeFileSync5(
8990
+ COMPANION_LOCK_FILE,
8991
+ JSON.stringify(
8992
+ {
8993
+ pid: process.pid,
8994
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8995
+ argv: process.argv.slice(2)
8996
+ },
8997
+ null,
8998
+ 2
8999
+ ),
9000
+ "utf-8"
9001
+ );
9002
+ if (platform() !== "win32") {
9003
+ chmodSync(COMPANION_LOCK_FILE, 384);
9004
+ }
9005
+ } catch {
9006
+ }
9007
+ }
8758
9008
  async function requestWsTicket() {
8759
9009
  try {
8760
9010
  const res = await fetch(`${API_URL}/ws/ticket`, {
@@ -8841,6 +9091,18 @@ function runSwift2(code, timeout = 1e4) {
8841
9091
  child.stdin?.end();
8842
9092
  });
8843
9093
  }
9094
+ async function hasScreenRecordingPermission2() {
9095
+ try {
9096
+ const out = await runSwift2(`
9097
+ import Cocoa
9098
+ import CoreGraphics
9099
+ print(CGPreflightScreenCaptureAccess() ? "granted" : "denied")
9100
+ `, 6e3);
9101
+ return out.trim().toLowerCase() === "granted";
9102
+ } catch {
9103
+ return false;
9104
+ }
9105
+ }
8844
9106
  var SETUP_DONE_FILE = join5(HOME4, ".pulso-companion-setup");
8845
9107
  async function setupPermissions() {
8846
9108
  const isFirstRun = !existsSync4(SETUP_DONE_FILE);
@@ -8944,6 +9206,39 @@ var ADAPTER_COMMANDS = {
8944
9206
  sys_open_url: (a, p) => a.openUrl(p.url),
8945
9207
  sys_speak: (a, p) => a.speak(p.text, p.voice),
8946
9208
  sys_notification: (a, p) => a.notification(p.title, p.message),
9209
+ sys_dialog_action: async (_, p) => {
9210
+ const buttonName = p.button;
9211
+ const procName = p.procName ?? activeDialog?.procName;
9212
+ if (!procName || !buttonName) {
9213
+ return { success: false, error: "No active dialog or button not specified" };
9214
+ }
9215
+ const script = `
9216
+ tell application "System Events"
9217
+ tell process "${procName.replace(/"/g, '\\"')}"
9218
+ set targetWindow to window 1
9219
+ repeat with w in windows
9220
+ try
9221
+ if role of w is "AXSheet" or role of w is "AXDialog" then
9222
+ set targetWindow to w
9223
+ exit repeat
9224
+ end if
9225
+ end try
9226
+ end repeat
9227
+ tell targetWindow
9228
+ click button "${buttonName.replace(/"/g, '\\"')}"
9229
+ end tell
9230
+ end tell
9231
+ end tell`;
9232
+ try {
9233
+ await runAppleScript2(script);
9234
+ console.log(` \u2713 Dialog action: clicked "${buttonName}" in [${procName}]`);
9235
+ activeDialog = null;
9236
+ lastDialogSignature = null;
9237
+ return { success: true, clicked: buttonName };
9238
+ } catch (e) {
9239
+ return { success: false, error: e.message };
9240
+ }
9241
+ },
8947
9242
  sys_clipboard_read: (a, _) => a.clipboardRead(),
8948
9243
  sys_clipboard_write: (a, p) => a.clipboardWrite(p.text),
8949
9244
  sys_screenshot: (a, _) => a.screenshot(),
@@ -8964,6 +9259,7 @@ var ADAPTER_COMMANDS = {
8964
9259
  sys_browser_new_tab: (a, p) => a.browserNewTab(p.url, p.browser),
8965
9260
  sys_browser_read_page: (a, p) => a.browserReadPage(p.browser, Number(p.maxLength) || void 0),
8966
9261
  sys_browser_execute_js: (a, p) => a.browserExecuteJs(p.code, p.browser),
9262
+ sys_browser_list_profiles: (a, _) => a.browserListProfiles(),
8967
9263
  sys_calendar_list: (a, p) => a.calendarList(Number(p.days) || void 0),
8968
9264
  sys_calendar_create: (a, p) => a.calendarCreate(
8969
9265
  p.title ?? p.summary,
@@ -9024,7 +9320,16 @@ var ADAPTER_COMMANDS = {
9024
9320
  return level !== void 0 ? a.setBrightness(Number(level)) : a.getBrightness();
9025
9321
  }
9026
9322
  };
9027
- async function handleCommand(command, params) {
9323
+ var claudePipeQueue = Promise.resolve();
9324
+ function runClaudePipeSerial(task) {
9325
+ const run = claudePipeQueue.then(task, task);
9326
+ claudePipeQueue = run.then(
9327
+ () => void 0,
9328
+ () => void 0
9329
+ );
9330
+ return run;
9331
+ }
9332
+ async function handleCommand(command, params, streamCb) {
9028
9333
  try {
9029
9334
  if (command === "ollama_detect") {
9030
9335
  try {
@@ -9072,6 +9377,14 @@ async function handleCommand(command, params) {
9072
9377
  const rh = params.height;
9073
9378
  if (rx == null || ry == null || rw == null || rh == null)
9074
9379
  return { success: false, error: "Missing x, y, width, or height" };
9380
+ const screenPermitted = await hasScreenRecordingPermission2();
9381
+ if (!screenPermitted) {
9382
+ return {
9383
+ success: false,
9384
+ error: "Screen Recording permission is not granted. Enable Pulso Companion in System Settings -> Privacy & Security -> Screen Recording, then reopen the app.",
9385
+ errorCode: "SCREEN_PERMISSION_REQUIRED"
9386
+ };
9387
+ }
9075
9388
  const ts2 = Date.now();
9076
9389
  const regPath = `/tmp/pulso-ss-region-${ts2}.png`;
9077
9390
  const regJpg = `/tmp/pulso-ss-region-${ts2}.jpg`;
@@ -9722,71 +10035,198 @@ print(result.stdout[:5000])
9722
10035
  const model = params.model;
9723
10036
  const maxTurns = params.max_turns;
9724
10037
  const systemPrompt = params.system_prompt;
9725
- const outputFormat = params.output_format || "json";
9726
- const timeout = Number(params.timeout) || 12e4;
9727
- const flags = ["-p", `--output-format ${outputFormat}`];
9728
- if (model) flags.push(`--model ${model}`);
9729
- if (maxTurns) flags.push(`--max-turns ${maxTurns}`);
9730
- if (systemPrompt) flags.push(`--append-system-prompt ${JSON.stringify(systemPrompt)}`);
9731
- flags.push("--allowedTools ''");
9732
- const cmd = `claude ${flags.join(" ")}`;
9733
- return new Promise((resolve5) => {
9734
- const child = exec5(cmd, { timeout }, (err, stdout, stderr) => {
9735
- if (err) {
9736
- resolve5({
9737
- success: false,
9738
- error: `Claude pipe error: ${stderr || err.message}`,
9739
- errorCode: "CLAUDE_PIPE_FAILED"
9740
- });
9741
- } else {
10038
+ const effort = params.effort || "low";
10039
+ const outputFormat = params.output_format || "text";
10040
+ const timeout = Number(params.timeout) || 18e4;
10041
+ const home = process.env.HOME || "";
10042
+ const claudePaths = [
10043
+ `${home}/.local/bin/claude`,
10044
+ `${home}/.local/share/claude/versions/latest/claude`,
10045
+ "/usr/local/bin/claude",
10046
+ "/opt/homebrew/bin/claude"
10047
+ ];
10048
+ let claudeBin = "claude";
10049
+ for (const p of claudePaths) {
10050
+ try {
10051
+ execSync3(`test -x "${p}"`, { stdio: "ignore" });
10052
+ claudeBin = p;
10053
+ break;
10054
+ } catch {
10055
+ }
10056
+ }
10057
+ const useStreamJson = Boolean(streamCb);
10058
+ const effectiveOutputFormat = useStreamJson ? "stream-json" : outputFormat;
10059
+ const args = ["-p", "--output-format", effectiveOutputFormat];
10060
+ if (useStreamJson) {
10061
+ args.push("--verbose", "--include-partial-messages");
10062
+ }
10063
+ args.push("--tools", "");
10064
+ if (systemPrompt && systemPrompt.trim()) {
10065
+ args.push("--system-prompt", systemPrompt.trim());
10066
+ }
10067
+ if (effort) args.push("--effort", effort);
10068
+ if (model) args.push("--model", model);
10069
+ if (maxTurns) args.push("--max-turns", String(maxTurns));
10070
+ const promptInput = prompt;
10071
+ return runClaudePipeSerial(
10072
+ () => new Promise((resolve5) => {
10073
+ let stdout = "";
10074
+ let stderr = "";
10075
+ let streamBuffer = "";
10076
+ let streamedResponse = "";
10077
+ let finalResultText = "";
10078
+ let assistantSnapshotText = "";
10079
+ const processJsonLine = (line) => {
10080
+ const trimmed = line.trim();
10081
+ if (!trimmed) return;
10082
+ let evt = null;
9742
10083
  try {
9743
- if (outputFormat === "json") {
9744
- const parsed = JSON.parse(stdout);
10084
+ evt = JSON.parse(trimmed);
10085
+ } catch {
10086
+ return;
10087
+ }
10088
+ const deltaType = evt?.event?.delta?.type;
10089
+ if (evt?.type === "stream_event" && deltaType === "text_delta") {
10090
+ const deltaText = String(evt?.event?.delta?.text ?? "");
10091
+ if (deltaText) {
10092
+ streamedResponse += deltaText;
10093
+ if (streamCb) streamCb(deltaText);
10094
+ }
10095
+ return;
10096
+ }
10097
+ if (evt?.type === "result" && typeof evt?.result === "string") {
10098
+ finalResultText = evt.result;
10099
+ return;
10100
+ }
10101
+ if (evt?.type === "assistant" && Array.isArray(evt?.message?.content)) {
10102
+ const textBlocks = evt.message.content.filter((p) => p?.type === "text" && typeof p?.text === "string").map((p) => p.text);
10103
+ if (textBlocks.length > 0) {
10104
+ assistantSnapshotText = textBlocks.join("");
10105
+ }
10106
+ }
10107
+ };
10108
+ const childEnv = {
10109
+ ...process.env,
10110
+ PATH: `${home}/.local/bin:${process.env.PATH}`
10111
+ };
10112
+ delete childEnv.CLAUDECODE;
10113
+ delete childEnv.CLAUDE_CODE;
10114
+ const child = spawn(claudeBin, args, {
10115
+ env: childEnv,
10116
+ timeout,
10117
+ stdio: ["pipe", "pipe", "pipe"]
10118
+ });
10119
+ child.stdout.on("data", (chunk) => {
10120
+ const text = chunk.toString();
10121
+ stdout += text;
10122
+ if (useStreamJson) {
10123
+ streamBuffer += text;
10124
+ const lines = streamBuffer.split(/\r?\n/);
10125
+ streamBuffer = lines.pop() || "";
10126
+ for (const ln of lines) {
10127
+ processJsonLine(ln);
10128
+ }
10129
+ } else if (streamCb) {
10130
+ streamCb(text);
10131
+ }
10132
+ });
10133
+ child.stderr.on("data", (chunk) => {
10134
+ stderr += chunk.toString();
10135
+ });
10136
+ child.on("close", (code) => {
10137
+ if (code !== 0) {
10138
+ const detail = (stderr.trim() || stdout.trim() || "Unknown error").slice(0, 500);
10139
+ const errorCode = detail.toLowerCase().includes("out of extra usage") ? "CLAUDE_USAGE_LIMIT" : "CLAUDE_PIPE_FAILED";
10140
+ resolve5({
10141
+ success: false,
10142
+ error: `Claude pipe error (exit ${code}): ${detail}`,
10143
+ errorCode
10144
+ });
10145
+ return;
10146
+ }
10147
+ try {
10148
+ if (effectiveOutputFormat === "stream-json") {
10149
+ if (streamBuffer.trim()) {
10150
+ processJsonLine(streamBuffer);
10151
+ }
10152
+ const response = (finalResultText || streamedResponse || assistantSnapshotText || "").trim();
9745
10153
  resolve5({
9746
10154
  success: true,
9747
10155
  data: {
9748
- response: parsed.result || stdout.trim(),
9749
- session_id: parsed.session_id,
9750
- cost_usd: parsed.total_cost_usd ?? 0,
9751
- duration_ms: parsed.duration_ms,
9752
- num_turns: parsed.num_turns,
10156
+ response,
9753
10157
  model: model || "default",
9754
10158
  via: "claude-max-subscription"
9755
10159
  }
9756
10160
  });
9757
- } else {
10161
+ return;
10162
+ }
10163
+ if (outputFormat === "json") {
10164
+ const parsed = JSON.parse(stdout);
9758
10165
  resolve5({
9759
10166
  success: true,
9760
10167
  data: {
9761
- response: stdout.trim(),
10168
+ response: parsed.result || stdout.trim(),
10169
+ session_id: parsed.session_id,
10170
+ cost_usd: 0,
10171
+ duration_ms: parsed.duration_ms,
10172
+ num_turns: parsed.num_turns,
9762
10173
  model: model || "default",
9763
10174
  via: "claude-max-subscription"
9764
10175
  }
9765
10176
  });
10177
+ return;
9766
10178
  }
9767
10179
  } catch {
9768
- resolve5({
9769
- success: true,
9770
- data: {
9771
- response: stdout.trim(),
9772
- model: model || "default",
9773
- via: "claude-max-subscription"
9774
- }
9775
- });
9776
10180
  }
9777
- }
9778
- });
9779
- child.stdin?.write(prompt);
9780
- child.stdin?.end();
9781
- });
10181
+ resolve5({
10182
+ success: true,
10183
+ data: {
10184
+ response: stdout.trim(),
10185
+ model: model || "default",
10186
+ via: "claude-max-subscription"
10187
+ }
10188
+ });
10189
+ });
10190
+ child.on("error", (err) => {
10191
+ resolve5({
10192
+ success: false,
10193
+ error: `Claude pipe spawn error: ${err.message}`,
10194
+ errorCode: "CLAUDE_PIPE_FAILED"
10195
+ });
10196
+ });
10197
+ child.stdin.write(promptInput);
10198
+ child.stdin.end();
10199
+ })
10200
+ );
9782
10201
  }
9783
10202
  case "sys_claude_status": {
9784
10203
  try {
9785
10204
  const version = await runShell4("claude --version 2>/dev/null", 5e3);
9786
10205
  let authStatus = "unknown";
10206
+ let authDetails;
9787
10207
  try {
9788
10208
  const status = await runShell4("claude auth status 2>&1", 1e4);
9789
- authStatus = status.includes("Authenticated") || status.includes("logged in") ? "authenticated" : "not_authenticated";
10209
+ const trimmed = status.trim();
10210
+ let parsed = null;
10211
+ try {
10212
+ parsed = JSON.parse(trimmed);
10213
+ } catch {
10214
+ parsed = null;
10215
+ }
10216
+ if (parsed && typeof parsed === "object") {
10217
+ const loggedIn = parsed.loggedIn === true;
10218
+ authStatus = loggedIn ? "authenticated" : "not_authenticated";
10219
+ authDetails = {
10220
+ authMethod: typeof parsed.authMethod === "string" ? parsed.authMethod : void 0,
10221
+ apiProvider: typeof parsed.apiProvider === "string" ? parsed.apiProvider : void 0,
10222
+ email: typeof parsed.email === "string" ? parsed.email : void 0,
10223
+ orgId: typeof parsed.orgId === "string" ? parsed.orgId : void 0,
10224
+ subscriptionType: typeof parsed.subscriptionType === "string" ? parsed.subscriptionType : void 0
10225
+ };
10226
+ } else {
10227
+ const lower = trimmed.toLowerCase();
10228
+ authStatus = lower.includes("authenticated") || lower.includes("logged in") || lower.includes('loggedin":true') ? "authenticated" : "not_authenticated";
10229
+ }
9790
10230
  } catch {
9791
10231
  authStatus = "not_authenticated";
9792
10232
  }
@@ -9796,7 +10236,8 @@ print(result.stdout[:5000])
9796
10236
  installed: true,
9797
10237
  version: version.trim(),
9798
10238
  authenticated: authStatus === "authenticated",
9799
- status: authStatus
10239
+ status: authStatus,
10240
+ ...authDetails ? { details: authDetails } : {}
9800
10241
  }
9801
10242
  };
9802
10243
  } catch {
@@ -9990,6 +10431,99 @@ function stopImessageMonitor() {
9990
10431
  imessageTimer = null;
9991
10432
  }
9992
10433
  }
10434
+ var permissionDialogTimer = null;
10435
+ var lastDialogSignature = null;
10436
+ var DIALOG_POLL_INTERVAL = 1500;
10437
+ var activeDialog = null;
10438
+ function startPermissionDialogWatcher() {
10439
+ if (adapter.platform !== "macos") return;
10440
+ permissionDialogTimer = setInterval(async () => {
10441
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
10442
+ try {
10443
+ const script = `
10444
+ tell application "System Events"
10445
+ set procList to every process where visible is true
10446
+ repeat with proc in procList
10447
+ set procName to name of proc as string
10448
+ repeat with w in (windows of proc)
10449
+ try
10450
+ set wRole to role of w as string
10451
+ if wRole is "AXSheet" or wRole is "AXDialog" then
10452
+ set wTitle to ""
10453
+ try
10454
+ set wTitle to title of w as string
10455
+ end try
10456
+ set wDesc to ""
10457
+ try
10458
+ set wDesc to description of w as string
10459
+ end try
10460
+ set btnNames to {}
10461
+ repeat with btn in (buttons of w)
10462
+ try
10463
+ set end of btnNames to (name of btn as string)
10464
+ end try
10465
+ end repeat
10466
+ if (count of btnNames) > 0 then
10467
+ set btnStr to ""
10468
+ repeat with b in btnNames
10469
+ if btnStr is not "" then set btnStr to btnStr & "|||"
10470
+ set btnStr to btnStr & b
10471
+ end repeat
10472
+ return procName & ":::" & wTitle & ":::" & wDesc & ":::" & btnStr
10473
+ end if
10474
+ end if
10475
+ end try
10476
+ end repeat
10477
+ end repeat
10478
+ return ""
10479
+ end tell`;
10480
+ const raw = await runAppleScript2(script);
10481
+ if (!raw) {
10482
+ if (activeDialog) {
10483
+ lastDialogSignature = null;
10484
+ activeDialog = null;
10485
+ }
10486
+ return;
10487
+ }
10488
+ const sig = raw;
10489
+ if (sig === lastDialogSignature) return;
10490
+ lastDialogSignature = sig;
10491
+ const [procName = "", title = "", desc = "", btnStr = ""] = raw.split(":::");
10492
+ const buttons = btnStr.split("|||").map((b) => b.trim()).filter(Boolean);
10493
+ const dialogId = `dlg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
10494
+ activeDialog = { dialogId, procName, buttons, detectedAt: Date.now() };
10495
+ let screenshot;
10496
+ try {
10497
+ const scrResult = await adapter.screenshot();
10498
+ if (scrResult.image) {
10499
+ screenshot = scrResult.image;
10500
+ }
10501
+ } catch {
10502
+ }
10503
+ console.log(`
10504
+ \u{1F514} Permission dialog: [${procName}] "${title}" \u2014 buttons: ${buttons.join(", ")}`);
10505
+ ws.send(JSON.stringify({
10506
+ type: "permission_dialog",
10507
+ dialogId,
10508
+ procName,
10509
+ title: title || procName,
10510
+ message: desc,
10511
+ buttons,
10512
+ screenshot,
10513
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10514
+ }));
10515
+ } catch {
10516
+ }
10517
+ }, DIALOG_POLL_INTERVAL);
10518
+ }
10519
+ function stopPermissionDialogWatcher() {
10520
+ if (permissionDialogTimer) {
10521
+ clearInterval(permissionDialogTimer);
10522
+ permissionDialogTimer = null;
10523
+ }
10524
+ lastDialogSignature = null;
10525
+ activeDialog = null;
10526
+ }
9993
10527
  var LAZY_CAPABILITY_TOOLS = [
9994
10528
  "sys_calendar_list",
9995
10529
  "sys_calendar_create",
@@ -10043,7 +10577,7 @@ var CAPABILITY_PROBES = [
10043
10577
  name: "claude_cli",
10044
10578
  test: async () => {
10045
10579
  try {
10046
- await runShell4("which claude >/dev/null 2>&1 && claude --version >/dev/null 2>&1", 5e3);
10580
+ await runShell4('(which claude >/dev/null 2>&1 || test -x "$HOME/.local/bin/claude" || test -x "$HOME/.local/share/claude/versions/"*/claude) && (claude --version >/dev/null 2>&1 || "$HOME/.local/bin/claude" --version >/dev/null 2>&1)', 5e3);
10047
10581
  return true;
10048
10582
  } catch {
10049
10583
  return false;
@@ -10193,6 +10727,8 @@ async function connect() {
10193
10727
  }, HEARTBEAT_INTERVAL);
10194
10728
  stopImessageMonitor();
10195
10729
  startImessageMonitor();
10730
+ stopPermissionDialogWatcher();
10731
+ startPermissionDialogWatcher();
10196
10732
  });
10197
10733
  ws.on("message", async (raw) => {
10198
10734
  try {
@@ -10210,7 +10746,13 @@ async function connect() {
10210
10746
  \u26A1 Command: ${msg.command}`,
10211
10747
  msg.params ? JSON.stringify(msg.params).slice(0, 200) : ""
10212
10748
  );
10213
- const result = await handleCommand(msg.command, msg.params ?? {});
10749
+ const streamCb = (chunk) => {
10750
+ try {
10751
+ ws.send(JSON.stringify({ id: msg.id, type: "stream", chunk }));
10752
+ } catch {
10753
+ }
10754
+ };
10755
+ const result = await handleCommand(msg.command, msg.params ?? {}, streamCb);
10214
10756
  console.log(
10215
10757
  ` \u2192 ${result.success ? "\u2705" : "\u274C"}`,
10216
10758
  result.success ? JSON.stringify(result.data).slice(0, 200) : result.error
@@ -10228,6 +10770,7 @@ async function connect() {
10228
10770
  console.log(`
10229
10771
  \u{1F50C} Disconnected (${code}: ${reasonStr})`);
10230
10772
  stopImessageMonitor();
10773
+ stopPermissionDialogWatcher();
10231
10774
  if (heartbeatTimer) {
10232
10775
  clearInterval(heartbeatTimer);
10233
10776
  heartbeatTimer = null;
@@ -10933,6 +11476,10 @@ function writeString(view, offset, str) {
10933
11476
  }
10934
11477
  var currentPlatform = detectPlatform();
10935
11478
  var platformName = { macos: "macOS", windows: "Windows", linux: "Linux" }[currentPlatform] || "Unknown";
11479
+ acquireCompanionLock();
11480
+ process.on("exit", () => {
11481
+ releaseCompanionLock();
11482
+ });
10936
11483
  console.log("");
10937
11484
  console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
10938
11485
  console.log(` \u2551 Pulso ${platformName} Companion v0.4.3 \u2551`);
@@ -10958,10 +11505,12 @@ process.on("SIGINT", () => {
10958
11505
  console.log("\n\u{1F44B} Shutting down Pulso Companion...");
10959
11506
  wakeWordActive = false;
10960
11507
  ws?.close(1e3, "User shutdown");
11508
+ releaseCompanionLock();
10961
11509
  process.exit(0);
10962
11510
  });
10963
11511
  process.on("SIGTERM", () => {
10964
11512
  wakeWordActive = false;
10965
11513
  ws?.close(1e3, "Process terminated");
11514
+ releaseCompanionLock();
10966
11515
  process.exit(0);
10967
11516
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {