paperclip-plugin-google-chat 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Measured Assets
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # paperclip-plugin-google-chat
2
+
3
+ Bidirectional **Google Chat** integration for [Paperclip](https://github.com/paperclipai/paperclip).
4
+
5
+ - **Outbound** — posts issue / approval / agent-failure notifications to Google Chat spaces, driven by Paperclip domain events. Agents can also push messages on demand via registered tools (`post_to_google_chat`, `escalate_to_human`, `send_briefing`).
6
+ - **Inbound** — a Google Chat app delivers events to the plugin's webhook endpoint; slash commands (`/status`, `/issues`, `/agents`, `/report`, `/objective`) query or drive Paperclip from a space.
7
+
8
+ Built on the first-party [Paperclip plugin SDK](https://github.com/paperclipai/paperclip/tree/master/packages/plugins/sdk) (`apiVersion: 1`). No external relay service required — Paperclip terminates the webhook and makes the outbound calls itself.
9
+
10
+ > Status: **0.1.0 — scaffold.** Outbound notifications + tools and inbound command routing are implemented and unit-tested. The space→company `/connect` mapping and the Chat REST API reply path are on the roadmap (see below).
11
+
12
+ ## Why a plugin (not a Cloudflare Worker + curl)
13
+
14
+ Everything this needs is a first-class SDK primitive, so the whole bridge lives inside Paperclip:
15
+
16
+ | Need | SDK capability |
17
+ |------|----------------|
18
+ | Receive Google Chat events | `webhooks.receive` |
19
+ | Post to a space | `http.outbound` (`ctx.http.fetch`) |
20
+ | Keep the webhook URL secret | `secrets.read-ref` (`ctx.secrets.resolve`) |
21
+ | Notify on issue/approval/agent events | `events.subscribe` |
22
+ | Let agents push messages | `agent.tools.register` |
23
+ | Daily digest | `jobs.schedule` |
24
+
25
+ The Google Chat webhook URL is **never stored in plaintext** — config holds a Paperclip secret *reference* (a UUID), resolved at runtime.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ # from a checkout of this repo
31
+ npm install
32
+ npm run build
33
+ # then register the built plugin with your Paperclip instance
34
+ paperclipai plugin install /absolute/path/to/paperclip-plugin-google-chat
35
+ ```
36
+
37
+ ## Configure
38
+
39
+ 1. **Create an incoming webhook** in your Google Chat space (Space → *Apps & integrations* → *Webhooks*). Copy the URL.
40
+ 2. In Paperclip, store it as a **company secret** (Settings → Secrets, or `POST /api/companies/{companyId}/secrets`). You get back a UUID.
41
+ 3. In the plugin's settings, paste that UUID into **`defaultWebhookUrlRef`** (and optionally `approvalsWebhookUrlRef` / `errorsWebhookUrlRef` / `digestWebhookUrlRef` for per-category routing).
42
+ 4. Toggle which events notify (`notifyOnIssueCompleted`, `notifyOnApprovalRequested`, …) and, if you want inbound commands, set up a Google Chat **app** pointing its HTTP endpoint at this plugin's webhook URL, then set `allowedSpaceIds` / `allowedUserEmails`.
43
+
44
+ ### Config keys
45
+
46
+ See [`src/config.ts`](src/config.ts) for the full schema. All credential fields are **secret references**, not raw values.
47
+
48
+ ## Slash commands
49
+
50
+ | Command | Action | Restricted |
51
+ |---------|--------|:---:|
52
+ | `/status` | Open-issue + active-agent snapshot | |
53
+ | `/issues` | List open issues | |
54
+ | `/agents` | Agent roster + state | |
55
+ | `/report` | Generate a status report | |
56
+ | `/objective <text>` | Set/queue an objective | ✅ allowlist |
57
+ | `/help` | Command help | |
58
+
59
+ Mutating commands require the sender's email to be in `allowedUserEmails`.
60
+
61
+ ## Architecture
62
+
63
+ ```
64
+ src/
65
+ manifest.ts capabilities + webhook/tool/job declarations
66
+ config.ts config schema + validator (secret-ref based)
67
+ google-chat.ts outbound client (incoming webhooks) + pure formatters
68
+ events.ts domain-event → notification mapping
69
+ commands.ts inbound event parsing + slash-command router
70
+ tools.ts agent-callable tool handlers
71
+ worker.ts definePlugin() wiring: setup / onWebhook / onHealth
72
+ ```
73
+
74
+ Pure logic (formatting, parsing, routing, validation) is separated from the SDK-facing worker so it is unit-tested without a running Paperclip (`tests/`).
75
+
76
+ ## Develop
77
+
78
+ ```bash
79
+ npm install
80
+ npm test # vitest — pure-logic unit tests
81
+ npm run typecheck
82
+ npm run build
83
+ ```
84
+
85
+ ## Roadmap
86
+
87
+ - **`/connect <company>`** — map a Google Chat space to a Paperclip company and persist it in plugin state (today inbound commands assume a single company scope).
88
+ - **Chat REST API path** — threaded, per-space replies and message reads via a service-account key (`serviceAccountKeyRef`); today replies post via the configured incoming webhook.
89
+ - **Cards v2** — richer notification/approval cards with interactive buttons (`useCards`).
90
+ - **Inbound verification** — validate Google's signed bearer token, not just a shared `verificationTokenRef`.
91
+
92
+ ## License
93
+
94
+ MIT © Measured Assets
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Inbound Google Chat event handling.
3
+ *
4
+ * A Google Chat *app* delivers events as JSON POSTs. The shapes we care about:
5
+ *
6
+ * { type: "MESSAGE", space: { name, type }, user: { email, displayName },
7
+ * message: { text, argumentText, slashCommand?, thread } }
8
+ * { type: "ADDED_TO_SPACE" | "REMOVED_FROM_SPACE", ... }
9
+ * { type: "CARD_CLICKED", ... }
10
+ *
11
+ * We translate a `MESSAGE` whose text begins with a slash into a Paperclip action.
12
+ * Parsing and authorization are pure functions; the side-effecting handlers take an
13
+ * injected `CommandDeps` so they can be unit-tested without the SDK.
14
+ */
15
+ import type { GoogleChatConfig } from "./config.js";
16
+ export interface ChatEvent {
17
+ type?: string;
18
+ space?: {
19
+ name?: string;
20
+ type?: string;
21
+ };
22
+ user?: {
23
+ email?: string;
24
+ displayName?: string;
25
+ };
26
+ message?: {
27
+ text?: string;
28
+ argumentText?: string;
29
+ thread?: {
30
+ name?: string;
31
+ };
32
+ };
33
+ }
34
+ export interface ParsedCommand {
35
+ command: string;
36
+ args: string;
37
+ spaceId: string;
38
+ userEmail: string;
39
+ threadKey?: string;
40
+ raw: string;
41
+ }
42
+ /** Extract a `/command args` from a Chat MESSAGE event. Returns null if not a command. */
43
+ export declare function parseChatEvent(event: ChatEvent): ParsedCommand | null;
44
+ /** Space-level allowlist (who may issue ANY command). */
45
+ export declare function isSpaceAllowed(spaceId: string, config: GoogleChatConfig): boolean;
46
+ /** User-level allowlist (who may issue MUTATING commands). */
47
+ export declare function isUserAllowed(email: string, config: GoogleChatConfig): boolean;
48
+ /** Commands that change Paperclip state (require user allowlist). */
49
+ export declare const MUTATING_COMMANDS: Set<string>;
50
+ /**
51
+ * Side-effecting dependencies the router needs. Implemented in the worker against
52
+ * the Paperclip SDK; mocked in tests.
53
+ */
54
+ export interface CommandDeps {
55
+ listIssues(): Promise<Array<{
56
+ title?: string;
57
+ status?: string;
58
+ }>>;
59
+ listAgents(): Promise<Array<{
60
+ name?: string;
61
+ status?: string;
62
+ }>>;
63
+ setObjective(text: string): Promise<string>;
64
+ buildReport(): Promise<string>;
65
+ }
66
+ /**
67
+ * Route a parsed command to a reply string. Returns the text to post back to the
68
+ * space. Authorization is enforced here; unknown commands return help.
69
+ */
70
+ export declare function handleCommand(parsed: ParsedCommand, config: GoogleChatConfig, deps: CommandDeps): Promise<string>;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Inbound Google Chat event handling.
3
+ *
4
+ * A Google Chat *app* delivers events as JSON POSTs. The shapes we care about:
5
+ *
6
+ * { type: "MESSAGE", space: { name, type }, user: { email, displayName },
7
+ * message: { text, argumentText, slashCommand?, thread } }
8
+ * { type: "ADDED_TO_SPACE" | "REMOVED_FROM_SPACE", ... }
9
+ * { type: "CARD_CLICKED", ... }
10
+ *
11
+ * We translate a `MESSAGE` whose text begins with a slash into a Paperclip action.
12
+ * Parsing and authorization are pure functions; the side-effecting handlers take an
13
+ * injected `CommandDeps` so they can be unit-tested without the SDK.
14
+ */
15
+ /** Extract a `/command args` from a Chat MESSAGE event. Returns null if not a command. */
16
+ export function parseChatEvent(event) {
17
+ if (event.type !== "MESSAGE")
18
+ return null;
19
+ const text = (event.message?.text ?? "").trim();
20
+ if (!text.startsWith("/"))
21
+ return null;
22
+ const withoutSlash = text.slice(1);
23
+ const spaceIdx = withoutSlash.search(/\s/);
24
+ const command = (spaceIdx === -1 ? withoutSlash : withoutSlash.slice(0, spaceIdx)).toLowerCase();
25
+ const args = spaceIdx === -1 ? "" : withoutSlash.slice(spaceIdx + 1).trim();
26
+ const spaceName = event.space?.name ?? "";
27
+ const spaceId = spaceName.startsWith("spaces/") ? spaceName.slice("spaces/".length) : spaceName;
28
+ return {
29
+ command,
30
+ args,
31
+ spaceId,
32
+ userEmail: event.user?.email ?? "",
33
+ threadKey: event.message?.thread?.name,
34
+ raw: text,
35
+ };
36
+ }
37
+ /** Space-level allowlist (who may issue ANY command). */
38
+ export function isSpaceAllowed(spaceId, config) {
39
+ const allow = config.allowedSpaceIds ?? [];
40
+ return allow.length === 0 || allow.includes(spaceId);
41
+ }
42
+ /** User-level allowlist (who may issue MUTATING commands). */
43
+ export function isUserAllowed(email, config) {
44
+ const allow = config.allowedUserEmails ?? [];
45
+ return allow.length === 0 || allow.includes(email);
46
+ }
47
+ /** Commands that change Paperclip state (require user allowlist). */
48
+ export const MUTATING_COMMANDS = new Set(["objective", "report", "approve"]);
49
+ const HELP_TEXT = [
50
+ "*Paperclip — Google Chat commands*",
51
+ "`/status` — fleet + issue snapshot",
52
+ "`/issues` — open issues",
53
+ "`/agents` — agent roster + state",
54
+ "`/report` — generate a status report",
55
+ "`/objective <text>` — set/queue an objective (restricted)",
56
+ "`/help` — this message",
57
+ ].join("\n");
58
+ /**
59
+ * Route a parsed command to a reply string. Returns the text to post back to the
60
+ * space. Authorization is enforced here; unknown commands return help.
61
+ */
62
+ export async function handleCommand(parsed, config, deps) {
63
+ if (config.enableCommands === false)
64
+ return ""; // silently ignore when disabled
65
+ if (!isSpaceAllowed(parsed.spaceId, config)) {
66
+ return "This space is not authorized to issue commands.";
67
+ }
68
+ if (MUTATING_COMMANDS.has(parsed.command) && !isUserAllowed(parsed.userEmail, config)) {
69
+ return `You (${parsed.userEmail || "unknown"}) are not authorized to run /${parsed.command}.`;
70
+ }
71
+ switch (parsed.command) {
72
+ case "help":
73
+ return HELP_TEXT;
74
+ case "status": {
75
+ const [issues, agents] = await Promise.all([deps.listIssues(), deps.listAgents()]);
76
+ const open = issues.filter((i) => i.status !== "done" && i.status !== "completed").length;
77
+ const running = agents.filter((a) => a.status === "running" || a.status === "active").length;
78
+ return `*Status* — ${open} open issues · ${running}/${agents.length} agents active`;
79
+ }
80
+ case "issues": {
81
+ const issues = await deps.listIssues();
82
+ if (issues.length === 0)
83
+ return "No issues.";
84
+ return issues
85
+ .slice(0, 20)
86
+ .map((i) => `• ${i.title ?? "(untitled)"}${i.status ? ` _(${i.status})_` : ""}`)
87
+ .join("\n");
88
+ }
89
+ case "agents": {
90
+ const agents = await deps.listAgents();
91
+ if (agents.length === 0)
92
+ return "No agents.";
93
+ return agents.map((a) => `• ${a.name ?? "(unnamed)"} — ${a.status ?? "unknown"}`).join("\n");
94
+ }
95
+ case "report":
96
+ return await deps.buildReport();
97
+ case "objective": {
98
+ if (!parsed.args)
99
+ return "Usage: /objective <text>";
100
+ return await deps.setObjective(parsed.args);
101
+ }
102
+ default:
103
+ return HELP_TEXT;
104
+ }
105
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Instance configuration for the Google Chat plugin.
3
+ *
4
+ * Secret values (Google Chat incoming-webhook URLs, an optional service-account
5
+ * key) are NEVER stored here in plaintext. The config holds **secret references**
6
+ * — opaque UUIDs minted by Paperclip's secret store — which the worker resolves at
7
+ * runtime via `ctx.secrets.resolve(ref)`. This mirrors the Telegram plugin's
8
+ * `telegramBotTokenRef` pattern and keeps credentials out of config exports/logs.
9
+ */
10
+ export interface GoogleChatConfig {
11
+ /** Secret ref → a Google Chat *incoming webhook* URL for the default space. */
12
+ defaultWebhookUrlRef?: string;
13
+ /** Optional per-category routing. Each is a secret ref to a space webhook URL. */
14
+ approvalsWebhookUrlRef?: string;
15
+ errorsWebhookUrlRef?: string;
16
+ digestWebhookUrlRef?: string;
17
+ /**
18
+ * Optional secret ref → a Google service-account JSON key. Only needed for the
19
+ * Chat REST API (threaded replies, reading messages). Plain incoming webhooks do
20
+ * not require it. Left unset = webhook-only mode.
21
+ */
22
+ serviceAccountKeyRef?: string;
23
+ notifyOnIssueCreated?: boolean;
24
+ notifyOnIssueCompleted?: boolean;
25
+ notifyOnApprovalRequested?: boolean;
26
+ notifyOnAgentRunFailed?: boolean;
27
+ /** Use Cards v2 formatting instead of plain text where supported. */
28
+ useCards?: boolean;
29
+ /** Master switch for inbound slash commands. */
30
+ enableCommands?: boolean;
31
+ /**
32
+ * Shared verification token. Google Chat includes a bearer/verification token on
33
+ * each request; we compare it to reject forged webhook deliveries. Stored as a
34
+ * secret ref. (For production, prefer verifying the Google-signed bearer JWT.)
35
+ */
36
+ verificationTokenRef?: string;
37
+ /** Allowlist of Google Chat space IDs permitted to issue commands. Empty = all. */
38
+ allowedSpaceIds?: string[];
39
+ /** Allowlist of user emails permitted to issue mutating commands. Empty = all. */
40
+ allowedUserEmails?: string[];
41
+ digestMode?: boolean;
42
+ /** 24h "HH:MM" local time for the daily digest. */
43
+ dailyDigestTime?: string;
44
+ }
45
+ export declare const DEFAULT_CONFIG: GoogleChatConfig;
46
+ /** JSON-schema fragment surfaced in Paperclip's plugin settings UI. */
47
+ export declare const INSTANCE_CONFIG_SCHEMA: {
48
+ readonly type: "object";
49
+ readonly properties: {
50
+ readonly defaultWebhookUrlRef: {
51
+ readonly type: "string";
52
+ readonly title: "Default space webhook (secret ref)";
53
+ readonly description: "Secret reference to a Google Chat incoming-webhook URL. Create the secret in Paperclip, paste the returned UUID here.";
54
+ };
55
+ readonly approvalsWebhookUrlRef: {
56
+ readonly type: "string";
57
+ readonly title: "Approvals space webhook (secret ref)";
58
+ };
59
+ readonly errorsWebhookUrlRef: {
60
+ readonly type: "string";
61
+ readonly title: "Errors space webhook (secret ref)";
62
+ };
63
+ readonly digestWebhookUrlRef: {
64
+ readonly type: "string";
65
+ readonly title: "Digest space webhook (secret ref)";
66
+ };
67
+ readonly serviceAccountKeyRef: {
68
+ readonly type: "string";
69
+ readonly title: "Service-account key (secret ref, optional)";
70
+ readonly description: "Only required for the Chat REST API (threaded replies). Webhook-only mode leaves this blank.";
71
+ };
72
+ readonly notifyOnIssueCreated: {
73
+ readonly type: "boolean";
74
+ readonly title: "Notify on issue created";
75
+ readonly default: false;
76
+ };
77
+ readonly notifyOnIssueCompleted: {
78
+ readonly type: "boolean";
79
+ readonly title: "Notify on issue completed";
80
+ readonly default: true;
81
+ };
82
+ readonly notifyOnApprovalRequested: {
83
+ readonly type: "boolean";
84
+ readonly title: "Notify on approval requested";
85
+ readonly default: true;
86
+ };
87
+ readonly notifyOnAgentRunFailed: {
88
+ readonly type: "boolean";
89
+ readonly title: "Notify on agent run failed";
90
+ readonly default: true;
91
+ };
92
+ readonly useCards: {
93
+ readonly type: "boolean";
94
+ readonly title: "Use Cards v2 formatting";
95
+ readonly default: false;
96
+ };
97
+ readonly enableCommands: {
98
+ readonly type: "boolean";
99
+ readonly title: "Enable inbound slash commands";
100
+ readonly default: true;
101
+ };
102
+ readonly verificationTokenRef: {
103
+ readonly type: "string";
104
+ readonly title: "Verification token (secret ref)";
105
+ };
106
+ readonly allowedSpaceIds: {
107
+ readonly type: "array";
108
+ readonly title: "Allowed space IDs";
109
+ readonly items: {
110
+ readonly type: "string";
111
+ };
112
+ };
113
+ readonly allowedUserEmails: {
114
+ readonly type: "array";
115
+ readonly title: "Allowed user emails";
116
+ readonly items: {
117
+ readonly type: "string";
118
+ };
119
+ };
120
+ readonly digestMode: {
121
+ readonly type: "boolean";
122
+ readonly title: "Enable daily digest";
123
+ readonly default: false;
124
+ };
125
+ readonly dailyDigestTime: {
126
+ readonly type: "string";
127
+ readonly title: "Daily digest time (HH:MM)";
128
+ readonly default: "08:00";
129
+ };
130
+ };
131
+ };
132
+ /** Pure validator used by the worker's `onValidateConfig` hook and unit tests. */
133
+ export declare function validateConfig(config: GoogleChatConfig): {
134
+ ok: boolean;
135
+ errors: string[];
136
+ warnings: string[];
137
+ };
package/dist/config.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Instance configuration for the Google Chat plugin.
3
+ *
4
+ * Secret values (Google Chat incoming-webhook URLs, an optional service-account
5
+ * key) are NEVER stored here in plaintext. The config holds **secret references**
6
+ * — opaque UUIDs minted by Paperclip's secret store — which the worker resolves at
7
+ * runtime via `ctx.secrets.resolve(ref)`. This mirrors the Telegram plugin's
8
+ * `telegramBotTokenRef` pattern and keeps credentials out of config exports/logs.
9
+ */
10
+ export const DEFAULT_CONFIG = {
11
+ notifyOnIssueCreated: false,
12
+ notifyOnIssueCompleted: true,
13
+ notifyOnApprovalRequested: true,
14
+ notifyOnAgentRunFailed: true,
15
+ useCards: false,
16
+ enableCommands: true,
17
+ allowedSpaceIds: [],
18
+ allowedUserEmails: [],
19
+ digestMode: false,
20
+ dailyDigestTime: "08:00",
21
+ };
22
+ /** JSON-schema fragment surfaced in Paperclip's plugin settings UI. */
23
+ export const INSTANCE_CONFIG_SCHEMA = {
24
+ type: "object",
25
+ properties: {
26
+ defaultWebhookUrlRef: {
27
+ type: "string",
28
+ title: "Default space webhook (secret ref)",
29
+ description: "Secret reference to a Google Chat incoming-webhook URL. Create the secret in Paperclip, paste the returned UUID here.",
30
+ },
31
+ approvalsWebhookUrlRef: { type: "string", title: "Approvals space webhook (secret ref)" },
32
+ errorsWebhookUrlRef: { type: "string", title: "Errors space webhook (secret ref)" },
33
+ digestWebhookUrlRef: { type: "string", title: "Digest space webhook (secret ref)" },
34
+ serviceAccountKeyRef: {
35
+ type: "string",
36
+ title: "Service-account key (secret ref, optional)",
37
+ description: "Only required for the Chat REST API (threaded replies). Webhook-only mode leaves this blank.",
38
+ },
39
+ notifyOnIssueCreated: { type: "boolean", title: "Notify on issue created", default: false },
40
+ notifyOnIssueCompleted: { type: "boolean", title: "Notify on issue completed", default: true },
41
+ notifyOnApprovalRequested: { type: "boolean", title: "Notify on approval requested", default: true },
42
+ notifyOnAgentRunFailed: { type: "boolean", title: "Notify on agent run failed", default: true },
43
+ useCards: { type: "boolean", title: "Use Cards v2 formatting", default: false },
44
+ enableCommands: { type: "boolean", title: "Enable inbound slash commands", default: true },
45
+ verificationTokenRef: { type: "string", title: "Verification token (secret ref)" },
46
+ allowedSpaceIds: { type: "array", title: "Allowed space IDs", items: { type: "string" } },
47
+ allowedUserEmails: { type: "array", title: "Allowed user emails", items: { type: "string" } },
48
+ digestMode: { type: "boolean", title: "Enable daily digest", default: false },
49
+ dailyDigestTime: { type: "string", title: "Daily digest time (HH:MM)", default: "08:00" },
50
+ },
51
+ };
52
+ /** Pure validator used by the worker's `onValidateConfig` hook and unit tests. */
53
+ export function validateConfig(config) {
54
+ const errors = [];
55
+ const warnings = [];
56
+ if (config.dailyDigestTime && !/^([01]\d|2[0-3]):[0-5]\d$/.test(config.dailyDigestTime)) {
57
+ errors.push("dailyDigestTime must be 24h HH:MM, e.g. 08:00");
58
+ }
59
+ if (config.digestMode && !config.digestWebhookUrlRef && !config.defaultWebhookUrlRef) {
60
+ errors.push("digestMode is on but no digest/default webhook secret ref is set");
61
+ }
62
+ const anyNotify = config.notifyOnIssueCreated ||
63
+ config.notifyOnIssueCompleted ||
64
+ config.notifyOnApprovalRequested ||
65
+ config.notifyOnAgentRunFailed;
66
+ if (anyNotify && !config.defaultWebhookUrlRef) {
67
+ warnings.push("Notifications are enabled but no defaultWebhookUrlRef is set — category routing only.");
68
+ }
69
+ if (config.serviceAccountKeyRef === "") {
70
+ warnings.push("serviceAccountKeyRef is an empty string; leave it unset for webhook-only mode.");
71
+ }
72
+ return { ok: errors.length === 0, errors, warnings };
73
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Stable identifiers for the Google Chat plugin. Keeping these in one place keeps
3
+ * the manifest, worker, and tests in sync (the kitchen-sink reference plugin uses
4
+ * the same convention).
5
+ */
6
+ export declare const PLUGIN_ID = "google-chat";
7
+ export declare const PLUGIN_VERSION = "0.1.0";
8
+ /** Webhook endpoint keys declared in the manifest and matched in `onWebhook`. */
9
+ export declare const WEBHOOK_KEYS: {
10
+ /** Google Chat app events (MESSAGE, ADDED_TO_SPACE, CARD_CLICKED, …) POST here. */
11
+ readonly chatEvents: "chat-events";
12
+ };
13
+ /** Agent-callable tool names. */
14
+ export declare const TOOL_NAMES: {
15
+ readonly postMessage: "post_to_google_chat";
16
+ readonly escalateToHuman: "escalate_to_human";
17
+ readonly sendBriefing: "send_briefing";
18
+ };
19
+ /** Scheduled job keys. */
20
+ export declare const JOB_KEYS: {
21
+ readonly dailyDigest: "daily-digest";
22
+ };
23
+ /**
24
+ * Domain events we translate into Google Chat notifications. The host emits these;
25
+ * we subscribe via `ctx.events.on`. Names mirror the kitchen-sink event surface.
26
+ */
27
+ export declare const DOMAIN_EVENTS: {
28
+ readonly issueCreated: "issue.created";
29
+ readonly issueUpdated: "issue.updated";
30
+ readonly issueCompleted: "issue.completed";
31
+ readonly approvalRequested: "approval.requested";
32
+ readonly agentRunFailed: "agent.run.failed";
33
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Stable identifiers for the Google Chat plugin. Keeping these in one place keeps
3
+ * the manifest, worker, and tests in sync (the kitchen-sink reference plugin uses
4
+ * the same convention).
5
+ */
6
+ export const PLUGIN_ID = "google-chat";
7
+ export const PLUGIN_VERSION = "0.1.0";
8
+ /** Webhook endpoint keys declared in the manifest and matched in `onWebhook`. */
9
+ export const WEBHOOK_KEYS = {
10
+ /** Google Chat app events (MESSAGE, ADDED_TO_SPACE, CARD_CLICKED, …) POST here. */
11
+ chatEvents: "chat-events",
12
+ };
13
+ /** Agent-callable tool names. */
14
+ export const TOOL_NAMES = {
15
+ postMessage: "post_to_google_chat",
16
+ escalateToHuman: "escalate_to_human",
17
+ sendBriefing: "send_briefing",
18
+ };
19
+ /** Scheduled job keys. */
20
+ export const JOB_KEYS = {
21
+ dailyDigest: "daily-digest",
22
+ };
23
+ /**
24
+ * Domain events we translate into Google Chat notifications. The host emits these;
25
+ * we subscribe via `ctx.events.on`. Names mirror the kitchen-sink event surface.
26
+ */
27
+ export const DOMAIN_EVENTS = {
28
+ issueCreated: "issue.created",
29
+ issueUpdated: "issue.updated",
30
+ issueCompleted: "issue.completed",
31
+ approvalRequested: "approval.requested",
32
+ agentRunFailed: "agent.run.failed",
33
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Domain-event → Google Chat notification mapping.
3
+ *
4
+ * The host emits domain events (issue lifecycle, approvals, agent runs). We
5
+ * subscribe in the worker and turn each into a `{ routeKey, text }` notification,
6
+ * gated by the per-event config toggles. The mapping is pure so it can be tested
7
+ * without the SDK; the worker performs the actual `postToWebhook`.
8
+ */
9
+ import type { GoogleChatConfig } from "./config.js";
10
+ import { type RouteKey } from "./google-chat.js";
11
+ /** Loose shape of a host PluginEvent payload. */
12
+ export interface DomainEvent {
13
+ type: string;
14
+ data?: Record<string, unknown>;
15
+ }
16
+ export interface Notification {
17
+ routeKey: RouteKey;
18
+ text: string;
19
+ }
20
+ /**
21
+ * Map a domain event to a notification, or null if the event is unmapped or its
22
+ * config toggle is off.
23
+ */
24
+ export declare function mapEventToNotification(event: DomainEvent, config: GoogleChatConfig): Notification | null;
package/dist/events.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Domain-event → Google Chat notification mapping.
3
+ *
4
+ * The host emits domain events (issue lifecycle, approvals, agent runs). We
5
+ * subscribe in the worker and turn each into a `{ routeKey, text }` notification,
6
+ * gated by the per-event config toggles. The mapping is pure so it can be tested
7
+ * without the SDK; the worker performs the actual `postToWebhook`.
8
+ */
9
+ import { DOMAIN_EVENTS } from "./constants.js";
10
+ import { formatAgentRunFailed, formatApprovalRequested, formatIssueCompleted, formatIssueCreated, } from "./google-chat.js";
11
+ function asIssue(data) {
12
+ const d = data ?? {};
13
+ return {
14
+ id: typeof d.id === "string" ? d.id : undefined,
15
+ title: typeof d.title === "string" ? d.title : undefined,
16
+ status: typeof d.status === "string" ? d.status : undefined,
17
+ url: typeof d.url === "string" ? d.url : undefined,
18
+ };
19
+ }
20
+ /**
21
+ * Map a domain event to a notification, or null if the event is unmapped or its
22
+ * config toggle is off.
23
+ */
24
+ export function mapEventToNotification(event, config) {
25
+ switch (event.type) {
26
+ case DOMAIN_EVENTS.issueCreated:
27
+ if (!config.notifyOnIssueCreated)
28
+ return null;
29
+ return { routeKey: "default", text: formatIssueCreated(asIssue(event.data)) };
30
+ case DOMAIN_EVENTS.issueCompleted:
31
+ if (!config.notifyOnIssueCompleted)
32
+ return null;
33
+ return { routeKey: "default", text: formatIssueCompleted(asIssue(event.data)) };
34
+ case DOMAIN_EVENTS.approvalRequested:
35
+ if (!config.notifyOnApprovalRequested)
36
+ return null;
37
+ return { routeKey: "approvals", text: formatApprovalRequested(asIssue(event.data)) };
38
+ case DOMAIN_EVENTS.agentRunFailed: {
39
+ if (!config.notifyOnAgentRunFailed)
40
+ return null;
41
+ const d = event.data ?? {};
42
+ const agentName = typeof d.agentName === "string" ? d.agentName : "agent";
43
+ const errorMsg = typeof d.error === "string" ? d.error : "unknown error";
44
+ return { routeKey: "errors", text: formatAgentRunFailed(agentName, errorMsg) };
45
+ }
46
+ default:
47
+ return null;
48
+ }
49
+ }