@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
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getPhoneNumberSid,
|
|
23
23
|
getTollFreeVerificationBySid,
|
|
24
24
|
getTollFreeVerificationStatus,
|
|
25
|
+
getTwilioCredentials,
|
|
25
26
|
hasTwilioCredentials,
|
|
26
27
|
listIncomingPhoneNumbers,
|
|
27
28
|
provisionPhoneNumber,
|
|
@@ -37,9 +38,9 @@ import { getReadinessService } from "../../daemon/handlers/config-channels.js";
|
|
|
37
38
|
import { syncTwilioWebhooks } from "../../daemon/handlers/config-ingress.js";
|
|
38
39
|
import type { IngressConfig } from "../../inbound/public-ingress-urls.js";
|
|
39
40
|
import {
|
|
40
|
-
|
|
41
|
+
deleteSecureKeyAsync,
|
|
41
42
|
getSecureKey,
|
|
42
|
-
|
|
43
|
+
setSecureKeyAsync,
|
|
43
44
|
} from "../../security/secure-keys.js";
|
|
44
45
|
import {
|
|
45
46
|
deleteCredentialMetadata,
|
|
@@ -149,7 +150,7 @@ function pruneAssistantPhoneNumbers(
|
|
|
149
150
|
export function handleGetTwilioConfig(): Response {
|
|
150
151
|
const hasCredentials = hasTwilioCredentials();
|
|
151
152
|
const accountSid = hasCredentials
|
|
152
|
-
?
|
|
153
|
+
? getTwilioCredentials().accountSid
|
|
153
154
|
: undefined;
|
|
154
155
|
const raw = loadRawConfig();
|
|
155
156
|
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
@@ -213,8 +214,8 @@ export async function handleSetTwilioCredentials(
|
|
|
213
214
|
});
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
// Store credentials securely
|
|
217
|
-
const sidStored =
|
|
217
|
+
// Store credentials securely (async — writes broker + encrypted store)
|
|
218
|
+
const sidStored = await setSecureKeyAsync(
|
|
218
219
|
"credential:twilio:account_sid",
|
|
219
220
|
body.accountSid,
|
|
220
221
|
);
|
|
@@ -226,12 +227,12 @@ export async function handleSetTwilioCredentials(
|
|
|
226
227
|
});
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
const tokenStored =
|
|
230
|
+
const tokenStored = await setSecureKeyAsync(
|
|
230
231
|
"credential:twilio:auth_token",
|
|
231
232
|
body.authToken,
|
|
232
233
|
);
|
|
233
234
|
if (!tokenStored) {
|
|
234
|
-
|
|
235
|
+
await deleteSecureKeyAsync("credential:twilio:account_sid");
|
|
235
236
|
return Response.json({
|
|
236
237
|
success: false,
|
|
237
238
|
hasCredentials: false,
|
|
@@ -239,6 +240,11 @@ export async function handleSetTwilioCredentials(
|
|
|
239
240
|
});
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
const raw = loadRawConfig();
|
|
244
|
+
const twilio = (raw?.twilio ?? {}) as Record<string, unknown>;
|
|
245
|
+
twilio.accountSid = body.accountSid;
|
|
246
|
+
saveRawConfig({ ...raw, twilio });
|
|
247
|
+
|
|
242
248
|
upsertCredentialMetadata("twilio", "account_sid", {
|
|
243
249
|
injectionTemplates: [
|
|
244
250
|
{
|
|
@@ -275,9 +281,25 @@ export async function handleSetTwilioCredentials(
|
|
|
275
281
|
/**
|
|
276
282
|
* DELETE /v1/integrations/twilio/credentials
|
|
277
283
|
*/
|
|
278
|
-
export function handleClearTwilioCredentials(): Response {
|
|
279
|
-
|
|
280
|
-
|
|
284
|
+
export async function handleClearTwilioCredentials(): Promise<Response> {
|
|
285
|
+
const r1 = await deleteSecureKeyAsync("credential:twilio:account_sid");
|
|
286
|
+
const r2 = await deleteSecureKeyAsync("credential:twilio:auth_token");
|
|
287
|
+
|
|
288
|
+
if (r1 === "error" || r2 === "error") {
|
|
289
|
+
return Response.json(
|
|
290
|
+
{
|
|
291
|
+
success: false,
|
|
292
|
+
error: "Failed to delete Twilio credentials from secure storage",
|
|
293
|
+
},
|
|
294
|
+
{ status: 500 },
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const raw = loadRawConfig();
|
|
299
|
+
const twilio = (raw?.twilio ?? {}) as Record<string, unknown>;
|
|
300
|
+
delete twilio.accountSid;
|
|
301
|
+
saveRawConfig({ ...raw, twilio });
|
|
302
|
+
|
|
281
303
|
deleteCredentialMetadata("twilio", "account_sid");
|
|
282
304
|
deleteCredentialMetadata("twilio", "auth_token");
|
|
283
305
|
|
|
@@ -296,8 +318,7 @@ export async function handleListTwilioNumbers(): Promise<Response> {
|
|
|
296
318
|
});
|
|
297
319
|
}
|
|
298
320
|
|
|
299
|
-
const accountSid =
|
|
300
|
-
const authToken = getSecureKey("credential:twilio:auth_token")!;
|
|
321
|
+
const { accountSid, authToken } = getTwilioCredentials();
|
|
301
322
|
const numbers = await listIncomingPhoneNumbers(accountSid, authToken);
|
|
302
323
|
|
|
303
324
|
return Response.json({ success: true, hasCredentials: true, numbers });
|
|
@@ -323,8 +344,7 @@ export async function handleProvisionTwilioNumber(
|
|
|
323
344
|
country?: string;
|
|
324
345
|
areaCode?: string;
|
|
325
346
|
};
|
|
326
|
-
const accountSid =
|
|
327
|
-
const authToken = getSecureKey("credential:twilio:auth_token")!;
|
|
347
|
+
const { accountSid, authToken } = getTwilioCredentials();
|
|
328
348
|
const country = body.country ?? "US";
|
|
329
349
|
|
|
330
350
|
const available = await searchAvailableNumbers(
|
|
@@ -347,7 +367,7 @@ export async function handleProvisionTwilioNumber(
|
|
|
347
367
|
available[0].phoneNumber,
|
|
348
368
|
);
|
|
349
369
|
|
|
350
|
-
const phoneStored =
|
|
370
|
+
const phoneStored = await setSecureKeyAsync(
|
|
351
371
|
"credential:twilio:phone_number",
|
|
352
372
|
purchased.phoneNumber,
|
|
353
373
|
);
|
|
@@ -403,7 +423,7 @@ export async function handleAssignTwilioNumber(
|
|
|
403
423
|
);
|
|
404
424
|
}
|
|
405
425
|
|
|
406
|
-
const phoneStored =
|
|
426
|
+
const phoneStored = await setSecureKeyAsync(
|
|
407
427
|
"credential:twilio:phone_number",
|
|
408
428
|
body.phoneNumber,
|
|
409
429
|
);
|
|
@@ -424,8 +444,8 @@ export async function handleAssignTwilioNumber(
|
|
|
424
444
|
// Best-effort webhook configuration when credentials are available
|
|
425
445
|
let webhookWarning: string | undefined;
|
|
426
446
|
if (hasTwilioCredentials()) {
|
|
427
|
-
const acctSid =
|
|
428
|
-
|
|
447
|
+
const { accountSid: acctSid, authToken: acctToken } =
|
|
448
|
+
getTwilioCredentials();
|
|
429
449
|
const webhookResult = await syncTwilioWebhooks(
|
|
430
450
|
body.phoneNumber,
|
|
431
451
|
acctSid,
|
|
@@ -473,8 +493,7 @@ export async function handleReleaseTwilioNumber(
|
|
|
473
493
|
});
|
|
474
494
|
}
|
|
475
495
|
|
|
476
|
-
const accountSid =
|
|
477
|
-
const authToken = getSecureKey("credential:twilio:auth_token")!;
|
|
496
|
+
const { accountSid, authToken } = getTwilioCredentials();
|
|
478
497
|
|
|
479
498
|
await releasePhoneNumber(accountSid, authToken, phoneNumber);
|
|
480
499
|
|
|
@@ -486,7 +505,7 @@ export async function handleReleaseTwilioNumber(
|
|
|
486
505
|
|
|
487
506
|
const storedPhone = getSecureKey("credential:twilio:phone_number");
|
|
488
507
|
if (storedPhone === phoneNumber) {
|
|
489
|
-
|
|
508
|
+
await deleteSecureKeyAsync("credential:twilio:phone_number");
|
|
490
509
|
}
|
|
491
510
|
|
|
492
511
|
return Response.json({
|
|
@@ -521,8 +540,7 @@ export async function handleGetSmsCompliance(): Promise<Response> {
|
|
|
521
540
|
});
|
|
522
541
|
}
|
|
523
542
|
|
|
524
|
-
const accountSid =
|
|
525
|
-
const authToken = getSecureKey("credential:twilio:auth_token")!;
|
|
543
|
+
const { accountSid, authToken } = getTwilioCredentials();
|
|
526
544
|
|
|
527
545
|
const tollFreePrefixes = [
|
|
528
546
|
"+1800",
|
|
@@ -691,8 +709,7 @@ export async function handleSubmitTollfreeVerification(
|
|
|
691
709
|
);
|
|
692
710
|
}
|
|
693
711
|
|
|
694
|
-
const accountSid =
|
|
695
|
-
const authToken = getSecureKey("credential:twilio:auth_token")!;
|
|
712
|
+
const { accountSid, authToken } = getTwilioCredentials();
|
|
696
713
|
|
|
697
714
|
const submitParams: TollFreeVerificationSubmitParams = {
|
|
698
715
|
tollfreePhoneNumberSid: vp.tollfreePhoneNumberSid as string,
|
|
@@ -743,8 +760,7 @@ export async function handleUpdateTollfreeVerification(
|
|
|
743
760
|
});
|
|
744
761
|
}
|
|
745
762
|
|
|
746
|
-
const accountSid =
|
|
747
|
-
const authToken = getSecureKey("credential:twilio:auth_token")!;
|
|
763
|
+
const { accountSid, authToken } = getTwilioCredentials();
|
|
748
764
|
|
|
749
765
|
const currentVerification = await getTollFreeVerificationBySid(
|
|
750
766
|
accountSid,
|
|
@@ -827,8 +843,7 @@ export async function handleDeleteTollfreeVerification(
|
|
|
827
843
|
});
|
|
828
844
|
}
|
|
829
845
|
|
|
830
|
-
const accountSid =
|
|
831
|
-
const authToken = getSecureKey("credential:twilio:auth_token")!;
|
|
846
|
+
const { accountSid, authToken } = getTwilioCredentials();
|
|
832
847
|
|
|
833
848
|
await deleteTollFreeVerification(accountSid, authToken, verificationSid);
|
|
834
849
|
|
|
@@ -885,8 +900,7 @@ export async function handleSmsSendTest(req: Request): Promise<Response> {
|
|
|
885
900
|
});
|
|
886
901
|
}
|
|
887
902
|
|
|
888
|
-
const accountSid =
|
|
889
|
-
const authToken = getSecureKey("credential:twilio:auth_token")!;
|
|
903
|
+
const { accountSid, authToken } = getTwilioCredentials();
|
|
890
904
|
const text = body.text || "Test SMS from your Vellum assistant";
|
|
891
905
|
|
|
892
906
|
// Send via gateway's /deliver/sms endpoint
|
|
@@ -1001,8 +1015,7 @@ export async function handleSmsDoctor(): Promise<Response> {
|
|
|
1001
1015
|
getSecureKey("credential:twilio:phone_number") ||
|
|
1002
1016
|
"";
|
|
1003
1017
|
if (phoneNumber) {
|
|
1004
|
-
const accountSid =
|
|
1005
|
-
const authToken = getSecureKey("credential:twilio:auth_token")!;
|
|
1018
|
+
const { accountSid, authToken } = getTwilioCredentials();
|
|
1006
1019
|
const isTollFree =
|
|
1007
1020
|
phoneNumber.startsWith("+1") &&
|
|
1008
1021
|
["800", "888", "877", "866", "855", "844", "833"].some((p) =>
|
|
@@ -1178,7 +1191,7 @@ export function twilioRouteDefinitions(): RouteDefinition[] {
|
|
|
1178
1191
|
{
|
|
1179
1192
|
endpoint: "integrations/twilio/credentials",
|
|
1180
1193
|
method: "DELETE",
|
|
1181
|
-
handler: () => handleClearTwilioCredentials(),
|
|
1194
|
+
handler: async () => handleClearTwilioCredentials(),
|
|
1182
1195
|
},
|
|
1183
1196
|
{
|
|
1184
1197
|
endpoint: "integrations/twilio/numbers",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasTwilioCredentials } from "../calls/twilio-rest.js";
|
|
1
2
|
import { getSecureKey } from "../security/secure-keys.js";
|
|
2
3
|
|
|
3
4
|
interface IntegrationProbe {
|
|
@@ -29,9 +30,7 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
29
30
|
{
|
|
30
31
|
name: "SMS",
|
|
31
32
|
category: "messaging",
|
|
32
|
-
isConnected: () =>
|
|
33
|
-
!!getSecureKey("credential:twilio:account_sid") &&
|
|
34
|
-
!!getSecureKey("credential:twilio:auth_token"),
|
|
33
|
+
isConnected: () => hasTwilioCredentials(),
|
|
35
34
|
},
|
|
36
35
|
{
|
|
37
36
|
name: "Telegram",
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../tools/credentials/metadata-store.js";
|
|
13
13
|
import { getLogger } from "../util/logger.js";
|
|
14
14
|
import { refreshOAuth2Token, type TokenEndpointAuthMethod } from "./oauth2.js";
|
|
15
|
-
import { getSecureKey,
|
|
15
|
+
import { getSecureKey, setSecureKeyAsync } from "./secure-keys.js";
|
|
16
16
|
|
|
17
17
|
const log = getLogger("token-manager");
|
|
18
18
|
|
|
@@ -217,7 +217,12 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
217
217
|
throw err;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
if (
|
|
220
|
+
if (
|
|
221
|
+
!(await setSecureKeyAsync(
|
|
222
|
+
`credential:${service}:access_token`,
|
|
223
|
+
result.accessToken,
|
|
224
|
+
))
|
|
225
|
+
) {
|
|
221
226
|
throw new TokenExpiredError(
|
|
222
227
|
service,
|
|
223
228
|
`Failed to store refreshed access token for "${service}".`,
|
|
@@ -226,7 +231,10 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
226
231
|
|
|
227
232
|
if (result.refreshToken) {
|
|
228
233
|
if (
|
|
229
|
-
!
|
|
234
|
+
!(await setSecureKeyAsync(
|
|
235
|
+
`credential:${service}:refresh_token`,
|
|
236
|
+
result.refreshToken,
|
|
237
|
+
))
|
|
230
238
|
) {
|
|
231
239
|
throw new TokenExpiredError(
|
|
232
240
|
service,
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
* ToolDefinition or ToolContext types.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { compileApp } from "../../bundler/app-compiler.js";
|
|
11
12
|
import { setHomeBaseAppLink } from "../../home-base/app-link-store.js";
|
|
12
13
|
import { generateAppIcon } from "../../media/app-icon-generator.js";
|
|
13
|
-
import type {
|
|
14
|
-
|
|
14
|
+
import type {
|
|
15
|
+
AppDefinition,
|
|
16
|
+
EditEngineResult,
|
|
17
|
+
} from "../../memory/app-store.js";
|
|
18
|
+
import { getAppsDir, isMultifileApp } from "../../memory/app-store.js";
|
|
15
19
|
|
|
16
20
|
// ---------------------------------------------------------------------------
|
|
17
21
|
// Shared result type
|
|
@@ -45,6 +49,7 @@ export interface AppStoreWriter {
|
|
|
45
49
|
schemaJson: string;
|
|
46
50
|
htmlDefinition: string;
|
|
47
51
|
pages?: Record<string, string>;
|
|
52
|
+
formatVersion?: number;
|
|
48
53
|
}): AppDefinition;
|
|
49
54
|
updateApp(
|
|
50
55
|
id: string,
|
|
@@ -77,6 +82,28 @@ export type ProxyResolver = (
|
|
|
77
82
|
input: Record<string, unknown>,
|
|
78
83
|
) => Promise<ExecutorResult>;
|
|
79
84
|
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Path resolution — multifile apps default to src/ for file operations
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* For multifile (formatVersion 2) apps, prepend `src/` to paths that don't
|
|
91
|
+
* already target a known top-level directory (src/, dist/, records/).
|
|
92
|
+
* Legacy apps pass through unchanged.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveAppFilePath(app: AppDefinition, path: string): string {
|
|
95
|
+
if (!isMultifileApp(app)) return path;
|
|
96
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
97
|
+
if (
|
|
98
|
+
normalized.startsWith("src/") ||
|
|
99
|
+
normalized.startsWith("dist/") ||
|
|
100
|
+
normalized.startsWith("records/")
|
|
101
|
+
) {
|
|
102
|
+
return normalized;
|
|
103
|
+
}
|
|
104
|
+
return `src/${normalized}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
// ---------------------------------------------------------------------------
|
|
81
108
|
// app_create
|
|
82
109
|
// ---------------------------------------------------------------------------
|
|
@@ -90,6 +117,8 @@ export interface AppCreateInput {
|
|
|
90
117
|
auto_open?: boolean;
|
|
91
118
|
set_as_home_base?: boolean;
|
|
92
119
|
preview?: Record<string, unknown>;
|
|
120
|
+
/** When provided, controls multifile scaffold behavior. */
|
|
121
|
+
featureFlags?: { multifileEnabled: boolean };
|
|
93
122
|
}
|
|
94
123
|
|
|
95
124
|
export async function executeAppCreate(
|
|
@@ -146,15 +175,71 @@ export async function executeAppCreate(
|
|
|
146
175
|
const rawIcon = preview?.icon as string | undefined;
|
|
147
176
|
const icon = rawIcon && !rawIcon.startsWith("http") ? rawIcon : undefined;
|
|
148
177
|
|
|
178
|
+
const multifileEnabled = input.featureFlags?.multifileEnabled === true;
|
|
179
|
+
|
|
149
180
|
const app = store.createApp({
|
|
150
181
|
name,
|
|
151
182
|
description,
|
|
152
183
|
icon,
|
|
153
184
|
schemaJson,
|
|
154
|
-
htmlDefinition,
|
|
155
|
-
pages,
|
|
185
|
+
htmlDefinition: multifileEnabled ? "" : htmlDefinition,
|
|
186
|
+
pages: multifileEnabled ? undefined : pages,
|
|
187
|
+
formatVersion: multifileEnabled ? 2 : undefined,
|
|
156
188
|
});
|
|
157
189
|
|
|
190
|
+
// Scaffold multifile app with src/ files and compile to dist/
|
|
191
|
+
if (multifileEnabled) {
|
|
192
|
+
const htmlSafeName = name
|
|
193
|
+
.replace(/&/g, "&")
|
|
194
|
+
.replace(/</g, "<")
|
|
195
|
+
.replace(/>/g, ">")
|
|
196
|
+
.replace(/"/g, """);
|
|
197
|
+
const jsxSafeName = name.replace(/[<>{}&"']/g, "");
|
|
198
|
+
|
|
199
|
+
const indexHtml =
|
|
200
|
+
typeof input.html === "string"
|
|
201
|
+
? input.html
|
|
202
|
+
: `<!DOCTYPE html>
|
|
203
|
+
<html lang="en">
|
|
204
|
+
<head>
|
|
205
|
+
<meta charset="UTF-8">
|
|
206
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
207
|
+
<title>${htmlSafeName}</title>
|
|
208
|
+
</head>
|
|
209
|
+
<body>
|
|
210
|
+
<div id="app"></div>
|
|
211
|
+
</body>
|
|
212
|
+
</html>`;
|
|
213
|
+
|
|
214
|
+
const mainTsx = `import { render } from 'preact';
|
|
215
|
+
|
|
216
|
+
function App() {
|
|
217
|
+
return <div>{"Hello, ${jsxSafeName}!"}</div>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
render(<App />, document.getElementById('app')!);
|
|
221
|
+
`;
|
|
222
|
+
|
|
223
|
+
store.writeAppFile(app.id, "src/index.html", indexHtml);
|
|
224
|
+
store.writeAppFile(app.id, "src/main.tsx", mainTsx);
|
|
225
|
+
|
|
226
|
+
// Compile src/ → dist/
|
|
227
|
+
const { join } = await import("node:path");
|
|
228
|
+
const appDir = join(getAppsDir(), app.id);
|
|
229
|
+
const compileResult = await compileApp(appDir);
|
|
230
|
+
if (!compileResult.ok) {
|
|
231
|
+
return {
|
|
232
|
+
content: JSON.stringify({
|
|
233
|
+
...app,
|
|
234
|
+
compile_errors: compileResult.errors,
|
|
235
|
+
compile_warnings: compileResult.warnings,
|
|
236
|
+
compile_duration_ms: compileResult.durationMs,
|
|
237
|
+
}),
|
|
238
|
+
isError: false,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
158
243
|
if (input.set_as_home_base) {
|
|
159
244
|
setHomeBaseAppLink(app.id, "personalized");
|
|
160
245
|
}
|
|
@@ -305,6 +390,23 @@ export function executeAppFileList(
|
|
|
305
390
|
store: AppStoreReader,
|
|
306
391
|
): ExecutorResult {
|
|
307
392
|
const files = store.listAppFiles(input.app_id);
|
|
393
|
+
const app = store.getApp(input.app_id);
|
|
394
|
+
|
|
395
|
+
if (app && isMultifileApp(app)) {
|
|
396
|
+
// Separate build output paths from source paths without mutating the
|
|
397
|
+
// file path strings — consumers need clean paths for subsequent tool calls.
|
|
398
|
+
const buildOutputPaths = files.filter((f) =>
|
|
399
|
+
f.replace(/\\/g, "/").startsWith("dist/"),
|
|
400
|
+
);
|
|
401
|
+
return {
|
|
402
|
+
content: JSON.stringify({
|
|
403
|
+
files,
|
|
404
|
+
buildOutput: buildOutputPaths,
|
|
405
|
+
}),
|
|
406
|
+
isError: false,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
308
410
|
return { content: JSON.stringify(files), isError: false };
|
|
309
411
|
}
|
|
310
412
|
|
|
@@ -326,7 +428,9 @@ export function executeAppFileRead(
|
|
|
326
428
|
const offset = input.offset ?? 1;
|
|
327
429
|
const limit = input.limit;
|
|
328
430
|
|
|
329
|
-
const
|
|
431
|
+
const app = store.getApp(input.app_id);
|
|
432
|
+
const resolvedPath = app ? resolveAppFilePath(app, input.path) : input.path;
|
|
433
|
+
const raw = store.readAppFile(input.app_id, resolvedPath);
|
|
330
434
|
const allLines = raw.split("\n");
|
|
331
435
|
const startIndex = Math.max(0, offset - 1);
|
|
332
436
|
const sliced =
|
|
@@ -368,10 +472,13 @@ export function executeAppFileEdit(
|
|
|
368
472
|
};
|
|
369
473
|
}
|
|
370
474
|
|
|
475
|
+
const app = store.getApp(input.app_id);
|
|
476
|
+
const resolvedPath = app ? resolveAppFilePath(app, input.path) : input.path;
|
|
477
|
+
|
|
371
478
|
const replaceAll = input.replace_all ?? false;
|
|
372
479
|
const result = store.editAppFile(
|
|
373
480
|
input.app_id,
|
|
374
|
-
|
|
481
|
+
resolvedPath,
|
|
375
482
|
input.old_string,
|
|
376
483
|
input.new_string,
|
|
377
484
|
replaceAll,
|
|
@@ -406,9 +513,10 @@ export function executeAppFileWrite(
|
|
|
406
513
|
};
|
|
407
514
|
}
|
|
408
515
|
|
|
409
|
-
|
|
516
|
+
const resolvedPath = resolveAppFilePath(app, input.path);
|
|
517
|
+
store.writeAppFile(input.app_id, resolvedPath, input.content);
|
|
410
518
|
return {
|
|
411
|
-
content: JSON.stringify({ written: true, path:
|
|
519
|
+
content: JSON.stringify({ written: true, path: resolvedPath }),
|
|
412
520
|
isError: false,
|
|
413
521
|
status: input.status,
|
|
414
522
|
};
|
|
@@ -25,6 +25,30 @@ function getDownloadsDir(): string {
|
|
|
25
25
|
return dir;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Wraps a promise with a timeout to prevent indefinite hangs. */
|
|
29
|
+
export function withTimeout<T>(
|
|
30
|
+
promise: Promise<T>,
|
|
31
|
+
ms: number,
|
|
32
|
+
label: string,
|
|
33
|
+
): Promise<T> {
|
|
34
|
+
return new Promise<T>((resolve, reject) => {
|
|
35
|
+
const timer = setTimeout(
|
|
36
|
+
() => reject(new Error(`${label} timed out after ${ms}ms`)),
|
|
37
|
+
ms,
|
|
38
|
+
);
|
|
39
|
+
promise.then(
|
|
40
|
+
(val) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
resolve(val);
|
|
43
|
+
},
|
|
44
|
+
(err) => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
reject(err);
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
export type DownloadInfo = { path: string; filename: string };
|
|
29
53
|
|
|
30
54
|
type BrowserContext = {
|
|
@@ -679,7 +703,7 @@ class BrowserManager {
|
|
|
679
703
|
try {
|
|
680
704
|
const filename = dl.suggestedFilename();
|
|
681
705
|
const destPath = join(getDownloadsDir(), `${Date.now()}-${filename}`);
|
|
682
|
-
await dl.saveAs(destPath);
|
|
706
|
+
await withTimeout(dl.saveAs(destPath), 120_000, "Download save");
|
|
683
707
|
const info: DownloadInfo = { path: destPath, filename };
|
|
684
708
|
|
|
685
709
|
// Resolve a pending waiter if one exists, otherwise store for later retrieval
|
|
@@ -696,7 +720,11 @@ class BrowserManager {
|
|
|
696
720
|
|
|
697
721
|
log.info({ sessionId, filename, path: destPath }, "Download completed");
|
|
698
722
|
} catch (err) {
|
|
699
|
-
const failure = await
|
|
723
|
+
const failure = await withTimeout(
|
|
724
|
+
dl.failure(),
|
|
725
|
+
5_000,
|
|
726
|
+
"Download failure check",
|
|
727
|
+
).catch(() => null);
|
|
700
728
|
log.warn({ err, failure, sessionId }, "Download failed");
|
|
701
729
|
|
|
702
730
|
// Reject any pending waiters
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* make cleanup decisions (e.g. only kill Chrome if *we* launched it).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { spawn as spawnChild } from "node:child_process";
|
|
10
|
+
import { execSync, spawn as spawnChild } from "node:child_process";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join as pathJoin } from "node:path";
|
|
13
13
|
|
|
@@ -51,14 +51,23 @@ export interface EnsureChromeOptions {
|
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Returns `true` when a CDP endpoint is responding at the given base URL
|
|
54
|
+
* Returns `true` when a CDP endpoint is responding at the given base URL
|
|
55
|
+
* and has at least one open page tab. A CDP endpoint with zero tabs is
|
|
56
|
+
* stale and unusable — callers should treat it as not ready.
|
|
55
57
|
*/
|
|
56
58
|
export async function isCdpReady(
|
|
57
59
|
cdpBase: string = DEFAULT_CDP_BASE,
|
|
58
60
|
): Promise<boolean> {
|
|
59
61
|
try {
|
|
60
62
|
const res = await fetch(`${cdpBase}/json/version`);
|
|
61
|
-
|
|
63
|
+
if (!res.ok) return false;
|
|
64
|
+
|
|
65
|
+
// Verify there's at least one page tab — a CDP endpoint with no tabs
|
|
66
|
+
// is a stale Chrome process that should be relaunched.
|
|
67
|
+
const listRes = await fetch(`${cdpBase}/json/list`);
|
|
68
|
+
if (!listRes.ok) return false;
|
|
69
|
+
const targets = (await listRes.json()) as Array<{ type: string }>;
|
|
70
|
+
return targets.some((t) => t.type === "page");
|
|
62
71
|
} catch {
|
|
63
72
|
return false;
|
|
64
73
|
}
|
|
@@ -86,6 +95,25 @@ export async function ensureChromeWithCdp(
|
|
|
86
95
|
return { baseUrl, launchedByUs: false, userDataDir };
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
// If CDP is responding but has no tabs (stale), kill the process holding the port.
|
|
99
|
+
try {
|
|
100
|
+
const versionRes = await fetch(`${baseUrl}/json/version`);
|
|
101
|
+
if (versionRes.ok) {
|
|
102
|
+
// Stale Chrome — CDP up but no tabs. Kill it so we can relaunch.
|
|
103
|
+
try {
|
|
104
|
+
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, {
|
|
105
|
+
stdio: "ignore",
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
// Ignore — process may have already exited.
|
|
109
|
+
}
|
|
110
|
+
// Brief wait for port to clear.
|
|
111
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// CDP not responding at all — port is free, proceed to launch.
|
|
115
|
+
}
|
|
116
|
+
|
|
89
117
|
const args = [
|
|
90
118
|
`--remote-debugging-port=${port}`,
|
|
91
119
|
`--force-renderer-accessibility`,
|
|
@@ -9,10 +9,10 @@ import { RiskLevel } from "../../permissions/types.js";
|
|
|
9
9
|
import type { ToolDefinition } from "../../providers/types.js";
|
|
10
10
|
import type { TokenEndpointAuthMethod } from "../../security/oauth2.js";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
deleteSecureKeyAsync,
|
|
13
13
|
getSecureKey,
|
|
14
14
|
listSecureKeys,
|
|
15
|
-
|
|
15
|
+
setSecureKeyAsync,
|
|
16
16
|
} from "../../security/secure-keys.js";
|
|
17
17
|
import { getLogger } from "../../util/logger.js";
|
|
18
18
|
import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
|
|
@@ -381,7 +381,7 @@ class CredentialStoreTool implements Tool {
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
const key = `credential:${service}:${field}`;
|
|
384
|
-
const ok =
|
|
384
|
+
const ok = await setSecureKeyAsync(key, value);
|
|
385
385
|
if (!ok) {
|
|
386
386
|
return {
|
|
387
387
|
content: "Error: failed to store credential",
|
|
@@ -494,7 +494,7 @@ class CredentialStoreTool implements Tool {
|
|
|
494
494
|
}
|
|
495
495
|
|
|
496
496
|
const key = `credential:${service}:${field}`;
|
|
497
|
-
const result =
|
|
497
|
+
const result = await deleteSecureKeyAsync(key);
|
|
498
498
|
if (result === "error") {
|
|
499
499
|
return {
|
|
500
500
|
content: `Error: failed to delete credential ${service}/${field} from secure storage`,
|
|
@@ -564,7 +564,9 @@ class CredentialStoreTool implements Tool {
|
|
|
564
564
|
const promptPolicy = toPolicyFromInput(promptPolicyInput);
|
|
565
565
|
|
|
566
566
|
// Parse and validate injection templates (same logic as store action)
|
|
567
|
-
const promptRawTemplates = input.injection_templates as
|
|
567
|
+
const promptRawTemplates = input.injection_templates as
|
|
568
|
+
| unknown[]
|
|
569
|
+
| undefined;
|
|
568
570
|
let promptInjectionTemplates: CredentialInjectionTemplate[] | undefined;
|
|
569
571
|
if (promptRawTemplates !== undefined) {
|
|
570
572
|
if (!Array.isArray(promptRawTemplates)) {
|
|
@@ -732,7 +734,7 @@ class CredentialStoreTool implements Tool {
|
|
|
732
734
|
|
|
733
735
|
// Default: persist to keychain
|
|
734
736
|
const key = `credential:${service}:${field}`;
|
|
735
|
-
const ok =
|
|
737
|
+
const ok = await setSecureKeyAsync(key, result.value);
|
|
736
738
|
if (!ok) {
|
|
737
739
|
return {
|
|
738
740
|
content: "Error: failed to store credential",
|
|
@@ -752,7 +754,7 @@ class CredentialStoreTool implements Tool {
|
|
|
752
754
|
"metadata write failed after storing credential",
|
|
753
755
|
);
|
|
754
756
|
}
|
|
755
|
-
|
|
757
|
+
const promptMeta = getCredentialMetadata(service, field);
|
|
756
758
|
const promptCredIdSuffix = promptMeta
|
|
757
759
|
? ` (credential_id: ${promptMeta.credentialId})`
|
|
758
760
|
: "";
|
package/src/tools/executor.ts
CHANGED
|
@@ -155,6 +155,10 @@ export class ToolExecutor {
|
|
|
155
155
|
);
|
|
156
156
|
// Buffer so the shell's own timeout fires first and handles cleanup
|
|
157
157
|
toolTimeoutMs = (shellTimeoutSec + 5) * 1000;
|
|
158
|
+
} else if (name === "claude_code") {
|
|
159
|
+
// Claude Code spawns a subprocess that manages its own turn limits
|
|
160
|
+
// (maxTurns). Give it a generous timeout so it isn't killed mid-task.
|
|
161
|
+
toolTimeoutMs = 10 * 60 * 1000; // 10 minutes
|
|
158
162
|
} else {
|
|
159
163
|
const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
|
|
160
164
|
toolTimeoutMs = safeTimeoutMs(rawTimeoutSec);
|