@canonmsg/claude-code-plugin 0.31.2 → 0.31.4
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/.claude-plugin/plugin.json +1 -1
- package/README.md +7 -0
- package/dist/canon-user-content.d.ts +22 -0
- package/dist/canon-user-content.js +72 -0
- package/dist/host.js +29 -29
- package/dist/server.js +3 -2
- package/dist/session-state.d.ts +20 -0
- package/dist/session-state.js +30 -0
- package/package.json +7 -7
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
|
|
@@ -87,6 +89,11 @@ canon-claude --cwd /path/to/project --turn-verbosity quiet
|
|
|
87
89
|
| `verbose` | Live streaming text plus the margin activity rows on the final, everywhere |
|
|
88
90
|
| `quiet` | The thinking indicator and the answer, nothing in between, everywhere |
|
|
89
91
|
|
|
92
|
+
In verbose mode, authored text remains continual across tool use: text before a
|
|
93
|
+
tool stays as its own speech bubble, the tool appears in the activity margin,
|
|
94
|
+
and text after the result resumes in a new bubble. These are ephemeral streaming
|
|
95
|
+
updates; only the final durable `turn_complete` message is notification-eligible.
|
|
96
|
+
|
|
90
97
|
Quiet drops the live `/streaming` narration and the final's `turnTrail` activity rows. It does **not** drop the thinking indicator (which now stays up for the turn's whole working phase rather than handing over to a bubble that never appears; while the turn is parked on an approval the clients suppress an agent's dots and the header line carries the state), the turn state, the answer — including every part of a long chunked one — failure notices, generated files, or approval and question cards and their receipts.
|
|
91
98
|
|
|
92
99
|
This is an agent-developer setting. Canon never changes it, and it is deliberately not part of the per-conversation session config a user can edit. `canon-necromance` replays a stored launch command verbatim, so add the flag at registration time if you want a non-default value.
|
|
@@ -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,14 +26,15 @@ 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 {
|
|
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 {
|
|
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';
|
|
35
36
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
36
|
-
import { applyClaudeSessionControl, boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, isClaudeTurnSlotReserved, readClaudeFinalDeliveryResume, releaseClaudeTurnSlot, runClaudeExhaustedFinalDelivery, shouldApplyClaudeEchoedSessionState, shouldReleaseClaudeTurnSlot, isClaudeMainTurnMessage, openClaudeTurn, planClaudeAssistantTrail, planClaudeStreamingWrite, planClaudeToolCallStart, planClaudeToolProgress, planClaudeToolResults, claudeFinalTurnTrail, publishedClaudeTurnState, rememberDispatchedClaudeInput, resetClaudeTurnActivityState, shouldOpenClaudeTurnOnRunning, shouldStopTypingDotsOnStreamedText, isOpenClaudeTurnState, takeClaudeResultOwner, takeClaudeToolBlockIdByIndex, claudeModelInfoToOption, claudeOriginForCanonSender, composeClaudeTurnFinal, describeUndeliveredClaudeFinal, confirmClaudeInterrupt, createClaudeInputEnvelope, deriveClaudeSupplementalModelProbes, formatClaudeCliVersion, formatClaudeControlError, isClaudeCustomModelOption, isSupportedClaudeCliVersion, mergeClaudeDiscoveredModelOptions, parseClaudeCliVersion, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, shouldRouteClaudeTurnArtifacts, MINIMUM_CLAUDE_CLI_VERSION, } from './session-state.js';
|
|
37
|
+
import { applyClaudeSessionControl, boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, beginClaudeAssistantResponse, claimClaudeTextSegmentId, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, endClaudeAssistantResponse, isClaudeTurnSlotReserved, readClaudeFinalDeliveryResume, releaseClaudeTurnSlot, runClaudeExhaustedFinalDelivery, shouldApplyClaudeEchoedSessionState, shouldReleaseClaudeTurnSlot, isClaudeMainTurnMessage, openClaudeTurn, planClaudeAssistantTrail, planClaudeStreamingWrite, planClaudeToolCallStart, planClaudeToolProgress, planClaudeToolResults, claudeFinalTurnTrail, publishedClaudeTurnState, rememberDispatchedClaudeInput, resetClaudeTurnActivityState, shouldOpenClaudeTurnOnRunning, shouldStopTypingDotsOnStreamedText, isOpenClaudeTurnState, takeClaudeResultOwner, takeClaudeToolBlockIdByIndex, claudeModelInfoToOption, claudeOriginForCanonSender, composeClaudeTurnFinal, describeUndeliveredClaudeFinal, confirmClaudeInterrupt, createClaudeInputEnvelope, deriveClaudeSupplementalModelProbes, formatClaudeCliVersion, formatClaudeControlError, isClaudeCustomModelOption, isSupportedClaudeCliVersion, mergeClaudeDiscoveredModelOptions, parseClaudeCliVersion, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, shouldRouteClaudeTurnArtifacts, MINIMUM_CLAUDE_CLI_VERSION, } from './session-state.js';
|
|
37
38
|
import { CLAUDE_SUPPORTED_DIALOG_KINDS, buildClaudeAskUserPermissionDenied, buildClaudeAskUserPermissionResult, createClaudeUserDialogCoordinator, parseClaudeAskUserDialog, parseClaudeAskUserToolInput, resolveClaudeUserDialogRequestId, } from './user-dialog.js';
|
|
38
39
|
function parseRuntimeVisibilityPreset(value) {
|
|
39
40
|
return value === 'normal' || value === 'minimal' || value === 'full' ? value : undefined;
|
|
@@ -781,43 +782,30 @@ function resolveClaudeTurnModes(participantContext) {
|
|
|
781
782
|
};
|
|
782
783
|
}
|
|
783
784
|
function renderInboundContent(message, materialized) {
|
|
784
|
-
return
|
|
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
|
-
|
|
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 {
|
|
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;
|
|
@@ -2078,7 +2066,17 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2078
2066
|
// being written.
|
|
2079
2067
|
if (!isClaudeMainTurnMessage(msg.parent_tool_use_id))
|
|
2080
2068
|
break;
|
|
2081
|
-
if (event?.type === '
|
|
2069
|
+
if (event?.type === 'message_start') {
|
|
2070
|
+
beginClaudeAssistantResponse(session.turnActivity);
|
|
2071
|
+
}
|
|
2072
|
+
else if (event?.type === 'message_stop') {
|
|
2073
|
+
endClaudeAssistantResponse(session.turnActivity);
|
|
2074
|
+
}
|
|
2075
|
+
else if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
|
|
2076
|
+
// A tool is also a response boundary for speech. This fallback
|
|
2077
|
+
// keeps continual streaming correct on SDK streams that omit raw
|
|
2078
|
+
// message_start/message_stop events.
|
|
2079
|
+
endClaudeAssistantResponse(session.turnActivity);
|
|
2082
2080
|
// Claimed per CALL, keyed by the SDK's tool_use id. The old id was
|
|
2083
2081
|
// the turn id, so every call in a turn wrote over the same row.
|
|
2084
2082
|
const commands = planClaudeToolCallStart(session.turnActivity, {
|
|
@@ -2116,8 +2114,10 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
2116
2114
|
stopVisibleWorkSignal();
|
|
2117
2115
|
}
|
|
2118
2116
|
writeTurn();
|
|
2119
|
-
|
|
2120
|
-
|
|
2117
|
+
onStreamDelta(event.delta.text, claimClaudeTextSegmentId(session.turnActivity, {
|
|
2118
|
+
turnId: session.currentTurnId ?? conversationId,
|
|
2119
|
+
index: event.index,
|
|
2120
|
+
}));
|
|
2121
2121
|
}
|
|
2122
2122
|
break;
|
|
2123
2123
|
}
|
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,
|
|
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
|
|
59
|
+
return renderClaudeInboundContent(message, materialized);
|
|
59
60
|
}
|
|
60
61
|
function toolArgumentError(error) {
|
|
61
62
|
return {
|
package/dist/session-state.d.ts
CHANGED
|
@@ -356,6 +356,10 @@ export interface ClaudeTurnActivityState {
|
|
|
356
356
|
blockIdByToolUseId: Map<string, string>;
|
|
357
357
|
/** This turn's plan block, once TodoWrite has produced one. */
|
|
358
358
|
planBlockId: string | null;
|
|
359
|
+
/** Monotonic identity for assistant responses inside this turn. */
|
|
360
|
+
assistantResponseCount: number;
|
|
361
|
+
/** The response whose text deltas are currently being streamed. */
|
|
362
|
+
activeAssistantResponseKey: string | null;
|
|
359
363
|
}
|
|
360
364
|
/**
|
|
361
365
|
* Whether an SDK message describes the agent's OWN work rather than a
|
|
@@ -367,6 +371,22 @@ export interface ClaudeTurnActivityState {
|
|
|
367
371
|
export declare function isClaudeMainTurnMessage(parentToolUseId: unknown): boolean;
|
|
368
372
|
export declare function createClaudeTurnActivityState(): ClaudeTurnActivityState;
|
|
369
373
|
export declare function resetClaudeTurnActivityState(state: ClaudeTurnActivityState): void;
|
|
374
|
+
/** Open a fresh assistant response, whose content-block indexes start at zero. */
|
|
375
|
+
export declare function beginClaudeAssistantResponse(state: ClaudeTurnActivityState): string;
|
|
376
|
+
/** Close the response so a later text delta cannot reuse its block identity. */
|
|
377
|
+
export declare function endClaudeAssistantResponse(state: ClaudeTurnActivityState): void;
|
|
378
|
+
/**
|
|
379
|
+
* Stable id for one text block inside one assistant response.
|
|
380
|
+
*
|
|
381
|
+
* Anthropic restarts `content_block.index` for every assistant response. The
|
|
382
|
+
* response key is therefore required to distinguish text before a tool from
|
|
383
|
+
* text emitted after its result. Lazily opening a response keeps older SDK
|
|
384
|
+
* streams that omit `message_start` safe as well.
|
|
385
|
+
*/
|
|
386
|
+
export declare function claimClaudeTextSegmentId(state: ClaudeTurnActivityState, input: {
|
|
387
|
+
turnId?: string | null;
|
|
388
|
+
index?: unknown;
|
|
389
|
+
}): string;
|
|
370
390
|
export declare function claudeToolBlockId(toolUseId: string): string;
|
|
371
391
|
export declare function claudePlanBlockId(turnId: string | null | undefined): string;
|
|
372
392
|
/**
|
package/dist/session-state.js
CHANGED
|
@@ -376,12 +376,42 @@ export function createClaudeTurnActivityState() {
|
|
|
376
376
|
blockIdByIndex: new Map(),
|
|
377
377
|
blockIdByToolUseId: new Map(),
|
|
378
378
|
planBlockId: null,
|
|
379
|
+
assistantResponseCount: 0,
|
|
380
|
+
activeAssistantResponseKey: null,
|
|
379
381
|
};
|
|
380
382
|
}
|
|
381
383
|
export function resetClaudeTurnActivityState(state) {
|
|
382
384
|
state.blockIdByIndex.clear();
|
|
383
385
|
state.blockIdByToolUseId.clear();
|
|
384
386
|
state.planBlockId = null;
|
|
387
|
+
state.assistantResponseCount = 0;
|
|
388
|
+
state.activeAssistantResponseKey = null;
|
|
389
|
+
}
|
|
390
|
+
/** Open a fresh assistant response, whose content-block indexes start at zero. */
|
|
391
|
+
export function beginClaudeAssistantResponse(state) {
|
|
392
|
+
state.assistantResponseCount += 1;
|
|
393
|
+
state.activeAssistantResponseKey = `response-${state.assistantResponseCount}`;
|
|
394
|
+
return state.activeAssistantResponseKey;
|
|
395
|
+
}
|
|
396
|
+
/** Close the response so a later text delta cannot reuse its block identity. */
|
|
397
|
+
export function endClaudeAssistantResponse(state) {
|
|
398
|
+
state.activeAssistantResponseKey = null;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Stable id for one text block inside one assistant response.
|
|
402
|
+
*
|
|
403
|
+
* Anthropic restarts `content_block.index` for every assistant response. The
|
|
404
|
+
* response key is therefore required to distinguish text before a tool from
|
|
405
|
+
* text emitted after its result. Lazily opening a response keeps older SDK
|
|
406
|
+
* streams that omit `message_start` safe as well.
|
|
407
|
+
*/
|
|
408
|
+
export function claimClaudeTextSegmentId(state, input) {
|
|
409
|
+
const responseKey = state.activeAssistantResponseKey
|
|
410
|
+
?? beginClaudeAssistantResponse(state);
|
|
411
|
+
const index = typeof input.index === 'number' && Number.isInteger(input.index)
|
|
412
|
+
? input.index
|
|
413
|
+
: 'latest';
|
|
414
|
+
return buildTrailBlockId('text', input.turnId ?? 'turn', responseKey, index);
|
|
385
415
|
}
|
|
386
416
|
export function claudeToolBlockId(toolUseId) {
|
|
387
417
|
return buildTrailBlockId('tool', toolUseId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.4",
|
|
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.
|
|
34
|
-
"@canonmsg/agent-sdk": "^8.
|
|
35
|
-
"@canonmsg/agent-tools": "^0.5.
|
|
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.
|
|
38
|
-
"@canonmsg/rich-cards": "^0.10.
|
|
39
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
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"
|