@pulso/companion 0.4.6 → 0.4.7

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 +704 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9899,6 +9899,395 @@ 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_infer") {
9959
+ const {
9960
+ messages,
9961
+ model,
9962
+ max_tokens = 2e3,
9963
+ temperature = 0.7,
9964
+ stream = false
9965
+ } = params;
9966
+ const localEndpoints = [];
9967
+ for (const [url, provider] of [
9968
+ ["http://localhost:11434", "ollama"],
9969
+ ["http://localhost:1234", "lmstudio"],
9970
+ ["http://localhost:1337", "jan"]
9971
+ ]) {
9972
+ try {
9973
+ const tagsUrl = provider === "ollama" ? `${url}/api/tags` : `${url}/v1/models`;
9974
+ const res = await fetch(tagsUrl, { signal: AbortSignal.timeout(1500) });
9975
+ if (res.ok) {
9976
+ const data = await res.json();
9977
+ const models = data.models?.map((m) => m.name) ?? data.data?.map((m) => m.id) ?? [];
9978
+ localEndpoints.push({ url, provider, models });
9979
+ }
9980
+ } catch {
9981
+ }
9982
+ }
9983
+ if (localEndpoints.length === 0) {
9984
+ return {
9985
+ success: false,
9986
+ error: "No local AI running. Please start Ollama, LM Studio, or Jan."
9987
+ };
9988
+ }
9989
+ let chosenEndpoint = localEndpoints[0];
9990
+ let chosenModel = model;
9991
+ if (model) {
9992
+ for (const ep of localEndpoints) {
9993
+ if (ep.models.some((m) => m.includes(model) || model.includes(m))) {
9994
+ chosenEndpoint = ep;
9995
+ chosenModel = ep.models.find((m) => m.includes(model) || model.includes(m));
9996
+ break;
9997
+ }
9998
+ }
9999
+ }
10000
+ if (!chosenModel && chosenEndpoint.models.length > 0) {
10001
+ const paramPriority = ["70b", "32b", "14b", "13b", "8b", "7b", "3b", "1b"];
10002
+ let found = false;
10003
+ for (const size of paramPriority) {
10004
+ const match = chosenEndpoint.models.find(
10005
+ (m) => m.toLowerCase().includes(size)
10006
+ );
10007
+ if (match) {
10008
+ chosenModel = match;
10009
+ found = true;
10010
+ break;
10011
+ }
10012
+ }
10013
+ if (!found) chosenModel = chosenEndpoint.models[0];
10014
+ }
10015
+ const apiUrl = chosenEndpoint.provider === "ollama" ? `${chosenEndpoint.url}/v1/chat/completions` : `${chosenEndpoint.url}/v1/chat/completions`;
10016
+ try {
10017
+ const inferRes = await fetch(apiUrl, {
10018
+ method: "POST",
10019
+ headers: { "Content-Type": "application/json" },
10020
+ body: JSON.stringify({
10021
+ model: chosenModel,
10022
+ messages,
10023
+ max_tokens,
10024
+ temperature,
10025
+ stream: false
10026
+ // Companion returns full response
10027
+ }),
10028
+ signal: AbortSignal.timeout(12e4)
10029
+ // 2 min max
10030
+ });
10031
+ if (!inferRes.ok) {
10032
+ const errText = await inferRes.text().catch(() => `HTTP ${inferRes.status}`);
10033
+ return { success: false, error: `Local AI error: ${errText.slice(0, 200)}` };
10034
+ }
10035
+ const data = await inferRes.json();
10036
+ const content = data.choices?.[0]?.message?.content ?? "";
10037
+ return {
10038
+ success: true,
10039
+ data: {
10040
+ content,
10041
+ model: data.model ?? chosenModel,
10042
+ provider: chosenEndpoint.provider,
10043
+ endpoint: chosenEndpoint.url,
10044
+ cost_usd: 0,
10045
+ // Local = always free
10046
+ usage: data.usage
10047
+ }
10048
+ };
10049
+ } catch (err) {
10050
+ return {
10051
+ success: false,
10052
+ error: `Inference failed: ${err.message}`
10053
+ };
10054
+ }
10055
+ }
10056
+ if (command === "llm_browser_claude") {
10057
+ const { prompt, system } = params;
10058
+ try {
10059
+ const tabsResult = await adapter.browserListTabs();
10060
+ const tabs = Array.isArray(tabsResult) ? tabsResult : tabsResult?.data ?? [];
10061
+ const claudeTab = tabs.find(
10062
+ (t) => t.url?.includes("claude.ai")
10063
+ );
10064
+ if (!claudeTab) {
10065
+ await adapter.browserNavigate("https://claude.ai/new");
10066
+ await new Promise((r) => setTimeout(r, 3e3));
10067
+ }
10068
+ const fullPrompt = system ? `[Context: ${system}]
10069
+
10070
+ ${prompt}` : prompt;
10071
+ const submitScript = `
10072
+ (async () => {
10073
+ // Navigate to new conversation if needed
10074
+ const url = window.location.href;
10075
+ if (!url.includes('/chat/') && !url.includes('/new')) {
10076
+ window.location.href = 'https://claude.ai/new';
10077
+ return { navigating: true };
10078
+ }
10079
+
10080
+ // Find the input field
10081
+ const editor = document.querySelector('[contenteditable="true"]') ||
10082
+ document.querySelector('textarea[placeholder]') ||
10083
+ document.querySelector('.ProseMirror');
10084
+
10085
+ if (!editor) return { error: 'Input field not found' };
10086
+
10087
+ // Clear and type the prompt
10088
+ editor.focus();
10089
+ document.execCommand('selectAll', false, null);
10090
+ document.execCommand('delete', false, null);
10091
+
10092
+ // Type the prompt
10093
+ const text = ${JSON.stringify(fullPrompt)};
10094
+ document.execCommand('insertText', false, text);
10095
+
10096
+ // Trigger React state update
10097
+ editor.dispatchEvent(new InputEvent('input', { bubbles: true }));
10098
+
10099
+ // Small delay for state update
10100
+ await new Promise(r => setTimeout(r, 500));
10101
+
10102
+ // Find and click send button
10103
+ const sendBtn = document.querySelector('button[aria-label*="Send"]') ||
10104
+ document.querySelector('button[data-testid="send-button"]') ||
10105
+ [...document.querySelectorAll('button')].find(
10106
+ b => b.getAttribute('aria-label')?.toLowerCase().includes('send')
10107
+ );
10108
+
10109
+ if (sendBtn && !sendBtn.disabled) {
10110
+ sendBtn.click();
10111
+ return { submitted: true };
10112
+ }
10113
+
10114
+ // Fallback: Enter key
10115
+ editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
10116
+ return { submitted: true, method: 'enter' };
10117
+ })()
10118
+ `;
10119
+ await adapter.browserExecuteJs(submitScript);
10120
+ const pollScript = `
10121
+ (() => {
10122
+ const stopBtn = document.querySelector('button[aria-label*="Stop"]') ||
10123
+ document.querySelector('[data-testid="stop-button"]');
10124
+ const isStreaming = !!stopBtn && !stopBtn.disabled;
10125
+
10126
+ // Get the last assistant message
10127
+ const msgs = document.querySelectorAll('[data-is-streaming]');
10128
+ const lastMsg = msgs[msgs.length - 1];
10129
+ const streaming = lastMsg?.getAttribute('data-is-streaming') === 'true';
10130
+
10131
+ // Get message text
10132
+ const allMsgs = document.querySelectorAll('.font-claude-message, [data-message-author-role="assistant"]');
10133
+ const lastContent = allMsgs[allMsgs.length - 1]?.innerText ?? '';
10134
+
10135
+ return {
10136
+ isStreaming: isStreaming || streaming,
10137
+ hasContent: lastContent.length > 10,
10138
+ preview: lastContent.slice(0, 100)
10139
+ };
10140
+ })()
10141
+ `;
10142
+ let responseText = "";
10143
+ const maxWait = 6e4;
10144
+ const pollInterval = 1500;
10145
+ const startTime = Date.now();
10146
+ while (Date.now() - startTime < maxWait) {
10147
+ await new Promise((r) => setTimeout(r, pollInterval));
10148
+ const pollResult = await adapter.browserExecuteJs(pollScript);
10149
+ const status = pollResult?.data ?? pollResult;
10150
+ if (!status.isStreaming && status.hasContent) {
10151
+ const extractScript = `
10152
+ (() => {
10153
+ const msgs = document.querySelectorAll(
10154
+ '.font-claude-message, [data-message-author-role="assistant"]'
10155
+ );
10156
+ const last = msgs[msgs.length - 1];
10157
+ return last ? last.innerText : '';
10158
+ })()
10159
+ `;
10160
+ const extracted = await adapter.browserExecuteJs(extractScript);
10161
+ responseText = String(extracted?.data ?? extracted ?? "").trim();
10162
+ if (responseText.length > 20) break;
10163
+ }
10164
+ }
10165
+ if (!responseText) {
10166
+ return {
10167
+ success: false,
10168
+ error: "Timeout waiting for Claude.ai response. Is claude.ai open and logged in?"
10169
+ };
10170
+ }
10171
+ return {
10172
+ success: true,
10173
+ data: {
10174
+ content: responseText,
10175
+ provider: "claude.ai",
10176
+ model: "claude-3-5-sonnet",
10177
+ // Claude.ai uses latest model
10178
+ cost_usd: 0,
10179
+ // Uses subscription — free per-message
10180
+ note: "Response via Claude.ai browser session"
10181
+ }
10182
+ };
10183
+ } catch (err) {
10184
+ return {
10185
+ success: false,
10186
+ error: `Browser Claude bridge failed: ${err.message}`
10187
+ };
10188
+ }
10189
+ }
10190
+ if (command === "llm_browser_chatgpt") {
10191
+ const { prompt, system } = params;
10192
+ try {
10193
+ const tabsResult = await adapter.browserListTabs();
10194
+ const tabs = Array.isArray(tabsResult) ? tabsResult : tabsResult?.data ?? [];
10195
+ const gptTab = tabs.find(
10196
+ (t) => t.url?.includes("chatgpt.com") || t.url?.includes("chat.openai.com")
10197
+ );
10198
+ if (!gptTab) {
10199
+ await adapter.browserNavigate("https://chatgpt.com/");
10200
+ await new Promise((r) => setTimeout(r, 3e3));
10201
+ }
10202
+ const fullPrompt = system ? `${system}
10203
+
10204
+ ${prompt}` : prompt;
10205
+ const submitScript = `
10206
+ (async () => {
10207
+ const editor = document.getElementById('prompt-textarea') ||
10208
+ document.querySelector('textarea[placeholder]') ||
10209
+ document.querySelector('[contenteditable="true"][id*="prompt"]');
10210
+
10211
+ if (!editor) return { error: 'Input not found' };
10212
+
10213
+ editor.focus();
10214
+ const nativeInputSetter = Object.getOwnPropertyDescriptor(
10215
+ editor.tagName === 'TEXTAREA'
10216
+ ? window.HTMLTextAreaElement.prototype
10217
+ : window.HTMLDivElement.prototype,
10218
+ 'value'
10219
+ );
10220
+ if (nativeInputSetter?.set) {
10221
+ nativeInputSetter.set.call(editor, ${JSON.stringify(fullPrompt)});
10222
+ } else {
10223
+ editor.textContent = ${JSON.stringify(fullPrompt)};
10224
+ }
10225
+ editor.dispatchEvent(new Event('input', { bubbles: true }));
10226
+
10227
+ await new Promise(r => setTimeout(r, 300));
10228
+
10229
+ const sendBtn = document.querySelector('button[data-testid="send-button"]') ||
10230
+ document.querySelector('[aria-label="Send prompt"]');
10231
+ if (sendBtn) { sendBtn.click(); return { submitted: true }; }
10232
+
10233
+ editor.dispatchEvent(new KeyboardEvent('keydown', {
10234
+ key: 'Enter', keyCode: 13, bubbles: true
10235
+ }));
10236
+ return { submitted: true, method: 'enter' };
10237
+ })()
10238
+ `;
10239
+ await adapter.browserExecuteJs(submitScript);
10240
+ const maxWait = 9e4;
10241
+ const pollInterval = 2e3;
10242
+ const startTime = Date.now();
10243
+ let responseText = "";
10244
+ while (Date.now() - startTime < maxWait) {
10245
+ await new Promise((r) => setTimeout(r, pollInterval));
10246
+ const pollResult = await adapter.browserExecuteJs(`
10247
+ (() => {
10248
+ const isGenerating = !!document.querySelector(
10249
+ 'button[aria-label="Stop streaming"], [data-testid="stop-button"]'
10250
+ );
10251
+ const msgs = document.querySelectorAll(
10252
+ '[data-message-author-role="assistant"] .markdown'
10253
+ );
10254
+ const last = msgs[msgs.length - 1];
10255
+ return {
10256
+ isGenerating,
10257
+ content: last ? last.innerText : '',
10258
+ count: msgs.length
10259
+ };
10260
+ })()
10261
+ `);
10262
+ const st = pollResult?.data ?? pollResult;
10263
+ if (!st.isGenerating && String(st.content ?? "").length > 20) {
10264
+ responseText = String(st.content).trim();
10265
+ break;
10266
+ }
10267
+ }
10268
+ if (!responseText) {
10269
+ return {
10270
+ success: false,
10271
+ error: "Timeout waiting for ChatGPT response. Is chatgpt.com open and logged in?"
10272
+ };
10273
+ }
10274
+ return {
10275
+ success: true,
10276
+ data: {
10277
+ content: responseText,
10278
+ provider: "chatgpt.com",
10279
+ model: "gpt-4o",
10280
+ cost_usd: 0,
10281
+ note: "Response via ChatGPT browser session"
10282
+ }
10283
+ };
10284
+ } catch (err) {
10285
+ return {
10286
+ success: false,
10287
+ error: `Browser ChatGPT bridge failed: ${err.message}`
10288
+ };
10289
+ }
10290
+ }
9902
10291
  if (command === "ollama_detect") {
9903
10292
  try {
9904
10293
  const controller = new AbortController();
@@ -10965,6 +11354,315 @@ print(result.stdout[:5000])
10965
11354
  return { success: false, error: err.message };
10966
11355
  }
10967
11356
  }
