@pulso/companion 0.4.1 → 0.4.2

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 +304 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/index.ts
4
10
  import WebSocket from "ws";
@@ -62,12 +68,33 @@ function runAppleScript(script) {
62
68
  }
63
69
  function runShell(cmd, timeout = 1e4) {
64
70
  return new Promise((resolve2, reject) => {
65
- exec(cmd, { timeout }, (err, stdout, stderr) => {
71
+ const shell = process.env.SHELL || "/bin/zsh";
72
+ exec(cmd, { timeout, shell, env: { ...process.env, PATH: augmentedPath() } }, (err, stdout, stderr) => {
66
73
  if (err) reject(new Error(stderr || err.message));
67
74
  else resolve2(stdout.trim());
68
75
  });
69
76
  });
70
77
  }
78
+ function augmentedPath() {
79
+ const base = process.env.PATH || "/usr/local/bin:/usr/bin:/bin";
80
+ const home = process.env.HOME || "";
81
+ const extras = [];
82
+ const nvmDir = process.env.NVM_DIR || `${home}/.nvm`;
83
+ try {
84
+ const fs = __require("fs");
85
+ const defaultAlias = fs.readFileSync(`${nvmDir}/alias/default`, "utf8").trim();
86
+ const versionsDir = `${nvmDir}/versions/node`;
87
+ const dirs = fs.readdirSync(versionsDir);
88
+ const match = dirs.filter((d) => d.includes(defaultAlias)).sort().pop() || dirs.sort().pop();
89
+ if (match) extras.push(`${versionsDir}/${match}/bin`);
90
+ } catch {
91
+ }
92
+ if (home) extras.push(`${home}/.volta/bin`);
93
+ if (home) extras.push(`${home}/.fnm/aliases/default/bin`);
94
+ extras.push("/opt/homebrew/bin", "/usr/local/bin");
95
+ const allParts = [...extras, ...base.split(":")];
96
+ return [...new Set(allParts)].join(":");
97
+ }
71
98
  function runSwift(code, timeout = 1e4) {
72
99
  return new Promise((resolve2, reject) => {
73
100
  const child = exec(`swift -`, { timeout }, (err, stdout, stderr) => {
@@ -534,11 +561,12 @@ async function handleCommand(command, params) {
534
561
  try {
535
562
  await runAppleScript(`
536
563
  tell application "Google Chrome"
564
+ make new window
565
+ set URL of active tab of front window to "${sanitizedUrl}"
537
566
  activate
538
- tell front window to make new tab with properties {URL:"${sanitizedUrl}"}
539
567
  end tell`);
540
568
  } catch {
541
- await runShell(`open "${url.replace(/"/g, "")}"`);
569
+ await runShell(`open -n "${url.replace(/"/g, "")}"`);
542
570
  }
543
571
  return { success: true, data: { opened: url } };
544
572
  }
@@ -1201,18 +1229,15 @@ print("\\(x),\\(y)")`;
1201
1229
  if (browser === "Safari") {
1202
1230
  await runAppleScript(`
1203
1231
  tell application "Safari"
1232
+ make new document with properties {URL:"${url.replace(/"/g, '\\"')}"}
1204
1233
  activate
1205
- if (count of windows) = 0 then make new document
1206
- tell front window to set current tab to (make new tab with properties {URL:"${url.replace(/"/g, '\\"')}"})
1207
1234
  end tell`);
1208
1235
  } else {
1209
1236
  await runAppleScript(`
1210
1237
  tell application "${browser.replace(/"/g, '\\"')}"
1238
+ make new window
1239
+ set URL of active tab of front window to "${url.replace(/"/g, '\\"')}"
1211
1240
  activate
1212
- if (count of windows) = 0 then
1213
- make new window
1214
- end if
1215
- tell front window to make new tab with properties {URL:"${url.replace(/"/g, '\\"')}"}
1216
1241
  end tell`);
1217
1242
  }
1218
1243
  return { success: true, data: { navigated: url, browser } };
@@ -1231,21 +1256,22 @@ print("\\(x),\\(y)")`;
1231
1256
  if (browser === "Safari") {
1232
1257
  await runAppleScript(`
1233
1258
  tell application "Safari"
1259
+ make new document with properties {URL:"${url.replace(/"/g, '\\"')}"}
1234
1260
  activate
1235
- tell front window to set current tab to (make new tab with properties {URL:"${url.replace(/"/g, '\\"')}"})
1236
1261
  end tell`);
1237
1262
  } else {
1238
1263
  await runAppleScript(`
1239
1264
  tell application "${browser.replace(/"/g, '\\"')}"
1265
+ make new window
1266
+ set URL of active tab of front window to "${url.replace(/"/g, '\\"')}"
1240
1267
  activate
1241
- tell front window to make new tab with properties {URL:"${url.replace(/"/g, '\\"')}"}
1242
1268
  end tell`);
1243
1269
  }
1244
1270
  return { success: true, data: { opened: url, browser } };
1245
1271
  } catch (err) {
1246
1272
  return {
1247
1273
  success: false,
1248
- error: `Failed to open tab: ${err.message}`
1274
+ error: `Failed to open window: ${err.message}`
1249
1275
  };
1250
1276
  }
