@desplega.ai/agent-swarm 1.79.0 → 1.79.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +2 -0
  2. package/openapi.json +559 -1
  3. package/package.json +4 -4
  4. package/plugin/skills/kv-storage/SKILL.md +168 -0
  5. package/plugin/skills/pages/SKILL.md +149 -0
  6. package/src/artifact-sdk/browser-sdk.ts +292 -0
  7. package/src/be/db.ts +309 -0
  8. package/src/be/migrations/061_kv_store.sql +34 -0
  9. package/src/be/migrations/062_pages_view_count.sql +9 -0
  10. package/src/commands/provider-credentials.ts +1 -1
  11. package/src/http/index.ts +2 -0
  12. package/src/http/kv.ts +658 -0
  13. package/src/http/page-proxy.ts +5 -0
  14. package/src/http/pages-public.ts +50 -6
  15. package/src/http/status.ts +1 -1
  16. package/src/providers/claude-adapter.ts +138 -7
  17. package/src/providers/pi-mono-adapter.ts +3 -3
  18. package/src/providers/pi-mono-extension.ts +1 -1
  19. package/src/server.ts +20 -1
  20. package/src/tasks/context-key.ts +28 -0
  21. package/src/telemetry.ts +65 -1
  22. package/src/tests/claude-adapter-binary.test.ts +628 -0
  23. package/src/tests/context-key.test.ts +17 -0
  24. package/src/tests/kv-http.test.ts +331 -0
  25. package/src/tests/kv-namespace-resolution.test.ts +172 -0
  26. package/src/tests/kv-page-proxy.test.ts +212 -0
  27. package/src/tests/kv-storage.test.ts +227 -0
  28. package/src/tests/kv-tool.test.ts +217 -0
  29. package/src/tests/page-proxy.test.ts +5 -1
  30. package/src/tests/page-session.test.ts +10 -5
  31. package/src/tests/pages-authed-mode.test.ts +5 -1
  32. package/src/tests/pages-public-html.test.ts +10 -1
  33. package/src/tests/pages-view-count.test.ts +220 -0
  34. package/src/tests/swarm-diff.test.ts +303 -0
  35. package/src/tests/telemetry-init.test.ts +149 -0
  36. package/src/tools/kv/index.ts +5 -0
  37. package/src/tools/kv/kv-delete.ts +89 -0
  38. package/src/tools/kv/kv-get.ts +64 -0
  39. package/src/tools/kv/kv-incr.ts +116 -0
  40. package/src/tools/kv/kv-list.ts +81 -0
  41. package/src/tools/kv/kv-set.ts +194 -0
  42. package/src/tools/kv/resolve-namespace.ts +58 -0
  43. package/src/tools/tool-config.ts +7 -0
  44. package/src/types.ts +53 -0
  45. package/src/utils/internal-ai/complete-structured.ts +7 -10
  46. package/src/utils/internal-ai/credentials.ts +3 -3
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import {
3
3
  _getInstallationIdForTests,
4
4
  _resetTelemetryStateForTests,
5
+ _resolveCloudMode,
5
6
  initTelemetry,
6
7
  track,
7
8
  } from "../telemetry";
