@automagik/omni 2.260922.2 → 2.260922.4

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.
@@ -0,0 +1,39 @@
1
+ -- Slack OAuth identity on instances (wish: slack-personal-oauth).
2
+ --
3
+ -- The one-click Slack install (OAuth v2) provisions one instance per
4
+ -- (workspace, authorizing user) and must find that instance again when the
5
+ -- same person re-authorizes, so the callback upserts by a persisted Slack
6
+ -- identity instead of by name. The connection method records how the
7
+ -- credentials arrived, so the manual pasted-token path keeps its behavior.
8
+ --
9
+ -- instances.slack_team_id Slack workspace id (T…); also filled for
10
+ -- pasted-token instances by the
11
+ -- instance.connected listener
12
+ -- instances.slack_user_id authorizing user id (U…); null for
13
+ -- bot-mode instances (keyed by team only)
14
+ -- instances.slack_connection_method 'manual' | 'oauth'; NULL reads as 'manual'
15
+ -- instances_slack_identity_idx composite index on (team, user) for the
16
+ -- identity finder a follow-up adds. It
17
+ -- serves NO query today: the callback's
18
+ -- upsert still scans `instances.list`,
19
+ -- which is capped at 1,000 Slack rows and
20
+ -- is the documented trigger for a
21
+ -- `findBySlackIdentity` service finder.
22
+ -- The index ships here so that finder, and
23
+ -- the uniqueness work tracked alongside
24
+ -- it, need no second migration.
25
+ --
26
+ -- All nullable; existing rows are untouched. Not a credential column.
27
+ --
28
+ -- Hand-written following the 0043/0044/0052 precedent (additive, idempotent).
29
+ -- No explicit BEGIN/COMMIT — the boot migrator executes this file on a pooled
30
+ -- postgres-js connection, which rejects raw transaction control.
31
+
32
+ ALTER TABLE "instances" ADD COLUMN IF NOT EXISTS "slack_team_id" varchar(32);
33
+
34
+ ALTER TABLE "instances" ADD COLUMN IF NOT EXISTS "slack_user_id" varchar(32);
35
+
36
+ ALTER TABLE "instances" ADD COLUMN IF NOT EXISTS "slack_connection_method" varchar(16);
37
+
38
+ CREATE INDEX IF NOT EXISTS "instances_slack_identity_idx"
39
+ ON "instances" ("slack_team_id", "slack_user_id");
@@ -547,6 +547,13 @@
547
547
  "when": 1787700000000,
548
548
  "tag": "0077_instances_close_contact_config",
549
549
  "breakpoints": true
550
+ },
551
+ {
552
+ "idx": 78,
553
+ "version": "7",
554
+ "when": 1787800000000,
555
+ "tag": "0078_instances_slack_oauth_identity",
556
+ "breakpoints": true
550
557
  }
551
558
  ]
552
559
  }
@@ -3,10 +3,82 @@
3
3
  *
4
4
  * omni slack dm <instance> <userId> — resolve/open the DM channel
5
5
  * omni slack search <instance> <query> — full-text search (user token only)
6
+ * omni slack app setup — register the deployment Slack app
7
+ * omni slack app status — is the deployment app configured?
8
+ * omni slack connect — one-click OAuth install from a terminal
6
9
  *
7
- * Neither has a cross-channel equivalent, which is why they live here rather
8
- * than under `omni messages`.
10
+ * None has a cross-channel equivalent, which is why they live here rather
11
+ * than under `omni messages` / `omni instances`.
12
+ *
13
+ * Exit codes across this group: 0 success, 2 usage, 3 API failure.
14
+ *
15
+ * Secrets (client secret, signing secret, app-level token) are accepted ONLY
16
+ * from a piped stdin (`--*-stdin`) or an interactive prompt on stderr — there
17
+ * is deliberately no `--client-secret <value>` style flag, because a value on
18
+ * the command line lands in the shell history and in `ps`. Anything this file
19
+ * echoes back goes through `maskSecret` first. `--*-stdin` is the
20
+ * non-echoing path; the prompt is the convenience path.
9
21
  */
22
+ import type { OmniClient } from '@omni/sdk';
10
23
  import { Command } from 'commander';
