@cloudgrid-io/mcp 0.19.2 → 0.20.2

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 (38) hide show
  1. package/README.md +35 -25
  2. package/package.json +2 -1
  3. package/src/auth.js +1 -1
  4. package/src/cli-shim.mjs +1 -1
  5. package/src/corpus/capability-map.md +3 -2
  6. package/src/corpus/cli-reference.md +19 -19
  7. package/src/corpus/cookbook.md +10 -10
  8. package/src/corpus/install-for-agents.md +5 -5
  9. package/src/corpus/install.md +5 -5
  10. package/src/corpus/rules/publish-fallbacks.md +2 -2
  11. package/src/corpus/skill-brain.md +6 -6
  12. package/src/corpus/skill-claim.md +1 -1
  13. package/src/corpus/skill-feedback.md +6 -6
  14. package/src/corpus/skill-init.md +8 -8
  15. package/src/corpus/skill-login.md +3 -3
  16. package/src/corpus/skill-logs.md +5 -5
  17. package/src/corpus/skill-plug.md +9 -9
  18. package/src/corpus/skill-share.md +6 -6
  19. package/src/corpus/skills-readme.md +2 -2
  20. package/src/corpus/templates/docs-app/astro.config.mjs +42 -0
  21. package/src/corpus/templates/docs-app/cloudgrid.yaml +24 -0
  22. package/src/corpus/templates/docs-app/index.md +40 -0
  23. package/src/corpus/templates/docs-app/package.json +19 -0
  24. package/src/corpus/templates/docs-app/src/content/docs/guides/getting-started.md +18 -0
  25. package/src/corpus/templates/docs-app/src/content/docs/index.mdx +22 -0
  26. package/src/corpus/templates/docs-app/src/content/docs/reference/overview.md +13 -0
  27. package/src/corpus/templates/docs-app/src/content.config.ts +7 -0
  28. package/src/corpus/templates/docs-app/tsconfig.json +5 -0
  29. package/src/corpus/troubleshooting/deploy-errors.md +1 -1
  30. package/src/corpus/usage.md +1 -1
  31. package/src/corpus/workflows/app-with-data.md +1 -1
  32. package/src/corpus/workflows/docs-app.md +120 -0
  33. package/src/corpus/workflows/docs-site.md +1 -1
  34. package/src/index.js +29 -0
  35. package/src/log-sink.js +45 -0
  36. package/src/session-logger.js +256 -0
  37. package/src/tools.js +61 -46
  38. package/src/web.js +15 -0
@@ -4,7 +4,7 @@ name: share
4
4
  description: |
5
5
  Make a CloudGrid entity shareable and print its URL. Use when the user wants to
6
6
  share a link, make something public, send an app to someone, or get a shareable
7
- URL. Wraps cloudgrid visibility.
7
+ URL. Wraps grid visibility.
8
8
  argument-hint: "[name]"
9
9
  allowed-tools: Bash
10
10
  ---
@@ -12,12 +12,12 @@ allowed-tools: Bash
12
12
  # CloudGrid Share
13
13
 
14
14
  Set an entity's visibility so others can reach it, then print the URL. Wraps
15
- `cloudgrid visibility set`.
15
+ `grid visibility set`.
16
16
 
17
17
  ## Step 0 — Bootstrap
18
18
 
19
- 1. If `cloudgrid` is not on `$PATH`: `npm install -g @cloudgrid-io/cli`
20
- 2. If `cloudgrid whoami` fails: ask the user to run `cloudgrid login`. Wait for
19
+ 1. If `grid` is not on `$PATH`: `npm install -g @cloudgrid-io/cli`
20
+ 2. If `grid whoami` fails: ask the user to run `grid login`. Wait for
21
21
  confirmation.
22
22
 
23
23
  ## UX rules
@@ -31,14 +31,14 @@ Set an entity's visibility so others can reach it, then print the URL. Wraps
31
31
  To make an entity reachable by anyone with the link:
32
32
 
33
33
  ```
34
- cloudgrid visibility set my-thing link
34
+ grid visibility set my-thing link
35
35
  ```
36
36
 
37
37
  The command prints the outlet URL. Hand that URL to the user.
38
38
 
39
39
  ## Visibility modes
40
40
 
41
- `cloudgrid visibility set <slug> <mode>` accepts:
41
+ `grid visibility set <slug> <mode>` accepts:
42
42
 
43
43
  - `link` — anyone with the URL can open it. Use this for sharing.
