Skip to content

Commit 29ca464

Browse files
authored
chore: sync tool contracts (#144)
1 parent 4e77ae3 commit 29ca464

6 files changed

Lines changed: 57 additions & 85 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@outlit/tools": patch
3+
"@outlit/cli": patch
4+
"@outlit/pi": patch
5+
---
6+
7+
Sync notification tool contracts with hosted churn agent destination IDs.

packages/cli/src/commands/notify.ts

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
11
import { readFileSync } from "node:fs"
2-
import {
3-
customerToolContracts,
4-
notificationProviderValues,
5-
notificationSeverityValues,
6-
} from "@outlit/tools"
2+
import { customerToolContracts, notificationSeverityValues } from "@outlit/tools"
73
import { defineCommand } from "citty"
84
import { authArgs } from "../args/auth"
95
import { AGENT_JSON_HINT, outputArgs } from "../args/output"
106
import { getClientOrExit, runTool } from "../lib/api"
117
import { errorMessage, outputError } from "../lib/output"
128

139
type NotificationSeverity = (typeof notificationSeverityValues)[number]
14-
type NotificationProvider = (typeof notificationProviderValues)[number]
15-
type NotificationDestination = {
16-
provider: NotificationProvider
17-
channelId?: string
18-
}
1910

20-
const MAX_DESTINATION_COUNT = 10
21-
const MAX_DESTINATION_CHANNEL_ID_LENGTH = 240
11+
const MAX_DESTINATION_ID_COUNT = 10
12+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
2213

2314
function parsePayload(raw: string): unknown {
2415
try {
@@ -56,40 +47,20 @@ function payloadSizeIsValid(payload: unknown): boolean {
5647
return typeof serialized === "string" && serialized.length <= 100000
5748
}
5849

59-
function parseDestinations(raw: string | undefined): NotificationDestination[] | null | undefined {
50+
function parseDestinationIds(raw: string | undefined): string[] | null | undefined {
6051
if (raw === undefined) {
6152
return undefined
6253
}
6354

64-
const destinations = raw.split(",").map((entry) => entry.trim())
55+
const destinationIds = raw.split(",").map((entry) => entry.trim())
6556
if (
66-
destinations.length > MAX_DESTINATION_COUNT ||
67-
destinations.some((entry) => entry.length === 0)
57+
destinationIds.length > MAX_DESTINATION_ID_COUNT ||
58+
destinationIds.some((entry) => entry.length === 0 || !UUID_PATTERN.test(entry))
6859
) {
6960
return null
7061
}
7162

72-
const parsed: NotificationDestination[] = []
73-
for (const destination of destinations) {
74-
const [providerInput, ...channelParts] = destination.split(":")
75-
const provider = providerInput?.trim().toLowerCase()
76-
const channelId = channelParts.join(":").trim()
77-
78-
if (!notificationProviderValues.includes(provider as NotificationProvider)) {
79-
return null
80-
}
81-
82-
if (channelId.length > MAX_DESTINATION_CHANNEL_ID_LENGTH) {
83-
return null
84-
}
85-
86-
parsed.push({
87-
provider: provider as NotificationProvider,
88-
...(channelId.length > 0 ? { channelId } : {}),
89-
})
90-
}
91-
92-
return parsed
63+
return destinationIds
9364
}
9465

9566
export default defineCommand({
@@ -99,13 +70,13 @@ export default defineCommand({
9970
"Send a notification through Outlit to the organization's configured notifier.",
10071
"",
10172
"Use --markdown or --markdown-file for the human-readable body; Outlit renders it for the destination platform.",
102-
"Add a JSON or raw payload when useful for structured context. When --destination is omitted, Outlit uses the default notifier.",
73+
"Add a JSON or raw payload when useful for structured context. When --destination-id is omitted, Outlit uses the default notifier.",
10374
"",
10475
"Examples:",
10576
" outlit notify --title 'Risk found' --markdown '**Risk found**\\n\\n- Customer: acme.com'",
10677
" outlit notify --title 'Risk found' '{\"customer\":\"acme.com\"}'",
10778
" outlit notify --title 'Risk found' --payload-file ./payload.json",
108-
" outlit notify --title 'Escalation' --markdown '**Check this account**' --destination slack:C123",
79+
" outlit notify --title 'Escalation' --markdown '**Check this account**' --destination-id 00000000-0000-4000-8000-000000000001",
10980
" outlit notify --title 'Escalation' --severity HIGH --message 'Check this account' '{\"customer\":\"acme.com\"}'",
11081
"",
11182
AGENT_JSON_HINT,
@@ -152,10 +123,10 @@ export default defineCommand({
152123
type: "string",
153124
description: "Optional subject line",
154125
},
155-
destination: {
126+
"destination-id": {
156127
type: "string",
157128
description:
158-
"Optional comma-separated destinations in provider[:channelId] form. Supported provider: slack",
129+
"Optional comma-separated notification destination IDs. Omit to use the default notifier.",
159130
},
160131
},
161132
async run({ args }) {
@@ -250,11 +221,11 @@ export default defineCommand({
250221
severity = normalized
251222
}
252223

253-
const destinations = parseDestinations(args.destination)
254-
if (destinations === null) {
224+
const destinationIds = parseDestinationIds(args["destination-id"])
225+
if (destinationIds === null) {
255226
return outputError(
256227
{
257-
message: `--destination must use provider[:channelId] with provider one of: ${notificationProviderValues.join(", ")}`,
228+
message: "--destination-id must be one or more comma-separated UUIDs",
258229
code: "invalid_input",
259230
},
260231
json,
@@ -329,8 +300,8 @@ export default defineCommand({
329300
params.subject = subject
330301
}
331302

332-
if (destinations !== undefined) {
333-
params.destinations = destinations
303+
if (destinationIds !== undefined) {
304+
params.destinationIds = destinationIds
334305
}
335306

336307
return runTool(client, customerToolContracts.outlit_send_notification.toolName, params, json)

packages/cli/tests/commands/notify.test.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,17 @@ describe("notify", () => {
100100
}
101101
})
102102

103-
test("sends markdown without a payload and forwards explicit destinations", async () => {
103+
test("sends markdown without a payload and forwards explicit destination IDs", async () => {
104104
const { default: notifyCmd } = await import("../../src/commands/notify")
105105
const writeSpy = spyOn(process.stdout, "write").mockImplementation(() => true)
106+
const destinationId = "00000000-0000-4000-8000-000000000001"
106107

107108
try {
108109
await notifyCmd.run!({
109110
args: {
110111
title: "Risk found",
111112
markdown: " **Risk found**\n\n- Customer: Acme ",
112-
destination: "slack:C456",
113+
"destination-id": destinationId,
113114
json: true,
114115
},
115116
} as Parameters<NonNullable<typeof notifyCmd.run>>[0])
@@ -119,7 +120,7 @@ describe("notify", () => {
119120
expect.objectContaining({
120121
title: "Risk found",
121122
markdown: "**Risk found**\n\n- Customer: Acme",
122-
destinations: [{ provider: "slack", channelId: "C456" }],
123+
destinationIds: [destinationId],
123124
}),
124125
)
125126
} finally {
@@ -341,7 +342,7 @@ describe("notify", () => {
341342
}
342343
})
343344

344-
test("invalid destination returns invalid_input", async () => {
345+
test("invalid destination ID returns invalid_input", async () => {
345346
const { default: notifyCmd } = await import("../../src/commands/notify")
346347
const exitSpy = mockExitThrow()
347348
const stderrSpy = spyOn(process.stderr, "write").mockImplementation(() => true)
@@ -352,7 +353,7 @@ describe("notify", () => {
352353
args: {
353354
title: "Risk found",
354355
markdown: "**Risk found**",
355-
destination: "teams:C123",
356+
"destination-id": "not-a-uuid",
356357
json: true,
357358
},
358359
} as Parameters<NonNullable<typeof notifyCmd.run>>[0])
@@ -372,14 +373,18 @@ describe("notify", () => {
372373
const { default: notifyCmd } = await import("../../src/commands/notify")
373374
const exitSpy = mockExitThrow()
374375
const stderrSpy = spyOn(process.stderr, "write").mockImplementation(() => true)
376+
const destinationIds = Array.from(
377+
{ length: 11 },
378+
(_, index) => `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`,
379+
).join(",")
375380

376381
let thrown: unknown
377382
try {
378383
await notifyCmd.run!({
379384
args: {
380385
title: "Risk found",
381386
markdown: "**Risk found**",
382-
destination: Array.from({ length: 11 }, (_, index) => `slack:C${index}`).join(","),
387+
"destination-id": destinationIds,
383388
json: true,
384389
},
385390
} as Parameters<NonNullable<typeof notifyCmd.run>>[0])
@@ -395,7 +400,7 @@ describe("notify", () => {
395400
}
396401
})
397402

398-
test("destination channelId over 240 characters returns invalid_input", async () => {
403+
test("mixed valid and invalid destination IDs return invalid_input", async () => {
399404
const { default: notifyCmd } = await import("../../src/commands/notify")
400405
const exitSpy = mockExitThrow()
401406
const stderrSpy = spyOn(process.stderr, "write").mockImplementation(() => true)
@@ -406,7 +411,7 @@ describe("notify", () => {
406411
args: {
407412
title: "Risk found",
408413
markdown: "**Risk found**",
409-
destination: `slack:${"C".repeat(241)}`,
414+
"destination-id": "00000000-0000-4000-8000-000000000001,not-a-uuid",
410415
json: true,
411416
},
412417
} as Parameters<NonNullable<typeof notifyCmd.run>>[0])

packages/pi/tests/extension.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ describe("createOutlitPiExtension", () => {
176176
) {
177177
expect(tool.parameters.required).toEqual(["title"])
178178
expect(Object.keys(tool.parameters.properties ?? {})).toEqual(
179-
expect.arrayContaining(["title", "markdown", "payload", "destinations"]),
179+
expect.arrayContaining(["title", "markdown", "payload", "destinationIds"]),
180180
)
181181
} else {
182182
throw new Error("Expected notification tool parameters to expose required fields")

packages/tools/src/contracts.ts

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -744,35 +744,16 @@ export const customerToolContracts = {
744744
minLength: 1,
745745
maxLength: 240,
746746
},
747-
destinations: {
748-
description:
749-
"Optional explicit notification destinations. Omit to use the default notifier.",
747+
destinationIds: {
748+
description: "Optional NotificationDestination ids. Omit to use the default notifier.",
750749
minItems: 1,
751750
maxItems: 10,
752751
type: "array",
753752
items: {
754-
type: "object",
755-
properties: {
756-
provider: {
757-
description: "Notification provider",
758-
type: "string",
759-
enum: ["slack"],
760-
},
761-
channelId: {
762-
description: "Provider-specific destination channel ID",
763-
type: "string",
764-
minLength: 1,
765-
maxLength: 240,
766-
},
767-
label: {
768-
description: "Optional destination label",
769-
type: "string",
770-
minLength: 1,
771-
maxLength: 120,
772-
},
773-
},
774-
required: ["provider"],
775-
additionalProperties: false,
753+
type: "string",
754+
format: "uuid",
755+
pattern:
756+
"^([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)$",
776757
},
777758
},
778759
},
@@ -919,7 +900,7 @@ export const workspaceUserListOrderFields = ["name", "email", "owned_customer_co
919900
export const schemaTables = ["activity", "customers", "users", "revenue"] as const
920901

921902
export const customerToolContractHash =
922-
"2dde2d1482c1e2c81bfd57e48e2f8d5e32d1a8f482fa8fd062708884833fa07a" as const
903+
"c483d23afc5530cdfae07e5e806b092ce0fb73baec919ef7f2b68afd03252844" as const
923904

924905
export type CustomerToolName = (typeof customerToolNames)[number]
925906
export type CustomerSourceType = (typeof customerSourceTypes)[number]

packages/tools/tests/client.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ describe("tool contracts", () => {
161161
{
162162
enum?: string[]
163163
type?: string
164-
items?: { properties?: Record<string, { enum?: string[] }> }
164+
items?: { format?: string; type?: string }
165165
}
166166
>
167167

@@ -172,7 +172,15 @@ describe("tool contracts", () => {
172172
type: "string",
173173
}),
174174
)
175-
expect(properties.destinations?.items?.properties?.provider?.enum).toEqual(["slack"])
175+
expect(properties.destinationIds).toEqual(
176+
expect.objectContaining({
177+
type: "array",
178+
items: expect.objectContaining({
179+
type: "string",
180+
format: "uuid",
181+
}),
182+
}),
183+
)
176184
expect(properties.severity).toEqual(
177185
expect.objectContaining({
178186
enum: ["low", "medium", "high"],
@@ -245,7 +253,7 @@ describe("createOutlitClient", () => {
245253
title: "Reminder",
246254
markdown: "**Reminder**\n\n- Customer: Acme",
247255
severity: "low",
248-
destinations: [{ provider: "slack", channelId: "C123" }],
256+
destinationIds: ["00000000-0000-4000-8000-000000000001"],
249257
})
250258

251259
expect(fetchMock).toHaveBeenCalledWith("https://example.outlit.test/api/tools/call", {
@@ -260,7 +268,7 @@ describe("createOutlitClient", () => {
260268
title: "Reminder",
261269
markdown: "**Reminder**\n\n- Customer: Acme",
262270
severity: "low",
263-
destinations: [{ provider: "slack", channelId: "C123" }],
271+
destinationIds: ["00000000-0000-4000-8000-000000000001"],
264272
},
265273
}),
266274
})

0 commit comments

Comments
 (0)