@pulso/companion 0.4.6 → 0.4.8

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 +756 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9899,6 +9899,428 @@ function runClaudePipeSerial(task) {
9899
9899
  }
9900
9900
  async function handleCommand(command, params, streamCb) {
9901
9901
  try {
9902
+ if (command === "llm_list_local") {
9903
+ const endpoints = [];
9904
+ try {
9905
+ const res = await fetch("http://localhost:11434/api/tags", {
9906
+ signal: AbortSignal.timeout(2e3)
9907
+ });
9908
+ if (res.ok) {
9909
+ const data = await res.json();
9910
+ endpoints.push({
9911
+ provider: "ollama",
9912
+ url: "http://localhost:11434",
9913
+ models: (data.models ?? []).map((m) => m.name),
9914
+ available: true
9915
+ });
9916
+ }
9917
+ } catch {
9918
+ }
9919
+ try {
9920
+ const res = await fetch("http://localhost:1234/v1/models", {
9921
+ signal: AbortSignal.timeout(2e3)
9922
+ });
9923
+ if (res.ok) {
9924
+ const data = await res.json();
9925
+ endpoints.push({
9926
+ provider: "lmstudio",
9927
+ url: "http://localhost:1234",
9928
+ models: (data.data ?? []).map((m) => m.id),
9929
+ available: true
9930
+ });
9931
+ }
9932
+ } catch {
9933
+ }
9934
+ try {
9935
+ const res = await fetch("http://localhost:1337/v1/models", {
9936
+ signal: AbortSignal.timeout(2e3)
9937
+ });
9938
+ if (res.ok) {
9939
+ const data = await res.json();
9940
+ endpoints.push({
9941
+ provider: "jan",
9942
+ url: "http://localhost:1337",
9943
+ models: (data.data ?? []).map((m) => m.id),
9944
+ available: true
9945
+ });
9946
+ }
9947
+ } catch {
9948
+ }
9949
+ return {
9950
+ success: true,
9951
+ data: {
9952
+ endpoints,
9953
+ total_models: endpoints.reduce((n, e) => n + e.models.length, 0),
9954
+ has_local_ai: endpoints.length > 0
9955
+ }
9956
+ };
9957
+ }
9958
+ if (command === "llm_detect_sessions") {
9959
+ const sessions = [];
9960
+ const PROVIDERS = [
9961
+ { provider: "claude", url: "claude.ai", login_indicator: "/login" },
9962
+ { provider: "chatgpt", url: "chatgpt.com", login_indicator: "/auth/login" },
9963
+ { provider: "perplexity", url: "perplexity.ai", login_indicator: "/login" },
9964
+ { provider: "gemini", url: "gemini.google.com", login_indicator: "/signin" }
9965
+ ];
9966
+ try {
9967
+ const tabsResult = await adapter.browserListTabs();
9968
+ const tabs = Array.isArray(tabsResult) ? tabsResult : tabsResult?.data ?? [];
9969
+ const tabList = tabs;
9970
+ for (const p of PROVIDERS) {
9971
+ const matchingTabs = tabList.filter((t) => t.url?.includes(p.url));
9972
+ const tabOpen = matchingTabs.length > 0;
9973
+ const loggedIn = tabOpen && !matchingTabs.some((t) => t.url?.includes(p.login_indicator));
9974
+ sessions.push({ provider: p.provider, url: p.url, logged_in: loggedIn, tab_open: tabOpen });
9975
+ }
9976
+ } catch {
9977
+ for (const p of PROVIDERS) {
9978
+ sessions.push({ provider: p.provider, url: p.url, logged_in: false, tab_open: false });
9979
+ }
9980
+ }
9981
+ const available = sessions.filter((s) => s.logged_in).map((s) => s.provider);
9982
+ return {
9983
+ success: true,
9984
+ data: {
9985
+ sessions,
9986
+ available_providers: available,
9987
+ has_browser_llm: available.length > 0
9988
+ }
9989
+ };
9990
+ }
9991
+ if (command === "llm_infer") {
9992
+ const {
9993
+ messages,
9994
+ model,
9995
+ max_tokens = 2e3,
9996
+ temperature = 0.7,
9997
+ stream = false
9998
+ } = params;
9999
+ const localEndpoints = [];
10000
+ for (const [url, provider] of [
10001
+ ["http://localhost:11434", "ollama"],
10002
+ ["http://localhost:1234", "lmstudio"],
10003
+ ["http://localhost:1337", "jan"]
10004
+ ]) {
10005
+ try {
10006
+ const tagsUrl = provider === "ollama" ? `${url}/api/tags` : `${url}/v1/models`;
10007
+ const res = await fetch(tagsUrl, { signal: AbortSignal.timeout(1500) });
10008
+ if (res.ok) {
10009
+ const data = await res.json();
10010
+ const models = data.models?.map((m) => m.name) ?? data.data?.map((m) => m.id) ?? [];
10011
+ localEndpoints.push({ url, provider, models });
10012
+ }
10013
+ } catch {
10014
+ }
10015
+ }
10016
+ if (localEndpoints.length === 0) {
10017
+ return {
10018
+ success: false,
10019
+ error: "No local AI running. Please start Ollama, LM Studio, or Jan."
10020
+ };
10021
+ }
10022
+ let chosenEndpoint = localEndpoints[0];
10023
+ let chosenModel = model;
10024
+ if (model) {
10025
+ for (const ep of localEndpoints) {
10026
+ if (ep.models.some((m) => m.includes(model) || model.includes(m))) {
10027
+ chosenEndpoint = ep;
10028
+ chosenModel = ep.models.find((m) => m.includes(model) || model.includes(m));
10029
+ break;
10030
+ }
10031
+ }
10032
+ }
10033
+ if (!chosenModel && chosenEndpoint.models.length > 0) {
10034
+ const paramPriority = ["70b", "32b", "14b", "13b", "8b", "7b", "3b", "1b"];
10035
+ let found = false;
10036
+ for (const size of paramPriority) {
10037
+ const match = chosenEndpoint.models.find(
10038
+ (m) => m.toLowerCase().includes(size)
10039
+ );
10040
+ if (match) {
10041
+ chosenModel = match;
10042
+ found = true;
10043
+ break;
10044
+ }
10045
+ }
10046
+ if (!found) chosenModel = chosenEndpoint.models[0];
10047
+ }
10048
+ const apiUrl = chosenEndpoint.provider === "ollama" ? `${chosenEndpoint.url}/v1/chat/completions` : `${chosenEndpoint.url}/v1/chat/completions`;
10049
+ try {
10050
+ const inferRes = await fetch(apiUrl, {
10051
+ method: "POST",
10052
+ headers: { "Content-Type": "application/json" },
10053
+ body: JSON.stringify({
10054
+ model: chosenModel,
10055
+ messages,
10056
+ max_tokens,
10057
+ temperature,
10058
+ stream: false
10059
+ // Companion returns full response
10060
+ }),
10061
+ signal: AbortSignal.timeout(12e4)
10062
+ // 2 min max
10063
+ });
10064
+ if (!inferRes.ok) {
10065
+ const errText = await inferRes.text().catch(() => `HTTP ${inferRes.status}`);
10066
+ return { success: false, error: `Local AI error: ${errText.slice(0, 200)}` };
10067
+ }
10068
+ const data = await inferRes.json();
10069
+ const content = data.choices?.[0]?.message?.content ?? "";
10070
+ return {
10071
+ success: true,
10072
+ data: {
10073
+ content,
10074
+ model: data.model ?? chosenModel,
10075
+ provider: chosenEndpoint.provider,
10076
+ endpoint: chosenEndpoint.url,
10077
+ cost_usd: 0,
10078
+ // Local = always free
10079
+ usage: data.usage
10080
+ }
10081
+ };
10082
+ } catch (err) {
10083
+ return {
10084
+ success: false,
10085
+ error: `Inference failed: ${err.message}`
10086
+ };
10087
+ }
10088
+ }
10089
+ if (command === "llm_browser_claude") {
10090
+ const { prompt, system } = params;
10091
+ try {
10092
+ const tabsResult = await adapter.browserListTabs();
10093
+ const tabs = Array.isArray(tabsResult) ? tabsResult : tabsResult?.data ?? [];
10094
+ const claudeTab = tabs.find(
10095
+ (t) => t.url?.includes("claude.ai")
10096
+ );
10097
+ if (!claudeTab) {
10098
+ await adapter.browserNavigate("https://claude.ai/new");
10099
+ await new Promise((r) => setTimeout(r, 3e3));
10100
+ }
10101
+ const fullPrompt = system ? `[Context: ${system}]
10102
+
10103
+ ${prompt}` : prompt;
10104
+ const submitScript = `
10105
+ (async () => {
10106
+ // Navigate to new conversation if needed
10107
+ const url = window.location.href;
10108
+ if (!url.includes('/chat/') && !url.includes('/new')) {
10109
+ window.location.href = 'https://claude.ai/new';
10110
+ return { navigating: true };
10111
+ }
10112
+
10113
+ // Find the input field
10114
+ const editor = document.querySelector('[contenteditable="true"]') ||
10115
+ document.querySelector('textarea[placeholder]') ||
10116
+ document.querySelector('.ProseMirror');
10117
+
10118
+ if (!editor) return { error: 'Input field not found' };
10119
+
10120
+ // Clear and type the prompt
10121
+ editor.focus();
10122
+ document.execCommand('selectAll', false, null);
10123
+ document.execCommand('delete', false, null);
10124
+
10125
+ // Type the prompt
10126
+ const text = ${JSON.stringify(fullPrompt)};
10127
+ document.execCommand('insertText', false, text);
10128
+
10129
+ // Trigger React state update
10130
+ editor.dispatchEvent(new InputEvent('input', { bubbles: true }));
10131
+
10132
+ // Small delay for state update
10133
+ await new Promise(r => setTimeout(r, 500));
10134
+
10135
+ // Find and click send button
10136
+ const sendBtn = document.querySelector('button[aria-label*="Send"]') ||
10137
+ document.querySelector('button[data-testid="send-button"]') ||
10138
+ [...document.querySelectorAll('button')].find(
10139
+ b => b.getAttribute('aria-label')?.toLowerCase().includes('send')
10140
+ );
10141
+
10142
+ if (sendBtn && !sendBtn.disabled) {
10143
+ sendBtn.click();
10144
+ return { submitted: true };
10145
+ }
10146
+
10147
+ // Fallback: Enter key
10148
+ editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
10149
+ return { submitted: true, method: 'enter' };
10150
+ })()
10151
+ `;
10152
+ await adapter.browserExecuteJs(submitScript);
10153
+ const pollScript = `
10154
+ (() => {
10155
+ const stopBtn = document.querySelector('button[aria-label*="Stop"]') ||
10156
+ document.querySelector('[data-testid="stop-button"]');
10157
+ const isStreaming = !!stopBtn && !stopBtn.disabled;
10158
+
10159
+ // Get the last assistant message
10160
+ const msgs = document.querySelectorAll('[data-is-streaming]');
10161
+ const lastMsg = msgs[msgs.length - 1];
10162
+ const streaming = lastMsg?.getAttribute('data-is-streaming') === 'true';
10163
+
10164
+ // Get message text
10165
+ const allMsgs = document.querySelectorAll('.font-claude-message, [data-message-author-role="assistant"]');
10166
+ const lastContent = allMsgs[allMsgs.length - 1]?.innerText ?? '';
10167
+
10168
+ return {
10169
+ isStreaming: isStreaming || streaming,
10170
+ hasContent: lastContent.length > 10,
10171
+ preview: lastContent.slice(0, 100)
10172
+ };
10173
+ })()
10174
+ `;
10175
+ let responseText = "";
10176
+ const maxWait = 6e4;
10177
+ const pollInterval = 1500;
10178
+ const startTime = Date.now();
10179
+ while (Date.now() - startTime < maxWait) {
10180
+ await new Promise((r) => setTimeout(r, pollInterval));
10181
+ const pollResult = await adapter.browserExecuteJs(pollScript);
10182
+ const status = pollResult?.data ?? pollResult;
10183
+ if (!status.isStreaming && status.hasContent) {
10184
+ const extractScript = `
10185
+ (() => {
10186
+ const msgs = document.querySelectorAll(
10187
+ '.font-claude-message, [data-message-author-role="assistant"]'
10188
+ );
10189
+ const last = msgs[msgs.length - 1];
10190
+ return last ? last.innerText : '';
10191
+ })()
10192
+ `;
10193
+ const extracted = await adapter.browserExecuteJs(extractScript);
10194
+ responseText = String(extracted?.data ?? extracted ?? "").trim();
10195
+ if (responseText.length > 20) break;
10196
+ }
10197
+ }
10198
+ if (!responseText) {
10199
+ return {
10200
+ success: false,
10201
+ error: "Timeout waiting for Claude.ai response. Is claude.ai open and logged in?"
10202
+ };
10203
+ }
10204
+ return {
10205
+ success: true,
10206
+ data: {
10207
+ content: responseText,
10208
+ provider: "claude.ai",
10209
+ model: "claude-3-5-sonnet",
10210
+ // Claude.ai uses latest model
10211
+ cost_usd: 0,
10212
+ // Uses subscription — free per-message
10213
+ note: "Response via Claude.ai browser session"
10214
+ }
10215
+ };
10216
+ } catch (err) {
10217
+ return {
10218
+ success: false,
10219
+ error: `Browser Claude bridge failed: ${err.message}`
10220
+ };
10221
+ }
10222
+ }
10223
+ if (command === "llm_browser_chatgpt") {
10224
+ const { prompt, system } = params;
10225
+ try {
10226
+ const tabsResult = await adapter.browserListTabs();
10227
+ const tabs = Array.isArray(tabsResult) ? tabsResult : tabsResult?.data ?? [];
10228
+ const gptTab = tabs.find(
10229
+ (t) => t.url?.includes("chatgpt.com") || t.url?.includes("chat.openai.com")
10230
+ );
10231
+ if (!gptTab) {
10232
+ await adapter.browserNavigate("https://chatgpt.com/");
10233
+ await new Promise((r) => setTimeout(r, 3e3));
10234
+ }
10235
+ const fullPrompt = system ? `${system}
10236
+
10237
+ ${prompt}` : prompt;
10238
+ const submitScript = `
10239
+ (async () => {
10240
+ const editor = document.getElementById('prompt-textarea') ||
10241
+ document.querySelector('textarea[placeholder]') ||
10242
+ document.querySelector('[contenteditable="true"][id*="prompt"]');
10243
+
10244
+ if (!editor) return { error: 'Input not found' };
10245
+
10246
+ editor.focus();
10247
+ const nativeInputSetter = Object.getOwnPropertyDescriptor(
10248
+ editor.tagName === 'TEXTAREA'
10249
+ ? window.HTMLTextAreaElement.prototype
10250
+ : window.HTMLDivElement.prototype,
10251
+ 'value'
10252
+ );
10253
+ if (nativeInputSetter?.set) {
10254
+ nativeInputSetter.set.call(editor, ${JSON.stringify(fullPrompt)});
10255
+ } else {
10256
+ editor.textContent = ${JSON.stringify(fullPrompt)};
10257
+ }
10258
+ editor.dispatchEvent(new Event('input', { bubbles: true }));
10259
+
10260
+ await new Promise(r => setTimeout(r, 300));
10261
+
10262
+ const sendBtn = document.querySelector('button[data-testid="send-button"]') ||
10263
+ document.querySelector('[aria-label="Send prompt"]');
10264
+ if (sendBtn) { sendBtn.click(); return { submitted: true }; }
10265
+
10266
+ editor.dispatchEvent(new KeyboardEvent('keydown', {
10267
+ key: 'Enter', keyCode: 13, bubbles: true
10268
+ }));
10269
+ return { submitted: true, method: 'enter' };
10270
+ })()
10271
+ `;
10272
+ await adapter.browserExecuteJs(submitScript);
10273
+ const maxWait = 9e4;
10274
+ const pollInterval = 2e3;
10275
+ const startTime = Date.now();
10276
+ let responseText = "";
10277
+ while (Date.now() - startTime < maxWait) {
10278
+ await new Promise((r) => setTimeout(r, pollInterval));
10279
+ const pollResult = await adapter.browserExecuteJs(`
10280
+ (() => {
10281
+ const isGenerating = !!document.querySelector(
10282
+ 'button[aria-label="Stop streaming"], [data-testid="stop-button"]'
10283
+ );
10284
+ const msgs = document.querySelectorAll(
10285
+ '[data-message-author-role="assistant"] .markdown'
10286
+ );
10287
+ const last = msgs[msgs.length - 1];
10288
+ return {
10289
+ isGenerating,
10290
+ content: last ? last.innerText : '',
10291
+ count: msgs.length
10292
+ };
10293
+ })()
10294
+ `);
10295
+ const st = pollResult?.data ?? pollResult;
10296
+ if (!st.isGenerating && String(st.content ?? "").length > 20) {
10297
+ responseText = String(st.content).trim();
10298
+ break;
10299
+ }
10300
+ }
10301
+ if (!responseText) {
10302
+ return {
10303
+ success: false,
10304
+ error: "Timeout waiting for ChatGPT response. Is chatgpt.com open and logged in?"
10305
+ };
10306
+ }
10307
+ return {
10308
+ success: true,
10309
+ data: {
10310
+ content: responseText,
10311
+ provider: "chatgpt.com",
10312
+ model: "gpt-4o",
10313
+ cost_usd: 0,
10314
+ note: "Response via ChatGPT browser session"
10315
+ }
10316
+ };
10317
+ } catch (err) {
10318
+ return {
10319
+ success: false,
10320
+ error: `Browser ChatGPT bridge failed: ${err.message}`
10321
+ };
10322
+ }
10323
+ }
9902
10324
  if (command === "ollama_detect") {
9903
10325
  try {
9904
10326
  const controller = new AbortController();
@@ -10811,12 +11233,28 @@ print(result.stdout[:5000])
10811
11233
  );
10812
11234
  }
10813
11235
  case "sys_claude_status": {
11236
+ const _home = process.env.HOME || "";
11237
+ const _claudePaths = [
11238
+ `${_home}/.local/bin/claude`,
11239
+ `${_home}/.local/share/claude/versions/latest/claude`,
11240
+ "/usr/local/bin/claude",
11241
+ "/opt/homebrew/bin/claude"
11242
+ ];
11243
+ let _claudeBin = "claude";
11244
+ for (const _p of _claudePaths) {
11245
+ try {
11246
+ execSync3(`test -x "${_p}"`, { stdio: "ignore" });
11247
+ _claudeBin = _p;
11248
+ break;
11249
+ } catch {
11250
+ }
11251
+ }
10814
11252
  try {
10815
- const version = await runShell4("claude --version 2>/dev/null", 5e3);
11253
+ const version = await runShell4(`"${_claudeBin}" --version 2>/dev/null`, 5e3);
10816
11254
  let authStatus = "unknown";
10817
11255
  let authDetails;
10818
11256
  try {
10819
- const status = await runShell4("claude auth status 2>&1", 1e4);
11257
+ const status = await runShell4(`"${_claudeBin}" auth status 2>&1`, 1e4);
10820
11258
  const trimmed = status.trim();
10821
11259
  let parsed = null;
10822
11260
  try {
@@ -10965,6 +11403,315 @@ print(result.stdout[:5000])
10965
11403
  return { success: false, error: err.message };
10966
11404
  }
10967
11405
  }
11406
+ // ── LLM Browser Sessions ───────────────────────────────
11407
+ case "sys_llm_list_sessions": {
11408
+ const LLM_PROVIDERS = [
11409
+ { domain: "claude.ai", name: "Claude" },
11410
+ { domain: "chatgpt.com", name: "ChatGPT" },
11411
+ { domain: "chat.openai.com", name: "ChatGPT" },
11412
+ { domain: "gemini.google.com", name: "Gemini" },
11413
+ { domain: "perplexity.ai", name: "Perplexity" },
11414
+ { domain: "copilot.microsoft.com", name: "Copilot" },
11415
+ { domain: "grok.com", name: "Grok" },
11416
+ { domain: "aistudio.google.com", name: "AI Studio" },
11417
+ { domain: "poe.com", name: "Poe" },
11418
+ { domain: "mistral.ai/chat", name: "Mistral" },
11419
+ { domain: "deepseek.com/chat", name: "DeepSeek" }
11420
+ ];
11421
+ const allTabs = await adapter.browserListTabs();
11422
+ const sessions = allTabs.filter((tab) => {
11423
+ const url = tab.url.toLowerCase();
11424
+ return LLM_PROVIDERS.some((p) => url.includes(p.domain));
11425
+ }).map((tab) => {
11426
+ const provider = LLM_PROVIDERS.find(
11427
+ (p) => tab.url.toLowerCase().includes(p.domain)
11428
+ );
11429
+ return {
11430
+ provider: provider?.name ?? "Unknown",
11431
+ browser: tab.browser,
11432
+ title: tab.title,
11433
+ url: tab.url,
11434
+ active: tab.active ?? false
11435
+ };
11436
+ });
11437
+ return {
11438
+ success: true,
11439
+ data: {
11440
+ sessions,
11441
+ count: sessions.length,
11442
+ note: sessions.length === 0 ? "No LLM tabs open. Open Claude, ChatGPT, Gemini, or Perplexity in your browser." : void 0
11443
+ }
11444
+ };
11445
+ }
11446
+ case "sys_llm_read_session": {
11447
+ const targetUrl = params.url;
11448
+ const targetIndex = params.index !== void 0 ? Number(params.index) : void 0;
11449
+ const LLM_PROVIDERS = [
11450
+ { domain: "claude.ai", name: "Claude" },
11451
+ { domain: "chatgpt.com", name: "ChatGPT" },
11452
+ { domain: "chat.openai.com", name: "ChatGPT" },
11453
+ { domain: "gemini.google.com", name: "Gemini" },
11454
+ { domain: "perplexity.ai", name: "Perplexity" },
11455
+ { domain: "copilot.microsoft.com", name: "Copilot" },
11456
+ { domain: "grok.com", name: "Grok" },
11457
+ { domain: "aistudio.google.com", name: "AI Studio" },
11458
+ { domain: "poe.com", name: "Poe" },
11459
+ { domain: "mistral.ai/chat", name: "Mistral" },
11460
+ { domain: "deepseek.com/chat", name: "DeepSeek" }
11461
+ ];
11462
+ const allTabs = await adapter.browserListTabs();
11463
+ const llmTabs = allTabs.filter(
11464
+ (tab) => LLM_PROVIDERS.some((p) => tab.url.toLowerCase().includes(p.domain))
11465
+ );
11466
+ if (llmTabs.length === 0) {
11467
+ return { success: false, error: "No LLM tabs found. Open Claude, ChatGPT, or Gemini in your browser." };
11468
+ }
11469
+ let targetTab = llmTabs[0];
11470
+ if (targetUrl) {
11471
+ const found = llmTabs.find((t) => t.url.toLowerCase().includes(targetUrl.toLowerCase()));
11472
+ if (found) targetTab = found;
11473
+ } else if (targetIndex !== void 0 && llmTabs[targetIndex]) {
11474
+ targetTab = llmTabs[targetIndex];
11475
+ } else {
11476
+ const active = llmTabs.find((t) => t.active);
11477
+ const claude = llmTabs.find((t) => t.url.includes("claude.ai"));
11478
+ targetTab = active ?? claude ?? llmTabs[0];
11479
+ }
11480
+ const provider = LLM_PROVIDERS.find(
11481
+ (p) => targetTab.url.toLowerCase().includes(p.domain)
11482
+ );
11483
+ const JS_EXTRACTORS = {
11484
+ Claude: `(function(){
11485
+ const msgs=[];
11486
+ const turns=document.querySelectorAll('[data-testid="human-turn"],[data-testid="ai-turn"]');
11487
+ turns.forEach(el=>{
11488
+ const role=el.getAttribute('data-testid')==='human-turn'?'user':'assistant';
11489
+ const text=(el.innerText||'').trim().slice(0,600);
11490
+ if(text) msgs.push({role,text});
11491
+ });
11492
+ if(!msgs.length){
11493
+ document.querySelectorAll('.font-claude-message,.prose-claude').forEach(el=>{
11494
+ const text=(el.innerText||'').trim().slice(0,600);
11495
+ if(text) msgs.push({role:'assistant',text});
11496
+ });
11497
+ }
11498
+ return JSON.stringify({provider:'Claude',messages:msgs.slice(-8),url:location.href,title:document.title});
11499
+ })()`,
11500
+ ChatGPT: `(function(){
11501
+ const msgs=[];
11502
+ document.querySelectorAll('[data-message-author-role]').forEach(el=>{
11503
+ const role=el.getAttribute('data-message-author-role')||'unknown';
11504
+ const content=el.querySelector('.markdown,.text-message,[data-message-content]');
11505
+ const text=((content||el).innerText||'').trim().slice(0,600);
11506
+ if(text) msgs.push({role,text});
11507
+ });
11508
+ return JSON.stringify({provider:'ChatGPT',messages:msgs.slice(-8),url:location.href,title:document.title});
11509
+ })()`,
11510
+ Gemini: `(function(){
11511
+ const msgs=[];
11512
+ document.querySelectorAll('user-query,model-response').forEach(el=>{
11513
+ const role=el.tagName.toLowerCase().includes('user')?'user':'assistant';
11514
+ const text=(el.innerText||'').trim().slice(0,600);
11515
+ if(text) msgs.push({role,text});
11516
+ });
11517
+ if(!msgs.length){
11518
+ document.querySelectorAll('[class*="user-query"],[class*="model-response"]').forEach(el=>{
11519
+ const isUser=el.className.toLowerCase().includes('user');
11520
+ const text=(el.innerText||'').trim().slice(0,600);
11521
+ if(text) msgs.push({role:isUser?'user':'assistant',text});
11522
+ });
11523
+ }
11524
+ return JSON.stringify({provider:'Gemini',messages:msgs.slice(-8),url:location.href,title:document.title});
11525
+ })()`,
11526
+ Perplexity: `(function(){
11527
+ const msgs=[];
11528
+ document.querySelectorAll('[class*="prose"],[class*="AnswerBody"],[class*="UserMessage"]').forEach(el=>{
11529
+ if(el.children.length>2){
11530
+ const text=(el.innerText||'').trim().slice(0,600);
11531
+ if(text.length>30) msgs.push({role:'assistant',text});
11532
+ }
11533
+ });
11534
+ return JSON.stringify({provider:'Perplexity',messages:msgs.slice(-4),url:location.href,title:document.title});
11535
+ })()`
11536
+ };
11537
+ const extractorKey = provider?.name ?? "Claude";
11538
+ const jsCode = JS_EXTRACTORS[extractorKey] ?? JS_EXTRACTORS.Claude;
11539
+ try {
11540
+ const browserName = targetTab.browser;
11541
+ if (browserName !== "Safari") {
11542
+ const switchScript = `
11543
+ tell application "${browserName.replace(/"/g, '\\"')}"
11544
+ repeat with w in windows
11545
+ set tabIdx to 1
11546
+ repeat with t in tabs of w
11547
+ if URL of t contains "${targetTab.url.slice(0, 80).replace(/"/g, '\\"')}" then
11548
+ set active tab index of w to tabIdx
11549
+ activate
11550
+ return "switched"
11551
+ end if
11552
+ set tabIdx to tabIdx + 1
11553
+ end repeat
11554
+ end repeat
11555
+ end tell`;
11556
+ await runAppleScript2(switchScript).catch(() => {
11557
+ });
11558
+ }
11559
+ const raw = await adapter.browserExecuteJs(jsCode, browserName);
11560
+ if (!raw.success) {
11561
+ return { success: false, error: `Could not read ${extractorKey} session: ${raw.error}` };
11562
+ }
11563
+ let parsed;
11564
+ try {
11565
+ parsed = JSON.parse(raw.data);
11566
+ } catch {
11567
+ return { success: false, error: "Failed to parse conversation content from browser." };
11568
+ }
11569
+ return {
11570
+ success: true,
11571
+ data: {
11572
+ provider: parsed.provider,
11573
+ browser: browserName,
11574
+ title: parsed.title ?? targetTab.title,
11575
+ url: parsed.url ?? targetTab.url,
11576
+ messages: parsed.messages,
11577
+ messageCount: parsed.messages.length
11578
+ }
11579
+ };
11580
+ } catch (err) {
11581
+ return { success: false, error: `Failed to read session: ${err.message}` };
11582
+ }
11583
+ }
11584
+ case "sys_llm_send_message": {
11585
+ const message = params.message;
11586
+ if (!message) return { success: false, error: "Missing message" };
11587
+ const targetUrl = params.url;
11588
+ const targetIndex = params.index !== void 0 ? Number(params.index) : void 0;
11589
+ const LLM_PROVIDERS = [
11590
+ { domain: "claude.ai", name: "Claude" },
11591
+ { domain: "chatgpt.com", name: "ChatGPT" },
11592
+ { domain: "chat.openai.com", name: "ChatGPT" },
11593
+ { domain: "gemini.google.com", name: "Gemini" },
11594
+ { domain: "perplexity.ai", name: "Perplexity" }
11595
+ ];
11596
+ const allTabs = await adapter.browserListTabs();
11597
+ const llmTabs = allTabs.filter(
11598
+ (tab) => LLM_PROVIDERS.some((p) => tab.url.toLowerCase().includes(p.domain))
11599
+ );
11600
+ if (llmTabs.length === 0) {
11601
+ return { success: false, error: "No LLM tabs found. Open Claude or ChatGPT in your browser first." };
11602
+ }
11603
+ let targetTab = llmTabs[0];
11604
+ if (targetUrl) {
11605
+ const found = llmTabs.find((t) => t.url.toLowerCase().includes(targetUrl.toLowerCase()));
11606
+ if (found) targetTab = found;
11607
+ } else if (targetIndex !== void 0 && llmTabs[targetIndex]) {
11608
+ targetTab = llmTabs[targetIndex];
11609
+ } else {
11610
+ const active = llmTabs.find((t) => t.active);
11611
+ const claude = llmTabs.find((t) => t.url.includes("claude.ai"));
11612
+ targetTab = active ?? claude ?? llmTabs[0];
11613
+ }
11614
+ const provider = LLM_PROVIDERS.find(
11615
+ (p) => targetTab.url.toLowerCase().includes(p.domain)
11616
+ );
11617
+ const safe = message.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
11618
+ const JS_SENDERS = {
11619
+ Claude: `(function(){
11620
+ const msg=\`${safe}\`;
11621
+ const input=document.querySelector('[contenteditable="true"][data-placeholder]')||
11622
+ document.querySelector('div[contenteditable="true"]');
11623
+ if(!input) return JSON.stringify({success:false,error:'Input field not found'});
11624
+ input.focus();
11625
+ input.innerText=msg;
11626
+ input.dispatchEvent(new InputEvent('input',{bubbles:true,inputType:'insertText',data:msg}));
11627
+ setTimeout(()=>{
11628
+ const btn=document.querySelector('button[aria-label*="Send"]')||
11629
+ document.querySelector('button[data-testid*="send"]')||
11630
+ document.querySelector('button[aria-label*="send"]');
11631
+ if(btn&&!btn.disabled){btn.click();return JSON.stringify({success:true,sent:msg});}
11632
+ },300);
11633
+ return JSON.stringify({success:true,sent:msg,note:'Typed message, attempting submit'});
11634
+ })()`,
11635
+ ChatGPT: `(function(){
11636
+ const msg=\`${safe}\`;
11637
+ const input=document.querySelector('#prompt-textarea')||
11638
+ document.querySelector('[contenteditable="true"]')||
11639
+ document.querySelector('textarea');
11640
+ if(!input) return JSON.stringify({success:false,error:'Input field not found'});
11641
+ input.focus();
11642
+ if(input.tagName==='TEXTAREA'){
11643
+ const setter=Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value').set;
11644
+ setter.call(input,msg);
11645
+ } else {
11646
+ input.innerText=msg;
11647
+ }
11648
+ input.dispatchEvent(new Event('input',{bubbles:true}));
11649
+ setTimeout(()=>{
11650
+ const btn=document.querySelector('[data-testid="send-button"]')||
11651
+ document.querySelector('button[aria-label*="Send"]');
11652
+ if(btn&&!btn.disabled) btn.click();
11653
+ },300);
11654
+ return JSON.stringify({success:true,sent:msg});
11655
+ })()`,
11656
+ Gemini: `(function(){
11657
+ const msg=\`${safe}\`;
11658
+ const input=document.querySelector('.ql-editor')||
11659
+ document.querySelector('[contenteditable="true"]')||
11660
+ document.querySelector('textarea');
11661
+ if(!input) return JSON.stringify({success:false,error:'Input field not found'});
11662
+ input.focus();
11663
+ input.innerText=msg;
11664
+ input.dispatchEvent(new InputEvent('input',{bubbles:true}));
11665
+ setTimeout(()=>{
11666
+ const btn=document.querySelector('button[aria-label*="Send"]')||
11667
+ document.querySelector('button.send-button');
11668
+ if(btn&&!btn.disabled) btn.click();
11669
+ },300);
11670
+ return JSON.stringify({success:true,sent:msg});
11671
+ })()`
11672
+ };
11673
+ const senderKey = provider?.name ?? "Claude";
11674
+ const jsCode = JS_SENDERS[senderKey] ?? JS_SENDERS.Claude;
11675
+ try {
11676
+ const browserName = targetTab.browser;
11677
+ if (browserName !== "Safari") {
11678
+ const switchScript = `
11679
+ tell application "${browserName.replace(/"/g, '\\"')}"
11680
+ repeat with w in windows
11681
+ set tabIdx to 1
11682
+ repeat with t in tabs of w
11683
+ if URL of t contains "${targetTab.url.slice(0, 80).replace(/"/g, '\\"')}" then
11684
+ set active tab index of w to tabIdx
11685
+ activate
11686
+ return "switched"
11687
+ end if
11688
+ set tabIdx to tabIdx + 1
11689
+ end repeat
11690
+ end repeat
11691
+ end tell`;
11692
+ await runAppleScript2(switchScript).catch(() => {
11693
+ });
11694
+ await new Promise((r) => setTimeout(r, 500));
11695
+ }
11696
+ const result = await adapter.browserExecuteJs(jsCode, browserName);
11697
+ if (!result.success) {
11698
+ return { success: false, error: `Could not send to ${senderKey}: ${result.error}` };
11699
+ }
11700
+ return {
11701
+ success: true,
11702
+ data: {
11703
+ provider: senderKey,
11704
+ browser: browserName,
11705
+ tab: targetTab.title,
11706
+ url: targetTab.url,
11707
+ message,
11708
+ note: "Message typed and submitted. Check browser to confirm."
11709
+ }
11710
+ };
11711
+ } catch (err) {
11712
+ return { success: false, error: `Failed to send message: ${err.message}` };
11713
+ }
11714
+ }
10968
11715
  // ── IDE Integration ────────────────────────────────────
10969
11716
  // Helper: extract open workspace paths from a VS Code/Cursor/Windsurf storage.json
10970
11717
  // Storage format: windowsState.lastActiveWindow / openedWindows
@@ -11593,6 +12340,13 @@ async function probeCapabilities() {
11593
12340
  }
11594
12341
  }
11595
12342
  }
12343
+ tools.add("llm_infer");
12344
+ tools.add("llm_list_local");
12345
+ tools.add("llm_detect_sessions");
12346
+ if (adapter.platform === "macos" || adapter.platform === "windows") {
12347
+ tools.add("llm_browser_claude");
12348
+ tools.add("llm_browser_chatgpt");
12349
+ }
11596
12350
  const cap = { available, unavailable, tools: Array.from(tools) };
11597
12351
  console.log(
11598
12352
  ` \u2705 Available: ${available.join(", ") || "all adapter tools"}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {