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 +21 -0
- package/README.md +94 -0
- package/dist/commands.d.ts +70 -0
- package/dist/commands.js +105 -0
- package/dist/config.d.ts +137 -0
- package/dist/config.js +73 -0
- package/dist/constants.d.ts +33 -0
- package/dist/constants.js +33 -0
- package/dist/events.d.ts +24 -0
- package/dist/events.js +49 -0
- package/dist/google-chat.d.ts +53 -0
- package/dist/google-chat.js +72 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/manifest.d.ts +167 -0
- package/dist/manifest.js +110 -0
- package/dist/tools.d.ts +28 -0
- package/dist/tools.js +71 -0
- package/dist/worker.d.ts +14 -0
- package/dist/worker.js +155 -0
- package/package.json +58 -0
package/dist/worker.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Chat plugin worker — wires the SDK seams together:
|
|
3
|
+
*
|
|
4
|
+
* - setup() registers domain-event subscriptions, the daily-digest job, and
|
|
5
|
+
* the agent tools.
|
|
6
|
+
* - onWebhook() receives Google Chat app events and routes slash commands.
|
|
7
|
+
* - outbound resolves a per-route incoming-webhook URL from a secret ref and
|
|
8
|
+
* POSTs the message via `ctx.http.fetch`.
|
|
9
|
+
*
|
|
10
|
+
* The pure logic (formatting, parsing, routing) lives in sibling modules and is
|
|
11
|
+
* unit-tested; this file is the thin SDK-facing shell.
|
|
12
|
+
*/
|
|
13
|
+
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
|
|
14
|
+
import { DEFAULT_CONFIG, validateConfig } from "./config.js";
|
|
15
|
+
import { DOMAIN_EVENTS, JOB_KEYS, WEBHOOK_KEYS } from "./constants.js";
|
|
16
|
+
import { handleCommand, parseChatEvent } from "./commands.js";
|
|
17
|
+
import { mapEventToNotification } from "./events.js";
|
|
18
|
+
import { postToWebhook } from "./google-chat.js";
|
|
19
|
+
import { registerTools } from "./tools.js";
|
|
20
|
+
let currentCtx = null;
|
|
21
|
+
async function getConfig(ctx) {
|
|
22
|
+
const raw = (await ctx.config.get());
|
|
23
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
24
|
+
}
|
|
25
|
+
/** Pick the secret ref for a route, falling back to the default webhook. */
|
|
26
|
+
function refForRoute(config, route) {
|
|
27
|
+
switch (route) {
|
|
28
|
+
case "approvals":
|
|
29
|
+
return config.approvalsWebhookUrlRef ?? config.defaultWebhookUrlRef;
|
|
30
|
+
case "errors":
|
|
31
|
+
return config.errorsWebhookUrlRef ?? config.defaultWebhookUrlRef;
|
|
32
|
+
case "digest":
|
|
33
|
+
return config.digestWebhookUrlRef ?? config.defaultWebhookUrlRef;
|
|
34
|
+
default:
|
|
35
|
+
return config.defaultWebhookUrlRef;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Resolve a route's webhook URL (from a secret ref) and POST a message. */
|
|
39
|
+
async function post(ctx, route, message) {
|
|
40
|
+
const config = await getConfig(ctx);
|
|
41
|
+
const ref = refForRoute(config, route);
|
|
42
|
+
if (!ref) {
|
|
43
|
+
ctx.logger?.warn?.(`No webhook secret ref configured for route "${route}"`);
|
|
44
|
+
return { ok: false, status: 0, body: `no webhook configured for route ${route}` };
|
|
45
|
+
}
|
|
46
|
+
const url = await ctx.secrets.resolve(ref);
|
|
47
|
+
return postToWebhook((u, init) => ctx.http.fetch(u, init), url, message);
|
|
48
|
+
}
|
|
49
|
+
/** Build the inbound command dependencies against the SDK domain APIs. */
|
|
50
|
+
function buildCommandDeps(ctx, companyId) {
|
|
51
|
+
const scope = companyId ? { companyId, limit: 50, offset: 0 } : { limit: 50, offset: 0 };
|
|
52
|
+
return {
|
|
53
|
+
async listIssues() {
|
|
54
|
+
try {
|
|
55
|
+
return (await ctx.issues.list(scope)) ?? [];
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
async listAgents() {
|
|
62
|
+
try {
|
|
63
|
+
return (await ctx.agents.list(scope)) ?? [];
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
// TODO: wire to a target agent/objective intake. Setting an objective and
|
|
70
|
+
// building a report both require a chosen company + agent; the space→company
|
|
71
|
+
// mapping (a `/connect` flow, see README) is the next milestone.
|
|
72
|
+
async setObjective(text) {
|
|
73
|
+
ctx.logger?.info?.(`/objective received: ${text}`);
|
|
74
|
+
return `Queued objective: "${text}" (intake wiring pending — see README §Roadmap).`;
|
|
75
|
+
},
|
|
76
|
+
async buildReport() {
|
|
77
|
+
const [issues, agents] = await Promise.all([this.listIssues(), this.listAgents()]);
|
|
78
|
+
return `*Report*\nIssues: ${issues.length}\nAgents: ${agents.length}`;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const plugin = definePlugin({
|
|
83
|
+
async setup(ctx) {
|
|
84
|
+
currentCtx = ctx;
|
|
85
|
+
// --- Outbound: subscribe to domain events → post notifications ---
|
|
86
|
+
for (const eventType of Object.values(DOMAIN_EVENTS)) {
|
|
87
|
+
ctx.events.on(eventType, async (event) => {
|
|
88
|
+
const config = await getConfig(ctx);
|
|
89
|
+
const notification = mapEventToNotification(event, config);
|
|
90
|
+
if (!notification)
|
|
91
|
+
return;
|
|
92
|
+
const res = await post(ctx, notification.routeKey, { text: notification.text });
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
ctx.logger?.warn?.(`Notification post failed for ${eventType}: HTTP ${res.status}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// --- Daily digest job (hourly tick, self-gated to configured HH:MM) ---
|
|
99
|
+
ctx.jobs.register(JOB_KEYS.dailyDigest, async () => {
|
|
100
|
+
const config = await getConfig(ctx);
|
|
101
|
+
if (!config.digestMode)
|
|
102
|
+
return;
|
|
103
|
+
const now = new Date();
|
|
104
|
+
const hhmm = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes() < 30 ? "00" : "30").padStart(2, "0")}`;
|
|
105
|
+
// Fire when the hour matches the configured digest hour.
|
|
106
|
+
const wantHour = (config.dailyDigestTime ?? "08:00").slice(0, 2);
|
|
107
|
+
if (hhmm.slice(0, 2) !== wantHour)
|
|
108
|
+
return;
|
|
109
|
+
const deps = buildCommandDeps(ctx, undefined);
|
|
110
|
+
const report = await deps.buildReport();
|
|
111
|
+
await post(ctx, "digest", { text: `🗓️ *Daily digest*\n${report}` });
|
|
112
|
+
});
|
|
113
|
+
// --- Agent tools ---
|
|
114
|
+
registerTools(ctx, (route, message) => post(ctx, route, message));
|
|
115
|
+
ctx.logger?.info?.("google-chat plugin setup complete");
|
|
116
|
+
},
|
|
117
|
+
async onHealth() {
|
|
118
|
+
const ctx = currentCtx;
|
|
119
|
+
const config = ctx ? await getConfig(ctx) : DEFAULT_CONFIG;
|
|
120
|
+
const configured = Boolean(config.defaultWebhookUrlRef);
|
|
121
|
+
return {
|
|
122
|
+
status: configured ? "ok" : "degraded",
|
|
123
|
+
message: configured ? "google-chat plugin ready" : "no defaultWebhookUrlRef configured",
|
|
124
|
+
details: { commandsEnabled: config.enableCommands !== false, digestMode: config.digestMode === true },
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
async onValidateConfig(config) {
|
|
128
|
+
return validateConfig({ ...DEFAULT_CONFIG, ...config });
|
|
129
|
+
},
|
|
130
|
+
async onWebhook(input) {
|
|
131
|
+
if (input.endpointKey !== WEBHOOK_KEYS.chatEvents) {
|
|
132
|
+
throw new Error(`Unsupported webhook endpoint "${input.endpointKey}"`);
|
|
133
|
+
}
|
|
134
|
+
const ctx = currentCtx;
|
|
135
|
+
if (!ctx)
|
|
136
|
+
return;
|
|
137
|
+
const event = (input.parsedBody ?? {});
|
|
138
|
+
const parsed = parseChatEvent(event);
|
|
139
|
+
if (!parsed)
|
|
140
|
+
return; // not a slash command (e.g. ADDED_TO_SPACE) — ignore for now
|
|
141
|
+
const config = await getConfig(ctx);
|
|
142
|
+
const deps = buildCommandDeps(ctx, undefined);
|
|
143
|
+
const reply = await handleCommand(parsed, config, deps);
|
|
144
|
+
if (!reply)
|
|
145
|
+
return;
|
|
146
|
+
// Incoming webhooks are per-space; reply via the default route. Threaded
|
|
147
|
+
// per-space replies require the Chat REST API (serviceAccountKeyRef) — roadmap.
|
|
148
|
+
await post(ctx, "default", { text: reply, threadKey: parsed.threadKey });
|
|
149
|
+
},
|
|
150
|
+
async onShutdown() {
|
|
151
|
+
currentCtx = null;
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
export default plugin;
|
|
155
|
+
runWorker(plugin, import.meta.url);
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "paperclip-plugin-google-chat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bidirectional Google Chat integration for Paperclip — post agent/issue notifications to spaces and drive Paperclip from Chat slash commands.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Measured Assets",
|
|
8
|
+
"homepage": "https://github.com/measured-assets/paperclip-plugin-google-chat",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/measured-assets/paperclip-plugin-google-chat.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"paperclip",
|
|
15
|
+
"paperclip-plugin",
|
|
16
|
+
"google-chat",
|
|
17
|
+
"google-workspace",
|
|
18
|
+
"chatops",
|
|
19
|
+
"agents"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"paperclipPlugin": {
|
|
30
|
+
"manifest": "./dist/manifest.js",
|
|
31
|
+
"worker": "./dist/worker.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"clean": "rm -rf dist",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"prepack": "npm run build"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@paperclipai/plugin-sdk": ">=2026.626.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@paperclipai/plugin-sdk": "^2026.626.0",
|
|
54
|
+
"@types/node": "^22.19.21",
|
|
55
|
+
"typescript": "^5.7.3",
|
|
56
|
+
"vitest": "^2.1.8"
|
|
57
|
+
}
|
|
58
|
+
}
|