@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292019.a6f6d85
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/.env.example +2 -0
- package/ARCHITECTURE.md +23 -0
- package/docs/automatic-done.md +32 -0
- package/docs/desktop-browser-cli.md +3 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
- package/node_modules/@vellumai/environments/src/shell.ts +22 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/openapi.yaml +270 -10
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-provider.test.ts +2 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
- package/src/__tests__/config-get-vision-flag.test.ts +1 -1
- package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
- package/src/__tests__/conversation-evictor.test.ts +54 -0
- package/src/__tests__/conversation-pairing.test.ts +112 -0
- package/src/__tests__/document-append-idempotency.test.ts +13 -0
- package/src/__tests__/document-create-dedupe.test.ts +13 -0
- package/src/__tests__/document-find-replace.test.ts +13 -0
- package/src/__tests__/document-revision-history.test.ts +390 -0
- package/src/__tests__/document-revision.test.ts +425 -0
- package/src/__tests__/document-tool-security.test.ts +13 -0
- package/src/__tests__/document-update-default-surface.test.ts +13 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
- package/src/__tests__/gateway-flag-listener.test.ts +59 -6
- package/src/__tests__/headless-browser-navigate.test.ts +20 -30
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -2
- package/src/__tests__/identity-routes.test.ts +4 -3
- package/src/__tests__/llm-catalog-parity.test.ts +4 -4
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/notification-platform-adapter.test.ts +83 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/openai-provider.test.ts +11 -11
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/public-ingress-urls.test.ts +26 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
- package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
- package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
- package/src/acp/agent-process.ts +6 -1
- package/src/acp/client-handler.ts +6 -1
- package/src/agent/loop.ts +29 -6
- package/src/api/events/document-editor-show.ts +5 -0
- package/src/api/events/document-editor-update.ts +10 -0
- package/src/api/events/tool-result.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
- package/src/calls/channel-admission-reader.ts +15 -9
- package/src/calls/twilio-provider.ts +14 -31
- package/src/calls/voice-session-bridge.ts +18 -1
- package/src/cli/commands/browser.ts +1 -2
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
- package/src/cli/lib/bundled-marketplace.json +41 -2
- package/src/cli/lib/inspect-plugin.ts +15 -15
- package/src/cli/lib/list-installed-plugins.ts +29 -28
- package/src/cli/lib/plugin-artifact.ts +74 -63
- package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
- package/src/cli/lib/plugin-details.ts +27 -42
- package/src/cli/lib/plugin-marketplace.ts +6 -8
- package/src/cli/lib/plugin-surfaces.ts +8 -7
- package/src/cli.ts +1 -3
- package/src/config/__tests__/chat-settings.test.ts +121 -0
- package/src/config/assistant-feature-flags.ts +28 -23
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/chat-settings.ts +100 -0
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/feature-flag-cache.ts +7 -25
- package/src/config/feature-flag-registry.json +1 -9
- package/src/config/loader.ts +5 -1
- package/src/config/profile-text-generation.ts +25 -21
- package/src/config/schemas/conversations.ts +14 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/notifications.ts +8 -1
- package/src/contacts/gateway-channel-read.ts +1 -1
- package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
- package/src/conversations/__tests__/auto-archive.test.ts +304 -0
- package/src/conversations/auto-archive-activity.ts +61 -0
- package/src/conversations/auto-archive.ts +201 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop.ts +11 -6
- package/src/daemon/conversation-evictor.ts +50 -16
- package/src/daemon/doordash-steps.ts +1 -3
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
- package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
- package/src/daemon/handlers/config-a2a.ts +75 -2
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/lifecycle.ts +15 -2
- package/src/daemon/shutdown-handlers.ts +3 -0
- package/src/daemon/turn-finalization.ts +4 -0
- package/src/desktop/desktop-browser-endpoint.ts +11 -2
- package/src/desktop/desktop-chrome-policy.test.ts +1 -0
- package/src/desktop/desktop-chrome-policy.ts +7 -1
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/desktop/desktop-session-manager.ts +5 -3
- package/src/documents/document-revisions-store.ts +153 -0
- package/src/documents/document-store.ts +471 -133
- package/src/followups/followup-store.ts +0 -4
- package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
- package/src/inbound/oauth-callback-url.ts +30 -1
- package/src/inbound/public-ingress-urls.ts +12 -2
- package/src/ipc/gateway-client.test.ts +40 -0
- package/src/ipc/gateway-client.ts +12 -29
- package/src/ipc/gateway-flag-listener.ts +7 -10
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/protocol.test.ts +77 -0
- package/src/live-voice/__tests__/share-targets.test.ts +163 -0
- package/src/live-voice/live-voice-session.ts +23 -6
- package/src/live-voice/protocol.ts +22 -0
- package/src/live-voice/share-targets.ts +225 -0
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
- package/src/mcp/client.ts +1 -19
- package/src/mcp/mcp-oauth-provider.ts +66 -49
- package/src/media/openrouter-image-service.ts +39 -22
- package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
- package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
- package/src/monitoring/control.ts +2 -1
- package/src/monitoring/oom-inheritance-sweep.ts +96 -0
- package/src/monitoring/oom-kill-reporter.ts +469 -0
- package/src/monitoring/resource-sampler.ts +30 -0
- package/src/notifications/README.md +53 -4
- package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
- package/src/notifications/__tests__/broadcaster.test.ts +558 -6
- package/src/notifications/__tests__/completion-policy.test.ts +37 -0
- package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
- package/src/notifications/adapters/platform.ts +14 -0
- package/src/notifications/assistant-initiated-thread.ts +101 -0
- package/src/notifications/broadcaster.ts +94 -16
- package/src/notifications/chat-reply-policy.ts +11 -0
- package/src/notifications/completion-policy.ts +12 -2
- package/src/notifications/conversation-pairing.ts +15 -70
- package/src/notifications/emit-signal.ts +34 -3
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connect-orchestrator.ts +1 -2
- package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
- package/src/persistence/conversation-auto-archive.ts +170 -0
- package/src/persistence/conversation-crud.ts +2 -1
- package/src/persistence/conversation-queries.ts +15 -8
- package/src/persistence/embeddings/embed.test.ts +96 -0
- package/src/persistence/embeddings/embed.ts +20 -2
- package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
- package/src/persistence/embeddings/embedding-gemini.ts +39 -1
- package/src/persistence/embeddings/embedding-local.ts +6 -0
- package/src/persistence/embeddings/qdrant-manager.ts +5 -0
- package/src/persistence/job-utils.ts +10 -0
- package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
- package/src/persistence/migrations/384-add-document-revision.ts +21 -0
- package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
- package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
- package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
- package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
- package/src/persistence/schema/conversations.ts +1 -0
- package/src/persistence/schema/documents.ts +20 -0
- package/src/persistence/steps.ts +6 -0
- package/src/platform/managed-speech.ts +42 -46
- package/src/plugin-api/vision-support.test.ts +21 -21
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +1 -1
- package/src/providers/fireworks/client.test.ts +168 -0
- package/src/providers/fireworks/client.ts +24 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/ionet/client.test.ts +143 -0
- package/src/providers/ionet/client.ts +37 -0
- package/src/providers/model-catalog.ts +141 -84
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +23 -1
- package/src/providers/retry.ts +56 -4
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +10 -2
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/assistant-event-hub.ts +23 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
- package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
- package/src/runtime/channel-retry-sweep.ts +1 -2
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
- package/src/runtime/migrations/vbundle-builder.ts +19 -3
- package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
- package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
- package/src/runtime/migrations/vbundle-validator.ts +54 -1
- package/src/runtime/pending-interactions.ts +20 -2
- package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
- package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
- package/src/runtime/routes/contact-routes.ts +2 -2
- package/src/runtime/routes/conversation-list-routes.ts +10 -2
- package/src/runtime/routes/conversation-query-routes.ts +48 -10
- package/src/runtime/routes/documents-routes.ts +185 -47
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -0
- package/src/runtime/routes/host-bash-routes.ts +1 -0
- package/src/runtime/routes/host-browser-routes.ts +15 -0
- package/src/runtime/routes/host-cu-routes.ts +4 -1
- package/src/runtime/routes/host-file-routes.ts +1 -0
- package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
- package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
- package/src/runtime/routes/host-transfer-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +3 -3
- package/src/runtime/routes/integrations/a2a.ts +4 -1
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/notification-routes.ts +1 -2
- package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
- package/src/runtime/routes/user-route-dispatcher.ts +5 -1
- package/src/runtime/verification-templates.ts +1 -2
- package/src/sequence/analytics.ts +0 -4
- package/src/sequence/engine.ts +1 -11
- package/src/sequence/guardrails.ts +1 -6
- package/src/sequence/importer.ts +0 -8
- package/src/sequence/store.ts +0 -10
- package/src/signals/user-message.ts +0 -6
- package/src/skills/catalog-install.ts +52 -23
- package/src/tools/browser/browser-execution.ts +20 -23
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
- package/src/tools/document/document-tool.ts +13 -11
- package/src/tools/executor.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +4 -2
- package/src/tools/network/__tests__/web-search.test.ts +105 -0
- package/src/tools/network/web-fetch.ts +2 -4
- package/src/tools/network/web-search.ts +239 -12
- package/src/tools/shared/filesystem/format-diff.ts +1 -2
- package/src/tools/shared/shell-output.test.ts +42 -0
- package/src/tools/shared/shell-output.ts +27 -4
- package/src/tools/terminal/shell.ts +3 -3
- package/src/tools/ui-surface/visual-validation.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/__tests__/oom-priority.test.ts +55 -0
- package/src/util/__tests__/worker-memory.test.ts +16 -0
- package/src/util/debounce.ts +0 -4
- package/src/util/oom-priority.ts +79 -0
- package/src/util/platform.ts +0 -2
- package/src/util/plugin-manifest.ts +5 -7
- package/src/util/pricing.ts +25 -0
- package/src/util/worker-memory.ts +9 -3
- package/src/util/worker-process.ts +8 -0
- package/src/workspace/byok-default-profile-ensure.ts +11 -0
- package/src/workspace/hatched-date.ts +11 -4
- package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
- package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -2397,6 +2397,60 @@ describe("desktop skill preactivation", () => {
|
|
|
2397
2397
|
expect(result.promptDuringLoop).toContain("skill_execute");
|
|
2398
2398
|
});
|
|
2399
2399
|
|
|
2400
|
+
test("a shared-screen turn is offered the surface's controls beside the instructions", async () => {
|
|
2401
|
+
registerHubClient({
|
|
2402
|
+
hub: assistantEventHub,
|
|
2403
|
+
clientId: "annotation-client",
|
|
2404
|
+
interfaceId: "macos",
|
|
2405
|
+
actorPrincipalId: "user-123",
|
|
2406
|
+
capabilities: ["host_cu", "host_cu_annotate"],
|
|
2407
|
+
});
|
|
2408
|
+
const result = await preactivatedFor({
|
|
2409
|
+
routingLeg: "escalated",
|
|
2410
|
+
macosDesktopSession: true,
|
|
2411
|
+
screenSharing: true,
|
|
2412
|
+
actorPrincipalId: "user-123",
|
|
2413
|
+
shareTargets: {
|
|
2414
|
+
targets: [
|
|
2415
|
+
{
|
|
2416
|
+
id: "t1",
|
|
2417
|
+
label: "root_Filters",
|
|
2418
|
+
role: "AXButton",
|
|
2419
|
+
section: "Toolbar",
|
|
2420
|
+
x: 0.8,
|
|
2421
|
+
y: 0.05,
|
|
2422
|
+
width: 0.05,
|
|
2423
|
+
height: 0.03,
|
|
2424
|
+
},
|
|
2425
|
+
],
|
|
2426
|
+
total: 1,
|
|
2427
|
+
},
|
|
2428
|
+
});
|
|
2429
|
+
expect(result.promptDuringLoop).toContain("ID: screen-annotation");
|
|
2430
|
+
expect(result.promptDuringLoop).toContain("<shared_screen_controls>");
|
|
2431
|
+
expect(result.promptDuringLoop).toContain(
|
|
2432
|
+
'- "root_Filters" (button, top right, in "Toolbar")',
|
|
2433
|
+
);
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
test("a shared-screen turn with no snapshot keeps the instructions alone", async () => {
|
|
2437
|
+
registerHubClient({
|
|
2438
|
+
hub: assistantEventHub,
|
|
2439
|
+
clientId: "annotation-client",
|
|
2440
|
+
interfaceId: "macos",
|
|
2441
|
+
actorPrincipalId: "user-123",
|
|
2442
|
+
capabilities: ["host_cu", "host_cu_annotate"],
|
|
2443
|
+
});
|
|
2444
|
+
const result = await preactivatedFor({
|
|
2445
|
+
routingLeg: "escalated",
|
|
2446
|
+
macosDesktopSession: true,
|
|
2447
|
+
screenSharing: true,
|
|
2448
|
+
actorPrincipalId: "user-123",
|
|
2449
|
+
});
|
|
2450
|
+
expect(result.promptDuringLoop).toContain("ID: screen-annotation");
|
|
2451
|
+
expect(result.promptDuringLoop).not.toContain("<shared_screen_controls>");
|
|
2452
|
+
});
|
|
2453
|
+
|
|
2400
2454
|
test.each([
|
|
2401
2455
|
{ screenSharing: false },
|
|
2402
2456
|
{ actorPrincipalId: "other-user" },
|
|
@@ -26,11 +26,15 @@ import {
|
|
|
26
26
|
type AdmissionPolicy,
|
|
27
27
|
isAdmissionPolicy,
|
|
28
28
|
} from "@vellumai/gateway-client";
|
|
29
|
+
import { z } from "zod";
|
|
29
30
|
|
|
30
31
|
import type { ChannelId } from "../channels/types.js";
|
|
31
32
|
import { ipcCall } from "../ipc/gateway-client.js";
|
|
32
33
|
|
|
33
34
|
const CACHE_TTL_MS = 5_000;
|
|
35
|
+
const AdmissionPolicyResponseSchema = z.object({
|
|
36
|
+
policy: z.unknown(),
|
|
37
|
+
});
|
|
34
38
|
|
|
35
39
|
// Short IPC timeout so admission fails closed PROMPTLY: a gateway that accepts
|
|
36
40
|
// the socket but stalls must never delay a live call handshake. 1s is generous
|
|
@@ -64,20 +68,22 @@ async function fetchAdmissionPolicy(
|
|
|
64
68
|
// ipcCall() returns undefined on transport failure (socket not found,
|
|
65
69
|
// timeout, parse error). That, a throw, or a malformed shape is a FAILURE:
|
|
66
70
|
// fail closed without caching so the next setup re-attempts the IPC.
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
const parsed = AdmissionPolicyResponseSchema.safeParse(
|
|
72
|
+
await ipcCall(
|
|
73
|
+
"get_channel_admission_policy",
|
|
74
|
+
{ channelType },
|
|
75
|
+
ADMISSION_IPC_TIMEOUT_MS,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
72
78
|
|
|
73
|
-
if (
|
|
79
|
+
if (!parsed.success) {
|
|
74
80
|
return { ok: false };
|
|
75
81
|
}
|
|
76
|
-
if (
|
|
77
|
-
return { ok: true, policy:
|
|
82
|
+
if (isAdmissionPolicy(parsed.data.policy)) {
|
|
83
|
+
return { ok: true, policy: parsed.data.policy };
|
|
78
84
|
}
|
|
79
85
|
// Explicit "no enforcement" — the gateway successfully answered. Cacheable.
|
|
80
|
-
if (
|
|
86
|
+
if (parsed.data.policy === null) {
|
|
81
87
|
return { ok: true, policy: null };
|
|
82
88
|
}
|
|
83
89
|
// Anything else (missing/invalid policy field) is a malformed shape.
|
|
@@ -23,27 +23,10 @@ const log = getLogger("twilio-provider");
|
|
|
23
23
|
export class TwilioVoiceProvider implements VoiceProvider {
|
|
24
24
|
readonly name = "twilio";
|
|
25
25
|
|
|
26
|
-
// ── Credential helpers ──────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
private async getCredentials(): Promise<{
|
|
29
|
-
accountSid: string;
|
|
30
|
-
authToken: string;
|
|
31
|
-
}> {
|
|
32
|
-
return await getTwilioCredentials();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
private authHeader(accountSid: string, authToken: string): string {
|
|
36
|
-
return twilioAuthHeader(accountSid, authToken);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private baseUrl(accountSid: string): string {
|
|
40
|
-
return twilioBaseUrl(accountSid);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
26
|
// ── VoiceProvider interface ─────────────────────────────────────────
|
|
44
27
|
|
|
45
28
|
async initiateCall(opts: InitiateCallOptions): Promise<{ callSid: string }> {
|
|
46
|
-
const { accountSid, authToken } = await
|
|
29
|
+
const { accountSid, authToken } = await getTwilioCredentials();
|
|
47
30
|
|
|
48
31
|
const body = new URLSearchParams({
|
|
49
32
|
From: opts.from,
|
|
@@ -80,10 +63,10 @@ export class TwilioVoiceProvider implements VoiceProvider {
|
|
|
80
63
|
|
|
81
64
|
log.info({ from: opts.from, to: opts.to }, "Initiating Twilio call");
|
|
82
65
|
|
|
83
|
-
const res = await fetch(`${
|
|
66
|
+
const res = await fetch(`${twilioBaseUrl(accountSid)}/Calls.json`, {
|
|
84
67
|
method: "POST",
|
|
85
68
|
headers: {
|
|
86
|
-
Authorization:
|
|
69
|
+
Authorization: twilioAuthHeader(accountSid, authToken),
|
|
87
70
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
88
71
|
},
|
|
89
72
|
body: body.toString(),
|
|
@@ -108,18 +91,18 @@ export class TwilioVoiceProvider implements VoiceProvider {
|
|
|
108
91
|
}
|
|
109
92
|
|
|
110
93
|
async endCall(callSid: string): Promise<void> {
|
|
111
|
-
const { accountSid, authToken } = await
|
|
94
|
+
const { accountSid, authToken } = await getTwilioCredentials();
|
|
112
95
|
|
|
113
96
|
log.info({ callSid }, "Ending Twilio call");
|
|
114
97
|
|
|
115
98
|
const body = new URLSearchParams({ Status: "completed" });
|
|
116
99
|
|
|
117
100
|
const res = await fetch(
|
|
118
|
-
`${
|
|
101
|
+
`${twilioBaseUrl(accountSid)}/Calls/${callSid}.json`,
|
|
119
102
|
{
|
|
120
103
|
method: "POST",
|
|
121
104
|
headers: {
|
|
122
|
-
Authorization:
|
|
105
|
+
Authorization: twilioAuthHeader(accountSid, authToken),
|
|
123
106
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
124
107
|
},
|
|
125
108
|
body: body.toString(),
|
|
@@ -143,14 +126,14 @@ export class TwilioVoiceProvider implements VoiceProvider {
|
|
|
143
126
|
}
|
|
144
127
|
|
|
145
128
|
async getCallStatus(callSid: string): Promise<string> {
|
|
146
|
-
const { accountSid, authToken } = await
|
|
129
|
+
const { accountSid, authToken } = await getTwilioCredentials();
|
|
147
130
|
|
|
148
131
|
const res = await fetch(
|
|
149
|
-
`${
|
|
132
|
+
`${twilioBaseUrl(accountSid)}/Calls/${callSid}.json`,
|
|
150
133
|
{
|
|
151
134
|
method: "GET",
|
|
152
135
|
headers: {
|
|
153
|
-
Authorization:
|
|
136
|
+
Authorization: twilioAuthHeader(accountSid, authToken),
|
|
154
137
|
},
|
|
155
138
|
},
|
|
156
139
|
);
|
|
@@ -183,7 +166,7 @@ export class TwilioVoiceProvider implements VoiceProvider {
|
|
|
183
166
|
async checkCallerIdEligibility(
|
|
184
167
|
phoneNumber: string,
|
|
185
168
|
): Promise<{ eligible: boolean; reason?: string }> {
|
|
186
|
-
const { accountSid, authToken } = await
|
|
169
|
+
const { accountSid, authToken } = await getTwilioCredentials();
|
|
187
170
|
const encodedNumber = encodeURIComponent(phoneNumber);
|
|
188
171
|
|
|
189
172
|
let incomingOk = false;
|
|
@@ -191,13 +174,13 @@ export class TwilioVoiceProvider implements VoiceProvider {
|
|
|
191
174
|
|
|
192
175
|
// Check incoming phone numbers (owned by this account)
|
|
193
176
|
const incomingRes = await fetch(
|
|
194
|
-
`${
|
|
177
|
+
`${twilioBaseUrl(
|
|
195
178
|
accountSid,
|
|
196
179
|
)}/IncomingPhoneNumbers.json?PhoneNumber=${encodedNumber}`,
|
|
197
180
|
{
|
|
198
181
|
method: "GET",
|
|
199
182
|
headers: {
|
|
200
|
-
Authorization:
|
|
183
|
+
Authorization: twilioAuthHeader(accountSid, authToken),
|
|
201
184
|
},
|
|
202
185
|
},
|
|
203
186
|
);
|
|
@@ -223,13 +206,13 @@ export class TwilioVoiceProvider implements VoiceProvider {
|
|
|
223
206
|
|
|
224
207
|
// Check outgoing caller IDs (verified with this account)
|
|
225
208
|
const outgoingRes = await fetch(
|
|
226
|
-
`${
|
|
209
|
+
`${twilioBaseUrl(
|
|
227
210
|
accountSid,
|
|
228
211
|
)}/OutgoingCallerIds.json?PhoneNumber=${encodedNumber}`,
|
|
229
212
|
{
|
|
230
213
|
method: "GET",
|
|
231
214
|
headers: {
|
|
232
|
-
Authorization:
|
|
215
|
+
Authorization: twilioAuthHeader(accountSid, authToken),
|
|
233
216
|
},
|
|
234
217
|
},
|
|
235
218
|
);
|
|
@@ -41,6 +41,10 @@ import {
|
|
|
41
41
|
pendingStandaloneImagePersist,
|
|
42
42
|
SIGHT_FRAME_TURN_HOLD_MS,
|
|
43
43
|
} from "../live-voice/live-voice-photo.js";
|
|
44
|
+
import {
|
|
45
|
+
formatShareTargetsForPrompt,
|
|
46
|
+
type ShareTargetSnapshot,
|
|
47
|
+
} from "../live-voice/share-targets.js";
|
|
44
48
|
import { resolveAttachmentsForPersist } from "../persistence/attachments-store.js";
|
|
45
49
|
import {
|
|
46
50
|
deleteMessageById,
|
|
@@ -462,6 +466,13 @@ export interface VoiceTurnOptions {
|
|
|
462
466
|
macosDesktopSession?: boolean;
|
|
463
467
|
/** The desktop client currently shares a surface with this voice session. */
|
|
464
468
|
screenSharing?: boolean;
|
|
469
|
+
/**
|
|
470
|
+
* The controls the shared surface offers to be pointed at, from the
|
|
471
|
+
* client's newest read of its accessibility tree. Offered alongside the
|
|
472
|
+
* screen-annotation instructions so the first `screen_point_at` names a
|
|
473
|
+
* control the lookup can resolve.
|
|
474
|
+
*/
|
|
475
|
+
shareTargets?: ShareTargetSnapshot;
|
|
465
476
|
/** Front-door verdict: this action needs only the shared screen and current context. */
|
|
466
477
|
screenAction?: boolean;
|
|
467
478
|
/** Whether this is an inbound call (no outbound task). */
|
|
@@ -2145,8 +2156,14 @@ export async function startVoiceTurn(
|
|
|
2145
2156
|
signal: opts.signal,
|
|
2146
2157
|
});
|
|
2147
2158
|
if (annotation !== null) {
|
|
2159
|
+
const controls =
|
|
2160
|
+
opts.shareTargets !== undefined
|
|
2161
|
+
? formatShareTargetsForPrompt(opts.shareTargets)
|
|
2162
|
+
: null;
|
|
2148
2163
|
conversation.setVoiceCallControlPrompt(
|
|
2149
|
-
[voiceCallControlPrompt, annotation]
|
|
2164
|
+
[voiceCallControlPrompt, annotation, controls]
|
|
2165
|
+
.filter(Boolean)
|
|
2166
|
+
.join("\n\n"),
|
|
2150
2167
|
);
|
|
2151
2168
|
}
|
|
2152
2169
|
}
|
|
@@ -214,8 +214,7 @@ function attachOperationAction(
|
|
|
214
214
|
opts.output &&
|
|
215
215
|
result.screenshots?.length
|
|
216
216
|
) {
|
|
217
|
-
const
|
|
218
|
-
const buffer = Buffer.from(screenshot.data, "base64");
|
|
217
|
+
const buffer = Buffer.from(result.screenshots[0].data, "base64");
|
|
219
218
|
try {
|
|
220
219
|
writeFileSync(String(opts.output), buffer);
|
|
221
220
|
} catch (err) {
|
|
@@ -123,6 +123,23 @@ describe("parsePluginArtifact", () => {
|
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
test("ignores a malformed icon beside a valid artifact", () => {
|
|
127
|
+
expect(
|
|
128
|
+
parsePluginArtifact({
|
|
129
|
+
vellum: {
|
|
130
|
+
artifact: {
|
|
131
|
+
url: "https://example.com/App.dmg",
|
|
132
|
+
sha256: VALID_SHA,
|
|
133
|
+
},
|
|
134
|
+
icon: 42,
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
).toEqual({
|
|
138
|
+
url: "https://example.com/App.dmg",
|
|
139
|
+
sha256: VALID_SHA,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
126
143
|
test.each([
|
|
127
144
|
["no vellum block", { name: "x" }],
|
|
128
145
|
["no artifact block", { vellum: {} }],
|
|
@@ -202,6 +219,12 @@ describe("parsePluginIcon", () => {
|
|
|
202
219
|
expect(parsePluginIcon({ vellum: { icon: sixteen } })).toBe(sixteen);
|
|
203
220
|
});
|
|
204
221
|
|
|
222
|
+
test("ignores a malformed artifact beside a valid icon", () => {
|
|
223
|
+
expect(
|
|
224
|
+
parsePluginIcon({ vellum: { artifact: "App.dmg", icon: "🚀" } }),
|
|
225
|
+
).toBe("🚀");
|
|
226
|
+
});
|
|
227
|
+
|
|
205
228
|
test.each([
|
|
206
229
|
["no vellum block", { name: "x" }],
|
|
207
230
|
["no icon field", { vellum: {} }],
|
|
@@ -33,6 +33,7 @@ describe("filterPluginCatalogByFeatureFlags", () => {
|
|
|
33
33
|
"ramp",
|
|
34
34
|
"semrush",
|
|
35
35
|
"typeform",
|
|
36
|
+
"upwork",
|
|
36
37
|
"notion",
|
|
37
38
|
]);
|
|
38
39
|
|
|
@@ -56,6 +57,7 @@ describe("filterPluginCatalogByFeatureFlags", () => {
|
|
|
56
57
|
"ramp",
|
|
57
58
|
"semrush",
|
|
58
59
|
"typeform",
|
|
60
|
+
"upwork",
|
|
59
61
|
]);
|
|
60
62
|
|
|
61
63
|
const result = filterPluginCatalogByFeatureFlags(input, () => true);
|
|
@@ -309,6 +309,22 @@ describe("getPluginDetails (bundled catalog, offline)", () => {
|
|
|
309
309
|
expect(details.icon).toBeNull();
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
+
test("reads a legacy object-form license from an installed package.json", async () => {
|
|
313
|
+
const target = join(workspace, "caveman");
|
|
314
|
+
mkdirSync(target, { recursive: true });
|
|
315
|
+
writeFileSync(
|
|
316
|
+
join(target, "package.json"),
|
|
317
|
+
JSON.stringify({ version: "2.0.0", license: { type: "MIT" } }),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const details = await getPluginDetails(
|
|
321
|
+
{ name: "caveman" },
|
|
322
|
+
{ fetch: makeFetch({}), workspacePluginsDir: workspace },
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
expect(details.license).toBe("MIT");
|
|
326
|
+
});
|
|
327
|
+
|
|
312
328
|
test("reads metadata from an installed standard-only plugin.json", async () => {
|
|
313
329
|
const target = join(workspace, "caveman");
|
|
314
330
|
mkdirSync(target, { recursive: true });
|
|
@@ -135,6 +135,32 @@
|
|
|
135
135
|
"homepage": "https://github.com/topoteretes/cognee-integrations/tree/main/integrations/vellum-assistant",
|
|
136
136
|
"icon": "\ud83d\udd78\ufe0f"
|
|
137
137
|
},
|
|
138
|
+
{
|
|
139
|
+
"name": "contractor-missed-call-dispatcher",
|
|
140
|
+
"description": "Captures missed trade contractor inbound calls, fires an instant 5-second SMS qualification text-back, triages emergencies, and stages draft jobs in Jobber or ServiceTitan.",
|
|
141
|
+
"category": "commerce",
|
|
142
|
+
"icon": "📲",
|
|
143
|
+
"license": "MIT",
|
|
144
|
+
"homepage": "https://github.com/ZeebBoyBlue/contractor-missed-call-dispatcher",
|
|
145
|
+
"source": {
|
|
146
|
+
"source": "github",
|
|
147
|
+
"repo": "ZeebBoyBlue/contractor-missed-call-dispatcher",
|
|
148
|
+
"ref": "2d293cdcb68af03d446b61a9915a0ee3dc842d4e"
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"name": "contractor-photo-to-quote",
|
|
153
|
+
"description": "Turns job-site and equipment photos into standardized trade estimates and quotes in Jobber or QuickBooks with multi-tier Good/Better/Best options in 5 minutes.",
|
|
154
|
+
"category": "commerce",
|
|
155
|
+
"icon": "📸",
|
|
156
|
+
"license": "MIT",
|
|
157
|
+
"homepage": "https://github.com/ZeebBoyBlue/contractor-photo-to-quote",
|
|
158
|
+
"source": {
|
|
159
|
+
"source": "github",
|
|
160
|
+
"repo": "ZeebBoyBlue/contractor-photo-to-quote",
|
|
161
|
+
"ref": "6c136186faaf03bcf125d202f0b1f51f97d94544"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
138
164
|
{
|
|
139
165
|
"name": "contractor-voice-invoicer",
|
|
140
166
|
"description": "Converts 20-second truck voice memos and job photos into itemized client invoices with custom labor rates, material markups, and one-click accounting staging.",
|
|
@@ -461,6 +487,19 @@
|
|
|
461
487
|
"license": "MIT",
|
|
462
488
|
"icon": "\ud83d\udcac"
|
|
463
489
|
},
|
|
490
|
+
{
|
|
491
|
+
"name": "quickbooks-expense-categorizer",
|
|
492
|
+
"description": "Scans uncategorized QuickBooks Online expenses and bank feeds, matches receipts, and stages categorized entries with confidence scoring for 1-click batch review.",
|
|
493
|
+
"category": "commerce",
|
|
494
|
+
"icon": "🧾",
|
|
495
|
+
"license": "MIT",
|
|
496
|
+
"homepage": "https://github.com/ZeebBoyBlue/quickbooks-expense-categorizer",
|
|
497
|
+
"source": {
|
|
498
|
+
"source": "github",
|
|
499
|
+
"repo": "ZeebBoyBlue/quickbooks-expense-categorizer",
|
|
500
|
+
"ref": "120776619698cc3e9490caf5c756465f5caaa6c2"
|
|
501
|
+
}
|
|
502
|
+
},
|
|
464
503
|
{
|
|
465
504
|
"name": "reading-pal",
|
|
466
505
|
"source": {
|
|
@@ -1368,9 +1407,9 @@
|
|
|
1368
1407
|
"source": {
|
|
1369
1408
|
"source": "github",
|
|
1370
1409
|
"repo": "vellum-ai/runway-plugin",
|
|
1371
|
-
"ref": "
|
|
1410
|
+
"ref": "bb017a1e018c7be140c02c8463273c2d85f3b9d9"
|
|
1372
1411
|
},
|
|
1373
|
-
"description": "
|
|
1412
|
+
"description": "Securely set up, collaboratively direct, approve, create, track, cancel, and download Runway Gen-4.5 videos.",
|
|
1374
1413
|
"category": "other",
|
|
1375
1414
|
"submittedBy": "marina"
|
|
1376
1415
|
},
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* is a thin wrapper that supplies production deps and formats the result.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
19
21
|
import { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
|
|
20
22
|
import type { FetchLike } from "./fetch-like.js";
|
|
21
23
|
import {
|
|
@@ -50,6 +52,14 @@ import type { PluginCatalog, PluginSearchMatch } from "./search-plugins.js";
|
|
|
50
52
|
/** Full commit SHA (40 hex SHA-1 or 64 hex SHA-256). */
|
|
51
53
|
const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
52
54
|
|
|
55
|
+
const GitHubCommitResponseSchema = z.object({
|
|
56
|
+
commit: z.object({
|
|
57
|
+
committer: z.object({
|
|
58
|
+
date: z.string(),
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
53
63
|
/**
|
|
54
64
|
* Drift classification between the installed copy and the marketplace pin.
|
|
55
65
|
*
|
|
@@ -294,23 +304,13 @@ export async function fetchCommitDate(
|
|
|
294
304
|
if (!res.ok) {
|
|
295
305
|
return null;
|
|
296
306
|
}
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const commit = (json as Record<string, unknown>).commit;
|
|
302
|
-
if (typeof commit !== "object" || commit === null) {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
const committer = (commit as Record<string, unknown>).committer;
|
|
306
|
-
if (typeof committer !== "object" || committer === null) {
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
const date = (committer as Record<string, unknown>).date;
|
|
310
|
-
if (typeof date !== "string") {
|
|
307
|
+
const parsed = GitHubCommitResponseSchema.safeParse(
|
|
308
|
+
JSON.parse(await res.text()),
|
|
309
|
+
);
|
|
310
|
+
if (!parsed.success) {
|
|
311
311
|
return null;
|
|
312
312
|
}
|
|
313
|
-
const ms = Date.parse(date);
|
|
313
|
+
const ms = Date.parse(parsed.data.commit.committer.date);
|
|
314
314
|
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
315
315
|
} catch {
|
|
316
316
|
return null;
|
|
@@ -16,12 +16,14 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
19
21
|
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
20
22
|
import {
|
|
21
23
|
LEGACY_PLUGIN_MANIFEST,
|
|
22
24
|
readPluginManifest,
|
|
23
25
|
} from "../../util/plugin-manifest.js";
|
|
24
|
-
import {
|
|
26
|
+
import { PluginPackageJsonSchema } from "./plugin-artifact.js";
|
|
25
27
|
import { readValidatedPluginIcon } from "./plugin-icon-file.js";
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -39,6 +41,15 @@ const DEFAULT_PLUGINS_DIR = join(
|
|
|
39
41
|
"defaults",
|
|
40
42
|
);
|
|
41
43
|
|
|
44
|
+
const DefaultPluginManifestSchema = z.object({
|
|
45
|
+
name: z.string(),
|
|
46
|
+
version: z.string().optional().catch(undefined),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const InstallMetadataSchema = z.object({
|
|
50
|
+
installedAt: z.string(),
|
|
51
|
+
});
|
|
52
|
+
|
|
42
53
|
/** Minimal manifest fields surfaced to the CLI. */
|
|
43
54
|
export interface PluginPackageMetadata {
|
|
44
55
|
readonly name?: string;
|
|
@@ -163,23 +174,17 @@ function readPluginEntry(
|
|
|
163
174
|
issues.push(err instanceof Error ? err.message : String(err));
|
|
164
175
|
return { name, target, packageJson: null, issues, ...iconFields };
|
|
165
176
|
}
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
manifest.source === LEGACY_PLUGIN_MANIFEST
|
|
169
|
-
?
|
|
170
|
-
:
|
|
177
|
+
const parsedPackageJson = PluginPackageJsonSchema.safeParse(manifest.raw);
|
|
178
|
+
const legacyMetadata =
|
|
179
|
+
manifest.source === LEGACY_PLUGIN_MANIFEST && parsedPackageJson.success
|
|
180
|
+
? parsedPackageJson.data
|
|
181
|
+
: null;
|
|
171
182
|
const packageJson: PluginPackageMetadata = {
|
|
172
183
|
name: manifest.name,
|
|
173
184
|
version: manifest.version,
|
|
174
185
|
description: manifest.description,
|
|
175
|
-
peerDependencies:
|
|
176
|
-
|
|
177
|
-
typeof meta.peerDependencies === "object" &&
|
|
178
|
-
meta.peerDependencies !== null &&
|
|
179
|
-
!Array.isArray(meta.peerDependencies)
|
|
180
|
-
? (meta.peerDependencies as Record<string, string>)
|
|
181
|
-
: undefined,
|
|
182
|
-
...(icon ? { icon } : {}),
|
|
186
|
+
peerDependencies: legacyMetadata?.peerDependencies,
|
|
187
|
+
...(legacyMetadata?.icon ? { icon: legacyMetadata.icon } : {}),
|
|
183
188
|
};
|
|
184
189
|
|
|
185
190
|
return { name, target, packageJson, issues, ...iconFields };
|
|
@@ -319,14 +324,11 @@ function readDefaultPluginManifests(): readonly DefaultPluginManifest[] {
|
|
|
319
324
|
continue;
|
|
320
325
|
}
|
|
321
326
|
try {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
version:
|
|
328
|
-
typeof parsed.version === "string" ? parsed.version : undefined,
|
|
329
|
-
});
|
|
327
|
+
const parsed = DefaultPluginManifestSchema.safeParse(
|
|
328
|
+
JSON.parse(readFileSync(pkgJsonPath, "utf8")),
|
|
329
|
+
);
|
|
330
|
+
if (parsed.success) {
|
|
331
|
+
manifests.push(parsed.data);
|
|
330
332
|
}
|
|
331
333
|
} catch {
|
|
332
334
|
// Skip malformed entries — lenient like listInstalledPlugins.
|
|
@@ -345,12 +347,11 @@ function getPluginInstallDate(plugin: AllPluginInfo): number {
|
|
|
345
347
|
const metaPath = join(plugin.target, "install-meta.json");
|
|
346
348
|
try {
|
|
347
349
|
if (existsSync(metaPath)) {
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const ms = Date.parse(raw.installedAt);
|
|
350
|
+
const parsed = InstallMetadataSchema.safeParse(
|
|
351
|
+
JSON.parse(readFileSync(metaPath, "utf8")),
|
|
352
|
+
);
|
|
353
|
+
if (parsed.success) {
|
|
354
|
+
const ms = Date.parse(parsed.data.installedAt);
|
|
354
355
|
if (Number.isFinite(ms)) {
|
|
355
356
|
return ms;
|
|
356
357
|
}
|