sidebud 0.1.0 → 0.4.0

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 (39) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/main.js +9361 -4209
  3. package/package.json +3 -2
  4. package/skills/widget-packs/SKILL.md +199 -0
  5. package/src/manage/guide/app-arrange.png +0 -0
  6. package/src/manage/guide/app-disk.png +0 -0
  7. package/src/manage/guide/app-open.png +0 -0
  8. package/src/manage/guide/app-thread.png +0 -0
  9. package/src/manage/guide/app-tiles.png +0 -0
  10. package/src/manage/guide/draft.png +0 -0
  11. package/src/manage/guide/layouts.png +0 -0
  12. package/src/manage/guide/library.png +0 -0
  13. package/src/manage/guide/pack-page.png +0 -0
  14. package/src/manage/guide/permissions.png +0 -0
  15. package/src/manage/guide/phone-home.png +0 -0
  16. package/src/manage/guide/phone-open.png +0 -0
  17. package/src/manage/guide/widget-voice.png +0 -0
  18. package/src/manage/guide.js +155 -0
  19. package/src/manage/guide.md +139 -0
  20. package/src/manage/index.html +14 -0
  21. package/src/manage/manage.css +782 -0
  22. package/src/manage/manage.js +155 -39
  23. package/src/manage/packs.js +347 -0
  24. package/src/manage/settings.js +1 -1
  25. package/src/manage/setup.js +83 -23
  26. package/packs/BRAND_ASSETS.md +0 -8
  27. package/packs/calendar/logo.png +0 -0
  28. package/packs/calendar/pack.json +0 -127
  29. package/packs/discord/pack.json +0 -107
  30. package/packs/filesystem/pack.json +0 -84
  31. package/packs/github/pack.json +0 -127
  32. package/packs/gmail/logo.png +0 -0
  33. package/packs/gmail/pack.json +0 -274
  34. package/packs/obsidian/pack.json +0 -88
  35. package/packs/t3-agents/logo.png +0 -0
  36. package/packs/t3-agents/pack.json +0 -492
  37. package/packs/telegram/pack.json +0 -137
  38. package/packs/trello/pack.json +0 -102
  39. package/packs/whatsapp/pack.json +0 -118
@@ -4,13 +4,11 @@ const SETUP_STEPS = [
4
4
  { id: 'execution', title: 'Choose who does the work', short: 'Execution agent' },
5
5
  { id: 'voice', title: 'Choose a voice', short: 'Voice agent' },
6
6
  { id: 'integrations', title: 'Add integrations', short: 'Integrations' },
7
+ { id: 'agents', title: 'Choose your default agents', short: 'Default agents' },
8
+ { id: 'decisions', title: 'Fast decisions', short: 'Fast decisions', beta: true, optional: true },
7
9
  { id: 'phone', title: 'Pair your phone', short: 'Pair phone' },
8
10
  { id: 'done', title: 'All set', short: 'Finish' },
9
11
  ];
10
- /** Recommended on a first setup; the owner can untick any of them. */
11
- const DEFAULT_INTEGRATIONS = new Set(['t3-agents', 'obsidian']);
12
- /** Catalog packs offered in setup; T3 Agents is only offered once the app is on this computer. */
13
- const SETUP_INTEGRATIONS = ['t3-agents', 'obsidian', 'calendar', 'gmail'];
14
12
  let setupSelections = null;
15
13
  let setupBusy = false;
16
14
 