1251
1277
  }
@@ -2952,6 +2978,170 @@ print(result.stdout[:5000])
2952
2978
  }
2953
2979
  return { success: false, error: "Use action: list, start, stop" };
2954
2980
  }
2981
+ // ── Claude Code Pipe (Max Subscription) ────────────────
2982
+ case "sys_claude_pipe": {
2983
+ const prompt = params.prompt;
2984
+ if (!prompt) return { success: false, error: "Missing prompt" };
2985
+ const model = params.model;
2986
+ const maxTurns = params.max_turns;
2987
+ const systemPrompt = params.system_prompt;
2988
+ const outputFormat = params.output_format || "json";
2989
+ const timeout = Number(params.timeout) || 12e4;
2990
+ const flags = ["-p", `--output-format ${outputFormat}`];
2991
+ if (model) flags.push(`--model ${model}`);
2992
+ if (maxTurns) flags.push(`--max-turns ${maxTurns}`);
2993
+ if (systemPrompt) flags.push(`--append-system-prompt ${JSON.stringify(systemPrompt)}`);
2994
+ flags.push("--allowedTools ''");
2995
+ const cmd = `claude ${flags.join(" ")}`;
2996
+ return new Promise((resolve2) => {
2997
+ const child = exec(cmd, { timeout }, (err, stdout, stderr) => {
2998
+ if (err) {
2999
+ resolve2({
3000
+ success: false,
3001
+ error: `Claude pipe error: ${stderr || err.message}`,
3002
+ errorCode: "CLAUDE_PIPE_FAILED"
3003
+ });
3004
+ } else {
3005
+ try {
3006
+ if (outputFormat === "json") {
3007
+ const parsed = JSON.parse(stdout);
3008
+ resolve2({
3009
+ success: true,
3010
+ data: {
3011
+ response: parsed.result || stdout.trim(),
3012
+ session_id: parsed.session_id,
3013
+ cost_usd: parsed.total_cost_usd ?? 0,
3014
+ duration_ms: parsed.duration_ms,
3015
+ num_turns: parsed.num_turns,
3016
+ model: model || "default",
3017
+ via: "claude-max-subscription"
3018
+ }
3019
+ });
3020
+ } else {
3021
+ resolve2({
3022
+ success: true,
3023
+ data: {
3024
+ response: stdout.trim(),
3025
+ model: model || "default",
3026
+ via: "claude-max-subscription"
3027
+ }
3028
+ });
3029
+ }
3030
+ } catch {
3031
+ resolve2({
3032
+ success: true,
3033
+ data: {
3034
+ response: stdout.trim(),
3035
+ model: model || "default",
3036
+ via: "claude-max-subscription"
3037
+ }
3038
+ });
3039
+ }
3040
+ }
3041
+ });
3042
+ child.stdin?.write(prompt);
3043
+ child.stdin?.end();
3044
+ });
3045
+ }
3046
+ case "sys_claude_status": {
3047
+ try {
3048
+ const version = await runShell("claude --version 2>/dev/null", 5e3);
3049
+ let authStatus = "unknown";
3050
+ try {
3051
+ const status = await runShell("claude auth status 2>&1", 1e4);
3052
+ authStatus = status.includes("Authenticated") || status.includes("logged in") ? "authenticated" : "not_authenticated";
3053
+ } catch {
3054
+ authStatus = "not_authenticated";
3055
+ }
3056
+ return {
3057
+ success: true,
3058
+ data: {
3059
+ installed: true,
3060
+ version: version.trim(),
3061
+ authenticated: authStatus === "authenticated",
3062
+ status: authStatus
3063
+ }
3064
+ };
3065
+ } catch {
3066
+ return {
3067
+ success: true,
3068
+ data: {
3069
+ installed: false,
3070
+ version: null,
3071
+ authenticated: false,
3072
+ status: "not_installed"
3073
+ }
3074
+ };
3075
+ }
3076
+ }
3077
+ // ── OpenAI Codex CLI (ChatGPT Subscription) ────────────────
3078
+ case "sys_codex_status": {
3079
+ try {
3080
+ const version = await runShell("codex --version 2>/dev/null", 5e3);
3081
+ let authStatus = "unknown";
3082
+ try {
3083
+ const status = await runShell("codex auth whoami 2>&1 || codex --help 2>&1 | head -5", 1e4);
3084
+ const lc = status.toLowerCase();
3085
+ authStatus = lc.includes("not logged in") || lc.includes("not authenticated") || lc.includes("sign in") || lc.includes("no api key") ? "not_authenticated" : "authenticated";
3086
+ } catch {
3087
+ try {
3088
+ await runShell("security find-generic-password -s 'openai-codex' 2>/dev/null || security find-generic-password -s 'codex' 2>/dev/null", 5e3);
3089
+ authStatus = "authenticated";
3090
+ } catch {
3091
+ authStatus = "not_authenticated";
3092
+ }
3093
+ }
3094
+ return {
3095
+ success: true,
3096
+ data: {
3097
+ installed: true,
3098
+ version: version.trim(),
3099
+ authenticated: authStatus === "authenticated",
3100
+ status: authStatus
3101
+ }
3102
+ };
3103
+ } catch {
3104
+ return {
3105
+ success: true,
3106
+ data: {
3107
+ installed: false,
3108
+ version: null,
3109
+ authenticated: false,
3110
+ status: "not_installed"
3111
+ }
3112
+ };
3113
+ }
3114
+ }
3115
+ case "sys_codex_pipe": {
3116
+ const prompt = params.prompt;
3117
+ if (!prompt) return { success: false, error: "Missing prompt" };
3118
+ const model = params.model;
3119
+ const timeout = Number(params.timeout) || 12e4;
3120
+ const args = ["exec"];
3121
+ if (model) args.push("--model", model);
3122
+ args.push(JSON.stringify(prompt));
3123
+ const cmd = `codex ${args.join(" ")}`;
3124
+ return new Promise((resolve2) => {
3125
+ exec(cmd, { timeout }, (err, stdout, stderr) => {
3126
+ if (err) {
3127
+ resolve2({
3128
+ success: false,
3129
+ error: `Codex pipe error: ${stderr || err.message}`,
3130
+ errorCode: "CODEX_PIPE_FAILED"
3131
+ });
3132
+ } else {
3133
+ resolve2({
3134
+ success: true,
3135
+ data: {
3136
+ response: stdout.trim(),
3137
+ model: model || "default",
3138
+ via: "chatgpt-subscription"
3139
+ }
3140
+ });
3141
+ }
3142
+ });
3143
+ });
3144
+ }
2955
3145
  default:
