@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.
- package/.github/skills/team/SKILL.md +82 -57
- package/.github/skills/team/scripts/team-launch.sh +140 -42
- package/README.md +31 -0
- package/catalog/skills-general.json +2 -2
- package/dist/src/cli.d.ts +1 -7
- package/dist/src/cli.js +355 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/registry.d.ts +3 -0
- package/dist/src/commands/registry.js +11 -0
- package/dist/src/commands/registry.js.map +1 -0
- package/dist/src/commands/suggest.d.ts +19 -0
- package/dist/src/commands/suggest.js +158 -0
- package/dist/src/commands/suggest.js.map +1 -0
- package/dist/src/commands/types.d.ts +16 -0
- package/dist/src/commands/types.js +2 -0
- package/dist/src/commands/types.js.map +1 -0
- package/dist/src/env/dotenv.d.ts +41 -0
- package/dist/src/env/dotenv.js +112 -0
- package/dist/src/env/dotenv.js.map +1 -0
- package/dist/src/env/init.d.ts +50 -0
- package/dist/src/env/init.js +276 -0
- package/dist/src/env/init.js.map +1 -0
- package/dist/src/gateway/connector.d.ts +37 -0
- package/dist/src/gateway/connector.js +12 -0
- package/dist/src/gateway/connector.js.map +1 -0
- package/dist/src/gateway/connectors/slack.d.ts +69 -0
- package/dist/src/gateway/connectors/slack.js +159 -0
- package/dist/src/gateway/connectors/slack.js.map +1 -0
- package/dist/src/gateway/registry.d.ts +29 -0
- package/dist/src/gateway/registry.js +37 -0
- package/dist/src/gateway/registry.js.map +1 -0
- package/dist/src/gateway/runtime.d.ts +58 -0
- package/dist/src/gateway/runtime.js +105 -0
- package/dist/src/gateway/runtime.js.map +1 -0
- package/dist/src/instructions-memory.js +8 -15
- package/dist/src/instructions-memory.js.map +1 -1
- package/dist/src/jira.js +2 -14
- package/dist/src/jira.js.map +1 -1
- package/dist/src/slack/config.d.ts +32 -0
- package/dist/src/slack/config.js +52 -0
- package/dist/src/slack/config.js.map +1 -0
- package/dist/src/slack/handler.d.ts +48 -0
- package/dist/src/slack/handler.js +68 -0
- package/dist/src/slack/handler.js.map +1 -0
- package/dist/src/slack/serve.d.ts +8 -0
- package/dist/src/slack/serve.js +7 -0
- package/dist/src/slack/serve.js.map +1 -0
- package/dist/src/team/index.d.ts +1 -0
- package/dist/src/team/index.js +1 -0
- package/dist/src/team/index.js.map +1 -1
- package/dist/src/team/pane-monitor.d.ts +39 -0
- package/dist/src/team/pane-monitor.js +128 -0
- package/dist/src/team/pane-monitor.js.map +1 -0
- package/dist/src/team/runtime.js +12 -1
- package/dist/src/team/runtime.js.map +1 -1
- package/dist/src/team/tmux.d.ts +13 -0
- package/dist/src/team/tmux.js +47 -0
- package/dist/src/team/tmux.js.map +1 -1
- package/docs/slack-setup.md +144 -0
- 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;
|