@@ -13,6 +14,9 @@ process.env.ANONYMIZED_TELEMETRY = "true";
13
14
  describe("initTelemetry", () => {
14
15
  beforeEach(() => {
15
16
  _resetTelemetryStateForTests();
17
+ // Tests below set MCP_BASE_URL to assert classification — clear between
18
+ // tests so cases that expect "unset" don't inherit a prior test's value.
19
+ delete process.env.MCP_BASE_URL;
16
20
  });
17
21
 
18
22
  test("without generateIfMissing + missing config → installationId stays null (track no-ops)", async () => {
@@ -241,4 +245,149 @@ describe("initTelemetry", () => {
241
245
  expect(_getInstallationIdForTests()).toBe(existing);
242
246
  expect(writesB).toEqual([]);
243
247
  });
248
+
249
+ describe("_resolveCloudMode (URL → is_cloud)", () => {
250
+ test("cloud apex host → cloud=true", () => {
251
+ expect(_resolveCloudMode("https://agent-swarm-mcp.desplega.sh")).toEqual({ isCloud: true });
252
+ expect(_resolveCloudMode("https://api.agent-swarm.dev")).toEqual({ isCloud: true });
253
+ expect(_resolveCloudMode("https://agent-swarm.dev")).toEqual({ isCloud: true });
254
+ // Future cloud subdomains (suffix match)
255
+ expect(_resolveCloudMode("https://mcp.agent-swarm.dev/")).toEqual({ isCloud: true });
256
+ // Trailing path / port / auth must not change the host classification
257
+ expect(_resolveCloudMode("https://user:tok@api.agent-swarm.dev:443/api/foo?x=1")).toEqual({
258
+ isCloud: true,
259
+ });
260
+ // Case-insensitive
261
+ expect(_resolveCloudMode("https://API.Agent-Swarm.DEV")).toEqual({ isCloud: true });
262
+ });
263
+
264
+ test("agent-swarm.cloud apex host → cloud=true", () => {
265
+ // Exact apex
266
+ expect(_resolveCloudMode("https://agent-swarm.cloud")).toEqual({ isCloud: true });
267
+ // Suffix subdomain
268
+ expect(_resolveCloudMode("https://api.agent-swarm.cloud")).toEqual({ isCloud: true });
269
+ expect(_resolveCloudMode("https://mcp.agent-swarm.cloud/")).toEqual({ isCloud: true });
270
+ // Trailing path / port / auth must not change classification
271
+ expect(_resolveCloudMode("https://user:tok@api.agent-swarm.cloud:443/api/foo?x=1")).toEqual({
272
+ isCloud: true,
273
+ });
274
+ // Case-insensitive
275
+ expect(_resolveCloudMode("https://API.Agent-Swarm.CLOUD")).toEqual({ isCloud: true });
276
+ });
277
+
278
+ test("self-hosted hosts → cloud=false", () => {
279
+ expect(_resolveCloudMode("http://localhost:3013")).toEqual({ isCloud: false });
280
+ expect(_resolveCloudMode("https://my-internal-mcp.example.com")).toEqual({ isCloud: false });
281
+ // Substring trap — must NOT be treated as cloud
282
+ expect(_resolveCloudMode("https://agent-swarm.dev.attacker.com")).toEqual({ isCloud: false });
283
+ expect(_resolveCloudMode("https://agent-swarm.cloud.attacker.com")).toEqual({
284
+ isCloud: false,
285
+ });
286
+ // IPv4 self-host
287
+ expect(_resolveCloudMode("http://127.0.0.1:3013")).toEqual({ isCloud: false });
288
+ });
289
+
290
+ test("bare hostname / unset / weird scheme → safe fallback", () => {
291
+ // Bare hostname (no scheme) — URL constructor throws
292
+ expect(_resolveCloudMode("agent-swarm-mcp.desplega.sh")).toEqual({ isCloud: false });
293
+ // Empty / undefined / null
294
+ expect(_resolveCloudMode(undefined)).toEqual({ isCloud: false });
295
+ expect(_resolveCloudMode(null)).toEqual({ isCloud: false });
296
+ expect(_resolveCloudMode("")).toEqual({ isCloud: false });
297
+ // Obvious garbage
298
+ expect(_resolveCloudMode("not a url")).toEqual({ isCloud: false });
299
+ // Weird scheme with no host component
300
+ expect(_resolveCloudMode("file:///tmp/foo")).toEqual({ isCloud: false });
301
+ });
302
+ });
303
+
304
+ describe("track() ships is_cloud in properties", () => {
305
+ const originalFetch = globalThis.fetch;
306
+ let captured: Record<string, unknown> | null = null;
307
+
308
+ beforeEach(() => {
309
+ captured = null;
310
+ globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
311
+ captured = init?.body ? JSON.parse(init.body) : null;
312
+ return new Response(null, { status: 204 });
313
+ }) as typeof fetch;
314
+ });
315
+
316
+ afterEach(() => {
317
+ globalThis.fetch = originalFetch;
318
+ delete process.env.MCP_BASE_URL;
319
+ });
320
+
321
+ test("cloud MCP_BASE_URL → properties.is_cloud=true", async () => {
322
+ process.env.MCP_BASE_URL = "https://agent-swarm-mcp.desplega.sh";
323
+ await initTelemetry(
324
+ "worker",
325
+ async () => "install_cloud_test",
326
+ async () => {},
327
+ );
328
+
329
+ track({ event: "server.started", properties: { port: 3013 } });
330
+ await new Promise((r) => setTimeout(r, 0));
331
+
332
+ const properties = (captured as { properties: Record<string, unknown> }).properties;
333
+ expect(properties.is_cloud).toBe(true);
334
+ // Hostname must NOT be emitted — telemetry is anonymous.
335
+ expect(properties.mcp_host).toBeUndefined();
336
+ // Caller's properties preserved alongside the cohort signal.
337
+ expect(properties.port).toBe(3013);
338
+ });
339
+
340
+ test("self-hosted MCP_BASE_URL → properties.is_cloud=false", async () => {
341
+ process.env.MCP_BASE_URL = "http://localhost:3013";
342
+ await initTelemetry(
343
+ "worker",
344
+ async () => "install_self_test",
345
+ async () => {},
346
+ );
347
+
348
+ track({ event: "test.event", properties: {} });
349
+ await new Promise((r) => setTimeout(r, 0));
350
+
351
+ const properties = (captured as { properties: Record<string, unknown> }).properties;
352
+ expect(properties.is_cloud).toBe(false);
353
+ expect(properties.mcp_host).toBeUndefined();
354
+ });
355
+
356
+ test("missing MCP_BASE_URL → safe fallback (false)", async () => {
357
+ delete process.env.MCP_BASE_URL;
358
+ await initTelemetry(
359
+ "api-server",
360
+ async () => "install_no_url",
361
+ async () => {},
362
+ );
363
+
364
+ track({ event: "test.event", properties: {} });
365
+ await new Promise((r) => setTimeout(r, 0));
366
+
367
+ const properties = (captured as { properties: Record<string, unknown> }).properties;
368
+ expect(properties.is_cloud).toBe(false);
369
+ expect(properties.mcp_host).toBeUndefined();
370
+ });
371
+
372
+ test("caller properties cannot override is_cloud", async () => {
373
+ // Defense-in-depth: even if a caller passes through user-supplied
374
+ // values, the cohort signal shipped on every event must come from
375
+ // initTelemetry — not from arbitrary call sites.
376
+ process.env.MCP_BASE_URL = "https://agent-swarm-mcp.desplega.sh";
377
+ await initTelemetry(
378
+ "worker",
379
+ async () => "install_override_test",
380
+ async () => {},
381
+ );
382
+
383
+ track({
384
+ event: "test.event",
385
+ properties: { is_cloud: false },
386
+ });
387
+ await new Promise((r) => setTimeout(r, 0));
388
+
389
+ const properties = (captured as { properties: Record<string, unknown> }).properties;
390
+ expect(properties.is_cloud).toBe(true);
391
+ });
392
+ });
244
393
  });
@@ -0,0 +1,5 @@
1
+ export { registerKvDeleteTool } from "./kv-delete";
2
+ export { registerKvGetTool } from "./kv-get";
3
+ export { registerKvIncrTool } from "./kv-incr";
4
+ export { registerKvListTool } from "./kv-list";
5
+ export { registerKvSetTool } from "./kv-set";
@@ -0,0 +1,89 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { deleteKv, getAgentById } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { KvKeySchema, KvNamespaceSchema } from "@/types";
6
+ import { resolveNamespace } from "./resolve-namespace";
7
+
8
+ function authError(namespace: string, info: { agentId: string | undefined }): string | null {
9
+ if (namespace.startsWith("task:page:")) {
10
+ return "task:page:* writes require a page-proxy request, not an MCP call";
11
+ }
12
+ if (namespace.startsWith("task:agent:")) {
13
+ const target = namespace.slice("task:agent:".length);
14
+ if (info.agentId && target === info.agentId) return null;
15
+ if (info.agentId) {
16
+ const agent = getAgentById(info.agentId);
17
+ if (agent?.isLead) return null;
18
+ }
19
+ return "writes to another agent's namespace require lead";
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export const registerKvDeleteTool = (server: McpServer) => {
25
+ createToolRegistrar(server)(
26
+ "kv-delete",
27
+ {
28
+ title: "KV Delete",
29
+ description:
30
+ "Remove a key from the swarm KV store. Returns whether a row was actually deleted. Namespace defaults to your current context.",
31
+ annotations: { idempotentHint: true },
32
+
33
+ inputSchema: z.object({
34
+ key: KvKeySchema,
35
+ namespace: KvNamespaceSchema.optional(),
36
+ }),
37
+ outputSchema: z.object({
38
+ yourAgentId: z.string().uuid().optional(),
39
+ success: z.boolean(),
40
+ message: z.string(),
41
+ namespace: z.string().optional(),
42
+ deleted: z.boolean().optional(),
43
+ }),
44
+ },
45
+ async ({ key, namespace }, requestInfo) => {
46
+ const resolved = resolveNamespace(namespace, requestInfo);
47
+ if ("error" in resolved) {
48
+ return {
49
+ content: [{ type: "text", text: resolved.error }],
50
+ structuredContent: {
51
+ yourAgentId: requestInfo.agentId,
52
+ success: false,
53
+ message: resolved.error,
54
+ },
55
+ };
56
+ }
57
+ const authErr = authError(resolved.namespace, { agentId: requestInfo.agentId });
58
+ if (authErr) {
59
+ return {
60
+ content: [{ type: "text", text: authErr }],
61
+ structuredContent: {
62
+ yourAgentId: requestInfo.agentId,
63
+ success: false,
64
+ message: authErr,
65
+ namespace: resolved.namespace,
66
+ },
67
+ };
68
+ }
69
+ const deleted = deleteKv(resolved.namespace, key);
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: deleted
75
+ ? `Deleted "${key}" from "${resolved.namespace}".`
76
+ : `No entry to delete at "${key}" in "${resolved.namespace}".`,
77
+ },
78
+ ],
79
+ structuredContent: {
80
+ yourAgentId: requestInfo.agentId,
81
+ success: true,
82
+ message: deleted ? "deleted" : "not found",
83
+ namespace: resolved.namespace,
84
+ deleted,
85
+ },
86
+ };
87
+ },
88
+ );
89
+ };
@@ -0,0 +1,64 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getKv } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { KvEntrySchema, KvKeySchema, KvNamespaceSchema } from "@/types";
6
+ import { resolveNamespace } from "./resolve-namespace";
7
+
8
+ export const registerKvGetTool = (server: McpServer) => {
9
+ createToolRegistrar(server)(
10
+ "kv-get",
11
+ {
12
+ title: "KV Get",
13
+ description:
14
+ "Read a key from the swarm KV store. Returns the entry or null if missing/expired. Namespace defaults to your current context (Slack thread / PR / Linear issue when invoked from a task; otherwise your agent scratchpad).",
15
+ annotations: { readOnlyHint: true },
16
+
17
+ inputSchema: z.object({
18
+ key: KvKeySchema.describe("KV key (≤512 chars, [a-zA-Z0-9._:/-])."),
19
+ namespace: KvNamespaceSchema.optional().describe(
20
+ "Optional explicit namespace. Defaults to the caller's contextKey.",
21
+ ),
22
+ }),
23
+ outputSchema: z.object({
24
+ yourAgentId: z.string().uuid().optional(),
25
+ success: z.boolean(),
26
+ message: z.string(),
27
+ namespace: z.string().optional(),
28
+ entry: KvEntrySchema.nullable().optional(),
29
+ }),
30
+ },
31
+ async ({ key, namespace }, requestInfo) => {
32
+ const resolved = resolveNamespace(namespace, requestInfo);
33
+ if ("error" in resolved) {
34
+ return {
35
+ content: [{ type: "text", text: resolved.error }],
36
+ structuredContent: {
37
+ yourAgentId: requestInfo.agentId,
38
+ success: false,
39
+ message: resolved.error,
40
+ },
41
+ };
42
+ }
43
+
44
+ const entry = getKv(resolved.namespace, key);
45
+ return {
46
+ content: [
47
+ {
48
+ type: "text",
49
+ text: entry
50
+ ? `Found "${key}" in "${resolved.namespace}".`
51
+ : `No entry for "${key}" in "${resolved.namespace}".`,
52
+ },
53
+ ],
54
+ structuredContent: {
55
+ yourAgentId: requestInfo.agentId,
56
+ success: true,
57
+ message: entry ? "ok" : "not found",
58
+ namespace: resolved.namespace,
59
+ entry: entry ?? null,
60
+ },
61
+ };
62
+ },
63
+ );
64
+ };
@@ -0,0 +1,116 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAgentById, incrKv, KvTypeCollisionError } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { KvEntrySchema, KvKeySchema, KvNamespaceSchema } from "@/types";
6
+ import { resolveNamespace } from "./resolve-namespace";
7
+
8
+ function authError(namespace: string, info: { agentId: string | undefined }): string | null {
9
+ if (namespace.startsWith("task:page:")) {
10
+ return "task:page:* writes require a page-proxy request, not an MCP call";
11
+ }
12
+ if (namespace.startsWith("task:agent:")) {
13
+ const target = namespace.slice("task:agent:".length);
14
+ if (info.agentId && target === info.agentId) return null;
15
+ if (info.agentId) {
16
+ const agent = getAgentById(info.agentId);
17
+ if (agent?.isLead) return null;
18
+ }
19
+ return "writes to another agent's namespace require lead";
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export const registerKvIncrTool = (server: McpServer) => {
25
+ createToolRegistrar(server)(
26
+ "kv-incr",
27
+ {
28
+ title: "KV Incr",
29
+ description:
30
+ "Atomically increment an integer KV entry. Creates the entry (set to `by`) if it doesn't exist or has expired. Fails if the existing value_type is not 'integer' (use kv-delete first if you want to switch).",
31
+ annotations: {},
32
+
33
+ inputSchema: z.object({
34
+ key: KvKeySchema,
35
+ by: z
36
+ .number()
37
+ .int()
38
+ .optional()
39
+ .describe("Increment (or decrement when negative). Default: 1."),
40
+ namespace: KvNamespaceSchema.optional(),
41
+ }),
42
+ outputSchema: z.object({
43
+ yourAgentId: z.string().uuid().optional(),
44
+ success: z.boolean(),
45
+ message: z.string(),
46
+ namespace: z.string().optional(),
47
+ entry: KvEntrySchema.optional(),
48
+ }),
49
+ },
50
+ async ({ key, by, namespace }, requestInfo) => {
51
+ const resolved = resolveNamespace(namespace, requestInfo);
52
+ if ("error" in resolved) {
53
+ return {
54
+ content: [{ type: "text", text: resolved.error }],
55
+ structuredContent: {
56
+ yourAgentId: requestInfo.agentId,
57
+ success: false,
58
+ message: resolved.error,
59
+ },
60
+ };
61
+ }
62
+ const authErr = authError(resolved.namespace, { agentId: requestInfo.agentId });
63
+ if (authErr) {
64
+ return {
65
+ content: [{ type: "text", text: authErr }],
66
+ structuredContent: {
67
+ yourAgentId: requestInfo.agentId,
68
+ success: false,
69
+ message: authErr,
70
+ namespace: resolved.namespace,
71
+ },
72
+ };
73
+ }
74
+ try {
75
+ const entry = incrKv(resolved.namespace, key, by ?? 1);
76
+ return {
77
+ content: [
78
+ {
79
+ type: "text",
80
+ text: `"${key}" now ${entry.value} in "${resolved.namespace}".`,
81
+ },
82
+ ],
83
+ structuredContent: {
84
+ yourAgentId: requestInfo.agentId,
85
+ success: true,
86
+ message: "ok",
87
+ namespace: resolved.namespace,
88
+ entry,
89
+ },
90
+ };
91
+ } catch (err) {
92
+ if (err instanceof KvTypeCollisionError) {
93
+ return {
94
+ content: [{ type: "text", text: err.message }],
95
+ structuredContent: {
96
+ yourAgentId: requestInfo.agentId,
97
+ success: false,
98
+ message: err.message,
99
+ namespace: resolved.namespace,
100
+ },
101
+ };
102
+ }
103
+ const msg = err instanceof Error ? err.message : "INCR failed";
104
+ return {
105
+ content: [{ type: "text", text: msg }],
106
+ structuredContent: {
107
+ yourAgentId: requestInfo.agentId,
108
+ success: false,
109
+ message: msg,
110
+ namespace: resolved.namespace,
111
+ },
112
+ };
113
+ }
114
+ },
115
+ );
116
+ };
@@ -0,0 +1,81 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { countKv, listKv } from "@/be/db";
4
+ import { createToolRegistrar } from "@/tools/utils";
5
+ import { KvEntrySchema, KvNamespaceSchema } from "@/types";
6
+ import { resolveNamespace } from "./resolve-namespace";
7
+
8
+ const MAX_KV_LIST_LIMIT = 1000;
9
+
10
+ export const registerKvListTool = (server: McpServer) => {
11
+ createToolRegistrar(server)(
12
+ "kv-list",
13
+ {
14
+ title: "KV List",
15
+ description:
16
+ "List KV entries in the resolved namespace (optionally filtered by key prefix). Expired entries are filtered out. Pagination via limit/offset (limit capped at 1000).",
17
+ annotations: { readOnlyHint: true },
18
+
19
+ inputSchema: z.object({
20
+ prefix: z.string().optional().describe("Key prefix to filter on."),
21
+ limit: z
22
+ .number()
23
+ .int()
24
+ .positive()
25
+ .max(MAX_KV_LIST_LIMIT)
26
+ .optional()
27
+ .describe("Max entries to return (default 100, max 1000)."),
28
+ offset: z.number().int().nonnegative().optional(),
29
+ namespace: KvNamespaceSchema.optional(),
30
+ }),
31
+ outputSchema: z.object({
32
+ yourAgentId: z.string().uuid().optional(),
33
+ success: z.boolean(),
34
+ message: z.string(),
35
+ namespace: z.string().optional(),
36
+ entries: z.array(KvEntrySchema).optional(),
37
+ total: z.number().optional(),
38
+ }),
39
+ },
40
+ async ({ prefix, limit, offset, namespace }, requestInfo) => {
41
+ const resolved = resolveNamespace(namespace, requestInfo);
42
+ if ("error" in resolved) {
43
+ return {
44
+ content: [{ type: "text", text: resolved.error }],
45
+ structuredContent: {
46
+ yourAgentId: requestInfo.agentId,
47
+ success: false,
48
+ message: resolved.error,
49
+ },
50
+ };
51
+ }
52
+ const effectiveLimit = Math.min(limit ?? 100, MAX_KV_LIST_LIMIT);
53
+ const effectivePrefix = prefix && prefix.length > 0 ? prefix : undefined;
54
+ const entries = listKv(resolved.namespace, {
55
+ prefix: effectivePrefix,
56
+ limit: effectiveLimit,
57
+ offset: offset ?? 0,
58
+ });
59
+ const total = countKv(resolved.namespace, { prefix: effectivePrefix });
60
+ return {
61
+ content: [
62
+ {
63
+ type: "text",
64
+ text:
65
+ entries.length === 0
66
+ ? `No entries in "${resolved.namespace}".`
67
+ : `Found ${entries.length} of ${total} entries in "${resolved.namespace}".`,
68
+ },
69
+ ],
70
+ structuredContent: {
71
+ yourAgentId: requestInfo.agentId,
72
+ success: true,
73
+ message: "ok",
74
+ namespace: resolved.namespace,
75
+ entries,
76
+ total,
77
+ },
78
+ };
79
+ },
80
+ );
81
+ };