2956
3146
  return { success: false, error: `Unknown command: ${command}` };
2957
3147
  }
@@ -3086,8 +3276,81 @@ async function sonosRequest(baseUrl, path) {
3086
3276
  var ws = null;
3087
3277
  var reconnectTimer = null;
3088
3278
  var heartbeatTimer = null;
3279
+ var imessageTimer = null;
3089
3280
  var HEARTBEAT_INTERVAL = 3e4;
3281
+ var IMESSAGE_POLL_INTERVAL = 3e3;
3090
3282
  var reconnectAttempts = 0;
3283
+ var lastImessageRowId = 0;
3284
+ function startImessageMonitor() {
3285
+ const chatDbPath = join(HOME, "Library/Messages/chat.db");
3286
+ if (!existsSync(chatDbPath)) {
3287
+ console.log(" \u26A0 iMessage: chat.db not found \u2014 monitor disabled");
3288
+ return;
3289
+ }
3290
+ try {
3291
+ const initResult = execSync(
3292
+ `sqlite3 "${chatDbPath}" "SELECT MAX(ROWID) FROM message"`,
3293
+ { encoding: "utf8", timeout: 5e3 }
3294
+ ).trim();
3295
+ lastImessageRowId = parseInt(initResult, 10) || 0;
3296
+ console.log(` \u2713 iMessage: monitoring from ROWID ${lastImessageRowId}`);
3297
+ } catch (err) {
3298
+ console.log(` \u26A0 iMessage: failed to read chat.db \u2014 ${err.message}`);
3299
+ console.log(" Grant Full Disk Access to Terminal/iTerm in System Settings \u2192 Privacy & Security");
3300
+ return;
3301
+ }
3302
+ imessageTimer = setInterval(async () => {
3303
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
3304
+ try {
3305
+ const query = `
3306
+ SELECT m.ROWID, m.text, m.date,
3307
+ COALESCE(h.id, '') as sender_id,
3308
+ COALESCE(c.display_name, h.id, 'Unknown') as sender_name,
3309
+ c.chat_identifier
3310
+ FROM message m
3311
+ LEFT JOIN handle h ON m.handle_id = h.ROWID
3312
+ LEFT JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
3313
+ LEFT JOIN chat c ON cmj.chat_id = c.ROWID
3314
+ WHERE m.ROWID > ${lastImessageRowId}
3315
+ AND m.is_from_me = 0
3316
+ AND m.text IS NOT NULL
3317
+ AND m.text != ''
3318
+ ORDER BY m.ROWID ASC
3319
+ LIMIT 10
3320
+ `.replace(/\n/g, " ");
3321
+ const result = execSync(
3322
+ `sqlite3 -separator '|||' "${chatDbPath}" "${query}"`,
3323
+ { encoding: "utf8", timeout: 5e3 }
3324
+ ).trim();
3325
+ if (!result) return;
3326
+ const lines = result.split("\n").filter(Boolean);
3327
+ for (const line of lines) {
3328
+ const [rowIdStr, text, , senderId, senderName, chatId] = line.split("|||");
3329
+ const rowId = parseInt(rowIdStr || "0", 10);
3330
+ if (rowId <= lastImessageRowId) continue;
3331
+ lastImessageRowId = rowId;
3332
+ if (!text || text.startsWith("\uFFFC")) continue;
3333
+ console.log(`
3334
+ \u{1F4AC} iMessage from ${senderName || senderId}: ${text.slice(0, 80)}`);
3335
+ ws.send(JSON.stringify({
3336
+ type: "imessage_incoming",
3337
+ from: senderId || "unknown",
3338
+ fromName: senderName || senderId || "Unknown",
3339
+ chatId: chatId || senderId || "unknown",
3340
+ text,
3341
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3342
+ }));
3343
+ }
3344
+ } catch {
3345
+ }
3346
+ }, IMESSAGE_POLL_INTERVAL);
3347
+ }
3348
+ function stopImessageMonitor() {
3349
+ if (imessageTimer) {
3350
+ clearInterval(imessageTimer);
3351
+ imessageTimer = null;
3352
+ }
3353
+ }
3091
3354
  var CAPABILITY_PROBES = [
3092
3355
  {
3093
3356
  name: "calendar",
@@ -3197,6 +3460,30 @@ var CAPABILITY_PROBES = [
3197
3460
  }
3198
3461
  },
3199
3462
  tools: ["sys_shortcuts_run", "sys_shortcuts_list"]
3463
+ },
3464
+ {
3465
+ name: "claude_cli",
3466
+ test: async () => {
3467
+ try {
3468
+ await runShell("which claude >/dev/null 2>&1 && claude --version >/dev/null 2>&1", 5e3);
3469
+ return true;
3470
+ } catch {
3471
+ return false;
3472
+ }
3473
+ },
3474
+ tools: ["sys_claude_pipe", "sys_claude_status"]
3475
+ },
3476
+ {
3477
+ name: "codex_cli",
3478
+ test: async () => {
3479
+ try {
3480
+ await runShell("which codex >/dev/null 2>&1 && codex --version >/dev/null 2>&1", 5e3);
3481
+ return true;
3482
+ } catch {
3483
+ return false;
3484
+ }
3485
+ },
3486
+ tools: ["sys_codex_pipe", "sys_codex_status"]
3200
3487
  }
3201
3488
  ];
3202
3489
  var verifiedCapabilities = {
@@ -3305,6 +3592,8 @@ function connect() {
3305
3592
  platform: "macos",
3306
3593
  version: "0.4.0",
3307
3594
  accessLevel: ACCESS_LEVEL,
3595
+ homeDir: HOME,
3596
+ hostname: __require("os").hostname(),
3308
3597
  capabilities: cap.available,
3309
3598
  unavailable: cap.unavailable,
3310
3599
  tools: cap.tools,
@@ -3317,6 +3606,8 @@ function connect() {
3317
3606
  ws.send(JSON.stringify({ type: "ping" }));
3318
3607
  }
3319
3608
  }, HEARTBEAT_INTERVAL);
3609
+ stopImessageMonitor();
3610
+ startImessageMonitor();
3320
3611
  });
3321
3612
  ws.on("message", async (raw) => {
3322
3613
  try {
@@ -3351,6 +3642,7 @@ function connect() {
3351
3642
  const reasonStr = reason.toString() || "unknown";
3352
3643
  console.log(`
3353
3644
  \u{1F50C} Disconnected (${code}: ${reasonStr})`);
3645
+ stopImessageMonitor();
3354
3646
  if (heartbeatTimer) {
3355
3647
  clearInterval(heartbeatTimer);
3356
3648
  heartbeatTimer = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {