@ekanos/cli 0.11.0 → 0.13.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 (57) hide show
  1. package/dist/apps-api.d.ts +3 -2
  2. package/dist/apps-api.js +1 -5
  3. package/dist/apps-api.js.map +1 -1
  4. package/dist/commands/agents-edit.js +1 -1
  5. package/dist/commands/agents-edit.js.map +1 -1
  6. package/dist/commands/apps.d.ts +19 -6
  7. package/dist/commands/apps.js +366 -43
  8. package/dist/commands/apps.js.map +1 -1
  9. package/dist/commands/knowledge.d.ts +37 -0
  10. package/dist/commands/knowledge.js +364 -0
  11. package/dist/commands/knowledge.js.map +1 -0
  12. package/dist/commands/routes.d.ts +23 -0
  13. package/dist/commands/routes.js +277 -0
  14. package/dist/commands/routes.js.map +1 -0
  15. package/dist/commands/sources.d.ts +4 -0
  16. package/dist/commands/sources.js +115 -1
  17. package/dist/commands/sources.js.map +1 -1
  18. package/dist/commands/team-agents-triggers.js +2 -1
  19. package/dist/commands/team-agents-triggers.js.map +1 -1
  20. package/dist/commands/teams-connect.d.ts +25 -0
  21. package/dist/commands/teams-connect.js +242 -0
  22. package/dist/commands/teams-connect.js.map +1 -0
  23. package/dist/commands/teams-onboard.d.ts +5 -2
  24. package/dist/commands/teams-onboard.js +12 -40
  25. package/dist/commands/teams-onboard.js.map +1 -1
  26. package/dist/commands/teams.d.ts +60 -5
  27. package/dist/commands/teams.js +141 -46
  28. package/dist/commands/teams.js.map +1 -1
  29. package/dist/connections-api.d.ts +125 -0
  30. package/dist/connections-api.js +156 -0
  31. package/dist/connections-api.js.map +1 -0
  32. package/dist/index.js +90 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/knowledge-api.d.ts +129 -0
  35. package/dist/knowledge-api.js +138 -0
  36. package/dist/knowledge-api.js.map +1 -0
  37. package/dist/routes-api.d.ts +105 -0
  38. package/dist/routes-api.js +147 -0
  39. package/dist/routes-api.js.map +1 -0
  40. package/dist/source-products-api.d.ts +59 -0
  41. package/dist/source-products-api.js +113 -0
  42. package/dist/source-products-api.js.map +1 -0
  43. package/dist/team-connect-api.d.ts +244 -0
  44. package/dist/team-connect-api.js +95 -0
  45. package/dist/team-connect-api.js.map +1 -0
  46. package/dist/teams-api.d.ts +1 -0
  47. package/dist/teams-api.js +1 -1
  48. package/dist/teams-api.js.map +1 -1
  49. package/package.json +2 -2
  50. package/templates/package.json.tmpl +4 -4
  51. package/templates/skills/build-agent/SKILL.md +18 -3
  52. package/templates/skills/build-agent/references/agent-json.md +29 -8
  53. package/templates/skills/build-agent/references/app-setup.md +2 -1
  54. package/templates/skills/build-agent/references/set-up-an-integration.md +76 -10
  55. package/templates/skills/build-agent/references/skills.md +7 -4
  56. package/templates/skills/build-agent/references/triggers.md +44 -15
  57. package/templates/skills/ekanos-platform/SKILL.md +40 -8
