@absolutejs/voice 0.0.22-beta.243 → 0.0.22-beta.244

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/README.md CHANGED
@@ -68,7 +68,7 @@ Use this checklist when a buyer asks, "How do I know this replaces a hosted voic
68
68
  | Can I prove provider fallback and latency? | provider contract matrix, provider status UI, `/turn-latency`, `/live-latency` | Provider choice, fallback behavior, server turn timing, browser p50/p95 timing |
69
69
  | Can operators intervene safely? | live-ops routes, action center, ops action audit routes, operations record | Pause/resume/takeover, injected instructions, operator action audit trail |
70
70
  | Can I run outbound campaigns? | `/voice/campaigns`, `/voice/campaigns/observability`, `/api/voice/campaigns/readiness-proof` | Recipient import evidence, consent/dedupe checks, scheduling policy, worker-safe attempts |
71
- | Can I handle post-call workflow? | reviews, tasks, integration events, outcome contracts, operations record | Summary/review artifacts, task creation, webhook/sink delivery, matched session proof |
71
+ | Can I handle post-call workflow? | `createVoicePostCallAnalysisRoutes(...)`, reviews, tasks, integration events, outcome contracts, operations record | Extracted-field proof, task creation, webhook/sink delivery, matched session proof |
72
72
  | Can I keep data in my infrastructure? | `/data-control`, `/data-control/audit.md`, retention dry-run/apply routes | Customer-owned storage, redaction, audit export, guarded deletion, zero-retention planning |
73
73
  | Can I prove release readiness? | `/production-readiness`, `/ops-recovery`, delivery runtime, readiness profiles | Deploy gates for session health, audits, delivery queues, provider/campaign/phone proof |
74
74
 
@@ -82,6 +82,37 @@ For a demo, the fastest convincing path is:
82
82
 
83
83
  If those five surfaces are green and linked, the buyer can see the core difference from Vapi-style hosted orchestration: the operational proof lives inside the app, not in a vendor dashboard.
84
84
 
85
+ ## Post-Call Analysis Proof
86
+
87
+ Use `createVoicePostCallAnalysisRoutes(...)` when the hosted-platform feature you need is call analysis plus follow-up proof. It validates that required extracted fields exist, expected ops tasks were created, integration/webhook events delivered, and the report links back to `/voice-operations/:sessionId`.
88
+
89
+ ```ts
90
+ import { createVoicePostCallAnalysisRoutes } from '@absolutejs/voice';
91
+
92
+ app.use(
93
+ createVoicePostCallAnalysisRoutes({
94
+ path: '/api/voice/post-call-analysis',
95
+ operationRecordBasePath: '/voice-operations/:sessionId',
96
+ reviews: runtime.reviews,
97
+ tasks: runtime.tasks,
98
+ integrationEvents: runtime.events,
99
+ source: ({ reviewId, sessionId }) => ({
100
+ reviewId,
101
+ sessionId,
102
+ // Use your own extractor output here, for example fields persisted from an LLM/tool result.
103
+ extractedFields: loadExtractedPostCallFields(reviewId ?? sessionId)
104
+ }),
105
+ fields: [
106
+ { path: 'review.postCall.target', label: 'customer target' },
107
+ { path: 'customerId' },
108
+ { path: 'category' }
109
+ ],
110
+ requiredTaskKinds: ['support-triage'],
111
+ requireDeliveredIntegrationEvent: true
112
+ })
113
+ );
114
+ ```
115
+
85
116
  ## Use-Case Recipe: Support Triage
86
117
 
87
118
  Use this path when you want a Vapi-style support assistant that can answer web or phone calls, look up customer context, route billing issues to a specialist, create follow-up work, and leave one debuggable call record. It is a recipe over primitives, not a support app kit.
package/dist/index.d.ts CHANGED
@@ -83,6 +83,7 @@ export { createVoiceCallReviewFromSession, recordVoiceRuntimeOps } from './runti
83
83
  export { createVoiceOpsRuntime } from './opsRuntime';
84
84
  export { resolveVoiceOpsPreset } from './opsPresets';
85
85
  export { resolveVoiceOutcomeRecipe } from './outcomeRecipes';
