@ekanos/cli 0.13.0 → 0.14.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 (173) hide show
  1. package/README.md +25 -8
  2. package/dist/agents-api.d.ts +11 -2
  3. package/dist/agents-api.js +39 -3
  4. package/dist/agents-api.js.map +1 -1
  5. package/dist/apps-api.js +2 -2
  6. package/dist/apps-api.js.map +1 -1
  7. package/dist/auth/ekanos-api.d.ts +16 -2
  8. package/dist/auth/ekanos-api.js +101 -15
  9. package/dist/auth/ekanos-api.js.map +1 -1
  10. package/dist/auth/host-trust.d.ts +46 -0
  11. package/dist/auth/host-trust.js +79 -0
  12. package/dist/auth/host-trust.js.map +1 -0
  13. package/dist/auth/session.d.ts +10 -1
  14. package/dist/auth/session.js +28 -28
  15. package/dist/auth/session.js.map +1 -1
  16. package/dist/commands/agents-edit.d.ts +6 -2
  17. package/dist/commands/agents-edit.js +57 -26
  18. package/dist/commands/agents-edit.js.map +1 -1
  19. package/dist/commands/agents.d.ts +13 -2
  20. package/dist/commands/agents.js +148 -36
  21. package/dist/commands/agents.js.map +1 -1
  22. package/dist/commands/apps.d.ts +2 -0
  23. package/dist/commands/apps.js +16 -12
  24. package/dist/commands/apps.js.map +1 -1
  25. package/dist/commands/catalog.js +7 -6
  26. package/dist/commands/catalog.js.map +1 -1
  27. package/dist/commands/knowledge.d.ts +19 -0
  28. package/dist/commands/knowledge.js +119 -27
  29. package/dist/commands/knowledge.js.map +1 -1
  30. package/dist/commands/login.d.ts +4 -0
  31. package/dist/commands/login.js +30 -2
  32. package/dist/commands/login.js.map +1 -1
  33. package/dist/commands/logout.d.ts +9 -1
  34. package/dist/commands/logout.js +56 -7
  35. package/dist/commands/logout.js.map +1 -1
  36. package/dist/commands/publish.js +3 -3
  37. package/dist/commands/publish.js.map +1 -1
  38. package/dist/commands/reviewers.d.ts +2 -0
  39. package/dist/commands/reviewers.js +7 -3
  40. package/dist/commands/reviewers.js.map +1 -1
  41. package/dist/commands/routes.d.ts +2 -0
  42. package/dist/commands/routes.js +42 -9
  43. package/dist/commands/routes.js.map +1 -1
  44. package/dist/commands/runs.d.ts +13 -2
  45. package/dist/commands/runs.js +96 -15
  46. package/dist/commands/runs.js.map +1 -1
  47. package/dist/commands/scope-notice.d.ts +21 -0
  48. package/dist/commands/scope-notice.js +35 -0
  49. package/dist/commands/scope-notice.js.map +1 -0
  50. package/dist/commands/skills.d.ts +7 -1
  51. package/dist/commands/skills.js +80 -24
  52. package/dist/commands/skills.js.map +1 -1
  53. package/dist/commands/sources.d.ts +1 -1
  54. package/dist/commands/sources.js +126 -12
  55. package/dist/commands/sources.js.map +1 -1
  56. package/dist/commands/status.js +5 -4
  57. package/dist/commands/status.js.map +1 -1
  58. package/dist/commands/team-agent-readiness.d.ts +59 -0
  59. package/dist/commands/team-agent-readiness.js +341 -0
  60. package/dist/commands/team-agent-readiness.js.map +1 -0
  61. package/dist/commands/team-agents-agents.d.ts +24 -3
  62. package/dist/commands/team-agents-agents.js +336 -35
  63. package/dist/commands/team-agents-agents.js.map +1 -1
  64. package/dist/commands/team-agents-mcp.js +32 -3
  65. package/dist/commands/team-agents-mcp.js.map +1 -1
  66. package/dist/commands/team-agents-triggers.js +230 -55
  67. package/dist/commands/team-agents-triggers.js.map +1 -1
  68. package/dist/commands/team-agents.d.ts +3 -1
  69. package/dist/commands/team-agents.js +27 -7
  70. package/dist/commands/team-agents.js.map +1 -1
  71. package/dist/commands/teams-channel-install.d.ts +24 -0
  72. package/dist/commands/teams-channel-install.js +94 -0
  73. package/dist/commands/teams-channel-install.js.map +1 -0
  74. package/dist/commands/teams-connect.d.ts +41 -0
  75. package/dist/commands/teams-connect.js +212 -57
  76. package/dist/commands/teams-connect.js.map +1 -1
  77. package/dist/commands/teams-events.d.ts +12 -0
  78. package/dist/commands/teams-events.js +138 -0
  79. package/dist/commands/teams-events.js.map +1 -0
  80. package/dist/commands/teams-onboard.d.ts +11 -3
  81. package/dist/commands/teams-onboard.js +60 -7
  82. package/dist/commands/teams-onboard.js.map +1 -1
  83. package/dist/commands/teams-overview.d.ts +11 -0
  84. package/dist/commands/teams-overview.js +160 -0
  85. package/dist/commands/teams-overview.js.map +1 -0
  86. package/dist/commands/teams-scope.d.ts +10 -0
  87. package/dist/commands/teams-scope.js +109 -0
  88. package/dist/commands/teams-scope.js.map +1 -0
  89. package/dist/commands/teams.d.ts +4 -2
  90. package/dist/commands/teams.js +113 -14
  91. package/dist/commands/teams.js.map +1 -1
  92. package/dist/commands/upgrade.js +12 -2
  93. package/dist/commands/upgrade.js.map +1 -1
  94. package/dist/commands/use.js +2 -2
  95. package/dist/commands/use.js.map +1 -1
  96. package/dist/commands/whoami.js +1 -1
  97. package/dist/commands/whoami.js.map +1 -1
  98. package/dist/connections-api.js +1 -1
  99. package/dist/connections-api.js.map +1 -1
  100. package/dist/delegate.js +11 -4
  101. package/dist/delegate.js.map +1 -1
  102. package/dist/harness-scaffold.js +12 -0
  103. package/dist/harness-scaffold.js.map +1 -1
  104. package/dist/index.d.ts +6 -0
  105. package/dist/index.js +683 -167
  106. package/dist/index.js.map +1 -1
  107. package/dist/knowledge-api.d.ts +46 -0
  108. package/dist/knowledge-api.js +32 -1
  109. package/dist/knowledge-api.js.map +1 -1
  110. package/dist/open-url.d.ts +16 -0
  111. package/dist/open-url.js +73 -0
  112. package/dist/open-url.js.map +1 -0
  113. package/dist/parse-argv.d.ts +2 -0
  114. package/dist/parse-argv.js.map +1 -1
  115. package/dist/publish-api.d.ts +2 -1
  116. package/dist/publish-api.js +4 -3
  117. package/dist/publish-api.js.map +1 -1
  118. package/dist/pull-write.d.ts +27 -0
  119. package/dist/pull-write.js +60 -0
  120. package/dist/pull-write.js.map +1 -0
  121. package/dist/routes-api.d.ts +3 -0
  122. package/dist/routes-api.js +9 -4
  123. package/dist/routes-api.js.map +1 -1
  124. package/dist/runs-api.d.ts +7 -1
  125. package/dist/runs-api.js +21 -6
  126. package/dist/runs-api.js.map +1 -1
  127. package/dist/safe-fs.d.ts +2 -0
  128. package/dist/safe-fs.js +12 -6
  129. package/dist/safe-fs.js.map +1 -1
  130. package/dist/secret-file.d.ts +17 -0
  131. package/dist/secret-file.js +69 -0
  132. package/dist/secret-file.js.map +1 -0
  133. package/dist/skills-api.d.ts +10 -2
  134. package/dist/skills-api.js +37 -2
  135. package/dist/skills-api.js.map +1 -1
  136. package/dist/source-products-api.d.ts +4 -0
  137. package/dist/source-products-api.js +14 -4
  138. package/dist/source-products-api.js.map +1 -1
  139. package/dist/source-target.js +2 -1
  140. package/dist/source-target.js.map +1 -1
  141. package/dist/sources-api.d.ts +2 -1
  142. package/dist/sources-api.js +3 -2
  143. package/dist/sources-api.js.map +1 -1
  144. package/dist/team-connect-api.d.ts +44 -2
  145. package/dist/team-connect-api.js +19 -7
  146. package/dist/team-connect-api.js.map +1 -1
  147. package/dist/teams-api.d.ts +41 -1
  148. package/dist/teams-api.js +97 -19
  149. package/dist/teams-api.js.map +1 -1
  150. package/dist/toolchain-api.d.ts +9 -0
  151. package/dist/toolchain-api.js +17 -2
  152. package/dist/toolchain-api.js.map +1 -1
  153. package/dist/toolchain-resolve.d.ts +3 -0
  154. package/dist/toolchain-resolve.js +15 -1
  155. package/dist/toolchain-resolve.js.map +1 -1
  156. package/dist/toolchain.d.ts +6 -0
  157. package/dist/toolchain.js +19 -0
  158. package/dist/toolchain.js.map +1 -1
  159. package/dist/update-notice.js +2 -1
  160. package/dist/update-notice.js.map +1 -1
  161. package/package.json +2 -2
  162. package/templates/AGENTS.md.tmpl +6 -1
  163. package/templates/claude-skill.md.tmpl +5 -0
  164. package/templates/package.json.tmpl +4 -4
  165. package/templates/skills/build-agent/SKILL.md +103 -17
  166. package/templates/skills/build-agent/references/agent-json.md +4 -2
  167. package/templates/skills/build-agent/references/rollout.md +27 -4
  168. package/templates/skills/build-agent/references/set-up-an-integration.md +55 -7
  169. package/templates/skills/build-agent/references/skills.md +20 -1
  170. package/templates/skills/build-agent/references/triggers.md +46 -11
  171. package/templates/skills/build-integration/references/widget-design.md +3 -3
  172. package/templates/skills/ekanos-platform/SKILL.md +43 -16
  173. package/templates/skills/polish-widgets/references/patterns.md +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"team-agents.js","sourceRoot":"","sources":["../../src/commands/team-agents.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireSession, resolveAuthEnvironment } from '../auth/session';\nimport type { CliContext } from '../context';\nimport { usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { resolveSourceTarget } from '../source-target';\nimport { runTeamAgentsArea } from './team-agents-agents';\nimport { runTeamMcp } from './team-agents-mcp';\nimport { runTeamLinks, runTeamTriggers } from './team-agents-triggers';\nimport { TeamsClient, type TeamsArgs } from './teams';\n\n/**\n * `ekanos teams agents|triggers|links|mcp …` — a Source admin setting up a\n * team's agents from the terminal, acting on that team through the partner\n * API (`/api/partner/teams/<team>/agents/*`, `/mcp/*`). Every write goes\n * through the same checks the app's agent pages make and leaves an Admin\n * Audit row naming the person.\n *\n * The shape is always `teams <area> <verb> <team> [<agent-key> …]`.\n */\n\nexport const TEAM_AGENT_AREAS = ['agents', 'triggers', 'links', 'mcp'] as const;\nexport type TeamAgentArea = (typeof TEAM_AGENT_AREAS)[number];\n\nexport function isTeamAgentArea(\n value: string | undefined,\n): value is TeamAgentArea {\n return TEAM_AGENT_AREAS.some((area) => area === value);\n}\n\n/** What every area handler gets: an authorized client and its operands. */\nexport interface TeamAgentRun {\n args: TeamsArgs;\n client: TeamsClient;\n host: string;\n /** The Source slug, for `?source=`. */\n source: string;\n /** The verb. */\n verb: string;\n /** Everything after the verb: `<team> [<key> …]`. */\n operands: readonly string[];\n}\n\nexport async function runTeamAgentCommands(\n ctx: CliContext,\n args: TeamsArgs,\n): Promise<ExitCode> {\n const [area, verb, ...operands] = args.positionals;\n if (!isTeamAgentArea(area)) {\n throw usageError(\n `Unknown teams area \"${area ?? ''}\".`,\n 'See \"ekanos teams --help\".',\n );\n }\n if (!verb) {\n throw usageError(`teams ${area} needs a subcommand.`, usageFor(area));\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 run: TeamAgentRun = {\n args,\n client: new TeamsClient(env, resolved.session),\n host: env.host,\n source: resolved.source,\n verb,\n operands,\n };\n\n switch (area) {\n case 'agents':\n return runTeamAgentsArea(ctx, run);\n case 'triggers':\n return runTeamTriggers(ctx, run);\n case 'links':\n return runTeamLinks(ctx, run);\n case 'mcp':\n return runTeamMcp(ctx, run);\n }\n}\n\nexport function usageFor(area: TeamAgentArea): string {\n switch (area) {\n case 'agents':\n return 'Run one of: ekanos teams agents list <team> | enable <team> <key> [--as <member-email>] | disable <team> <key> | pin <team> <key> <version> | unpin <team> <key> | policy <team> <key> [set <tool> auto|approve | unset <tool> | --reads auto|approve --writes auto|approve].';\n case 'triggers':\n return 'Run one of: ekanos teams triggers list <team> <key> | add <team> <key> <manual|schedule|webhook|event> … | remove <team> <key> <trigger-id>.';\n case 'links':\n return 'Run one of: ekanos teams links list <team> <key> | add <team> <from-key> <to-key> --kind talk|delegate [--mode await|handoff] | remove <team> <from-key> <link-id>.';\n case 'mcp':\n return 'Run one of: ekanos teams mcp list <team> | add <team> <server-key> --url <url> [--auth none|token|oauth] [--name <name>] | status <team> <server-key> | token <team> <server-key> (hidden input at your terminal) | effect <team> <server-key> <tool> read|write.';\n }\n}\n\n/** A slug operand (team, agent key, server key), or a usage error naming it. */\nexport function requireOperand(\n value: string | undefined,\n what: string,\n area: TeamAgentArea,\n): 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 ? `\"${value}\" is not a ${what}.` : `Name the ${what}.`,\n usageFor(area),\n );\n}\n"]}
1
+ {"version":3,"file":"team-agents.js","sourceRoot":"","sources":["../../src/commands/team-agents.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireSession, resolveAuthEnvironment } from '../auth/session';\nimport type { CliContext } from '../context';\nimport { usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { resolveSourceTarget } from '../source-target';\nimport { runTeamAgentsArea } from './team-agents-agents';\nimport { runTeamMcp } from './team-agents-mcp';\nimport { runTeamLinks, runTeamTriggers } from './team-agents-triggers';\nimport { TeamsClient, type TeamsArgs } from './teams';\n\n/**\n * `ekanos teams agents|triggers|links|mcp …` — a Source admin setting up a\n * team's agents from the terminal, acting on that team through the partner\n * API (`/api/partner/teams/<team>/agents/*`, `/mcp/*`). Every write goes\n * through the same checks the app's agent pages make and leaves an Admin\n * Audit row naming the person.\n *\n * The shape is always `teams <area> <verb> <team> [<agent-key> …]`; the\n * team may also be named with `--team <slug>`, as `runs`, `approvals`,\n * `skills` and `knowledge` name it.\n */\n\nexport const TEAM_AGENT_AREAS = ['agents', 'triggers', 'links', 'mcp'] as const;\nexport type TeamAgentArea = (typeof TEAM_AGENT_AREAS)[number];\n\nexport function isTeamAgentArea(\n value: string | undefined,\n): value is TeamAgentArea {\n return TEAM_AGENT_AREAS.some((area) => area === value);\n}\n\n/** What every area handler gets: an authorized client and its operands. */\nexport interface TeamAgentRun {\n args: TeamsArgs;\n client: TeamsClient;\n host: string;\n /** The Source slug, for `?source=`. */\n source: string;\n /** The verb. */\n verb: string;\n /** Everything after the verb: `<team> [<key> …]`. */\n operands: readonly string[];\n}\n\nexport async function runTeamAgentCommands(\n ctx: CliContext,\n args: TeamsArgs,\n): Promise<ExitCode> {\n const [area, verb, ...positional] = args.positionals;\n if (!isTeamAgentArea(area)) {\n throw usageError(\n `Unknown teams area \"${area ?? ''}\".`,\n 'See \"ekanos teams --help\".',\n );\n }\n if (!verb) {\n throw usageError(`teams ${area} needs a subcommand.`, usageFor(area));\n }\n const operands = withTeamFlag(positional, args.flags?.team);\n\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const resolved = await resolveSourceTarget(env, requireSession(env), args);\n const run: TeamAgentRun = {\n args,\n client: new TeamsClient(env, resolved.session),\n host: env.host,\n source: resolved.source,\n verb,\n operands,\n };\n\n switch (area) {\n case 'agents':\n return runTeamAgentsArea(ctx, run);\n case 'triggers':\n return runTeamTriggers(ctx, run);\n case 'links':\n return runTeamLinks(ctx, run);\n case 'mcp':\n return runTeamMcp(ctx, run);\n }\n}\n\nexport function usageFor(area: TeamAgentArea): string {\n switch (area) {\n case 'agents':\n return 'Run one of: ekanos teams agents list <team> | show <team> <key> | enable <team> <key> [--as <member-email>] [--draft] [--confirm-access] | confirm <team> <key> | disable <team> <key> --yes | pin <team> <key> <version> | pin <team> <key> --draft | unpin <team> <key> | policy <team> <key> [set <tool> auto|approve | unset <tool> | --reads auto|approve --writes auto|approve] [--confirm-access] | skills <team> <key> [add|remove <skill>].';\n case 'triggers':\n return 'Run one of: ekanos teams triggers list <team> <key> | add <team> <key> <manual|schedule|webhook|event|slack|linear> … | remove <team> <key> <trigger-id|key> --yes | rotate <team> <key> <trigger-key|id> [--yes].';\n case 'links':\n return 'Run one of: ekanos teams links list <team> <key> | add <team> <from-key> <to-key> --kind talk|delegate [--mode await|handoff] | remove <team> <from-key> <link-id|to-key> [--kind talk|delegate] --yes. The team may be --team <slug> instead.';\n case 'mcp':\n return 'Run one of: ekanos teams mcp list <team> | add <team> <server-key> --url <url> [--auth none|token|oauth] [--name <name>] | status <team> <server-key> | token <team> <server-key> (hidden input at your terminal) | effect <team> <server-key> <tool> read|write | remove <team> <server-key> --yes.';\n }\n}\n\n/**\n * `--team <slug>` in place of the leading team operand. Given both, they\n * must agree; given only the flag, it becomes the first operand.\n */\nfunction withTeamFlag(\n operands: readonly string[],\n flag: string | boolean | string[] | undefined,\n): readonly string[] {\n if (flag === undefined) return operands;\n const team = typeof flag === 'string' ? flag.trim().toLowerCase() : '';\n if (!team) {\n throw usageError('--team needs a team slug.', 'e.g. --team bank-a');\n }\n return operands[0]?.trim().toLowerCase() === team\n ? operands\n : [team, ...operands];\n}\n\n/** A slug operand (team, agent key, server key), or a usage error naming it. */\nexport function requireOperand(\n value: string | undefined,\n what: string,\n area: TeamAgentArea,\n): 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 ? `\"${value}\" is not a ${what}.` : `Name the ${what}.`,\n usageFor(area),\n );\n}\n"]}
@@ -0,0 +1,24 @@
1
+ import { type TeamChannel } from '@ekanos/integration-schema';
2
+ import type { CliContext } from '../context.js';
3
+ import type { ExitCode } from '../exit-codes.js';
4
+ import type { TeamsArgs, TeamsClient } from './teams.js';
5
+ /**
6
+ * `teams connect <team> slack|linear-app` — install, for one team, the
7
+ * channel a `slack` or `linear` agent trigger answers through: Slack, or
8
+ * Ekanos's own Linear Agents app (not the team's own Linear app, which is
9
+ * `teams connect <team> linear --connection agent`).
10
+ *
11
+ * The same open-the-link-and-wait as `teams connect`'s sign-in: the host
12
+ * mints a one-time link for the team member who installs it (the caller when
13
+ * a member, else `--as`), the CLI opens it for the caller, prints it always,
14
+ * and polls until it is installed. Only that member, signed in to the app,
15
+ * can finish it.
16
+ */
17
+ export declare function isTeamChannel(value: string): value is TeamChannel;
18
+ export declare function runTeamChannelInstall(ctx: CliContext, args: TeamsArgs, client: TeamsClient, host: string, source: string, input: {
19
+ team: string;
20
+ channel: TeamChannel;
21
+ as?: string;
22
+ /** Opens the link in the caller's browser (`teams connect`'s opener). */
23
+ open: (url: string) => void;
24
+ }): Promise<ExitCode>;
@@ -0,0 +1,94 @@
1
+ import { TeamChannelInstallStartSchema, TeamChannelInstallStatusSchema, TeamChannelSchema, } from '@ekanos/integration-schema';
2
+ import { invalidStateError, validationError } from '../errors.js';
3
+ import { assertBrowserSafeUrl } from '../open-url.js';
4
+ import { call } from '../teams-api.js';
5
+ /**
6
+ * `teams connect <team> slack|linear-app` — install, for one team, the
7
+ * channel a `slack` or `linear` agent trigger answers through: Slack, or
8
+ * Ekanos's own Linear Agents app (not the team's own Linear app, which is
9
+ * `teams connect <team> linear --connection agent`).
10
+ *
11
+ * The same open-the-link-and-wait as `teams connect`'s sign-in: the host
12
+ * mints a one-time link for the team member who installs it (the caller when
13
+ * a member, else `--as`), the CLI opens it for the caller, prints it always,
14
+ * and polls until it is installed. Only that member, signed in to the app,
15
+ * can finish it.
16
+ */
17
+ export function isTeamChannel(value) {
18
+ return TeamChannelSchema.safeParse(value).success;
19
+ }
20
+ export async function runTeamChannelInstall(ctx, args, client, host, source, input) {
21
+ var _a, _b, _c, _d;
22
+ const path = `/api/partner/teams/${encodeURIComponent(input.team)}/channels/${input.channel}`;
23
+ const read = () => client.call((token) => call(host, token, TeamChannelInstallStatusSchema, {
24
+ method: 'GET',
25
+ path,
26
+ source,
27
+ }));
28
+ const status = await read();
29
+ if (status.installed) {
30
+ return ctx.succeed(Object.assign({ host }, status), ctx.ui.result({
31
+ title: `${status.team.slug} has installed ${status.name}`,
32
+ rows: [['Workspace', (_a = status.workspace) !== null && _a !== void 0 ? _a : 'installed']],
33
+ notes: [triggerHint(status.team.slug, input.channel)],
34
+ }));
35
+ }
36
+ if (status.problem) {
37
+ throw invalidStateError(`${status.problem}.`, input.channel === 'slack'
38
+ ? `"ekanos sources products --source ${source}" shows whether Slack is enabled; a super-admin enables it.`
39
+ : 'Ekanos sets the Linear Agents app up for a host; ask Ekanos.');
40
+ }
41
+ const started = await client.call((token) => call(host, token, TeamChannelInstallStartSchema, {
42
+ method: 'POST',
43
+ path: `${path}/install`,
44
+ source,
45
+ body: input.as ? { as: input.as } : {},
46
+ }));
47
+ assertBrowserSafeUrl(started.authorizeUrl, 'install link');
48
+ const { member } = started;
49
+ const expires = started.expiresAt.replace('T', ' ').slice(0, 16);
50
+ const consent = input.channel === 'slack'
51
+ ? 'pick the Slack workspace and approve'
52
+ : 'approve it as an admin of the Linear workspace it goes into';
53
+ ctx.log(member.isCaller
54
+ ? [
55
+ `Install ${status.name} for ${status.team.slug} — sign in to the app as yourself if asked, then ${consent}:`,
56
+ ` ${started.authorizeUrl}`,
57
+ ` (one-time link, valid until ${expires} UTC)`,
58
+ ].join('\n')
59
+ : [
60
+ `Send this link to ${(_b = member.email) !== null && _b !== void 0 ? _b : 'the member'} — they install ${status.name} for ${status.team.slug}, signed in to the app as themselves (${consent}):`,
61
+ ` ${started.authorizeUrl}`,
62
+ ` (one-time, for them only, valid until ${expires} UTC)`,
63
+ ].join('\n'));
64
+ // Only ever the caller's own browser: the link is bound to its member.
65
+ if (member.isCaller && !args.noOpen && !ctx.jsonMode) {
66
+ input.open(started.authorizeUrl);
67
+ }
68
+ if (args.noWait || ctx.jsonMode) {
69
+ return ctx.succeed(Object.assign(Object.assign({ host }, started.status), { authorizeUrl: started.authorizeUrl, expiresAt: started.expiresAt, member }), ctx.ui.hint(`Then check with "ekanos teams connect ${input.team} ${input.channel}".`));
70
+ }
71
+ ctx.log('Waiting for the install to finish (Ctrl-C stops waiting; the link stays valid)…');
72
+ const deadline = Date.parse(started.expiresAt) || Date.now() + 10 * 60 * 1000;
73
+ while (Date.now() < deadline) {
74
+ await new Promise((resolve) => { var _a; return setTimeout(resolve, (_a = args.pollDelayMs) !== null && _a !== void 0 ? _a : 3000); });
75
+ const polled = await read();
76
+ if (polled.installed) {
77
+ return ctx.succeed(Object.assign({ host }, polled), ctx.ui.result({
78
+ title: `Installed ${polled.name} for ${polled.team.slug}`,
79
+ rows: [
80
+ ['Workspace', (_c = polled.workspace) !== null && _c !== void 0 ? _c : 'installed'],
81
+ ['Installed by', (_d = member.email) !== null && _d !== void 0 ? _d : 'the member'],
82
+ ],
83
+ notes: [triggerHint(polled.team.slug, input.channel)],
84
+ }));
85
+ }
86
+ }
87
+ throw validationError('The install did not finish before the link expired.', `Run "ekanos teams connect ${input.team} ${input.channel}${input.as ? ` --as ${input.as}` : ''}" again for a fresh link.`);
88
+ }
89
+ function triggerHint(team, channel) {
90
+ return channel === 'slack'
91
+ ? `Next: an agent answers there once it has a slack trigger — "ekanos teams triggers add ${team} <agent-key> slack --channel '#<channel>' --yes" (invite the bot to the channel first).`
92
+ : `Next: an agent answers @mentions there once it has a linear trigger — "ekanos teams triggers add ${team} <agent-key> linear".`;
93
+ }
94
+ //# sourceMappingURL=teams-channel-install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams-channel-install.js","sourceRoot":"","sources":["../../src/commands/teams-channel-install.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,6BAA6B,EAC7B,8BAA8B,EAC9B,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGpC;;;;;;;;;;;GAWG;AAEH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,iBAAiB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,GAAe,EACf,IAAe,EACf,MAAmB,EACnB,IAAY,EACZ,MAAc,EACd,KAMC;;IAED,MAAM,IAAI,GAAG,sBAAsB,kBAAkB,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,KAAK,CAAC,OAAO,EAAE,CAAC;IAC9F,MAAM,IAAI,GAAG,GAAG,EAAE,CAChB,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CACpB,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,8BAA8B,EAAE;QAChD,MAAM,EAAE,KAAK;QACb,IAAI;QACJ,MAAM;KACP,CAAC,CACH,CAAC;IAEJ,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;IAC5B,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,OAAO,GAAG,CAAC,OAAO,iBACd,IAAI,IAAK,MAAM,GACjB,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC;YACZ,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,kBAAkB,MAAM,CAAC,IAAI,EAAE;YACzD,IAAI,EAAE,CAAC,CAAC,WAAW,QAAE,MAAM,CAAC,SAAS,mCAAI,WAAW,CAAC,CAAC;YACtD,KAAK,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;SACtD,CAAC,CACH,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,iBAAiB,CACrB,GAAG,MAAM,CAAC,OAAO,GAAG,EACpB,KAAK,CAAC,OAAO,KAAK,OAAO;YACvB,CAAC,CAAC,qCAAqC,MAAM,6DAA6D;YAC1G,CAAC,CAAC,8DAA8D,CACnE,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAC1C,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,6BAA6B,EAAE;QAC/C,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,GAAG,IAAI,UAAU;QACvB,MAAM;QACN,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE;KACvC,CAAC,CACH,CAAC;IACF,oBAAoB,CAAC,OAAO,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IAC3D,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAC3B,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjE,MAAM,OAAO,GACX,KAAK,CAAC,OAAO,KAAK,OAAO;QACvB,CAAC,CAAC,sCAAsC;QACxC,CAAC,CAAC,6DAA6D,CAAC;IACpE,GAAG,CAAC,GAAG,CACL,MAAM,CAAC,QAAQ;QACb,CAAC,CAAC;YACE,WAAW,MAAM,CAAC,IAAI,QAAQ,MAAM,CAAC,IAAI,CAAC,IAAI,oDAAoD,OAAO,GAAG;YAC5G,KAAK,OAAO,CAAC,YAAY,EAAE;YAC3B,iCAAiC,OAAO,OAAO;SAChD,CAAC,IAAI,CAAC,IAAI,CAAC;QACd,CAAC,CAAC;YACE,qBAAqB,MAAA,MAAM,CAAC,KAAK,mCAAI,YAAY,mBAAmB,MAAM,CAAC,IAAI,QAAQ,MAAM,CAAC,IAAI,CAAC,IAAI,yCAAyC,OAAO,IAAI;YAC3J,KAAK,OAAO,CAAC,YAAY,EAAE;YAC3B,2CAA2C,OAAO,OAAO;SAC1D,CAAC,IAAI,CAAC,IAAI,CAAC,CACjB,CAAC;IAEF,uEAAuE;IACvE,IAAI,MAAM,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QACrD,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACnC,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QAChC,OAAO,GAAG,CAAC,OAAO,+BAEd,IAAI,IACD,OAAO,CAAC,MAAM,KACjB,YAAY,EAAE,OAAO,CAAC,YAAY,EAClC,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,KAER,GAAG,CAAC,EAAE,CAAC,IAAI,CACT,yCAAyC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,IAAI,CACzE,CACF,CAAC;IACJ,CAAC;IAED,GAAG,CAAC,GAAG,CACL,iFAAiF,CAClF,CAAC;IACF,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC9E,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,kBAC5B,UAAU,CAAC,OAAO,QAAE,IAAI,CAAC,WAAW,mCAAI,IAAI,CAAC,GAAA,CAC9C,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;QAC5B,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACrB,OAAO,GAAG,CAAC,OAAO,iBACd,IAAI,IAAK,MAAM,GACjB,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC;gBACZ,KAAK,EAAE,aAAa,MAAM,CAAC,IAAI,QAAQ,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE;gBACzD,IAAI,EAAE;oBACJ,CAAC,WAAW,QAAE,MAAM,CAAC,SAAS,mCAAI,WAAW,CAAC;oBAC9C,CAAC,cAAc,QAAE,MAAM,CAAC,KAAK,mCAAI,YAAY,CAAC;iBAC/C;gBACD,KAAK,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;aACtD,CAAC,CACH,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,eAAe,CACnB,qDAAqD,EACrD,6BAA6B,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,2BAA2B,CAC1H,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,OAAoB;IACrD,OAAO,OAAO,KAAK,OAAO;QACxB,CAAC,CAAC,yFAAyF,IAAI,yFAAyF;QACxL,CAAC,CAAC,oGAAoG,IAAI,uBAAuB,CAAC;AACtI,CAAC","sourcesContent":["import {\n type TeamChannel,\n TeamChannelInstallStartSchema,\n TeamChannelInstallStatusSchema,\n TeamChannelSchema,\n} from '@ekanos/integration-schema';\n\nimport type { CliContext } from '../context';\nimport { invalidStateError, validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { assertBrowserSafeUrl } from '../open-url';\nimport { call } from '../teams-api';\nimport type { TeamsArgs, TeamsClient } from './teams';\n\n/**\n * `teams connect <team> slack|linear-app` — install, for one team, the\n * channel a `slack` or `linear` agent trigger answers through: Slack, or\n * Ekanos's own Linear Agents app (not the team's own Linear app, which is\n * `teams connect <team> linear --connection agent`).\n *\n * The same open-the-link-and-wait as `teams connect`'s sign-in: the host\n * mints a one-time link for the team member who installs it (the caller when\n * a member, else `--as`), the CLI opens it for the caller, prints it always,\n * and polls until it is installed. Only that member, signed in to the app,\n * can finish it.\n */\n\nexport function isTeamChannel(value: string): value is TeamChannel {\n return TeamChannelSchema.safeParse(value).success;\n}\n\nexport async function runTeamChannelInstall(\n ctx: CliContext,\n args: TeamsArgs,\n client: TeamsClient,\n host: string,\n source: string,\n input: {\n team: string;\n channel: TeamChannel;\n as?: string;\n /** Opens the link in the caller's browser (`teams connect`'s opener). */\n open: (url: string) => void;\n },\n): Promise<ExitCode> {\n const path = `/api/partner/teams/${encodeURIComponent(input.team)}/channels/${input.channel}`;\n const read = () =>\n client.call((token) =>\n call(host, token, TeamChannelInstallStatusSchema, {\n method: 'GET',\n path,\n source,\n }),\n );\n\n const status = await read();\n if (status.installed) {\n return ctx.succeed(\n { host, ...status },\n ctx.ui.result({\n title: `${status.team.slug} has installed ${status.name}`,\n rows: [['Workspace', status.workspace ?? 'installed']],\n notes: [triggerHint(status.team.slug, input.channel)],\n }),\n );\n }\n if (status.problem) {\n throw invalidStateError(\n `${status.problem}.`,\n input.channel === 'slack'\n ? `\"ekanos sources products --source ${source}\" shows whether Slack is enabled; a super-admin enables it.`\n : 'Ekanos sets the Linear Agents app up for a host; ask Ekanos.',\n );\n }\n\n const started = await client.call((token) =>\n call(host, token, TeamChannelInstallStartSchema, {\n method: 'POST',\n path: `${path}/install`,\n source,\n body: input.as ? { as: input.as } : {},\n }),\n );\n assertBrowserSafeUrl(started.authorizeUrl, 'install link');\n const { member } = started;\n const expires = started.expiresAt.replace('T', ' ').slice(0, 16);\n const consent =\n input.channel === 'slack'\n ? 'pick the Slack workspace and approve'\n : 'approve it as an admin of the Linear workspace it goes into';\n ctx.log(\n member.isCaller\n ? [\n `Install ${status.name} for ${status.team.slug} — sign in to the app as yourself if asked, then ${consent}:`,\n ` ${started.authorizeUrl}`,\n ` (one-time link, valid until ${expires} UTC)`,\n ].join('\\n')\n : [\n `Send this link to ${member.email ?? 'the member'} — they install ${status.name} for ${status.team.slug}, signed in to the app as themselves (${consent}):`,\n ` ${started.authorizeUrl}`,\n ` (one-time, for them only, valid until ${expires} UTC)`,\n ].join('\\n'),\n );\n\n // Only ever the caller's own browser: the link is bound to its member.\n if (member.isCaller && !args.noOpen && !ctx.jsonMode) {\n input.open(started.authorizeUrl);\n }\n\n if (args.noWait || ctx.jsonMode) {\n return ctx.succeed(\n {\n host,\n ...started.status,\n authorizeUrl: started.authorizeUrl,\n expiresAt: started.expiresAt,\n member,\n },\n ctx.ui.hint(\n `Then check with \"ekanos teams connect ${input.team} ${input.channel}\".`,\n ),\n );\n }\n\n ctx.log(\n 'Waiting for the install to finish (Ctrl-C stops waiting; the link stays valid)…',\n );\n const deadline = Date.parse(started.expiresAt) || Date.now() + 10 * 60 * 1000;\n while (Date.now() < deadline) {\n await new Promise((resolve) =>\n setTimeout(resolve, args.pollDelayMs ?? 3000),\n );\n const polled = await read();\n if (polled.installed) {\n return ctx.succeed(\n { host, ...polled },\n ctx.ui.result({\n title: `Installed ${polled.name} for ${polled.team.slug}`,\n rows: [\n ['Workspace', polled.workspace ?? 'installed'],\n ['Installed by', member.email ?? 'the member'],\n ],\n notes: [triggerHint(polled.team.slug, input.channel)],\n }),\n );\n }\n }\n throw validationError(\n 'The install did not finish before the link expired.',\n `Run \"ekanos teams connect ${input.team} ${input.channel}${input.as ? ` --as ${input.as}` : ''}\" again for a fresh link.`,\n );\n}\n\nfunction triggerHint(team: string, channel: TeamChannel): string {\n return channel === 'slack'\n ? `Next: an agent answers there once it has a slack trigger — \"ekanos teams triggers add ${team} <agent-key> slack --channel '#<channel>' --yes\" (invite the bot to the channel first).`\n : `Next: an agent answers @mentions there once it has a linear trigger — \"ekanos teams triggers add ${team} <agent-key> linear\".`;\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type TeamIntegrationStatus } from '@ekanos/integration-schema';
1
2
  import type { CliContext } from '../context.js';
2
3
  import type { ExitCode } from '../exit-codes.js';
3
4
  import { type TeamsArgs, type TeamsClient } from './teams.js';
@@ -16,10 +17,50 @@ import { type TeamsArgs, type TeamsClient } from './teams.js';
16
17
  * team is connected (unless `--no-wait`);
17
18
  * - a form only the browser can fill → prints the team's page.
18
19
  *
20
+ * `--connection <id>` connects one of the integration's named connections
21
+ * instead of the main one — Linear's `agent`, the team's own Linear app
22
+ * installed as a workspace member, which is what makes Linear send the
23
+ * @mention and assignment webhooks an agent's triggers listen for. It runs
24
+ * the same way: the team app's fields when missing, then the sign-in, then
25
+ * the wait until it is installed.
26
+ *
27
+ * Without `--connection`, once the main connection is made (or already
28
+ * was), a named connection the team's agents' triggers wait on is offered
29
+ * next — at a terminal it asks and installs it, otherwise it names the
30
+ * command. "Connected" is not the goal; an agent that can be reached is.
31
+ *
32
+ * `--reconnect` connects it again even though it reads as connected — for
33
+ * a revoked or expired sign-in, or new form values. An OAuth reconnect
34
+ * prints the link and returns: the team already reads as connected, so
35
+ * there is nothing to wait for.
36
+ *
19
37
  * `--link-only` keeps the old behaviour: the team's page and its status.
38
+ *
39
+ * `slack` and `linear-app` install the channels agent triggers answer
40
+ * through — Slack, and Ekanos's own Linear Agents app — by the same
41
+ * link-and-wait (`teams-channel-install.ts`).
20
42
  */
21
43
  export declare function runTeamConnect(ctx: CliContext, args: TeamsArgs, client: TeamsClient, host: string, source: string, input: {
22
44
  team: string;
23
45
  product: string;
24
46
  as?: string;
47
+ connection?: string;
25
48
  }): Promise<ExitCode>;
49
+ /** Named connections the team's agents wait on and the team has not installed. */
50
+ export declare function channelsToInstall(status: TeamIntegrationStatus): {
51
+ connectionId: string;
52
+ label: string;
53
+ teamApp: boolean;
54
+ credentialsSaved: boolean | null;
55
+ connected: boolean;
56
+ connectedAt: string | null;
57
+ connectedBy: {
58
+ userId: string;
59
+ name: string | null;
60
+ email: string | null;
61
+ } | null;
62
+ neededBy: {
63
+ agent: string;
64
+ triggers: string[];
65
+ }[];
66
+ }[];
@@ -1,9 +1,12 @@
1
- import { spawn } from 'node:child_process';
1
+ import { TeamAgentListSchema, } from '@ekanos/integration-schema';
2
2
  import { invalidStateError, usageError, validationError } from '../errors.js';
3
+ import { assertBrowserSafeUrl, openInBrowser } from '../open-url.js';
3
4
  import { showTeamConnect, startTeamConnect, } from '../team-connect-api.js';
4
- import { showTeamIntegration } from '../teams-api.js';
5
+ import { call, showTeamIntegration } from '../teams-api.js';
5
6
  import { promptHidden, promptLine } from './apps.js';
7
+ import { scopeHiddenLines } from './scope-notice.js';
6
8
  import { canPrompt, checklist, promptAndSaveTeamApp, } from './teams.js';
9
+ import { isTeamChannel, runTeamChannelInstall } from './teams-channel-install.js';
7
10
  /**
8
11
  * `teams connect <team> <product>` — connect a product for one team, doing
9
12
  * whichever the integration needs (the host's plan decides, never the CLI):
@@ -19,24 +22,61 @@ import { canPrompt, checklist, promptAndSaveTeamApp, } from './teams.js';
19
22
  * team is connected (unless `--no-wait`);
20
23
  * - a form only the browser can fill → prints the team's page.
21
24
  *
25
+ * `--connection <id>` connects one of the integration's named connections
26
+ * instead of the main one — Linear's `agent`, the team's own Linear app
27
+ * installed as a workspace member, which is what makes Linear send the
28
+ * @mention and assignment webhooks an agent's triggers listen for. It runs
29
+ * the same way: the team app's fields when missing, then the sign-in, then
30
+ * the wait until it is installed.
31
+ *
32
+ * Without `--connection`, once the main connection is made (or already
33
+ * was), a named connection the team's agents' triggers wait on is offered
34
+ * next — at a terminal it asks and installs it, otherwise it names the
35
+ * command. "Connected" is not the goal; an agent that can be reached is.
36
+ *
37
+ * `--reconnect` connects it again even though it reads as connected — for
38
+ * a revoked or expired sign-in, or new form values. An OAuth reconnect
39
+ * prints the link and returns: the team already reads as connected, so
40
+ * there is nothing to wait for.
41
+ *
22
42
  * `--link-only` keeps the old behaviour: the team's page and its status.
43
+ *
44
+ * `slack` and `linear-app` install the channels agent triggers answer
45
+ * through — Slack, and Ekanos's own Linear Agents app — by the same
46
+ * link-and-wait (`teams-channel-install.ts`).
23
47
  */
24
48
  export async function runTeamConnect(ctx, args, client, host, source, input) {
25
- var _a, _b, _c, _d;
26
- const target = { source, team: input.team, product: input.product };
49
+ var _a, _b, _c, _d, _e;
50
+ const target = Object.assign({ source, team: input.team, product: input.product }, (input.connection ? { connection: input.connection } : {}));
51
+ // Slack and Ekanos's Linear Agents app: the channels a `slack` or `linear`
52
+ // agent trigger answers through, installed by their own flows.
53
+ if (!input.connection && !args.linkOnly && isTeamChannel(input.product)) {
54
+ return runTeamChannelInstall(ctx, args, client, host, source, Object.assign(Object.assign({ team: input.team, channel: input.product }, (input.as ? { as: input.as } : {})), { open: (_a = args.openUrl) !== null && _a !== void 0 ? _a : openInBrowser }));
55
+ }
27
56
  if (args.linkOnly)
28
57
  return printLink(ctx, client, host, target);
29
58
  const { plan } = await client.call((token) => showTeamConnect(host, token, target, input.as));
30
- const name = plan.product.name;
59
+ const named = isNamed(plan);
60
+ const name = named
61
+ ? await connectionLabel(client, host, target, plan)
62
+ : plan.product.name;
63
+ await warnOtherRunsAs(ctx, client, host, source, plan);
64
+ for (const line of scopeHiddenLines(plan.productScope, {
65
+ team: plan.team.slug,
66
+ product: plan.product.slug,
67
+ source: plan.source.slug,
68
+ })) {
69
+ ctx.warn(line);
70
+ }
31
71
  switch (plan.mode) {
32
72
  case 'not-entitled':
33
- throw invalidStateError((_a = plan.reason) !== null && _a !== void 0 ? _a : `"${input.product}" is not enabled on ${plan.source.slug}.`, `A super-admin enables it for the Source first ("ekanos sources products" shows what ${plan.source.slug} has; "ekanos sources products enable ${input.product}" from a super-admin session, or /admin in the browser).`);
73
+ throw invalidStateError((_b = plan.reason) !== null && _b !== void 0 ? _b : `"${input.product}" is not enabled on ${plan.source.slug}.`, `Enable it for the Source first: "ekanos sources products enable ${input.product}" as a Source admin if ${plan.source.slug} built it, otherwise a super-admin (/admin in the browser). "ekanos sources products" shows what ${plan.source.slug} has.`);
34
74
  case 'not-self-serve':
35
- throw invalidStateError((_b = plan.reason) !== null && _b !== void 0 ? _b : `${name} is not connected by a team.`, plan.product.kind === 'experience'
75
+ throw invalidStateError((_c = plan.reason) !== null && _c !== void 0 ? _c : `${name} is not connected by a team.`, plan.product.kind === 'experience'
36
76
  ? `"ekanos sources products --source ${plan.source.slug}" shows whether it is on for the Source.`
37
77
  : 'It is turned on for the Source by Ekanos; there is nothing for the team to do.');
38
78
  case 'unsupported':
39
- throw invalidStateError((_c = plan.reason) !== null && _c !== void 0 ? _c : `${name} cannot be connected from here.`, `Open the team's page instead: ${plan.connectUrl}`);
79
+ throw invalidStateError((_d = plan.reason) !== null && _d !== void 0 ? _d : `${name} cannot be connected from here.`, `Open the team's page instead: ${plan.connectUrl}`);
40
80
  case 'works-as-shipped':
41
81
  return ctx.succeed({ host, plan, connected: true }, ctx.ui.result({
42
82
  title: `${name} works as shipped for ${plan.team.slug}`,
@@ -55,29 +95,137 @@ export async function runTeamConnect(ctx, args, client, host, source, input) {
55
95
  notes: plan.connected
56
96
  ? []
57
97
  : [
58
- (_d = plan.reason) !== null && _d !== void 0 ? _d : `${name} connects through its own form in the app.`,
98
+ (_e = plan.reason) !== null && _e !== void 0 ? _e : `${name} connects through its own form in the app.`,
59
99
  'A team admin signs in there and clicks Connect.',
60
100
  ],
61
101
  }));
62
102
  default:
63
103
  break;
64
104
  }
65
- if (plan.connected) {
66
- return ctx.succeed({ host, plan, connected: true }, ctx.ui.result({
67
- title: `${plan.team.slug} has already connected ${name}`,
105
+ if (plan.connected && !args.reconnect) {
106
+ const summary = ctx.ui.result({
107
+ title: `${plan.team.slug} has already ${named ? 'installed' : 'connected'} ${name}`,
68
108
  rows: [
69
- ['Status', statusOf(plan)],
109
+ ['Status', statusOf(plan, named)],
70
110
  ['Team page', plan.connectUrl],
71
111
  ],
72
112
  notes: [
73
113
  `"ekanos teams show ${plan.team.slug} ${plan.product.slug}" shows the full setup.`,
114
+ `Broken (revoked or expired)? Connect it again: ekanos teams connect ${plan.team.slug} ${plan.product.slug}${named ? ` --connection ${plan.connectionId}` : ''} --reconnect`,
74
115
  ],
75
- }));
116
+ });
117
+ return named
118
+ ? ctx.succeed({ host, plan, connected: true }, summary)
119
+ : offerChannels(ctx, args, client, host, source, input, {
120
+ data: { host, plan, connected: true },
121
+ summary,
122
+ });
76
123
  }
77
124
  if (plan.mode === 'fields' || plan.mode === 'no-fields') {
78
125
  return activate(ctx, args, client, host, target, plan);
79
126
  }
80
- return connectOAuth(ctx, args, client, host, target, plan, input.as);
127
+ return connectOAuth(ctx, args, client, host, target, plan, input.as, {
128
+ name,
129
+ then: named
130
+ ? undefined
131
+ : (done) => offerChannels(ctx, args, client, host, source, input, done),
132
+ });
133
+ }
134
+ /**
135
+ * A personal connection belongs to whoever makes it, but a provided agent
136
+ * uses the personal connection of the person it runs as — so connecting
137
+ * your own leaves the agent missing it (CLI audit P0-5). Name each agent
138
+ * that runs as someone else and the command that connects it for them.
139
+ */
140
+ async function warnOtherRunsAs(ctx, client, host, source, plan) {
141
+ var _a, _b, _c, _d;
142
+ const email = (_b = (_a = plan.member) === null || _a === void 0 ? void 0 : _a.email) === null || _b === void 0 ? void 0 : _b.toLowerCase();
143
+ if (plan.scope !== 'user' || !email)
144
+ return;
145
+ let list;
146
+ try {
147
+ list = await client.call((token) => call(host, token, TeamAgentListSchema, {
148
+ method: 'GET',
149
+ path: `/api/partner/teams/${encodeURIComponent(plan.team.slug)}/agents`,
150
+ source,
151
+ }));
152
+ }
153
+ catch (error) {
154
+ ctx.log(`Could not check who ${plan.team.slug}'s agents run as: ${error instanceof Error ? error.message : String(error)}`);
155
+ return;
156
+ }
157
+ for (const entry of list.agents) {
158
+ const runsAs = (_d = (_c = entry.agent) === null || _c === void 0 ? void 0 : _c.requirements.find((item) => item.key === plan.product.slug &&
159
+ item.via === 'person' &&
160
+ item.viaDetail &&
161
+ item.viaDetail.toLowerCase() !== email)) === null || _d === void 0 ? void 0 : _d.viaDetail;
162
+ if (!runsAs)
163
+ continue;
164
+ ctx.warn(`Note: ${entry.key} runs as ${runsAs} and uses ${runsAs}'s ${plan.product.name} connection, not ${email}'s. For the agent, run: npx @ekanos/cli teams connect ${plan.team.slug} ${plan.product.slug} --as ${runsAs} --source ${source}`);
165
+ }
166
+ }
167
+ /**
168
+ * After the main connection: the named connections the team's agents'
169
+ * triggers wait on and the team has not installed (Linear's agent). At a
170
+ * terminal, ask and install each; otherwise name the command, in the
171
+ * result and its JSON (`channelsToInstall`).
172
+ */
173
+ async function offerChannels(ctx, args, client, host, source, input, done) {
174
+ var _a;
175
+ const status = await client.call((token) => showTeamIntegration(host, token, {
176
+ source,
177
+ team: input.team,
178
+ product: input.product,
179
+ }));
180
+ const waiting = channelsToInstall(status);
181
+ if (waiting.length === 0)
182
+ return ctx.succeed(done.data, done.summary);
183
+ const command = (connectionId) => `ekanos teams connect ${input.team} ${input.product} --connection ${connectionId} --source ${status.source.slug}`;
184
+ const why = (entry) => `${entry.label} is not installed for ${input.team}, and ${entry.neededBy.map((need) => `${need.agent} (${need.triggers.join(', ')})`).join(', ')} only start${entry.neededBy.length === 1 ? 's' : ''} through it.`;
185
+ if (!canPrompt(ctx, args)) {
186
+ return ctx.succeed(Object.assign(Object.assign({}, done.data), { channelsToInstall: waiting.map((entry) => ({
187
+ connectionId: entry.connectionId,
188
+ label: entry.label,
189
+ neededBy: entry.neededBy,
190
+ command: command(entry.connectionId),
191
+ })) }), [
192
+ done.summary,
193
+ ...waiting.flatMap((entry) => [
194
+ `Next: ${why(entry)}`,
195
+ ` Install it in your terminal: npx @ekanos/cli ${command(entry.connectionId)}`,
196
+ ]),
197
+ ].join('\n'));
198
+ }
199
+ ctx.log(done.summary);
200
+ let code = 0;
201
+ for (const entry of waiting) {
202
+ ctx.log(why(entry));
203
+ const answer = (await ((_a = args.ask) !== null && _a !== void 0 ? _a : promptLine)(`Install ${entry.label} now? [Y/n] `))
204
+ .trim()
205
+ .toLowerCase();
206
+ if (answer !== '' && answer !== 'y' && answer !== 'yes') {
207
+ ctx.log(`Skipped. Later: ${command(entry.connectionId)}`);
208
+ continue;
209
+ }
210
+ code = await runTeamConnect(ctx, args, client, host, source, Object.assign(Object.assign({}, input), { connection: entry.connectionId }));
211
+ }
212
+ return code;
213
+ }
214
+ /** Named connections the team's agents wait on and the team has not installed. */
215
+ export function channelsToInstall(status) {
216
+ var _a;
217
+ return ((_a = status.connections) !== null && _a !== void 0 ? _a : []).filter((entry) => entry.connectionId !== 'default' &&
218
+ !entry.connected &&
219
+ entry.neededBy.length > 0);
220
+ }
221
+ function isNamed(plan) {
222
+ return Boolean(plan.connectionId && plan.connectionId !== 'default');
223
+ }
224
+ async function connectionLabel(client, host, target, plan) {
225
+ var _a, _b, _c;
226
+ var _d, _e;
227
+ const status = await client.call((token) => showTeamIntegration(host, token, target));
228
+ return ((_e = (_d = (_b = (_a = status.connections) === null || _a === void 0 ? void 0 : _a.find((entry) => entry.connectionId === plan.connectionId)) === null || _b === void 0 ? void 0 : _b.label) !== null && _d !== void 0 ? _d : (_c = status.teamApps.find((entry) => entry.connectionId === plan.connectionId)) === null || _c === void 0 ? void 0 : _c.label) !== null && _e !== void 0 ? _e : `${plan.product.name} (${plan.connectionId})`);
81
229
  }
82
230
  async function activate(ctx, args, client, host, target, plan) {
83
231
  var _a;
@@ -120,19 +268,23 @@ async function activate(ctx, args, client, host, target, plan) {
120
268
  ],
121
269
  }));
122
270
  }
123
- async function connectOAuth(ctx, args, client, host, target, plan, asEmail) {
124
- var _a, _b, _c, _d, _e, _f;
271
+ async function connectOAuth(ctx, args, client, host, target, plan, asEmail, options) {
272
+ var _a, _b, _c, _d, _e, _f, _g;
273
+ const named = isNamed(plan);
274
+ const name = options.name;
275
+ const again = `ekanos teams connect ${target.team} ${target.product}${named ? ` --connection ${plan.connectionId}` : ''}`;
125
276
  if (!plan.member) {
126
- throw invalidStateError((_a = plan.memberProblem) !== null && _a !== void 0 ? _a : 'No member of the team can sign in yet.', `Name a member of ${plan.team.slug} who signs in: ekanos teams connect ${target.team} ${target.product} --as <email>`);
277
+ throw invalidStateError((_a = plan.memberProblem) !== null && _a !== void 0 ? _a : 'No member of the team can sign in yet.', `Name a member of ${plan.team.slug} who signs in: ${again} --as <email>`);
127
278
  }
128
279
  if (plan.teamApp) {
129
280
  const status = await client.call((token) => showTeamIntegration(host, token, target));
130
- const app = (_b = status.teamApps.find((entry) => entry.connectionId === 'default')) !== null && _b !== void 0 ? _b : status.teamApps[0];
281
+ const connectionId = (_b = plan.connectionId) !== null && _b !== void 0 ? _b : 'default';
282
+ const app = (_c = status.teamApps.find((entry) => entry.connectionId === connectionId)) !== null && _c !== void 0 ? _c : (named ? undefined : status.teamApps[0]);
131
283
  if (app && app.fields.some((field) => !field.set)) {
132
284
  if (!canPrompt(ctx, args)) {
133
- throw usageError(`${plan.product.name} needs ${plan.team.slug}'s own app first, entered at a terminal.`, `Run it yourself in your terminal: npx @ekanos/cli teams connect ${target.team} ${target.product} --source ${plan.source.slug}`);
285
+ throw usageError(`${name} needs ${plan.team.slug}'s own app first, entered at a terminal.`, `Run it yourself in your terminal: npx @ekanos/cli ${again} --source ${plan.source.slug}`);
134
286
  }
135
- ctx.log(`${plan.product.name} signs ${plan.team.slug} in through the team's own app. Set it up first:`);
287
+ ctx.log(`${name} signs ${plan.team.slug} in through the team's own app. Set it up first:`);
136
288
  await promptAndSaveTeamApp(ctx, args, client, host, target, status, app);
137
289
  }
138
290
  }
@@ -140,33 +292,41 @@ async function connectOAuth(ctx, args, client, host, target, plan, asEmail) {
140
292
  if (started.result !== 'consent') {
141
293
  throw invalidStateError(`The host answered "${started.result}" where a sign-in link was expected.`, 'Run "ekanos upgrade" — the CLI and the host disagree on the connect contract.');
142
294
  }
143
- const member = (_c = started.plan.member) !== null && _c !== void 0 ? _c : plan.member;
295
+ assertBrowserSafeUrl(started.authorizeUrl, 'sign-in link');
296
+ const member = (_d = started.plan.member) !== null && _d !== void 0 ? _d : plan.member;
144
297
  const forCaller = member.isCaller;
145
298
  const expires = started.expiresAt.replace('T', ' ').slice(0, 16);
146
299
  ctx.log(forCaller
147
300
  ? [
148
- `Sign in to ${plan.product.name} as the account ${plan.team.slug} should use${plan.scope === 'user' ? ' (a personal connection: yours)' : ''}:`,
301
+ named
302
+ ? `Install ${name} for ${plan.team.slug} — sign in as an admin of the workspace it goes into, and approve it:`
303
+ : `Sign in to ${plan.product.name} as the account ${plan.team.slug} should use${plan.scope === 'user' ? ' (a personal connection: yours)' : ''}:`,
149
304
  ` ${started.authorizeUrl}`,
150
305
  ` (one-time link, valid until ${expires} UTC)`,
151
306
  ].join('\n')
152
307
  : [
153
- `Send this link to ${(_d = member.email) !== null && _d !== void 0 ? _d : 'the member'} — they sign in to ${plan.product.name} for ${plan.team.slug}${plan.scope === 'user' ? ' (a personal connection: theirs)' : ''}:`,
308
+ `Send this link to ${(_e = member.email) !== null && _e !== void 0 ? _e : 'the member'} — they ${named ? `install ${name}` : `sign in to ${plan.product.name}`} for ${plan.team.slug}${plan.scope === 'user' ? ' (a personal connection: theirs)' : ''}:`,
154
309
  ` ${started.authorizeUrl}`,
155
310
  ` (one-time, for them only, valid until ${expires} UTC)`,
156
311
  ].join('\n'));
157
312
  // Only ever the caller's own browser: opening a link meant for another
158
313
  // member would sign the caller's provider account in for them.
159
314
  if (forCaller && !args.noOpen && !ctx.jsonMode) {
160
- ((_e = args.openUrl) !== null && _e !== void 0 ? _e : openInBrowser)(started.authorizeUrl);
315
+ ((_f = args.openUrl) !== null && _f !== void 0 ? _f : openInBrowser)(started.authorizeUrl);
161
316
  }
162
- if (args.noWait || ctx.jsonMode) {
317
+ // A reconnect can't be waited on: the team already reads as connected, so
318
+ // nothing the host reports changes when the new sign-in lands.
319
+ const reconnecting = args.reconnect === true && plan.connected;
320
+ if (args.noWait || ctx.jsonMode || reconnecting) {
163
321
  return ctx.succeed({
164
322
  host,
165
323
  plan: started.plan,
166
324
  authorizeUrl: started.authorizeUrl,
167
325
  expiresAt: started.expiresAt,
168
326
  connected: false,
169
- }, ctx.ui.hint(`Then check with "ekanos teams show ${target.team} ${target.product}".`));
327
+ }, ctx.ui.hint(reconnecting
328
+ ? `The current connection keeps working until the sign-in finishes; then its tokens are replaced. Check with "ekanos teams show ${target.team} ${target.product}".`
329
+ : `Then check with "ekanos teams show ${target.team} ${target.product}".`));
170
330
  }
171
331
  ctx.log('Waiting for the sign-in to finish (Ctrl-C stops waiting; the link stays valid)…');
172
332
  const deadline = Date.parse(started.expiresAt) || Date.now() + 10 * 60 * 1000;
@@ -175,19 +335,30 @@ async function connectOAuth(ctx, args, client, host, target, plan, asEmail) {
175
335
  const polled = await client.call((token) => showTeamConnect(host, token, target, asEmail));
176
336
  if (polled.plan.connected) {
177
337
  const status = await client.call((token) => showTeamIntegration(host, token, target));
178
- return ctx.succeed({ host, plan: polled.plan, status, connected: true }, [
179
- ctx.ui.result({
180
- title: `Connected ${plan.product.name} for ${plan.team.slug}`,
181
- rows: [
182
- ['Signed in by', (_f = member.email) !== null && _f !== void 0 ? _f : member.userId],
183
- ['Status', statusOf(polled.plan)],
184
- ],
185
- }),
186
- checklist(status).join('\n'),
187
- ].join('\n'));
338
+ const done = {
339
+ data: { host, plan: polled.plan, status, connected: true },
340
+ summary: [
341
+ ctx.ui.result({
342
+ title: `${named ? 'Installed' : 'Connected'} ${name} for ${plan.team.slug}`,
343
+ rows: [
344
+ ['Signed in by', (_g = member.email) !== null && _g !== void 0 ? _g : member.userId],
345
+ ['Status', statusOf(polled.plan, named)],
346
+ ],
347
+ notes: named
348
+ ? [
349
+ `Prove it end to end: @mention the agent in ${plan.product.name}, then "ekanos runs list --team ${plan.team.slug}" shows the run it started.`,
350
+ ]
351
+ : [],
352
+ }),
353
+ checklist(status).join('\n'),
354
+ ].join('\n'),
355
+ };
356
+ return options.then
357
+ ? options.then(done)
358
+ : ctx.succeed(done.data, done.summary);
188
359
  }
189
360
  }
190
- throw validationError('The sign-in did not finish before the link expired.', `Run "ekanos teams connect ${target.team} ${target.product}${asEmail ? ` --as ${asEmail}` : ''}" again for a fresh link.`);
361
+ throw validationError('The sign-in did not finish before the link expired.', `Run "${again}${asEmail ? ` --as ${asEmail}` : ''}" again for a fresh link.`);
191
362
  }
192
363
  /** `--link-only`: the team's page and whether it is connected. */
193
364
  async function printLink(ctx, client, host, target) {
@@ -218,25 +389,9 @@ async function printLink(ctx, client, host, target) {
218
389
  ],
219
390
  }));
220
391
  }
221
- function statusOf(plan) {
392
+ function statusOf(plan, named = false) {
222
393
  if (!plan.connected)
223
- return 'not connected';
224
- return `connected${plan.connectedAt ? ` ${plan.connectedAt.slice(0, 10)}` : ''}`;
225
- }
226
- /** Best effort: the link is always printed, so a failed open costs nothing. */
227
- function openInBrowser(url) {
228
- const [command, argv] = process.platform === 'darwin'
229
- ? ['open', [url]]
230
- : process.platform === 'win32'
231
- ? ['cmd', ['/c', 'start', '""', url]]
232
- : ['xdg-open', [url]];
233
- try {
234
- const child = spawn(command, argv, { detached: true, stdio: 'ignore' });
235
- child.on('error', () => { });
236
- child.unref();
237
- }
238
- catch (_a) {
239
- // Printed above; nothing to do.
240
- }
394
+ return named ? 'not installed' : 'not connected';
395
+ return `${named ? 'installed' : 'connected'}${plan.connectedAt ? ` ${plan.connectedAt.slice(0, 10)}` : ''}`;
241
396
  }
242
397
  //# sourceMappingURL=teams-connect.js.map