@canonmsg/claude-code-plugin 0.31.2 → 0.31.3

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.31.2",
4
+ "version": "0.31.3",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
package/README.md CHANGED
@@ -63,6 +63,8 @@ canon-claude --cwd ~/dev --workspace-root ~/dev
63
63
 
64
64
  `--cwd` is the default workspace. Each `--workspace-root` value is an approved local root; the host discovers immediate child projects with common markers such as `.git`, `package.json`, `pyproject.toml`, `Cargo.toml`, or `go.mod` and publishes them as selectable projects during session creation. Use repeated `--workspace /path/to/project` entries to advertise specific projects outside those roots. Worktree mode creates a best-effort per-conversation git worktree under `~/.canon/conversation-worktrees`; shared-project mode runs directly in the selected directory.
65
65
 
66
+ Inbound Canon images are materialized with the Agent SDK's bounded 10 MiB default. The host sends supported images to the direct Anthropic transport as native base64 blocks only when each raw file is at most 7 MiB and the exact encoded content array plus prompt text fit the aggregate request budget with reserved framing headroom. Seven MiB is derived specifically from the direct transport's 10 MB encoded-image limit; it is not a portable default for Bedrock or Vertex. Materialized images omitted from native blocks remain available by local path. Images over the 10 MiB automatic-download limit instead receive a validated HTTPS link in the Claude prompt; materialized images never receive a duplicate link.
67
+
66
68
  Current Canon truth for Claude host mode:
67
69
 
68
70
  - model is live-editable