86
+ export { buildVoicePostCallAnalysisReport, createVoicePostCallAnalysisRoutes, renderVoicePostCallAnalysisMarkdown } from './postCallAnalysis';
86
87
  export { createId, createVoiceSessionRecord } from './store';
87
88
  export { createVoiceSTTRoutingCorrectionHandler, resolveVoiceSTTRoutingStrategy } from './routing';
88
89
  export { applyRiskTieredPhraseHintCorrections, applyPhraseHintCorrections, createDomainLexicon, createDomainPhraseHints, createPhraseHintCorrectionHandler, createRiskyTurnCorrectionHandler } from './correction';
@@ -135,6 +136,7 @@ export type { VoiceToolContractCase, VoiceToolContractCaseReport, VoiceToolContr
135
136
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
136
137
  export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
137
138
  export type { VoiceOutcomeRecipe, VoiceOutcomeRecipeName, VoiceOutcomeRecipeOptions } from './outcomeRecipes';
139
+ export type { VoicePostCallAnalysisFieldRequirement, VoicePostCallAnalysisFieldResult, VoicePostCallAnalysisIssue, VoicePostCallAnalysisIssueCode, VoicePostCallAnalysisOptions, VoicePostCallAnalysisReport, VoicePostCallAnalysisRoutesOptions, VoicePostCallAnalysisStatus } from './postCallAnalysis';
138
140
  export type { VoiceCRMActivitySinkOptions, VoiceHubSpotTaskSinkOptions, VoiceHubSpotTaskUpdateSinkOptions, VoiceHelpdeskTicketSinkOptions, VoiceIntegrationHTTPSinkOptions, VoiceIntegrationSink, VoiceIntegrationSinkDeliveryResult, VoiceLinearIssueSinkOptions, VoiceLinearIssueUpdateSinkOptions, VoiceZendeskTicketSinkOptions, VoiceZendeskTicketUpdateSinkOptions } from './opsSinks';
139
141
  export type { VoiceOpsWebhookEnvelope, VoiceOpsWebhookEntity, VoiceOpsWebhookLinkResolver, VoiceOpsWebhookReceiverRoutesOptions, VoiceOpsWebhookSinkOptions, VoiceOpsWebhookVerificationResult } from './opsWebhook';
140
142
  export type { VoiceHandoffDelivery, VoiceHandoffDeliveryRecord, VoiceHandoffDeliveryRecordInput, VoiceHandoffFanoutResult, VoiceQueuedHandoffDeliveryOptions, VoiceTwilioRedirectHandoffAdapterOptions, VoiceWebhookHandoffAdapterOptions } from './handoff';
package/dist/index.js CHANGED
@@ -28727,6 +28727,210 @@ var resolveVoiceOpsPreset = (name, overrides = {}) => {
28727
28727
  taskPolicies: mergePolicies(preset.taskPolicies, overrides.taskPolicies)
28728
28728
  };
28729
28729
  };