24
+ /** Every handler returns the process exit code it earned (0 — failures exit through `output.error`). */
25
+ interface CommandResult {
26
+ readonly exitCode: number;
27
+ readonly instanceId?: string;
28
+ }
29
+ /**
30
+ * Mask a secret for display. Local to this file on purpose: `instances.ts`
31
+ * masks API *body fields*, this masks the values `slack app setup` collected
32
+ * before they reach the API, and the two must be free to diverge.
33
+ */
34
+ declare function maskSecret(secret: string): string;
35
+ /** Side effects `slack app setup` needs — injected so the test never touches the real stdin. */
36
+ interface SetupPorts {
37
+ readonly readStdin: () => Promise<string>;
38
+ readonly prompt: (question: string) => Promise<string>;
39
+ }
40
+ /** Side effects `slack connect` needs — injected so the test never waits or launches anything. */
41
+ interface ConnectPorts {
42
+ readonly openBrowser: (url: string) => Promise<boolean>;
43
+ readonly sleep: (ms: number) => Promise<void>;
44
+ readonly now: () => number;
45
+ readonly intervalMs: number;
46
+ }
47
+ interface SetupOptions {
48
+ readonly publicUrl?: string;
49
+ readonly clientId?: string;
50
+ readonly clientSecretStdin?: boolean;
51
+ readonly signingSecretStdin?: boolean;
52
+ readonly appTokenStdin?: boolean;
53
+ readonly nonInteractive?: boolean;
54
+ }
55
+ declare function handleAppSetup(client: OmniClient, opts: SetupOptions, ports?: SetupPorts): Promise<CommandResult>;
56
+ declare function handleAppStatus(client: OmniClient): Promise<CommandResult>;
57
+ interface ConnectOptions {
58
+ readonly mode?: string;
59
+ /** commander sets this to `false` for `--no-open`. */
60
+ readonly open?: boolean;
61
+ readonly timeout?: string;
62
+ }
63
+ declare function handleConnect(client: OmniClient, opts: ConnectOptions, ports?: ConnectPorts): Promise<CommandResult>;
11
64
  export declare function createSlackCommand(): Command;
65
+ export declare const __testables: {
66
+ handleAppSetup: typeof handleAppSetup;
67
+ handleAppStatus: typeof handleAppStatus;
68
+ handleConnect: typeof handleConnect;
69
+ maskSecret: typeof maskSecret;
70
+ SETTING_KEYS: {
71
+ readonly clientId: "slack.app.client_id";
72
+ readonly clientSecret: "slack.app.client_secret";
73
+ readonly signingSecret: "slack.app.signing_secret";
74
+ readonly appToken: "slack.app.app_token";
75
+ readonly publicUrl: "server.public_url";
76
+ };
77
+ EXIT_OK: number;
78
+ EXIT_USAGE: number;
79
+ EXIT_API: number;
80
+ POLL_INTERVAL_MS: number;
81
+ DEFAULT_TIMEOUT_SECONDS: number;
82
+ };
83
+ export {};
12
84
  //# sourceMappingURL=slack.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../src/commands/slack.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,kBAAkB,IAAI,OAAO,CAoD5C"}
