@desplega.ai/agent-swarm 1.90.0 → 1.91.0

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 (52) hide show
  1. package/openapi.json +74 -1
  2. package/package.json +5 -5
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/memory/providers/sqlite-store.ts +6 -1
  5. package/src/be/memory/types.ts +1 -0
  6. package/src/be/scripts/typecheck.ts +132 -1
  7. package/src/be/seed-scripts/catalog/compound-insights.ts +188 -0
  8. package/src/be/seed-scripts/catalog/schedule-health.ts +73 -0
  9. package/src/be/seed-scripts/catalog/smart-recall.ts +65 -0
  10. package/src/be/seed-scripts/catalog/tool-usage.ts +56 -0
  11. package/src/be/seed-scripts/index.ts +36 -0
  12. package/src/commands/artifact.ts +3 -2
  13. package/src/commands/profile-sync.ts +310 -0
  14. package/src/commands/runner.ts +91 -1
  15. package/src/hooks/hook.ts +32 -9
  16. package/src/http/index.ts +47 -0
  17. package/src/http/integrations.ts +6 -1
  18. package/src/http/mcp-bridge.ts +117 -0
  19. package/src/http/mcp-oauth.ts +97 -39
  20. package/src/http/memory.ts +5 -2
  21. package/src/http/openapi.ts +2 -2
  22. package/src/http/pages-public.ts +10 -11
  23. package/src/http/pages.ts +7 -11
  24. package/src/http/scripts.ts +24 -1
  25. package/src/http/utils.ts +11 -4
  26. package/src/jira/app.ts +2 -3
  27. package/src/jira/webhook-lifecycle.ts +2 -1
  28. package/src/linear/app.ts +2 -3
  29. package/src/providers/claude-adapter.ts +26 -0
  30. package/src/scripts-runtime/executors/native.ts +1 -0
  31. package/src/scripts-runtime/sdk-allowlist.ts +121 -0
  32. package/src/scripts-runtime/swarm-sdk.ts +198 -3
  33. package/src/scripts-runtime/types/stdlib.d.ts +227 -0
  34. package/src/scripts-runtime/types/swarm-sdk.d.ts +227 -0
  35. package/src/tests/claude-adapter-otel.test.ts +85 -1
  36. package/src/tests/hook-registration-nudge.test.ts +69 -0
  37. package/src/tests/mcp-oauth-manual-client.test.ts +213 -0
  38. package/src/tests/pages-public-html.test.ts +41 -0
  39. package/src/tests/pages-public-json-redirect.test.ts +37 -2
  40. package/src/tests/profile-sync.test.ts +282 -0
  41. package/src/tests/scripts-runtime.test.ts +33 -0
  42. package/src/tests/seed-scripts.test.ts +2 -2
  43. package/src/tools/create-metric.ts +2 -3
  44. package/src/tools/create-page.ts +3 -6
  45. package/src/tools/memory-rate.ts +2 -1
  46. package/src/tools/memory-search.ts +1 -0
  47. package/src/tools/register-kapso-number.ts +2 -4
  48. package/src/tools/request-human-input.ts +2 -1
  49. package/src/tools/script-common.ts +2 -4
  50. package/src/tools/script-run.ts +7 -0
  51. package/src/utils/constants.ts +58 -8
  52. package/templates/skills/swarm-scripts/content.md +46 -7
@@ -5,6 +5,7 @@ import { assertSelectOnlyQuery } from "@/http/db-query";
5
5
  import { snapshotMetric } from "@/metrics/version";
6
6
  import { createToolRegistrar } from "@/tools/utils";
7
7
  import { MetricDefinitionSchema } from "@/types";
8
+ import { getAppUrl } from "@/utils/constants";
8
9
 
9
10
  function slugify(input: string): string {
10
11
  const slug = input
@@ -16,9 +17,7 @@ function slugify(input: string): string {
16
17
  }
17
18
 
18
19
  function getAppBaseUrl(): string {
19
- const env = process.env.APP_URL?.trim();
20
- if (env) return env.replace(/\/+$/, "");
21
- return "http://localhost:5274";
20
+ return getAppUrl();
22
21
  }
23
22
 
24
23
  function metricEditCounter(metricId: string): number {
@@ -23,6 +23,7 @@ import { createPage, getPage, getPageBySlug, getPageVersions, updatePage } from
23
23
  import { snapshotPage } from "@/pages/version";
24
24
  import { createToolRegistrar } from "@/tools/utils";
25
25
  import { PageAuthModeSchema, PageContentTypeSchema } from "@/types";
26
+ import { getAppUrl, getPublicMcpBaseUrl } from "@/utils/constants";
26
27
 
27
28
  /** Same slugifier used by the HTTP createPage handler. */
28
29
  function slugify(input: string): string {
@@ -35,15 +36,11 @@ function slugify(input: string): string {
35
36
  }
36
37
 
37
38
  function getApiBaseUrl(): string {
38
- const env = process.env.MCP_BASE_URL?.trim();
39
- if (env) return env.replace(/\/+$/, "");
40
- return `http://localhost:${process.env.PORT || "3013"}`;
39
+ return getPublicMcpBaseUrl();
41
40
  }
42
41
 
43
42
  function getAppBaseUrl(): string {
44
- const env = process.env.APP_URL?.trim();
45
- if (env) return env.replace(/\/+$/, "");
46
- return "http://localhost:5274";
43
+ return getAppUrl();
47
44
  }
48
45
 
49
46
  /**
@@ -3,6 +3,7 @@ import * as z from "zod";
3
3
  import { REFERENCES_SOURCE_MAX_LENGTH, sanitizeReferencesSource } from "@/be/memory/raters/types";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
5
  import { getApiKey } from "@/utils/api-key";
6
+ import { getMcpBaseUrl } from "@/utils/constants";
6
7
 
7
8
  /**
8
9
  * Plan: thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-5.md §1
@@ -92,7 +93,7 @@ export const registerMemoryRateTool = (server: McpServer) => {
92
93
  cleanedReferencesSource = cleaned;
93
94
  }
94
95
 
95
- const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
96
+ const apiUrl = getMcpBaseUrl();
96
97
  const apiKey = getApiKey();
97
98
 
98
99
  const event = {
@@ -125,6 +125,7 @@ export const registerMemorySearchTool = (server: McpServer) => {
125
125
  scope: scope as "agent" | "swarm" | "all",
126
126
  limit,
127
127
  isLead,
128
+ source,
128
129
  });
129
130
 
130
131
  const mapped = recent.map((r) => ({
@@ -10,13 +10,11 @@ import {
10
10
  putKapsoNumberMapping,
11
11
  } from "@/integrations/kapso/config";
12
12
  import { createToolRegistrar } from "@/tools/utils";
13
+ import { getPublicMcpBaseUrl } from "@/utils/constants";
13
14
 
14
15
  /** Build the native inbound webhook URL the swarm exposes for Kapso deliveries. */
15
16
  function nativeWebhookUrl(): string {
16
- const base = (
17
- process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`
18
- ).replace(/\/+$/, "");
19
- return `${base}/api/integrations/kapso/webhook`;
17
+ return `${getPublicMcpBaseUrl()}/api/integrations/kapso/webhook`;
20
18
  }
21
19
 
22
20
  export const registerRegisterKapsoNumberTool = (server: McpServer) => {
@@ -2,6 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as z from "zod";
3
3
  import { createApprovalRequest, getAgentCurrentTask } from "@/be/db";
4
4
  import { createToolRegistrar } from "@/tools/utils";
5
+ import { getAppUrl } from "@/utils/constants";
5
6
 
6
7
  const QuestionSchema = z.object({
7
8
  id: z.string().describe("Unique ID for the question (used as key in responses)"),
@@ -94,7 +95,7 @@ export const registerRequestHumanInputTool = (server: McpServer) => {
94
95
  timeoutSeconds,
95
96
  });
96
97
 
97
- const appUrl = process.env.APP_URL || "http://localhost:5274";
98
+ const appUrl = getAppUrl();
98
99
  const url = `${appUrl}/approval-requests/${request.id}`;
99
100
 
100
101
  return {
@@ -1,6 +1,7 @@
1
1
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  import * as z from "zod";
3
3
  import { getApiKey } from "@/utils/api-key";
4
+ import { getMcpBaseUrl } from "@/utils/constants";
4
5
  import type { RequestInfo } from "./utils";
5
6
 
6
7
  export const SCRIPT_TRANSPORT_ERROR =
@@ -20,10 +21,7 @@ export const scriptToolOutputSchema = z.object({
20
21
  export type ScriptToolStructuredContent = z.infer<typeof scriptToolOutputSchema>;
21
22
 
22
23
  function apiBaseUrl(): string {
23
- return (process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`).replace(
24
- /\/+$/,
25
- "",
26
- );
24
+ return getMcpBaseUrl();
27
25
  }
28
26
 
29
27
  function toolError(message: string, status = 400): CallToolResult {
@@ -28,6 +28,13 @@ export const registerScriptRunTool = (server: McpServer) => {
28
28
  fsMode: scriptFsModeSchema
29
29
  .default("none")
30
30
  .describe("Filesystem mode. v1 supports none only."),
31
+ idempotencyKey: z
32
+ .string()
33
+ .max(200)
34
+ .optional()
35
+ .describe(
36
+ "When set, output is auto-persisted to kv under script:executions/{key}. Re-running with the same key overwrites. Queryable via kv-get.",
37
+ ),
31
38
  }),
32
39
  outputSchema: scriptToolOutputSchema,
33
40
  },
@@ -3,19 +3,69 @@
3
3
  */
4
4
 
5
5
  /**
6
- * Default dashboard URL used when `APP_URL` is unset. Points at the public
7
- * production dashboard so links (Slack messages, approval URLs, etc.) are
8
- * always renderable. Self-hosted operators should set `APP_URL` to override.
6
+ * Default dashboard URL used when neither `APP_URL` nor the deprecated
7
+ * `DASHBOARD_URL` is set. Points at the public production dashboard so links
8
+ * (Slack messages, approval URLs, page share links, post-OAuth redirects)
9
+ * stay renderable even when an operator forgets to configure it. Local dev
10
+ * should set `APP_URL` (e.g. in `.env`) to point at the local dashboard.
9
11
  */
10
12
  export const DEFAULT_APP_URL = "https://app.agent-swarm.dev";
11
13
 
12
14
  /**
13
- * Resolve the effective app/dashboard URL from `APP_URL` (with trailing
14
- * slashes stripped), falling back to {@link DEFAULT_APP_URL}.
15
+ * Resolve every explicitly configured app/dashboard URL. Each env var may be
16
+ * a comma-separated origin list; entries are returned in precedence order with
17
+ * trailing slashes stripped.
18
+ *
19
+ * Precedence: `APP_URL` entries → `DASHBOARD_URL` entries (deprecated alias,
20
+ * kept for back-compat).
21
+ */
22
+ export function getConfiguredAppUrls(): string[] {
23
+ return [process.env.APP_URL, process.env.DASHBOARD_URL]
24
+ .flatMap((value) => (value ?? "").split(","))
25
+ .map((value) => value.trim().replace(/\/+$/, ""))
26
+ .filter(Boolean);
27
+ }
28
+
29
+ /**
30
+ * Resolve the effective app/dashboard URL — the public origin the user's
31
+ * browser is sent to (post-login redirects, Slack/approval links, page
32
+ * `app_url` share links). Trailing slashes are stripped.
33
+ *
34
+ * Precedence: first configured `APP_URL` entry → first configured
35
+ * `DASHBOARD_URL` entry (deprecated alias, kept for back-compat) → fallback.
36
+ * This is the single source of truth; call sites must not re-read
37
+ * `APP_URL`/`DASHBOARD_URL` directly.
38
+ */
39
+ export function getAppUrl(fallback = DEFAULT_APP_URL): string {
40
+ return (getConfiguredAppUrls()[0] || fallback).replace(/\/+$/, "");
41
+ }
42
+
43
+ /**
44
+ * Internal API/MCP base URL — how workers/agents and in-process callers reach
45
+ * the API server. May be a private/cluster address (e.g. the Helm ClusterIP
46
+ * `http://<release>-api:3013`). Do NOT use for browser-facing or
47
+ * externally-registered URLs (OAuth redirect URIs, webhook URLs): those must
48
+ * resolve to a host the browser / third party can reach — use
49
+ * {@link getPublicMcpBaseUrl} (no request context) or `deriveApiBaseUrl(req)`
50
+ * (request-scoped) instead. Trailing slashes are stripped.
51
+ */
52
+ export function getMcpBaseUrl(): string {
53
+ const raw = process.env.MCP_BASE_URL?.trim();
54
+ return (raw || `http://localhost:${process.env.PORT || "3013"}`).replace(/\/+$/, "");
55
+ }
56
+
57
+ /**
58
+ * Public, browser-/externally-reachable origin of the API server — where
59
+ * `/api/mcp-oauth/callback`, OAuth redirect URIs, and registered webhook URLs
60
+ * resolve. Falls back to {@link getMcpBaseUrl} when the public and internal
61
+ * hosts are the same (local dev, single-box, or an ngrok/tunnel set as
62
+ * `MCP_BASE_URL`). In split deployments (Helm), set `PUBLIC_MCP_BASE_URL` to
63
+ * the public ingress URL while `MCP_BASE_URL` stays the internal service
64
+ * address. Trailing slashes are stripped.
15
65
  */
16
- export function getAppUrl(): string {
17
- const raw = process.env.APP_URL?.trim();
18
- return (raw || DEFAULT_APP_URL).replace(/\/+$/, "");
66
+ export function getPublicMcpBaseUrl(): string {
67
+ const raw = process.env.PUBLIC_MCP_BASE_URL?.trim();
68
+ return raw ? raw.replace(/\/+$/, "") : getMcpBaseUrl();
19
69
  }
20
70
 
21
71
  /**
@@ -32,16 +32,18 @@ Use `script-query-types` before non-trivial work so the script matches the live
32
32
  Use `script-run` with inline source for one-off work:
33
33
 
34
34
  ```typescript
35
- export default async function main(args: { status: string; limit: number }, ctx) {
35
+ export default async function main(args: any, ctx: any) {
36
36
  const { swarm, logger } = ctx;
37
- const result = await swarm.task_list({ status: args.status, limit: args.limit });
38
- logger.info(`Fetched ${result.tasks.length} tasks`);
37
+ // All SDK methods return Promise<unknown> unwrap defensively.
38
+ const res: any = await swarm.task_list({ status: args?.status, limit: args?.limit ?? 50 });
39
+ const tasks: any[] = res?.data?.tasks ?? res?.tasks ?? [];
40
+ logger.info(`Fetched ${tasks.length} tasks`);
39
41
  return {
40
- total: result.tasks.length,
41
- tasks: result.tasks.map((task) => ({
42
+ total: tasks.length,
43
+ tasks: tasks.map((task: any) => ({
42
44
  id: task.id,
43
45
  status: task.status,
44
- title: task.task.slice(0, 120),
46
+ title: task.task?.slice(0, 120),
45
47
  })),
46
48
  };
47
49
  }
@@ -60,8 +62,45 @@ Good named scripts:
60
62
  - Fan out over many swarm tasks, memories, repos, or schedules.
61
63
  - Convert noisy JSON or HTML into a compact summary.
62
64
 
65
+ ## Using `db_query` For Aggregation
66
+
67
+ For scripts that aggregate over tasks, sessions, or memory, `ctx.swarm.db_query` with direct SQL is far more efficient than fetching lists client-side.
68
+
69
+ **The parameter is `sql`, not `query`:**
70
+
71
+ ```typescript
72
+ // CORRECT
73
+ const res = await ctx.swarm.db_query({ sql: "SELECT status, count(*) as cnt FROM agent_tasks GROUP BY status" });
74
+
75
+ // WRONG — silently returns no data
76
+ const res = await ctx.swarm.db_query({ query: "SELECT ..." });
77
+ ```
78
+
79
+ **`db_query` returns positional rows, not objects.** The response shape is `{ rows: unknown[][], columns: string[] }`. Zip them into objects:
80
+
81
+ ```typescript
82
+ function rowsToObjects(res: any): any[] {
83
+ const p = res?.data ?? res;
84
+ const cols: string[] = p?.columns ?? [];
85
+ return (p?.rows ?? []).map((r: any) =>
86
+ Array.isArray(r) ? Object.fromEntries(cols.map((c, i) => [c, r[i]])) : r,
87
+ );
88
+ }
89
+
90
+ const rows = rowsToObjects(await ctx.swarm.db_query({
91
+ sql: `SELECT status, count(*) as cnt FROM agent_tasks WHERE createdAt > datetime('now','-3 days') GROUP BY status`,
92
+ }));
93
+ // rows = [{ status: "completed", cnt: 42 }, ...]
94
+ ```
95
+
96
+ **Common tables:** `agent_tasks` (tasks), `session_logs` (tool call logs), `agent_memory` (memories), `scheduled_tasks` (schedules), `agents` (agent registry).
97
+
98
+ **`session_logs` has no `tool_name` column.** Tool names are embedded in the `content` JSON column. Extract them SQL-side with `instr`/`substr` or parse JSON in JS after fetching.
99
+
63
100
  ## SDK And Context Gotchas
64
101
 
102
+ - **`args` can be undefined.** When a script is called without arguments, `args` is `undefined`. Always guard: `argsSchema.safeParse(args || {})` or use optional chaining (`args?.field`).
103
+ - **All SDK methods return `Promise<unknown>`.** Never assume a specific return shape without defensive unwrapping (`res?.data?.tasks ?? res?.tasks ?? []`). Run `script-query-types` to see live type signatures — return types are `unknown` and actual shapes vary by endpoint.
65
104
  - `agentId` is propagated to scripts via the `X-Agent-ID` header, so SDK calls run as the invoking agent.
66
105
  - `taskId` is not ambient. If a script needs to call `ctx.swarm.task_storeProgress`, pass `taskId` explicitly in `args`.
67
106
  - Scripts invoked from a workflow script node may run with a workflow identity rather than a human or worker agent identity.
@@ -73,7 +112,7 @@ Good named scripts:
73
112
  Thread task identity explicitly:
74
113
 
75
114
  ```typescript
76
- export default async function main(args: { taskId: string; items: string[] }, ctx) {
115
+ export default async function main(args: { taskId: string; items: string[] }, ctx: any) {
77
116
  const { swarm } = ctx;
78
117
  await swarm.task_storeProgress({
79
118
  taskId: args.taskId,