@damian87/omp 0.6.0 → 0.8.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 (60) hide show
  1. package/.github/skills/team/SKILL.md +82 -57
  2. package/.github/skills/team/scripts/team-launch.sh +140 -42
  3. package/README.md +31 -0
  4. package/catalog/skills-general.json +2 -2
  5. package/dist/src/cli.d.ts +1 -7
  6. package/dist/src/cli.js +355 -1
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/commands/registry.d.ts +3 -0
  9. package/dist/src/commands/registry.js +11 -0
  10. package/dist/src/commands/registry.js.map +1 -0
  11. package/dist/src/commands/suggest.d.ts +19 -0
  12. package/dist/src/commands/suggest.js +158 -0
  13. package/dist/src/commands/suggest.js.map +1 -0
  14. package/dist/src/commands/types.d.ts +16 -0
  15. package/dist/src/commands/types.js +2 -0
  16. package/dist/src/commands/types.js.map +1 -0
  17. package/dist/src/env/dotenv.d.ts +41 -0
  18. package/dist/src/env/dotenv.js +112 -0
  19. package/dist/src/env/dotenv.js.map +1 -0
  20. package/dist/src/env/init.d.ts +50 -0
  21. package/dist/src/env/init.js +276 -0
  22. package/dist/src/env/init.js.map +1 -0
  23. package/dist/src/gateway/connector.d.ts +37 -0
  24. package/dist/src/gateway/connector.js +12 -0
  25. package/dist/src/gateway/connector.js.map +1 -0
  26. package/dist/src/gateway/connectors/slack.d.ts +69 -0
  27. package/dist/src/gateway/connectors/slack.js +159 -0
  28. package/dist/src/gateway/connectors/slack.js.map +1 -0
  29. package/dist/src/gateway/registry.d.ts +29 -0
  30. package/dist/src/gateway/registry.js +37 -0
  31. package/dist/src/gateway/registry.js.map +1 -0
  32. package/dist/src/gateway/runtime.d.ts +58 -0
  33. package/dist/src/gateway/runtime.js +105 -0
  34. package/dist/src/gateway/runtime.js.map +1 -0
  35. package/dist/src/instructions-memory.js +8 -15
  36. package/dist/src/instructions-memory.js.map +1 -1
  37. package/dist/src/jira.js +2 -14
  38. package/dist/src/jira.js.map +1 -1
  39. package/dist/src/slack/config.d.ts +32 -0
  40. package/dist/src/slack/config.js +52 -0
  41. package/dist/src/slack/config.js.map +1 -0
  42. package/dist/src/slack/handler.d.ts +48 -0
  43. package/dist/src/slack/handler.js +68 -0
  44. package/dist/src/slack/handler.js.map +1 -0
  45. package/dist/src/slack/serve.d.ts +8 -0
  46. package/dist/src/slack/serve.js +7 -0
  47. package/dist/src/slack/serve.js.map +1 -0
  48. package/dist/src/team/index.d.ts +1 -0
  49. package/dist/src/team/index.js +1 -0
  50. package/dist/src/team/index.js.map +1 -1
  51. package/dist/src/team/pane-monitor.d.ts +39 -0
  52. package/dist/src/team/pane-monitor.js +128 -0
  53. package/dist/src/team/pane-monitor.js.map +1 -0
  54. package/dist/src/team/runtime.js +12 -1
  55. package/dist/src/team/runtime.js.map +1 -1
  56. package/dist/src/team/tmux.d.ts +13 -0
  57. package/dist/src/team/tmux.js +47 -0
  58. package/dist/src/team/tmux.js.map +1 -1
  59. package/docs/slack-setup.md +144 -0
  60. package/package.json +4 -2
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Tiny dotenv reader for the omp CLI. Two purposes:
3
+ *
4
+ * 1. {@link parseDotEnv} — pure string → record parser. Same rules the legacy
5
+ * `loadDotEnv` in src/jira.ts:100-117 has used in production. Shared here
6
+ * so jira and the new `~/.omp/.env` loader stay in sync.
7
+ *
8
+ * 2. {@link loadOmpEnv} — auto-loads `~/.omp/.env` into `process.env` so the
9
+ * CLI works from any cwd without needing `set -a; source .env; set +a`.
10
+ * Precedence: shell `process.env` always wins; the file only fills in
11
+ * keys that are not already set, so CI environments and one-off
12
+ * `KEY=value omp ...` invocations are unaffected.
13
+ *
14
+ * Both functions are designed to fail open — a missing or unreadable file
15
+ * must never crash the CLI; we degrade silently (loadOmpEnv) or emit a
16
+ * one-line stderr warning, never the file contents.
17
+ */
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ export const OMP_ENV_DIRNAME = ".omp";
22
+ export const OMP_ENV_FILENAME = ".env";
23
+ /**
24
+ * Parse the text of a `.env` file. Accepted syntax (a deliberate subset, the
25
+ * same rules src/jira.ts already follows):
26
+ *
27
+ * KEY=value → { KEY: "value" }
28
+ * KEY="quoted value" → { KEY: "quoted value" } (single OR double quotes; matching outer pair stripped)
29
+ * KEY=value with spaces → { KEY: "value with spaces" }
30
+ * # comment → ignored
31
+ * (blank line) → ignored
32
+ * malformed (no `=`) → skipped (no throw)
33
+ * KEY= → empty string → caller decides whether to apply
34
+ *
35
+ * NOT supported (out of scope for `omp`): `export KEY=`, `${VAR}` interpolation,
36
+ * multi-line values, escape sequences. Keep this tiny.
37
+ */
38
+ export function parseDotEnv(text) {
39
+ const env = {};
40
+ const lines = text.split(/\r?\n/);
41
+ for (const rawLine of lines) {
42
+ const line = rawLine.trim();
43
+ if (!line || line.startsWith("#"))
44
+ continue;
45
+ const equals = line.indexOf("=");
46
+ if (equals === -1)
47
+ continue;
48
+ const key = line.slice(0, equals).trim();
49
+ if (!key)
50
+ continue;
51
+ // Strip a leading/trailing quote independently — preserves the legacy
52
+ // src/jira.ts loadDotEnv behavior verbatim so any existing .env files
53
+ // that jira used keep producing the same map.
54
+ const value = line
55
+ .slice(equals + 1)
56
+ .trim()
57
+ .replace(/^['"]|['"]$/g, "");
58
+ env[key] = value;
59
+ }
60
+ return env;
61
+ }
62
+ /**
63
+ * Locate `<home>/.omp/.env`, parse it, and apply non-conflicting keys onto
64
+ * `processEnv`. Keys already present in `processEnv` (incl. empty strings) are
65
+ * preserved — shell environment always wins.
66
+ *
67
+ * Returns a summary so the caller can log "loaded N keys from <path>" once at
68
+ * startup if desired. We do NOT log secret values; only the path and counts.
69
+ */
70
+ export function loadOmpEnv(opts = {}) {
71
+ const home = opts.homeDir ?? homedir();
72
+ const env = opts.processEnv ?? process.env;
73
+ const log = opts.log ?? ((m) => console.error(m));
74
+ // Test escape hatch — OMP_SKIP_USER_ENV=1 in the target env disables the
75
+ // loader. Real CLI calls inherit process.env by default, so a single
76
+ // vitest setup line (process.env.OMP_SKIP_USER_ENV="1") opts the entire
77
+ // suite out. Unit tests that DO want to exercise the loader pass their
78
+ // own clean processEnv and don't inherit the flag.
79
+ if (env.OMP_SKIP_USER_ENV) {
80
+ return { loaded: 0, path: null };
81
+ }
82
+ const path = join(home, OMP_ENV_DIRNAME, OMP_ENV_FILENAME);
83
+ if (!existsSync(path))
84
+ return { loaded: 0, path: null };
85
+ let text;
86
+ try {
87
+ text = readFileSync(path, "utf8");
88
+ }
89
+ catch (err) {
90
+ // Permissions error, racing delete — degrade gracefully. Don't leak path
91
+ // contents; just say we couldn't read it.
92
+ const msg = err instanceof Error ? err.message : String(err);
93
+ log(`omp: could not read ${path}: ${msg}`);
94
+ return { loaded: 0, path };
95
+ }
96
+ const parsed = parseDotEnv(text);
97
+ let loaded = 0;
98
+ for (const [key, value] of Object.entries(parsed)) {
99
+ // Skip empty values — treat `KEY=` in the file as "no opinion", so an
100
+ // empty file entry can't accidentally shadow a process.env value the
101
+ // user later sets in a parent shell.
102
+ if (value === "")
103
+ continue;
104
+ // process.env wins — only fill the gap.
105
+ if (env[key] !== undefined)
106
+ continue;
107
+ env[key] = value;
108
+ loaded++;
109
+ }
110
+ return { loaded, path };
111
+ }
112
+ //# sourceMappingURL=dotenv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dotenv.js","sourceRoot":"","sources":["../../../src/env/dotenv.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /** Where to send instructional and prompt text. */
2
+ export interface InitIO {
3
+ /** Display a line to the user (stdout in interactive use). */
4
+ print(line: string): void;
5
+ /** Display a diagnostic warning (stderr — must NEVER pollute stdout when --json is in play). */
6
+ warn?(line: string): void;
7
+ /** Read one line of input (or undefined when stream closed/non-interactive). */
8
+ ask(prompt: string): Promise<string | undefined>;
9
+ }
10
+ export interface InitOptions {
11
+ io: InitIO;
12
+ /** Override the user's home directory (tests). */
13
+ homeDir?: string;
14
+ /** Use these answers verbatim — skips prompts (`--non-interactive` mode). */
15
+ answers?: Partial<InitAnswers>;
16
+ /** When true, overwrite an existing ~/.omp/.env without asking. */
17
+ force?: boolean;
18
+ }
19
+ export interface InitAnswers {
20
+ slackBotToken: string;
21
+ slackAppToken: string;
22
+ copilotTmuxSession: string;
23
+ slackAllowedUsers: string;
24
+ }
25
+ export interface InitResult {
26
+ /** Whether the file was written. False when the user aborted. */
27
+ ok: boolean;
28
+ /** Resolved file path. */
29
+ path: string;
30
+ /** Reason when ok=false. */
31
+ reason?: string;
32
+ }
33
+ /**
34
+ * Slack app manifest pre-configured for the omp gateway bridge. Includes the
35
+ * bot scopes Slack requires before letting you install the app, plus event
36
+ * subscriptions for DMs and @mentions, plus Socket Mode (no public URL).
37
+ *
38
+ * Keep this in sync with docs/slack-setup.md. If you change one, change both.
39
+ */
40
+ export declare const SLACK_APP_MANIFEST_YAML = "display_information:\n name: omp-copilot\n description: Bridge to a local GitHub Copilot CLI session\nfeatures:\n bot_user:\n display_name: omp-copilot\n always_online: true\n app_home:\n messages_tab_enabled: true\n messages_tab_read_only_enabled: false\noauth_config:\n scopes:\n bot:\n - app_mentions:read\n - chat:write\n - im:history\n - im:read\n - im:write\nsettings:\n event_subscriptions:\n bot_events:\n - app_mention\n - message.im\n interactivity:\n is_enabled: false\n org_deploy_enabled: false\n socket_mode_enabled: true\n";
41
+ /**
42
+ * Run the interactive setup. Idempotent — re-running shows masked existing
43
+ * values and offers to overwrite (or pass `force: true`).
44
+ *
45
+ * Validation:
46
+ * - bot token must start with `xoxb-` (we re-prompt up to 2 times)
47
+ * - app token must start with `xapp-` (we re-prompt up to 2 times)
48
+ * - empty bot/app token in non-interactive mode is an error
49
+ */
50
+ export declare function runEnvInit(opts: InitOptions): Promise<InitResult>;
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Interactive setup for `~/.omp/.env`.
3
+ *
4
+ * `omp env init` walks the user through getting their Slack tokens, prompts
5
+ * them, writes the file (chmod 600), and tells them what to run next. Anything
6
+ * that would prompt the user lives behind injectable I/O so unit tests can
7
+ * exercise the full happy path and the validation/abort paths without ever
8
+ * touching a real terminal.
9
+ */
10
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { dirname, join } from "node:path";
13
+ import { OMP_ENV_DIRNAME, OMP_ENV_FILENAME } from "./dotenv.js";
14
+ const BOT_TOKEN_PREFIX = "xoxb-";
15
+ const APP_TOKEN_PREFIX = "xapp-";
16
+ const SLACK_APP_URL = "https://api.slack.com/apps";
17
+ /**
18
+ * Slack app manifest pre-configured for the omp gateway bridge. Includes the
19
+ * bot scopes Slack requires before letting you install the app, plus event
20
+ * subscriptions for DMs and @mentions, plus Socket Mode (no public URL).
21
+ *
22
+ * Keep this in sync with docs/slack-setup.md. If you change one, change both.
23
+ */
24
+ export const SLACK_APP_MANIFEST_YAML = `display_information:
25
+ name: omp-copilot
26
+ description: Bridge to a local GitHub Copilot CLI session
27
+ features:
28
+ bot_user:
29
+ display_name: omp-copilot
30
+ always_online: true
31
+ app_home:
32
+ messages_tab_enabled: true
33
+ messages_tab_read_only_enabled: false
34
+ oauth_config:
35
+ scopes:
36
+ bot:
37
+ - app_mentions:read
38
+ - chat:write
39
+ - im:history
40
+ - im:read
41
+ - im:write
42
+ settings:
43
+ event_subscriptions:
44
+ bot_events:
45
+ - app_mention
46
+ - message.im
47
+ interactivity:
48
+ is_enabled: false
49
+ org_deploy_enabled: false
50
+ socket_mode_enabled: true
51
+ `;
52
+ const INTRO_LINES = [
53
+ "",
54
+ "omp env init — set up ~/.omp/.env",
55
+ "",
56
+ "This writes your Slack tokens (and optional defaults) to ~/.omp/.env so",
57
+ "`omp gateway serve` works from any shell, without `source .env`.",
58
+ "Shell exports always win, so a one-off override still works.",
59
+ "",
60
+ "──────────────────────────────────────────────────────────────────────",
61
+ "STEP 1 — create the Slack app FROM AN APP MANIFEST (not from scratch).",
62
+ "──────────────────────────────────────────────────────────────────────",
63
+ ` • Open ${SLACK_APP_URL} → "Create New App" → "From an app manifest".`,
64
+ " • Pick your workspace.",
65
+ ` • Choose YAML and paste the manifest below, then "Create" → "Install to Workspace".`,
66
+ " (The manifest includes the required scopes so Slack will let you install it.",
67
+ " Picking 'From scratch' leaves scopes empty — Slack then refuses to install.)",
68
+ "",
69
+ "── manifest (copy from here to the next dashed line) ──",
70
+ ...SLACK_APP_MANIFEST_YAML.trimEnd().split("\n"),
71
+ "── end of manifest ──",
72
+ "",
73
+ "──────────────────────────────────────────────────────────────────────",
74
+ "STEP 2 — grab the two tokens (≈1 min):",
75
+ "──────────────────────────────────────────────────────────────────────",
76
+ ` • Bot token (xoxb-…): "OAuth & Permissions" → "Bot User OAuth Token"`,
77
+ " (visible after the 'Install to Workspace' step above).",
78
+ ` • App-level token (xapp-…): "Basic Information" → "App-Level Tokens"`,
79
+ " → Generate, with scope `connections:write`.",
80
+ "",
81
+ "Then paste both tokens at the prompts below. Press ENTER on optional ones to skip.",
82
+ "",
83
+ ];
84
+ /**
85
+ * Run the interactive setup. Idempotent — re-running shows masked existing
86
+ * values and offers to overwrite (or pass `force: true`).
87
+ *
88
+ * Validation:
89
+ * - bot token must start with `xoxb-` (we re-prompt up to 2 times)
90
+ * - app token must start with `xapp-` (we re-prompt up to 2 times)
91
+ * - empty bot/app token in non-interactive mode is an error
92
+ */
93
+ export async function runEnvInit(opts) {
94
+ const { io, force, answers } = opts;
95
+ const home = opts.homeDir ?? homedir();
96
+ const path = join(home, OMP_ENV_DIRNAME, OMP_ENV_FILENAME);
97
+ // Non-interactive path: take everything from `answers`. Used by the CLI
98
+ // when the user passes --bot-token / --app-token flags, or by tests.
99
+ const interactive = !answers;
100
+ if (interactive) {
101
+ for (const line of INTRO_LINES)
102
+ io.print(line);
103
+ }
104
+ if (existsSync(path) && !force) {
105
+ if (interactive) {
106
+ const existing = readExistingMasked(path);
107
+ io.print(`Existing config at ${path}:`);
108
+ for (const line of existing)
109
+ io.print(` ${line}`);
110
+ io.print("");
111
+ const overwrite = await io.ask("Overwrite? [y/N] ");
112
+ if ((overwrite ?? "").trim().toLowerCase() !== "y") {
113
+ return { ok: false, path, reason: "aborted by user (no overwrite)" };
114
+ }
115
+ }
116
+ else {
117
+ return { ok: false, path, reason: `${path} already exists (use --force to overwrite)` };
118
+ }
119
+ }
120
+ const collected = {
121
+ slackBotToken: "",
122
+ slackAppToken: "",
123
+ copilotTmuxSession: "",
124
+ slackAllowedUsers: "",
125
+ };
126
+ if (answers) {
127
+ Object.assign(collected, answers);
128
+ }
129
+ else {
130
+ collected.slackBotToken = await promptForToken(io, "Slack BOT token", BOT_TOKEN_PREFIX);
131
+ collected.slackAppToken = await promptForToken(io, "Slack APP-LEVEL token", APP_TOKEN_PREFIX);
132
+ collected.copilotTmuxSession = (await io.ask("Pin Copilot tmux session (optional, e.g. omp-9999): ")) ?? "";
133
+ collected.slackAllowedUsers = (await io.ask("Slack user ID allowlist (optional, comma-separated, e.g. U0123ABCD): ")) ?? "";
134
+ }
135
+ // Final validation — in both interactive and non-interactive modes the
136
+ // required tokens must be present and prefix-shaped, otherwise refuse to
137
+ // write a config that wouldn't pass `gateway doctor`.
138
+ const botToken = collected.slackBotToken.trim();
139
+ const appToken = collected.slackAppToken.trim();
140
+ if (!botToken || !botToken.startsWith(BOT_TOKEN_PREFIX)) {
141
+ return {
142
+ ok: false,
143
+ path,
144
+ reason: `Slack BOT token is required and must start with "${BOT_TOKEN_PREFIX}".`,
145
+ };
146
+ }
147
+ if (!appToken || !appToken.startsWith(APP_TOKEN_PREFIX)) {
148
+ return {
149
+ ok: false,
150
+ path,
151
+ reason: `Slack APP-LEVEL token is required and must start with "${APP_TOKEN_PREFIX}".`,
152
+ };
153
+ }
154
+ const session = collected.copilotTmuxSession.trim();
155
+ const users = collected.slackAllowedUsers.trim();
156
+ const content = renderEnvFile({
157
+ botToken,
158
+ appToken,
159
+ session: session || undefined,
160
+ users: users || undefined,
161
+ });
162
+ mkdirSync(dirname(path), { recursive: true });
163
+ // Atomic, perm-safe write. mkdtempSync creates a brand-new sibling dir with
164
+ // a unique suffix (mode 0o700 on POSIX), so the temp file inside is always
165
+ // freshly created — `{ mode: 0o600 }` is guaranteed to apply, and there's
166
+ // no chance of stomping a pre-existing temp file with stale perms. The
167
+ // final rename then atomically installs the 0o600 file into place.
168
+ let tmpDir = null;
169
+ let tmpFile = null;
170
+ try {
171
+ tmpDir = mkdtempSync(join(dirname(path), ".env-init-"));
172
+ tmpFile = join(tmpDir, "env");
173
+ writeFileSync(tmpFile, content, { mode: 0o600, encoding: "utf8" });
174
+ // Defense-in-depth: confirm the perms we asked for are what we got
175
+ // before we publish the file. On Windows the check is a no-op.
176
+ if (process.platform !== "win32") {
177
+ const mode = statSync(tmpFile).mode & 0o777;
178
+ if (mode !== 0o600) {
179
+ throw new Error(`temp file mode is ${mode.toString(8)}, expected 600`);
180
+ }
181
+ }
182
+ renameSync(tmpFile, path);
183
+ }
184
+ catch (err) {
185
+ const msg = err instanceof Error ? err.message : String(err);
186
+ return { ok: false, path, reason: `failed to write ${path}: ${msg}` };
187
+ }
188
+ finally {
189
+ if (tmpDir) {
190
+ try {
191
+ rmSync(tmpDir, { recursive: true, force: true });
192
+ }
193
+ catch {
194
+ /* best effort */
195
+ }
196
+ }
197
+ }
198
+ // POSIX: file was created with mode 0o600 and that survives the rename.
199
+ // Windows: mode bits are largely meaningless. Either way, we never end up
200
+ // in a state where we'd need to apologize for the perms — if anything went
201
+ // wrong above we'd have returned the error already.
202
+ const lockedDown = process.platform !== "win32";
203
+ if (interactive) {
204
+ io.print("");
205
+ io.print(lockedDown ? `Wrote ${path} (chmod 600).` : `Wrote ${path}.`);
206
+ io.print("");
207
+ io.print("Next:");
208
+ io.print(" 1. start a Copilot tmux session if one isn't running already");
209
+ io.print(" (any `omp-<digits>` name; e.g. `tmux new-session -d -s omp-9999`)");
210
+ io.print(" 2. `omp gateway status` — should report ready=true");
211
+ io.print(" 3. `omp gateway serve` — blocks; ^C to stop");
212
+ io.print("");
213
+ }
214
+ return { ok: true, path };
215
+ }
216
+ function renderEnvFile(k) {
217
+ const lines = [
218
+ "# Written by `omp env init`. Edit by hand or re-run the command.",
219
+ "# Precedence: shell exports always win over values in this file.",
220
+ "",
221
+ `SLACK_BOT_TOKEN=${k.botToken}`,
222
+ `SLACK_APP_TOKEN=${k.appToken}`,
223
+ ];
224
+ if (k.session)
225
+ lines.push(`COPILOT_TMUX_SESSION=${k.session}`);
226
+ if (k.users)
227
+ lines.push(`SLACK_ALLOWED_USERS=${k.users}`);
228
+ lines.push("");
229
+ return lines.join("\n");
230
+ }
231
+ async function promptForToken(io, label, prefix) {
232
+ for (let attempt = 0; attempt < 3; attempt++) {
233
+ const value = ((await io.ask(`${label} (starts with ${prefix}): `)) ?? "").trim();
234
+ if (value.startsWith(prefix))
235
+ return value;
236
+ if (!value) {
237
+ io.print(` ${label} is required.`);
238
+ continue;
239
+ }
240
+ io.print(` That doesn't look like a ${label.toLowerCase()} (expected to start with "${prefix}").`);
241
+ }
242
+ // Caller's final validation will reject — we don't throw here so the call
243
+ // site can return a structured error.
244
+ return "";
245
+ }
246
+ /** Read an existing file and return user-visible lines with values masked. */
247
+ function readExistingMasked(path) {
248
+ try {
249
+ return readFileSync(path, "utf8")
250
+ .split(/\r?\n/)
251
+ .filter((l) => l.trim() && !l.trim().startsWith("#"))
252
+ .map((line) => {
253
+ const i = line.indexOf("=");
254
+ if (i === -1)
255
+ return line;
256
+ const key = line.slice(0, i);
257
+ const val = line.slice(i + 1);
258
+ return `${key}=${maskValue(val)}`;
259
+ });
260
+ }
261
+ catch {
262
+ return ["(could not read existing file)"];
263
+ }
264
+ }
265
+ function maskValue(v) {
266
+ const trimmed = v.trim();
267
+ if (trimmed.length <= 4)
268
+ return "****";
269
+ // Show prefix (e.g. xoxb-) + 3 stars + last 4 chars to confirm identity
270
+ // without leaking the bulk of the secret.
271
+ const dashIndex = trimmed.indexOf("-");
272
+ const prefix = dashIndex >= 0 ? trimmed.slice(0, dashIndex + 1) : "";
273
+ const tail = trimmed.slice(-4);
274
+ return `${prefix}***${tail}`;
275
+ }
276
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/env/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A Gateway Connector is a long-lived event source that the gateway runtime
3
+ * starts, watches for readiness, and stops cleanly on shutdown. Connectors are
4
+ * intentionally tiny: name + lifecycle + status snapshot. All transport-specific
5
+ * concerns (Bolt, HTTP, MCP, etc.) live inside the connector implementation.
6
+ *
7
+ * Why not just call `runSlackBot()`? Adding a second connector later (Telegram,
8
+ * Discord, webhook) becomes one file implementing this interface, not a new
9
+ * top-level command and a new lifecycle.
10
+ */
11
+ export interface ConnectorStatus {
12
+ /** True when the connector is connected and ready to handle events. */
13
+ ready: boolean;
14
+ /** Optional human-readable detail — error message, "not started", etc. */
15
+ detail?: string;
16
+ }
17
+ export interface Connector {
18
+ /** Stable identifier, e.g. "slack". Used for `--only` filtering and status output. */
19
+ readonly name: string;
20
+ /** Open whatever long-lived resources are needed (sockets, listeners). */
21
+ start(): Promise<void>;
22
+ /** Close cleanly. Must be idempotent — calling stop() twice is a no-op. */
23
+ stop(): Promise<void>;
24
+ /** Synchronous readiness snapshot — never opens sockets or makes I/O calls. */
25
+ status(): ConnectorStatus;
26
+ }
27
+ /**
28
+ * Static (no-I/O) readiness check for the doctor command. Connectors expose
29
+ * this so `omp gateway status` can answer "is this startable?" without
30
+ * actually starting it.
31
+ */
32
+ export interface ConnectorDoctor {
33
+ /** Same name as the Connector. */
34
+ readonly name: string;
35
+ /** Returns a snapshot reporting whether start() would currently succeed. */
36
+ doctor(): ConnectorStatus;
37
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A Gateway Connector is a long-lived event source that the gateway runtime
3
+ * starts, watches for readiness, and stops cleanly on shutdown. Connectors are
4
+ * intentionally tiny: name + lifecycle + status snapshot. All transport-specific
5
+ * concerns (Bolt, HTTP, MCP, etc.) live inside the connector implementation.
6
+ *
7
+ * Why not just call `runSlackBot()`? Adding a second connector later (Telegram,
8
+ * Discord, webhook) becomes one file implementing this interface, not a new
9
+ * top-level command and a new lifecycle.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=connector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connector.js","sourceRoot":"","sources":["../../../src/gateway/connector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Slack {@link Connector} — wraps the @slack/bolt Socket Mode adapter that
3
+ * used to live in src/slack/serve.ts. Pure handler logic (handler.ts) and
4
+ * config loader (config.ts) are reused unchanged.
5
+ *
6
+ * Lifecycle:
7
+ * start() → instantiate Bolt App, auth.test for botUserId, subscribe to
8
+ * app.message (DMs) + app.event("app_mention"), then app.start().
9
+ * If anything throws, state.error is recorded and status reports
10
+ * not ready — the gateway runtime treats it as a failed start.
11
+ * stop() → idempotent app.stop().
12
+ * status()→ derived from internal state; never opens sockets.
13
+ */
14
+ import type { Connector, ConnectorDoctor } from "../connector.js";
15
+ import type { SlackConfig } from "../../slack/config.js";
16
+ import { type SlackHandlerDeps } from "../../slack/handler.js";
17
+ export interface BoltLike {
18
+ client: {
19
+ auth: {
20
+ test: () => Promise<{
21
+ user_id?: string;
22
+ }>;
23
+ };
24
+ };
25
+ start: () => Promise<unknown>;
26
+ stop: () => Promise<unknown>;
27
+ message: (handler: (args: {
28
+ message: SlackMessage;
29
+ say: SaySig;
30
+ }) => Promise<void>) => void;
31
+ event: (name: "app_mention", handler: (args: {
32
+ event: SlackMessage;
33
+ say: SaySig;
34
+ }) => Promise<void>) => void;
35
+ }
36
+ export type AppFactory = (config: SlackConfig) => BoltLike;
37
+ export type SaySig = (msg: {
38
+ text: string;
39
+ thread_ts?: string;
40
+ }) => Promise<unknown>;
41
+ export interface SlackMessage {
42
+ text?: string;
43
+ user?: string;
44
+ bot_id?: string;
45
+ subtype?: string;
46
+ channel_type?: string;
47
+ thread_ts?: string;
48
+ ts?: string;
49
+ }
50
+ export interface SlackConnectorOptions {
51
+ config: SlackConfig;
52
+ /** Inject an App factory for testing; default lazy-loads @slack/bolt. */
53
+ appFactory?: AppFactory;
54
+ /** Inject handler deps for testing; default wires comms resolveSession + commsAsk. */
55
+ handlerDeps?: Omit<SlackHandlerDeps, "allowedUsers" | "requireMention" | "sessionEnv">;
56
+ /** Logger; defaults to console.error. */
57
+ log?: (msg: string) => void;
58
+ }
59
+ export declare const SLACK_CONNECTOR_NAME = "slack";
60
+ /**
61
+ * Build a Slack {@link Connector}. The factory is sync; all I/O happens in
62
+ * `start()`.
63
+ */
64
+ export declare function createSlackConnector(opts: SlackConnectorOptions): Connector;
65
+ /**
66
+ * Static readiness check (no sockets opened). True when both tokens are
67
+ * present in the loaded config AND a Copilot tmux session can be resolved.
68
+ */
69
+ export declare function slackDoctor(config: SlackConfig | null, errorIfNoConfig?: string): ConnectorDoctor;