1
+ {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../src/commands/slack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0CpC,wGAAwG;AACxG,UAAU,aAAa;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAMD;;;;GAIG;AACH,iBAAS,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAG1C;AAkDD,gGAAgG;AAChG,UAAU,UAAU;IAClB,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACxD;AAED,kGAAkG;AAClG,UAAU,YAAY;IACpB,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAkDD,UAAU,YAAY;IACpB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IACrC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC;AAgJD,iBAAe,cAAc,CAC3B,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,YAAY,EAClB,KAAK,GAAE,UAA8B,GACpC,OAAO,CAAC,aAAa,CAAC,CAcxB;AAED,iBAAe,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,CAGzE;AAMD,UAAU,cAAc;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AA0CD,iBAAe,aAAa,CAC1B,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,cAAc,EACpB,KAAK,GAAE,YAAkC,GACxC,OAAO,CAAC,aAAa,CAAC,CAgDxB;AAgCD,wBAAgB,kBAAkB,IAAI,OAAO,CAkE5C;AAED,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;CAWvB,CAAC"}
package/dist/index.js CHANGED
@@ -82328,6 +82328,9 @@ var init_schema2 = __esm(() => {
82328
82328
  slackAuthMode: varchar("slack_auth_mode", { length: 10 }),
82329
82329
  slackAppToken: text("slack_app_token"),
82330
82330
  slackSigningSecret: text("slack_signing_secret"),
82331
+ slackTeamId: varchar("slack_team_id", { length: 32 }),
82332
+ slackUserId: varchar("slack_user_id", { length: 32 }),
82333
+ slackConnectionMethod: varchar("slack_connection_method", { length: 16 }),
82331
82334
  telegramBotToken: text("telegram_bot_token"),
82332
82335
  telegramReactionLevel: varchar("telegram_reaction_level", { length: 20 }).notNull().default("off"),
82333
82336
  gupshupCallbackUrl: text("gupshup_callback_url"),
@@ -82449,6 +82452,7 @@ var init_schema2 = __esm(() => {
82449
82452
  isDefaultIdx: index("instances_is_default_idx").on(table3.isDefault),
82450
82453
  agentIdIdx: index("instances_agent_id_idx").on(table3.agentId),
82451
82454
  metaPhoneNumberIdx: index("instances_meta_phone_number_idx").on(table3.metaPhoneNumberId),
82455
+ slackIdentityIdx: index("instances_slack_identity_idx").on(table3.slackTeamId, table3.slackUserId),
82452
82456
  chainModeCheck: check("instances_chain_mode_check", sql`${table3.chainMode} IN ('off', 'forward', 'bidirectional')`)
82453
82457
  }));
82454
82458
  whatsappTemplates = pgTable("whatsapp_templates", {
@@ -137204,6 +137208,31 @@ function createOmniClient(config) {
137204
137208
  if (!resp.ok)
137205
137209
  throw OmniApiError.from(json, resp.status);
137206
137210
  return json?.data ?? [];
137211
+ },
137212
+ async appStatus() {
137213
+ const resp = await apiFetch(`${baseUrl}/api/v2/slack/app`);
137214
+ const json = await resp.json();
137215
+ if (!resp.ok)
137216
+ throw OmniApiError.from(json, resp.status);
137217
+ return json;
137218
+ },
137219
+ async oauthStart(body) {
137220
+ const resp = await apiFetch(`${baseUrl}/api/v2/slack/oauth/start`, {
137221
+ method: "POST",
137222
+ headers: { "Content-Type": "application/json" },
137223
+ body: JSON.stringify(body)
137224
+ });
137225
+ const json = await resp.json();
137226
+ if (!resp.ok)
137227
+ throw OmniApiError.from(json, resp.status);
137228
+ return json;
137229
+ },
137230
+ async oauthResult(nonce) {
137231
+ const resp = await apiFetch(`${baseUrl}/api/v2/slack/oauth/result/${encodeURIComponent(nonce)}`);
137232
+ const json = await resp.json();
137233
+ if (!resp.ok)
137234
+ throw OmniApiError.from(json, resp.status);
137235
+ return json;
137207
137236
  }
137208
137237
  },
137209
137238
  followUp: {
@@ -137424,7 +137453,7 @@ import { fileURLToPath } from "url";
137424
137453
  // package.json
137425
137454
  var package_default = {
137426
137455
  name: "@automagik/omni",
137427
- version: "2.260922.2",
137456
+ version: "2.260922.4",
137428
137457
  repository: {
137429
137458
  type: "git",
137430
137459
  url: "git+https://github.com/automagik-dev/omni.git",
@@ -151184,9 +151213,275 @@ async function runSetupAgent(options3) {
151184
151213
  }
151185
151214
 
151186
151215
  // src/commands/slack.ts
151216
+ import { createInterface as createInterface3 } from "readline";
151187
151217
  init_output();
151218
+ var EXIT_OK = 0;
151219
+ var EXIT_USAGE = 2;
151220
+ var EXIT_API = 3;
151221
+ var SETTING_KEYS = {
151222
+ clientId: "slack.app.client_id",
151223
+ clientSecret: "slack.app.client_secret",
151224
+ signingSecret: "slack.app.signing_secret",
151225
+ appToken: "slack.app.app_token",
151226
+ publicUrl: "server.public_url"
151227
+ };
151228
+ var POLL_INTERVAL_MS2 = 2000;
151229
+ var DEFAULT_TIMEOUT_SECONDS = 240;
151230
+ function maskSecret2(secret) {
151231
+ if (secret.length <= 12)
151232
+ return "****";
151233
+ return `****${secret.slice(-4)}`;
151234
+ }
151235
+ async function readStdinText2() {
151236
+ if (process.stdin.isTTY) {
151237
+ throw new Error('a --*-stdin flag requires piped stdin (e.g. `printf %s "$SECRET" | omni slack app setup ...`)');
151238
+ }
151239
+ const chunks = [];
151240
+ for await (const chunk of process.stdin) {
151241
+ chunks.push(Buffer.from(chunk));
151242
+ }
151243
+ return Buffer.concat(chunks).toString("utf-8");
151244
+ }
151245
+ async function promptLine2(question) {
151246
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
151247
+ try {
151248
+ const answer = await new Promise((resolve4) => {
151249
+ rl.question(`${question}: `, resolve4);
151250
+ });
151251
+ return answer.trim();
151252
+ } finally {
151253
+ rl.close();
151254
+ }
151255
+ }
151256
+ async function openBrowser(url) {
151257
+ const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
151258
+ try {
151259
+ const proc = Bun.spawn({ cmd, stdout: "ignore", stderr: "ignore" });
151260
+ return await proc.exited === 0;
151261
+ } catch {
151262
+ return false;
151263
+ }
151264
+ }
151265
+ var defaultSetupPorts = { readStdin: readStdinText2, prompt: promptLine2 };
151266
+ var defaultConnectPorts = {
151267
+ openBrowser,
151268
+ sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms)),
151269
+ now: () => Date.now(),
151270
+ intervalMs: POLL_INTERVAL_MS2
151271
+ };
151272
+ function messageOf(err2) {
151273
+ return err2 instanceof Error ? err2.message : "Unknown error";
151274
+ }
151275
+ function codeOf(err2) {
151276
+ if (typeof err2 === "object" && err2 !== null && "code" in err2) {
151277
+ const code = err2.code;
151278
+ if (typeof code === "string")
151279
+ return code;
151280
+ }
151281
+ return;
151282
+ }
151283
+ async function fetchAppStatus(client) {
151284
+ try {
151285
+ return await client.slack.appStatus();
151286
+ } catch (err2) {
151287
+ error(`Failed to read the Slack app status: ${messageOf(err2)}`, undefined, EXIT_API);
151288
+ }
151289
+ }
151290
+ function printAppStatus(status) {
151291
+ keyValue("configured", status.configured);
151292
+ keyValue("missing", status.missing);
151293
+ keyValue("redirectUrl", status.redirectUrl);
151294
+ keyValue("manifestUrl", status.manifestUrl);
151295
+ if (!status.configured) {
151296
+ info(`Not configured yet \u2014 missing: ${status.missing.join(", ")}. Run: omni slack app setup`);
151297
+ }
151298
+ }
151299
+ var SECRET_REQUESTS = [
151300
+ { field: "clientSecret", flag: "--client-secret-stdin", question: "Slack app client secret" },
151301
+ { field: "signingSecret", flag: "--signing-secret-stdin", question: "Slack app signing secret" },
151302
+ { field: "appToken", flag: "--app-token-stdin", question: "Slack app-level token (xapp-\u2026)" }
151303
+ ];
151304
+ function wantsStdin(opts, field) {
151305
+ if (field === "clientSecret")
151306
+ return opts.clientSecretStdin === true;
151307
+ if (field === "signingSecret")
151308
+ return opts.signingSecretStdin === true;
151309
+ return opts.appTokenStdin === true;
151310
+ }
151311
+ async function readSecretsFromStdin(requests, ports) {
151312
+ const collected = new Map;
151313
+ if (requests.length === 0)
151314
+ return collected;
151315
+ let text3;
151316
+ try {
151317
+ text3 = await ports.readStdin();
151318
+ } catch (err2) {
151319
+ error(messageOf(err2), undefined, EXIT_USAGE);
151320
+ }
151321
+ const lines = text3.split(/\r?\n/).map((line3) => line3.trim()).filter((line3) => line3.length > 0);
151322
+ if (lines.length < requests.length) {
151323
+ const flags = requests.map((r6) => r6.flag).join(", ");
151324
+ error(`stdin carried ${lines.length} non-empty line(s) but ${requests.length} were requested (${flags}), one line per flag in that order`, undefined, EXIT_USAGE);
151325
+ }
151326
+ requests.forEach((request, index2) => collected.set(request.field, lines[index2]));
151327
+ return collected;
151328
+ }
151329
+ async function resolvePlainValue(given, question, flag2, opts, ports) {
151330
+ const trimmed = given?.trim();
151331
+ if (trimmed)
151332
+ return trimmed;
151333
+ if (opts.nonInteractive) {
151334
+ error(`--non-interactive requires ${flag2}`, undefined, EXIT_USAGE);
151335
+ }
151336
+ const answer = (await ports.prompt(question)).trim();
151337
+ if (!answer) {
151338
+ error(`${flag2} is required`, undefined, EXIT_USAGE);
151339
+ }
151340
+ return answer;
151341
+ }
151342
+ async function resolveSetupValues(opts, ports) {
151343
+ const requested = SECRET_REQUESTS.filter((request) => wantsStdin(opts, request.field));
151344
+ const fromStdin = await readSecretsFromStdin(requested, ports);
151345
+ const publicUrl = await resolvePlainValue(opts.publicUrl, "Public base URL of this deployment (https://\u2026)", "--public-url <url>", opts, ports);
151346
+ const clientId = await resolvePlainValue(opts.clientId, "Slack app client id", "--client-id <id>", opts, ports);
151347
+ const secrets2 = new Map(fromStdin);
151348
+ for (const request of SECRET_REQUESTS) {
151349
+ if (secrets2.has(request.field))
151350
+ continue;
151351
+ if (opts.nonInteractive) {
151352
+ error(`--non-interactive requires ${request.flag}`, undefined, EXIT_USAGE);
151353
+ }
151354
+ const answer = (await ports.prompt(request.question)).trim();
151355
+ if (!answer) {
151356
+ error(`${request.flag} is required`, undefined, EXIT_USAGE);
151357
+ }
151358
+ secrets2.set(request.field, answer);
151359
+ }
151360
+ return {
151361
+ publicUrl,
151362
+ clientId,
151363
+ clientSecret: secrets2.get("clientSecret") ?? "",
151364
+ signingSecret: secrets2.get("signingSecret") ?? "",
151365
+ appToken: secrets2.get("appToken") ?? ""
151366
+ };
151367
+ }
151368
+ async function writeAppSettings(client, values2) {
151369
+ const entries = [
151370
+ [SETTING_KEYS.clientId, values2.clientId, false],
151371
+ [SETTING_KEYS.clientSecret, values2.clientSecret, true],
151372
+ [SETTING_KEYS.signingSecret, values2.signingSecret, true],
151373
+ [SETTING_KEYS.appToken, values2.appToken, true],
151374
+ [SETTING_KEYS.publicUrl, values2.publicUrl, false]
151375
+ ];
151376
+ for (const [key, value] of entries) {
151377
+ try {
151378
+ await client.settings.set(key, value);
151379
+ } catch (err2) {
151380
+ error(`Failed to write ${key}: ${messageOf(err2)}`, undefined, EXIT_API);
151381
+ }
151382
+ }
151383
+ success(`Wrote ${entries.length} Slack app settings`);
151384
+ list(entries.map(([key, value, secret]) => ({ key, value: secret ? maskSecret2(value) : value })));
151385
+ }
151386
+ async function handleAppSetup(client, opts, ports = defaultSetupPorts) {
151387
+ const before = await fetchAppStatus(client);
151388
+ if (before.manifestUrl) {
151389
+ info("Create (or update) the Slack app from this pre-filled manifest, then come back:");
151390
+ keyValue("manifestUrl", before.manifestUrl);
151391
+ } else {
151392
+ info("The pre-filled manifest link appears once server.public_url is known \u2014 this setup writes it.");
151393
+ }
151394
+ const values2 = await resolveSetupValues(opts, ports);
151395
+ await writeAppSettings(client, values2);
151396
+ printAppStatus(await fetchAppStatus(client));
151397
+ return { exitCode: EXIT_OK };
151398
+ }
151399
+ async function handleAppStatus(client) {
151400
+ printAppStatus(await fetchAppStatus(client));
151401
+ return { exitCode: EXIT_OK };
151402
+ }
151403
+ function resolveMode(opts) {
151404
+ if (opts.mode === undefined)
151405
+ return;
151406
+ if (opts.mode === "user" || opts.mode === "bot")
151407
+ return opts.mode;
151408
+ error(`--mode must be 'user' or 'bot' (got '${opts.mode}')`, undefined, EXIT_USAGE);
151409
+ }
151410
+ function resolveTimeoutSeconds(opts) {
151411
+ if (opts.timeout === undefined)
151412
+ return DEFAULT_TIMEOUT_SECONDS;
151413
+ const seconds = Number(opts.timeout);
151414
+ if (!Number.isFinite(seconds) || seconds <= 0) {
151415
+ error(`--timeout must be a positive number of seconds (got '${opts.timeout}')`, undefined, EXIT_USAGE);
151416
+ }
151417
+ return seconds;
151418
+ }
151419
+ async function pollOauthResult(client, nonce, ports, timeoutMs) {
151420
+ const deadline = ports.now() + timeoutMs;
151421
+ const maxPolls = Math.max(1, Math.ceil(timeoutMs / Math.max(1, ports.intervalMs)));
151422
+ for (let attempt = 0;attempt < maxPolls; attempt++) {
151423
+ await ports.sleep(ports.intervalMs);
151424
+ let result;
151425
+ try {
151426
+ result = await client.slack.oauthResult(nonce);
151427
+ } catch (err2) {
151428
+ error(`Failed to read the install result: ${messageOf(err2)}`, undefined, EXIT_API);
151429
+ }
151430
+ if (result.status !== "pending")
151431
+ return result;
151432
+ if (ports.now() >= deadline)
151433
+ break;
151434
+ }
151435
+ return;
151436
+ }
151437
+ async function handleConnect(client, opts, ports = defaultConnectPorts) {
151438
+ const mode = resolveMode(opts);
151439
+ const timeoutSeconds = resolveTimeoutSeconds(opts);
151440
+ let started;
151441
+ try {
151442
+ started = await client.slack.oauthStart({ entry: "cli", ...mode ? { mode } : {} });
151443
+ } catch (err2) {
151444
+ if (codeOf(err2) === "SLACK_APP_NOT_CONFIGURED") {
151445
+ error(`The deployment Slack app is not configured: ${messageOf(err2)}. Run: omni slack app setup`, undefined, EXIT_API);
151446
+ }
151447
+ error(`Failed to start the Slack install: ${messageOf(err2)}`, undefined, EXIT_API);
151448
+ }
151449
+ info("Approve the install in Slack \u2014 open this URL if a browser does not:");
151450
+ keyValue("authorizeUrl", started.authorizeUrl);
151451
+ keyValue("nonce", started.nonce);
151452
+ keyValue("expiresAt", started.expiresAt);
151453
+ if (opts.open !== false) {
151454
+ const launched = await ports.openBrowser(started.authorizeUrl);
151455
+ if (!launched)
151456
+ info("Could not open a browser here \u2014 copy the URL above.");
151457
+ }
151458
+ const result = await pollOauthResult(client, started.nonce, ports, timeoutSeconds * 1000);
151459
+ if (!result) {
151460
+ error(`Timed out after ${timeoutSeconds}s waiting for Slack. The pending install (nonce ${started.nonce}) is single-use and cannot be resumed \u2014 start over with: omni slack connect`, undefined, EXIT_API);
151461
+ }
151462
+ if (result.status === "error") {
151463
+ error(`Slack install failed: ${result.message}`, { code: result.code }, EXIT_API);
151464
+ }
151465
+ if (result.status !== "done") {
151466
+ error(`Slack install did not complete (status: ${result.status})`, undefined, EXIT_API);
151467
+ }
151468
+ success(`Slack connected: instance ${result.instanceId}`);
151469
+ keyValue("instanceId", result.instanceId);
151470
+ keyValue("status", result.status);
151471
+ return { exitCode: EXIT_OK, instanceId: result.instanceId };
151472
+ }
151473
+ function createSlackAppCommand() {
151474
+ const app = new Command("app").description("Deployment-level Slack app registration (one app, every workspace)");
151475
+ app.command("setup").description("Register the deployment Slack app: print the manifest link, collect the values, write the settings").option("--public-url <url>", "Public base URL of this deployment (https://\u2026)").option("--client-id <id>", "Slack app client id").option("--client-secret-stdin", "Read the client secret from stdin (one line)").option("--signing-secret-stdin", "Read the signing secret from stdin (one line)").option("--app-token-stdin", "Read the app-level token from stdin (one line)").option("--non-interactive", "Never prompt: every value must arrive by flag or stdin").action(async (opts) => {
151476
+ await handleAppSetup(getClient(), opts);
151477
+ });
151478
+ app.command("status").description("Is the deployment Slack app configured? Shows missing keys, redirect URL and manifest link.").action(async () => {
151479
+ await handleAppStatus(getClient());
151480
+ });
151481
+ return app;
151482
+ }
151188
151483
  function createSlackCommand() {
151189
- const cmd = new Command("slack").description("Slack-only operations: open a DM, search messages (#889)");
151484
+ const cmd = new Command("slack").description("Slack-only operations: connect a workspace, open a DM, search messages (#889)");
151190
151485
  cmd.command("dm <instance> <userId>").description("Resolve the DM channel id for a Slack user (U\u2026). Idempotent.").action(async (instance3, userId) => {
151191
151486
  try {
151192
151487
  const instanceId = await resolveInstanceId(instance3);
@@ -151219,6 +151514,10 @@ function createSlackCommand() {
151219
151514
  error(`Search failed: ${err2 instanceof Error ? err2.message : "Unknown error"}`, undefined, 3);
151220
151515
  }
151221
151516
  });
151517
+ cmd.addCommand(createSlackAppCommand());
151518
+ cmd.command("connect").description("Connect a Slack workspace through the deployment app (opens Slack, then waits for the callback)").option("--mode <mode>", "Authorize as the person ('user', default) or as the workspace bot ('bot')").option("--no-open", "Do not try to open a browser; just print the URL").option("--timeout <seconds>", `Seconds to wait for the callback (default ${DEFAULT_TIMEOUT_SECONDS})`).action(async (opts) => {
151519
+ await handleConnect(getClient(), opts);
151520
+ });
151222
151521
  return cmd;
151223
151522
  }
151224
151523
 
@@ -152995,7 +153294,7 @@ function createTurnsCommand() {
152995
153294
  // src/commands/update.ts
152996
153295
  import { existsSync as existsSync20 } from "fs";
152997
153296
  import { join as join19 } from "path";
152998
- import { createInterface as createInterface3 } from "readline";
153297
+ import { createInterface as createInterface4 } from "readline";
152999
153298
  init_source();
153000
153299
  init_config();
153001
153300
 
@@ -153667,7 +153966,7 @@ async function restartServicesAndVerify(servicesToRestart, latest, options3, dia
153667
153966
  }
153668
153967
  }
153669
153968
  async function promptConfirm(question) {
153670
- const rl = createInterface3({
153969
+ const rl = createInterface4({
153671
153970
  input: process.stdin,
153672
153971
  output: process.stdout
153673
153972
  });
@@ -2588,6 +2588,12 @@ export declare function createOmniClient(config: OmniClientConfig): {
2588
2588
  count?: number;
2589
2589
  page?: number;
2590
2590
  }): Promise<Record<string, unknown>[]>;
2591
+ /** Deployment Slack app status: configured?, missing setting keys, redirect + manifest URLs. */
2592
+ appStatus(): Promise<components["schemas"]["SlackAppStatus"]>;
2593
+ /** Start a one-click OAuth install; open `authorizeUrl` in a browser, then poll `oauthResult(nonce)`. */
2594
+ oauthStart(body: components["schemas"]["SlackOAuthStartBody"]): Promise<components["schemas"]["SlackOAuthStart"]>;
2595
+ /** Single-use outcome of an install: `pending` until the callback lands, then `done` or `error` once. */
2596
+ oauthResult(nonce: string): Promise<components["schemas"]["SlackOAuthResult"]>;
2591
2597
  };
2592
2598
  followUp: {
2593
2599
  getAgent(id: string): Promise<components["schemas"]["FollowUpSequenceConfig"] | null>;