k-linkedin-mcp 0.1.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/.dockerignore ADDED
@@ -0,0 +1,15 @@
1
+ .git/
2
+ .agent/
3
+ AGENTS.md
4
+ CLAUDE.md
5
+ GEMINI.md
6
+ COPILOT.md
7
+ VIBE.md
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+ deploy/docker-compose.override.yml
12
+ node_modules/
13
+ coverage/
14
+ *.log
15
+ TODO.md
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,78 @@
1
+ # linkedin-mcp — GitLab CI/CD Pipeline
2
+ # Stages: test → build image → deploy to homelab
3
+
4
+ stages:
5
+ - test
6
+ - build
7
+ - deploy
8
+
9
+ variables:
10
+ IMAGE_NAME: registry.gitlab.com/kpihx-labs/linkedin-mcp
11
+ CONTAINER_NAME: linkedin_mcp
12
+
13
+ # ── Test ──────────────────────────────────────────────────────────────────────
14
+
15
+ test:
16
+ stage: test
17
+ image: oven/bun:1-slim
18
+ script:
19
+ - bun install --frozen-lockfile
20
+ - bun test
21
+ coverage: '/\d+ pass/'
22
+ artifacts:
23
+ reports:
24
+ junit: test-results.xml
25
+ when: always
26
+ expire_in: 7 days
27
+ rules:
28
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
29
+ - if: '$CI_COMMIT_BRANCH == "main"'
30
+ - if: '$CI_COMMIT_BRANCH == "develop"'
31
+
32
+ # ── Build Docker image ────────────────────────────────────────────────────────
33
+
34
+ build:
35
+ stage: build
36
+ image: docker:27
37
+ services:
38
+ - docker:27-dind
39
+ before_script:
40
+ - echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
41
+ script:
42
+ - docker build -t $IMAGE_NAME:$CI_COMMIT_SHORT_SHA -t $IMAGE_NAME:latest .
43
+ - docker push $IMAGE_NAME:$CI_COMMIT_SHORT_SHA
44
+ - docker push $IMAGE_NAME:latest
45
+ rules:
46
+ - if: '$CI_COMMIT_BRANCH == "main"'
47
+
48
+ # ── Deploy to Homelab ─────────────────────────────────────────────────────────
49
+
50
+ deploy:
51
+ stage: deploy
52
+ image: alpine:3.20
53
+ before_script:
54
+ - apk add --no-cache openssh-client
55
+ - mkdir -p ~/.ssh
56
+ - echo "$HOMELAB_SSH_KEY" > ~/.ssh/id_ed25519
57
+ - chmod 600 ~/.ssh/id_ed25519
58
+ - ssh-keyscan -H $HOMELAB_HOST >> ~/.ssh/known_hosts
59
+ script:
60
+ - |
61
+ ssh -i ~/.ssh/id_ed25519 kpihx@$HOMELAB_HOST "
62
+ echo '$CI_REGISTRY_PASSWORD' | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
63
+ docker pull $IMAGE_NAME:latest
64
+ cd ~/stacks/linkedin-mcp && docker compose pull && docker compose up -d --remove-orphans
65
+ docker image prune -f
66
+ "
67
+ environment:
68
+ name: homelab
69
+ url: https://linkedin.kpihx-labs.com
70
+ rules:
71
+ - if: '$CI_COMMIT_BRANCH == "main"'
72
+ needs: [build]
73
+
74
+ # ── Variables to define in GitLab CI/CD Settings → Variables ─────────────────
75
+ # HOMELAB_SSH_KEY — private SSH key for homelab deploy user (masked, protected)
76
+ # HOMELAB_HOST — homelab IP/hostname reachable from CI runner (masked)
77
+ # CI_REGISTRY_USER — GitLab container registry user (auto)
78
+ # CI_REGISTRY_PASSWORD — GitLab registry password / deploy token (masked)
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog — linkedin-mcp
2
+
3
+ ---
4
+
5
+ ## [0.1.0] — 2026-03-22
6
+
7
+ ### Added
8
+ - [x] Full MCP server — 8 tools: guide, auth_status, get_profile, create_post, create_image_post, delete_post, like_post, create_comment
9
+ - [x] LinkedIn OAuth 2.0 + OIDC flow (`linkedin-admin auth`)
10
+ - [x] Three-level admin: CLI (`linkedin-admin`) · HTTP (`/admin/*`) · Telegram bot
11
+ - [x] Dual transport: stdio + Streamable HTTP (Express, port 8095)
12
+ - [x] Token lifecycle management — 60-day expiry tracking, `tokenSummary` with days_left
13
+ - [x] Test suite — 41 tests, 5 files, 0 live API calls (mocked fetch + temp dirs)
14
+ - [x] Docker image (`oven/bun:1-slim`) + `deploy/docker-compose.yml` with Traefik labels
15
+ - [x] GitLab CI — test → build → deploy pipeline
16
+ - [x] Editable install via `bun link`
package/Dockerfile ADDED
@@ -0,0 +1,27 @@
1
+ FROM oven/bun:1-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json bun.lock* ./
6
+ RUN bun install --frozen-lockfile --production
7
+
8
+ COPY src ./src
9
+ COPY config.json ./config.json
10
+ COPY README.md ./README.md
11
+ COPY CHANGELOG.md ./CHANGELOG.md
12
+
13
+ RUN chmod +x /app/src/main.js /app/src/admin.js /app/src/admin/cli.js \
14
+ && ln -sf /app/src/main.js /usr/local/bin/linkedin-mcp \
15
+ && ln -sf /app/src/admin.js /usr/local/bin/linkedin-admin
16
+
17
+ ENV NODE_ENV=production
18
+ ENV LINKEDIN_STATE_DIR=/data/state
19
+ ENV LINKEDIN_MCP_HTTP_HOST=0.0.0.0
20
+ ENV LINKEDIN_MCP_HTTP_PORT=8095
21
+ ENV LINKEDIN_MCP_HTTP_PATH=/mcp
22
+
23
+ VOLUME ["/data"]
24
+
25
+ EXPOSE 8095
26
+
27
+ CMD ["bun", "src/main.js", "serve-http"]
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # linkedin-mcp
2
+
3
+ > **0 Trust · 100% Control | 0 Magic · 100% Transparency | 0 Hardcoding · 100% Flexibility**
4
+
5
+ A **Model Context Protocol (MCP) server** for LinkedIn — official OAuth 2.0, intent-first tool surface, three-level admin (CLI · HTTP · Telegram). Designed for the KpihX homelab stack; deployment-ready with Docker + Traefik.
6
+
7
+ **Repos:** [GitHub](https://github.com/KpihX/linkedin-mcp) · [GitLab](https://gitlab.com/kpihx-labs/linkedin-mcp)
8
+
9
+ ---
10
+
11
+ ## Why this exists
12
+
13
+ LinkedIn has no general-purpose developer API for personal accounts. The official OAuth surface (`openid profile email w_member_social`) covers everything an individual power-user needs:
14
+
15
+ - Post, delete, like, comment on content
16
+ - Read own profile and auth status
17
+ - Guide agents through the flow with `linkedin_guide`
18
+
19
+ This MCP wraps that surface with **full operational visibility**: a CLI admin, an HTTP admin API, and an optional Telegram bot — the same architecture as `whats-mcp`, `tick-mcp`, `mail-mcp`.
20
+
21
+ ---
22
+
23
+ ## Architecture
24
+
25
+ ```
26
+ linkedin-mcp/
27
+ ├── src/
28
+ │ ├── main.js ← entry: serve (stdio) | serve-http
29
+ │ ├── admin.js ← entry: CLI admin (linkedin-admin)
30
+ │ ├── config.js ← loadConfig() — deep merge defaults → config.json → env
31
+ │ ├── token.js ← save / load / clear / summary (~/.mcps/linkedin/token.json)
32
+ │ ├── client.js ← LinkedInClient — official REST API calls
33
+ │ ├── server.js ← createMcpServer() — MCP SDK wiring
34
+ │ ├── http_app.js ← Express HTTP app: /health /admin/* /mcp
35
+ │ ├── .env.example ← required secrets template
36
+ │ └── admin/
37
+ │ ├── cli.js ← Commander CLI (auth / status / logout / logs / health / urls / guide)
38
+ │ ├── service.js ← shared text helpers (statusSummaryText, healthSummaryText, …)
39
+ │ ├── oauth.js ← LinkedIn OAuth 2.0 + OIDC flow
40
+ │ └── telegram.js ← optional Telegram bot admin
41
+ ├── tools/
42
+ │ ├── registry.js ← listTools() + callTool()
43
+ │ ├── guide.js ← linkedin_guide
44
+ │ ├── profile.js ← linkedin_get_profile, linkedin_auth_status
45
+ │ ├── posts.js ← linkedin_create_post, linkedin_create_image_post, linkedin_delete_post
46
+ │ └── social.js ← linkedin_like_post, linkedin_create_comment
47
+ ├── tests/ ← bun test (41 tests, 0 deps on live API)
48
+ ├── deploy/
49
+ │ ├── docker-compose.yml
50
+ │ └── docker-compose.override.example.yml
51
+ ├── Dockerfile
52
+ ├── .gitlab-ci.yml
53
+ └── config.json ← non-secret defaults (port 8095, state dir, scopes)
54
+ ```
55
+
56
+ **Transport strategy:**
57
+
58
+ ```
59
+ Agent / Claude / Gemini
60
+
61
+ ├─── HTTP (homelab) ──→ https://linkedin.kpihx-labs.com/mcp (primary)
62
+ └─── stdio (local) ──→ ~/.bun/bin/linkedin-mcp serve (fallback)
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Tools (8)
68
+
69
+ | Tool | Description |
70
+ |------|-------------|
71
+ | `linkedin_guide` | Orientation — all tools, auth flow, quick start |
72
+ | `linkedin_auth_status` | Token presence, validity, days remaining, profile |
73
+ | `linkedin_get_profile` | Fetch own LinkedIn profile (name, email, sub) |
74
+ | `linkedin_create_post` | Create a text post (PUBLIC or CONNECTIONS) |
75
+ | `linkedin_create_image_post` | Create a post with an attached image |
76
+ | `linkedin_delete_post` | Delete a post by URN |
77
+ | `linkedin_like_post` | Like a post by URN |
78
+ | `linkedin_create_comment` | Add a comment to a post by URN |
79
+
80
+ ---
81
+
82
+ ## Admin interfaces
83
+
84
+ ### CLI (`linkedin-admin`)
85
+
86
+ ```bash
87
+ linkedin-admin auth # Run OAuth flow — opens browser, saves token
88
+ linkedin-admin status # Token status + profile
89
+ linkedin-admin status --json # Machine-readable JSON
90
+ linkedin-admin logout # Clear saved token
91
+ linkedin-admin logs --lines 50
92
+ linkedin-admin health # HTTP server reachability
93
+ linkedin-admin urls # All public/private endpoints
94
+ linkedin-admin guide # Full help text
95
+ ```
96
+
97
+ ### HTTP admin
98
+
99
+ | Endpoint | Description |
100
+ |----------|-------------|
101
+ | `GET /health` | Liveness probe |
102
+ | `GET /admin/status` | Token status + Telegram runtime state |
103
+ | `GET /admin/help` | All commands reference |
104
+ | `GET /admin/logs?lines=50` | Recent admin log tail |
105
+ | `POST /mcp` | MCP Streamable HTTP transport |
106
+
107
+ ### Telegram bot (optional)
108
+
109
+ Set `TELEGRAM_LINKEDIN_TOKEN` + `TELEGRAM_CHAT_IDS` and the server polls every 5 s.
110
+
111
+ | Command | Action |
112
+ |---------|--------|
113
+ | `/start` `/help` | Full command reference |
114
+ | `/status` | Token + service status |
115
+ | `/health` | HTTP server health |
116
+ | `/urls` | Endpoint map |
117
+ | `/logs [n]` | Last n admin log lines (default 20) |
118
+
119
+ ---
120
+
121
+ ## Quick start
122
+
123
+ ### 1. LinkedIn App setup
124
+
125
+ 1. Go to [LinkedIn Developer Portal](https://developer.linkedin.com)
126
+ 2. Create an app → add products: **"Sign In with LinkedIn using OpenID Connect"** + **"Share on LinkedIn"**
127
+ 3. Set redirect URI: `http://localhost:3000/callback`
128
+ 4. Note `Client ID` and `Client Secret`
129
+
130
+ ### 2. Configure secrets
131
+
132
+ ```bash
133
+ cp src/.env.example .env
134
+ # Fill in LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET
135
+ ```
136
+
137
+ ### 3. Install & authenticate
138
+
139
+ ```bash
140
+ bun install
141
+ bun link # editable install: ~/.bun/bin/linkedin-mcp + linkedin-admin
142
+
143
+ linkedin-admin auth # opens browser → authorizes → saves token
144
+ linkedin-admin status # verify token is valid
145
+ ```
146
+
147
+ ### 4. Start MCP server
148
+
149
+ ```bash
150
+ # stdio (for agent config)
151
+ linkedin-mcp serve
152
+
153
+ # HTTP (for homelab deployment)
154
+ linkedin-mcp serve-http
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Docker / Homelab deployment
160
+
161
+ ```bash
162
+ cp deploy/docker-compose.yml docker-compose.yml
163
+ cp deploy/docker-compose.override.example.yml docker-compose.override.yml
164
+ # Edit docker-compose.override.yml with your secrets
165
+
166
+ docker compose up -d
167
+ ```
168
+
169
+ Traefik labels in `deploy/docker-compose.yml` expose:
170
+ - `https://linkedin.kpihx-labs.com` (primary private trusted route)
171
+ - `https://linkedin.homelab` (fallback)
172
+
173
+ ---
174
+
175
+ ## Agent registration
176
+
177
+ ### Claude Code (`~/.claude.json`)
178
+
179
+ ```json
180
+ "linkedin-mcp": {
181
+ "type": "http",
182
+ "url": "https://linkedin.kpihx-labs.com/mcp"
183
+ },
184
+ "linkedin-mcp--fallback": {
185
+ "command": "/home/kpihx/.bun/bin/linkedin-mcp",
186
+ "args": ["serve"]
187
+ }
188
+ ```
189
+
190
+ ### Gemini (`~/.gemini/extensions/linkedin_mcp/gemini-extension.json`)
191
+
192
+ ```json
193
+ {
194
+ "name": "linkedin-mcp",
195
+ "version": "0.1.0",
196
+ "mcpServers": {
197
+ "linkedin-mcp": {
198
+ "httpUrl": "https://linkedin.kpihx-labs.com/mcp"
199
+ },
200
+ "linkedin-mcp--fallback": {
201
+ "command": "/home/kpihx/.bun/bin/linkedin-mcp",
202
+ "args": ["serve"]
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Tests
211
+
212
+ ```bash
213
+ bun test # 41 tests, 5 files, 0 live API calls
214
+ bun test --watch # watch mode
215
+ ```
216
+
217
+ All tests use mocked `fetch` and temp directories — no credentials needed.
218
+
219
+ ---
220
+
221
+ ## Token lifecycle
222
+
223
+ LinkedIn personal OAuth tokens expire in **60 days**. There is no refresh token for non-Partner apps.
224
+
225
+ ```
226
+ Day 0 → linkedin-admin auth (OAuth flow, browser consent)
227
+ Day 55+ → linkedin-admin status (shows days_left, warns if < 7)
228
+ Day 60 → token invalid, re-run auth
229
+ ```
230
+
231
+ ---
232
+
233
+ ## License
234
+
235
+ MIT — see [LICENSE](LICENSE).
package/config.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "server": {
3
+ "state_directory": "~/.mcps/linkedin",
4
+ "http_host": "127.0.0.1",
5
+ "http_port": 8095,
6
+ "http_mcp_path": "/mcp",
7
+ "public_base_url": "https://linkedin.kpihx-labs.com",
8
+ "fallback_base_url": "https://linkedin.homelab"
9
+ },
10
+ "oauth": {
11
+ "redirect_port": 3000,
12
+ "redirect_uri": "http://localhost:3000/callback",
13
+ "scopes": ["openid", "profile", "email", "w_member_social"]
14
+ },
15
+ "logging": {
16
+ "level": "error"
17
+ }
18
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "k-linkedin-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for LinkedIn — intent-first posting, profile management, and professional networking over stdio and HTTP.",
5
+ "main": "src/main.js",
6
+ "bin": {
7
+ "linkedin-mcp": "src/main.js",
8
+ "linkedin-admin": "src/admin.js"
9
+ },
10
+ "scripts": {
11
+ "start": "bun src/main.js serve",
12
+ "serve:http": "bun src/main.js serve-http",
13
+ "admin": "bun src/admin.js",
14
+ "auth": "bun src/admin.js auth",
15
+ "status": "bun src/admin.js status",
16
+ "test": "bun test",
17
+ "test:watch": "bun test --watch"
18
+ },
19
+ "keywords": [
20
+ "linkedin",
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "oauth",
24
+ "professional-network",
25
+ "automation"
26
+ ],
27
+ "author": "Ivann KAMDEM <kapoivha@gmail.com>",
28
+ "license": "MIT",
29
+ "type": "commonjs",
30
+ "engines": {
31
+ "bun": ">=1.0.0",
32
+ "node": ">=18.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.26.0",
36
+ "commander": "^13.1.0",
37
+ "express": "^5.2.1",
38
+ "pino": "^10.3.1"
39
+ },
40
+ "devDependencies": {}
41
+ }
@@ -0,0 +1,17 @@
1
+ # LinkedIn OAuth credentials — register your app at https://developer.linkedin.com
2
+ # Products to add: "Sign In with LinkedIn using OpenID Connect" + "Share on LinkedIn"
3
+ LINKEDIN_CLIENT_ID=your_client_id_here
4
+ LINKEDIN_CLIENT_SECRET=your_client_secret_here
5
+
6
+ # Optional: pre-set access token (bypasses oauth flow). Populated by: linkedin-admin auth
7
+ # LINKEDIN_ACCESS_TOKEN=your_access_token_here
8
+
9
+ # Optional: Telegram admin bot
10
+ # TELEGRAM_LINKEDIN_TOKEN=your_bot_token_here (from @BotFather)
11
+ # TELEGRAM_CHAT_IDS=123456789,987654321 (comma-separated chat IDs)
12
+
13
+ # Optional server overrides
14
+ # LINKEDIN_MCP_HTTP_HOST=0.0.0.0
15
+ # LINKEDIN_MCP_HTTP_PORT=8095
16
+ # LINKEDIN_MCP_LOG_LEVEL=info
17
+ # LINKEDIN_STATE_DIR=~/.mcps/linkedin
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * linkedin-mcp — Admin CLI (linkedin-admin).
4
+ *
5
+ * Commands:
6
+ * auth Start OAuth 2.0 flow (opens browser, saves token)
7
+ * status [--json] Show token + member info
8
+ * logout Clear stored token
9
+ * logs [--lines N] Tail the admin log
10
+ * health Show HTTP endpoint URLs
11
+ * urls Show all public URLs
12
+ * guide Show admin capabilities
13
+ */
14
+
15
+ "use strict";
16
+
17
+ const { Command } = require("commander");
18
+
19
+ const { loadConfig } = require("../config");
20
+ const { tokenSummary, clearToken } = require("../token");
21
+ const { runOAuthFlow } = require("./oauth");
22
+ const {
23
+ adminHelpText,
24
+ getLogsText,
25
+ healthSummaryText,
26
+ statusSummaryText,
27
+ urlsSummary,
28
+ appendAdminLog,
29
+ } = require("./service");
30
+
31
+ const PKG = require("../../package.json");
32
+
33
+ const program = new Command();
34
+ program
35
+ .name("linkedin-admin")
36
+ .description("linkedin-mcp administration: auth, status, logout, logs")
37
+ .version(PKG.version);
38
+
39
+ // ── auth ─────────────────────────────────────────────────────────────────────
40
+
41
+ program
42
+ .command("auth")
43
+ .description("Start LinkedIn OAuth 2.0 flow. Opens a browser for authorization.")
44
+ .action(async () => {
45
+ const config = loadConfig();
46
+ if (!config.oauth.client_id || !config.oauth.client_secret) {
47
+ console.error(
48
+ "\nError: LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET are not set.\n" +
49
+ "Steps:\n" +
50
+ " 1. Create app at https://developer.linkedin.com\n" +
51
+ " 2. Add products: 'Sign In with LinkedIn using OpenID Connect' + 'Share on LinkedIn'\n" +
52
+ " 3. Set redirect URI: http://localhost:3000/callback\n" +
53
+ " 4. Store credentials via bw-env: LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET\n",
54
+ );
55
+ process.exit(1);
56
+ }
57
+ console.log("\nStarting LinkedIn OAuth flow...");
58
+ console.log(`Redirect URI : ${config.oauth.redirect_uri}`);
59
+ console.log(`Scopes : ${config.oauth.scopes.join(", ")}\n`);
60
+ try {
61
+ const tokenData = await runOAuthFlow(config, (url) => {
62
+ console.log("Open this URL in your browser:\n");
63
+ console.log(` ${url}\n`);
64
+ const { execSync } = require("child_process");
65
+ try { execSync(`xdg-open "${url}"`, { stdio: "ignore" }); } catch { /* silent */ }
66
+ });
67
+ appendAdminLog(`auth success member=${tokenData.member_id} name=${tokenData.name}`);
68
+ console.log(`\nAuthenticated: ${tokenData.name || "LinkedIn member"}`);
69
+ console.log(`Email : ${tokenData.email || "N/A"}`);
70
+ console.log(`Expires in : ${Math.floor((tokenData.expires_in || 5184000) / 86400)} days`);
71
+ console.log("\nToken saved. linkedin-mcp is ready.\n");
72
+ } catch (err) {
73
+ appendAdminLog(`auth error ${err.message}`);
74
+ console.error(`\nAuth failed: ${err.message}\n`);
75
+ process.exit(1);
76
+ }
77
+ });
78
+
79
+ // ── status ────────────────────────────────────────────────────────────────────
80
+
81
+ program
82
+ .command("status")
83
+ .description("Show current authentication status and token details.")
84
+ .option("--json", "Output raw JSON")
85
+ .action((opts) => {
86
+ const config = loadConfig();
87
+ const summary = tokenSummary(config);
88
+ if (opts.json) {
89
+ console.log(JSON.stringify(summary, null, 2));
90
+ } else {
91
+ console.log(`\n${statusSummaryText()}\n`);
92
+ }
93
+ });
94
+
95
+ // ── logout ────────────────────────────────────────────────────────────────────
96
+
97
+ program
98
+ .command("logout")
99
+ .description("Clear the stored LinkedIn access token.")
100
+ .action(() => {
101
+ const config = loadConfig();
102
+ clearToken(config);
103
+ appendAdminLog("logout: token cleared");
104
+ console.log("\nToken cleared. Run 'linkedin-admin auth' to re-authenticate.\n");
105
+ });
106
+
107
+ // ── logs ──────────────────────────────────────────────────────────────────────
108
+
109
+ program
110
+ .command("logs")
111
+ .description("Show recent admin log lines.")
112
+ .option("--lines <n>", "Number of lines to show", "50")
113
+ .action((opts) => {
114
+ const limit = parseInt(opts.lines, 10) || 50;
115
+ console.log(getLogsText(limit));
116
+ });
117
+
118
+ // ── health ────────────────────────────────────────────────────────────────────
119
+
120
+ program
121
+ .command("health")
122
+ .description("Show HTTP endpoint URLs and health check info.")
123
+ .action(() => {
124
+ console.log(`\n${healthSummaryText()}\n`);
125
+ });
126
+
127
+ // ── urls ──────────────────────────────────────────────────────────────────────
128
+
129
+ program
130
+ .command("urls")
131
+ .description("Show all public and fallback endpoint URLs.")
132
+ .action(() => {
133
+ console.log(`\n${urlsSummary()}\n`);
134
+ });
135
+
136
+ // ── guide ─────────────────────────────────────────────────────────────────────
137
+
138
+ program
139
+ .command("guide")
140
+ .description("Show full admin capabilities (CLI + HTTP + Telegram).")
141
+ .action(() => {
142
+ console.log(`\n${adminHelpText()}\n`);
143
+ });
144
+
145
+ module.exports = { program };