@vellumai/assistant 0.4.37 → 0.4.41
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.
- package/ARCHITECTURE.md +3 -3
- package/README.md +13 -13
- package/bun.lock +80 -24
- package/docs/architecture/integrations.md +126 -128
- package/docs/runbook-trusted-contacts.md +1 -1
- package/docs/trusted-contact-access.md +12 -12
- package/package.json +3 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
- package/src/__tests__/app-bundler.test.ts +209 -0
- package/src/__tests__/app-compiler.test.ts +279 -0
- package/src/__tests__/app-executors.test.ts +293 -483
- package/src/__tests__/app-migration.test.ts +148 -0
- package/src/__tests__/app-routes-csp.test.ts +202 -0
- package/src/__tests__/avatar-e2e.test.ts +452 -0
- package/src/__tests__/avatar-generator.test.ts +193 -0
- package/src/__tests__/avatar-router.test.ts +186 -0
- package/src/__tests__/browser-download-timeout.test.ts +28 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
- package/src/__tests__/call-domain.test.ts +3 -7
- package/src/__tests__/credential-security-e2e.test.ts +19 -12
- package/src/__tests__/credentials-cli.test.ts +30 -4
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/handlers-slack-config.test.ts +0 -72
- package/src/__tests__/handlers-telegram-config.test.ts +19 -12
- package/src/__tests__/handlers-twitter-config.test.ts +105 -48
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/integration-status.test.ts +15 -5
- package/src/__tests__/integrations-cli.test.ts +1 -1
- package/src/__tests__/invite-redemption-service.test.ts +62 -7
- package/src/__tests__/ipc-snapshot.test.ts +0 -8
- package/src/__tests__/managed-avatar-client.test.ts +280 -0
- package/src/__tests__/mcp-cli.test.ts +3 -3
- package/src/__tests__/oauth-cli.test.ts +203 -0
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/secret-onetime-send.test.ts +19 -12
- package/src/__tests__/secure-keys.test.ts +78 -0
- package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
- package/src/__tests__/slack-channel-config.test.ts +23 -16
- package/src/__tests__/slack-share-routes.test.ts +263 -0
- package/src/__tests__/sms-messaging-provider.test.ts +3 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/__tests__/twilio-config.test.ts +15 -36
- package/src/__tests__/twilio-provider.test.ts +4 -0
- package/src/__tests__/twitter-auth-handler.test.ts +27 -14
- package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
- package/src/__tests__/twitter-cli-routing.test.ts +38 -53
- package/src/__tests__/twitter-oauth-client.test.ts +18 -47
- package/src/__tests__/voice-invite-redemption.test.ts +27 -3
- package/src/amazon/cart.ts +1 -1
- package/src/amazon/client.ts +89 -7
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/bundler/app-bundler.ts +77 -32
- package/src/bundler/app-compiler.ts +195 -0
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/package-resolver.ts +185 -0
- package/src/calls/call-domain.ts +4 -14
- package/src/calls/relay-server.ts +2 -2
- package/src/calls/twilio-config.ts +5 -24
- package/src/calls/twilio-rest.ts +19 -5
- package/src/cli/amazon.ts +74 -249
- package/src/cli/audit.ts +2 -2
- package/src/cli/autonomy.ts +9 -9
- package/src/cli/channels.ts +5 -5
- package/src/cli/completions.ts +27 -27
- package/src/cli/config.ts +14 -14
- package/src/cli/contacts.ts +27 -27
- package/src/cli/credentials.ts +28 -28
- package/src/cli/dev.ts +2 -2
- package/src/cli/doctor.ts +2 -2
- package/src/cli/email.ts +82 -82
- package/src/cli/influencer.ts +13 -13
- package/src/cli/integrations.ts +19 -144
- package/src/cli/keys.ts +10 -10
- package/src/cli/map.ts +4 -4
- package/src/cli/mcp.ts +17 -17
- package/src/cli/memory.ts +18 -18
- package/src/cli/notifications.ts +13 -13
- package/src/cli/oauth.ts +77 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/sequence.ts +27 -27
- package/src/cli/sessions.ts +12 -12
- package/src/cli/trust.ts +8 -8
- package/src/cli/twitter.ts +124 -70
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
- package/src/config/bundled-skills/amazon/SKILL.md +54 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
- package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
- package/src/config/bundled-skills/contacts/SKILL.md +12 -12
- package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
- package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/influencer/SKILL.md +13 -13
- package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
- package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
- package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
- package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
- package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
- package/src/config/bundled-skills/twitter/SKILL.md +68 -44
- package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
- package/src/config/core-schema.ts +26 -0
- package/src/config/env.ts +4 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/schema.ts +8 -0
- package/src/config/system-prompt.ts +6 -3
- package/src/config/templates/BOOTSTRAP.md +7 -5
- package/src/contacts/contacts-write.ts +5 -1
- package/src/daemon/handlers/apps.ts +31 -4
- package/src/daemon/handlers/config-ingress.ts +3 -3
- package/src/daemon/handlers/config-integrations.ts +120 -49
- package/src/daemon/handlers/config-slack-channel.ts +26 -7
- package/src/daemon/handlers/config-slack.ts +1 -54
- package/src/daemon/handlers/config-telegram.ts +28 -10
- package/src/daemon/handlers/config.ts +1 -4
- package/src/daemon/handlers/twitter-auth.ts +11 -4
- package/src/daemon/ipc-contract/apps.ts +0 -13
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/session-messaging.ts +2 -2
- package/src/daemon/tool-side-effects.ts +30 -0
- package/src/email/providers/agentmail.ts +1 -1
- package/src/email/providers/index.ts +1 -1
- package/src/email/service.ts +1 -1
- package/src/gallery/default-gallery.ts +538 -0
- package/src/gallery/gallery-manifest.ts +5 -1
- package/src/influencer/client.ts +8 -6
- package/src/mcp/client.ts +1 -1
- package/src/media/avatar-router.ts +99 -0
- package/src/media/avatar-types.ts +60 -0
- package/src/media/managed-avatar-client.ts +189 -0
- package/src/memory/app-migration.ts +114 -0
- package/src/memory/app-store.ts +11 -0
- package/src/memory/qdrant-client.ts +1 -1
- package/src/messaging/providers/slack/client.ts +12 -2
- package/src/messaging/providers/sms/adapter.ts +6 -10
- package/src/migrations/data-layout.ts +8 -1
- package/src/oauth/token-persistence.ts +9 -6
- package/src/runtime/assistant-scope.ts +5 -0
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-readiness-service.ts +9 -4
- package/src/runtime/gateway-internal-client.ts +11 -3
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +23 -13
- package/src/runtime/middleware/twilio-validation.ts +2 -2
- package/src/runtime/routes/app-routes.ts +131 -3
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
- package/src/runtime/routes/integration-routes.ts +2 -2
- package/src/runtime/routes/slack-share-routes.ts +235 -0
- package/src/runtime/routes/twilio-routes.ts +47 -34
- package/src/schedule/integration-status.ts +2 -3
- package/src/security/token-manager.ts +11 -3
- package/src/tools/apps/executors.ts +116 -8
- package/src/tools/browser/browser-manager.ts +30 -2
- package/src/tools/browser/chrome-cdp.ts +31 -3
- package/src/tools/credentials/vault.ts +9 -7
- package/src/tools/executor.ts +4 -0
- package/src/tools/system/avatar-generator.ts +55 -34
- package/src/twitter/client.ts +1 -1
- package/src/twitter/oauth-client.ts +31 -43
- package/src/twitter/router.ts +25 -23
- package/src/util/platform.ts +5 -0
- package/src/slack/slack-webhook.ts +0 -66
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
+
import { compileApp } from "../bundler/app-compiler.js";
|
|
12
13
|
import { generateAppIcon } from "../media/app-icon-generator.js";
|
|
14
|
+
import { getApp, getAppsDir, isMultifileApp } from "../memory/app-store.js";
|
|
13
15
|
import { updatePublishedAppDeployment } from "../services/published-app-updater.js";
|
|
14
16
|
import type { ToolExecutionResult } from "../tools/types.js";
|
|
15
17
|
import { getLogger } from "../util/logger.js";
|
|
@@ -44,6 +46,34 @@ function handleAppChange(
|
|
|
44
46
|
broadcastToAllClients: ((msg: ServerMessage) => void) | undefined,
|
|
45
47
|
opts?: { fileChange?: boolean; status?: string },
|
|
46
48
|
): void {
|
|
49
|
+
const app = getApp(appId);
|
|
50
|
+
|
|
51
|
+
// Multifile apps need a recompile before refreshing surfaces so the
|
|
52
|
+
// WebView picks up the latest compiled output.
|
|
53
|
+
if (app && isMultifileApp(app)) {
|
|
54
|
+
const appDir = join(getAppsDir(), appId);
|
|
55
|
+
void compileApp(appDir)
|
|
56
|
+
.then((result) => {
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
log.warn(
|
|
59
|
+
{ appId, errors: result.errors },
|
|
60
|
+
"Recompile failed on app change, serving stale dist/",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
refreshSurfacesForApp(ctx, appId, opts);
|
|
64
|
+
broadcastToAllClients?.({ type: "app_files_changed", appId });
|
|
65
|
+
void updatePublishedAppDeployment(appId);
|
|
66
|
+
})
|
|
67
|
+
.catch((err) => {
|
|
68
|
+
log.warn({ appId, err }, "Recompile threw on app change");
|
|
69
|
+
// Still refresh surfaces with stale output
|
|
70
|
+
refreshSurfacesForApp(ctx, appId, opts);
|
|
71
|
+
broadcastToAllClients?.({ type: "app_files_changed", appId });
|
|
72
|
+
void updatePublishedAppDeployment(appId);
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
47
77
|
refreshSurfacesForApp(ctx, appId, opts);
|
|
48
78
|
broadcastToAllClients?.({ type: "app_files_changed", appId });
|
|
49
79
|
void updatePublishedAppDeployment(appId);
|
|
@@ -236,7 +236,7 @@ export class AgentMailProvider implements EmailProvider {
|
|
|
236
236
|
return inboxes.inboxes[0].inboxId;
|
|
237
237
|
}
|
|
238
238
|
throw new ConfigError(
|
|
239
|
-
"No inboxes found. Run:
|
|
239
|
+
"No inboxes found. Run: assistant email setup inboxes --domain <domain>",
|
|
240
240
|
);
|
|
241
241
|
}
|
|
242
242
|
}
|
|
@@ -51,7 +51,7 @@ export async function createProvider(
|
|
|
51
51
|
}
|
|
52
52
|
if (!apiKey) {
|
|
53
53
|
throw new ConfigError(
|
|
54
|
-
"No AgentMail API key configured. Run:
|
|
54
|
+
"No AgentMail API key configured. Run: assistant keys set agentmail <key>",
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
const { AgentMailClient } = await import("agentmail");
|
package/src/email/service.ts
CHANGED
|
@@ -227,7 +227,7 @@ export class EmailService {
|
|
|
227
227
|
const health = await p.health();
|
|
228
228
|
if (health.inboxes.length === 0) {
|
|
229
229
|
throw new ConfigError(
|
|
230
|
-
"No inboxes found. Run:
|
|
230
|
+
"No inboxes found. Run: assistant email setup inboxes --domain <domain>",
|
|
231
231
|
);
|
|
232
232
|
}
|
|
233
233
|
const inbox = health.inboxes.find(
|
|
@@ -752,6 +752,540 @@ const expenseTrackerHtml = `<!DOCTYPE html>
|
|
|
752
752
|
</body>
|
|
753
753
|
</html>`;
|
|
754
754
|
|
|
755
|
+
// -- Multi-file source files for Focus Timer (formatVersion 2) --
|
|
756
|
+
|
|
757
|
+
const focusTimerSourceFiles: Record<string, string> = {
|
|
758
|
+
"src/index.html": `<!DOCTYPE html>
|
|
759
|
+
<html lang="en">
|
|
760
|
+
<head>
|
|
761
|
+
<meta charset="UTF-8">
|
|
762
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
763
|
+
<title>Focus Timer</title>
|
|
764
|
+
</head>
|
|
765
|
+
<body>
|
|
766
|
+
<div id="app"></div>
|
|
767
|
+
</body>
|
|
768
|
+
</html>`,
|
|
769
|
+
|
|
770
|
+
"src/main.tsx": `import { render } from "preact";
|
|
771
|
+
import { Timer } from "./components/Timer.js";
|
|
772
|
+
import "./styles.css";
|
|
773
|
+
|
|
774
|
+
function App() {
|
|
775
|
+
return <Timer workMinutes={25} breakMinutes={5} />;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
render(<App />, document.getElementById("app")!);
|
|
779
|
+
`,
|
|
780
|
+
|
|
781
|
+
"src/components/Timer.tsx": `import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
|
782
|
+
|
|
783
|
+
interface TimerProps {
|
|
784
|
+
workMinutes: number;
|
|
785
|
+
breakMinutes: number;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export function Timer({ workMinutes, breakMinutes }: TimerProps) {
|
|
789
|
+
const [secondsLeft, setSecondsLeft] = useState(workMinutes * 60);
|
|
790
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
791
|
+
const [isBreak, setIsBreak] = useState(false);
|
|
792
|
+
const [sessions, setSessions] = useState(0);
|
|
793
|
+
const [totalMinutes, setTotalMinutes] = useState(0);
|
|
794
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
795
|
+
|
|
796
|
+
const clearTimer = useCallback(() => {
|
|
797
|
+
if (intervalRef.current !== null) {
|
|
798
|
+
clearInterval(intervalRef.current);
|
|
799
|
+
intervalRef.current = null;
|
|
800
|
+
}
|
|
801
|
+
}, []);
|
|
802
|
+
|
|
803
|
+
// Tick effect: runs when the timer is active
|
|
804
|
+
useEffect(() => {
|
|
805
|
+
if (!isRunning) return;
|
|
806
|
+
|
|
807
|
+
intervalRef.current = setInterval(() => {
|
|
808
|
+
setSecondsLeft((prev) => {
|
|
809
|
+
if (prev <= 1) {
|
|
810
|
+
clearTimer();
|
|
811
|
+
setIsRunning(false);
|
|
812
|
+
if (!isBreak) {
|
|
813
|
+
setSessions((s) => s + 1);
|
|
814
|
+
setTotalMinutes((t) => t + workMinutes);
|
|
815
|
+
setIsBreak(true);
|
|
816
|
+
return breakMinutes * 60;
|
|
817
|
+
} else {
|
|
818
|
+
setIsBreak(false);
|
|
819
|
+
return workMinutes * 60;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return prev - 1;
|
|
823
|
+
});
|
|
824
|
+
}, 1000);
|
|
825
|
+
|
|
826
|
+
return clearTimer;
|
|
827
|
+
}, [isRunning, isBreak, workMinutes, breakMinutes, clearTimer]);
|
|
828
|
+
|
|
829
|
+
const toggle = () => setIsRunning((r) => !r);
|
|
830
|
+
|
|
831
|
+
const reset = () => {
|
|
832
|
+
clearTimer();
|
|
833
|
+
setIsRunning(false);
|
|
834
|
+
setIsBreak(false);
|
|
835
|
+
setSecondsLeft(workMinutes * 60);
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const minutes = Math.floor(secondsLeft / 60);
|
|
839
|
+
const seconds = secondsLeft % 60;
|
|
840
|
+
const display =
|
|
841
|
+
String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0");
|
|
842
|
+
|
|
843
|
+
return (
|
|
844
|
+
<div class="container">
|
|
845
|
+
<h1>Focus Timer</h1>
|
|
846
|
+
<div class={\`mode-label\${isBreak ? " break" : ""}\`}>
|
|
847
|
+
{isBreak ? "Break Time" : "Work Session"}
|
|
848
|
+
</div>
|
|
849
|
+
<div class="timer-display">{display}</div>
|
|
850
|
+
<div class="controls">
|
|
851
|
+
<button class="btn-primary" onClick={toggle}>
|
|
852
|
+
{isRunning ? "Pause" : "Start"}
|
|
853
|
+
</button>
|
|
854
|
+
<button class="btn-secondary" onClick={reset}>
|
|
855
|
+
Reset
|
|
856
|
+
</button>
|
|
857
|
+
</div>
|
|
858
|
+
<div class="stats">
|
|
859
|
+
<div class="stat-item">
|
|
860
|
+
<div class="stat-value">{sessions}</div>
|
|
861
|
+
<div class="stat-label">Sessions</div>
|
|
862
|
+
</div>
|
|
863
|
+
<div class="stat-item">
|
|
864
|
+
<div class="stat-value">{totalMinutes}</div>
|
|
865
|
+
<div class="stat-label">Minutes</div>
|
|
866
|
+
</div>
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
`,
|
|
872
|
+
|
|
873
|
+
"src/styles.css": `:root {
|
|
874
|
+
--bg: #1a1a2e;
|
|
875
|
+
--surface: #16213e;
|
|
876
|
+
--primary: #e94560;
|
|
877
|
+
--primary-hover: #c73e54;
|
|
878
|
+
--text: #eee;
|
|
879
|
+
--text-secondary: #aaa;
|
|
880
|
+
--break-color: #0f9b58;
|
|
881
|
+
--radius: 12px;
|
|
882
|
+
}
|
|
883
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
884
|
+
body {
|
|
885
|
+
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
|
886
|
+
background: var(--bg);
|
|
887
|
+
color: var(--text);
|
|
888
|
+
display: flex;
|
|
889
|
+
justify-content: center;
|
|
890
|
+
align-items: center;
|
|
891
|
+
min-height: 100vh;
|
|
892
|
+
padding: 20px;
|
|
893
|
+
}
|
|
894
|
+
.container {
|
|
895
|
+
text-align: center;
|
|
896
|
+
max-width: 400px;
|
|
897
|
+
width: 100%;
|
|
898
|
+
}
|
|
899
|
+
h1 {
|
|
900
|
+
font-size: 1.4rem;
|
|
901
|
+
font-weight: 600;
|
|
902
|
+
margin-bottom: 8px;
|
|
903
|
+
}
|
|
904
|
+
.mode-label {
|
|
905
|
+
font-size: 0.9rem;
|
|
906
|
+
color: var(--text-secondary);
|
|
907
|
+
margin-bottom: 32px;
|
|
908
|
+
text-transform: uppercase;
|
|
909
|
+
letter-spacing: 1px;
|
|
910
|
+
}
|
|
911
|
+
.mode-label.break { color: var(--break-color); }
|
|
912
|
+
.timer-display {
|
|
913
|
+
font-size: 5rem;
|
|
914
|
+
font-weight: 200;
|
|
915
|
+
font-variant-numeric: tabular-nums;
|
|
916
|
+
letter-spacing: 2px;
|
|
917
|
+
margin-bottom: 40px;
|
|
918
|
+
}
|
|
919
|
+
.controls {
|
|
920
|
+
display: flex;
|
|
921
|
+
gap: 12px;
|
|
922
|
+
justify-content: center;
|
|
923
|
+
margin-bottom: 32px;
|
|
924
|
+
}
|
|
925
|
+
button {
|
|
926
|
+
font-family: inherit;
|
|
927
|
+
font-size: 0.95rem;
|
|
928
|
+
font-weight: 500;
|
|
929
|
+
padding: 10px 28px;
|
|
930
|
+
border: none;
|
|
931
|
+
border-radius: var(--radius);
|
|
932
|
+
cursor: pointer;
|
|
933
|
+
transition: background 0.2s;
|
|
934
|
+
}
|
|
935
|
+
.btn-primary {
|
|
936
|
+
background: var(--primary);
|
|
937
|
+
color: white;
|
|
938
|
+
}
|
|
939
|
+
.btn-primary:hover { background: var(--primary-hover); }
|
|
940
|
+
.btn-secondary {
|
|
941
|
+
background: var(--surface);
|
|
942
|
+
color: var(--text);
|
|
943
|
+
border: 1px solid #333;
|
|
944
|
+
}
|
|
945
|
+
.btn-secondary:hover { background: #1e2d4f; }
|
|
946
|
+
.stats {
|
|
947
|
+
display: flex;
|
|
948
|
+
justify-content: center;
|
|
949
|
+
gap: 32px;
|
|
950
|
+
}
|
|
951
|
+
.stat-item {
|
|
952
|
+
text-align: center;
|
|
953
|
+
}
|
|
954
|
+
.stat-value {
|
|
955
|
+
font-size: 1.6rem;
|
|
956
|
+
font-weight: 600;
|
|
957
|
+
}
|
|
958
|
+
.stat-label {
|
|
959
|
+
font-size: 0.75rem;
|
|
960
|
+
color: var(--text-secondary);
|
|
961
|
+
text-transform: uppercase;
|
|
962
|
+
letter-spacing: 0.5px;
|
|
963
|
+
margin-top: 2px;
|
|
964
|
+
}
|
|
965
|
+
`,
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// -- Multi-file source files for Habit Tracker (formatVersion 2) --
|
|
969
|
+
|
|
970
|
+
const habitTrackerSourceFiles: Record<string, string> = {
|
|
971
|
+
"src/index.html": `<!DOCTYPE html>
|
|
972
|
+
<html lang="en">
|
|
973
|
+
<head>
|
|
974
|
+
<meta charset="UTF-8">
|
|
975
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
976
|
+
<title>Habit Tracker</title>
|
|
977
|
+
</head>
|
|
978
|
+
<body>
|
|
979
|
+
<div id="app"></div>
|
|
980
|
+
</body>
|
|
981
|
+
</html>`,
|
|
982
|
+
|
|
983
|
+
"src/main.tsx": `import { render } from "preact";
|
|
984
|
+
import { HabitTracker } from "./components/HabitTracker.js";
|
|
985
|
+
import "./styles.css";
|
|
986
|
+
|
|
987
|
+
render(<HabitTracker />, document.getElementById("app")!);
|
|
988
|
+
`,
|
|
989
|
+
|
|
990
|
+
"src/components/HabitTracker.tsx": `import { useCallback, useEffect, useState } from "preact/hooks";
|
|
991
|
+
import { HabitRow } from "./HabitRow.js";
|
|
992
|
+
|
|
993
|
+
declare const vellum: {
|
|
994
|
+
data: {
|
|
995
|
+
query(): Promise<Array<{ id: string; data: Record<string, string> }>>;
|
|
996
|
+
create(data: Record<string, string>): Promise<void>;
|
|
997
|
+
update(id: string, data: Record<string, string>): Promise<void>;
|
|
998
|
+
delete(id: string): Promise<void>;
|
|
999
|
+
};
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
function getDates(): string[] {
|
|
1003
|
+
const dates: string[] = [];
|
|
1004
|
+
for (let i = 6; i >= 0; i--) {
|
|
1005
|
+
const d = new Date();
|
|
1006
|
+
d.setDate(d.getDate() - i);
|
|
1007
|
+
dates.push(d.toISOString().slice(0, 10));
|
|
1008
|
+
}
|
|
1009
|
+
return dates;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function getDayNames(dates: string[]): string[] {
|
|
1013
|
+
const names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
1014
|
+
return dates.map((d) => names[new Date(d + "T12:00:00").getDay()]);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
interface HabitRecord {
|
|
1018
|
+
id: string;
|
|
1019
|
+
data: { name: string; completedDates: string };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export function HabitTracker() {
|
|
1023
|
+
const [habits, setHabits] = useState<HabitRecord[]>([]);
|
|
1024
|
+
const [input, setInput] = useState("");
|
|
1025
|
+
const dates = getDates();
|
|
1026
|
+
const dayNames = getDayNames(dates);
|
|
1027
|
+
|
|
1028
|
+
const loadHabits = useCallback(() => {
|
|
1029
|
+
vellum.data.query().then((records) => {
|
|
1030
|
+
setHabits(records as unknown as HabitRecord[]);
|
|
1031
|
+
});
|
|
1032
|
+
}, []);
|
|
1033
|
+
|
|
1034
|
+
useEffect(() => {
|
|
1035
|
+
loadHabits();
|
|
1036
|
+
}, [loadHabits]);
|
|
1037
|
+
|
|
1038
|
+
const addHabit = () => {
|
|
1039
|
+
const name = input.trim();
|
|
1040
|
+
if (!name) return;
|
|
1041
|
+
setInput("");
|
|
1042
|
+
vellum.data.create({ name, completedDates: "[]" }).then(loadHabits);
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const toggleDate = (recordId: string, date: string) => {
|
|
1046
|
+
const record = habits.find((h) => h.id === recordId);
|
|
1047
|
+
if (!record) return;
|
|
1048
|
+
let completed: string[] = [];
|
|
1049
|
+
try {
|
|
1050
|
+
completed = JSON.parse(record.data.completedDates || "[]");
|
|
1051
|
+
} catch {
|
|
1052
|
+
// ignore parse errors
|
|
1053
|
+
}
|
|
1054
|
+
const idx = completed.indexOf(date);
|
|
1055
|
+
if (idx === -1) completed.push(date);
|
|
1056
|
+
else completed.splice(idx, 1);
|
|
1057
|
+
vellum.data
|
|
1058
|
+
.update(recordId, {
|
|
1059
|
+
name: record.data.name,
|
|
1060
|
+
completedDates: JSON.stringify(completed),
|
|
1061
|
+
})
|
|
1062
|
+
.then(loadHabits);
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
const deleteHabit = (recordId: string) => {
|
|
1066
|
+
vellum.data.delete(recordId).then(loadHabits);
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
return (
|
|
1070
|
+
<div>
|
|
1071
|
+
<div class="header">
|
|
1072
|
+
<h1>Habit Tracker</h1>
|
|
1073
|
+
</div>
|
|
1074
|
+
<div class="add-form">
|
|
1075
|
+
<input
|
|
1076
|
+
type="text"
|
|
1077
|
+
value={input}
|
|
1078
|
+
onInput={(e) => setInput((e.target as HTMLInputElement).value)}
|
|
1079
|
+
onKeyDown={(e) => e.key === "Enter" && addHabit()}
|
|
1080
|
+
placeholder="Add a new habit..."
|
|
1081
|
+
/>
|
|
1082
|
+
<button class="btn-primary" onClick={addHabit}>
|
|
1083
|
+
Add
|
|
1084
|
+
</button>
|
|
1085
|
+
</div>
|
|
1086
|
+
<div class="days-header">
|
|
1087
|
+
<div />
|
|
1088
|
+
{dayNames.map((name, i) => (
|
|
1089
|
+
<div key={i} class="day-label">
|
|
1090
|
+
{name}
|
|
1091
|
+
</div>
|
|
1092
|
+
))}
|
|
1093
|
+
</div>
|
|
1094
|
+
<div>
|
|
1095
|
+
{habits.length === 0 ? (
|
|
1096
|
+
<div class="empty-state">No habits yet. Add one above!</div>
|
|
1097
|
+
) : (
|
|
1098
|
+
habits.map((record) => (
|
|
1099
|
+
<HabitRow
|
|
1100
|
+
key={record.id}
|
|
1101
|
+
record={record}
|
|
1102
|
+
dates={dates}
|
|
1103
|
+
onToggle={toggleDate}
|
|
1104
|
+
onDelete={deleteHabit}
|
|
1105
|
+
/>
|
|
1106
|
+
))
|
|
1107
|
+
)}
|
|
1108
|
+
</div>
|
|
1109
|
+
</div>
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
`,
|
|
1113
|
+
|
|
1114
|
+
"src/components/HabitRow.tsx": `interface HabitRowProps {
|
|
1115
|
+
record: { id: string; data: { name: string; completedDates: string } };
|
|
1116
|
+
dates: string[];
|
|
1117
|
+
onToggle: (id: string, date: string) => void;
|
|
1118
|
+
onDelete: (id: string) => void;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export function HabitRow({ record, dates, onToggle, onDelete }: HabitRowProps) {
|
|
1122
|
+
let completed: string[] = [];
|
|
1123
|
+
try {
|
|
1124
|
+
completed = JSON.parse(record.data.completedDates || "[]");
|
|
1125
|
+
} catch {
|
|
1126
|
+
// ignore parse errors
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return (
|
|
1130
|
+
<div class="habit-row">
|
|
1131
|
+
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
|
1132
|
+
<span class="habit-name">{record.data.name}</span>
|
|
1133
|
+
<button class="delete-btn" onClick={() => onDelete(record.id)}>
|
|
1134
|
+
x
|
|
1135
|
+
</button>
|
|
1136
|
+
</div>
|
|
1137
|
+
{dates.map((date) => {
|
|
1138
|
+
const checked = completed.includes(date);
|
|
1139
|
+
return (
|
|
1140
|
+
<div key={date} class="check-cell">
|
|
1141
|
+
<button
|
|
1142
|
+
class={\`check-btn\${checked ? " checked" : ""}\`}
|
|
1143
|
+
onClick={() => onToggle(record.id, date)}
|
|
1144
|
+
>
|
|
1145
|
+
{checked ? "\\u2713" : ""}
|
|
1146
|
+
</button>
|
|
1147
|
+
</div>
|
|
1148
|
+
);
|
|
1149
|
+
})}
|
|
1150
|
+
</div>
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
`,
|
|
1154
|
+
|
|
1155
|
+
"src/styles.css": `:root {
|
|
1156
|
+
--bg: #0f172a;
|
|
1157
|
+
--surface: #1e293b;
|
|
1158
|
+
--surface-hover: #263348;
|
|
1159
|
+
--primary: #6366f1;
|
|
1160
|
+
--primary-hover: #5558e6;
|
|
1161
|
+
--success: #22c55e;
|
|
1162
|
+
--text: #f1f5f9;
|
|
1163
|
+
--text-secondary: #94a3b8;
|
|
1164
|
+
--border: #334155;
|
|
1165
|
+
--radius: 10px;
|
|
1166
|
+
}
|
|
1167
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1168
|
+
body {
|
|
1169
|
+
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
|
1170
|
+
background: var(--bg);
|
|
1171
|
+
color: var(--text);
|
|
1172
|
+
padding: 24px;
|
|
1173
|
+
min-height: 100vh;
|
|
1174
|
+
}
|
|
1175
|
+
.header {
|
|
1176
|
+
display: flex;
|
|
1177
|
+
justify-content: space-between;
|
|
1178
|
+
align-items: center;
|
|
1179
|
+
margin-bottom: 24px;
|
|
1180
|
+
}
|
|
1181
|
+
h1 { font-size: 1.4rem; font-weight: 600; }
|
|
1182
|
+
.add-form {
|
|
1183
|
+
display: flex;
|
|
1184
|
+
gap: 8px;
|
|
1185
|
+
margin-bottom: 24px;
|
|
1186
|
+
}
|
|
1187
|
+
.add-form input {
|
|
1188
|
+
flex: 1;
|
|
1189
|
+
padding: 10px 14px;
|
|
1190
|
+
background: var(--surface);
|
|
1191
|
+
border: 1px solid var(--border);
|
|
1192
|
+
border-radius: var(--radius);
|
|
1193
|
+
color: var(--text);
|
|
1194
|
+
font-family: inherit;
|
|
1195
|
+
font-size: 0.9rem;
|
|
1196
|
+
outline: none;
|
|
1197
|
+
}
|
|
1198
|
+
.add-form input:focus { border-color: var(--primary); }
|
|
1199
|
+
.add-form input::placeholder { color: var(--text-secondary); }
|
|
1200
|
+
button {
|
|
1201
|
+
font-family: inherit;
|
|
1202
|
+
font-size: 0.85rem;
|
|
1203
|
+
font-weight: 500;
|
|
1204
|
+
padding: 10px 18px;
|
|
1205
|
+
border: none;
|
|
1206
|
+
border-radius: var(--radius);
|
|
1207
|
+
cursor: pointer;
|
|
1208
|
+
transition: background 0.2s;
|
|
1209
|
+
}
|
|
1210
|
+
.btn-primary {
|
|
1211
|
+
background: var(--primary);
|
|
1212
|
+
color: white;
|
|
1213
|
+
}
|
|
1214
|
+
.btn-primary:hover { background: var(--primary-hover); }
|
|
1215
|
+
.days-header {
|
|
1216
|
+
display: grid;
|
|
1217
|
+
grid-template-columns: 1fr repeat(7, 40px);
|
|
1218
|
+
gap: 4px;
|
|
1219
|
+
margin-bottom: 8px;
|
|
1220
|
+
padding: 0 4px;
|
|
1221
|
+
}
|
|
1222
|
+
.day-label {
|
|
1223
|
+
text-align: center;
|
|
1224
|
+
font-size: 0.7rem;
|
|
1225
|
+
color: var(--text-secondary);
|
|
1226
|
+
text-transform: uppercase;
|
|
1227
|
+
}
|
|
1228
|
+
.habit-row {
|
|
1229
|
+
display: grid;
|
|
1230
|
+
grid-template-columns: 1fr repeat(7, 40px);
|
|
1231
|
+
gap: 4px;
|
|
1232
|
+
padding: 10px 4px;
|
|
1233
|
+
border-radius: var(--radius);
|
|
1234
|
+
margin-bottom: 4px;
|
|
1235
|
+
align-items: center;
|
|
1236
|
+
}
|
|
1237
|
+
.habit-row:hover { background: var(--surface); }
|
|
1238
|
+
.habit-name {
|
|
1239
|
+
font-size: 0.9rem;
|
|
1240
|
+
font-weight: 500;
|
|
1241
|
+
overflow: hidden;
|
|
1242
|
+
text-overflow: ellipsis;
|
|
1243
|
+
white-space: nowrap;
|
|
1244
|
+
}
|
|
1245
|
+
.check-cell {
|
|
1246
|
+
display: flex;
|
|
1247
|
+
justify-content: center;
|
|
1248
|
+
align-items: center;
|
|
1249
|
+
}
|
|
1250
|
+
.check-btn {
|
|
1251
|
+
width: 28px;
|
|
1252
|
+
height: 28px;
|
|
1253
|
+
border-radius: 6px;
|
|
1254
|
+
border: 2px solid var(--border);
|
|
1255
|
+
background: transparent;
|
|
1256
|
+
cursor: pointer;
|
|
1257
|
+
padding: 0;
|
|
1258
|
+
display: flex;
|
|
1259
|
+
align-items: center;
|
|
1260
|
+
justify-content: center;
|
|
1261
|
+
transition: all 0.15s;
|
|
1262
|
+
color: transparent;
|
|
1263
|
+
font-size: 14px;
|
|
1264
|
+
}
|
|
1265
|
+
.check-btn.checked {
|
|
1266
|
+
background: var(--success);
|
|
1267
|
+
border-color: var(--success);
|
|
1268
|
+
color: white;
|
|
1269
|
+
}
|
|
1270
|
+
.check-btn:hover { border-color: var(--success); }
|
|
1271
|
+
.delete-btn {
|
|
1272
|
+
background: transparent;
|
|
1273
|
+
color: var(--text-secondary);
|
|
1274
|
+
border: none;
|
|
1275
|
+
padding: 4px 8px;
|
|
1276
|
+
font-size: 0.8rem;
|
|
1277
|
+
cursor: pointer;
|
|
1278
|
+
border-radius: 4px;
|
|
1279
|
+
}
|
|
1280
|
+
.delete-btn:hover { color: #ef4444; background: rgba(239,68,68,0.1); }
|
|
1281
|
+
.empty-state {
|
|
1282
|
+
text-align: center;
|
|
1283
|
+
padding: 48px 0;
|
|
1284
|
+
color: var(--text-secondary);
|
|
1285
|
+
}
|
|
1286
|
+
`,
|
|
1287
|
+
};
|
|
1288
|
+
|
|
755
1289
|
export const defaultGallery: GalleryManifest = {
|
|
756
1290
|
version: 1,
|
|
757
1291
|
updatedAt: "2026-02-15T00:00:00Z",
|
|
@@ -776,6 +1310,8 @@ export const defaultGallery: GalleryManifest = {
|
|
|
776
1310
|
additionalProperties: false,
|
|
777
1311
|
}),
|
|
778
1312
|
htmlDefinition: focusTimerHtml,
|
|
1313
|
+
formatVersion: 2,
|
|
1314
|
+
sourceFiles: focusTimerSourceFiles,
|
|
779
1315
|
},
|
|
780
1316
|
{
|
|
781
1317
|
id: "gallery-habit-tracker",
|
|
@@ -795,6 +1331,8 @@ export const defaultGallery: GalleryManifest = {
|
|
|
795
1331
|
required: ["name", "completedDates"],
|
|
796
1332
|
}),
|
|
797
1333
|
htmlDefinition: habitTrackerHtml,
|
|
1334
|
+
formatVersion: 2,
|
|
1335
|
+
sourceFiles: habitTrackerSourceFiles,
|
|
798
1336
|
},
|
|
799
1337
|
{
|
|
800
1338
|
id: "gallery-expense-tracker",
|
|
@@ -20,5 +20,9 @@ export interface GalleryApp {
|
|
|
20
20
|
version: string; // e.g. "1.0.0"
|
|
21
21
|
featured?: boolean;
|
|
22
22
|
schemaJson: string; // JSON schema for app records
|
|
23
|
-
htmlDefinition: string; // Complete HTML app
|
|
23
|
+
htmlDefinition: string; // Complete HTML app (also serves as compiled fallback)
|
|
24
|
+
/** 2 = multi-file TSX format with sourceFiles */
|
|
25
|
+
formatVersion?: number;
|
|
26
|
+
/** Maps relative path to file content, e.g. { "src/main.tsx": "...", "src/index.html": "..." } */
|
|
27
|
+
sourceFiles?: Record<string, string>;
|
|
24
28
|
}
|
package/src/influencer/client.ts
CHANGED
|
@@ -50,7 +50,11 @@ import type {
|
|
|
50
50
|
ExtensionResponse,
|
|
51
51
|
} from "../browser-extension-relay/protocol.js";
|
|
52
52
|
import { extensionRelayServer } from "../browser-extension-relay/server.js";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
initAuthSigningKey,
|
|
55
|
+
isSigningKeyInitialized,
|
|
56
|
+
loadOrCreateSigningKey,
|
|
57
|
+
} from "../runtime/auth/token-service.js";
|
|
54
58
|
import { gatewayPost } from "../runtime/gateway-internal-client.js";
|
|
55
59
|
|
|
56
60
|
// ---------------------------------------------------------------------------
|
|
@@ -135,12 +139,10 @@ async function sendRelayCommand(
|
|
|
135
139
|
|
|
136
140
|
// Fall back to HTTP relay endpoint via the gateway.
|
|
137
141
|
// The gateway validates edge JWTs (aud=vellum-gateway) and mints an
|
|
138
|
-
// exchange token for the runtime.
|
|
139
|
-
//
|
|
142
|
+
// exchange token for the runtime. In CLI out-of-process contexts the
|
|
143
|
+
// signing key may not be initialized yet — load it from disk.
|
|
140
144
|
if (!isSigningKeyInitialized()) {
|
|
141
|
-
|
|
142
|
-
"Auth signing key not initialized — browser-relay commands require the daemon to be running",
|
|
143
|
-
);
|
|
145
|
+
initAuthSigningKey(loadOrCreateSigningKey());
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
const { data } = await gatewayPost<ExtensionResponse>(
|
package/src/mcp/client.ts
CHANGED
|
@@ -107,7 +107,7 @@ export class McpClient {
|
|
|
107
107
|
(err.code === 401 || err.code === 403));
|
|
108
108
|
|
|
109
109
|
if (isAuthError) {
|
|
110
|
-
// Auth-related — user can run `
|
|
110
|
+
// Auth-related — user can run `assistant mcp auth <name>` to authenticate.
|
|
111
111
|
log.info(
|
|
112
112
|
{ serverId: this.serverId, err },
|
|
113
113
|
"MCP server requires authentication",
|