@agent-native/core 0.7.54 → 0.7.56

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 (60) hide show
  1. package/dist/a2a/artifact-response.d.ts +1 -0
  2. package/dist/a2a/artifact-response.d.ts.map +1 -1
  3. package/dist/a2a/artifact-response.js +111 -13
  4. package/dist/a2a/artifact-response.js.map +1 -1
  5. package/dist/a2a/task-store.d.ts +1 -0
  6. package/dist/a2a/task-store.d.ts.map +1 -1
  7. package/dist/a2a/task-store.js +15 -0
  8. package/dist/a2a/task-store.js.map +1 -1
  9. package/dist/cli/templates-meta.js +1 -1
  10. package/dist/cli/templates-meta.js.map +1 -1
  11. package/dist/client/AssistantChat.d.ts +15 -0
  12. package/dist/client/AssistantChat.d.ts.map +1 -1
  13. package/dist/client/AssistantChat.js +55 -52
  14. package/dist/client/AssistantChat.js.map +1 -1
  15. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  16. package/dist/client/MultiTabAssistantChat.js +0 -13
  17. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  18. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  19. package/dist/client/composer/TiptapComposer.js +59 -19
  20. package/dist/client/composer/TiptapComposer.js.map +1 -1
  21. package/dist/client/composer/useVoiceDictation.d.ts +8 -3
  22. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
  23. package/dist/client/composer/useVoiceDictation.js +278 -22
  24. package/dist/client/composer/useVoiceDictation.js.map +1 -1
  25. package/dist/client/index.d.ts +1 -0
  26. package/dist/client/index.d.ts.map +1 -1
  27. package/dist/client/index.js +1 -0
  28. package/dist/client/index.js.map +1 -1
  29. package/dist/client/resources/ResourcesPanel.js +2 -2
  30. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  31. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  32. package/dist/client/settings/SettingsPanel.js +7 -5
  33. package/dist/client/settings/SettingsPanel.js.map +1 -1
  34. package/dist/client/settings/VoiceTranscriptionSection.d.ts +4 -2
  35. package/dist/client/settings/VoiceTranscriptionSection.d.ts.map +1 -1
  36. package/dist/client/settings/VoiceTranscriptionSection.js +164 -60
  37. package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -1
  38. package/dist/client/use-chat-models.d.ts +33 -0
  39. package/dist/client/use-chat-models.d.ts.map +1 -0
  40. package/dist/client/use-chat-models.js +183 -0
  41. package/dist/client/use-chat-models.js.map +1 -0
  42. package/dist/integrations/a2a-continuation-processor.js +29 -15
  43. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  44. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  45. package/dist/server/agent-chat-plugin.js +22 -1
  46. package/dist/server/agent-chat-plugin.js.map +1 -1
  47. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  48. package/dist/server/core-routes-plugin.js +6 -0
  49. package/dist/server/core-routes-plugin.js.map +1 -1
  50. package/dist/server/google-realtime-session.d.ts +14 -0
  51. package/dist/server/google-realtime-session.d.ts.map +1 -0
  52. package/dist/server/google-realtime-session.js +155 -0
  53. package/dist/server/google-realtime-session.js.map +1 -0
  54. package/dist/server/voice-providers-status.d.ts +7 -0
  55. package/dist/server/voice-providers-status.d.ts.map +1 -1
  56. package/dist/server/voice-providers-status.js +14 -1
  57. package/dist/server/voice-providers-status.js.map +1 -1
  58. package/docs/content/sharing.md +155 -0
  59. package/docs/content/template-clips.md +8 -5
  60. package/package.json +1 -1
