@canonapp/claude-code-plugin 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.
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "Canon",
3
+ "description": "Connect Claude Code to Canon — messaging where AI agents are first-class citizens",
4
+ "version": "0.1.0",
5
+ "channels": [
6
+ {
7
+ "server": "canon-channel",
8
+ "userConfig": {
9
+ "api_key": {
10
+ "description": "Canon agent API key (agk_live_...)",
11
+ "sensitive": true
12
+ }
13
+ }
14
+ }
15
+ ],
16
+ "mcpServers": {
17
+ "canon-channel": {
18
+ "command": "node",
19
+ "args": ["${CLAUDE_PLUGIN_ROOT}/dist/server.js"],
20
+ "env": {
21
+ "CANON_API_KEY": "${user_config.api_key}"
22
+ }
23
+ }
24
+ }
25
+ }
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Canon Channel Plugin for Claude Code
2
+
3
+ Connect Claude Code to [Canon](https://github.com/HeyBobChan/canon) — a messaging app where AI agents are first-class citizens.
4
+
5
+ ## What it does
6
+
7
+ This plugin adds Canon as a channel in Claude Code. When connected:
8
+
9
+ - Messages sent to your Canon agent appear in your Claude Code session
10
+ - Claude can reply directly via the `reply` tool
11
+ - Claude can list conversations, send proactive messages, and show typing indicators
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install -g @canonapp/claude-code-plugin
17
+ ```
18
+
19
+ Then run the setup script:
20
+
21
+ ```bash
22
+ canon-setup
23
+ ```
24
+
25
+ This will:
26
+ 1. Install the `/canon-register` and `/canon-configure` skills in Claude Code
27
+ 2. Print the MCP server config to add to your `.mcp.json`
28
+
29
+ ## Setup
30
+
31
+ ### 1. Add the MCP server
32
+
33
+ Add to your project's `.mcp.json` (or `~/.mcp.json` for global):
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "canon-channel": {
39
+ "command": "canon-channel-server"
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ### 2. Register your agent
46
+
47
+ Start Claude Code and run `/canon-register`. You'll be asked for:
48
+
49
+ 1. Agent name
50
+ 2. Agent description
51
+ 3. Your phone number (owner of the Canon account)
52
+
53
+ Then approve the registration in your Canon app. The API key is saved automatically.
54
+
55
+ If you already have an API key, run `/canon-configure` instead.
56
+
57
+ ### 3. Connect
58
+
59
+ Start Claude Code with the channel enabled:
60
+
61
+ ```bash
62
+ claude --dangerously-load-development-channels server:canon-channel
63
+ ```
64
+
65
+ Send a message to your agent in Canon — it will appear in your Claude Code session.
66
+
67
+ ## Tools
68
+
69
+ | Tool | Description |
70
+ |------|-------------|
71
+ | `reply` | Reply to a Canon conversation |
72
+ | `send_message` | Send a message (text, image, or audio) |
73
+ | `list_conversations` | List conversations the agent is in |
74
+ | `set_typing` | Show/hide typing indicator |
75
+
76
+ ## Configuration
77
+
78
+ The API key is stored at `~/.claude/channels/canon/.env`. To change it, run `/canon-configure` again.
79
+
80
+ ## Troubleshooting
81
+
82
+ ### SSE connection limit
83
+
84
+ Canon allows 5 concurrent SSE connections per API key. If you restart Claude Code rapidly, stale connections may still be open. Wait 1-2 minutes for them to expire.
85
+
86
+ ### DNS issues
87
+
88
+ On some networks, Node.js DNS resolution may fail. The plugin includes a built-in IPv4-first fix, but if `curl` commands in the registration skill also fail, try a different network.
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ cd packages/claude-code-plugin
94
+ npm install
95
+ npm run build
96
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { setDefaultResultOrder } from 'node:dns';
3
+ setDefaultResultOrder('ipv4first');
4
+ /**
5
+ * CLI script for Canon agent registration.
6
+ * Invoked by the /canon:register skill.
7
+ *
8
+ * Usage:
9
+ * node register.js --name "My Agent" --description "What it does" --phone "+15551234567"
10
+ */
11
+ import { registerAndWaitForApproval } from '@canonapp/core';
12
+ import { parseArgs } from 'node:util';
13
+ const { values } = parseArgs({
14
+ options: {
15
+ name: { type: 'string' },
16
+ description: { type: 'string' },
17
+ phone: { type: 'string' },
18
+ 'base-url': { type: 'string' },
19
+ },
20
+ strict: true,
21
+ });
22
+ if (!values.name || !values.description || !values.phone) {
23
+ console.error('Usage: node register.js --name "Agent Name" --description "Description" --phone "+15551234567"');
24
+ process.exit(1);
25
+ }
26
+ console.log(`Registering agent "${values.name}"...`);
27
+ const result = await registerAndWaitForApproval({
28
+ name: values.name,
29
+ description: values.description,
30
+ ownerPhone: values.phone,
31
+ developerInfo: 'Claude Code plugin',
32
+ baseUrl: values['base-url'],
33
+ }, {
34
+ onSubmitted: (requestId) => {
35
+ console.log(`Registration submitted (request ID: ${requestId}).`);
36
+ console.log('Waiting for approval in Canon app...');
37
+ },
38
+ onPollUpdate: () => {
39
+ process.stdout.write('.');
40
+ },
41
+ });
42
+ console.log(''); // newline after dots
43
+ switch (result.status) {
44
+ case 'approved':
45
+ console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
46
+ console.log(`API_KEY=${result.apiKey}`);
47
+ break;
48
+ case 'rejected':
49
+ console.log('Registration was rejected.');
50
+ process.exit(1);
51
+ break;
52
+ case 'timeout':
53
+ console.log('Registration timed out (5 minutes). Try again later.');
54
+ process.exit(1);
55
+ break;
56
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+ import { setDefaultResultOrder } from 'node:dns';
3
+ setDefaultResultOrder('ipv4first');
4
+ /**
5
+ * Canon channel MCP server for Claude Code.
6
+ *
7
+ * Connects to Canon's SSE stream for real-time inbound messages and exposes
8
+ * tools for Claude to reply, send messages, list conversations, and set
9
+ * typing indicators.
10
+ */
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
14
+ import { CanonClient, CanonStream, } from '@canonapp/core';
15
+ import { readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ // ── Config resolution ──────────────────────────────────────────────────
18
+ function resolveApiKey() {
19
+ // 1. Environment variable (set by plugin.json or shell)
20
+ if (process.env.CANON_API_KEY) {
21
+ return process.env.CANON_API_KEY;
22
+ }
23
+ // 2. .env file at ~/.claude/channels/canon/.env
24
+ try {
25
+ const home = process.env.HOME || process.env.USERPROFILE || '';
26
+ const envPath = join(home, '.claude', 'channels', 'canon', '.env');
27
+ const content = readFileSync(envPath, 'utf-8');
28
+ for (const line of content.split('\n')) {
29
+ const match = line.match(/^CANON_API_KEY=(.+)$/);
30
+ if (match)
31
+ return match[1].trim();
32
+ }
33
+ }
34
+ catch {
35
+ // File doesn't exist — fall through
36
+ }
37
+ return '';
38
+ }
39
+ // ── MCP server ─────────────────────────────────────────────────────────
40
+ const server = new Server({ name: 'canon-channel', version: '0.1.0' }, {
41
+ capabilities: {
42
+ tools: {},
43
+ experimental: { 'claude/channel': {} },
44
+ },
45
+ });
46
+ let client = null;
47
+ let stream = null;
48
+ let agentContext = null;
49
+ const conversationCache = new Map();
50
+ // ── Tool definitions ───────────────────────────────────────────────────
51
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
52
+ tools: [
53
+ {
54
+ name: 'reply',
55
+ description: 'Reply to a Canon conversation',
56
+ inputSchema: {
57
+ type: 'object',
58
+ properties: {
59
+ conversation_id: {
60
+ type: 'string',
61
+ description: 'The conversation ID to reply to',
62
+ },
63
+ text: {
64
+ type: 'string',
65
+ description: 'Message text to send',
66
+ },
67
+ },
68
+ required: ['conversation_id', 'text'],
69
+ },
70
+ },
71
+ {
72
+ name: 'send_message',
73
+ description: 'Send a message to a Canon conversation (supports text, images, audio)',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ conversation_id: {
78
+ type: 'string',
79
+ description: 'The conversation ID',
80
+ },
81
+ text: {
82
+ type: 'string',
83
+ description: 'Message text',
84
+ },
85
+ content_type: {
86
+ type: 'string',
87
+ enum: ['text', 'image', 'audio'],
88
+ description: 'Content type (default: text)',
89
+ },
90
+ image_url: {
91
+ type: 'string',
92
+ description: 'Image URL (when content_type is image)',
93
+ },
94
+ audio_url: {
95
+ type: 'string',
96
+ description: 'Audio URL (when content_type is audio)',
97
+ },
98
+ },
99
+ required: ['conversation_id', 'text'],
100
+ },
101
+ },
102
+ {
103
+ name: 'list_conversations',
104
+ description: "List Canon conversations this agent participates in",
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ limit: {
109
+ type: 'number',
110
+ description: 'Max conversations to return (default: 20)',
111
+ },
112
+ },
113
+ },
114
+ },
115
+ {
116
+ name: 'set_typing',
117
+ description: 'Show or hide typing indicator in a Canon conversation',
118
+ inputSchema: {
119
+ type: 'object',
120
+ properties: {
121
+ conversation_id: {
122
+ type: 'string',
123
+ description: 'The conversation ID',
124
+ },
125
+ typing: {
126
+ type: 'boolean',
127
+ description: 'Whether to show typing indicator',
128
+ },
129
+ },
130
+ required: ['conversation_id', 'typing'],
131
+ },
132
+ },
133
+ ],
134
+ }));
135
+ // ── Tool handlers ──────────────────────────────────────────────────────
136
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
137
+ if (!client) {
138
+ return {
139
+ content: [{ type: 'text', text: 'Canon not connected' }],
140
+ isError: true,
141
+ };
142
+ }
143
+ const args = request.params.arguments;
144
+ switch (request.params.name) {
145
+ case 'reply': {
146
+ const conversationId = args.conversation_id;
147
+ const text = args.text;
148
+ const result = await client.sendMessage(conversationId, text);
149
+ return {
150
+ content: [
151
+ {
152
+ type: 'text',
153
+ text: `Message sent (${result.messageId})`,
154
+ },
155
+ ],
156
+ };
157
+ }
158
+ case 'send_message': {
159
+ const conversationId = args.conversation_id;
160
+ const text = args.text;
161
+ const contentType = args.content_type || 'text';
162
+ const opts = {
163
+ contentType: contentType,
164
+ };
165
+ if (args.image_url)
166
+ opts.imageUrl = args.image_url;
167
+ if (args.audio_url)
168
+ opts.audioUrl = args.audio_url;
169
+ const result = await client.sendMessage(conversationId, text, opts);
170
+ return {
171
+ content: [
172
+ {
173
+ type: 'text',
174
+ text: `Message sent (${result.messageId})`,
175
+ },
176
+ ],
177
+ };
178
+ }
179
+ case 'list_conversations': {
180
+ const limit = args.limit || 20;
181
+ const convos = await client.getConversations();
182
+ const slice = convos.slice(0, limit);
183
+ const summary = slice.map((c) => ({
184
+ id: c.id,
185
+ type: c.type,
186
+ name: c.name,
187
+ members: c.memberIds.length,
188
+ lastMessage: c.lastMessage?.text?.slice(0, 100) || null,
189
+ }));
190
+ return {
191
+ content: [
192
+ { type: 'text', text: JSON.stringify(summary, null, 2) },
193
+ ],
194
+ };
195
+ }
196
+ case 'set_typing': {
197
+ const conversationId = args.conversation_id;
198
+ const typing = args.typing;
199
+ await client.setTyping(conversationId, typing);
200
+ return {
201
+ content: [
202
+ {
203
+ type: 'text',
204
+ text: typing ? 'Typing indicator shown' : 'Typing indicator hidden',
205
+ },
206
+ ],
207
+ };
208
+ }
209
+ default:
210
+ return {
211
+ content: [
212
+ { type: 'text', text: `Unknown tool: ${request.params.name}` },
213
+ ],
214
+ isError: true,
215
+ };
216
+ }
217
+ });
218
+ // ── Inbound message handler ────────────────────────────────────────────
219
+ async function handleInboundMessage(payload) {
220
+ const m = payload.message;
221
+ const convo = conversationCache.get(payload.conversationId);
222
+ // meta must be Record<string, string> — non-string values cause silent drops
223
+ const meta = {
224
+ conversation_id: payload.conversationId,
225
+ sender_id: m.senderId,
226
+ sender_name: m.senderName || m.senderId,
227
+ sender_type: m.senderType || 'human',
228
+ is_owner: String(m.isOwner ?? false),
229
+ chat_type: convo?.type || 'direct',
230
+ content_type: m.contentType || 'text',
231
+ ts: new Date().toISOString(),
232
+ };
233
+ if (m.imageUrl)
234
+ meta.image_url = m.imageUrl;
235
+ if (m.audioUrl)
236
+ meta.audio_url = m.audioUrl;
237
+ console.error(`[canon] Inbound message from ${meta.sender_name}: "${m.text}"`);
238
+ try {
239
+ await server.notification({
240
+ method: 'notifications/claude/channel',
241
+ params: {
242
+ content: m.text || '',
243
+ meta,
244
+ },
245
+ });
246
+ console.error('[canon] Notification sent to Claude Code');
247
+ }
248
+ catch (err) {
249
+ const msg = err instanceof Error ? err.message : String(err);
250
+ console.error(`[canon] Failed to send notification: ${msg}`);
251
+ }
252
+ }
253
+ // ── Start ──────────────────────────────────────────────────────────────
254
+ async function startChannel() {
255
+ const apiKey = resolveApiKey();
256
+ if (!apiKey) {
257
+ console.error('[canon] No API key found. Run /canon:configure or /canon:register to set up.');
258
+ return;
259
+ }
260
+ client = new CanonClient(apiKey);
261
+ // Get agent identity — try /agents/me first, fall back to /agents/auth-token
262
+ let agentId;
263
+ try {
264
+ agentContext = await client.getAgentMe();
265
+ agentId = agentContext.agentId;
266
+ console.error(`[canon] Connected as ${agentContext.displayName || agentId}`);
267
+ }
268
+ catch {
269
+ // /agents/me may not be deployed — fall back to auth-token exchange
270
+ try {
271
+ const auth = await client.getAuthToken();
272
+ agentId = auth.agentId;
273
+ console.error(`[canon] Authenticated as ${agentId}`);
274
+ }
275
+ catch (err) {
276
+ const msg = err instanceof Error ? err.message : String(err);
277
+ console.error(`[canon] Failed to authenticate: ${msg}`);
278
+ return;
279
+ }
280
+ }
281
+ // Populate conversation cache
282
+ try {
283
+ const convos = await client.getConversations();
284
+ for (const c of convos)
285
+ conversationCache.set(c.id, c);
286
+ console.error(`[canon] Loaded ${convos.length} conversations`);
287
+ }
288
+ catch {
289
+ // Non-fatal — will be populated as messages arrive
290
+ }
291
+ // Start SSE stream
292
+ stream = new CanonStream({
293
+ apiKey,
294
+ agentId,
295
+ handler: {
296
+ onMessage: handleInboundMessage,
297
+ onAgentContext: (ctx) => {
298
+ agentContext = ctx;
299
+ },
300
+ onConnected: () => {
301
+ console.error('[canon] SSE stream connected');
302
+ },
303
+ onDisconnected: () => {
304
+ console.error('[canon] SSE stream disconnected');
305
+ },
306
+ onError: (err) => {
307
+ console.error(`[canon] SSE error: ${err.message}`);
308
+ },
309
+ },
310
+ });
311
+ stream.start().catch((err) => {
312
+ const msg = err instanceof Error ? err.message : String(err);
313
+ console.error(`[canon] SSE start error: ${msg}`);
314
+ });
315
+ }
316
+ // ── Main ───────────────────────────────────────────────────────────────
317
+ async function main() {
318
+ const transport = new StdioServerTransport();
319
+ await server.connect(transport);
320
+ // Start the Canon channel after MCP connection is established
321
+ await startChannel();
322
+ // Graceful shutdown
323
+ const shutdown = () => {
324
+ stream?.stop();
325
+ process.exit(0);
326
+ };
327
+ process.on('SIGINT', shutdown);
328
+ process.on('SIGTERM', shutdown);
329
+ }
330
+ main().catch((err) => {
331
+ console.error('[canon] Fatal error:', err);
332
+ process.exit(1);
333
+ });
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Setup script for the Canon Claude Code plugin.
4
+ *
5
+ * Run after `npm install -g @canon/claude-code-plugin` to:
6
+ * 1. Install the /canon-register and /canon-configure skills
7
+ * 2. Show the user how to add the MCP server to their .mcp.json
8
+ */
9
+ export {};
package/dist/setup.js ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Setup script for the Canon Claude Code plugin.
4
+ *
5
+ * Run after `npm install -g @canon/claude-code-plugin` to:
6
+ * 1. Install the /canon-register and /canon-configure skills
7
+ * 2. Show the user how to add the MCP server to their .mcp.json
8
+ */
9
+ import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
10
+ import { join, dirname } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const home = process.env.HOME || process.env.USERPROFILE || '';
14
+ const skillsDir = join(home, '.claude', 'skills');
15
+ // ── Install skills ─────────────────────────────────────────────────────
16
+ function installSkills() {
17
+ const pluginRoot = join(__dirname, '..');
18
+ const sourceSkills = join(pluginRoot, 'skills');
19
+ const skills = ['register', 'configure'];
20
+ for (const skill of skills) {
21
+ const srcFile = join(sourceSkills, skill, 'SKILL.md');
22
+ const destDir = join(skillsDir, `canon-${skill}`);
23
+ const destFile = join(destDir, 'SKILL.md');
24
+ if (!existsSync(srcFile)) {
25
+ console.log(` Skipping canon-${skill} (source not found at ${srcFile})`);
26
+ continue;
27
+ }
28
+ mkdirSync(destDir, { recursive: true });
29
+ // Read the skill and replace ${CLAUDE_PLUGIN_ROOT} with the actual installed path
30
+ let content = readFileSync(srcFile, 'utf-8');
31
+ content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginRoot);
32
+ writeFileSync(destFile, content);
33
+ console.log(` Installed /canon-${skill}`);
34
+ }
35
+ }
36
+ // ── Show MCP config instructions ───────────────────────────────────────
37
+ function showMcpInstructions() {
38
+ // Find the installed server.js path
39
+ const serverPath = join(__dirname, 'server.js');
40
+ console.log(`
41
+ Add this to your project's .mcp.json (or ~/.mcp.json for global):
42
+
43
+ {
44
+ "mcpServers": {
45
+ "canon-channel": {
46
+ "command": "node",
47
+ "args": ["${serverPath}"]
48
+ }
49
+ }
50
+ }
51
+
52
+ Then start Claude Code with:
53
+
54
+ claude --dangerously-load-development-channels server:canon-channel
55
+ `);
56
+ }
57
+ // ── Main ───────────────────────────────────────────────────────────────
58
+ console.log('Canon Claude Code Plugin Setup');
59
+ console.log('==============================\n');
60
+ console.log('Installing skills...');
61
+ installSkills();
62
+ console.log('\nRegistering your agent...');
63
+ console.log(' Run /canon-register in Claude Code to register a new agent');
64
+ console.log(' Run /canon-configure if you already have an API key\n');
65
+ console.log('MCP server configuration:');
66
+ showMcpInstructions();
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@canonapp/claude-code-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "bin": {
8
+ "canon-channel-server": "dist/server.js",
9
+ "canon-register": "dist/register.js",
10
+ "canon-setup": "dist/setup.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "skills",
15
+ ".claude-plugin"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "dev": "tsc --watch",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "@canonapp/core": "^0.1.0",
24
+ "@modelcontextprotocol/sdk": "^1.12.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "keywords": [
30
+ "canon",
31
+ "claude-code",
32
+ "channels",
33
+ "mcp",
34
+ "ai-agents",
35
+ "messaging"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/HeyBobChan/canon",
40
+ "directory": "packages/claude-code-plugin"
41
+ },
42
+ "homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/claude-code-plugin",
43
+ "devDependencies": {
44
+ "@types/node": "^22.0.0",
45
+ "typescript": "~5.7.0"
46
+ },
47
+ "license": "MIT"
48
+ }
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: canon-configure
3
+ description: Configure an existing Canon agent API key
4
+ user-invocable: true
5
+ allowed-tools:
6
+ - Bash(mkdir *)
7
+ - Bash(chmod *)
8
+ - Write
9
+ - Read
10
+ ---
11
+
12
+ # Configure Canon API Key
13
+
14
+ Set up the Canon channel with an existing agent API key.
15
+
16
+ ## Steps
17
+
18
+ 1. Ask the user for their Canon agent API key. It should start with `agk_live_`.
19
+
20
+ 2. Validate the format — it must start with `agk_live_` and be non-empty.
21
+
22
+ 3. Store the API key:
23
+ ```bash
24
+ mkdir -p ~/.claude/channels/canon
25
+ ```
26
+ Then write to `~/.claude/channels/canon/.env`:
27
+ ```
28
+ CANON_API_KEY=<the_api_key>
29
+ ```
30
+ Then set permissions:
31
+ ```bash
32
+ chmod 600 ~/.claude/channels/canon/.env
33
+ ```
34
+
35
+ 4. Tell the user: "Canon API key configured. Restart Claude Code or run /reload-plugins to connect."
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: canon-register
3
+ description: Register a new Canon agent and get an API key
4
+ user-invocable: true
5
+ allowed-tools:
6
+ - Bash(curl *)
7
+ - Bash(mkdir *)
8
+ - Bash(chmod *)
9
+ - Write
10
+ - Read
11
+ ---
12
+
13
+ # Canon Agent Registration
14
+
15
+ Register a new Canon agent so it can send and receive messages via the Canon channel.
16
+
17
+ ## Steps
18
+
19
+ 1. Ask the user for:
20
+ - **Agent name** — The display name for the agent in Canon
21
+ - **Description** — What the agent does (shown to users in Canon)
22
+ - **Owner phone number** — The Canon account owner's phone number in E.164 format (e.g., +15551234567)
23
+
24
+ 2. Submit the registration request:
25
+ ```bash
26
+ curl -s -X POST https://api-6m6mlelskq-uc.a.run.app/agents/register \
27
+ -H "Content-Type: application/json" \
28
+ -d '{"name":"<name>","description":"<description>","ownerPhone":"<phone>","developerInfo":"Claude Code channel"}'
29
+ ```
30
+ This returns JSON with a `requestId` field.
31
+
32
+ 3. Tell the user: **"Open your Canon app and approve the agent registration request. I'll poll for up to 5 minutes."**
33
+
34
+ 4. Poll for approval every 3 seconds:
35
+ ```bash
36
+ curl -s https://api-6m6mlelskq-uc.a.run.app/agents/status/<requestId>
37
+ ```
38
+ This returns JSON with `status` (pending/approved/rejected), `agentName`, `agentId`, and `apiKey` (on approval).
39
+
40
+ Keep polling while `status` is `"pending"`. Stop after 5 minutes (100 attempts).
41
+
42
+ 5. On approval, parse the `apiKey` from the response and store it:
43
+ ```bash
44
+ mkdir -p ~/.claude/channels/canon
45
+ ```
46
+ Then write to `~/.claude/channels/canon/.env`:
47
+ ```
48
+ CANON_API_KEY=<the_api_key>
49
+ ```
50
+ Then set permissions:
51
+ ```bash
52
+ chmod 600 ~/.claude/channels/canon/.env
53
+ ```
54
+
55
+ 6. Tell the user: **"Registration complete! Restart Claude Code to connect the Canon channel."**
56
+
57
+ ## Error handling
58
+
59
+ - If the registration POST fails, show the error and ask the user to retry.
60
+ - If status returns `"rejected"`, tell the user the registration was rejected by the owner.
61
+ - If polling times out after 5 minutes, tell the user to try again later.
62
+ - If the phone number format is wrong (must start with `+`, 8-16 digits), ask the user to re-enter it.