28730
+ // src/postCallAnalysis.ts
28731
+ import { Elysia as Elysia49 } from "elysia";
28732
+ var isStore = (value) => Boolean(value) && typeof value === "object" && value !== null && ("list" in value);
28733
+ var asArray = async (value) => Array.isArray(value) ? value : isStore(value) ? await value.list() : [];
28734
+ var getPathValue3 = (source, path) => {
28735
+ const parts = path.split(".").filter(Boolean);
28736
+ let current = source;
28737
+ for (const part of parts) {
28738
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
28739
+ return;
28740
+ }
28741
+ current = current[part];
28742
+ }
28743
+ return current;
28744
+ };
28745
+ var hasValue2 = (value) => {
28746
+ if (value === undefined || value === null) {
28747
+ return false;
28748
+ }
28749
+ if (typeof value === "string") {
28750
+ return value.trim().length > 0;
28751
+ }
28752
+ if (Array.isArray(value)) {
28753
+ return value.length > 0;
28754
+ }
28755
+ return true;
28756
+ };
28757
+ var matchesReview = (reviewId, id) => Boolean(reviewId && id && (id === reviewId || id.startsWith(`${reviewId}:`)));
28758
+ var matchesSession = (sessionId, event) => {
28759
+ const payloadSessionId = event.payload.sessionId;
28760
+ return Boolean(sessionId && (event.id === sessionId || event.id.startsWith(`${sessionId}:`) || payloadSessionId === sessionId));
28761
+ };
28762
+ var matchesIntegrationEvent = (input) => {
28763
+ const payloadReviewId = input.event.payload.reviewId;
28764
+ return matchesReview(input.reviewId, input.event.id) || payloadReviewId === input.reviewId || matchesSession(input.sessionId, input.event);
28765
+ };
28766
+ var normalizeOperationRecordHref = (basePath, sessionId) => {
28767
+ if (!basePath || !sessionId) {
28768
+ return;
28769
+ }
28770
+ return basePath.includes(":sessionId") ? basePath.replace(":sessionId", encodeURIComponent(sessionId)) : `${basePath.replace(/\/$/, "")}/${encodeURIComponent(sessionId)}`;
28771
+ };
28772
+ var isPostCallAnalysisReport = (value) => ("status" in value) && ("summary" in value) && Array.isArray(value.issues);
28773
+ var buildVoicePostCallAnalysisReport = async (options = {}) => {
28774
+ const reviews = await asArray(options.reviews);
28775
+ const review = options.review ?? reviews.find((candidate) => options.reviewId ? candidate.id === options.reviewId : options.sessionId ? candidate.id.startsWith(`${options.sessionId}:`) : false);
28776
+ const reviewId = options.reviewId ?? review?.id;
28777
+ const sessionId = options.sessionId ?? (reviewId?.endsWith(":review") ? reviewId.slice(0, -":review".length) : undefined);
28778
+ const allTasks = await asArray(options.tasks);
28779
+ const tasks = allTasks.filter((task) => reviewId ? task.reviewId === reviewId || task.intakeId === reviewId || matchesReview(reviewId, task.id) : false);
28780
+ const allIntegrationEvents = await asArray(options.integrationEvents);
28781
+ const integrationEvents = allIntegrationEvents.filter((event) => matchesIntegrationEvent({ event, reviewId, sessionId }));
28782
+ const fieldSource = {
28783
+ extractedFields: options.extractedFields ?? {},
28784
+ review
28785
+ };
28786
+ const fields = (options.fields ?? []).map((field) => {
28787
+ const value = getPathValue3(fieldSource.extractedFields, field.path) ?? getPathValue3(fieldSource, field.path);
28788
+ const required = field.required !== false;
28789
+ return {
28790
+ label: field.label ?? field.path,
28791
+ ok: !required || hasValue2(value),
28792
+ path: field.path,
28793
+ required,
28794
+ value
28795
+ };
28796
+ });
28797
+ const requiredTaskKinds = options.requiredTaskKinds ?? [];
28798
+ const missingTaskKinds = requiredTaskKinds.filter((kind) => !tasks.some((task) => task.kind === kind));
28799
+ const deliveredIntegrationEvents = integrationEvents.filter((event) => event.deliveryStatus === "delivered").length;
28800
+ const failedIntegrationEvents = integrationEvents.filter((event) => event.deliveryStatus === "failed").length;
28801
+ const issues = [];
28802
+ if (!review) {
28803
+ issues.push({
28804
+ code: "voice.post_call_analysis.review_missing",
28805
+ label: "Review missing",
28806
+ severity: "fail"
28807
+ });
28808
+ } else if (review.summary.pass === false) {
28809
+ issues.push({
28810
+ code: "voice.post_call_analysis.review_failed",
28811
+ detail: review.errors.join("; ") || review.summary.outcome,
28812
+ label: "Review failed",
28813
+ severity: "fail"
28814
+ });
28815
+ }
28816
+ for (const field of fields) {
28817
+ if (field.required && !field.ok) {
28818
+ issues.push({
28819
+ code: "voice.post_call_analysis.required_field_missing",
28820
+ detail: field.path,
28821
+ label: `Missing ${field.label}`,
28822
+ severity: "fail"
28823
+ });
28824
+ }
28825
+ }
28826
+ for (const kind of missingTaskKinds) {
28827
+ issues.push({
28828
+ code: "voice.post_call_analysis.required_task_missing",
28829
+ detail: kind,
28830
+ label: `Missing ${kind} task`,
28831
+ severity: "fail"
28832
+ });
28833
+ }
28834
+ if (options.requireDeliveredIntegrationEvent && deliveredIntegrationEvents === 0) {
28835
+ issues.push({
28836
+ code: "voice.post_call_analysis.integration_missing",
28837
+ label: "Delivered integration event missing",
28838
+ severity: "fail"
28839
+ });
28840
+ }
28841
+ if (failedIntegrationEvents > 0) {
28842
+ issues.push({
28843
+ code: "voice.post_call_analysis.integration_failed",
28844
+ detail: `${failedIntegrationEvents} failed integration event(s)`,
28845
+ label: "Integration delivery failed",
28846
+ severity: "warn"
28847
+ });
28848
+ }
28849
+ const status = issues.some((issue) => issue.severity === "fail") ? "fail" : issues.length > 0 ? "warn" : "pass";
28850
+ return {
28851
+ checkedAt: options.at ?? Date.now(),
28852
+ fields,
28853
+ integrationEvents,
28854
+ issues,
28855
+ operationRecordHref: normalizeOperationRecordHref(options.operationRecordBasePath, sessionId),
28856
+ review,
28857
+ reviewId,
28858
+ sessionId,
28859
+ status,
28860
+ summary: {
28861
+ deliveredIntegrationEvents,
28862
+ failedIntegrationEvents,
28863
+ fields: fields.length,
28864
+ missingRequiredFields: fields.filter((field) => field.required && !field.ok).length,
28865
+ missingRequiredTasks: missingTaskKinds.length,
28866
+ requiredFields: fields.filter((field) => field.required).length,
28867
+ requiredTaskKinds: requiredTaskKinds.length,
28868
+ tasks: tasks.length
28869
+ },
28870
+ tasks
28871
+ };
28872
+ };
28873
+ var renderVoicePostCallAnalysisMarkdown = (report) => {
28874
+ const lines = [
28875
+ "# Voice Post-Call Analysis",
28876
+ "",
28877
+ `Status: ${report.status}`,
28878
+ `Checked: ${new Date(report.checkedAt).toISOString()}`,
28879
+ report.reviewId ? `Review: ${report.reviewId}` : undefined,
28880
+ report.sessionId ? `Session: ${report.sessionId}` : undefined,
28881
+ report.operationRecordHref ? `Operations record: ${report.operationRecordHref}` : undefined,
28882
+ "",
28883
+ "## Summary",
28884
+ `- Fields: ${report.summary.fields}`,
28885
+ `- Missing required fields: ${report.summary.missingRequiredFields}`,
28886
+ `- Tasks: ${report.summary.tasks}`,
28887
+ `- Missing required tasks: ${report.summary.missingRequiredTasks}`,
28888
+ `- Delivered integration events: ${report.summary.deliveredIntegrationEvents}`,
28889
+ `- Failed integration events: ${report.summary.failedIntegrationEvents}`,
28890
+ "",
28891
+ "## Issues",
28892
+ ...report.issues.length ? report.issues.map((issue) => `- ${issue.severity}: ${issue.code} - ${issue.label}${issue.detail ? ` (${issue.detail})` : ""}`) : ["- none"]
28893
+ ].filter((line) => line !== undefined);
28894
+ return `${lines.join(`
28895
+ `)}
28896
+ `;
28897
+ };
28898
+ var resolvePostCallAnalysisReport = async (options, input) => {
28899
+ const source = options.source === undefined ? options : typeof options.source === "function" ? await options.source(input) : options.source;
28900
+ const merged = {
28901
+ ...options,
28902
+ ...source,
28903
+ reviewId: input.reviewId ?? source.reviewId ?? options.reviewId,
28904
+ sessionId: input.sessionId ?? source.sessionId ?? options.sessionId
28905
+ };
28906
+ return isPostCallAnalysisReport(merged) ? merged : buildVoicePostCallAnalysisReport(merged);
28907
+ };
28908
+ var createVoicePostCallAnalysisRoutes = (options = {}) => {
28909
+ const path = options.path ?? "/api/voice/post-call-analysis";
28910
+ const routes = new Elysia49({
28911
+ name: options.name ?? "absolutejs-voice-post-call-analysis"
28912
+ });
28913
+ routes.get(path, async ({ query }) => {
28914
+ const report = await resolvePostCallAnalysisReport(options, {
28915
+ reviewId: typeof query.reviewId === "string" ? query.reviewId : undefined,
28916
+ sessionId: typeof query.sessionId === "string" ? query.sessionId : undefined
28917
+ });
28918
+ return Response.json(report, { headers: options.headers });
28919
+ });
28920
+ routes.get(`${path}.md`, async ({ query }) => {
28921
+ const report = await resolvePostCallAnalysisReport(options, {
28922
+ reviewId: typeof query.reviewId === "string" ? query.reviewId : undefined,
28923
+ sessionId: typeof query.sessionId === "string" ? query.sessionId : undefined
28924
+ });
28925
+ return new Response(renderVoicePostCallAnalysisMarkdown(report), {
28926
+ headers: {
28927
+ "content-type": "text/markdown; charset=utf-8",
28928
+ ...options.headers
28929
+ }
28930
+ });
28931
+ });
28932
+ return routes;
28933
+ };
28730
28934
  // src/correction.ts