@@ -0,0 +1,22 @@
1
+ import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
+ import { type AnthropicImageBudgetOptions, type MaterializedCanonAttachment } from '@canonmsg/agent-sdk';
3
+ import { renderCanonHostInboundContent, type CanonReplyContext } from '@canonmsg/core';
4
+ type ClaudeRenderableInboundMessage = Parameters<typeof renderCanonHostInboundContent>[0];
5
+ /**
6
+ * Claude-only rendering policy for Canon media. Core deliberately omits all
7
+ * URLs globally. This wrapper restores validated HTTPS links only for images
8
+ * that the bounded runtime did not materialize, while materialized images keep
9
+ * their local-path placeholder without a duplicate link.
10
+ */
11
+ export declare function renderClaudeInboundContent(message: ClaudeRenderableInboundMessage, materialized?: ReadonlyArray<MaterializedCanonAttachment>): string;
12
+ export declare function withClaudeReplyContextMediaReferences(replyContext: CanonReplyContext | null, materialized: ReadonlyArray<MaterializedCanonAttachment>): CanonReplyContext | null;
13
+ /**
14
+ * Build the Claude Code SDK's multimodal user content without allowing native
15
+ * image blocks to consume the whole request. The prompt text is preserved
16
+ * verbatim, including local paths for images omitted from native blocks.
17
+ */
18
+ export declare function buildCanonUserContent(input: {
19
+ promptText: string;
20
+ materialized: ReadonlyArray<MaterializedCanonAttachment>;
21
+ }, budget?: Omit<AnthropicImageBudgetOptions, 'promptText'>): Promise<string | SDKUserMessage['message']['content']>;
22
+ export {};
@@ -0,0 +1,72 @@
1
+ import { toAnthropicImageBlocksWithinBudget, } from '@canonmsg/agent-sdk';
2
+ import { renderCanonHostInboundContent, } from '@canonmsg/core';
3
+ const MAX_PROMPT_ATTACHMENT_URL_LENGTH = 8_192;
4
+ function validatedPromptAttachmentUrl(value) {
5
+ if (typeof value !== 'string'
6
+ || value.length === 0
7
+ || value.length > MAX_PROMPT_ATTACHMENT_URL_LENGTH
8
+ || /[\u0000-\u0020\u007f]/.test(value)) {
9
+ return null;
10
+ }
11
+ try {
12
+ const parsed = new URL(value);
13
+ if (parsed.protocol !== 'https:'
14
+ || parsed.hostname.length === 0
15
+ || parsed.username.length > 0
16
+ || parsed.password.length > 0) {
17
+ return null;
18
+ }
19
+ return parsed.href;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /**
26
+ * Claude-only rendering policy for Canon media. Core deliberately omits all
27
+ * URLs globally. This wrapper restores validated HTTPS links only for images
28
+ * that the bounded runtime did not materialize, while materialized images keep
29
+ * their local-path placeholder without a duplicate link.
30
+ */
31
+ export function renderClaudeInboundContent(message, materialized = []) {
32
+ const rendered = renderCanonHostInboundContent(message, materialized);
33
+ const materializedIndexes = new Set(materialized.map((attachment) => attachment.index));
34
+ const imageLinks = (message.attachments ?? []).flatMap((attachment, index) => {
35
+ if (attachment.kind !== 'image' || materializedIndexes.has(index))
36
+ return [];
37
+ const url = validatedPromptAttachmentUrl(attachment.url);
38
+ return url ? [`[Image ${index + 1} link: ${url}]`] : [];
39
+ });
40
+ return imageLinks.length > 0 ? `${rendered}\n${imageLinks.join('\n')}` : rendered;
41
+ }
42
+ export function withClaudeReplyContextMediaReferences(replyContext, materialized) {
43
+ if (!replyContext?.found)
44
+ return replyContext;
45
+ return {
46
+ ...replyContext,
47
+ body: renderClaudeInboundContent({
48
+ text: replyContext.text,
49
+ contentType: replyContext.contentType,
50
+ attachments: replyContext.attachments,
51
+ contactCard: replyContext.contactCard,
52
+ senderType: replyContext.senderType ?? undefined,
53
+ }, materialized),
54
+ };
55
+ }
56
+ /**
57
+ * Build the Claude Code SDK's multimodal user content without allowing native
58
+ * image blocks to consume the whole request. The prompt text is preserved
59
+ * verbatim, including local paths for images omitted from native blocks.
60
+ */
61
+ export async function buildCanonUserContent(input, budget) {
62
+ const imageBlocks = await toAnthropicImageBlocksWithinBudget(input.materialized, {
63
+ promptText: input.promptText,
64
+ ...budget,
65
+ });
66
+ if (imageBlocks.length === 0)
67
+ return input.promptText;
68
+ return [
69
+ { type: 'text', text: input.promptText },
70
+ ...imageBlocks,
71
+ ];
72
+ }
package/dist/host.js CHANGED
@@ -26,9 +26,10 @@ import { execFileSync } from 'node:child_process';
26
26
  import { existsSync } from 'node:fs';
27
27
  import { readFile } from 'node:fs/promises';
28
28
  import { query, } from '@anthropic-ai/claude-agent-sdk';
29
- import { isAnthropicImageAttachment, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
29
+ import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
30
30
  import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages, createReconnectRecoveryGate, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
31
- import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
31
+ import { buildCanonUserContent, renderClaudeInboundContent, withClaudeReplyContextMediaReferences, } from './canon-user-content.js';
32
+ import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
32
33
  import { runCli } from '@canonmsg/core';
33
34
  import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
34
35
  import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
@@ -781,43 +782,30 @@ function resolveClaudeTurnModes(participantContext) {
781
782
  };
782
783
  }
783
784
  function renderInboundContent(message, materialized) {
784
- return renderCanonHostInboundContent(message, materialized);
785
+ return renderClaudeInboundContent(message, materialized);
785
786
  }
786
787
  async function materializePromptReplyContext(input) {
787
788
  if (!input.replyContext?.found || !input.replyContext.attachments?.length) {
788
789
  return { replyContext: input.replyContext, materialized: [] };
789
790
  }
790
791
  try {
791
- return await materializeReplyContextMedia(input.replyContext, {
792
+ const result = await materializeReplyContextMedia(input.replyContext, {
792
793
  agentId: input.agentId,
793
794
  conversationId: input.conversationId,
794
795
  });
796
+ return {
797
+ ...result,
798
+ replyContext: withClaudeReplyContextMediaReferences(result.replyContext, result.materialized),
799
+ };
795
800
  }
796
801
  catch (error) {
797
802
  console.error(`${input.logPrefix} Failed to materialize replied-to media:`, error instanceof Error ? error.message : error);
798
- return { replyContext: input.replyContext, materialized: [] };
803
+ return {
804
+ replyContext: withClaudeReplyContextMediaReferences(input.replyContext, []),
805
+ materialized: [],
806
+ };
799
807
  }
800
808
  }
801
- /**
802
- * Build the multimodal `content` payload handed to the Claude Code SDK.
803
- *
804
- * Images are delivered as native Anthropic `image` blocks (base64) so the
805
- * vision model sees pixels, not a text reference to a URL. Non-image
806
- * attachments (audio, files) are referenced by their local path inside the
807
- * text prompt — see `renderCanonHostInboundContent` — so the agent can
808
- * Read them on demand.
809
- */
810
- async function buildCanonUserContent(input) {
811
- const imageAttachments = input.materialized.filter(isAnthropicImageAttachment);
812
- if (imageAttachments.length === 0) {
813
- return input.promptText;
814
- }
815
- const imageBlocks = await Promise.all(imageAttachments.map((attachment) => toAnthropicImageBlock(attachment)));
816
- return [
817
- { type: 'text', text: input.promptText },
818
- ...imageBlocks,
819
- ];
820
- }
821
809
  // ── Session factory ─────────────────────────────────────────────────
822
810
  function createSession(conversationId, environment, agentId, client, typingSignals, runtimeState, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
823
811
  const { cwd } = environment;
package/dist/server.js CHANGED
@@ -10,9 +10,10 @@ import { setDefaultResultOrder } from 'node:dns';
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
- import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, verifyResolvedAgentEnvironment, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, renderCanonHostInboundContent, } from '@canonmsg/core';
13
+ import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, verifyResolvedAgentEnvironment, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, } from '@canonmsg/core';
14
14
  import { materializeMessageMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
15
15
  import { ApprovalHttpServer } from './approval-server.js';
16
+ import { renderClaudeInboundContent } from './canon-user-content.js';
16
17
  import { runCli } from '@canonmsg/core';
17
18
  import { parseReplyArgs, parseSendMessageArgs, parseSetTypingArgs, } from './mcp-args.js';
18
19
  import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, stampSendToTurnComplete, } from '@canonmsg/agent-tools';
@@ -55,7 +56,7 @@ function getPrimaryAttachment(message) {
55
56
  return null;
56
57
  }
57
58
  function renderInboundContent(message, materialized) {
58
- return renderCanonHostInboundContent(message, materialized);
59
+ return renderClaudeInboundContent(message, materialized);
59
60
  }
60
61
  function toolArgumentError(error) {
61
62
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.31.2",
3
+ "version": "0.31.3",
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",
@@ -30,13 +30,13 @@
30
30
  "test": "vitest run"
31
31
  },
32
32
  "dependencies": {
33
- "@anthropic-ai/claude-agent-sdk": "0.3.220",
34
- "@canonmsg/agent-sdk": "^8.3.0",
35
- "@canonmsg/agent-tools": "^0.5.2",
33
+ "@anthropic-ai/claude-agent-sdk": "0.3.228",
34
+ "@canonmsg/agent-sdk": "^8.6.0",
35
+ "@canonmsg/agent-tools": "^0.5.3",
36
36
  "@canonmsg/coding-agent-host": "^0.5.0",
37
- "@canonmsg/core": "^10.3.1",
38
- "@canonmsg/rich-cards": "^0.10.0",
39
- "@modelcontextprotocol/sdk": "^1.29.0"
37
+ "@canonmsg/core": "^10.4.0",
38
+ "@canonmsg/rich-cards": "^0.10.2",
39
+ "@modelcontextprotocol/sdk": "^1.30.0"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=18.0.0"