@@ -66,15 +64,15 @@ function checkCard(kind, intro) {
66
64
  function setupStepper(current) {
67
65
  return `<ol class="setup-steps">${SETUP_STEPS.map((step, index) => {
68
66
  const done = step.id === 'done' ? Boolean(state.setup.completedAt) : state.setup.steps[step.id].done;
69
- return `<li><button data-action="setup-step" data-step="${step.id}" class="${step.id === current ? 'current' : ''} ${done ? 'done' : ''}" ${step.id === current ? 'aria-current="step"' : ''}><span class="step-mark" aria-hidden="true">${done ? '✓' : index + 1}</span><span>${esc(step.short)}</span></button></li>`;
67
+ return `<li><button data-action="setup-step" data-step="${step.id}" class="${step.id === current ? 'current' : ''} ${done ? 'done' : ''}" ${step.id === current ? 'aria-current="step"' : ''}><span class="step-mark" aria-hidden="true">${done ? '✓' : index + 1}</span><span>${esc(step.short)}${step.beta ? '<span class="tag-beta">Beta</span>' : ''}</span></button></li>`;
70
68
  }).join('')}</ol>`;
71
69
  }
72
- function setupNav(current, { next = 'Continue', nextAction = 'setup-next', skip = true } = {}) {
70
+ function setupNav(current, { next = 'Continue', nextAction = 'setup-next', skip = true, skipAction = 'setup-next' } = {}) {
73
71
  const index = SETUP_STEPS.findIndex((step) => step.id === current);
74
- return `<div class="setup-nav">${index > 0 ? `<button data-action="setup-step" data-step="${SETUP_STEPS[index - 1].id}">Back</button>` : '<span></span>'}<div class="actions">${skip ? '<button data-action="setup-next">Skip for now</button>' : ''}<button class="primary" data-action="${nextAction}">${esc(next)}</button></div></div>`;
72
+ return `<div class="setup-nav">${index > 0 ? `<button data-action="setup-step" data-step="${SETUP_STEPS[index - 1].id}">Back</button>` : '<span></span>'}<div class="actions">${skip ? `<button data-action="${skipAction}">Skip for now</button>` : ''}<button class="primary" data-action="${nextAction}">${esc(next)}</button></div></div>`;
75
73
  }
76
74
  function appStep() {
77
- return `<div class="setup-grid"><div class="card stack"><h3>Install Sidebud on your phone</h3><p>Scan this code with your phone’s camera. It opens the right store for your phone.</p><img class="download-qr" src="${esc(state.download.qr)}" alt="QR code to download the Sidebud app"><p class="hint">Or open <a href="${esc(state.download.url)}" target="_blank" rel="noopener">${esc(state.download.url.replace(/^https:\/\//, ''))}</a> on your phone. iPhone builds are available through TestFlight during the beta.</p></div><div class="card stack"><h3>What happens next</h3><ol class="plain-steps"><li><strong>Pick an execution agent.</strong> It does the work on this computer, and we test it with a real prompt.</li><li><strong>Pick a voice.</strong> We test that the voice can hand requests to your agent.</li><li><strong>Add integrations</strong> such as T3 Agents and Obsidian.</li><li><strong>Pair your phone</strong> by scanning a code in the app. You can <a href="#setup/phone">pair now</a> if the app is already installed.</li></ol></div></div>${setupNav('app', { next: 'I have the app', nextAction: 'setup-app-done', skip: false })}`;
75
+ return `<div class="setup-grid"><div class="card stack"><h3>Install Sidebud on your phone</h3><p>Scan this code with your phone’s camera. It opens the right store for your phone.</p><img class="download-qr" src="${esc(state.download.qr)}" alt="QR code to download the Sidebud app"><p class="hint">Or open <a href="${esc(state.download.url)}" target="_blank" rel="noopener">${esc(state.download.url.replace(/^https:\/\//, ''))}</a> on your phone. iPhone builds are available through TestFlight during the beta.</p></div><div class="card stack"><h3>What happens next</h3><ol class="plain-steps"><li><strong>Pick an execution agent.</strong> It does the work on this computer, and we test it with a real prompt.</li><li><strong>Pick a voice.</strong> We test that the voice can hand requests to your agent.</li><li><strong>Add integrations</strong> from the widget library, or have your agent build your own.</li><li><strong>Choose your default agents</strong>: the agents app with your main work agents, such as T3 Agents.</li><li><strong>Turn on fast decisions</strong> (optional, beta) so simple requests are answered in about a second.</li><li><strong>Pair your phone</strong> by scanning a code in the app. You can <a href="#setup/phone">pair now</a> if the app is already installed.</li></ol></div></div>${setupNav('app', { next: 'I have the app', nextAction: 'setup-app-done', skip: false })}`;
78
76
  }
79
77
  function executionStep() {
80
78
  if (!state.providers || state.connector.kind === 'memory')
@@ -90,24 +88,46 @@ function voiceStep() {
90
88
  const model = provider.models.some((option) => option.id === saved.model) ? saved.model : provider.defaultModel;
91
89
  const agent = state.setup.companionAgent;
92
90
  // Same controls and ids as the Voice page, so its provider/model handlers and the voice sample work here too.
93
- return `<div class="card stack"><h3>Voice agent</h3><p class="hint">The voice agent talks with you in calls and typed chat, and hands work to your execution agent. Its API key stays on this computer.</p><label>Voice provider<select id="voice-provider">${Object.values(catalog).map((option) => `<option value="${esc(option.id)}" ${option.id === selectedId ? 'selected' : ''}>${esc(option.label)}${option.id === 'gemini' ? ' (recommended)' : ''}</option>`).join('')}</select></label><p id="voice-untested" class="hint" ${provider.untested ? '' : 'hidden'}>Untested: this provider has not been run against the real service yet, so expect rough edges.</p><label>Voice model<select id="voice-model">${voiceModelOptions(provider, model)}</select></label><div id="voice-endpoint" class="stack">${voiceEndpointFields(provider, saved)}</div><label>Voice<select id="voice-name">${voiceNameOptions(voiceOptionsFor(provider, model), saved.voiceName || provider.defaultVoice)}</select></label><div class="voice-preview"><button type="button" data-action="preview-voice">Play voice sample</button><button type="button" data-action="stop-voice-preview" hidden>Stop sample</button><audio id="voice-preview-audio" controls preload="none" hidden></audio></div><p id="voice-preview-status" class="hint" role="status" aria-live="polite">Save a key below, then play a sample.</p></div><div id="voice-key-panel" class="stack">${voiceKeysView(selectedId)}</div><div class="actions"><button class="primary" data-action="setup-save-voice">Save voice and test it</button></div>${checkCard('voice', agent ? `Sends the voice agent a request as if you typed it, and checks that ${esc(agent.label)} receives and answers it.` : 'Finish the execution agent step first.')}${setupNav('voice', { next: state.setup.steps.voice.done ? 'Continue' : 'Continue without testing', skip: false })}`;
91
+ return `<div class="card stack"><h3>Voice agent</h3><p class="hint">The voice agent talks with you in calls and typed chat, and hands work to your execution agent. Its API key stays on this computer.</p><label>Voice provider<select id="voice-provider">${Object.values(catalog).map((option) => `<option value="${esc(option.id)}" ${option.id === selectedId ? 'selected' : ''}>${esc(option.label)}${option.id === 'gemini' ? ' (recommended)' : ''}</option>`).join('')}</select></label><p id="voice-untested" class="hint" ${provider.untested ? '' : 'hidden'}>Untested: this provider has not been run against the real service yet, so expect rough edges.</p><label>Voice model<select id="voice-model">${voiceModelOptions(provider, model)}</select></label><div id="voice-endpoint" class="stack">${voiceEndpointFields(provider, saved)}</div><label>Voice<select id="voice-name">${voiceNameOptions(voiceOptionsFor(provider, model), saved.voiceName || provider.defaultVoice)}</select></label><div class="voice-preview"><button type="button" data-action="preview-voice">Play voice sample</button><button type="button" data-action="stop-voice-preview" hidden>Stop sample</button><audio id="voice-preview-audio" controls preload="none" hidden></audio></div><p id="voice-preview-status" class="hint" role="status" aria-live="polite">Save a key below, then play a sample.</p></div><div id="voice-key-panel" class="stack">${voiceKeysView(selectedId)}</div><div class="card stack"><h3>Spoken mute and unmute</h3>${commandPhraseFields(voice.commands)}<p class="hint">The defaults work as they are. Change them any time on the <a href="#voice">Voice</a> page, along with auto-mute.</p><div class="actions"><button data-action="setup-save-commands">Save phrases</button></div></div><div class="actions"><button class="primary" data-action="setup-save-voice">Save voice and test it</button></div>${checkCard('voice', agent ? `Sends the voice agent a request as if you typed it, and checks that ${esc(agent.label)} receives and answers it.` : 'Finish the execution agent step first.')}${setupNav('voice', { next: state.setup.steps.voice.done ? 'Continue' : 'Continue without testing', skip: false })}`;
94
92
  }
93
+ /** Library packs whose app is on this computer are suggested; the owner can untick any of them. */
95
94
  function integrationSelections() {
96
95
  if (!setupSelections) {
97
- setupSelections = new Set([...DEFAULT_INTEGRATIONS].filter((id) => !state.integrationCatalog.find((entry) => entry.id === id)?.installed && (id !== 't3-agents' || state.t3Agents.installed)));
96
+ setupSelections = new Set(state.integrationCatalog.filter((entry) => entry.detected && !entry.installed).map((entry) => entry.id));
98
97
  }
99
98
  return setupSelections;
100
99
  }
101
100
  function integrationsStep() {
102
101
  const selected = integrationSelections();
103
102
  const option = (entry) => {
104
- const t3 = entry.id === 't3-agents' ? state.t3Agents : null;
105
- const unavailable = t3 && !t3.installed;
106
- const note = entry.installed ? 'Added' : t3?.note || entry.account;
107
- return `<label class="integration-option ${unavailable ? 'unavailable' : ''}"><input type="checkbox" data-integration="${esc(entry.id)}" ${entry.installed ? 'checked disabled' : unavailable ? 'disabled' : selected.has(entry.id) ? 'checked' : ''}><span><strong>${esc(entry.name)}</strong>${DEFAULT_INTEGRATIONS.has(entry.id) ? ' <span class="pill">Recommended</span>' : ''}<br><span class="hint">${esc(entry.description)}</span><br><span class="hint">${esc(note)}</span></span></label>`;
103
+ const note = entry.installed ? 'Added' : entry.account;
104
+ return `<label class="integration-option"><input type="checkbox" data-integration="${esc(entry.id)}" ${entry.installed ? 'checked disabled' : selected.has(entry.id) ? 'checked' : ''}><span><strong>${esc(entry.name)}</strong>${entry.detected ? ' <span class="pill">Found on this computer</span>' : ''}<br><span class="hint">${esc(entry.description)}</span><br><span class="hint">${esc(note)}</span></span></label>`;
108
105
  };
109
- const packs = SETUP_INTEGRATIONS.map((id) => state.integrationCatalog.find((entry) => entry.id === id)).filter(Boolean);
110
- return `<div class="card stack"><h3>What should Sidebud reach?</h3><p class="hint">Each integration is added disabled. Your execution agent then sets it up, uses settings and keys it finds on this computer, and reports on your phone what it needs from you.</p><div class="integration-options">${packs.map(option).join('')}</div><p class="hint">More integrations, and ways to make your own, are under <a href="#mcp">MCP &amp; widgets</a>.</p></div>${setupNav('integrations', { next: 'Add selected and continue', nextAction: 'setup-add-integrations' })}`;
106
+ // Apps found on this computer first, then the rest of the library.
107
+ const packs = [...state.integrationCatalog].sort((a, b) => Number(b.detected) - Number(a.detected));
108
+ const empty = state.libraryError
109
+ ? `<p class="hint">The widget library could not be reached (${esc(state.libraryError)}). You can add integrations later under <a href="#mcp">MCP &amp; widgets</a>, or ask your agent to build one.</p>`
110
+ : '<p class="hint">The widget library has no packs yet.</p>';
111
+ return `<div class="card stack"><h3>What should Sidebud reach?</h3><p class="hint">These are widget packs from the Sidebud widget library. Each is added disabled. Your execution agent then sets it up, uses settings and keys it finds on this computer, and reports on your phone what it needs from you. Your agent can also build packs for anything else you use.</p>${packs.length ? `<div class="integration-options">${packs.map(option).join('')}</div>` : empty}<p class="hint">More integrations, and ways to make your own, are under <a href="#mcp">MCP &amp; widgets</a>.</p></div>${setupNav('integrations', { next: 'Add selected and continue', nextAction: 'setup-add-integrations' })}`;
112
+ }
113
+ /** The user's default agents: their main work agents app, which is not the execution agent. */
114
+ function agentsStep() {
115
+ const apps = (state.packs?.packs || []).filter((pack) => pack.agentsApp);
116
+ const chosen = state.packs?.defaultAgents || '';
117
+ const status = (pack) => (pack.status.state === 'ok' ? '' : pack.status.state === 'disabled' ? ' · not turned on yet' : ' · being set up');
118
+ const body = apps.length
119
+ ? `<div class="integration-options">${apps.map((pack) => `<label class="integration-option"><input type="radio" name="default-agents" value="${esc(pack.id)}" ${pack.id === chosen ? 'checked' : ''}><span><strong>${esc(pack.name)}</strong>${pack.id === chosen ? ' <span class="pill">Default</span>' : ''}<br><span class="hint">${esc(pack.description)}${esc(status(pack))}</span></span></label>`).join('')}</div>`
120
+ : '<p class="muted">No agents app is installed yet. Add T3 Agents, Hermes, OpenClaw, or another agents app under <a href="#mcp">MCP &amp; widgets</a>, then choose it under Execution → Default agents.</p>';
121
+ return `<div class="card stack"><h3>Where do your main work agents live?</h3><p class="hint">Your default agents are the agents app you work with most. When you ask “what agents are running?”, Sidebud checks this app. They are not your execution agent: the execution agent takes requests from voice and does the work on this computer.</p>${body}</div>${setupNav('agents', apps.length ? { next: 'Save and continue', nextAction: 'setup-save-agents' } : { next: 'Continue', nextAction: 'setup-ack-agents', skip: false })}`;
122
+ }
123
+ /** Optional while in beta: a fast decision model (the user's own TypeSafe key, or a local server). */
124
+ function decisionsStep() {
125
+ const jev = state.jev;
126
+ const on = jev.status.state === 'ready';
127
+ return `<div class="card stack"><div class="row"><div><h3>Answer simple requests in about a second <span class="tag-beta">Beta</span></h3><p class="hint">A fast decision model (TypeSafe’s Jev) decides whether this computer can handle a request itself: open or quit apps, volume, music, reminders, what your default agents are doing, one integration’s data, and home tiles. Anything else, or anything it is unsure about, goes to your execution agent as usual. You can skip this and turn it on later under Fast decisions.</p></div><span class="badge ${on ? 'good' : 'warn'}">${on ? `On · ${jev.status.kind === 'local' ? 'local server' : 'TypeSafe'}` : 'Off'}</span></div>
128
+ <label>TypeSafe API key<input id="setup-jev-key" type="password" autocomplete="new-password" placeholder="${jev.key.isSet ? 'A key is saved. Paste a new one to replace it.' : 'Paste your key from typesafe.ai'}"></label>
129
+ <label>Or a local Jev-compatible server (optional)<input id="setup-jev-url" type="url" placeholder="Empty: TypeSafe’s hosted API" value="${esc(jev.config.url ?? '')}"></label>
130
+ <p class="hint">Your key stays in this computer’s keychain and is only sent to TypeSafe. A local server gets no key.</p></div>${setupNav('decisions', { next: on ? 'Test and continue' : 'Save and test', nextAction: 'setup-save-decisions', skipAction: 'setup-skip-decisions' })}`;
111
131
  }
112
132
  function phoneStep() {
113
133
  const active = state.devices.filter((device) => !device.revokedAt);
@@ -121,6 +141,7 @@ function completedView() {
121
141
  { hash: 'execution', title: 'Execution agents', text: 'Add agents, change the default, set thinking levels and prompts.' },
122
142
  { hash: 'mcp', title: 'MCP & widgets', text: 'Add integrations, set them up, and choose what your phone shows and hears.' },
123
143
  { hash: 'voice', title: 'Voice', text: 'Change the voice agent, model, and voice.' },
144
+ { hash: 'decisions', title: 'Fast decisions (Beta)', text: 'Add or change your Jev key, or a local server, and test it.' },
124
145
  { hash: 'connection', title: 'Connection', text: 'Pair another phone or remove one.' },
125
146
  ];
126
147
  return `<div class="card stack setup-complete"><div class="row"><span class="done-badge" aria-hidden="true">✓</span><div><h3>Well done. Sidebud is set up on ${esc(state.name)}.</h3><p>Continue in the Sidebud app on your phone from here: start a voice chat, send a message, and check on your widgets and running work.</p></div></div></div><div class="card stack"><div><h3>Manage everything from this dashboard, any time</h3><p class="hint">Keep the companion running on this computer. Open this page again with <span class="code">sidebud manage</span> or <span class="code">sidebud setup</span>.</p></div><div class="manage-links">${manage.map((item) => `<a class="manage-link" href="#${item.hash}"><strong>${esc(item.title)}</strong><span class="hint">${esc(item.text)}</span></a>`).join('')}</div></div><div class="actions"><button data-action="setup-reopen">Run setup again</button></div>`;
@@ -129,17 +150,17 @@ function doneStep() {
129
150
  if (state.setup.completedAt) return completedView();
130
151
  const rows = SETUP_STEPS.filter((step) => step.id !== 'done').map((step) => {
131
152
  const done = state.setup.steps[step.id].done;
132
- return `<li class="${done ? 'done' : ''}"><span class="step-mark" aria-hidden="true">${done ? '✓' : '•'}</span><span>${esc(step.title)}</span>${done ? '' : `<button data-action="setup-step" data-step="${step.id}">Finish this</button>`}</li>`;
153
+ return `<li class="${done ? 'done' : ''}"><span class="step-mark" aria-hidden="true">${done ? '✓' : '•'}</span><span>${esc(step.title)}${step.optional ? ' <span class="hint">(optional)</span>' : ''}</span>${done ? '' : `<button data-action="setup-step" data-step="${step.id}">Finish this</button>`}</li>`;
133
154
  }).join('');
134
- const ready = SETUP_STEPS.every((step) => step.id === 'done' || state.setup.steps[step.id].done);
155
+ const ready = SETUP_STEPS.every((step) => step.id === 'done' || step.optional || state.setup.steps[step.id].done);
135
156
  return `<div class="card stack"><h3>${state.setup.completedAt ? 'Setup is finished' : ready ? 'Everything is ready' : 'Almost there'}</h3><ul class="setup-summary">${rows}</ul><p class="hint">Change any of this later from the sections on the left. Your phone shows your widgets, running tasks, and a button to start a voice chat.</p><div class="actions">${state.setup.completedAt ? '<button data-action="setup-reopen">Run setup again</button>' : '<button class="primary" data-action="setup-complete">Finish setup</button>'}</div></div>`;
136
157
  }
137
158
  function setupView() {
138
159
  const current = currentSetupStep();
139
160
  const step = SETUP_STEPS.find((candidate) => candidate.id === current);
140
- const body = { app: appStep, execution: executionStep, voice: voiceStep, integrations: integrationsStep, phone: phoneStep, done: doneStep }[current]();
161
+ const body = { app: appStep, execution: executionStep, voice: voiceStep, integrations: integrationsStep, agents: agentsStep, decisions: decisionsStep, phone: phoneStep, done: doneStep }[current]();
141
162
  const title = current === 'done' && state.setup.completedAt ? 'Well done' : step.title;
142
- return `<section class="section setup"><div><h2>${esc(title)}</h2><p class="lede">Set up Sidebud on ${esc(state.name)}. Keys, accounts, and agents stay on this computer.</p></div>${setupStepper(current)}${body}</section>`;
163
+ return `<section class="section setup"><div><h2>${esc(title)}${step.beta ? ' <span class="tag-beta">Beta</span>' : ''}</h2><p class="lede">Set up Sidebud on ${esc(state.name)}. Keys, accounts, and agents stay on this computer.</p></div>${setupStepper(current)}${body}</section>`;
143
164
  }
144
165
  /** Handles setup buttons; returns false for actions it does not own. */
145
166
  async function handleSetupAction(action, button) {
@@ -161,16 +182,25 @@ async function handleSetupAction(action, button) {
161
182
  const live = state.voice.catalog[provider];
162
183
  const endpoint = voiceEndpoint();
163
184
  if (live?.needsWorkspaceId && !endpoint.workspaceId) throw new Error(`Add your ${live.label} workspace ID.`);
185
+ const phrases = commandPhrases();
186
+ if (phrases.error) throw new Error(phrases.error);
164
187
  const typedKey = document.querySelector(`[data-voice-key="${provider}"]`)?.value;
165
188
  if (typedKey) await api('voice-key', { provider, value: typedKey });
166
189
  else if (!state.voice.keyStates[provider]?.isSet) throw new Error(`Add a ${live.label} API key first.`);
167
- // Keep the owner's other voice settings (phrases, call limits); setup changes only the agent.
168
- await api('voice', { voice: { ...state.voice.voice, provider, model: $('#voice-model').value || null, voiceName: $('#voice-name').value || null, ...endpoint } });
190
+ // Keep the owner's other voice settings (call limits, auto-mute); setup changes the agent and the spoken commands.
191
+ await api('voice', { voice: { ...state.voice.voice, provider, model: $('#voice-model').value || null, voiceName: $('#voice-name').value || null, ...endpoint, commands: phrases.commands } });
169
192
  await reload();
170
193
  if (await applyChanges()) {
171
194
  await api('setup/check', { kind: 'voice' });
172
195
  await reload();
173
196
  }
197
+ } else if (action === 'setup-save-commands') {
198
+ // Saved on their own too, so they need no voice key and survive skipping the voice test.
199
+ const phrases = commandPhrases();
200
+ if (phrases.error) throw new Error(phrases.error);
201
+ await api('voice', { voice: { ...state.voice.voice, commands: phrases.commands } });
202
+ notice('Spoken commands saved.');
203
+ await reload();
174
204
  } else if (action === 'setup-add-integrations') {
175
205
  const chosen = [...document.querySelectorAll('[data-integration]')].filter((box) => box.checked && !box.disabled).map((box) => box.dataset.integration);
176
206
  const failures = [];
@@ -178,7 +208,7 @@ async function handleSetupAction(action, button) {
178
208
  // Each pack installs disabled and starts the execution agent on its setup, the same as from MCP & widgets.
179
209
  for (const id of chosen) {
180
210
  try {
181
- const result = await api('pack/install-bundled', { service: id });
211
+ const result = await api('pack/install-library', { id });
182
212
  if (!result.setup.started) failures.push(`${result.name}: ${result.setup.note}`);
183
213
  } catch (error) {
184
214
  failures.push(`${state.integrationCatalog.find((entry) => entry.id === id)?.name || id}: ${error.message}`);
@@ -190,6 +220,36 @@ async function handleSetupAction(action, button) {
190
220
  if (failures.length) notice(`Added, but some need attention: ${failures.join(' ')}`, true);
191
221
  else if (chosen.length) notice(`Added. Your execution agent is setting up ${chosen.length === 1 ? 'the integration' : 'the integrations'} and reports on your phone when it needs you.`);
192
222
  next();
223
+ } else if (action === 'setup-save-agents') {
224
+ const picked = document.querySelector('input[name="default-agents"]:checked')?.value;
225
+ if (!picked) throw new Error('Choose the agents app with your main work agents, or skip for now.');
226
+ await api('default-agents', { packId: picked });
227
+ await api('setup/ack', { step: 'agents' });
228
+ await reload();
229
+ next();
230
+ } else if (action === 'setup-ack-agents') {
231
+ await api('setup/ack', { step: 'agents' });
232
+ await reload();
233
+ next();
234
+ } else if (action === 'setup-skip-decisions') {
235
+ // Optional while in beta: skipping counts as done, so it never holds up finishing setup.
236
+ await api('setup/ack', { step: 'decisions' });
237
+ await reload();
238
+ next();
239
+ } else if (action === 'setup-save-decisions') {
240
+ const key = $('#setup-jev-key').value.trim();
241
+ const url = $('#setup-jev-url').value.trim();
242
+ if (!key && !url && !state.jev.key.isSet) throw new Error('Paste your TypeSafe key, or add a local server, or skip for now.');
243
+ if (key) await api('jev-key', { value: key });
244
+ if (url !== (state.jev.config.url ?? '') || state.jev.config.mode !== 'auto') await api('jev', { jev: { ...state.jev.config, mode: 'auto', url: url || null } });
245
+ await reload();
246
+ // Keys and servers apply on restart; the test then runs against the new setting.
247
+ if (state.restart.pending && !(await applyChanges())) return true;
248
+ const result = await api('jev/test', {});
249
+ await api('setup/ack', { step: 'decisions' });
250
+ await reload();
251
+ notice(`Fast decisions work: ${result.model} answered in ${result.ms} ms.`);
252
+ next();
193
253
  } else if (action === 'setup-complete') {
194
254
  await api('setup/complete', {});
195
255
  history.pushState(null, '', '#setup/done');
@@ -1,8 +0,0 @@
1
- # Integration logos
2
-
3
- The Gmail and Google Calendar `logo.png` files identify their respective integrations. They are rendered at 192 px from Google's official product assets, retrieved 2026-09-23:
4
-
5
- - https://www.gstatic.com/images/branding/productlogos/gmail_2026/v2/web/192px.svg
6
- - https://www.gstatic.com/images/branding/productlogos/calendar_2026/v2/web/192px.svg
7
-
8
- Google retains its trademarks and rights in these logos; they are not covered by this repository's MIT license. Their use does not imply Google endorsement. The companion serves the small raster files by content hash over the paired connection; the mobile widget JSON contains only that hash.
Binary file
@@ -1,127 +0,0 @@
1
- {
2
- "schemaVersion": 1,
3
- "id": "dev.remotevoiceagent.calendar",
4
- "name": "Calendar",
5
- "description": "Upcoming events and calendar availability. Read-only Google API access stays on your computer.",
6
- "version": "1.0.2",
7
- "author": {
8
- "name": "Sidebud"
9
- },
10
- "license": "MIT",
11
- "logo": {
12
- "kind": "file",
13
- "path": "logo.png"
14
- },
15
- "tile": {
16
- "size": "medium"
17
- },
18
- "mcp": {
19
- "transport": "builtin",
20
- "service": "calendar"
21
- },
22
- "auth": {
23
- "type": "oauth2",
24
- "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
25
- "tokenUrl": "https://oauth2.googleapis.com/token",
26
- "scopes": [
27
- "https://www.googleapis.com/auth/calendar.calendarlist.readonly",
28
- "https://www.googleapis.com/auth/calendar.events.readonly",
29
- "https://www.googleapis.com/auth/calendar.events.freebusy"
30
- ],
31
- "clientIdSecret": "GOOGLE_OAUTH_CLIENT_ID",
32
- "clientSecretSecret": "GOOGLE_OAUTH_CLIENT_SECRET",
33
- "authorizationParams": {
34
- "access_type": "offline",
35
- "prompt": "consent"
36
- }
37
- },
38
- "secrets": [
39
- {
40
- "name": "GOOGLE_OAUTH_CLIENT_ID",
41
- "description": "Desktop OAuth client ID with Calendar API enabled"
42
- },
43
- {
44
- "name": "GOOGLE_OAUTH_CLIENT_SECRET",
45
- "description": "Desktop OAuth client secret"
46
- }
47
- ],
48
- "permissions": {
49
- "tools": [
50
- {
51
- "name": "list_calendars",
52
- "access": "read",
53
- "description": "List calendars"
54
- },
55
- {
56
- "name": "list_events",
57
- "access": "read",
58
- "description": "Read upcoming events"
59
- },
60
- {
61
- "name": "get_event",
62
- "access": "read",
63
- "description": "Read one event"
64
- },
65
- {
66
- "name": "get_free_busy",
67
- "access": "read",
68
- "description": "Read availability"
69
- }
70
- ],
71
- "network": [
72
- "www.googleapis.com",
73
- "accounts.google.com",
74
- "oauth2.googleapis.com"
75
- ]
76
- },
77
- "settings": [
78
- {
79
- "id": "calendarId",
80
- "label": "Calendar ID",
81
- "type": "text",
82
- "default": "primary",
83
- "description": "Use primary for your main calendar."
84
- }
85
- ],
86
- "data": [
87
- {
88
- "id": "upcoming",
89
- "tool": "list_events",
90
- "arguments": {
91
- "calendarId": "{{setting.calendarId}}",
92
- "pageSize": 20
93
- },
94
- "refreshSeconds": 60
95
- }
96
- ],
97
- "glance": {
98
- "stats": [
99
- {
100
- "label": "Next 7 days",
101
- "value": "{{upcoming/events|count}}",
102
- "tone": "info"
103
- }
104
- ]
105
- },
106
- "blocks": [
107
- {
108
- "type": "list",
109
- "items": "{{upcoming/events}}",
110
- "item": {
111
- "title": "{{item/summary}}",
112
- "subtitle": "{{item/startsAt}}"
113
- },
114
- "limit": 10,
115
- "emptyText": "No upcoming events."
116
- }
117
- ],
118
- "controls": [
119
- {
120
- "id": "agenda",
121
- "kind": "prompt",
122
- "label": "Read my agenda",
123
- "prompt": "Summarize my upcoming calendar events.",
124
- "default": true
125
- }
126
- ]
127
- }
@@ -1,107 +0,0 @@
1
- {
2
- "$schema": "../../../../docs/schemas/pack-v2.schema.json",
3
- "schemaVersion": 2,
4
- "id": "dev.remotevoiceagent.discord",
5
- "name": "Discord",
6
- "description": "A Discord channel through a bot (barryyip0625/mcp-discord). Bots see only servers they are in and DMs sent to the bot, never your personal DMs. Read-only here.",
7
- "version": "1.1.0",
8
- "author": {
9
- "name": "Sidebud"
10
- },
11
- "license": "MIT",
12
- "logo": {
13
- "kind": "icon",
14
- "name": "chat"
15
- },
16
- "tile": {
17
- "size": "small"
18
- },
19
- "mcp": {
20
- "transport": "stdio",
21
- "command": "npx",
22
- "args": [
23
- "-y",
24
- "mcp-discord@1.5.1"
25
- ],
26
- "env": {
27
- "DISCORD_TOKEN": "{{secret.DISCORD_TOKEN}}"
28
- }
29
- },
30
- "secrets": [
31
- {
32
- "name": "DISCORD_TOKEN",
33
- "description": "A bot token from the Discord Developer Portal (never a user token: that breaks Discord's terms)"
34
- }
35
- ],
36
- "permissions": {
37
- "tools": [
38
- {
39
- "name": "discord_read_messages",
40
- "access": "read",
41
- "description": "Messages in a channel",
42
- "argumentSettings": {
43
- "channelId": "channelId"
44
- }
45
- }
46
- ]
47
- },
48
- "settings": [
49
- {
50
- "id": "channelId",
51
- "label": "Channel id",
52
- "type": "text",
53
- "default": "",
54
- "description": "The channel (or bot DM channel) to watch; enable Developer Mode in Discord and copy the channel id.",
55
- "required": true
56
- }
57
- ],
58
- "data": [
59
- {
60
- "id": "recent",
61
- "tool": "discord_read_messages",
62
- "arguments": {
63
- "channelId": "{{setting.channelId}}",
64
- "limit": 20
65
- },
66
- "extract": "text-json",
67
- "refreshSeconds": 30
68
- }
69
- ],
70
- "glance": {
71
- "line": "{{recent/messages/0/author/username|default:No messages}}: {{recent/messages/0/content|truncate:50}}"
72
- },
73
- "blocks": [
74
- {
75
- "type": "list",
76
- "items": "{{recent/messages}}",
77
- "item": {
78
- "title": "{{item/author/username}}",
79
- "subtitle": "{{item/content|truncate:120}}"
80
- },
81
- "limit": 8,
82
- "emptyText": "No messages"
83
- }
84
- ],
85
- "controls": [
86
- {
87
- "id": "summarize",
88
- "kind": "prompt",
89
- "label": "Summarize the channel",
90
- "prompt": "Summarize the recent messages in my Discord channel.",
91
- "default": true
92
- }
93
- ],
94
- "alerts": [
95
- {
96
- "id": "incoming",
97
- "items": "{{recent/messages}}",
98
- "key": "{{item/id}}",
99
- "sender": [
100
- "{{item/author/username}}",
101
- "{{item/author/id}}"
102
- ],
103
- "text": "{{item/content}}",
104
- "fromMe": "{{item/author/bot}}"
105
- }
106
- ]
107
- }
@@ -1,84 +0,0 @@
1
- {
2
- "$schema": "../../../../docs/schemas/pack-v2.schema.json",
3
- "schemaVersion": 2,
4
- "id": "app.sidebud.filesystem",
5
- "name": "Filesystem",
6
- "description": "Read and search one explicitly selected folder on this computer. This baseline exposes no write tools.",
7
- "version": "1.0.0",
8
- "author": {
9
- "name": "Sidebud"
10
- },
11
- "license": "MIT",
12
- "logo": {
13
- "kind": "emoji",
14
- "value": "\ud83d\udcc1"
15
- },
16
- "controls": [
17
- {
18
- "id": "summarize",
19
- "kind": "prompt",
20
- "label": "Summarize",
21
- "prompt": "Summarize the information available through my Filesystem integration.",
22
- "default": true
23
- }
24
- ],
25
- "mcp": {
26
- "transport": "stdio",
27
- "command": "npx",
28
- "args": [
29
- "-y",
30
- "@modelcontextprotocol/server-filesystem@2026.8.31",
31
- "{{setting.folder}}"
32
- ]
33
- },
34
- "settings": [
35
- {
36
- "id": "folder",
37
- "label": "Allowed folder",
38
- "type": "text",
39
- "required": true,
40
- "format": "directory",
41
- "description": "An existing absolute folder. Select the smallest folder needed; do not select your home or disk root."
42
- }
43
- ],
44
- "permissions": {
45
- "tools": [
46
- {
47
- "name": "read_text_file",
48
- "access": "read"
49
- },
50
- {
51
- "name": "list_directory",
52
- "access": "read"
53
- },
54
- {
55
- "name": "search_files",
56
- "access": "read"
57
- },
58
- {
59
- "name": "get_file_info",
60
- "access": "read"
61
- }
62
- ]
63
- },
64
- "data": [
65
- {
66
- "id": "files",
67
- "tool": "list_directory",
68
- "arguments": {
69
- "path": "{{setting.folder}}"
70
- },
71
- "extract": "text",
72
- "refreshSeconds": 300
73
- }
74
- ],
75
- "glance": {
76
- "line": "{{setting.folder}}"
77
- },
78
- "blocks": [
79
- {
80
- "type": "text",
81
- "text": "{{files|truncate:550}}"
82
- }
83
- ]
84
- }