28731
28935
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28732
28936
  var buildAliasMatcher = (alias) => new RegExp(`(?<![\\p{L}\\p{N}'])${escapeRegExp(alias)}(?![\\p{L}\\p{N}'])`, "giu");
@@ -29219,6 +29423,7 @@ export {
29219
29423
  renderVoiceProviderContractMatrixHTML,
29220
29424
  renderVoiceProviderCapabilityHTML,
29221
29425
  renderVoiceProductionReadinessHTML,
29426
+ renderVoicePostCallAnalysisMarkdown,
29222
29427
  renderVoicePhoneAgentProductionSmokeHTML,
29223
29428
  renderVoiceOutcomeContractHTML,
29224
29429
  renderVoiceOpsStatusHTML,
@@ -29414,6 +29619,7 @@ export {
29414
29619
  createVoicePostgresCampaignStore,
29415
29620
  createVoicePostgresAuditSinkDeliveryStore,
29416
29621
  createVoicePostgresAuditEventStore,
29622
+ createVoicePostCallAnalysisRoutes,
29417
29623
  createVoicePlivoCampaignDialer,
29418
29624
  createVoicePlatformCoverageRoutes,
29419
29625
  createVoicePhoneAgentProductionSmokeRoutes,
@@ -29570,6 +29776,7 @@ export {
29570
29776
  buildVoiceProofTrendReport,
29571
29777
  buildVoiceProductionReadinessReport,
29572
29778
  buildVoiceProductionReadinessGate,
29779
+ buildVoicePostCallAnalysisReport,
29573
29780
  buildVoicePlatformCoverageSummary,
29574
29781
  buildVoiceOpsTaskFromSLABreach,
29575
29782
  buildVoiceOpsTaskFromReview,
@@ -0,0 +1,98 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { StoredVoiceIntegrationEvent, StoredVoiceOpsTask, VoiceIntegrationEventStore, VoiceOpsTaskKind, VoiceOpsTaskStore } from './ops';
3
+ import type { StoredVoiceCallReviewArtifact, VoiceCallReviewStore } from './testing/review';
4
+ export type VoicePostCallAnalysisStatus = 'fail' | 'pass' | 'warn';
5
+ export type VoicePostCallAnalysisFieldRequirement = {
6
+ label?: string;
7
+ path: string;
8
+ required?: boolean;
9
+ };
10
+ export type VoicePostCallAnalysisFieldResult = {
11
+ label: string;
12
+ ok: boolean;
13
+ path: string;
14
+ required: boolean;
15
+ value?: unknown;
16
+ };
17
+ export type VoicePostCallAnalysisIssueCode = 'voice.post_call_analysis.integration_failed' | 'voice.post_call_analysis.integration_missing' | 'voice.post_call_analysis.required_field_missing' | 'voice.post_call_analysis.required_task_missing' | 'voice.post_call_analysis.review_failed' | 'voice.post_call_analysis.review_missing';
18
+ export type VoicePostCallAnalysisIssue = {
19
+ code: VoicePostCallAnalysisIssueCode;
20
+ detail?: string;
21
+ label: string;
22
+ severity: Exclude<VoicePostCallAnalysisStatus, 'pass'>;
23
+ };
24
+ export type VoicePostCallAnalysisReport = {
25
+ checkedAt: number;
26
+ fields: VoicePostCallAnalysisFieldResult[];
27
+ integrationEvents: StoredVoiceIntegrationEvent[];
28
+ issues: VoicePostCallAnalysisIssue[];
29
+ operationRecordHref?: string;
30
+ review?: StoredVoiceCallReviewArtifact;
31
+ reviewId?: string;
32
+ sessionId?: string;
33
+ status: VoicePostCallAnalysisStatus;
34
+ summary: {
35
+ deliveredIntegrationEvents: number;
36
+ failedIntegrationEvents: number;
37
+ fields: number;
38
+ missingRequiredFields: number;
39
+ missingRequiredTasks: number;
40
+ requiredFields: number;
41
+ requiredTaskKinds: number;
42
+ tasks: number;
43
+ };
44
+ tasks: StoredVoiceOpsTask[];
45
+ };
46
+ export type VoicePostCallAnalysisOptions = {
47
+ at?: number;
48
+ extractedFields?: Record<string, unknown>;
49
+ fields?: VoicePostCallAnalysisFieldRequirement[];
50
+ integrationEvents?: StoredVoiceIntegrationEvent[] | VoiceIntegrationEventStore;
51
+ operationRecordBasePath?: string;
52
+ requireDeliveredIntegrationEvent?: boolean;
53
+ requiredTaskKinds?: VoiceOpsTaskKind[];
54
+ review?: StoredVoiceCallReviewArtifact;
55
+ reviewId?: string;
56
+ reviews?: StoredVoiceCallReviewArtifact[] | VoiceCallReviewStore;
57
+ sessionId?: string;
58
+ tasks?: StoredVoiceOpsTask[] | VoiceOpsTaskStore;
59
+ };
60
+ export type VoicePostCallAnalysisRoutesOptions = VoicePostCallAnalysisOptions & {
61
+ headers?: HeadersInit;
62
+ name?: string;
63
+ path?: string;
64
+ source?: ((input: {
65
+ reviewId?: string;
66
+ sessionId?: string;
67
+ }) => Promise<VoicePostCallAnalysisOptions | VoicePostCallAnalysisReport> | VoicePostCallAnalysisOptions | VoicePostCallAnalysisReport) | VoicePostCallAnalysisOptions | VoicePostCallAnalysisReport;
68
+ };
69
+ export declare const buildVoicePostCallAnalysisReport: (options?: VoicePostCallAnalysisOptions) => Promise<VoicePostCallAnalysisReport>;
70
+ export declare const renderVoicePostCallAnalysisMarkdown: (report: VoicePostCallAnalysisReport) => string;
71
+ export declare const createVoicePostCallAnalysisRoutes: (options?: VoicePostCallAnalysisRoutesOptions) => Elysia<"", {
72
+ decorator: {};
73
+ store: {};
74
+ derive: {};
75
+ resolve: {};
76
+ }, {
77
+ typebox: {};
78
+ error: {};
79
+ }, {
80
+ schema: {};
81
+ standaloneSchema: {};
82
+ macro: {};
83
+ macroFn: {};
84
+ parser: {};
85
+ response: {};
86
+ }, {}, {
87
+ derive: {};
88
+ resolve: {};
89
+ schema: {};
90
+ standaloneSchema: {};
91
+ response: {};
92
+ }, {
93
+ derive: {};
94
+ resolve: {};
95
+ schema: {};
96
+ standaloneSchema: {};
97
+ response: {};
98
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.243",
3
+ "version": "0.0.22-beta.244",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",