@agent-native/core 0.165.1 → 0.165.3
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/corpus/templates/analytics/CHANGELOG.md +37 -3
- package/corpus/templates/analytics/changelog/2026-08-20-staged-data-limit-recovery.md +6 -0
- package/corpus/templates/assets/CHANGELOG.md +36 -2
- package/corpus/templates/brain/CHANGELOG.md +33 -3
- package/corpus/templates/calendar/CHANGELOG.md +45 -2
- package/corpus/templates/chat/CHANGELOG.md +29 -3
- package/corpus/templates/clips/CHANGELOG.md +55 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +6 -11
- package/corpus/templates/clips/app/root.tsx +2 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +3 -3
- package/corpus/templates/clips/app/routes/download.tsx +111 -17
- package/corpus/templates/clips/changelog/2026-08-20-cross-tab-recording-controls.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-20-overlay-follow-review-fixes.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-20-provider-key-recovery.md +6 -0
- package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +29 -46
- package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +1 -1
- package/corpus/templates/clips/chrome-extension/public/manifest.json +9 -2
- package/corpus/templates/clips/chrome-extension/src/background.ts +65 -46
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +25 -17
- package/corpus/templates/clips/chrome-extension/src/overlay-follow.ts +25 -0
- package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +109 -46
- package/corpus/templates/content/CHANGELOG.md +45 -2
- package/corpus/templates/crm/CHANGELOG.md +28 -16
- package/corpus/templates/design/CHANGELOG.md +54 -2
- package/corpus/templates/dispatch/CHANGELOG.md +51 -2
- package/corpus/templates/factory/CHANGELOG.md +28 -2
- package/corpus/templates/forms/CHANGELOG.md +39 -3
- package/corpus/templates/macros/CHANGELOG.md +30 -2
- package/corpus/templates/mail/CHANGELOG.md +34 -3
- package/corpus/templates/plan/CHANGELOG.md +34 -3
- package/corpus/templates/slides/CHANGELOG.md +73 -2
- package/corpus/templates/tasks/CHANGELOG.md +35 -0
- package/corpus/templates/tasks/app/root.tsx +8 -1
- package/dist/changelog/parse.d.ts +12 -4
- package/dist/changelog/parse.js +61 -23
- package/dist/cli/changelog.js +9 -11
- package/dist/client/blocks/library/diagram.js +1 -1
- package/dist/client/chat/run-recovery.js +8 -3
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/workspace-deploy.js +7 -0
- package/dist/mcp-client/oauth-routes.d.ts +1 -0
- package/dist/mcp-client/oauth-routes.js +15 -2
- package/dist/mcp-client/remote-store.js +83 -43
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.js +16 -4
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/google-oauth-credentials.js +14 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/index.js +1 -1
- package/dist/settings/store.d.ts +7 -1
- package/dist/settings/store.js +25 -3
- package/dist/settings/user-settings.d.ts +2 -0
- package/dist/settings/user-settings.js +65 -2
- package/dist/templates/chat/CHANGELOG.md +29 -3
- package/docs/content/deployment.mdx +89 -14
- package/package.json +3 -3
- package/src/templates/chat/CHANGELOG.md +29 -3
- package/corpus/templates/.retired/issues/package.json +0 -7
- package/corpus/templates/.retired/meeting-notes/package.json +0 -7
- package/corpus/templates/.retired/recruiting/package.json +0 -7
- package/corpus/templates/.retired/scheduling/package.json +0 -7
- package/corpus/templates/.retired/voice/package.json +0 -7
- package/corpus/templates/voice/.agents/skills/upgrade-agent-native/SKILL.md +0 -87
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import { writeAppSecret, readAppSecret, deleteAppSecret, } from "../secrets/storage.js";
|
|
24
|
-
import { getOrgSetting,
|
|
25
|
-
import { getUserSetting,
|
|
24
|
+
import { getOrgSetting, mutateOrgSetting } from "../settings/org-settings.js";
|
|
25
|
+
import { getUserSetting, mutateUserSetting, } from "../settings/user-settings.js";
|
|
26
26
|
import { getMcpOAuthAccessToken, revokeMcpOAuthCredentials, saveMcpOAuthCredentials, } from "./oauth-client.js";
|
|
27
27
|
import { validateRemoteUrl } from "./remote-url.js";
|
|
28
28
|
export { validateRemoteUrl } from "./remote-url.js";
|
|
@@ -107,20 +107,21 @@ function sanitiseOrgId(orgId) {
|
|
|
107
107
|
return orgId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
108
108
|
}
|
|
109
109
|
async function readList(scope, scopeId) {
|
|
110
|
-
|
|
110
|
+
return parseServerList(scope === "user"
|
|
111
111
|
? await getUserSetting(scopeId, SETTINGS_KEY)
|
|
112
|
-
: await getOrgSetting(scopeId, SETTINGS_KEY);
|
|
112
|
+
: await getOrgSetting(scopeId, SETTINGS_KEY));
|
|
113
|
+
}
|
|
114
|
+
function parseServerList(raw) {
|
|
113
115
|
if (!raw || !Array.isArray(raw.servers))
|
|
114
116
|
return [];
|
|
115
117
|
return raw.servers.filter((s) => s && typeof s.id === "string" && typeof s.url === "string");
|
|
116
118
|
}
|
|
117
|
-
async function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
119
|
+
async function mutateServerList(scope, scopeId, updater) {
|
|
120
|
+
const mutate = scope === "user" ? mutateUserSetting : mutateOrgSetting;
|
|
121
|
+
const next = await mutate(scopeId, SETTINGS_KEY, async (raw) => ({
|
|
122
|
+
servers: await updater(parseServerList(raw)),
|
|
123
|
+
}));
|
|
124
|
+
return parseServerList(next);
|
|
124
125
|
}
|
|
125
126
|
export async function listRemoteServers(scope, scopeId) {
|
|
126
127
|
return readList(scope, scopeId);
|
|
@@ -228,6 +229,7 @@ export async function replaceOAuthRemoteServer(scope, scopeId, serverId, credent
|
|
|
228
229
|
...credentials,
|
|
229
230
|
serverUrl: credentialResource.url.toString(),
|
|
230
231
|
};
|
|
232
|
+
let updated;
|
|
231
233
|
try {
|
|
232
234
|
await saveMcpOAuthCredentials({
|
|
233
235
|
key: nextSecretKey,
|
|
@@ -235,21 +237,22 @@ export async function replaceOAuthRemoteServer(scope, scopeId, serverId, credent
|
|
|
235
237
|
scopeId,
|
|
236
238
|
credentials: nextCredentials,
|
|
237
239
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
240
|
+
updated = await mutateServerList(scope, scopeId, (servers) => {
|
|
241
|
+
const latestIndex = servers.findIndex((server) => server.id === serverId);
|
|
242
|
+
const latest = latestIndex >= 0 ? servers[latestIndex] : undefined;
|
|
243
|
+
if (!latest ||
|
|
244
|
+
latest.oauthSecretKey !== current.oauthSecretKey ||
|
|
245
|
+
latest.url !== current.url) {
|
|
246
|
+
throw new Error("MCP server changed while reconnecting");
|
|
247
|
+
}
|
|
248
|
+
const next = [...servers];
|
|
249
|
+
next[latestIndex] = {
|
|
250
|
+
...latest,
|
|
251
|
+
url: nextCredentials.serverUrl,
|
|
252
|
+
oauthSecretKey: nextSecretKey,
|
|
253
|
+
};
|
|
254
|
+
return next;
|
|
255
|
+
});
|
|
253
256
|
}
|
|
254
257
|
catch (err) {
|
|
255
258
|
await revokeMcpOAuthCredentials({
|
|
@@ -263,6 +266,18 @@ export async function replaceOAuthRemoteServer(scope, scopeId, serverId, credent
|
|
|
263
266
|
error: `Failed to replace MCP OAuth credentials: ${err?.message ?? err}`,
|
|
264
267
|
};
|
|
265
268
|
}
|
|
269
|
+
// The settings row now points at the replacement grant. Old-grant cleanup
|
|
270
|
+
// is best effort and must not roll back or invalidate the committed row.
|
|
271
|
+
await revokeMcpOAuthCredentials({
|
|
272
|
+
key: current.oauthSecretKey,
|
|
273
|
+
scope,
|
|
274
|
+
scopeId,
|
|
275
|
+
serverUrl: current.url,
|
|
276
|
+
}).catch(() => undefined);
|
|
277
|
+
const server = updated.find((candidate) => candidate.id === serverId);
|
|
278
|
+
return server
|
|
279
|
+
? { ok: true, server }
|
|
280
|
+
: { ok: false, error: "MCP server was not found" };
|
|
266
281
|
}
|
|
267
282
|
export async function addFirstPartyRemoteServer(orgId, input) {
|
|
268
283
|
const trust = await isFirstPartyRemoteEndpointTrusted(orgId, input.appId, input.url);
|
|
@@ -327,7 +342,24 @@ async function addRemoteServerInternal(scope, scopeId, input) {
|
|
|
327
342
|
description: input.description?.trim() || undefined,
|
|
328
343
|
createdAt: Date.now(),
|
|
329
344
|
};
|
|
330
|
-
|
|
345
|
+
try {
|
|
346
|
+
await mutateServerList(scope, scopeId, (servers) => {
|
|
347
|
+
if (servers.some((candidate) => candidate.name === name)) {
|
|
348
|
+
throw new Error(`A server named "${name}" already exists`);
|
|
349
|
+
}
|
|
350
|
+
return [...servers, server];
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
if (headerSecretKey) {
|
|
355
|
+
await deleteAppSecret({
|
|
356
|
+
key: headerSecretKey,
|
|
357
|
+
scope: toSecretScope(scope),
|
|
358
|
+
scopeId,
|
|
359
|
+
}).catch(() => undefined);
|
|
360
|
+
}
|
|
361
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
362
|
+
}
|
|
331
363
|
return { ok: true, server };
|
|
332
364
|
}
|
|
333
365
|
function normalizeEndpointPath(pathname) {
|
|
@@ -396,9 +428,29 @@ export async function isFirstPartyRemoteEndpointTrusted(orgId, appId, endpoint)
|
|
|
396
428
|
}
|
|
397
429
|
export async function removeRemoteServer(scope, scopeId, id) {
|
|
398
430
|
const existing = await readList(scope, scopeId);
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
431
|
+
const expected = existing.find((s) => s.id === id);
|
|
432
|
+
if (!expected)
|
|
433
|
+
return false;
|
|
434
|
+
let removed;
|
|
435
|
+
try {
|
|
436
|
+
await mutateServerList(scope, scopeId, (servers) => {
|
|
437
|
+
removed = undefined;
|
|
438
|
+
const latest = servers.find((server) => server.id === id);
|
|
439
|
+
if (!latest)
|
|
440
|
+
return servers;
|
|
441
|
+
if (latest.oauthSecretKey !== expected.oauthSecretKey) {
|
|
442
|
+
throw new Error(`MCP OAuth credentials changed while removing ${expected.name}`);
|
|
443
|
+
}
|
|
444
|
+
removed = latest;
|
|
445
|
+
return servers.filter((server) => server.id !== id);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
// eslint-disable-next-line no-console
|
|
450
|
+
console.warn(`[mcp-client] Failed to remove MCP server ${expected.name}: ${err?.message ?? err}`);
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
if (!removed)
|
|
402
454
|
return false;
|
|
403
455
|
if (removed?.oauthSecretKey) {
|
|
404
456
|
try {
|
|
@@ -409,8 +461,7 @@ export async function removeRemoteServer(scope, scopeId, id) {
|
|
|
409
461
|
serverUrl: removed.url,
|
|
410
462
|
});
|
|
411
463
|
if (result.local === "replaced") {
|
|
412
|
-
console.warn(`[mcp-client] MCP OAuth credentials changed while removing ${removed.name}; the server was
|
|
413
|
-
return false;
|
|
464
|
+
console.warn(`[mcp-client] MCP OAuth credentials changed while removing ${removed.name}; the server row was already removed.`);
|
|
414
465
|
}
|
|
415
466
|
if (result.remote === "failed") {
|
|
416
467
|
console.warn(`[mcp-client] MCP OAuth revocation failed for ${removed.name}; local credentials were removed.`);
|
|
@@ -421,17 +472,6 @@ export async function removeRemoteServer(scope, scopeId, id) {
|
|
|
421
472
|
console.warn(`[mcp-client] Failed to delete MCP OAuth credentials ${removed.oauthSecretKey}: ${err?.message ?? err}`);
|
|
422
473
|
}
|
|
423
474
|
}
|
|
424
|
-
if (next.length === 0) {
|
|
425
|
-
if (scope === "user") {
|
|
426
|
-
await deleteUserSetting(scopeId, SETTINGS_KEY);
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
await deleteOrgSetting(scopeId, SETTINGS_KEY);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
await writeList(scope, scopeId, next);
|
|
434
|
-
}
|
|
435
475
|
// Best-effort: drop the encrypted-headers secret too. Errors are logged
|
|
436
476
|
// but don't fail the deletion — the settings row is already gone, so a
|
|
437
477
|
// dangling secret is harmless (it just can't be read back).
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
|
|
14
14
|
count: number;
|
|
15
15
|
updated?: undefined;
|
|
16
|
-
error?: undefined;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error?: undefined;
|
|
18
18
|
} | {
|
|
19
19
|
count?: undefined;
|
|
20
20
|
updated: number;
|
|
21
|
-
error?: undefined;
|
|
22
21
|
ok?: undefined;
|
|
22
|
+
error?: undefined;
|
|
23
23
|
} | {
|
|
24
24
|
count?: undefined;
|
|
25
25
|
updated?: undefined;
|
|
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
28
28
|
} | {
|
|
29
29
|
count?: undefined;
|
|
30
30
|
updated?: undefined;
|
|
31
|
-
error?: undefined;
|
|
32
31
|
ok: boolean;
|
|
32
|
+
error?: undefined;
|
|
33
33
|
}>>;
|
|
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
-
error?: undefined;
|
|
45
|
-
ok?: undefined;
|
|
46
44
|
summary: import("./types.js").TraceSummary;
|
|
47
45
|
spans: import("./types.js").TraceSpan[];
|
|
48
46
|
id?: undefined;
|
|
49
|
-
} | {
|
|
50
47
|
error?: undefined;
|
|
51
48
|
ok?: undefined;
|
|
49
|
+
} | {
|
|
52
50
|
summary?: undefined;
|
|
53
51
|
spans?: undefined;
|
|
54
52
|
id: string;
|
|
55
|
-
|
|
53
|
+
error?: undefined;
|
|
56
54
|
ok?: undefined;
|
|
55
|
+
} | {
|
|
57
56
|
summary?: undefined;
|
|
58
57
|
spans?: undefined;
|
|
59
58
|
id?: undefined;
|
|
60
59
|
error: any;
|
|
60
|
+
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
-
error?: undefined;
|
|
63
62
|
summary?: undefined;
|
|
64
63
|
spans?: undefined;
|
|
65
64
|
id?: undefined;
|
|
65
|
+
error?: undefined;
|
|
66
66
|
ok: boolean;
|
|
67
67
|
}>>;
|
|
@@ -193,6 +193,13 @@ export function createProviderApiDocsSchema(providerSchema = z.string().min(1))
|
|
|
193
193
|
search: WebContentSearchSchema.optional().describe("Optional post-fetch search over extracted content by default. Supports query, queries, terms, regex, source, maxMatches, and contextChars."),
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
|
+
const STAGED_DATASET_CAP_ERROR = /Staged dataset (?:byte )?cap exceeded/i;
|
|
197
|
+
function rethrowStagingError(error) {
|
|
198
|
+
if (error instanceof Error && STAGED_DATASET_CAP_ERROR.test(error.message)) {
|
|
199
|
+
throw new Error(`${error.message} Recover by deleting older staged datasets with list-staged-datasets/delete-staged-dataset, or switch this request to saveToFile / a smaller staged result before trying again.`, { cause: error });
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
196
203
|
export function createProviderApiRequestAction(runtime, options = {}) {
|
|
197
204
|
const schema = options.schema ?? createProviderApiRequestSchema();
|
|
198
205
|
return defineAction({
|
|
@@ -244,10 +251,15 @@ export function createProviderApiRequestAction(runtime, options = {}) {
|
|
|
244
251
|
if (!ownerEmail) {
|
|
245
252
|
throw new Error("No authenticated context for provider API staging.");
|
|
246
253
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
254
|
+
try {
|
|
255
|
+
return await stagingExecuteRequest(args, runtime.executeRequest, {
|
|
256
|
+
appId: options.appId,
|
|
257
|
+
ownerEmail,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
rethrowStagingError(error);
|
|
262
|
+
}
|
|
251
263
|
}
|
|
252
264
|
return runtime.executeRequest(args);
|
|
253
265
|
},
|
|
@@ -51,8 +51,8 @@ export declare function handleDeleteResource(event: any): Promise<{
|
|
|
51
51
|
error: string;
|
|
52
52
|
ok?: undefined;
|
|
53
53
|
} | {
|
|
54
|
-
error?: undefined;
|
|
55
54
|
ok: boolean;
|
|
55
|
+
error?: undefined;
|
|
56
56
|
}>;
|
|
57
57
|
/** POST /_agent-native/resources/upload — upload a file as a resource */
|
|
58
58
|
export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
|
|
@@ -73,10 +73,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
|
|
|
73
73
|
runId: string | null;
|
|
74
74
|
expiresAt: number | null;
|
|
75
75
|
metadata: string | null;
|
|
76
|
-
error?: undefined;
|
|
77
76
|
url: string;
|
|
78
77
|
provider: string;
|
|
79
78
|
storageSetupRequired?: undefined;
|
|
79
|
+
error?: undefined;
|
|
80
80
|
} | {
|
|
81
81
|
error: string;
|
|
82
82
|
storageSetupRequired: boolean;
|
|
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
+
error?: undefined;
|
|
30
31
|
ok: boolean;
|
|
31
32
|
key: string;
|
|
32
33
|
baseUrlKey?: string;
|
|
33
34
|
scope: AgentEngineApiKeyScope;
|
|
34
|
-
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
|
@@ -57,7 +57,20 @@ export async function resolveGoogleProviderCredentialCandidatesWithReader(option
|
|
|
57
57
|
* remain the backwards-compatible provider OAuth credentials.
|
|
58
58
|
*/
|
|
59
59
|
export function resolveGoogleSignInCredentials() {
|
|
60
|
-
|
|
60
|
+
const signIn = readCredentialPair("GOOGLE_SIGN_IN_CLIENT_ID", "GOOGLE_SIGN_IN_CLIENT_SECRET");
|
|
61
|
+
const provider = readCredentialPair("GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET");
|
|
62
|
+
// Both pairs configured for different clients means one of them is dead
|
|
63
|
+
// config that nothing will ever read. Silence here cost a fleet-wide beta
|
|
64
|
+
// sign-in outage: GOOGLE_CLIENT_SECRET was repaired and verified while the
|
|
65
|
+
// callback kept using a stale GOOGLE_SIGN_IN_CLIENT_SECRET, so the repaired
|
|
66
|
+
// variable looked correct and changed nothing. Name the loser explicitly.
|
|
67
|
+
if (signIn && provider && signIn.clientId !== provider.clientId) {
|
|
68
|
+
console.warn("[agent-native][google-oauth] GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_CLIENT_ID " +
|
|
69
|
+
"are set to different Google clients. Sign-in uses GOOGLE_SIGN_IN_CLIENT_ID; " +
|
|
70
|
+
"GOOGLE_CLIENT_ID/SECRET are ignored for sign-in. Editing them will not " +
|
|
71
|
+
"change sign-in behaviour.");
|
|
72
|
+
}
|
|
73
|
+
return signIn ?? provider;
|
|
61
74
|
}
|
|
62
75
|
export function hasGoogleSignInCredentials() {
|
|
63
76
|
return resolveGoogleSignInCredentials() !== null;
|
|
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
expiresAt?: undefined;
|
|
27
27
|
ttlSeconds?: undefined;
|
|
28
28
|
} | {
|
|
29
|
-
error?: undefined;
|
|
30
29
|
token: string;
|
|
31
30
|
expiresAt: string;
|
|
32
31
|
ttlSeconds: number;
|
|
32
|
+
error?: undefined;
|
|
33
33
|
}>>;
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getSetting, mutateSetting, putSetting, deleteSetting, getAllSettings, getSettingsEmitter, type StoreWriteOptions, } from "./store.js";
|
|
1
|
+
export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, getSettingsEmitter, type StoreWriteOptions, } from "./store.js";
|
|
2
2
|
export { getSettingHandler, putSettingHandler, deleteSettingHandler, } from "./handlers.js";
|
|
3
3
|
export { readSetting, writeSetting, removeSetting } from "./script-helpers.js";
|
|
4
4
|
export { getUserSetting, putUserSetting, deleteUserSetting, } from "./user-settings.js";
|
package/dist/settings/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Store
|
|
2
|
-
export { getSetting, mutateSetting, putSetting, deleteSetting, getAllSettings, getSettingsEmitter, } from "./store.js";
|
|
2
|
+
export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, getSettingsEmitter, } from "./store.js";
|
|
3
3
|
// H3 route handlers
|
|
4
4
|
export { getSettingHandler, putSettingHandler, deleteSettingHandler, } from "./handlers.js";
|
|
5
5
|
// Script helpers
|
package/dist/settings/store.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { EventEmitter } from "node:events";
|
|
2
2
|
export declare function getSettingsEmitter(): EventEmitter;
|
|
3
3
|
export declare function ensureTable(): Promise<void>;
|
|
4
|
-
export
|
|
4
|
+
export interface StoreReadOptions {
|
|
5
|
+
/** Skip the per-request snapshot when a cross-request race must be checked. */
|
|
6
|
+
bypassCache?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function getSetting(key: string, options?: StoreReadOptions): Promise<Record<string, unknown> | null>;
|
|
5
9
|
export interface StoreWriteOptions {
|
|
6
10
|
/** Tag identifying who initiated this write (e.g. a tab ID). */
|
|
7
11
|
requestSource?: string;
|
|
@@ -16,6 +20,8 @@ export interface StoreWriteOptions {
|
|
|
16
20
|
export declare function mutateSetting(key: string, updater: (current: Record<string, unknown> | null) => Record<string, unknown> | Promise<Record<string, unknown>>, options?: StoreWriteOptions): Promise<Record<string, unknown>>;
|
|
17
21
|
export declare function putSetting(key: string, value: Record<string, unknown>, options?: StoreWriteOptions): Promise<void>;
|
|
18
22
|
export declare function deleteSetting(key: string, options?: StoreWriteOptions): Promise<boolean>;
|
|
23
|
+
/** Delete a setting only when its stored value still matches the inspected value. */
|
|
24
|
+
export declare function deleteSettingIfValue(key: string, expected: Record<string, unknown>, options?: StoreWriteOptions): Promise<boolean>;
|
|
19
25
|
/**
|
|
20
26
|
* Delete every setting whose key starts with `prefix`. Returns the number of
|
|
21
27
|
* rows removed. Used when an owning entity is deleted (e.g. an organization
|
package/dist/settings/store.js
CHANGED
|
@@ -115,9 +115,9 @@ export async function ensureTable() {
|
|
|
115
115
|
}
|
|
116
116
|
return _initPromise;
|
|
117
117
|
}
|
|
118
|
-
export async function getSetting(key) {
|
|
118
|
+
export async function getSetting(key, options) {
|
|
119
119
|
const cache = requestSettingsCache();
|
|
120
|
-
if (cache?.has(key)) {
|
|
120
|
+
if (!options?.bypassCache && cache?.has(key)) {
|
|
121
121
|
const cached = cache.get(key);
|
|
122
122
|
return cached == null ? null : JSON.parse(cached);
|
|
123
123
|
}
|
|
@@ -129,7 +129,8 @@ export async function getSetting(key) {
|
|
|
129
129
|
args: [key],
|
|
130
130
|
});
|
|
131
131
|
const raw = rows.length === 0 ? null : rows[0].value;
|
|
132
|
-
|
|
132
|
+
if (!options?.bypassCache)
|
|
133
|
+
cache?.set(key, raw);
|
|
133
134
|
return raw == null ? null : JSON.parse(raw);
|
|
134
135
|
}
|
|
135
136
|
const SETTINGS_MUTATION_ATTEMPTS = 25;
|
|
@@ -221,6 +222,27 @@ export async function deleteSetting(key, options) {
|
|
|
221
222
|
}
|
|
222
223
|
return false;
|
|
223
224
|
}
|
|
225
|
+
/** Delete a setting only when its stored value still matches the inspected value. */
|
|
226
|
+
export async function deleteSettingIfValue(key, expected, options) {
|
|
227
|
+
await ensureTable();
|
|
228
|
+
const client = getDbExec();
|
|
229
|
+
const table = settingsTable();
|
|
230
|
+
const result = await client.execute({
|
|
231
|
+
sql: `DELETE FROM ${table} WHERE key = ? AND value = ?`,
|
|
232
|
+
args: [key, JSON.stringify(expected)],
|
|
233
|
+
});
|
|
234
|
+
if (result.rowsAffected === 0)
|
|
235
|
+
return false;
|
|
236
|
+
requestSettingsCache()?.set(key, null);
|
|
237
|
+
invalidateRequestAllSettings();
|
|
238
|
+
settingsEmitter().emit("settings", {
|
|
239
|
+
source: "settings",
|
|
240
|
+
type: "delete",
|
|
241
|
+
key,
|
|
242
|
+
...(options?.requestSource && { requestSource: options.requestSource }),
|
|
243
|
+
});
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
224
246
|
/**
|
|
225
247
|
* Delete every setting whose key starts with `prefix`. Returns the number of
|
|
226
248
|
* rows removed. Used when an owning entity is deleted (e.g. an organization
|
|
@@ -12,5 +12,7 @@ import { type StoreWriteOptions } from "./store.js";
|
|
|
12
12
|
export declare function getUserSetting(email: string, key: string): Promise<Record<string, unknown> | null>;
|
|
13
13
|
/** Write a user-scoped setting. Always writes to the prefixed key. */
|
|
14
14
|
export declare function putUserSetting(email: string, key: string, value: Record<string, unknown>, options?: StoreWriteOptions): Promise<void>;
|
|
15
|
+
/** Atomically derive and persist one user-scoped setting. */
|
|
16
|
+
export declare function mutateUserSetting(email: string, key: string, updater: (current: Record<string, unknown> | null) => Record<string, unknown> | Promise<Record<string, unknown>>, options?: StoreWriteOptions): Promise<Record<string, unknown>>;
|
|
15
17
|
/** Delete a user-scoped setting. */
|
|
16
18
|
export declare function deleteUserSetting(email: string, key: string, options?: StoreWriteOptions): Promise<boolean>;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* No global fallback — each user starts with a clean slate. This
|
|
8
8
|
* prevents one user's private data from leaking to other users.
|
|
9
9
|
*/
|
|
10
|
-
import { getSetting, putSetting,
|
|
10
|
+
import { getSetting, mutateSetting, putSetting, deleteSettingIfValue, } from "./store.js";
|
|
11
11
|
function userKey(email, key) {
|
|
12
12
|
return `u:${email.trim().toLowerCase()}:${key}`;
|
|
13
13
|
}
|
|
@@ -31,7 +31,70 @@ export async function getUserSetting(email, key) {
|
|
|
31
31
|
export async function putUserSetting(email, key, value, options) {
|
|
32
32
|
return putSetting(userKey(email, key), value, options);
|
|
33
33
|
}
|
|
34
|
+
/** Atomically derive and persist one user-scoped setting. */
|
|
35
|
+
export async function mutateUserSetting(email, key, updater, options) {
|
|
36
|
+
const normalized = userKey(email, key);
|
|
37
|
+
const legacy = legacyUserKey(email, key);
|
|
38
|
+
let migratedLegacy = false;
|
|
39
|
+
let migratedLegacyValue = null;
|
|
40
|
+
const result = await mutateSetting(normalized, async (current) => {
|
|
41
|
+
if (current !== null) {
|
|
42
|
+
migratedLegacy = false;
|
|
43
|
+
migratedLegacyValue = null;
|
|
44
|
+
return updater(current);
|
|
45
|
+
}
|
|
46
|
+
const legacyCurrent = legacy === normalized
|
|
47
|
+
? null
|
|
48
|
+
: await getSetting(legacy, { bypassCache: true });
|
|
49
|
+
migratedLegacy = legacyCurrent !== null;
|
|
50
|
+
migratedLegacyValue = legacyCurrent;
|
|
51
|
+
return updater(legacyCurrent);
|
|
52
|
+
}, options);
|
|
53
|
+
if (!migratedLegacy)
|
|
54
|
+
return result;
|
|
55
|
+
// If the legacy row disappeared after the updater read it, a concurrent
|
|
56
|
+
// delete won the race. Remove only our exact canonical write; never delete
|
|
57
|
+
// a newer canonical value from another writer.
|
|
58
|
+
if (!migratedLegacyValue ||
|
|
59
|
+
!(await deleteSettingIfValue(legacy, migratedLegacyValue, options))) {
|
|
60
|
+
if (await getSetting(legacy, { bypassCache: true }))
|
|
61
|
+
return result;
|
|
62
|
+
const removed = await deleteSettingIfValue(normalized, result, options);
|
|
63
|
+
if (!removed) {
|
|
64
|
+
// The canonical write committed successfully. A concurrent canonical
|
|
65
|
+
// writer may have replaced it after the legacy row disappeared; keep
|
|
66
|
+
// the committed value visible instead of making callers revoke side
|
|
67
|
+
// effects for a write that is already persisted.
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
throw new Error("User setting was deleted while migrating its legacy key");
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
34
74
|
/** Delete a user-scoped setting. */
|
|
35
75
|
export async function deleteUserSetting(email, key, options) {
|
|
36
|
-
|
|
76
|
+
const normalized = userKey(email, key);
|
|
77
|
+
const legacy = legacyUserKey(email, key);
|
|
78
|
+
const normalizedCurrent = await getSetting(normalized, {
|
|
79
|
+
bypassCache: true,
|
|
80
|
+
});
|
|
81
|
+
if (legacy === normalized) {
|
|
82
|
+
return normalizedCurrent === null
|
|
83
|
+
? false
|
|
84
|
+
: deleteSettingIfValue(normalized, normalizedCurrent, options);
|
|
85
|
+
}
|
|
86
|
+
const legacyCurrent = await getSetting(legacy, { bypassCache: true });
|
|
87
|
+
// Retire the fallback row first. If the canonical delete fails, the
|
|
88
|
+
// remaining canonical value still wins reads instead of resurrecting legacy
|
|
89
|
+
// data after a partial delete; a retry can safely finish the operation.
|
|
90
|
+
const deletedLegacy = legacyCurrent === null
|
|
91
|
+
? false
|
|
92
|
+
: await deleteSettingIfValue(legacy, legacyCurrent, options);
|
|
93
|
+
const normalizedAfterLegacyCleanup = normalizedCurrent === null && deletedLegacy
|
|
94
|
+
? await getSetting(normalized, { bypassCache: true })
|
|
95
|
+
: normalizedCurrent;
|
|
96
|
+
const deletedNormalized = normalizedAfterLegacyCleanup === null
|
|
97
|
+
? false
|
|
98
|
+
: await deleteSettingIfValue(normalized, normalizedAfterLegacyCleanup, options);
|
|
99
|
+
return deletedNormalized || deletedLegacy;
|
|
37
100
|
}
|
|
@@ -3,8 +3,34 @@
|
|
|
3
3
|
All notable user-facing changes to Chat are documented here. Open it any
|
|
4
4
|
time from the command menu (Cmd+K → "What's new").
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Older updates live in [the changelog folder](./changelog/) and are included in the in-app "What's new" view.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## 2026-08-11
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Chrome no longer offers to install Chat as a desktop app.
|
|
13
|
+
|
|
14
|
+
## 2026-08-10
|
|
15
|
+
|
|
16
|
+
### Improved
|
|
17
|
+
|
|
18
|
+
- Full-page chat now uses the available width up to 1000px for more comfortable prompts and responses.
|
|
19
|
+
|
|
20
|
+
## 2026-07-31
|
|
21
|
+
|
|
22
|
+
### Improved
|
|
23
|
+
|
|
24
|
+
- Clicking the Agent Native logo now toggles the app sidebar.
|
|
25
|
+
|
|
26
|
+
## 2026-07-29
|
|
27
|
+
|
|
28
|
+
### Improved
|
|
29
|
+
|
|
30
|
+
- Sidebar footers now keep Feedback, Search, and Collapse together without a separate language shortcut.
|
|
31
|
+
|
|
32
|
+
## 2026-07-25
|
|
33
|
+
|
|
34
|
+
### Improved
|
|
35
|
+
|
|
36
|
+
- Settings navigation now keeps Manage agent as a dedicated linked destination at the bottom.
|