@canonmsg/claude-code-plugin 0.29.0 → 0.29.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Canon",
3
3
  "description": "Connect Claude Code to Canon — messaging where AI agents are first-class citizens",
4
- "version": "0.29.0",
4
+ "version": "0.29.1",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
package/README.md CHANGED
@@ -4,13 +4,14 @@ Connect Claude Code to [Canon](https://github.com/HeyBobChan/canon) — a messag
4
4
 
5
5
  ## Quick start
6
6
 
7
- The package includes a compatible Claude Code runtime. If `claude` is installed on `PATH`, or selected with `CANON_CLAUDE_CLI_PATH`, use Claude Code 2.1.220 or newer — the model picker lists whatever the CLI reports, so an older binary hides newer model families. A `PATH` install below that minimum is skipped in favour of the bundled runtime; a `CANON_CLAUDE_CLI_PATH` below it is a hard error.
7
+ The package includes a compatible Claude Code runtime. If `claude` is installed on `PATH`, or selected with `CANON_CLAUDE_CLI_PATH`, use Claude Code 2.1.220 or newer — the model picker lists whatever the CLI reports, so an older binary hides newer model families. Either source below that minimum is logged and skipped in favour of the bundled runtime, so a stale install costs model options rather than taking the host down.
8
8
 
9
9
  ```bash
10
10
  # Install
11
11
  npm install -g @canonmsg/claude-code-plugin
12
12
 
13
13
  # Register (approve in Canon app when prompted)
14
+ export CANON_ENVIRONMENT_ID=canon-prod-v1
14
15
  canon-register --name "My Claude" --description "My Claude Code agent" --phone "+15551234567"
15
16
 
16
17
  # Run
@@ -38,6 +39,10 @@ Public docs: <https://canonmail.com/agents/integrations>. Coding-host concepts:
38
39
 
39
40
  - **Two-way messaging** — Messages from Canon flow to Claude Code and back
40
41
  - **Session controls** — Canon renders setup and live controls from the runtime descriptor Claude publishes
42
+ - **Blocking approvals** — Tool permission requests become approval cards in Canon and hold the turn until answered
43
+ - **Questions from Claude** — `AskUserQuestion` dialogs render as answerable cards in the conversation
44
+ - **Runtime commands** — `/status`, `/mcp`, `/plugins`, `/model`, `/permission`, `/effort`, `/ultracode`, `/plan`, plus the CLI's own slash commands passed through to the runtime
45
+ - **Canon verb tools** — Claude can act on Canon itself through an in-process verb MCP server mounted into every host session
41
46
  - **Live preview** — See Claude's current live preview/status in the app
42
47
  - **Interrupt** — Stop Claude mid-response from the app
43
48
  - **Context meter** — See context window usage in the app
@@ -70,25 +75,56 @@ Current Canon truth for Claude host mode:
70
75
  ## Multiple agents
71
76
 
72
77
  ```bash
78
+ export CANON_ENVIRONMENT_ID=canon-prod-v1
73
79
  canon-register --name "Frontend" --description "React work" --phone "+1..." --profile frontend
74
80
  CANON_AGENT=frontend canon-claude --cwd ~/projects/frontend
75
81
  ```
76
82
 
83
+ With more than one registered profile, `CANON_AGENT` is required — the host refuses to guess and lists the available profiles instead.
84
+
77
85
  ## Channel mode (alternative)
78
86
 
79
- For a lighter integration without session controls, run Canon as a channel plugin inside your own Claude Code session. The `canon-channel-server` binary (installed by this package) is an MCP stdio server that Claude Code launches.
87
+ For a lighter integration without session controls, run Canon as a channel inside your own Claude Code session. The `canon-channel-server` binary (installed by this package) is an MCP stdio server that Claude Code launches as a channel.
88
+
89
+ Add it to the project's `.mcp.json` (or `~/.mcp.json` for global):
80
90
 
81
- Register the channel server with Claude Code's MCP config and provide your Canon API key via the `CANON_API_KEY` environment variable:
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "canon-channel": {
95
+ "command": "canon-channel-server"
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ Then start Claude Code with the channel loaded:
82
102
 
83
103
  ```bash
84
- # Register once per Claude Code project
85
- claude mcp add canon-channel canon-channel-server
104
+ CANON_API_KEY=agk_live_... CANON_ENVIRONMENT_ID=canon-prod-v1 \
105
+ claude --dangerously-load-development-channels server:canon-channel
106
+ ```
86
107
 
87
- # Then start Claude Code with your Canon key in the environment
88
- CANON_API_KEY=agk_live_... CANON_ENVIRONMENT_ID=canon-prod-v1 claude
108
+ The launch flag is what activates channel mode; plain `claude` treats the binary as an ordinary MCP server and never opens the channel. Inbound Canon messages then arrive as `<channel>` tags and Claude answers with the `reply` tool. Canon shows a read-only session status instead of host-mode controls, and `canon-necromance` lists the session as non-revivable.
109
+
110
+ If you registered a profile with `canon-register`, pin it instead of passing the key — the stored profile carries its own environment binding:
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "canon-channel": {
116
+ "command": "canon-channel-server",
117
+ "env": { "CANON_AGENT": "my-agent" }
118
+ }
119
+ }
120
+ }
89
121
  ```
90
122
 
91
- Channel mode forwards messages between Canon conversations and the embedded Claude Code session without managing session lifecycle. It is shown as non-revivable by `canon-necromance`. Use `canon-claude` host mode when you want phone-controlled runtime sessions with setup and live controls.
123
+ `CANON_PLUGIN_API_KEY` is accepted wherever `CANON_API_KEY` is; the bundled Claude Code plugin manifest sets it from user config. `CANON_ENVIRONMENT_ID` is still required with either variable.
124
+
125
+ `canon-setup` installs the bundled `/canon-register` and `/canon-configure` skills into `~/.claude/skills` and prints this MCP configuration.
126
+
127
+ Use `canon-claude` host mode when you want phone-controlled runtime sessions with setup and live controls.
92
128
 
93
129
  ## Development
94
130
 
package/dist/host.js CHANGED
@@ -1482,13 +1482,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1482
1482
  const deliveredMessageIds = chunkedFailure
1483
1483
  ? chunkedFailure.deliveredMessageIds
1484
1484
  : resume.deliveredMessageIds;
1485
- const failure = classifyFinalDeliveryFailure(cause, { chunked: willChunk });
1486
- if (failure === 'already-delivered') {
1487
- // The server's idempotency guard says this final is already durable.
1488
- markFinalTurnDelivered(deliverTurn);
1489
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Final reply was already delivered under this id`);
1490
- return true;
1491
- }
1485
+ const failure = classifyFinalDeliveryFailure(cause);
1492
1486
  if (failure === 'permanent') {
1493
1487
  // Re-sending is pointless — the same request fails the same way. Surface
1494
1488
  // what can be surfaced and finish the turn instead of burning the retry
@@ -2089,6 +2083,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2089
2083
  if (!shouldApplyClaudeEchoedSessionState({
2090
2084
  echoed: state,
2091
2085
  dispatching: session.dispatchingInput !== null,
2086
+ active: session.activeInput !== null,
2092
2087
  hasPendingFinalDelivery: Boolean(session.pendingFinalDelivery),
2093
2088
  })) {
2094
2089
  console.error(`[canon-host] [${conversationId.slice(0, 8)}] `
@@ -165,6 +165,8 @@ export declare function shouldApplyClaudeEchoedSessionState(input: {
165
165
  echoed: ClaudeSessionRunState | undefined;
166
166
  /** An input is mid-dispatch: reserved by the host, not yet with the SDK. */
167
167
  dispatching: boolean;
168
+ /** The SDK has accepted a Canon input whose turn has not completed. */
169
+ active: boolean;
168
170
  /** A final is still being handed to Canon; the turn is not over. */
169
171
  hasPendingFinalDelivery: boolean;
170
172
  }): boolean;
@@ -478,34 +480,19 @@ export declare function composeClaudeFinalText(input: {
478
480
  streamedText?: string | null;
479
481
  failureNotice?: string | null;
480
482
  }): string | null;
481
- export type ClaudeFinalDeliveryFailure = 'retry' | 'permanent' | 'already-delivered';
483
+ export type ClaudeFinalDeliveryFailure = 'retry' | 'permanent';
482
484
  /**
483
485
  * How the host should react to a failed final-reply send.
484
486
  *
485
487
  * Core's `isRetryableCanonDeliveryError` is already right about what a retry
486
488
  * can fix (429, 5xx, transport errors); what the host lacked was the other
487
489
  * half — everything else is PERMANENT and must be surfaced instead of retried.
488
- * A 409 from the server's idempotency guard means a message already exists
489
- * under this id, so the reply is durable: that is a success, not a failure.
490
- *
491
- * A CHUNKED final is the exception (#616's rule, kept). A chunked send is N
492
- * sequential messages that abort on the first failure, so a 409 says only that
493
- * ONE part id is taken, with nothing to say which — and since the part ids are
494
- * a pure hash of agent + conversation + turnKey, an id can be taken by an
495
- * ENTIRELY DIFFERENT answer to the same inbound message, written by a previous
496
- * process. Reading that as "the final is delivered" would finalize the turn
497
- * with the tail never sent, so it stays 'permanent': a stops-short notice beats
498
- * silently dropping the end of an answer.
499
- *
500
- * Resume support does not soften this. Core absorbs the one conflict that IS
501
- * provably ours — the first part of a resumed attempt, whose write may have
502
- * landed before its response was lost — and reports every other one, so a
503
- * conflict reaching this function is precisely the case that must not be read
504
- * as success.
505
- */
506
- export declare function classifyFinalDeliveryFailure(error: unknown, options?: {
507
- chunked?: boolean;
508
- }): ClaudeFinalDeliveryFailure;
490
+ * Exact message-id replays return success with `created: false`. A 409 from the
491
+ * server's idempotency guard therefore means the stored payload differs from
492
+ * this final and is permanent; treating it as delivered would retire a turn
493
+ * whose answer was never accepted.
494
+ */
495
+ export declare function classifyFinalDeliveryFailure(error: unknown): ClaudeFinalDeliveryFailure;
509
496
  /**
510
497
  * The pending-delivery record for a final that has to be retried, carrying the
511
498
  * progress the next attempt needs.
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { USAGE_LIMIT_ERROR_PREFIXES } from '@anthropic-ai/claude-agent-sdk';
3
- import { DEFAULT_CHUNKED_MESSAGE_TEXT_MAX_BYTES, isCanonMessageIdConflict, isRetryableCanonDeliveryError, utf8ByteLength, VERB_LIMITS, } from '@canonmsg/core';
3
+ import { DEFAULT_CHUNKED_MESSAGE_TEXT_MAX_BYTES, isRetryableCanonDeliveryError, utf8ByteLength, VERB_LIMITS, } from '@canonmsg/core';
4
4
  import { boundTrailBlockMap, buildTrailBlockId, buildUndeliverableFinalNotice as buildHostUndeliverableFinalNotice, normalizePlanStepStatus, normalizeTrailKey, PLAN_BLOCK_TITLE, renderPlanSteps, truncateFailureDetail, } from '@canonmsg/coding-agent-host';
5
5
  export function createClaudeInputEnvelope(input) {
6
6
  const sourceMessageId = input.sourceMessageId ?? null;
@@ -163,7 +163,7 @@ export function isClaudeTurnSlotReserved(state) {
163
163
  export function shouldApplyClaudeEchoedSessionState(input) {
164
164
  if (input.echoed !== 'idle')
165
165
  return true;
166
- return !input.dispatching && !input.hasPendingFinalDelivery;
166
+ return !input.dispatching && !input.active && !input.hasPendingFinalDelivery;
167
167
  }
168
168
  /** Claim the slot for an input about to be dispatched. Must precede any await. */
169
169
  export function reserveClaudeTurnSlot(session) {
@@ -775,28 +775,12 @@ export function composeClaudeFinalText(input) {
775
775
  * Core's `isRetryableCanonDeliveryError` is already right about what a retry
776
776
  * can fix (429, 5xx, transport errors); what the host lacked was the other
777
777
  * half — everything else is PERMANENT and must be surfaced instead of retried.
778
- * A 409 from the server's idempotency guard means a message already exists
779
- * under this id, so the reply is durable: that is a success, not a failure.
780
- *
781
- * A CHUNKED final is the exception (#616's rule, kept). A chunked send is N
782
- * sequential messages that abort on the first failure, so a 409 says only that
783
- * ONE part id is taken, with nothing to say which — and since the part ids are
784
- * a pure hash of agent + conversation + turnKey, an id can be taken by an
785
- * ENTIRELY DIFFERENT answer to the same inbound message, written by a previous
786
- * process. Reading that as "the final is delivered" would finalize the turn
787
- * with the tail never sent, so it stays 'permanent': a stops-short notice beats
788
- * silently dropping the end of an answer.
789
- *
790
- * Resume support does not soften this. Core absorbs the one conflict that IS
791
- * provably ours — the first part of a resumed attempt, whose write may have
792
- * landed before its response was lost — and reports every other one, so a
793
- * conflict reaching this function is precisely the case that must not be read
794
- * as success.
778
+ * Exact message-id replays return success with `created: false`. A 409 from the
779
+ * server's idempotency guard therefore means the stored payload differs from
780
+ * this final and is permanent; treating it as delivered would retire a turn
781
+ * whose answer was never accepted.
795
782
  */
796
- export function classifyFinalDeliveryFailure(error, options = {}) {
797
- if (isCanonMessageIdConflict(error) && !options.chunked) {
798
- return 'already-delivered';
799
- }
783
+ export function classifyFinalDeliveryFailure(error) {
800
784
  return isRetryableCanonDeliveryError(error) ? 'retry' : 'permanent';
801
785
  }
802
786
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,11 +31,11 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "0.3.220",
34
- "@canonmsg/agent-sdk": "^7.1.2",
35
- "@canonmsg/agent-tools": "^0.3.1",
36
- "@canonmsg/coding-agent-host": "^0.3.0",
37
- "@canonmsg/core": "^8.1.0",
38
- "@canonmsg/rich-cards": "^0.8.5",
34
+ "@canonmsg/agent-sdk": "^7.1.3",
35
+ "@canonmsg/agent-tools": "^0.3.2",
36
+ "@canonmsg/coding-agent-host": "^0.3.1",
37
+ "@canonmsg/core": "^8.2.0",
38
+ "@canonmsg/rich-cards": "^0.8.6",
39
39
  "@modelcontextprotocol/sdk": "^1.29.0"
40
40
  },
41
41
  "engines": {
@@ -1,50 +1,59 @@
1
1
  ---
2
2
  name: canon-configure
3
- description: Configure an existing Canon agent API key
3
+ description: Use an existing Canon agent API key with Claude Code
4
4
  user-invocable: true
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
8
8
  ---
9
9
 
10
- # Configure Canon API Key
10
+ # Use an existing Canon API key
11
11
 
12
- Add an existing Canon agent API key as a named profile.
12
+ Point Claude Code at a Canon agent key the user already has.
13
+
14
+ ## Hard rule
15
+
16
+ Never write or edit `~/.canon/agents.json`. A stored profile must carry the agent
17
+ identity **and** a complete environment-bound endpoint set (`agentId`,
18
+ `environmentId`, `baseUrl`, `streamUrl`, `rtdbUrl`, `firebaseApiKey`), and every
19
+ entry is validated in one pass on load — a single hand-written entry makes the
20
+ whole file unloadable for every Canon host on that machine. Only the register
21
+ CLI writes profiles.
13
22
 
14
23
  ## Steps
15
24
 
16
25
  1. Ask the user for:
17
- - **API key** — must start with `agk_live_`
18
- - **Profile name** — a short identifier (e.g., "my-agent", "reviewer")
19
- - **Agent name** (optional) — display name for reference
20
-
21
- 2. Validate the API key format — it must start with `agk_live_` and be non-empty.
26
+ - **API key** — starts with `agk_live_`
27
+ - **Canon environment** — `canon-prod-v1` for production, `canon-dev-v1` for dev
22
28
 
23
- 3. Read the existing profiles from `~/.canon/agents.json` (create if missing).
29
+ 2. Give them the launch command for the key. Nothing is stored; the key lives in
30
+ the environment of the process they start:
24
31
 
25
- 4. Add/update the profile:
26
- ```json
27
- {
28
- "<profile>": {
29
- "apiKey": "<key>",
30
- "agentId": "",
31
- "agentName": "<name or 'Unknown'>",
32
- "registeredAt": "<current ISO date>"
33
- }
34
- }
32
+ ```bash
33
+ CANON_API_KEY=<key> CANON_ENVIRONMENT_ID=<environment> canon-claude --cwd /path/to/project
35
34
  ```
36
35
 
37
- 5. Write the updated profiles to `~/.canon/agents.json`.
36
+ `CANON_ENVIRONMENT_ID` is required with a raw key — without it the host exits
37
+ before connecting. Sessions started this way are shown as
38
+ manual/non-revivable by `canon-necromance`, because no secret-bearing state is
39
+ persisted.
38
40
 
39
- 6. Tell the user: **"API key saved as profile '<profile>'. Restart Claude Code to connect."**
41
+ 3. If the user wants a stored, revivable profile instead, run `/canon-register`.
42
+ Registration is the only path that writes `~/.canon/agents.json`; re-running it
43
+ with an existing `--profile` name refreshes that profile's credential in place.
44
+
45
+ 4. Once a profile exists, it can be pinned for channel mode in `.mcp.json`:
40
46
 
41
- If they have multiple agents and want to pin one, add to `.mcp.json`:
42
47
  ```json
43
48
  {
44
49
  "mcpServers": {
45
50
  "canon-channel": {
51
+ "command": "canon-channel-server",
46
52
  "env": { "CANON_AGENT": "<profile>" }
47
53
  }
48
54
  }
49
55
  }
50
56
  ```
57
+
58
+ `CANON_AGENT` is required as soon as more than one profile exists — the host
59
+ refuses to guess between them.
@@ -18,7 +18,7 @@ Register a new Canon agent so it can send and receive messages. Each agent is sa
18
18
  - **Agent name** — The display name for the agent in Canon
19
19
  - **Description** — What the agent does (shown to users in Canon)
20
20
  - **Owner phone number** — The Canon account owner's phone number in E.164 format (e.g., +15551234567)
21
- - **Canon environment** — The trust-domain ID supplied by Canon (production defaults to `canon-prod-v1`)
21
+ - **Canon environment** — The trust-domain ID supplied by Canon (`canon-prod-v1` for production, `canon-dev-v1` for dev). There is no CLI default; the command fails without it.
22
22
  - **Profile name** (optional) — A short identifier for this agent (e.g., "reviewer", "notifier"). Defaults to a sanitized version of the agent name.
23
23
 
24
24
  2. Run the registration CLI:
@@ -26,18 +26,23 @@ Register a new Canon agent so it can send and receive messages. Each agent is sa
26
26
  canon-register --environment "<environment>" --name "<name>" --description "<description>" --phone "<phone>" --profile "<profile>"
27
27
  ```
28
28
 
29
+ `CANON_ENVIRONMENT_ID=<environment>` in the environment is equivalent to `--environment`.
30
+
29
31
  3. Tell the user: **"Open your Canon app and approve the agent registration request. Waiting for approval..."**
30
32
 
31
33
  The CLI will poll automatically for up to 5 minutes.
32
34
 
33
35
  4. On approval, the CLI saves the agent to `~/.canon/agents.json` automatically.
34
36
 
35
- 5. Tell the user: **"Agent registered! Run `canon-claude` to start. Or specify a project: `canon-claude --cwd /path/to/project`"**
37
+ 5. Tell the user: **"Agent registered! Start it with `CANON_AGENT=<profile> canon-claude`. Or specify a project: `CANON_AGENT=<profile> canon-claude --cwd /path/to/project`"**
38
+
39
+ `CANON_AGENT` is required once more than one profile exists — the host refuses to guess between them.
36
40
 
37
41
  ## Error handling
38
42
 
39
43
  - If the registration fails, show the error and ask the user to retry.
40
44
  - If status returns `"rejected"`, tell the user the registration was rejected by the owner.
41
45
  - If polling times out after 5 minutes, tell the user to try again later.
42
- - If the phone number format is wrong (must start with `+`, 8-16 digits), ask the user to re-enter it.
46
+ - If the server returns `Invalid phone number format`, the number could not be parsed as a real number; ask the user to re-enter it in full E.164 form (`+<country code><number>`).
47
+ - If the server returns `Owner phone number not found on platform`, the number is valid but no Canon account uses it; ask the user for the phone number of their Canon account.
43
48
  - If environment verification fails, do not retry against another URL. Confirm the environment ID and endpoint set with the user.