@@ -0,0 +1,277 @@
1
+ import { refreshStoredSession, requireSession, resolveAuthEnvironment, } from '../auth/session.js';
2
+ import { authRequiredError, usageError } from '../errors.js';
3
+ import { ReplaySchema, RouteListSchema, RouteOptionsSchema, RouteRemoveSchema, RouteSetSchema, RouteWebhookSchema, UnroutedListSchema, routesCall, } from '../routes-api.js';
4
+ import { resolveSourceTarget } from '../source-target.js';
5
+ export const ROUTES_SUBCOMMANDS = [
6
+ 'list',
7
+ 'options',
8
+ 'set',
9
+ 'remove',
10
+ 'webhook',
11
+ 'unrouted',
12
+ 'replay',
13
+ ];
14
+ const USAGE = 'Run one of: ekanos routes list [<product>] | options <product> | set <product> <value> <team> [--label <name>] | remove <product> <value> | webhook <product> [--webhook <id>] | unrouted [<product>] | replay <event-id>.';
15
+ /**
16
+ * `routes` — a Source admin's routing of a Source connection's events to
17
+ * teams (plan 241.4). One Zendesk connected for the whole Source sends every
18
+ * ticket to one webhook; each ticket's organization decides which team's
19
+ * agents see it. `set` maps a value to a team, `remove` unmaps it, `options`
20
+ * lists the values the integration knows (Zendesk's organizations),
21
+ * `webhook` mints the Source's endpoint URL (shown once), and `unrouted` /
22
+ * `replay` deal with events no team was mapped to — they started nothing.
23
+ */
24
+ export async function runRoutes(ctx, args) {
25
+ var _a, _b;
26
+ const [rawSubcommand, ...operands] = args.positionals;
27
+ const subcommand = parseSubcommand(rawSubcommand);
28
+ const expected = OPERANDS[subcommand];
29
+ if (operands.length < expected.min || operands.length > expected.max) {
30
+ throw usageError(`routes ${subcommand} takes ${expected.usage}.`, `e.g. "ekanos routes ${subcommand} ${expected.example}".`);
31
+ }
32
+ const env = resolveAuthEnvironment(ctx, args.host, args.env, {
33
+ projectDir: args.dir,
34
+ });
35
+ const resolved = await resolveSourceTarget(env, requireSession(env.store, env.host), args);
36
+ const client = new RoutesClient(env, resolved.session);
37
+ const source = resolved.source;
38
+ const send = (schema, init) => client.call((token) => routesCall(env.host, token, schema, Object.assign(Object.assign({}, init), { query: Object.assign({ source }, init.query) })));
39
+ switch (subcommand) {
40
+ case 'list': {
41
+ const product = operands[0] ? requireSlug(operands[0]) : undefined;
42
+ const list = await send(RouteListSchema, {
43
+ method: 'GET',
44
+ query: { product },
45
+ });
46
+ if (list.routes.length === 0) {
47
+ return ctx.succeed(Object.assign({ host: env.host }, list), ctx.ui.result({
48
+ title: `No routes on ${list.source.slug}${product ? ` for ${product}` : ''}`,
49
+ tone: 'warn',
50
+ notes: [
51
+ "Events on the Source's endpoint stay unrouted — they start nothing — until their value is mapped to a team.",
52
+ 'Map one: "ekanos routes set <product> <value> <team>".',
53
+ ],
54
+ }));
55
+ }
56
+ return ctx.succeed(Object.assign({ host: env.host }, list), [
57
+ ctx.ui.bold(`Routes on ${list.source.slug}`),
58
+ ctx.ui.table(['Product', 'Key', 'Value', 'Label', 'Team'], list.routes.map(routeRow)),
59
+ ].join('\n'));
60
+ }
61
+ case 'options': {
62
+ const product = requireSlug(operands[0]);
63
+ const result = await send(RouteOptionsSchema, {
64
+ method: 'GET',
65
+ path: `/${encodeURIComponent(product)}/options`,
66
+ query: {},
67
+ });
68
+ if (!result.options) {
69
+ return ctx.succeed(Object.assign({ host: env.host, product }, result), ctx.ui.result({
70
+ title: `${product} does not list its ${result.key} values`,
71
+ tone: 'warn',
72
+ notes: [
73
+ `Type the ${result.key} id: "ekanos routes set ${product} <value> <team>".`,
74
+ ],
75
+ }));
76
+ }
77
+ return ctx.succeed(Object.assign({ host: env.host, product }, result), [
78
+ ctx.ui.bold(`${product} ${result.key} values`),
79
+ ctx.ui.table(['Value', 'Label'], result.options.map((option) => [option.value, option.label])),
80
+ ].join('\n'));
81
+ }
82
+ case 'set': {
83
+ const product = requireSlug(operands[0]);
84
+ const value = requireValue(operands[1]);
85
+ const team = requireSlug(operands[2], 'team');
86
+ const result = await send(RouteSetSchema, {
87
+ method: 'PUT',
88
+ path: `/${encodeURIComponent(product)}`,
89
+ query: {},
90
+ body: Object.assign({ value, team }, (args.label ? { label: args.label } : {})),
91
+ });
92
+ return ctx.succeed(Object.assign({ host: env.host }, result), ctx.ui.result({
93
+ title: `Routed ${product} ${result.route.key} ${value} to ${(_a = result.route.team.slug) !== null && _a !== void 0 ? _a : result.route.team.name}`,
94
+ rows: [
95
+ ['Source', source],
96
+ ['Label', (_b = result.route.label) !== null && _b !== void 0 ? _b : '—'],
97
+ [
98
+ 'Replaced',
99
+ result.replaced ? 'yes (moved from another team)' : 'no',
100
+ ],
101
+ ],
102
+ notes: [
103
+ 'New events with this value start that team\'s agents. Earlier unrouted ones: "ekanos routes unrouted" then "ekanos routes replay <event-id>".',
104
+ ],
105
+ }));
106
+ }
107
+ case 'remove': {
108
+ const product = requireSlug(operands[0]);
109
+ const value = requireValue(operands[1]);
110
+ const result = await send(RouteRemoveSchema, {
111
+ method: 'DELETE',
112
+ path: `/${encodeURIComponent(product)}`,
113
+ query: { value },
114
+ });
115
+ return ctx.succeed(Object.assign({ host: env.host }, result), ctx.ui.result({
116
+ title: `Unmapped ${product} ${value}`,
117
+ rows: [['Source', source]],
118
+ notes: ['Its events now stay unrouted and start nothing.'],
119
+ }));
120
+ }
121
+ case 'webhook': {
122
+ const product = requireSlug(operands[0]);
123
+ const result = await send(RouteWebhookSchema, {
124
+ method: 'POST',
125
+ path: `/${encodeURIComponent(product)}/webhook`,
126
+ query: { webhook: args.webhook },
127
+ });
128
+ return ctx.succeed(Object.assign({ host: env.host, product }, result), ctx.ui.result({
129
+ title: `Webhook URL for ${product} on ${source} (shown once)`,
130
+ rows: [
131
+ ['Webhook', result.webhookId],
132
+ ['URL', result.url],
133
+ ],
134
+ notes: [
135
+ 'Point the vendor at this URL once, for the whole Source. Minting again revokes the previous URL.',
136
+ ...(result.signature
137
+ ? [
138
+ `Deliveries must be signed (${result.signature.header}); the Source connection needs the "${result.signature.secretName}" secret.`,
139
+ ]
140
+ : []),
141
+ ],
142
+ }));
143
+ }
144
+ case 'unrouted': {
145
+ const product = operands[0] ? requireSlug(operands[0]) : undefined;
146
+ const result = await send(UnroutedListSchema, {
147
+ method: 'GET',
148
+ path: '/unrouted',
149
+ query: { product },
150
+ });
151
+ if (result.events.length === 0) {
152
+ return ctx.succeed(Object.assign({ host: env.host }, result), ctx.ui.result({ title: 'No unrouted events', tone: 'ok' }));
153
+ }
154
+ return ctx.succeed(Object.assign({ host: env.host }, result), [
155
+ ctx.ui.bold('Unrouted events (no team mapped; nothing started)'),
156
+ ctx.ui.table(['Id', 'Product', 'Event', 'Value', 'Received'], result.events.map((event) => { var _a; return [
157
+ event.id,
158
+ event.product,
159
+ event.eventId,
160
+ event.routeValue
161
+ ? `${(_a = event.routeKey) !== null && _a !== void 0 ? _a : ''} ${event.routeValue}`.trim()
162
+ : '(none)',
163
+ event.createdAt,
164
+ ]; })),
165
+ 'Map the value with "ekanos routes set", then "ekanos routes replay <id>".',
166
+ ].join('\n'));
167
+ }
168
+ case 'replay': {
169
+ const id = requireEventId(operands[0]);
170
+ const result = await send(ReplaySchema, {
171
+ method: 'POST',
172
+ path: `/unrouted/${encodeURIComponent(id)}/replay`,
173
+ query: {},
174
+ });
175
+ return ctx.succeed(Object.assign({ host: env.host, id }, result), ctx.ui.result({
176
+ title: result.status === 'routed'
177
+ ? `Routed ${id}`
178
+ : result.status === 'already-routed'
179
+ ? `${id} was already routed`
180
+ : `${id} is still unrouted`,
181
+ tone: result.status === 'unrouted' ? 'warn' : 'ok',
182
+ notes: result.status === 'unrouted'
183
+ ? [
184
+ 'No team is mapped to its value yet: "ekanos routes set" first.',
185
+ ]
186
+ : [],
187
+ }));
188
+ }
189
+ }
190
+ }
191
+ const OPERANDS = {
192
+ list: { min: 0, max: 1, usage: 'an optional product', example: 'zendesk' },
193
+ options: { min: 1, max: 1, usage: 'a product', example: 'zendesk' },
194
+ set: {
195
+ min: 3,
196
+ max: 3,
197
+ usage: '<product> <value> <team>',
198
+ example: 'zendesk 360001234567 first-bank',
199
+ },
200
+ remove: {
201
+ min: 2,
202
+ max: 2,
203
+ usage: '<product> <value>',
204
+ example: 'zendesk 360001234567',
205
+ },
206
+ webhook: { min: 1, max: 1, usage: 'a product', example: 'zendesk' },
207
+ unrouted: {
208
+ min: 0,
209
+ max: 1,
210
+ usage: 'an optional product',
211
+ example: 'zendesk',
212
+ },
213
+ replay: {
214
+ min: 1,
215
+ max: 1,
216
+ usage: 'an event id from "ekanos routes unrouted"',
217
+ example: '<event-id>',
218
+ },
219
+ };
220
+ function routeRow(route) {
221
+ var _a, _b;
222
+ return [
223
+ route.product,
224
+ route.key,
225
+ route.value,
226
+ (_a = route.label) !== null && _a !== void 0 ? _a : '—',
227
+ (_b = route.team.slug) !== null && _b !== void 0 ? _b : route.team.name,
228
+ ];
229
+ }
230
+ class RoutesClient {
231
+ constructor(env, session) {
232
+ this.env = env;
233
+ this.session = session;
234
+ }
235
+ async call(send) {
236
+ let result = await send(this.session.accessToken);
237
+ if (result.status === 'unauthorized') {
238
+ this.session = await refreshStoredSession(this.env, this.session);
239
+ result = await send(this.session.accessToken);
240
+ }
241
+ if (result.status === 'ok')
242
+ return result.data;
243
+ throw authRequiredError(`The session for ${this.env.host} could not authorize the routes call.`, `Run "ekanos login --host ${this.env.host}" and retry.`);
244
+ }
245
+ }
246
+ function parseSubcommand(value) {
247
+ const match = ROUTES_SUBCOMMANDS.find((name) => name === value);
248
+ if (match)
249
+ return match;
250
+ throw usageError(value
251
+ ? `Unknown routes subcommand "${value}".`
252
+ : 'routes needs a subcommand.', USAGE);
253
+ }
254
+ function requireSlug(value, what = 'product') {
255
+ var _a;
256
+ const slug = (_a = value === null || value === void 0 ? void 0 : value.trim().toLowerCase()) !== null && _a !== void 0 ? _a : '';
257
+ if (/^[a-z0-9][a-z0-9-]{0,63}$/.test(slug))
258
+ return slug;
259
+ throw usageError(`"${value !== null && value !== void 0 ? value : ''}" is not a ${what} slug.`, what === 'team'
260
+ ? 'Run "ekanos teams list" to see the teams on this Source.'
261
+ : 'Run "ekanos catalog" to see what the Source has.');
262
+ }
263
+ function requireValue(value) {
264
+ var _a;
265
+ const trimmed = (_a = value === null || value === void 0 ? void 0 : value.trim()) !== null && _a !== void 0 ? _a : '';
266
+ if (trimmed.length > 0 && trimmed.length <= 200)
267
+ return trimmed;
268
+ throw usageError('A route value is 1 to 200 characters.', '"ekanos routes options <product>" lists the values the integration knows.');
269
+ }
270
+ function requireEventId(value) {
271
+ var _a;
272
+ const id = (_a = value === null || value === void 0 ? void 0 : value.trim()) !== null && _a !== void 0 ? _a : '';
273
+ if (/^[0-9a-f-]{36}$/i.test(id))
274
+ return id;
275
+ throw usageError(`"${value !== null && value !== void 0 ? value : ''}" is not an event id.`, '"ekanos routes unrouted" lists the events waiting for a mapping.');
276
+ }
277
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/commands/routes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { z } from 'zod';\n\nimport type { StoredSession } from '../auth/credential-store';\nimport {\n type AuthEnvironment,\n refreshStoredSession,\n requireSession,\n resolveAuthEnvironment,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport { authRequiredError, usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport {\n ReplaySchema,\n RouteListSchema,\n RouteOptionsSchema,\n RouteRemoveSchema,\n RouteSetSchema,\n RouteWebhookSchema,\n type RoutesApiResult,\n type SourceEventRoute,\n UnroutedListSchema,\n routesCall,\n} from '../routes-api';\nimport { resolveSourceTarget } from '../source-target';\n\nexport const ROUTES_SUBCOMMANDS = [\n 'list',\n 'options',\n 'set',\n 'remove',\n 'webhook',\n 'unrouted',\n 'replay',\n] as const;\ntype RoutesSubcommand = (typeof ROUTES_SUBCOMMANDS)[number];\n\nexport interface RoutesArgs {\n host?: string;\n source?: string;\n /** `list [product]`, `set <product> <value> <team>`, `replay <event-id>`, … */\n positionals: readonly string[];\n label?: string;\n webhook?: string;\n dir: string;\n env: Record<string, string | undefined>;\n}\n\nconst USAGE =\n 'Run one of: ekanos routes list [<product>] | options <product> | set <product> <value> <team> [--label <name>] | remove <product> <value> | webhook <product> [--webhook <id>] | unrouted [<product>] | replay <event-id>.';\n\n/**\n * `routes` — a Source admin's routing of a Source connection's events to\n * teams (plan 241.4). One Zendesk connected for the whole Source sends every\n * ticket to one webhook; each ticket's organization decides which team's\n * agents see it. `set` maps a value to a team, `remove` unmaps it, `options`\n * lists the values the integration knows (Zendesk's organizations),\n * `webhook` mints the Source's endpoint URL (shown once), and `unrouted` /\n * `replay` deal with events no team was mapped to — they started nothing.\n */\nexport async function runRoutes(\n ctx: CliContext,\n args: RoutesArgs,\n): Promise<ExitCode> {\n const [rawSubcommand, ...operands] = args.positionals;\n const subcommand = parseSubcommand(rawSubcommand);\n const expected = OPERANDS[subcommand];\n if (operands.length < expected.min || operands.length > expected.max) {\n throw usageError(\n `routes ${subcommand} takes ${expected.usage}.`,\n `e.g. \"ekanos routes ${subcommand} ${expected.example}\".`,\n );\n }\n\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const resolved = await resolveSourceTarget(\n env,\n requireSession(env.store, env.host),\n args,\n );\n const client = new RoutesClient(env, resolved.session);\n const source = resolved.source;\n const send = <T>(\n schema: z.ZodType<T>,\n init: Parameters<typeof routesCall>[3],\n ) =>\n client.call((token) =>\n routesCall(env.host, token, schema, {\n ...init,\n query: { source, ...init.query },\n }),\n );\n\n switch (subcommand) {\n case 'list': {\n const product = operands[0] ? requireSlug(operands[0]) : undefined;\n const list = await send(RouteListSchema, {\n method: 'GET',\n query: { product },\n });\n if (list.routes.length === 0) {\n return ctx.succeed(\n { host: env.host, ...list },\n ctx.ui.result({\n title: `No routes on ${list.source.slug}${product ? ` for ${product}` : ''}`,\n tone: 'warn',\n notes: [\n \"Events on the Source's endpoint stay unrouted — they start nothing — until their value is mapped to a team.\",\n 'Map one: \"ekanos routes set <product> <value> <team>\".',\n ],\n }),\n );\n }\n return ctx.succeed(\n { host: env.host, ...list },\n [\n ctx.ui.bold(`Routes on ${list.source.slug}`),\n ctx.ui.table(\n ['Product', 'Key', 'Value', 'Label', 'Team'],\n list.routes.map(routeRow),\n ),\n ].join('\\n'),\n );\n }\n\n case 'options': {\n const product = requireSlug(operands[0]);\n const result = await send(RouteOptionsSchema, {\n method: 'GET',\n path: `/${encodeURIComponent(product)}/options`,\n query: {},\n });\n if (!result.options) {\n return ctx.succeed(\n { host: env.host, product, ...result },\n ctx.ui.result({\n title: `${product} does not list its ${result.key} values`,\n tone: 'warn',\n notes: [\n `Type the ${result.key} id: \"ekanos routes set ${product} <value> <team>\".`,\n ],\n }),\n );\n }\n return ctx.succeed(\n { host: env.host, product, ...result },\n [\n ctx.ui.bold(`${product} ${result.key} values`),\n ctx.ui.table(\n ['Value', 'Label'],\n result.options.map((option) => [option.value, option.label]),\n ),\n ].join('\\n'),\n );\n }\n\n case 'set': {\n const product = requireSlug(operands[0]);\n const value = requireValue(operands[1]);\n const team = requireSlug(operands[2], 'team');\n const result = await send(RouteSetSchema, {\n method: 'PUT',\n path: `/${encodeURIComponent(product)}`,\n query: {},\n body: { value, team, ...(args.label ? { label: args.label } : {}) },\n });\n return ctx.succeed(\n { host: env.host, ...result },\n ctx.ui.result({\n title: `Routed ${product} ${result.route.key} ${value} to ${result.route.team.slug ?? result.route.team.name}`,\n rows: [\n ['Source', source],\n ['Label', result.route.label ?? '—'],\n [\n 'Replaced',\n result.replaced ? 'yes (moved from another team)' : 'no',\n ],\n ],\n notes: [\n 'New events with this value start that team\\'s agents. Earlier unrouted ones: \"ekanos routes unrouted\" then \"ekanos routes replay <event-id>\".',\n ],\n }),\n );\n }\n\n case 'remove': {\n const product = requireSlug(operands[0]);\n const value = requireValue(operands[1]);\n const result = await send(RouteRemoveSchema, {\n method: 'DELETE',\n path: `/${encodeURIComponent(product)}`,\n query: { value },\n });\n return ctx.succeed(\n { host: env.host, ...result },\n ctx.ui.result({\n title: `Unmapped ${product} ${value}`,\n rows: [['Source', source]],\n notes: ['Its events now stay unrouted and start nothing.'],\n }),\n );\n }\n\n case 'webhook': {\n const product = requireSlug(operands[0]);\n const result = await send(RouteWebhookSchema, {\n method: 'POST',\n path: `/${encodeURIComponent(product)}/webhook`,\n query: { webhook: args.webhook },\n });\n return ctx.succeed(\n { host: env.host, product, ...result },\n ctx.ui.result({\n title: `Webhook URL for ${product} on ${source} (shown once)`,\n rows: [\n ['Webhook', result.webhookId],\n ['URL', result.url],\n ],\n notes: [\n 'Point the vendor at this URL once, for the whole Source. Minting again revokes the previous URL.',\n ...(result.signature\n ? [\n `Deliveries must be signed (${result.signature.header}); the Source connection needs the \"${result.signature.secretName}\" secret.`,\n ]\n : []),\n ],\n }),\n );\n }\n\n case 'unrouted': {\n const product = operands[0] ? requireSlug(operands[0]) : undefined;\n const result = await send(UnroutedListSchema, {\n method: 'GET',\n path: '/unrouted',\n query: { product },\n });\n if (result.events.length === 0) {\n return ctx.succeed(\n { host: env.host, ...result },\n ctx.ui.result({ title: 'No unrouted events', tone: 'ok' }),\n );\n }\n return ctx.succeed(\n { host: env.host, ...result },\n [\n ctx.ui.bold('Unrouted events (no team mapped; nothing started)'),\n ctx.ui.table(\n ['Id', 'Product', 'Event', 'Value', 'Received'],\n result.events.map((event) => [\n event.id,\n event.product,\n event.eventId,\n event.routeValue\n ? `${event.routeKey ?? ''} ${event.routeValue}`.trim()\n : '(none)',\n event.createdAt,\n ]),\n ),\n 'Map the value with \"ekanos routes set\", then \"ekanos routes replay <id>\".',\n ].join('\\n'),\n );\n }\n\n case 'replay': {\n const id = requireEventId(operands[0]);\n const result = await send(ReplaySchema, {\n method: 'POST',\n path: `/unrouted/${encodeURIComponent(id)}/replay`,\n query: {},\n });\n return ctx.succeed(\n { host: env.host, id, ...result },\n ctx.ui.result({\n title:\n result.status === 'routed'\n ? `Routed ${id}`\n : result.status === 'already-routed'\n ? `${id} was already routed`\n : `${id} is still unrouted`,\n tone: result.status === 'unrouted' ? 'warn' : 'ok',\n notes:\n result.status === 'unrouted'\n ? [\n 'No team is mapped to its value yet: \"ekanos routes set\" first.',\n ]\n : [],\n }),\n );\n }\n }\n}\n\nconst OPERANDS: Record<\n RoutesSubcommand,\n { min: number; max: number; usage: string; example: string }\n> = {\n list: { min: 0, max: 1, usage: 'an optional product', example: 'zendesk' },\n options: { min: 1, max: 1, usage: 'a product', example: 'zendesk' },\n set: {\n min: 3,\n max: 3,\n usage: '<product> <value> <team>',\n example: 'zendesk 360001234567 first-bank',\n },\n remove: {\n min: 2,\n max: 2,\n usage: '<product> <value>',\n example: 'zendesk 360001234567',\n },\n webhook: { min: 1, max: 1, usage: 'a product', example: 'zendesk' },\n unrouted: {\n min: 0,\n max: 1,\n usage: 'an optional product',\n example: 'zendesk',\n },\n replay: {\n min: 1,\n max: 1,\n usage: 'an event id from \"ekanos routes unrouted\"',\n example: '<event-id>',\n },\n};\n\nfunction routeRow(route: SourceEventRoute): string[] {\n return [\n route.product,\n route.key,\n route.value,\n route.label ?? '—',\n route.team.slug ?? route.team.name,\n ];\n}\n\nclass RoutesClient {\n constructor(\n private readonly env: AuthEnvironment,\n private session: StoredSession,\n ) {}\n\n async call<T>(\n send: (accessToken: string) => Promise<RoutesApiResult<T>>,\n ): Promise<T> {\n let result = await send(this.session.accessToken);\n if (result.status === 'unauthorized') {\n this.session = await refreshStoredSession(this.env, this.session);\n result = await send(this.session.accessToken);\n }\n if (result.status === 'ok') return result.data;\n throw authRequiredError(\n `The session for ${this.env.host} could not authorize the routes call.`,\n `Run \"ekanos login --host ${this.env.host}\" and retry.`,\n );\n }\n}\n\nfunction parseSubcommand(value: string | undefined): RoutesSubcommand {\n const match = ROUTES_SUBCOMMANDS.find((name) => name === value);\n if (match) return match;\n throw usageError(\n value\n ? `Unknown routes subcommand \"${value}\".`\n : 'routes needs a subcommand.',\n USAGE,\n );\n}\n\nfunction requireSlug(value: string | undefined, what = 'product'): string {\n const slug = value?.trim().toLowerCase() ?? '';\n if (/^[a-z0-9][a-z0-9-]{0,63}$/.test(slug)) return slug;\n throw usageError(\n `\"${value ?? ''}\" is not a ${what} slug.`,\n what === 'team'\n ? 'Run \"ekanos teams list\" to see the teams on this Source.'\n : 'Run \"ekanos catalog\" to see what the Source has.',\n );\n}\n\nfunction requireValue(value: string | undefined): string {\n const trimmed = value?.trim() ?? '';\n if (trimmed.length > 0 && trimmed.length <= 200) return trimmed;\n throw usageError(\n 'A route value is 1 to 200 characters.',\n '\"ekanos routes options <product>\" lists the values the integration knows.',\n );\n}\n\nfunction requireEventId(value: string | undefined): string {\n const id = value?.trim() ?? '';\n if (/^[0-9a-f-]{36}$/i.test(id)) return id;\n throw usageError(\n `\"${value ?? ''}\" is not an event id.`,\n '\"ekanos routes unrouted\" lists the events waiting for a mapping.',\n );\n}\n"]}
@@ -2,6 +2,10 @@ import type { CliContext } from '../context.js';
2
2
  import type { ExitCode } from '../exit-codes.js';
3
3
  export interface SourcesArgs {
4
4
  host?: string;
5
+ /** `products [enable|disable <product>]`; none lists the sources. */
6
+ positionals?: readonly string[];
7
+ source?: string;
8
+ yes?: boolean;
5
9
  /** Where a project's ekanos.json may sit — its `source` field, if any, is the marker. */
6
10
  dir: string;
7
11
  env: Record<string, string | undefined>;
@@ -1,8 +1,11 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import { requireSession, resolveAuthEnvironment } from '../auth/session.js';
3
+ import { refreshStoredSession, requireSession, resolveAuthEnvironment, } from '../auth/session.js';
4
+ import { authRequiredError, invalidStateError, usageError } from '../errors.js';
4
5
  import { EKANOS_CONFIG_FILENAME } from '../project.js';
5
6
  import { fetchSeatsWithOneRefresh } from '../seats.js';
7
+ import { listSourceProducts, setSourceProduct, } from '../source-products-api.js';
8
+ import { resolveSourceTarget } from '../source-target.js';
6
9
  import { stillUnauthorizedSourceSeats } from '../sources-api.js';
7
10
  import { pluralize, shortHost } from '../ui.js';
8
11
  /**
@@ -14,6 +17,14 @@ import { pluralize, shortHost } from '../ui.js';
14
17
  * publish to?" before running `ekanos use` or passing `--source`.
15
18
  */
16
19
  export async function runSources(ctx, args) {
20
+ var _a;
21
+ const [subcommand, ...rest] = (_a = args.positionals) !== null && _a !== void 0 ? _a : [];
22
+ if (subcommand !== undefined) {
23
+ if (subcommand !== 'products') {
24
+ throw usageError(`Unknown sources subcommand "${subcommand}".`, 'Run "ekanos sources" to list your sources, or "ekanos sources products [enable|disable <product>]".');
25
+ }
26
+ return runProducts(ctx, args, rest);
27
+ }
17
28
  const env = resolveAuthEnvironment(ctx, args.host, args.env, {
18
29
  projectDir: args.dir,
19
30
  });
@@ -82,4 +93,107 @@ function humanSummary(ui, host, sources, activeSource) {
82
93
  : [],
83
94
  });
84
95
  }
96
+ /**
97
+ * `sources products` — the catalog as one Source has it: every product, its
98
+ * kind (Experience or Integration), and whether it is enabled for the Source.
99
+ * `enable|disable <product>` switch one through the levers `/admin` uses
100
+ * (`attachProductToSource` / `detachProductFromSource` for an Integration,
101
+ * the Experience lever for an Experience). That is a super-admin grant: the
102
+ * host refuses a Source admin, and an `ekanos login` session never carries
103
+ * super-admin, so from the terminal the write answers with where to do it.
104
+ */
105
+ async function runProducts(ctx, args, rest) {
106
+ const [verb, rawProduct, ...extra] = rest;
107
+ if (extra.length > 0) {
108
+ throw usageError(`Unexpected argument "${extra[0]}".`, 'Run "ekanos sources products [enable|disable <product>] [--source <slug>]".');
109
+ }
110
+ if (verb !== undefined && verb !== 'enable' && verb !== 'disable') {
111
+ throw usageError(`Unknown sources products subcommand "${verb}".`, 'Run "ekanos sources products", "ekanos sources products enable <product>" or "ekanos sources products disable <product> --yes".');
112
+ }
113
+ const product = verb ? productSlug(rawProduct, verb) : null;
114
+ if (verb === 'disable' && !args.yes) {
115
+ throw invalidStateError(`Disabling ${product} removes it from the Source: its plans, widget settings, Source credentials and workspaces go with it.`, `Re-run with --yes: ekanos sources products disable ${product} --yes`);
116
+ }
117
+ const env = resolveAuthEnvironment(ctx, args.host, args.env, {
118
+ projectDir: args.dir,
119
+ });
120
+ const resolved = await resolveSourceTarget(env, requireSession(env.store, env.host), { source: args.source, dir: args.dir });
121
+ const client = new ProductsClient(env, resolved.session);
122
+ if (!verb || !product) {
123
+ const list = await client.call((token) => listSourceProducts(env.host, token, resolved.source));
124
+ const byKind = (kind) => list.products
125
+ .filter((entry) => entry.kind === kind)
126
+ .map((entry) => [
127
+ entry.slug,
128
+ `${entry.enabled ? '✓ enabled' : '✗ off'} · ${entry.name}${entry.registered ? '' : ' · no package on this host'}`,
129
+ ]);
130
+ return ctx.succeed(Object.assign({ host: env.host }, list), [
131
+ ctx.ui.result({
132
+ title: `Experiences on ${list.source.slug}`,
133
+ rows: byKind('experience'),
134
+ }),
135
+ ctx.ui.result({
136
+ title: `Integrations on ${list.source.slug}`,
137
+ rows: byKind('integration'),
138
+ notes: [
139
+ list.canEnable
140
+ ? '"ekanos sources products enable|disable <product>" switches one.'
141
+ : 'Enabling one is a super-admin grant, made in the browser at /admin/sources → the Source → Products.',
142
+ '"ekanos teams connect <team> <product>" connects an enabled integration for a team.',
143
+ ],
144
+ }),
145
+ ].join('\n'));
146
+ }
147
+ const enabled = verb === 'enable';
148
+ const result = await client.call((token) => setSourceProduct(env.host, token, {
149
+ source: resolved.source,
150
+ product,
151
+ enabled,
152
+ }));
153
+ return ctx.succeed(Object.assign({ host: env.host }, result), ctx.ui.result({
154
+ title: result.changed
155
+ ? `${enabled ? 'Enabled' : 'Disabled'} ${result.product.name} for ${result.source.slug}`
156
+ : `${result.product.name} was already ${enabled ? 'enabled' : 'off'} for ${result.source.slug}`,
157
+ rows: [
158
+ ['Kind', result.product.kind],
159
+ [
160
+ 'Switched by',
161
+ result.product.lever === 'flag'
162
+ ? 'its Flag'
163
+ : result.product.lever === 'plan'
164
+ ? 'its plan'
165
+ : 'the entitlement',
166
+ ],
167
+ ],
168
+ notes: enabled && result.product.kind === 'integration'
169
+ ? [
170
+ `Next: "ekanos teams connect <team> ${result.product.slug}" connects it for a team.`,
171
+ ]
172
+ : [],
173
+ }));
174
+ }
175
+ function productSlug(value, verb) {
176
+ const slug = value === null || value === void 0 ? void 0 : value.trim().toLowerCase();
177
+ if (slug && /^[a-z0-9][a-z0-9-]*$/.test(slug))
178
+ return slug;
179
+ throw usageError(value
180
+ ? `"${value}" is not a product slug.`
181
+ : `sources products ${verb} needs a product.`, '"ekanos sources products" lists them, e.g. "ekanos sources products enable agents".');
182
+ }
183
+ class ProductsClient {
184
+ constructor(env, session) {
185
+ this.env = env;
186
+ this.session = session;
187
+ }
188
+ async call(send) {
189
+ let result = await send(this.session.accessToken);
190
+ if (result.status === 'unauthorized') {
191
+ this.session = await refreshStoredSession(this.env, this.session);
192
+ result = await send(this.session.accessToken);
193
+ }
194
+ if (result.status === 'ok')
195
+ return result.data;
196
+ throw authRequiredError(`The session for ${this.env.host} could not authorize the sources call.`, `Run "ekanos login --host ${this.env.host}" and retry.`);
197
+ }
198
+ }
85
199
  //# sourceMappingURL=sources.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sources.js","sourceRoot":"","sources":["../../src/commands/sources.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { requireSession, resolveAuthEnvironment } from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\nimport { EKANOS_CONFIG_FILENAME } from '../project';\nimport { fetchSeatsWithOneRefresh } from '../seats';\nimport { type SourceSeat, stillUnauthorizedSourceSeats } from '../sources-api';\nimport { type Ui, pluralize, shortHost } from '../ui';\n\nexport interface SourcesArgs {\n host?: string;\n /** Where a project's ekanos.json may sit — its `source` field, if any, is the marker. */\n dir: string;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `sources` — list every source the caller holds a developer or admin seat\n * on, the read half of the publish-target-verification story.\n *\n * The list itself is the mechanism that makes `publish` safe: a partner (or\n * an agent acting for one) can always answer \"which sources can I possibly\n * publish to?\" before running `ekanos use` or passing `--source`.\n */\nexport async function runSources(\n ctx: CliContext,\n args: SourcesArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const session = requireSession(env.store, env.host);\n\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'unauthorized') {\n stillUnauthorizedSourceSeats(env.host);\n }\n\n const activeSource = readActiveSource(args.dir);\n\n return ctx.succeed(\n { host: env.host, sources: result.sources, activeSource },\n humanSummary(ctx.ui, env.host, result.sources, activeSource),\n );\n}\n\n/**\n * Best-effort, lenient read of ekanos.json's `source` field — mirrors\n * `readProjectHost` in `auth/session.ts`. A missing, unparseable, or\n * sourceless ekanos.json (or no project at all) simply means \"no active\n * source to mark\", never an error: `sources` must answer outside a project\n * too.\n */\nexport function readActiveSource(dir: string): string | null {\n const configPath = path.join(dir, EKANOS_CONFIG_FILENAME);\n\n let raw: string;\n try {\n raw = fs.readFileSync(configPath, 'utf8');\n } catch {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const source =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { source?: unknown }).source\n : undefined;\n\n return typeof source === 'string' && source.trim().length > 0\n ? source.trim()\n : null;\n}\n\nfunction humanSummary(\n ui: Ui,\n host: string,\n sources: SourceSeat[],\n activeSource: string | null,\n): string {\n if (sources.length === 0) {\n return ui.result({\n title: 'No sources',\n tone: 'warn',\n rows: [['Host', shortHost(host)]],\n notes: ['This account holds no developer seats on this host.'],\n });\n }\n\n const orphaned =\n activeSource && !sources.some((s) => s.slug === activeSource);\n\n return ui.result({\n title: `${pluralize(sources.length, 'source')} on ${shortHost(host)}`,\n rows: sources.map((source): [string, string] => [\n source.slug,\n [\n source.name ?? '(unnamed)',\n source.roles.join(', '),\n ...(source.slug === activeSource ? [ui.accent('this project')] : []),\n ].join(' · '),\n ]),\n notes: orphaned\n ? [\n `This project's ekanos.json points at \"${activeSource}\", which is not in the list above.`,\n ]\n : [],\n });\n}\n"]}
1
+ {"version":3,"file":"sources.js","sourceRoot":"","sources":["../../src/commands/sources.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { StoredSession } from '../auth/credential-store';\nimport {\n type AuthEnvironment,\n refreshStoredSession,\n requireSession,\n resolveAuthEnvironment,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport { authRequiredError, invalidStateError, usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { EKANOS_CONFIG_FILENAME } from '../project';\nimport { fetchSeatsWithOneRefresh } from '../seats';\nimport {\n type SourceProduct,\n type SourceProductsApiResult,\n listSourceProducts,\n setSourceProduct,\n} from '../source-products-api';\nimport { resolveSourceTarget } from '../source-target';\nimport { type SourceSeat, stillUnauthorizedSourceSeats } from '../sources-api';\nimport { type Ui, pluralize, shortHost } from '../ui';\n\nexport interface SourcesArgs {\n host?: string;\n /** `products [enable|disable <product>]`; none lists the sources. */\n positionals?: readonly string[];\n source?: string;\n yes?: boolean;\n /** Where a project's ekanos.json may sit — its `source` field, if any, is the marker. */\n dir: string;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `sources` — list every source the caller holds a developer or admin seat\n * on, the read half of the publish-target-verification story.\n *\n * The list itself is the mechanism that makes `publish` safe: a partner (or\n * an agent acting for one) can always answer \"which sources can I possibly\n * publish to?\" before running `ekanos use` or passing `--source`.\n */\nexport async function runSources(\n ctx: CliContext,\n args: SourcesArgs,\n): Promise<ExitCode> {\n const [subcommand, ...rest] = args.positionals ?? [];\n if (subcommand !== undefined) {\n if (subcommand !== 'products') {\n throw usageError(\n `Unknown sources subcommand \"${subcommand}\".`,\n 'Run \"ekanos sources\" to list your sources, or \"ekanos sources products [enable|disable <product>]\".',\n );\n }\n return runProducts(ctx, args, rest);\n }\n\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const session = requireSession(env.store, env.host);\n\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'unauthorized') {\n stillUnauthorizedSourceSeats(env.host);\n }\n\n const activeSource = readActiveSource(args.dir);\n\n return ctx.succeed(\n { host: env.host, sources: result.sources, activeSource },\n humanSummary(ctx.ui, env.host, result.sources, activeSource),\n );\n}\n\n/**\n * Best-effort, lenient read of ekanos.json's `source` field — mirrors\n * `readProjectHost` in `auth/session.ts`. A missing, unparseable, or\n * sourceless ekanos.json (or no project at all) simply means \"no active\n * source to mark\", never an error: `sources` must answer outside a project\n * too.\n */\nexport function readActiveSource(dir: string): string | null {\n const configPath = path.join(dir, EKANOS_CONFIG_FILENAME);\n\n let raw: string;\n try {\n raw = fs.readFileSync(configPath, 'utf8');\n } catch {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const source =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { source?: unknown }).source\n : undefined;\n\n return typeof source === 'string' && source.trim().length > 0\n ? source.trim()\n : null;\n}\n\nfunction humanSummary(\n ui: Ui,\n host: string,\n sources: SourceSeat[],\n activeSource: string | null,\n): string {\n if (sources.length === 0) {\n return ui.result({\n title: 'No sources',\n tone: 'warn',\n rows: [['Host', shortHost(host)]],\n notes: ['This account holds no developer seats on this host.'],\n });\n }\n\n const orphaned =\n activeSource && !sources.some((s) => s.slug === activeSource);\n\n return ui.result({\n title: `${pluralize(sources.length, 'source')} on ${shortHost(host)}`,\n rows: sources.map((source): [string, string] => [\n source.slug,\n [\n source.name ?? '(unnamed)',\n source.roles.join(', '),\n ...(source.slug === activeSource ? [ui.accent('this project')] : []),\n ].join(' · '),\n ]),\n notes: orphaned\n ? [\n `This project's ekanos.json points at \"${activeSource}\", which is not in the list above.`,\n ]\n : [],\n });\n}\n\n/**\n * `sources products` — the catalog as one Source has it: every product, its\n * kind (Experience or Integration), and whether it is enabled for the Source.\n * `enable|disable <product>` switch one through the levers `/admin` uses\n * (`attachProductToSource` / `detachProductFromSource` for an Integration,\n * the Experience lever for an Experience). That is a super-admin grant: the\n * host refuses a Source admin, and an `ekanos login` session never carries\n * super-admin, so from the terminal the write answers with where to do it.\n */\nasync function runProducts(\n ctx: CliContext,\n args: SourcesArgs,\n rest: readonly string[],\n): Promise<ExitCode> {\n const [verb, rawProduct, ...extra] = rest;\n if (extra.length > 0) {\n throw usageError(\n `Unexpected argument \"${extra[0]}\".`,\n 'Run \"ekanos sources products [enable|disable <product>] [--source <slug>]\".',\n );\n }\n if (verb !== undefined && verb !== 'enable' && verb !== 'disable') {\n throw usageError(\n `Unknown sources products subcommand \"${verb}\".`,\n 'Run \"ekanos sources products\", \"ekanos sources products enable <product>\" or \"ekanos sources products disable <product> --yes\".',\n );\n }\n const product = verb ? productSlug(rawProduct, verb) : null;\n if (verb === 'disable' && !args.yes) {\n throw invalidStateError(\n `Disabling ${product} removes it from the Source: its plans, widget settings, Source credentials and workspaces go with it.`,\n `Re-run with --yes: ekanos sources products disable ${product} --yes`,\n );\n }\n\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const resolved = await resolveSourceTarget(\n env,\n requireSession(env.store, env.host),\n { source: args.source, dir: args.dir },\n );\n const client = new ProductsClient(env, resolved.session);\n\n if (!verb || !product) {\n const list = await client.call((token) =>\n listSourceProducts(env.host, token, resolved.source),\n );\n const byKind = (kind: SourceProduct['kind']) =>\n list.products\n .filter((entry) => entry.kind === kind)\n .map((entry): [string, string] => [\n entry.slug,\n `${entry.enabled ? '✓ enabled' : '✗ off'} · ${entry.name}${entry.registered ? '' : ' · no package on this host'}`,\n ]);\n return ctx.succeed(\n { host: env.host, ...list },\n [\n ctx.ui.result({\n title: `Experiences on ${list.source.slug}`,\n rows: byKind('experience'),\n }),\n ctx.ui.result({\n title: `Integrations on ${list.source.slug}`,\n rows: byKind('integration'),\n notes: [\n list.canEnable\n ? '\"ekanos sources products enable|disable <product>\" switches one.'\n : 'Enabling one is a super-admin grant, made in the browser at /admin/sources → the Source → Products.',\n '\"ekanos teams connect <team> <product>\" connects an enabled integration for a team.',\n ],\n }),\n ].join('\\n'),\n );\n }\n\n const enabled = verb === 'enable';\n const result = await client.call((token) =>\n setSourceProduct(env.host, token, {\n source: resolved.source,\n product,\n enabled,\n }),\n );\n return ctx.succeed(\n { host: env.host, ...result },\n ctx.ui.result({\n title: result.changed\n ? `${enabled ? 'Enabled' : 'Disabled'} ${result.product.name} for ${result.source.slug}`\n : `${result.product.name} was already ${enabled ? 'enabled' : 'off'} for ${result.source.slug}`,\n rows: [\n ['Kind', result.product.kind],\n [\n 'Switched by',\n result.product.lever === 'flag'\n ? 'its Flag'\n : result.product.lever === 'plan'\n ? 'its plan'\n : 'the entitlement',\n ],\n ],\n notes:\n enabled && result.product.kind === 'integration'\n ? [\n `Next: \"ekanos teams connect <team> ${result.product.slug}\" connects it for a team.`,\n ]\n : [],\n }),\n );\n}\n\nfunction productSlug(value: string | undefined, verb: string): string {\n const slug = value?.trim().toLowerCase();\n if (slug && /^[a-z0-9][a-z0-9-]*$/.test(slug)) return slug;\n throw usageError(\n value\n ? `\"${value}\" is not a product slug.`\n : `sources products ${verb} needs a product.`,\n '\"ekanos sources products\" lists them, e.g. \"ekanos sources products enable agents\".',\n );\n}\n\nclass ProductsClient {\n constructor(\n private readonly env: AuthEnvironment,\n private session: StoredSession,\n ) {}\n\n async call<T>(\n send: (accessToken: string) => Promise<SourceProductsApiResult<T>>,\n ): Promise<T> {\n let result = await send(this.session.accessToken);\n if (result.status === 'unauthorized') {\n this.session = await refreshStoredSession(this.env, this.session);\n result = await send(this.session.accessToken);\n }\n if (result.status === 'ok') return result.data;\n throw authRequiredError(\n `The session for ${this.env.host} could not authorize the sources call.`,\n `Run \"ekanos login --host ${this.env.host}\" and retry.`,\n );\n }\n}\n"]}
@@ -178,8 +178,9 @@ function triggerBody(run, team, key) {
178
178
  };
179
179
  }
180
180
  case 'slack':
181
+ throw usageError('A slack trigger is connected in the app, where the team picks its workspace and channels.', 'Open the agent in the app and add it under Triggers.');
181
182
  case 'linear':
182
- throw usageError(`A ${type} trigger is connected in the app, where the team picks its workspace and channels.`, 'Open the agent in the app and add it under Triggers.');
183
+ throw usageError('A "linear" trigger is the Ekanos Agents app for Linear (@mentions and assignments only), connected in the app. The Linear integration\'s own events are added here as event triggers.', `e.g. ekanos teams triggers add ${team} ${key} event linear.issue_labeled --filter '{"labels":["feedback"]}' — "ekanos catalog linear --json" lists every agentEvents id.`);
183
184
  default:
184
185
  throw usageError(type ? `Unknown trigger type "${type}".` : 'Name the trigger type.', usageFor('triggers'));
185
186
  }