@pulso/companion 0.4.5 → 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 +849 -101
  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,118 +11354,457 @@ 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 ────────────────────────────────────
11667
+ // Helper: extract open workspace paths from a VS Code/Cursor/Windsurf storage.json
11668
+ // Storage format: windowsState.lastActiveWindow / openedWindows
11669
+ // Each window has workspaceIdentifier.configURIPath (workspace file) OR folderUri (folder)
10969
11670
  case "sys_ide_list_open": {
10970
- return new Promise((resolve5) => {
10971
- exec5(
10972
- "ps aux",
10973
- { timeout: 5e3 },
10974
- (err, stdout) => {
10975
- if (err) {
10976
- resolve5({ success: false, error: err.message });
10977
- return;
11671
+ let readIdeWorkspaces2 = function(storagePath) {
11672
+ const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
11673
+ const ws2 = storage["windowsState"] ?? {};
11674
+ const allWindows = [
11675
+ ws2["lastActiveWindow"],
11676
+ ...Array.isArray(ws2["openedWindows"]) ? ws2["openedWindows"] : []
11677
+ ].filter(Boolean);
11678
+ const seen = /* @__PURE__ */ new Set();
11679
+ const paths = [];
11680
+ for (const w of allWindows) {
11681
+ const configURI = w["workspaceIdentifier"]?.["configURIPath"] ?? "";
11682
+ const folderURI = w["folderUri"] ?? "";
11683
+ for (const uri of [configURI, folderURI]) {
11684
+ if (!uri) continue;
11685
+ let p = uri.replace(/^file:\/\//, "");
11686
+ if (p.endsWith(".code-workspace")) p = dirname(p);
11687
+ if (p && !seen.has(p)) {
11688
+ seen.add(p);
11689
+ paths.push(p);
10978
11690
  }
10979
- const IDE_PATTERNS = {
10980
- "Cursor Helper": "Cursor",
10981
- "Cursor.app": "Cursor",
10982
- "Code Helper": "VS Code",
10983
- "Visual Studio Code": "VS Code",
10984
- "Windsurf Helper": "Windsurf",
10985
- "Windsurf.app": "Windsurf",
10986
- "zed": "Zed",
10987
- "WebStorm": "WebStorm",
10988
- "IntelliJ IDEA": "IntelliJ IDEA",
10989
- "PyCharm": "PyCharm",
10990
- "GoLand": "GoLand"
10991
- };
10992
- const found = {};
10993
- for (const line of stdout.split("\n")) {
10994
- for (const [pattern, ideName] of Object.entries(IDE_PATTERNS)) {
10995
- if (line.includes(pattern) && !line.includes("grep")) {
10996
- if (!found[ideName]) found[ideName] = { ide: ideName, workspaces: [] };
10997
- const matches = line.match(/\/(Users|home)\/[^\s]+/g) ?? [];
10998
- for (const m of matches) {
10999
- if (!m.includes(".app/") && !found[ideName].workspaces.includes(m)) {
11000
- try {
11001
- if (statSync4(m).isDirectory()) {
11002
- found[ideName].workspaces.push(m);
11003
- }
11004
- } catch {
11005
- }
11006
- }
11007
- }
11008
- }
11691
+ }
11692
+ }
11693
+ const activePath = (() => {
11694
+ const aw = ws2["lastActiveWindow"];
11695
+ if (!aw) return null;
11696
+ const configURI = aw["workspaceIdentifier"]?.["configURIPath"] ?? "";
11697
+ const folderURI = aw["folderUri"] ?? "";
11698
+ const raw = configURI || folderURI;
11699
+ if (!raw) return null;
11700
+ let p = raw.replace(/^file:\/\//, "");
11701
+ if (p.endsWith(".code-workspace")) p = dirname(p);
11702
+ return p || null;
11703
+ })();
11704
+ return { active: activePath, all: paths };
11705
+ };
11706
+ var readIdeWorkspaces = readIdeWorkspaces2;
11707
+ return new Promise((resolve5) => {
11708
+ exec5("ps aux", { timeout: 5e3 }, (err, stdout) => {
11709
+ if (err) {
11710
+ resolve5({ success: false, error: err.message });
11711
+ return;
11712
+ }
11713
+ const IDE_PATTERNS = {
11714
+ "Cursor Helper": "Cursor",
11715
+ "Cursor.app": "Cursor",
11716
+ "Code Helper": "VS Code",
11717
+ "Visual Studio Code": "VS Code",
11718
+ "Windsurf Helper": "Windsurf",
11719
+ "Windsurf.app": "Windsurf",
11720
+ "zed": "Zed",
11721
+ "WebStorm": "WebStorm",
11722
+ "IntelliJ IDEA": "IntelliJ IDEA",
11723
+ "PyCharm": "PyCharm",
11724
+ "GoLand": "GoLand"
11725
+ };
11726
+ const running = /* @__PURE__ */ new Set();
11727
+ for (const line of stdout.split("\n")) {
11728
+ for (const [pattern, ideName] of Object.entries(IDE_PATTERNS)) {
11729
+ if (line.includes(pattern) && !line.includes("grep")) {
11730
+ running.add(ideName);
11009
11731
  }
11010
11732
  }
11011
- const ides = Object.values(found);
11012
- const home = homedir4();
11013
- const storagePaths = [
11014
- { ide: "VS Code", path: join5(home, "Library/Application Support/Code/User/globalStorage/storage.json") },
11015
- { ide: "Cursor", path: join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json") },
11016
- { ide: "Windsurf", path: join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json") }
11017
- ];
11018
- for (const { ide: ideName, path: storagePath } of storagePaths) {
11019
- if (!existsSync4(storagePath)) continue;
11020
- try {
11021
- const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
11022
- const recentFolders = (storage["recently.opened"]?.workspaces ?? []).map(
11023
- (w) => typeof w === "string" ? w : w.folderUri ?? ""
11024
- ).filter(Boolean).map((p) => p.replace(/^file:\/\//, "")).slice(0, 3);
11025
- if (recentFolders.length > 0) {
11026
- const existing = found[ideName];
11027
- if (existing) {
11028
- for (const folder of recentFolders) {
11029
- if (!existing.workspaces.includes(folder)) {
11030
- existing.workspaces.push(folder);
11031
- }
11032
- }
11033
- } else if (ides.find((i) => i.ide === ideName) === void 0) {
11034
- ides.push({ ide: ideName, workspaces: recentFolders });
11035
- }
11036
- }
11037
- } catch {
11038
- }
11733
+ }
11734
+ const home = homedir4();
11735
+ const storagePaths = [
11736
+ { ide: "VS Code", path: join5(home, "Library/Application Support/Code/User/globalStorage/storage.json") },
11737
+ { ide: "Cursor", path: join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json") },
11738
+ { ide: "Windsurf", path: join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json") }
11739
+ ];
11740
+ const ides = [];
11741
+ for (const { ide: ideName, path: storagePath } of storagePaths) {
11742
+ if (!existsSync4(storagePath)) continue;
11743
+ try {
11744
+ const { active, all } = readIdeWorkspaces2(storagePath);
11745
+ ides.push({ ide: ideName, active, workspaces: all, running: running.has(ideName) });
11746
+ } catch {
11039
11747
  }
11040
- resolve5({
11041
- success: true,
11042
- data: {
11043
- ides: ides.length > 0 ? ides : [],
11044
- count: ides.length,
11045
- note: ides.length === 0 ? "No IDEs detected. Open VS Code, Cursor, Windsurf, or Zed." : void 0
11046
- }
11047
- });
11048
11748
  }
11049
- );
11749
+ if (running.has("Zed") && !ides.find((i) => i.ide === "Zed")) {
11750
+ ides.push({ ide: "Zed", active: null, workspaces: [], running: true });
11751
+ }
11752
+ resolve5({
11753
+ success: true,
11754
+ data: {
11755
+ ides: ides.length > 0 ? ides : [],
11756
+ count: ides.length,
11757
+ note: ides.length === 0 ? "No IDEs detected." : void 0
11758
+ }
11759
+ });
11760
+ });
11050
11761
  });
11051
11762
  }
11052
11763
  case "sys_ide_get_context": {
11053
11764
  const targetIde = params.ide ?? "";
11054
11765
  const home = homedir4();
11055
11766
  const storageMap = {
11056
- vscode: join5(home, "Library/Application Support/Code/User/globalStorage/storage.json"),
11057
- cursor: join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json"),
11058
- windsurf: join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json")
11767
+ vscode: { label: "VS Code", path: join5(home, "Library/Application Support/Code/User/globalStorage/storage.json") },
11768
+ cursor: { label: "Cursor", path: join5(home, "Library/Application Support/Cursor/User/globalStorage/storage.json") },
11769
+ windsurf: { label: "Windsurf", path: join5(home, "Library/Application Support/Windsurf/User/globalStorage/storage.json") }
11059
11770
  };
11060
11771
  const ideKey = targetIde.toLowerCase().replace(/[\s-]/g, "");
11061
- const pathsToTry = ideKey && storageMap[ideKey] ? [{ ide: ideKey, path: storageMap[ideKey] }] : Object.entries(storageMap).map(([ide, path]) => ({ ide, path }));
11062
- for (const { ide, path: storagePath } of pathsToTry) {
11772
+ const pathsToTry = ideKey && storageMap[ideKey] ? [{ key: ideKey, ...storageMap[ideKey] }] : Object.entries(storageMap).map(([key, v]) => ({ key, ...v }));
11773
+ for (const { key: _key, label, path: storagePath } of pathsToTry) {
11063
11774
  if (!existsSync4(storagePath)) continue;
11064
11775
  try {
11065
11776
  const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
11066
- const recentWorkspaces = (storage["recently.opened"]?.workspaces ?? []).map(
11067
- (w) => typeof w === "string" ? w : w.folderUri ?? ""
11068
- ).filter(Boolean).map((p) => p.replace(/^file:\/\//, "")).slice(0, 5);
11069
- const activeWorkspace = recentWorkspaces[0] ?? null;
11070
- const recentFiles = (storage["recently.opened"]?.files ?? []).map(
11071
- (f) => typeof f === "string" ? f : f.fileUri ?? ""
11072
- ).filter(Boolean).map((p) => p.replace(/^file:\/\//, "")).slice(0, 5);
11777
+ const ws2 = storage["windowsState"] ?? {};
11778
+ const allWindows = [
11779
+ ws2["lastActiveWindow"],
11780
+ ...Array.isArray(ws2["openedWindows"]) ? ws2["openedWindows"] : []
11781
+ ].filter(Boolean);
11782
+ const seen = /* @__PURE__ */ new Set();
11783
+ const workspaces = [];
11784
+ for (const w of allWindows) {
11785
+ const configURI = w["workspaceIdentifier"]?.["configURIPath"] ?? "";
11786
+ const folderURI = w["folderUri"] ?? "";
11787
+ for (const uri of [configURI, folderURI]) {
11788
+ if (!uri) continue;
11789
+ let p = uri.replace(/^file:\/\//, "");
11790
+ if (p.endsWith(".code-workspace")) p = dirname(p);
11791
+ if (p && !seen.has(p)) {
11792
+ seen.add(p);
11793
+ workspaces.push(p);
11794
+ }
11795
+ }
11796
+ }
11797
+ const activeWindow = ws2["lastActiveWindow"];
11798
+ const activeConfigURI = activeWindow?.["workspaceIdentifier"]?.["configURIPath"] ?? "";
11799
+ const activeFolderURI = activeWindow?.["folderUri"] ?? "";
11800
+ let activeWorkspace = (activeConfigURI || activeFolderURI).replace(/^file:\/\//, "");
11801
+ if (activeWorkspace.endsWith(".code-workspace")) activeWorkspace = dirname(activeWorkspace);
11073
11802
  return {
11074
11803
  success: true,
11075
11804
  data: {
11076
- ide,
11077
- activeWorkspace,
11078
- recentWorkspaces,
11079
- recentFiles
11805
+ ide: label,
11806
+ activeWorkspace: activeWorkspace || null,
11807
+ openWorkspaces: workspaces
11080
11808
  }
11081
11809
  };
11082
11810
  } catch {
@@ -11084,7 +11812,7 @@ print(result.stdout[:5000])
11084
11812
  }
11085
11813
  return {
11086
11814
  success: false,
11087
- error: "No IDE context found. Make sure VS Code, Cursor, or Windsurf has been used."
11815
+ error: "No IDE context found. Make sure VS Code, Cursor, or Windsurf has been opened."
11088
11816
  };
11089
11817
  }
11090
11818
  case "sys_ide_run_terminal": {
@@ -11101,10 +11829,17 @@ print(result.stdout[:5000])
11101
11829
  if (!existsSync4(storagePath)) continue;
11102
11830
  try {
11103
11831
  const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
11104
- const firstWorkspace = (storage["recently.opened"]?.workspaces ?? [])[0];
11105
- if (firstWorkspace) {
11106
- const p = typeof firstWorkspace === "string" ? firstWorkspace : firstWorkspace.folderUri ?? "";
11107
- cwd = p.replace(/^file:\/\//, "");
11832
+ const ws2 = storage["windowsState"] ?? {};
11833
+ const aw = ws2["lastActiveWindow"];
11834
+ if (!aw) continue;
11835
+ const configURI = aw["workspaceIdentifier"]?.["configURIPath"] ?? "";
11836
+ const folderURI = aw["folderUri"] ?? "";
11837
+ const raw = configURI || folderURI;
11838
+ if (!raw) continue;
11839
+ let p = raw.replace(/^file:\/\//, "");
11840
+ if (p.endsWith(".code-workspace")) p = dirname(p);
11841
+ if (p) {
11842
+ cwd = p;
11108
11843
  break;
11109
11844
  }
11110
11845
  } catch {
@@ -11179,10 +11914,17 @@ STDERR: ${stderr}` : "")).slice(0, 1e4),
11179
11914
  if (!existsSync4(storagePath)) continue;
11180
11915
  try {
11181
11916
  const storage = JSON.parse(readFileSync4(storagePath, "utf-8"));
11182
- const firstWorkspace = (storage["recently.opened"]?.workspaces ?? [])[0];
11183
- if (firstWorkspace) {
11184
- const p = typeof firstWorkspace === "string" ? firstWorkspace : firstWorkspace.folderUri ?? "";
11185
- cwd = p.replace(/^file:\/\//, "");
11917
+ const ws2 = storage["windowsState"] ?? {};
11918
+ const aw = ws2["lastActiveWindow"];
11919
+ if (!aw) continue;
11920
+ const configURI = aw["workspaceIdentifier"]?.["configURIPath"] ?? "";
11921
+ const folderURI = aw["folderUri"] ?? "";
11922
+ const raw = configURI || folderURI;
11923
+ if (!raw) continue;
11924
+ let p = raw.replace(/^file:\/\//, "");
11925
+ if (p.endsWith(".code-workspace")) p = dirname(p);
11926
+ if (p) {
11927
+ cwd = p;
11186
11928
  break;
11187
11929
  }
11188
11930
  } catch {
@@ -11549,6 +12291,12 @@ async function probeCapabilities() {
11549
12291
  }
11550
12292
  }
11551
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
+ }
11552
12300
  const cap = { available, unavailable, tools: Array.from(tools) };
11553
12301
  console.log(
11554
12302
  ` \u2705 Available: ${available.join(", ") || "all adapter tools"}`
@@ -12407,7 +13155,7 @@ process.on("exit", () => {
12407
13155
  });
12408
13156
  console.log("");
12409
13157
  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");
12410
- console.log(` \u2551 Pulso ${platformName} Companion v0.4.3 \u2551`);
13158
+ console.log(` \u2551 Pulso ${platformName} Companion v0.4.5 \u2551`);
12411
13159
  console.log(" \u255A\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\u255D");
12412
13160
  console.log("");
12413
13161
  console.log(` Platform: ${currentPlatform}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.4.5",
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": {