@pulso/companion 0.4.0 → 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 +312 -13
  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) => {
@@ -530,7 +557,17 @@ async function handleCommand(command, params) {
530
557
  case "sys_open_url": {
531
558
  const url = params.url;
532
559
  if (!url) return { success: false, error: "Missing URL" };
533
- await runShell(`open "${url.replace(/"/g, "")}"`);
560
+ const sanitizedUrl = url.replace(/"/g, '\\"');
561
+ try {
562
+ await runAppleScript(`
563
+ tell application "Google Chrome"
564
+ make new window
565
+ set URL of active tab of front window to "${sanitizedUrl}"
566
+ activate
567
+ end tell`);
568
+ } catch {
569
+ await runShell(`open -n "${url.replace(/"/g, "")}"`);
570
+ }
534
571
  return { success: true, data: { opened: url } };
535
572
  }
536
573
  case "sys_speak": {
@@ -1192,20 +1229,15 @@ print("\\(x),\\(y)")`;
1192
1229
  if (browser === "Safari") {
1193
1230
  await runAppleScript(`
1194
1231
  tell application "Safari"
1232
+ make new document with properties {URL:"${url.replace(/"/g, '\\"')}"}
1195
1233
  activate
1196
- if (count of windows) = 0 then make new document
1197
- set URL of front document to "${url.replace(/"/g, '\\"')}"
1198
1234
  end tell`);
1199
1235
  } else {
1200
1236
  await runAppleScript(`
1201
1237
  tell application "${browser.replace(/"/g, '\\"')}"
1238
+ make new window
1239
+ set URL of active tab of front window to "${url.replace(/"/g, '\\"')}"
1202
1240
  activate
1203
- if (count of windows) = 0 then
1204
- make new window
1205
- set URL of active tab of front window to "${url.replace(/"/g, '\\"')}"
1206
- else
1207
- set URL of active tab of front window to "${url.replace(/"/g, '\\"')}"
1208
- end if
1209
1241
  end tell`);
1210
1242
  }
1211
1243
  return { success: true, data: { navigated: url, browser } };
@@ -1224,21 +1256,22 @@ print("\\(x),\\(y)")`;
1224
1256
  if (browser === "Safari") {
1225
1257
  await runAppleScript(`
1226
1258
  tell application "Safari"
1259
+ make new document with properties {URL:"${url.replace(/"/g, '\\"')}"}
1227
1260
  activate
1228
- tell front window to set current tab to (make new tab with properties {URL:"${url.replace(/"/g, '\\"')}"})
1229
1261
  end tell`);
1230
1262
  } else {
1231
1263
  await runAppleScript(`
1232
1264
  tell application "${browser.replace(/"/g, '\\"')}"
1265
+ make new window
1266
+ set URL of active tab of front window to "${url.replace(/"/g, '\\"')}"
1233
1267
  activate
1234
- tell front window to make new tab with properties {URL:"${url.replace(/"/g, '\\"')}"}
1235
1268
  end tell`);
1236
1269
  }
1237
1270
  return { success: true, data: { opened: url, browser } };
1238
1271
  } catch (err) {
1239
1272
  return {
1240
1273
  success: false,
1241
- error: `Failed to open tab: ${err.message}`
1274
+ error: `Failed to open window: ${err.message}`
1242
1275
  };
1243
1276
  }
1244
1277
  }
@@ -2945,6 +2978,170 @@ print(result.stdout[:5000])
2945
2978
  }
2946
2979
  return { success: false, error: "Use action: list, start, stop" };
2947
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
+ }
2948
3145
  default:
2949
3146
  return { success: false, error: `Unknown command: ${command}` };
2950
3147
  }
@@ -3079,8 +3276,81 @@ async function sonosRequest(baseUrl, path) {
3079
3276
  var ws = null;
3080
3277
  var reconnectTimer = null;
3081
3278
  var heartbeatTimer = null;
3279
+ var imessageTimer = null;
3082
3280
  var HEARTBEAT_INTERVAL = 3e4;
3281
+ var IMESSAGE_POLL_INTERVAL = 3e3;
3083
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
+ }
3084
3354
  var CAPABILITY_PROBES = [
3085
3355
  {
3086
3356
  name: "calendar",
@@ -3190,6 +3460,30 @@ var CAPABILITY_PROBES = [
3190
3460
  }
3191
3461
  },
3192
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"]
3193
3487
  }
3194
3488
  ];
3195
3489
  var verifiedCapabilities = {
@@ -3298,6 +3592,8 @@ function connect() {
3298
3592
  platform: "macos",
3299
3593
  version: "0.4.0",
3300
3594
  accessLevel: ACCESS_LEVEL,
3595
+ homeDir: HOME,
3596
+ hostname: __require("os").hostname(),
3301
3597
  capabilities: cap.available,
3302
3598
  unavailable: cap.unavailable,
3303
3599
  tools: cap.tools,
@@ -3310,6 +3606,8 @@ function connect() {
3310
3606
  ws.send(JSON.stringify({ type: "ping" }));
3311
3607
  }
3312
3608
  }, HEARTBEAT_INTERVAL);
3609
+ stopImessageMonitor();
3610
+ startImessageMonitor();
3313
3611
  });
3314
3612
  ws.on("message", async (raw) => {
3315
3613
  try {
@@ -3344,6 +3642,7 @@ function connect() {
3344
3642
  const reasonStr = reason.toString() || "unknown";
3345
3643
  console.log(`
3346
3644
  \u{1F50C} Disconnected (${code}: ${reasonStr})`);
3645
+ stopImessageMonitor();
3347
3646
  if (heartbeatTimer) {
3348
3647
  clearInterval(heartbeatTimer);
3349
3648
  heartbeatTimer = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.4.0",
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": {