44
44
  - `private` — only the owner.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Multi-agent skills for CloudGrid. Install them in Claude Code, Codex, or Cursor,
4
4
  and your agent can build, ship, and run things on CloudGrid through the
5
- `cloudgrid` CLI.
5
+ `grid` CLI.
6
6
 
7
7
  ## Why CloudGrid
8
8
 
@@ -22,7 +22,7 @@ behalf: scaffold, deploy, tail logs, share, read feedback. Everything here is fr
22
22
  2. Log in:
23
23
 
24
24
  ```
25
- cloudgrid login
25
+ grid login
26
26
  ```
27
27
 
28
28
  3. Add the agent-core. In Claude Code (recommended):
@@ -0,0 +1,42 @@
1
+ // @ts-check
2
+ import { defineConfig } from 'astro/config';
3
+ import starlight from '@astrojs/starlight';
4
+ import starlightLlmsTxt from 'starlight-llms-txt';
5
+
6
+ // A docs-app is served at the entity ROOT (e.g. https://my-docs--<grid>.cloudgrid.io/),
7
+ // so there is NO `base`. (A base is only needed when a docs site is mounted under
8
+ // a sub-path like /docs — that is a special case, not the default.)
9
+ // https://astro.build/config
10
+ export default defineConfig({
11
+ site: 'https://example.com',
12
+ integrations: [
13
+ starlight({
14
+ // Emit agent-readable docs on every build: /llms.txt (index) and
15
+ // /llms-full.txt (full content). Keeps in sync with the content.
16
+ plugins: [
17
+ starlightLlmsTxt({
18
+ projectName: 'My Project',
19
+ description: 'One-line summary of what this project is — shown to agents at /llms.txt.',
20
+ }),
21
+ ],
22
+ title: 'My Project Docs',
23
+ description: 'Documentation for My Project.',
24
+ social: [
25
+ { icon: 'github', label: 'GitHub', href: 'https://github.com/your-org/your-repo' },
26
+ ],
27
+ sidebar: [
28
+ {
29
+ label: 'Start here',
30
+ items: [
31
+ { label: 'Introduction', slug: 'index' },
32
+ { label: 'Getting started', slug: 'guides/getting-started' },
33
+ ],
34
+ },
35
+ {
36
+ label: 'Reference',
37
+ items: [{ label: 'Overview', slug: 'reference/overview' }],
38
+ },
39
+ ],
40
+ }),
41
+ ],
42
+ });
@@ -0,0 +1,24 @@
1
+ # docs-app — a multi-page documentation SITE (Astro Starlight): sidebar,
2
+ # client-side search, and agent-readable llms.txt / llms-full.txt.
3
+ #
4
+ # It is a static site WITH a build step, served at the entity ROOT. Multi-file +
5
+ # build → deploy is async (like a runtime app) and needs the LOCAL edition
6
+ # (Claude Desktop/Code or the CLI); the hosted MCP cannot build it. Deploy with
7
+ # `grid plug` on the linked folder. `llms.txt` and `llms-full.txt` are generated
8
+ # automatically at the site root.
9
+ #
10
+ # For a quick single-page doc with no build, use the `docs-site` archetype instead.
11
+ name: docs-app
12
+ services:
13
+ site:
14
+ type: static
15
+ path: /
16
+ node_version: "22"
17
+ # source.path: . tells the platform the project is at the folder ROOT (not
18
+ # under services/site/). Required for a single root-level build like this;
19
+ # without it the build looks for services/<name>/ and fails.
20
+ source:
21
+ path: .
22
+ build:
23
+ command: "npm run build"
24
+ output: "dist"
@@ -0,0 +1,40 @@
1
+ # docs-app template
2
+
3
+ A multi-page documentation **site** for CloudGrid, built with
4
+ [Astro Starlight](https://starlight.astro.build): sidebar navigation,
5
+ client-side full-text search, and agent-readable `llms.txt` / `llms-full.txt`
6
+ generated on every build.
7
+
8
+ Use this when the docs are more than one page and you will keep editing them.
9
+ For a quick single-page doc baked into one HTML file (instant, works on the
10
+ hosted MCP), use the `docs-site` archetype instead.
11
+
12
+ ## Layout
13
+
14
+ ```
15
+ package.json Astro + Starlight + starlight-llms-txt
16
+ astro.config.mjs site config, sidebar, the llms.txt plugin
17
+ tsconfig.json
18
+ cloudgrid.yaml static + build block, served at the entity root
19
+ src/content.config.ts
20
+ src/content/docs/
21
+ index.mdx the home page
22
+ guides/getting-started.md
23
+ reference/overview.md
24
+ ```
25
+
26
+ ## Build + deploy
27
+
28
+ This is a **built** static site, so it needs the local edition (Claude
29
+ Desktop/Code) or the CLI — the hosted MCP cannot build it.
30
+
31
+ ```sh
32
+ npm install
33
+ npm run build # produces dist/ (and dist/llms.txt, dist/llms-full.txt)
34
+ grid plug # deploys the folder; live in ~30s at the entity root
35
+ ```
36
+
37
+ `cloudgrid.yaml` declares a single `static` service with a `build:` block
38
+ (`npm run build` → `dist`). The deploy is async — poll to the live URL, then
39
+ return it. Add pages under `src/content/docs/` and list them in the
40
+ `sidebar` in `astro.config.mjs`.
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "docs-app",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "A multi-page documentation site on CloudGrid (Astro Starlight + agent-readable llms.txt).",
6
+ "scripts": {
7
+ "dev": "astro dev",
8
+ "build": "astro build",
9
+ "preview": "astro preview"
10
+ },
11
+ "dependencies": {
12
+ "@astrojs/starlight": "^0.41.3",
13
+ "astro": "^7.0.2",
14
+ "sharp": "^0.34.5"
15
+ },
16
+ "devDependencies": {
17
+ "starlight-llms-txt": "^0.11.0"
18
+ }
19
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: Getting started
3
+ description: Install and run My Project.
4
+ ---
5
+
6
+ Replace this guide with real getting-started steps for your project.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ # however your users install it
12
+ npm install my-project
13
+ ```
14
+
15
+ ## First run
16
+
17
+ Explain the smallest useful thing a reader can do, then link them to the
18
+ [reference](/reference/overview/) for the details.
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: My Project
3
+ description: Documentation for My Project. Replace this with a one-line summary.
4
+ ---
5
+
6
+ import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components';
7
+
8
+ Welcome to the documentation for **My Project**. Replace this page with your own
9
+ introduction: what the project is, who it is for, and where to start.
10
+
11
+ <CardGrid>
12
+ <LinkCard title="Getting started" href="/guides/getting-started/" description="Install and run My Project in a few minutes." />
13
+ <LinkCard title="Reference" href="/reference/overview/" description="Full reference for the API and configuration." />
14
+ </CardGrid>
15
+
16
+ ## What is here
17
+
18
+ - **Guides** — task-oriented walkthroughs.
19
+ - **Reference** — the complete, look-it-up documentation.
20
+
21
+ The sidebar and search box are generated automatically. Add pages under
22
+ `src/content/docs/` and list them in `astro.config.mjs`.
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: Reference overview
3
+ description: The complete reference for My Project.
4
+ ---
5
+
6
+ Replace this with your reference material — API, configuration, CLI flags, and so
7
+ on. Reference pages are for looking things up, not for reading top to bottom.
8
+
9
+ ## Configuration
10
+
11
+ | Option | Default | What it does |
12
+ | --- | --- | --- |
13
+ | `example` | `false` | Replace with a real option. |
@@ -0,0 +1,7 @@
1
+ import { defineCollection } from 'astro:content';
2
+ import { docsLoader } from '@astrojs/starlight/loaders';
3
+ import { docsSchema } from '@astrojs/starlight/schema';
4
+
5
+ export const collections = {
6
+ docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
7
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "include": [".astro/types.d.ts", "**/*"],
4
+ "exclude": ["dist"]
5
+ }
@@ -10,7 +10,7 @@ Match the symptom to the row, apply the fix, and stop. Do not retry permutations
10
10
 
11
11
  | Symptom | Cause | Fix |
12
12
  | --- | --- | --- |
13
- | `SCOPE_INVALID` on deploy | Known platform issue, not your request | Deploy from a terminal with `grid plug <path>` or `cloudgrid plug <path>`. Do not retry flag or path permutations. If no terminal is available, tell the user it is a known issue that needs the CLI. |
13
+ | `SCOPE_INVALID` on deploy | Known platform issue, not your request | Deploy from a terminal with `grid plug <path>`. Do not retry flag or path permutations. If no terminal is available, tell the user it is a known issue that needs the CLI. |
14
14
  | `429` anonymous cap reached | Anonymous deploy quota is exhausted | Stop. Do not log in to work around it and do not retry today. Tell the user the cap is reached; they can try later or sign in for a higher limit as a deliberate choice. |
15
15
  | `409 EDIT_REJECTED` on re-plug | The target entity is archived or expired | Do not silently create a new entity — that mints a new URL and orphans the shared one. Ask the user before creating a fresh entity. |
16
16
  | `401` on an anonymous edit | Missing or stale `owner_token` | Re-plug with the `owner_token` from the last deploy of that entity. Each anonymous edit returns a fresh token — store the new one and use it next time. |
@@ -15,7 +15,7 @@ Which one you use depends on the client. Pick your client below.
15
15
 
16
16
  - **Node 18+** (for `npx`).
17
17
  - **No login needed to try it** — anonymous drop works out of the box. For owned
18
- drops (into your org), run `cloudgrid login` once, or use the `login` skill / the
18
+ drops (into your org), run `grid login` once, or use the `login` skill / the
19
19
  `grid_login` MCP tool.
20
20
 
21
21
  ---
@@ -115,7 +115,7 @@ response is `status: "building"` with a `poll_url` / entity, NOT a live URL yet.
115
115
 
116
116
  Give the user the live app URL — that is the deliverable. To iterate, re-plug
117
117
  the SAME entity (`target_entity_id`) so it updates the same URL. Runtime re-plug
118
- uses the CLI path (`cloudgrid plug` in the linked folder), per the plug
118
+ uses the CLI path (`grid plug` in the linked folder), per the plug
119
119
  description.
120
120
 
121
121
  Keep it honest: async build, local-edition only, credentials injected by the
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: docs-app
3
+ when: multi-page documentation SITE, developer docs portal, knowledge base, help center you'll keep editing — docs with a sidebar, full-text search, and agent-readable llms.txt, built with Astro/Starlight. SEPARATE content files, not one page. For a quick single-page doc baked into one HTML file (instant, works on the hosted MCP), use docs-site instead. For an interactive REST API endpoint reference, use api-docs.
4
+ needs: none
5
+ deploy: static build
6
+ editions: local
7
+ capabilities_note: a built, multi-page static documentation site (Astro Starlight) — sidebar nav, client-side search, and llms.txt / llms-full.txt generated on every build. No infrastructure (no needs:). Because it BUILDS from multiple files, it deploys async and needs the local edition; the hosted MCP cannot build it. Served at the entity root via grid_plug.
8
+ summary: Scaffold an Astro Starlight docs site from the docs-app template, fill in the content, build it, deploy the folder with grid_plug (async → poll to a live URL), then ask the user what visibility they want. llms.txt / llms-full.txt are generated automatically.
9
+ ---
10
+
11
+ # Workflow: docs-app
12
+
13
+ The user wants a **real, multi-page documentation site** they will keep editing —
14
+ a docs portal / knowledge base with a sidebar, search, and agent-readable
15
+ `llms.txt`. That is a **built static site** (Astro Starlight), not a single page.
16
+
17
+ Choose the right archetype first:
18
+ - **Quick, single-page doc**, or you're on the **hosted MCP** (Claude Web) → use
19
+ `docs-site` (one self-contained HTML file, instant inspiration, any edition).
20
+ - **Interactive REST API endpoint reference** → use `api-docs`.
21
+ - **Multi-page site you'll maintain (sidebar + search + llms.txt)** → `docs-app`,
22
+ this workflow.
23
+
24
+ Be honest that a `docs-app` deploy is a build (async, not instant like a single
25
+ HTML drop) and that it needs the local edition.
26
+
27
+ ## 1. Edition check FIRST (hard gate)
28
+
29
+ A docs-app is built (`npm run build`) from multiple files, so it requires the
30
+ **local edition** (Claude Desktop / Claude Code) or the CLI — the grid folder-plugs
31
+ and builds your project.
32
+
33
+ - **Hosted edition (Claude Web / hosted MCP):** you CANNOT build a multi-file
34
+ site. Tell the user plainly: "A multi-page docs site needs the local edition
35
+ (Claude Desktop/Code) or the CloudGrid CLI; the hosted edition can only publish
36
+ a single page — I can make you a one-page `docs-site` instead." Offer the
37
+ `docs-site` fallback; do not try to build here.
38
+ - **Local edition:** continue.
39
+
40
+ ## 2. Auth + grid
41
+
42
+ 1. Ensure signed in: `grid_login_status`; if not, `grid_login`.
43
+ 2. A grid is required. Respect the grid picker: if the user has more than one
44
+ grid, ask which to use; do not assume a target.
45
+
46
+ ## 3. Scaffold
47
+
48
+ `grid_init` an app `<name>`. `init` creates the entity + `.cloudgrid/link.json`
49
+ and writes a `cloudgrid.yaml`. Then fetch the template:
50
+ `grid_fetch("template", "docs-app")` — a minimal Astro Starlight project:
51
+
52
+ ```
53
+ package.json Astro + Starlight + starlight-llms-txt
54
+ astro.config.mjs site config, sidebar, the llms.txt plugin
55
+ tsconfig.json
56
+ cloudgrid.yaml static + build block, served at the entity root
57
+ src/content.config.ts
58
+ src/content/docs/index.mdx + guides/ + reference/
59
+ ```
60
+
61
+ Write those files into the linked folder (keep the project at the folder root —
62
+ this is a static build, so unlike a `node`/`nextjs` service the project is NOT
63
+ nested under `services/<name>/`).
64
+
65
+ ## 4. Fill in the content
66
+
67
+ - Set `title`, `description`, and the `starlightLlmsTxt({ projectName, description })`
68
+ values in `astro.config.mjs` to the user's project.
69
+ - Write the actual pages under `src/content/docs/` (Markdown/MDX, one file per
70
+ page) and list them in the `sidebar` array in `astro.config.mjs`.
71
+ - Keep internal links root-relative (`/guides/getting-started/`). The site is
72
+ served at the entity **root**, so do NOT set a `base`.
73
+ - Do not add external CDN scripts, fonts, or stylesheets.
74
+
75
+ ## 5. cloudgrid.yaml
76
+
77
+ The template already declares the right shape — a single `static` service with a
78
+ build step, served at the root:
79
+
80
+ ```yaml
81
+ name: my-docs
82
+ services:
83
+ site:
84
+ type: static
85
+ path: /
86
+ node_version: "22"
87
+ source:
88
+ path: . # project is at the folder root, not under services/site/
89
+ build:
90
+ command: "npm run build"
91
+ output: "dist"
92
+ ```
93
+
94
+ `source.path: .` is REQUIRED for this root-level build — without it the platform
95
+ looks for `services/site/` and fails with "Service directory not found". No
96
+ `needs:` — a docs site has no infrastructure. Never author an active `needs:` and
97
+ `requires:` together (the validator rejects the combo).
98
+
99
+ ## 6. Build + deploy (async)
100
+
101
+ Deploy the folder with `grid_plug`. The platform (Cloud Build) runs `npm run
102
+ build` for you and serves the `dist/` output — you do NOT need to build first. A build deploy is **ASYNC**: the first response is
103
+ `status: "building"` with a `poll_url` / entity, NOT a live URL yet.
104
+ - Poll `grid_status` (or the returned poll_url) until the entity is live.
105
+ - Surface a liveness signal while it builds — never a bare silent wait.
106
+ - Only once it is live, return the deployed URL.
107
+
108
+ ## 7. Return the live URL, point at llms.txt, and ASK visibility
109
+
110
+ - Give the user the live docs URL — that is the deliverable.
111
+ - Point out the agent-readable docs: `<url>/llms.txt` (index) and
112
+ `<url>/llms-full.txt` (full content), generated automatically.
113
+ - This is a NEW deploy, so ASK the user who should be able to open it (private /
114
+ a space / your grid / anyone with the link / anyone signed in) and set their
115
+ choice with `grid_visibility`. Do not pick the visibility for them.
116
+
117
+ To iterate, edit the content and re-plug the SAME entity (`target_entity_id`) so
118
+ it updates the same URL.
119
+
120
+ Keep it honest: multi-file build, async deploy, local-edition only.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: docs-site
3
- when: documentation site, docs, developer docs, guide, manual, knowledge base, help center (static, content baked in). For an interactive API endpoint reference, use api-docs instead.
3
+ when: single-page docs, a quick guide/manual/README-as-a-page, small knowledge base (static, content baked into ONE HTML file). For a MULTI-PAGE site you'll keep editing (sidebar + full-text search + llms.txt, built with Starlight), use docs-app instead. For an interactive API endpoint reference, use api-docs instead.
4
4
  needs: none
5
5
  deploy: inspiration
6
6
  editions: all
package/src/index.js CHANGED
@@ -15,6 +15,9 @@ import {
15
15
  writeCredentials,
16
16
  credentialsPath,
17
17
  } from "./auth.js";
18
+ import { randomUUID } from "node:crypto";
19
+ import { createSessionLogger } from "./session-logger.js";
20
+ import { createSink } from "./log-sink.js";
18
21
 
19
22
  const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url)));
20
23
 
@@ -28,6 +31,18 @@ const ctx = {
28
31
  savedLocationNote: () => `Credentials saved to ${credentialsPath()}.`,
29
32
  };
30
33
 
34
+ // QA session log (dark by default: no CLOUDGRID_QA_SLACK_WEBHOOK → null → no-op).
35
+ // The stdio process IS the session. user_request is forwarded by Claude Code via
36
+ // CLOUDGRID_USER_REQUEST when present.
37
+ ctx.sessionId = `cli-${randomUUID()}`;
38
+ ctx.logger = createSessionLogger({
39
+ transport: "stdio",
40
+ sessionId: ctx.sessionId,
41
+ sink: createSink(process.env),
42
+ ctx,
43
+ userRequest: process.env.CLOUDGRID_USER_REQUEST || null,
44
+ });
45
+
31
46
  const server = new McpServer({ name: "cloudgrid-mcp", version });
32
47
  registerTools(server, ctx);
33
48
 
@@ -45,3 +60,17 @@ server.server.oninitialized = () => {
45
60
 
46
61
  const transport = new StdioServerTransport();
47
62
  await server.connect(transport);
63
+
64
+ // Flush the QA log once on shutdown if nothing triggered a flush during the
65
+ // session (abandoned / build-only session). Best-effort — a hard kill may cut
66
+ // delivery short; that is acceptable for QA. Guarded so it never crashes exit.
67
+ let shuttingDown = false;
68
+ async function flushAndExit(code) {
69
+ if (shuttingDown) return;
70
+ shuttingDown = true;
71
+ try { await ctx.logger?.flush("abandoned"); } catch { /* never */ }
72
+ process.exit(code);
73
+ }
74
+ process.on("SIGINT", () => flushAndExit(0));
75
+ process.on("SIGTERM", () => flushAndExit(0));
76
+ process.on("beforeExit", () => { ctx.logger?.flush("abandoned").catch(() => {}); });
@@ -0,0 +1,45 @@
1
+ // src/log-sink.js
2
+ // Pluggable delivery for the QA session log. Default sink posts to a Slack
3
+ // incoming webhook. Dark by default: createSink returns null when the webhook
4
+ // env var is unset, so the logger factory returns null and capture is a no-op.
5
+ //
6
+ // NOTE: Slack incoming webhooks cannot upload file attachments — only a JSON
7
+ // message body. We post the summary + the full log inside a fenced code block.
8
+ // True file attachment (files.upload) needs a bot token and is deferred.
9
+ const SLACK_TEXT_LIMIT = 38000; // headroom under Slack's ~40k message cap
10
+
11
+ export class SlackWebhookSink {
12
+ constructor(url, { fetchImpl } = {}) {
13
+ this.url = url;
14
+ this.fetchImpl = fetchImpl || globalThis.fetch;
15
+ }
16
+ async send({ filename, summary, text }) {
17
+ try {
18
+ const body = String(text || "");
19
+ const clipped = body.length > SLACK_TEXT_LIMIT
20
+ ? body.slice(0, SLACK_TEXT_LIMIT) + "\n…(truncated)"
21
+ : body;
22
+ const message = `*${filename}* — ${summary}\n\`\`\`\n${clipped}\n\`\`\``;
23
+ // Bound the POST so a hung webhook host can't stall flushAndExit on
24
+ // SIGINT/SIGTERM. Abort after 5s; clear the timer once the fetch settles.
25
+ const ac = new AbortController();
26
+ const timer = setTimeout(() => ac.abort(), 5000);
27
+ try {
28
+ await this.fetchImpl(this.url, {
29
+ method: "POST",
30
+ headers: { "content-type": "application/json" },
31
+ body: JSON.stringify({ text: message }),
32
+ signal: ac.signal,
33
+ });
34
+ } finally {
35
+ clearTimeout(timer);
36
+ }
37
+ } catch { /* delivery is best-effort; never throws */ }
38
+ }
39
+ }
40
+
41
+ export function createSink(env = process.env) {
42
+ const url = env.CLOUDGRID_QA_SLACK_WEBHOOK;
43
+ if (!url) return null;
44
+ return new SlackWebhookSink(url);
45
+ }