@@ -0,0 +1,14 @@
1
+ interface GoogleRealtimeSessionResponse {
2
+ websocketUrl: string;
3
+ sessionToken: string;
4
+ websocketProtocol?: string;
5
+ }
6
+ export declare function resolveGoogleRealtimeCredentials(opts: {
7
+ userEmail?: string | null;
8
+ orgId?: string | null;
9
+ }): Promise<string | null>;
10
+ export declare function createGoogleRealtimeSessionHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<GoogleRealtimeSessionResponse | {
11
+ error: any;
12
+ }>>;
13
+ export {};
14
+ //# sourceMappingURL=google-realtime-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-realtime-session.d.ts","sourceRoot":"","sources":["../../src/server/google-realtime-session.ts"],"names":[],"mappings":"AAgBA,UAAU,6BAA6B;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAqCD,wBAAsB,gCAAgC,CAAC,IAAI,EAAE;IAC3D,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BzB;AAED,wBAAgB,kCAAkC;;IAqGjD"}
@@ -0,0 +1,155 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { defineEventHandler, getMethod, getRequestHeader, readBody, setResponseStatus, } from "h3";
3
+ import { readAppSecret } from "../secrets/storage.js";
4
+ import { resolveCredential } from "../credentials/index.js";
5
+ import { getSession } from "./auth.js";
6
+ import { getOrgContext } from "../org/context.js";
7
+ import { runWithRequestContext } from "./request-context.js";
8
+ import { resolveBuilderCredentials } from "./credential-provider.js";
9
+ function isSameOriginRequest(event) {
10
+ const host = getRequestHeader(event, "host");
11
+ const origin = getRequestHeader(event, "origin");
12
+ if (origin && host) {
13
+ try {
14
+ const parsed = new URL(origin);
15
+ if (parsed.host === host)
16
+ return true;
17
+ if (parsed.protocol === "tauri:" && parsed.hostname === "localhost") {
18
+ return true;
19
+ }
20
+ if ((parsed.protocol === "http:" || parsed.protocol === "https:") &&
21
+ parsed.hostname === "tauri.localhost" &&
22
+ (host.startsWith("localhost:") || host.startsWith("127.0.0.1:"))) {
23
+ return true;
24
+ }
25
+ if (parsed.protocol === "http:" &&
26
+ (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") &&
27
+ parsed.port === "1420" &&
28
+ (host.startsWith("localhost:") || host.startsWith("127.0.0.1:"))) {
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ const fetchSite = getRequestHeader(event, "sec-fetch-site");
38
+ if (fetchSite)
39
+ return fetchSite === "same-origin" || fetchSite === "none";
40
+ return true;
41
+ }
42
+ export async function resolveGoogleRealtimeCredentials(opts) {
43
+ if (opts.userEmail) {
44
+ const userSecret = await readAppSecret({
45
+ key: "GOOGLE_APPLICATION_CREDENTIALS",
46
+ scope: "user",
47
+ scopeId: opts.userEmail,
48
+ }).catch(() => null);
49
+ const fromUserSecret = userSecret?.value?.trim();
50
+ if (fromUserSecret)
51
+ return fromUserSecret;
52
+ }
53
+ const stored = await resolveCredential("GOOGLE_APPLICATION_CREDENTIALS", {
54
+ userEmail: opts.userEmail ?? undefined,
55
+ orgId: opts.orgId ?? undefined,
56
+ }).catch(() => undefined);
57
+ const fromSettings = stored?.trim();
58
+ if (fromSettings)
59
+ return fromSettings;
60
+ const envValue = process.env.GOOGLE_APPLICATION_CREDENTIALS?.trim();
61
+ if (!envValue)
62
+ return null;
63
+ if (envValue.startsWith("{"))
64
+ return envValue;
65
+ try {
66
+ const fileContents = await readFile(envValue, "utf8");
67
+ const trimmed = fileContents.trim();
68
+ return trimmed || null;
69
+ }
70
+ catch {
71
+ throw new Error("GOOGLE_APPLICATION_CREDENTIALS points to a file path the framework server could not read");
72
+ }
73
+ }
74
+ export function createGoogleRealtimeSessionHandler() {
75
+ return defineEventHandler(async (event) => {
76
+ if (getMethod(event) !== "POST") {
77
+ setResponseStatus(event, 405);
78
+ return { error: "Method not allowed" };
79
+ }
80
+ if (!isSameOriginRequest(event)) {
81
+ setResponseStatus(event, 403);
82
+ return { error: "Cross-origin request rejected" };
83
+ }
84
+ const session = await getSession(event).catch(() => null);
85
+ if (!session?.email) {
86
+ setResponseStatus(event, 401);
87
+ return { error: "Authentication required" };
88
+ }
89
+ const orgCtx = await getOrgContext(event).catch(() => null);
90
+ const requestContext = {
91
+ userEmail: session.email,
92
+ orgId: orgCtx?.orgId ?? undefined,
93
+ };
94
+ return runWithRequestContext(requestContext, async () => {
95
+ const googleApplicationCredentials = await resolveGoogleRealtimeCredentials({
96
+ userEmail: session.email,
97
+ orgId: orgCtx?.orgId ?? undefined,
98
+ });
99
+ if (!googleApplicationCredentials) {
100
+ setResponseStatus(event, 400);
101
+ return {
102
+ error: "Configure GOOGLE_APPLICATION_CREDENTIALS in Settings to use Google realtime transcription.",
103
+ };
104
+ }
105
+ const builderCreds = await resolveBuilderCredentials();
106
+ if (!builderCreds.privateKey || !builderCreds.publicKey) {
107
+ setResponseStatus(event, 400);
108
+ return {
109
+ error: "Builder must be connected to mint a managed realtime transcription session.",
110
+ };
111
+ }
112
+ const apiHost = process.env.BUILDER_API_HOST || "https://ai-services.builder.io";
113
+ const body = ((await readBody(event).catch(() => ({}))) || {});
114
+ const res = await fetch(`${apiHost}/agent-native/transcribe-stream/session`, {
115
+ method: "POST",
116
+ headers: {
117
+ "Content-Type": "application/json",
118
+ Authorization: `Bearer ${builderCreds.privateKey}`,
119
+ "x-builder-api-key": builderCreds.publicKey,
120
+ ...(builderCreds.userId
121
+ ? { "x-builder-user-id": builderCreds.userId }
122
+ : {}),
123
+ },
124
+ body: JSON.stringify({
125
+ googleApplicationCredentials,
126
+ language: typeof body?.language === "string"
127
+ ? body.language.trim()
128
+ : undefined,
129
+ }),
130
+ }).catch((err) => {
131
+ throw new Error(err?.message || "Failed to reach realtime transcription service");
132
+ });
133
+ if (!res.ok) {
134
+ const errorBody = await res
135
+ .json()
136
+ .catch(() => ({ error: `HTTP ${res.status}` }));
137
+ setResponseStatus(event, res.status);
138
+ return {
139
+ error: typeof errorBody?.error === "string"
140
+ ? errorBody.error
141
+ : `Realtime session failed (${res.status})`,
142
+ };
143
+ }
144
+ const payload = (await res.json());
145
+ if (!payload?.websocketUrl) {
146
+ setResponseStatus(event, 502);
147
+ return {
148
+ error: "Realtime transcription service did not return a websocket URL.",
149
+ };
150
+ }
151
+ return payload;
152
+ });
153
+ });
154
+ }
155
+ //# sourceMappingURL=google-realtime-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-realtime-session.js","sourceRoot":"","sources":["../../src/server/google-realtime-session.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport {\n defineEventHandler,\n getMethod,\n getRequestHeader,\n readBody,\n setResponseStatus,\n type H3Event,\n} from \"h3\";\nimport { readAppSecret } from \"../secrets/storage.js\";\nimport { resolveCredential } from \"../credentials/index.js\";\nimport { getSession } from \"./auth.js\";\nimport { getOrgContext } from \"../org/context.js\";\nimport { runWithRequestContext } from \"./request-context.js\";\nimport { resolveBuilderCredentials } from \"./credential-provider.js\";\n\ninterface GoogleRealtimeSessionResponse {\n websocketUrl: string;\n sessionToken: string;\n websocketProtocol?: string;\n}\n\nfunction isSameOriginRequest(event: H3Event): boolean {\n const host = getRequestHeader(event, \"host\");\n const origin = getRequestHeader(event, \"origin\");\n if (origin && host) {\n try {\n const parsed = new URL(origin);\n if (parsed.host === host) return true;\n if (parsed.protocol === \"tauri:\" && parsed.hostname === \"localhost\") {\n return true;\n }\n if (\n (parsed.protocol === \"http:\" || parsed.protocol === \"https:\") &&\n parsed.hostname === \"tauri.localhost\" &&\n (host.startsWith(\"localhost:\") || host.startsWith(\"127.0.0.1:\"))\n ) {\n return true;\n }\n if (\n parsed.protocol === \"http:\" &&\n (parsed.hostname === \"localhost\" || parsed.hostname === \"127.0.0.1\") &&\n parsed.port === \"1420\" &&\n (host.startsWith(\"localhost:\") || host.startsWith(\"127.0.0.1:\"))\n ) {\n return true;\n }\n return false;\n } catch {\n return false;\n }\n }\n const fetchSite = getRequestHeader(event, \"sec-fetch-site\");\n if (fetchSite) return fetchSite === \"same-origin\" || fetchSite === \"none\";\n return true;\n}\n\nexport async function resolveGoogleRealtimeCredentials(opts: {\n userEmail?: string | null;\n orgId?: string | null;\n}): Promise<string | null> {\n if (opts.userEmail) {\n const userSecret = await readAppSecret({\n key: \"GOOGLE_APPLICATION_CREDENTIALS\",\n scope: \"user\",\n scopeId: opts.userEmail,\n }).catch(() => null);\n const fromUserSecret = userSecret?.value?.trim();\n if (fromUserSecret) return fromUserSecret;\n }\n\n const stored = await resolveCredential(\"GOOGLE_APPLICATION_CREDENTIALS\", {\n userEmail: opts.userEmail ?? undefined,\n orgId: opts.orgId ?? undefined,\n }).catch(() => undefined);\n const fromSettings = stored?.trim();\n if (fromSettings) return fromSettings;\n\n const envValue = process.env.GOOGLE_APPLICATION_CREDENTIALS?.trim();\n if (!envValue) return null;\n if (envValue.startsWith(\"{\")) return envValue;\n\n try {\n const fileContents = await readFile(envValue, \"utf8\");\n const trimmed = fileContents.trim();\n return trimmed || null;\n } catch {\n throw new Error(\n \"GOOGLE_APPLICATION_CREDENTIALS points to a file path the framework server could not read\",\n );\n }\n}\n\nexport function createGoogleRealtimeSessionHandler() {\n return defineEventHandler(async (event: H3Event) => {\n if (getMethod(event) !== \"POST\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n if (!isSameOriginRequest(event)) {\n setResponseStatus(event, 403);\n return { error: \"Cross-origin request rejected\" };\n }\n\n const session = await getSession(event).catch(() => null);\n if (!session?.email) {\n setResponseStatus(event, 401);\n return { error: \"Authentication required\" };\n }\n\n const orgCtx = await getOrgContext(event).catch(() => null);\n const requestContext = {\n userEmail: session.email,\n orgId: orgCtx?.orgId ?? undefined,\n };\n\n return runWithRequestContext(requestContext, async () => {\n const googleApplicationCredentials =\n await resolveGoogleRealtimeCredentials({\n userEmail: session.email,\n orgId: orgCtx?.orgId ?? undefined,\n });\n if (!googleApplicationCredentials) {\n setResponseStatus(event, 400);\n return {\n error:\n \"Configure GOOGLE_APPLICATION_CREDENTIALS in Settings to use Google realtime transcription.\",\n };\n }\n\n const builderCreds = await resolveBuilderCredentials();\n if (!builderCreds.privateKey || !builderCreds.publicKey) {\n setResponseStatus(event, 400);\n return {\n error:\n \"Builder must be connected to mint a managed realtime transcription session.\",\n };\n }\n\n const apiHost =\n process.env.BUILDER_API_HOST || \"https://ai-services.builder.io\";\n const body = ((await readBody(event).catch(() => ({}))) || {}) as {\n language?: unknown;\n };\n const res = await fetch(\n `${apiHost}/agent-native/transcribe-stream/session`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${builderCreds.privateKey}`,\n \"x-builder-api-key\": builderCreds.publicKey,\n ...(builderCreds.userId\n ? { \"x-builder-user-id\": builderCreds.userId }\n : {}),\n },\n body: JSON.stringify({\n googleApplicationCredentials,\n language:\n typeof body?.language === \"string\"\n ? body.language.trim()\n : undefined,\n }),\n },\n ).catch((err: any) => {\n throw new Error(\n err?.message || \"Failed to reach realtime transcription service\",\n );\n });\n\n if (!res.ok) {\n const errorBody = await res\n .json()\n .catch(() => ({ error: `HTTP ${res.status}` }));\n setResponseStatus(event, res.status);\n return {\n error:\n typeof errorBody?.error === \"string\"\n ? errorBody.error\n : `Realtime session failed (${res.status})`,\n };\n }\n\n const payload = (await res.json()) as GoogleRealtimeSessionResponse;\n if (!payload?.websocketUrl) {\n setResponseStatus(event, 502);\n return {\n error:\n \"Realtime transcription service did not return a websocket URL.\",\n };\n }\n return payload;\n });\n });\n}\n"]}
@@ -3,6 +3,13 @@ export interface VoiceProvidersStatus {
3
3
  gemini: boolean;
4
4
  openai: boolean;
5
5
  groq: boolean;
6
+ /**
7
+ * Google Speech-to-Text realtime streaming is BYOK-only for v1. This reports
8
+ * whether a service-account credential is configured; the actual stream runs
9
+ * through the dedicated WebSocket -> StreamingRecognize path, not the batch
10
+ * transcribe route.
11
+ */
12
+ googleRealtime: boolean;
6
13
  /** Always true — the Web Speech API is available in WebKit-based clients. */
7
14
  browser: true;
8
15
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"voice-providers-status.d.ts","sourceRoot":"","sources":["../../src/server/voice-providers-status.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,6EAA6E;IAC7E,OAAO,EAAE,IAAI,CAAC;IACd;;;;;OAKG;IACH,MAAM,EAAE,IAAI,CAAC;CACd;AAED,wBAAgB,iCAAiC;;IAiEhD"}
1
+ {"version":3,"file":"voice-providers-status.d.ts","sourceRoot":"","sources":["../../src/server/voice-providers-status.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB,6EAA6E;IAC7E,OAAO,EAAE,IAAI,CAAC;IACd;;;;;OAKG;IACH,MAAM,EAAE,IAAI,CAAC;CACd;AAED,wBAAgB,iCAAiC;;IA6EhD"}
@@ -20,6 +20,7 @@ import { getSession } from "./auth.js";
20
20
  import { resolveHasBuilderPrivateKey } from "./credential-provider.js";
21
21
  import { getOrgContext } from "../org/context.js";
22
22
  import { runWithRequestContext } from "./request-context.js";
23
+ import { resolveGoogleRealtimeCredentials } from "./google-realtime-session.js";
23
24
  export function createVoiceProvidersStatusHandler() {
24
25
  return defineEventHandler(async (event) => {
25
26
  if (getMethod(event) !== "GET") {
@@ -29,6 +30,16 @@ export function createVoiceProvidersStatusHandler() {
29
30
  const session = await getSession(event).catch(() => null);
30
31
  async function hasKey(key) {
31
32
  try {
33
+ if (key === "GOOGLE_APPLICATION_CREDENTIALS") {
34
+ const orgCtx = session?.email
35
+ ? await getOrgContext(event).catch(() => null)
36
+ : null;
37
+ const resolved = await resolveGoogleRealtimeCredentials({
38
+ userEmail: session?.email,
39
+ orgId: orgCtx?.orgId ?? undefined,
40
+ });
41
+ return typeof resolved === "string" && resolved.length > 0;
42
+ }
32
43
  const ctx = { userEmail: session?.email };
33
44
  if (!session?.email) {
34
45
  const v = await resolveCredential(key, ctx);
@@ -65,16 +76,18 @@ export function createVoiceProvidersStatusHandler() {
65
76
  catch {
66
77
  builder = false;
67
78
  }
68
- const [gemini, openai, groq] = await Promise.all([
79
+ const [gemini, openai, groq, googleRealtime] = await Promise.all([
69
80
  hasKey("GEMINI_API_KEY"),
70
81
  hasKey("OPENAI_API_KEY"),
71
82
  hasKey("GROQ_API_KEY"),
83
+ hasKey("GOOGLE_APPLICATION_CREDENTIALS"),
72
84
  ]);
73
85
  const status = {
74
86
  builder,
75
87
  gemini,
76
88
  openai,
77
89
  groq,
90
+ googleRealtime,
78
91
  browser: true,
79
92
  native: true,
80
93
  };
@@ -1 +1 @@
1
- {"version":3,"file":"voice-providers-status.js","sourceRoot":"","sources":["../../src/server/voice-providers-status.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GET /_agent-native/voice-providers/status\n *\n * Reports which voice transcription providers are configured for the\n * current user. The desktop Settings UI uses this to show \"Connect\" vs\n * \"Connected\" status pills next to each provider option.\n *\n * Resolution mirrors `transcribe-voice.ts`: we try the user-scoped\n * encrypted secret first (set via the sidebar settings UI) and fall back\n * to `resolveCredential()` (env var + SQL settings store). Each lookup is\n * wrapped in try/catch — one provider's failure must never break the\n * whole response.\n *\n * Returns booleans only — never the actual key material.\n */\nimport {\n defineEventHandler,\n getMethod,\n setResponseStatus,\n type H3Event,\n} from \"h3\";\nimport { readAppSecret } from \"../secrets/storage.js\";\nimport { resolveCredential } from \"../credentials/index.js\";\nimport { getSession } from \"./auth.js\";\nimport { resolveHasBuilderPrivateKey } from \"./credential-provider.js\";\nimport { getOrgContext } from \"../org/context.js\";\nimport { runWithRequestContext } from \"./request-context.js\";\n\nexport interface VoiceProvidersStatus {\n builder: boolean;\n gemini: boolean;\n openai: boolean;\n groq: boolean;\n /** Always true — the Web Speech API is available in WebKit-based clients. */\n browser: true;\n /**\n * Apple's SFSpeechRecognizer + AVAudioEngine, exposed by the Tauri\n * desktop client. Always reported as `true` from the server — the\n * desktop client gates this on macOS at the Tauri-command boundary, so\n * non-macOS hosts return a clear error instead of attempting to use it.\n */\n native: true;\n}\n\nexport function createVoiceProvidersStatusHandler() {\n return defineEventHandler(async (event: H3Event) => {\n if (getMethod(event) !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n\n const session = await getSession(event).catch(() => null);\n\n async function hasKey(key: string): Promise<boolean> {\n try {\n const ctx = { userEmail: session?.email };\n if (!session?.email) {\n const v = await resolveCredential(key, ctx);\n return typeof v === \"string\" && v.length > 0;\n }\n const userSecret = await readAppSecret({\n key,\n scope: \"user\",\n scopeId: session.email,\n }).catch(() => null);\n if (userSecret?.value && userSecret.value.length > 0) return true;\n const fallback = await resolveCredential(key, ctx);\n return typeof fallback === \"string\" && fallback.length > 0;\n } catch {\n return false;\n }\n }\n\n let builder = false;\n try {\n const orgCtx = session?.email\n ? await getOrgContext(event).catch(() => null)\n : null;\n const resolve = () => resolveHasBuilderPrivateKey();\n builder =\n (session?.email\n ? await runWithRequestContext(\n {\n userEmail: session.email,\n orgId: orgCtx?.orgId ?? undefined,\n },\n resolve,\n )\n : await resolve()) === true;\n } catch {\n builder = false;\n }\n\n const [gemini, openai, groq] = await Promise.all([\n hasKey(\"GEMINI_API_KEY\"),\n hasKey(\"OPENAI_API_KEY\"),\n hasKey(\"GROQ_API_KEY\"),\n ]);\n\n const status: VoiceProvidersStatus = {\n builder,\n gemini,\n openai,\n groq,\n browser: true,\n native: true,\n };\n return status;\n });\n}\n"]}
1
+ {"version":3,"file":"voice-providers-status.js","sourceRoot":"","sources":["../../src/server/voice-providers-status.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GET /_agent-native/voice-providers/status\n *\n * Reports which voice transcription providers are configured for the\n * current user. The desktop Settings UI uses this to show \"Connect\" vs\n * \"Connected\" status pills next to each provider option.\n *\n * Resolution mirrors `transcribe-voice.ts`: we try the user-scoped\n * encrypted secret first (set via the sidebar settings UI) and fall back\n * to `resolveCredential()` (env var + SQL settings store). Each lookup is\n * wrapped in try/catch — one provider's failure must never break the\n * whole response.\n *\n * Returns booleans only — never the actual key material.\n */\nimport {\n defineEventHandler,\n getMethod,\n setResponseStatus,\n type H3Event,\n} from \"h3\";\nimport { readAppSecret } from \"../secrets/storage.js\";\nimport { resolveCredential } from \"../credentials/index.js\";\nimport { getSession } from \"./auth.js\";\nimport { resolveHasBuilderPrivateKey } from \"./credential-provider.js\";\nimport { getOrgContext } from \"../org/context.js\";\nimport { runWithRequestContext } from \"./request-context.js\";\nimport { resolveGoogleRealtimeCredentials } from \"./google-realtime-session.js\";\n\nexport interface VoiceProvidersStatus {\n builder: boolean;\n gemini: boolean;\n openai: boolean;\n groq: boolean;\n /**\n * Google Speech-to-Text realtime streaming is BYOK-only for v1. This reports\n * whether a service-account credential is configured; the actual stream runs\n * through the dedicated WebSocket -> StreamingRecognize path, not the batch\n * transcribe route.\n */\n googleRealtime: boolean;\n /** Always true — the Web Speech API is available in WebKit-based clients. */\n browser: true;\n /**\n * Apple's SFSpeechRecognizer + AVAudioEngine, exposed by the Tauri\n * desktop client. Always reported as `true` from the server — the\n * desktop client gates this on macOS at the Tauri-command boundary, so\n * non-macOS hosts return a clear error instead of attempting to use it.\n */\n native: true;\n}\n\nexport function createVoiceProvidersStatusHandler() {\n return defineEventHandler(async (event: H3Event) => {\n if (getMethod(event) !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n\n const session = await getSession(event).catch(() => null);\n\n async function hasKey(key: string): Promise<boolean> {\n try {\n if (key === \"GOOGLE_APPLICATION_CREDENTIALS\") {\n const orgCtx = session?.email\n ? await getOrgContext(event).catch(() => null)\n : null;\n const resolved = await resolveGoogleRealtimeCredentials({\n userEmail: session?.email,\n orgId: orgCtx?.orgId ?? undefined,\n });\n return typeof resolved === \"string\" && resolved.length > 0;\n }\n const ctx = { userEmail: session?.email };\n if (!session?.email) {\n const v = await resolveCredential(key, ctx);\n return typeof v === \"string\" && v.length > 0;\n }\n const userSecret = await readAppSecret({\n key,\n scope: \"user\",\n scopeId: session.email,\n }).catch(() => null);\n if (userSecret?.value && userSecret.value.length > 0) return true;\n const fallback = await resolveCredential(key, ctx);\n return typeof fallback === \"string\" && fallback.length > 0;\n } catch {\n return false;\n }\n }\n\n let builder = false;\n try {\n const orgCtx = session?.email\n ? await getOrgContext(event).catch(() => null)\n : null;\n const resolve = () => resolveHasBuilderPrivateKey();\n builder =\n (session?.email\n ? await runWithRequestContext(\n {\n userEmail: session.email,\n orgId: orgCtx?.orgId ?? undefined,\n },\n resolve,\n )\n : await resolve()) === true;\n } catch {\n builder = false;\n }\n\n const [gemini, openai, groq, googleRealtime] = await Promise.all([\n hasKey(\"GEMINI_API_KEY\"),\n hasKey(\"OPENAI_API_KEY\"),\n hasKey(\"GROQ_API_KEY\"),\n hasKey(\"GOOGLE_APPLICATION_CREDENTIALS\"),\n ]);\n\n const status: VoiceProvidersStatus = {\n builder,\n gemini,\n openai,\n groq,\n googleRealtime,\n browser: true,\n native: true,\n };\n return status;\n });\n}\n"]}
@@ -0,0 +1,155 @@
1
+ ---
2
+ title: "Sharing & Privacy"
3
+ description: "Google-Docs-style sharing, built into the framework. Every user-created resource — docs, dashboards, designs, decks, clips, recordings, forms — gets the same private-by-default model with one consistent share UI."
4
+ ---
5
+
6
+ # Sharing & Privacy
7
+
8
+ Every resource a user creates in an agent-native app — a document, a dashboard, a design, a deck, a video edit, a screen recording, a meeting transcript, a form, a booking link — is **private to the creator by default**. Other people see it only when the creator explicitly shares it, or changes its visibility to `org` or `public`.
9
+
10
+ It looks and works like Google Docs. The same share button, the same dialog, the same three-tier visibility model, the same per-user/per-org grants — across every template, with no per-app reinvention.
11
+
12
+ ## Why one model {#why}
13
+
14
+ Most app frameworks make sharing a per-feature project. The result: every doc-like surface ends up with its own share dialog, its own permissions schema, its own access-check bugs. In agent-native, sharing is a **framework primitive**. The schema columns, the access-check helpers, the share popover, and the agent-callable share actions all ship with the core. A new template gets the full sharing story by adding two columns and one line of registration.
15
+
16
+ This also means the agent never has to learn a new sharing model per app. Tell the agent "share this with Alice as an editor" in any template and the same `share-resource` action fires.
17
+
18
+ ## The three visibility levels {#visibility}
19
+
20
+ Coarse visibility lives on the resource itself; fine-grained grants live in a companion shares table.
21
+
22
+ | Visibility | Who can see it |
23
+ | ---------- | --------------------------------------------------------------------------------------------------- |
24
+ | `private` | Owner + people explicitly granted. **Default for every new resource.** |
25
+ | `org` | Owner + explicit grants + anyone in the same organization (read-only). |
26
+ | `public` | Owner + explicit grants + anyone with the link (read-only). Doesn't appear in others' lists/search. |
27
+
28
+ `public` is a deliberately quiet level: a public resource is reachable by direct link, but it does **not** show up in other users' sidebars, lists, or search. That keeps "public for sharing the URL" separate from "public for cross-user discovery." Galleries and template catalogs that genuinely want cross-user discovery opt in explicitly.
29
+
30
+ ## Roles on a share grant {#roles}
31
+
32
+ When you share with a specific user or org, you pick a role:
33
+
34
+ - **Viewer** — read only.
35
+ - **Editor** — read + write.
36
+ - **Admin** — read + write + manage shares (can add/remove other people).
37
+
38
+ `admin` does NOT change ownership — there's still exactly one owner per resource, distinct from the share grants.
39
+
40
+ ## What's covered {#covered}
41
+
42
+ Every template that stores user-authored work uses this model. Concretely:
43
+
44
+ - **Content** — documents
45
+ - **Slides** — decks
46
+ - **Design** — designs and assets
47
+ - **Video** — compositions
48
+ - **Clips** — screen recordings (Loom-style)
49
+ - **Calls** — meeting recordings and transcripts (Granola-style)
50
+ - **Meeting Notes** — transcripts and summaries
51
+ - **Forms** — form definitions
52
+ - **Calendar** — events and booking links
53
+ - **Analytics** — dashboards (rolling out — see the analytics template's `AGENTS.md`)
54
+ - **Tools** — sandboxed mini-apps (see [Tools](/docs/tools#sharing))
55
+
56
+ Every one of these uses the same `ownableColumns()` schema helper, the same `share-resource` action, and the same `<ShareButton>` UI. Move from one template to another and the share dialog looks identical.
57
+
58
+ ## What's not covered {#not-covered}
59
+
60
+ A few areas are intentionally outside the sharing system:
61
+
62
+ - **Personal-data apps** (Mail, Macros) — user-scoped by design. There's no "share my inbox" concept.
63
+ - **External source-of-truth apps** (Issues → Jira, Recruiting → Greenhouse) — access control lives in the upstream system, not the agent-native app.
64
+ - **Anonymous public URLs** — form publish slugs and booking-link slugs that expose a URL to logged-out users are a separate axis. They live alongside the sharing system, not on top of it.
65
+
66
+ ## The share UI {#share-ui}
67
+
68
+ Every shareable resource gets a share button in its header. Clicking it opens a popover anchored to the button (not a modal) with:
69
+
70
+ - Visibility selector (`Private` / `Organization` / `Public link`).
71
+ - "Add people or teams" autocomplete — search users in the org or paste an email.
72
+ - A list of current grants with role pickers and a remove control.
73
+ - A copy-link button that respects the current visibility.
74
+
75
+ The share button is a single import:
76
+
77
+ ```tsx
78
+ import { ShareButton } from "@agent-native/core/client";
79
+
80
+ <ShareButton
81
+ resourceType="deck"
82
+ resourceId={deck.id}
83
+ resourceTitle={deck.title}
84
+ />;
85
+ ```
86
+
87
+ For lists, drop a `<VisibilityBadge visibility={row.visibility} />` next to each row so users can see at a glance what's private vs. shared.
88
+
89
+ ## Same model, agent and UI {#agent-and-ui}
90
+
91
+ The framework auto-mounts these actions in every template — the agent calls them as tools, the UI calls them as HTTP endpoints:
92
+
93
+ | Action | What it does |
94
+ | ------------------------- | ------------------------------------------------- |
95
+ | `share-resource` | Grant a user or org access at a specific role. |
96
+ | `unshare-resource` | Revoke access for a user or org. |
97
+ | `list-resource-shares` | Show current visibility plus all explicit grants. |
98
+ | `set-resource-visibility` | Change to `private`, `org`, or `public`. |
99
+
100
+ Tell the agent "share this design with the marketing team as editors" and it calls `share-resource` against the same endpoint the UI uses. The result shows up in the share dialog the next render.
101
+
102
+ ## Building it into a new template {#building}
103
+
104
+ If you're creating a template (see [Creating Templates](/docs/creating-templates)), wiring sharing in is short. Two additions to your schema:
105
+
106
+ ```ts
107
+ import {
108
+ table,
109
+ text,
110
+ ownableColumns,
111
+ createSharesTable,
112
+ } from "@agent-native/core/db/schema";
113
+
114
+ export const decks = table("decks", {
115
+ id: text("id").primaryKey(),
116
+ title: text("title").notNull(),
117
+ data: text("data").notNull(),
118
+ ...ownableColumns(), // adds owner_email, org_id, visibility
119
+ });
120
+
121
+ export const deckShares = createSharesTable("deck_shares");
122
+ ```
123
+
124
+ One registration call in `server/db/index.ts`:
125
+
126
+ ```ts
127
+ import { registerShareableResource } from "@agent-native/core/sharing";
128
+
129
+ registerShareableResource({
130
+ type: "deck",
131
+ resourceTable: schema.decks,
132
+ sharesTable: schema.deckShares,
133
+ displayName: "Deck",
134
+ titleColumn: "title",
135
+ getDb,
136
+ });
137
+ ```
138
+
139
+ After that, list/read queries pass through `accessFilter()` and write actions use `assertAccess()` to enforce roles. The full pattern (including create-action ownership stamping and the migration recipe for existing tables) lives in the `sharing` agent skill — the agent reads it on demand when building a sharing-aware feature.
140
+
141
+ ## Security guarantees {#security}
142
+
143
+ Sharing rides on the framework's broader data-scoping model. The two non-negotiable rules:
144
+
145
+ - **No unscoped queries.** Every query against an ownable table must go through `accessFilter()` (lists), `resolveAccess()` (read-by-id), or `assertAccess()` (writes). A CI guard fails the build if a query against an ownable table runs without one of these helpers.
146
+ - **Org isolation.** Resources tagged with an `org_id` are invisible to users from other orgs even to the agent. The active-org session value flows through to SQL, so cross-org leaks are impossible by construction.
147
+
148
+ See [Security & Data Scoping](/docs/security) for the full model and threat surface.
149
+
150
+ ## See also {#see-also}
151
+
152
+ - [Security & Data Scoping](/docs/security) — the access-filter and ownership model that sharing rides on.
153
+ - [Authentication](/docs/authentication) — sessions, organizations, and how identity flows into the request context.
154
+ - [Tools](/docs/tools#sharing) — sharing in the sandboxed mini-app surface.
155
+ - [Creating Templates](/docs/creating-templates) — wiring `ownableColumns` into a new template's schema.
@@ -1,19 +1,21 @@
1
1
  ---
2
2
  title: "Clips"
3
- description: "Record your screen, get an AI-generated title, summary, and chapter markers automatically, and search across every recording you've ever made."
3
+ description: "Async screen recording (Loom-style), calendar-synced meeting notes (Granola-style), and push-to-talk voice dictation (Wisprflow-style) — all transcribed, summarized, and searchable in one app you own."
4
4
  ---
5
5
 
6
6
  # Clips
7
7
 
8
- A screen-recording app where the agent does the post-production work for you. Record your screen, and Clips transcribes it, suggests a title and summary, builds chapter markers, and tags the content automatically. Ask "find the clip where we discussed the rollout plan" and the agent searches across every transcript you've ever made.
8
+ A capture-everything app: screen recordings (Loom-style), meeting notes from your calendar (Granola-style), and Fn-hold voice dictation (Wisprflow-style). The agent transcribes, titles, summarizes, and indexes all of it then lets you ask "find the clip where we discussed the rollout plan" and searches across every transcript you've ever made.
9
9
 
10
- Think along the lines of products that record short async videos for your team — but the agent is a first-class editor, and the recordings are yours, not a SaaS vendor's.
10
+ Think along the lines of Loom + Granola + Wisprflow rolled into one app — but the agent is a first-class editor across every surface, and the recordings, meetings, and dictations are yours, not a SaaS vendor's.
11
11
 
12
12
  ## What you can do with it
13
13
 
14
14
  - **Record your screen** with a built-in recorder, webcam overlay, audio capture, and pause/trim.
15
+ - **Capture meetings from your calendar.** Connect Google Calendar, see upcoming meetings in the sidebar, and hit record on any one. You get a live transcript plus AI summary, bullet notes, and action items the moment it ends.
16
+ - **Push-to-talk dictation.** Hold Fn on your machine, speak, and the cleaned-up text drops into whatever app you're using. Every dictation is kept in a searchable history with originals and AI-cleaned versions side by side.
15
17
  - **Get an auto-generated title, summary, and chapter markers** for every recording — the agent fills them in and keeps them current.
16
- - **Search across every transcript** with full-text search. "Find the clip where we discussed the rollout plan."
18
+ - **Search across every transcript** screen recordings, meetings, and dictations all in one library. "Find the clip where we discussed the rollout plan."
17
19
  - **Share clips** with per-clip permissions (public, team, private). Link tracking and threaded comments work too.
18
20
  - **Smart library views.** Group by project, filter by speaker, auto-tag based on content.
19
21
  - **Edit the transcript through chat.** "Fix the mis-transcribed word at 1:42." "Pull three quotes for a blog post." The agent edits the transcript and the UI updates live.
@@ -43,7 +45,8 @@ Clips is a larger template with a native recorder (it ships a desktop companion
43
45
  Clips is a full cloneable SaaS — fork it and ask the agent to extend it. Some examples:
44
46
 
45
47
  - "Add a filler-word removal button that strips ums and uhs from the transcript and re-stitches the video."
46
- - "Auto-post a new clip to Slack #eng-demos whenever I finish a recording." (Connect Slack first via [Messaging](/docs/messaging).)
48
+ - "Auto-post my standup notes to Slack #eng whenever a meeting ends." (Connect Slack first via [Messaging](/docs/messaging).)
49
+ - "Add a hotkey that drops the last dictation into Linear as a new ticket."
47
50
  - "Group the library by project — detect the project from the first words of each transcript."
48
51
  - "Add a 'Generate blog post from this clip' button that drafts a post from the transcript and saves it as a draft."
49
52
  - "Let viewers leave timestamped reactions on a shared clip."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.7.54",
3
+ "version": "0.7.56",
4
4
  "type": "module",
5
5
  "description": "Framework for agent-native application development — where AI agents and UI share state via files",
6
6
  "license": "MIT",