11357
+ // ── LLM Browser Sessions ───────────────────────────────
11358
+ case "sys_llm_list_sessions": {
11359
+ const LLM_PROVIDERS = [
11360
+ { domain: "claude.ai", name: "Claude" },
11361
+ { domain: "chatgpt.com", name: "ChatGPT" },
11362
+ { domain: "chat.openai.com", name: "ChatGPT" },
11363
+ { domain: "gemini.google.com", name: "Gemini" },
11364
+ { domain: "perplexity.ai", name: "Perplexity" },
11365
+ { domain: "copilot.microsoft.com", name: "Copilot" },
11366
+ { domain: "grok.com", name: "Grok" },
11367
+ { domain: "aistudio.google.com", name: "AI Studio" },
11368
+ { domain: "poe.com", name: "Poe" },
11369
+ { domain: "mistral.ai/chat", name: "Mistral" },
11370
+ { domain: "deepseek.com/chat", name: "DeepSeek" }
11371
+ ];
11372
+ const allTabs = await adapter.browserListTabs();
11373
+ const sessions = allTabs.filter((tab) => {
11374
+ const url = tab.url.toLowerCase();
11375
+ return LLM_PROVIDERS.some((p) => url.includes(p.domain));
11376
+ }).map((tab) => {
11377
+ const provider = LLM_PROVIDERS.find(
11378
+ (p) => tab.url.toLowerCase().includes(p.domain)
11379
+ );
11380
+ return {
11381
+ provider: provider?.name ?? "Unknown",
11382
+ browser: tab.browser,
11383
+ title: tab.title,
11384
+ url: tab.url,
11385
+ active: tab.active ?? false
11386
+ };
11387
+ });
11388
+ return {
11389
+ success: true,
11390
+ data: {
11391
+ sessions,
11392
+ count: sessions.length,
11393
+ note: sessions.length === 0 ? "No LLM tabs open. Open Claude, ChatGPT, Gemini, or Perplexity in your browser." : void 0
11394
+ }
11395
+ };
11396
+ }
11397
+ case "sys_llm_read_session": {
11398
+ const targetUrl = params.url;
11399
+ const targetIndex = params.index !== void 0 ? Number(params.index) : void 0;
11400
+ const LLM_PROVIDERS = [
11401
+ { domain: "claude.ai", name: "Claude" },
11402
+ { domain: "chatgpt.com", name: "ChatGPT" },
11403
+ { domain: "chat.openai.com", name: "ChatGPT" },
11404
+ { domain: "gemini.google.com", name: "Gemini" },
11405
+ { domain: "perplexity.ai", name: "Perplexity" },
11406
+ { domain: "copilot.microsoft.com", name: "Copilot" },
11407
+ { domain: "grok.com", name: "Grok" },
11408
+ { domain: "aistudio.google.com", name: "AI Studio" },
11409
+ { domain: "poe.com", name: "Poe" },
11410
+ { domain: "mistral.ai/chat", name: "Mistral" },
11411
+ { domain: "deepseek.com/chat", name: "DeepSeek" }
11412
+ ];
11413
+ const allTabs = await adapter.browserListTabs();
11414
+ const llmTabs = allTabs.filter(
11415
+ (tab) => LLM_PROVIDERS.some((p) => tab.url.toLowerCase().includes(p.domain))
11416
+ );
11417
+ if (llmTabs.length === 0) {
11418
+ return { success: false, error: "No LLM tabs found. Open Claude, ChatGPT, or Gemini in your browser." };
11419
+ }
11420
+ let targetTab = llmTabs[0];
11421
+ if (targetUrl) {
11422
+ const found = llmTabs.find((t) => t.url.toLowerCase().includes(targetUrl.toLowerCase()));
11423
+ if (found) targetTab = found;
11424
+ } else if (targetIndex !== void 0 && llmTabs[targetIndex]) {
11425
+ targetTab = llmTabs[targetIndex];
11426
+ } else {
11427
+ const active = llmTabs.find((t) => t.active);
11428
+ const claude = llmTabs.find((t) => t.url.includes("claude.ai"));
11429
+ targetTab = active ?? claude ?? llmTabs[0];
11430
+ }
11431
+ const provider = LLM_PROVIDERS.find(
11432
+ (p) => targetTab.url.toLowerCase().includes(p.domain)
11433
+ );
11434
+ const JS_EXTRACTORS = {
11435
+ Claude: `(function(){
11436
+ const msgs=[];
11437
+ const turns=document.querySelectorAll('[data-testid="human-turn"],[data-testid="ai-turn"]');
11438
+ turns.forEach(el=>{
11439
+ const role=el.getAttribute('data-testid')==='human-turn'?'user':'assistant';
11440
+ const text=(el.innerText||'').trim().slice(0,600);
11441
+ if(text) msgs.push({role,text});
11442
+ });
11443
+ if(!msgs.length){
11444
+ document.querySelectorAll('.font-claude-message,.prose-claude').forEach(el=>{
11445
+ const text=(el.innerText||'').trim().slice(0,600);
11446
+ if(text) msgs.push({role:'assistant',text});
11447
+ });
11448
+ }
11449
+ return JSON.stringify({provider:'Claude',messages:msgs.slice(-8),url:location.href,title:document.title});
11450
+ })()`,
11451
+ ChatGPT: `(function(){
11452
+ const msgs=[];
11453
+ document.querySelectorAll('[data-message-author-role]').forEach(el=>{
11454
+ const role=el.getAttribute('data-message-author-role')||'unknown';
11455
+ const content=el.querySelector('.markdown,.text-message,[data-message-content]');
11456
+ const text=((content||el).innerText||'').trim().slice(0,600);
11457
+ if(text) msgs.push({role,text});
11458
+ });
11459
+ return JSON.stringify({provider:'ChatGPT',messages:msgs.slice(-8),url:location.href,title:document.title});
11460
+ })()`,
11461
+ Gemini: `(function(){
11462
+ const msgs=[];
11463
+ document.querySelectorAll('user-query,model-response').forEach(el=>{
11464
+ const role=el.tagName.toLowerCase().includes('user')?'user':'assistant';
11465
+ const text=(el.innerText||'').trim().slice(0,600);
11466
+ if(text) msgs.push({role,text});
11467
+ });
11468
+ if(!msgs.length){
11469
+ document.querySelectorAll('[class*="user-query"],[class*="model-response"]').forEach(el=>{
11470
+ const isUser=el.className.toLowerCase().includes('user');
11471
+ const text=(el.innerText||'').trim().slice(0,600);
11472
+ if(text) msgs.push({role:isUser?'user':'assistant',text});
11473
+ });
11474
+ }
11475
+ return JSON.stringify({provider:'Gemini',messages:msgs.slice(-8),url:location.href,title:document.title});
11476
+ })()`,
11477
+ Perplexity: `(function(){
11478
+ const msgs=[];
11479
+ document.querySelectorAll('[class*="prose"],[class*="AnswerBody"],[class*="UserMessage"]').forEach(el=>{
11480
+ if(el.children.length>2){
11481
+ const text=(el.innerText||'').trim().slice(0,600);
11482
+ if(text.length>30) msgs.push({role:'assistant',text});
11483
+ }
11484
+ });
11485
+ return JSON.stringify({provider:'Perplexity',messages:msgs.slice(-4),url:location.href,title:document.title});
11486
+ })()`
11487
+ };
11488
+ const extractorKey = provider?.name ?? "Claude";
11489
+ const jsCode = JS_EXTRACTORS[extractorKey] ?? JS_EXTRACTORS.Claude;
11490
+ try {
11491
+ const browserName = targetTab.browser;
11492
+ if (browserName !== "Safari") {
11493
+ const switchScript = `
11494
+ tell application "${browserName.replace(/"/g, '\\"')}"
11495
+ repeat with w in windows
11496
+ set tabIdx to 1
11497
+ repeat with t in tabs of w
11498
+ if URL of t contains "${targetTab.url.slice(0, 80).replace(/"/g, '\\"')}" then
11499
+ set active tab index of w to tabIdx
11500
+ activate
11501
+ return "switched"
11502
+ end if
11503
+ set tabIdx to tabIdx + 1
11504
+ end repeat
11505
+ end repeat
11506
+ end tell`;
11507
+ await runAppleScript2(switchScript).catch(() => {
11508
+ });
11509
+ }
11510
+ const raw = await adapter.browserExecuteJs(jsCode, browserName);
11511
+ if (!raw.success) {
11512
+ return { success: false, error: `Could not read ${extractorKey} session: ${raw.error}` };
11513
+ }
11514
+ let parsed;
11515
+ try {
11516
+ parsed = JSON.parse(raw.data);
11517
+ } catch {
11518
+ return { success: false, error: "Failed to parse conversation content from browser." };
11519
+ }
11520
+ return {
11521
+ success: true,
11522
+ data: {
11523
+ provider: parsed.provider,
11524
+ browser: browserName,
11525
+ title: parsed.title ?? targetTab.title,
11526
+ url: parsed.url ?? targetTab.url,
11527
+ messages: parsed.messages,
11528
+ messageCount: parsed.messages.length
11529
+ }
11530
+ };
11531
+ } catch (err) {
11532
+ return { success: false, error: `Failed to read session: ${err.message}` };
11533
+ }
11534
+ }
11535
+ case "sys_llm_send_message": {
11536
+ const message = params.message;
11537
+ if (!message) return { success: false, error: "Missing message" };
11538
+ const targetUrl = params.url;
11539
+ const targetIndex = params.index !== void 0 ? Number(params.index) : void 0;
11540
+ const LLM_PROVIDERS = [
11541
+ { domain: "claude.ai", name: "Claude" },
11542
+ { domain: "chatgpt.com", name: "ChatGPT" },
11543
+ { domain: "chat.openai.com", name: "ChatGPT" },
11544
+ { domain: "gemini.google.com", name: "Gemini" },
11545
+ { domain: "perplexity.ai", name: "Perplexity" }
11546
+ ];
11547
+ const allTabs = await adapter.browserListTabs();
11548
+ const llmTabs = allTabs.filter(
11549
+ (tab) => LLM_PROVIDERS.some((p) => tab.url.toLowerCase().includes(p.domain))
11550
+ );
11551
+ if (llmTabs.length === 0) {
11552
+ return { success: false, error: "No LLM tabs found. Open Claude or ChatGPT in your browser first." };
11553
+ }
11554
+ let targetTab = llmTabs[0];
11555
+ if (targetUrl) {
11556
+ const found = llmTabs.find((t) => t.url.toLowerCase().includes(targetUrl.toLowerCase()));
11557
+ if (found) targetTab = found;
11558
+ } else if (targetIndex !== void 0 && llmTabs[targetIndex]) {
11559
+ targetTab = llmTabs[targetIndex];
11560
+ } else {
11561
+ const active = llmTabs.find((t) => t.active);
11562
+ const claude = llmTabs.find((t) => t.url.includes("claude.ai"));
11563
+ targetTab = active ?? claude ?? llmTabs[0];
11564
+ }
11565
+ const provider = LLM_PROVIDERS.find(
11566
+ (p) => targetTab.url.toLowerCase().includes(p.domain)
11567
+ );
11568
+ const safe = message.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
11569
+ const JS_SENDERS = {
11570
+ Claude: `(function(){
11571
+ const msg=\`${safe}\`;
11572
+ const input=document.querySelector('[contenteditable="true"][data-placeholder]')||
11573
+ document.querySelector('div[contenteditable="true"]');
11574
+ if(!input) return JSON.stringify({success:false,error:'Input field not found'});
11575
+ input.focus();
11576
+ input.innerText=msg;
11577
+ input.dispatchEvent(new InputEvent('input',{bubbles:true,inputType:'insertText',data:msg}));
11578
+ setTimeout(()=>{
11579
+ const btn=document.querySelector('button[aria-label*="Send"]')||
11580
+ document.querySelector('button[data-testid*="send"]')||
11581
+ document.querySelector('button[aria-label*="send"]');
11582
+ if(btn&&!btn.disabled){btn.click();return JSON.stringify({success:true,sent:msg});}
11583
+ },300);
11584
+ return JSON.stringify({success:true,sent:msg,note:'Typed message, attempting submit'});
11585
+ })()`,
11586
+ ChatGPT: `(function(){
11587
+ const msg=\`${safe}\`;
11588
+ const input=document.querySelector('#prompt-textarea')||
11589
+ document.querySelector('[contenteditable="true"]')||
11590
+ document.querySelector('textarea');
11591
+ if(!input) return JSON.stringify({success:false,error:'Input field not found'});
11592
+ input.focus();
11593
+ if(input.tagName==='TEXTAREA'){
11594
+ const setter=Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value').set;
11595
+ setter.call(input,msg);
11596
+ } else {
11597
+ input.innerText=msg;
11598
+ }
11599
+ input.dispatchEvent(new Event('input',{bubbles:true}));
11600
+ setTimeout(()=>{
11601
+ const btn=document.querySelector('[data-testid="send-button"]')||
11602
+ document.querySelector('button[aria-label*="Send"]');
11603
+ if(btn&&!btn.disabled) btn.click();
11604
+ },300);
11605
+ return JSON.stringify({success:true,sent:msg});
11606
+ })()`,
11607
+ Gemini: `(function(){
11608
+ const msg=\`${safe}\`;
11609
+ const input=document.querySelector('.ql-editor')||
11610
+ document.querySelector('[contenteditable="true"]')||
11611
+ document.querySelector('textarea');
11612
+ if(!input) return JSON.stringify({success:false,error:'Input field not found'});
11613
+ input.focus();
11614
+ input.innerText=msg;
11615
+ input.dispatchEvent(new InputEvent('input',{bubbles:true}));
11616
+ setTimeout(()=>{
11617
+ const btn=document.querySelector('button[aria-label*="Send"]')||
11618
+ document.querySelector('button.send-button');
11619
+ if(btn&&!btn.disabled) btn.click();
11620
+ },300);
11621
+ return JSON.stringify({success:true,sent:msg});
11622
+ })()`
11623
+ };
11624
+ const senderKey = provider?.name ?? "Claude";
11625
+ const jsCode = JS_SENDERS[senderKey] ?? JS_SENDERS.Claude;
11626
+ try {
11627
+ const browserName = targetTab.browser;
11628
+ if (browserName !== "Safari") {
11629
+ const switchScript = `
11630
+ tell application "${browserName.replace(/"/g, '\\"')}"
11631
+ repeat with w in windows
11632
+ set tabIdx to 1
11633
+ repeat with t in tabs of w
11634
+ if URL of t contains "${targetTab.url.slice(0, 80).replace(/"/g, '\\"')}" then
11635
+ set active tab index of w to tabIdx
11636
+ activate
11637
+ return "switched"
11638
+ end if
11639
+ set tabIdx to tabIdx + 1
11640
+ end repeat
11641
+ end repeat
11642
+ end tell`;
11643
+ await runAppleScript2(switchScript).catch(() => {
11644
+ });
11645
+ await new Promise((r) => setTimeout(r, 500));
11646
+ }
11647
+ const result = await adapter.browserExecuteJs(jsCode, browserName);
11648
+ if (!result.success) {
11649
+ return { success: false, error: `Could not send to ${senderKey}: ${result.error}` };
11650
+ }
11651
+ return {
11652
+ success: true,
11653
+ data: {
11654
+ provider: senderKey,
11655
+ browser: browserName,
11656
+ tab: targetTab.title,
11657
+ url: targetTab.url,
11658
+ message,
11659
+ note: "Message typed and submitted. Check browser to confirm."
11660
+ }
11661
+ };
11662
+ } catch (err) {
11663
+ return { success: false, error: `Failed to send message: ${err.message}` };
11664
+ }
11665
+ }
10968
11666
  // ── IDE Integration ────────────────────────────────────
10969
11667
  // Helper: extract open workspace paths from a VS Code/Cursor/Windsurf storage.json
10970
11668
  // Storage format: windowsState.lastActiveWindow / openedWindows
@@ -11593,6 +12291,12 @@ async function probeCapabilities() {
11593
12291
  }
11594
12292
  }
11595
12293
  }
12294
+ tools.add("llm_infer");
12295
+ tools.add("llm_list_local");
12296
+ if (adapter.platform === "macos" || adapter.platform === "windows") {
12297
+ tools.add("llm_browser_claude");
12298
+ tools.add("llm_browser_chatgpt");
12299
+ }
11596
12300
  const cap = { available, unavailable, tools: Array.from(tools) };
11597
12301
  console.log(
11598
12302
  ` \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.7",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {