From 7e845b3694bc8bf6ed646f5d6e2e36954fb9230c Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 11 May 2026 23:23:47 -0700 Subject: [PATCH 1/3] fix(tools): use notification destination ids --- .changeset/agent-notification-destinations.md | 6 ++ packages/cli/src/commands/notify.ts | 59 +++++-------------- packages/cli/tests/commands/notify.test.ts | 15 +++-- packages/tools/src/contracts.ts | 31 ++-------- packages/tools/tests/client.test.ts | 19 ++++-- 5 files changed, 50 insertions(+), 80 deletions(-) create mode 100644 .changeset/agent-notification-destinations.md diff --git a/.changeset/agent-notification-destinations.md b/.changeset/agent-notification-destinations.md new file mode 100644 index 00000000..90eb9d41 --- /dev/null +++ b/.changeset/agent-notification-destinations.md @@ -0,0 +1,6 @@ +--- +"@outlit/cli": patch +"@outlit/tools": patch +--- + +Use notification destination ids for Outlit notification tools. diff --git a/packages/cli/src/commands/notify.ts b/packages/cli/src/commands/notify.ts index 3139a755..a5766f9b 100644 --- a/packages/cli/src/commands/notify.ts +++ b/packages/cli/src/commands/notify.ts @@ -1,9 +1,5 @@ import { readFileSync } from "node:fs" -import { - customerToolContracts, - notificationProviderValues, - notificationSeverityValues, -} from "@outlit/tools" +import { customerToolContracts, notificationSeverityValues } from "@outlit/tools" import { defineCommand } from "citty" import { authArgs } from "../args/auth" import { AGENT_JSON_HINT, outputArgs } from "../args/output" @@ -11,14 +7,10 @@ import { getClientOrExit, runTool } from "../lib/api" import { errorMessage, outputError } from "../lib/output" type NotificationSeverity = (typeof notificationSeverityValues)[number] -type NotificationProvider = (typeof notificationProviderValues)[number] -type NotificationDestination = { - provider: NotificationProvider - channelId?: string -} const MAX_DESTINATION_COUNT = 10 -const MAX_DESTINATION_CHANNEL_ID_LENGTH = 240 +const destinationIdPattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i function parsePayload(raw: string): unknown { try { @@ -56,40 +48,20 @@ function payloadSizeIsValid(payload: unknown): boolean { return typeof serialized === "string" && serialized.length <= 100000 } -function parseDestinations(raw: string | undefined): NotificationDestination[] | null | undefined { +function parseDestinationIds(raw: string | undefined): string[] | null | undefined { if (raw === undefined) { return undefined } - const destinations = raw.split(",").map((entry) => entry.trim()) + const destinationIds = raw.split(",").map((entry) => entry.trim()) if ( - destinations.length > MAX_DESTINATION_COUNT || - destinations.some((entry) => entry.length === 0) + destinationIds.length > MAX_DESTINATION_COUNT || + destinationIds.some((entry) => entry.length === 0 || !destinationIdPattern.test(entry)) ) { return null } - const parsed: NotificationDestination[] = [] - for (const destination of destinations) { - const [providerInput, ...channelParts] = destination.split(":") - const provider = providerInput?.trim().toLowerCase() - const channelId = channelParts.join(":").trim() - - if (!notificationProviderValues.includes(provider as NotificationProvider)) { - return null - } - - if (channelId.length > MAX_DESTINATION_CHANNEL_ID_LENGTH) { - return null - } - - parsed.push({ - provider: provider as NotificationProvider, - ...(channelId.length > 0 ? { channelId } : {}), - }) - } - - return parsed + return destinationIds } export default defineCommand({ @@ -105,7 +77,7 @@ export default defineCommand({ " outlit notify --title 'Risk found' --markdown '**Risk found**\\n\\n- Customer: acme.com'", " outlit notify --title 'Risk found' '{\"customer\":\"acme.com\"}'", " outlit notify --title 'Risk found' --payload-file ./payload.json", - " outlit notify --title 'Escalation' --markdown '**Check this account**' --destination slack:C123", + " outlit notify --title 'Escalation' --markdown '**Check this account**' --destination 00000000-0000-4000-8000-000000000001", " outlit notify --title 'Escalation' --severity HIGH --message 'Check this account' '{\"customer\":\"acme.com\"}'", "", AGENT_JSON_HINT, @@ -154,8 +126,7 @@ export default defineCommand({ }, destination: { type: "string", - description: - "Optional comma-separated destinations in provider[:channelId] form. Supported provider: slack", + description: "Optional comma-separated NotificationDestination ids.", }, }, async run({ args }) { @@ -250,11 +221,11 @@ export default defineCommand({ severity = normalized } - const destinations = parseDestinations(args.destination) - if (destinations === null) { + const destinationIds = parseDestinationIds(args.destination) + if (destinationIds === null) { return outputError( { - message: `--destination must use provider[:channelId] with provider one of: ${notificationProviderValues.join(", ")}`, + message: "--destination must be one or more comma-separated NotificationDestination ids", code: "invalid_input", }, json, @@ -329,8 +300,8 @@ export default defineCommand({ params.subject = subject } - if (destinations !== undefined) { - params.destinations = destinations + if (destinationIds !== undefined) { + params.destinationIds = destinationIds } return runTool(client, customerToolContracts.outlit_send_notification.toolName, params, json) diff --git a/packages/cli/tests/commands/notify.test.ts b/packages/cli/tests/commands/notify.test.ts index f41fd772..4e3b6147 100644 --- a/packages/cli/tests/commands/notify.test.ts +++ b/packages/cli/tests/commands/notify.test.ts @@ -109,7 +109,7 @@ describe("notify", () => { args: { title: "Risk found", markdown: " **Risk found**\n\n- Customer: Acme ", - destination: "slack:C456", + destination: "00000000-0000-4000-8000-000000000001", json: true, }, } as Parameters>[0]) @@ -119,7 +119,7 @@ describe("notify", () => { expect.objectContaining({ title: "Risk found", markdown: "**Risk found**\n\n- Customer: Acme", - destinations: [{ provider: "slack", channelId: "C456" }], + destinationIds: ["00000000-0000-4000-8000-000000000001"], }), ) } finally { @@ -352,7 +352,7 @@ describe("notify", () => { args: { title: "Risk found", markdown: "**Risk found**", - destination: "teams:C123", + destination: "not-a-destination-id", json: true, }, } as Parameters>[0]) @@ -379,7 +379,10 @@ describe("notify", () => { args: { title: "Risk found", markdown: "**Risk found**", - destination: Array.from({ length: 11 }, (_, index) => `slack:C${index}`).join(","), + destination: Array.from( + { length: 11 }, + (_, index) => `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`, + ).join(","), json: true, }, } as Parameters>[0]) @@ -395,7 +398,7 @@ describe("notify", () => { } }) - test("destination channelId over 240 characters returns invalid_input", async () => { + test("malformed destination id returns invalid_input", async () => { const { default: notifyCmd } = await import("../../src/commands/notify") const exitSpy = mockExitThrow() const stderrSpy = spyOn(process.stderr, "write").mockImplementation(() => true) @@ -406,7 +409,7 @@ describe("notify", () => { args: { title: "Risk found", markdown: "**Risk found**", - destination: `slack:${"C".repeat(241)}`, + destination: "00000000-0000-0000-0000-000000000001", json: true, }, } as Parameters>[0]) diff --git a/packages/tools/src/contracts.ts b/packages/tools/src/contracts.ts index c75405da..5ba5e6b5 100644 --- a/packages/tools/src/contracts.ts +++ b/packages/tools/src/contracts.ts @@ -674,35 +674,14 @@ export const customerToolContracts = { minLength: 1, maxLength: 240, }, - destinations: { - description: - "Optional explicit notification destinations. Omit to use the default notifier.", + destinationIds: { + description: "Optional NotificationDestination ids. Omit to use the default notifier.", minItems: 1, maxItems: 10, type: "array", items: { - type: "object", - properties: { - provider: { - description: "Notification provider", - type: "string", - enum: ["slack"], - }, - channelId: { - description: "Provider-specific destination channel ID", - type: "string", - minLength: 1, - maxLength: 240, - }, - label: { - description: "Optional destination label", - type: "string", - minLength: 1, - maxLength: 120, - }, - }, - required: ["provider"], - additionalProperties: false, + type: "string", + format: "uuid", }, }, }, @@ -847,7 +826,7 @@ export const userListOrderFields = ["last_activity_at", "first_seen_at", "email" export const schemaTables = ["activity", "customers", "users", "revenue"] as const export const customerToolContractHash = - "5016d146842452855bfdd9ecc8f6be1a122125d0c392ceff315aeb2e44fefc7f" as const + "0374a55c4eb25b31c9ac41f50b890992691ea9be233d0febe64724b7b3c2fba6" as const export type CustomerToolName = (typeof customerToolNames)[number] export type CustomerSourceType = (typeof customerSourceTypes)[number] diff --git a/packages/tools/tests/client.test.ts b/packages/tools/tests/client.test.ts index a4cf3c35..969af46f 100644 --- a/packages/tools/tests/client.test.ts +++ b/packages/tools/tests/client.test.ts @@ -129,7 +129,7 @@ describe("tool contracts", () => { { enum?: string[] type?: string - items?: { properties?: Record } + items?: { format?: string; type?: string } } > @@ -140,7 +140,18 @@ describe("tool contracts", () => { type: "string", }), ) - expect(properties.destinations?.items?.properties?.provider?.enum).toEqual(["slack"]) + expect(properties.destinations).toBeUndefined() + expect(properties.destinationIds).toEqual( + expect.objectContaining({ + type: "array", + minItems: 1, + maxItems: 10, + items: expect.objectContaining({ + type: "string", + format: "uuid", + }), + }), + ) expect(properties.severity).toEqual( expect.objectContaining({ enum: ["low", "medium", "high"], @@ -213,7 +224,7 @@ describe("createOutlitClient", () => { title: "Reminder", markdown: "**Reminder**\n\n- Customer: Acme", severity: "low", - destinations: [{ provider: "slack", channelId: "C123" }], + destinationIds: ["00000000-0000-4000-8000-000000000001"], }) expect(fetchMock).toHaveBeenCalledWith("https://example.outlit.test/api/tools/call", { @@ -228,7 +239,7 @@ describe("createOutlitClient", () => { title: "Reminder", markdown: "**Reminder**\n\n- Customer: Acme", severity: "low", - destinations: [{ provider: "slack", channelId: "C123" }], + destinationIds: ["00000000-0000-4000-8000-000000000001"], }, }), }) From 62fb14ed6358202b53a07431410daa6ae8306cf1 Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 11 May 2026 23:38:48 -0700 Subject: [PATCH 2/3] fix(tools): regenerate notification destination contracts --- packages/tools/src/contracts.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/tools/src/contracts.ts b/packages/tools/src/contracts.ts index 5ba5e6b5..be863af4 100644 --- a/packages/tools/src/contracts.ts +++ b/packages/tools/src/contracts.ts @@ -682,6 +682,8 @@ export const customerToolContracts = { items: { type: "string", format: "uuid", + pattern: + "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", }, }, }, @@ -826,7 +828,7 @@ export const userListOrderFields = ["last_activity_at", "first_seen_at", "email" export const schemaTables = ["activity", "customers", "users", "revenue"] as const export const customerToolContractHash = - "0374a55c4eb25b31c9ac41f50b890992691ea9be233d0febe64724b7b3c2fba6" as const + "03e7ece1d8834b065592368173f5ff46a24ffc50f72d1bb9b0505ec5864f3ff6" as const export type CustomerToolName = (typeof customerToolNames)[number] export type CustomerSourceType = (typeof customerSourceTypes)[number] From 6ea6b47b83ead963438c3c31475263da20d89acd Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 11 May 2026 23:45:46 -0700 Subject: [PATCH 3/3] fix(pi): align notification destination contract test --- .changeset/agent-notification-destinations.md | 1 + packages/pi/tests/extension.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/agent-notification-destinations.md b/.changeset/agent-notification-destinations.md index 90eb9d41..5f56ec56 100644 --- a/.changeset/agent-notification-destinations.md +++ b/.changeset/agent-notification-destinations.md @@ -1,5 +1,6 @@ --- "@outlit/cli": patch +"@outlit/pi": patch "@outlit/tools": patch --- diff --git a/packages/pi/tests/extension.test.ts b/packages/pi/tests/extension.test.ts index 54606575..cc38c4c8 100644 --- a/packages/pi/tests/extension.test.ts +++ b/packages/pi/tests/extension.test.ts @@ -176,7 +176,7 @@ describe("createOutlitPiExtension", () => { ) { expect(tool.parameters.required).toEqual(["title"]) expect(Object.keys(tool.parameters.properties ?? {})).toEqual( - expect.arrayContaining(["title", "markdown", "payload", "destinations"]), + expect.arrayContaining(["title", "markdown", "payload", "destinationIds"]), ) } else { throw new Error("Expected notification tool parameters to expose required fields")