@canonmsg/claude-code-plugin 0.19.2 → 0.19.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.
package/dist/host.js CHANGED
@@ -27,8 +27,8 @@ import { existsSync } from 'node:fs';
27
27
  import { readFile } from 'node:fs/promises';
28
28
  import { query, } from '@anthropic-ai/claude-agent-sdk';
29
29
  import { isAnthropicImageAttachment, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
30
- import { buildTurnArtifactContextLine, ensureTurnArtifactDir, flushTurnArtifacts, } from '@canonmsg/coding-agent-host';
31
- import { CLAUDE_EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPlanApprovalRequest, resolveQuestionAllowOther, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, 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, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, sendMessageWithRetry, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCanonTurnBriefPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
30
+ import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/coding-agent-host';
31
+ import { CLAUDE_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, buildPlanApprovalRequest, resolveQuestionAllowOther, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, 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, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, sendMessageWithRetry, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
32
32
  import { decideAutoReply, } from './inbound-policy.js';
33
33
  import { runCli } from './cli-entry.js';
34
34
  import { collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
@@ -580,7 +580,7 @@ function buildClaudeRuntimeInfo(input) {
580
580
  {
581
581
  id: 'mediaOut',
582
582
  label: 'Media out',
583
- value: 'Explicit turn artifact directory',
583
+ value: 'Generated media artifacts',
584
584
  tier: 'detail',
585
585
  },
586
586
  ].filter((item) => Boolean(item));
@@ -670,11 +670,7 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
670
670
  supportsNonFinalPermanentMessages: false,
671
671
  };
672
672
  function buildCanonPrompt(input) {
673
- const sessionContextLines = [
674
- ...(input.session ? buildClaudeSessionContextLines(input.session) : []),
675
- ...(input.artifactDir ? [buildTurnArtifactContextLine(input.artifactDir)] : []),
676
- ];
677
- return renderCanonTurnBriefPrompt(buildCanonTurnContextV2({
673
+ return renderCodingHostInboundPrompt(buildCanonInboundFrameV1(buildCanonTurnContextV2({
678
674
  content: input.content,
679
675
  conversationId: input.conversationId,
680
676
  participantContext: input.participantContext,
@@ -683,31 +679,13 @@ function buildCanonPrompt(input) {
683
679
  activeSelfContextId: input.activeSelfContextId,
684
680
  provenance: input.provenance,
685
681
  replyContext: input.replyContext,
686
- sessionContextLines: sessionContextLines.length ? sessionContextLines : undefined,
687
682
  message: input.message,
688
- }));
683
+ })));
689
684
  }
690
- function optionLabel(value, options) {
691
- if (!value)
692
- return null;
693
- const label = options.find((option) => option.value === value)?.label ?? value;
694
- return label === value ? value : `${label} (${value})`;
695
- }
696
- function buildClaudeSessionContextLines(session) {
697
- const workspace = workspaceOptions.find((option) => option.cwd === session.environment.baseCwd);
698
- const permissionMode = optionLabel(session.state.permissionMode, CLAUDE_PERMISSION_MODE_OPTIONS);
699
- const effort = optionLabel(session.state.effort, CLAUDE_EFFORT_OPTIONS);
700
- const model = optionLabel(session.state.model, session.availableModels);
701
- return [
702
- model ? `Selected model: ${model}` : null,
703
- permissionMode ? `Permission mode: ${permissionMode}` : null,
704
- effort ? `Thinking level: ${effort}` : null,
705
- workspace ? `Project: ${workspace.label} (${workspace.id})` : null,
706
- `Execution mode: ${session.environment.mode}`,
707
- `Working directory: ${session.cwd}`,
708
- session.environment.branch ? `Git branch: ${session.environment.branch}` : null,
709
- session.environment.worktreePath ? `Worktree: ${session.environment.worktreePath}` : null,
710
- ].filter((line) => Boolean(line));
685
+ function resolveArtifactRoutingMode(participantContext) {
686
+ return participantContext.conversationType === 'direct' && participantContext.isOwner
687
+ ? 'workspace-generated'
688
+ : 'disabled';
711
689
  }
712
690
  function renderInboundContent(message, materialized) {
713
691
  return renderCanonHostInboundContent(message, materialized);
@@ -767,15 +745,15 @@ function createSession(conversationId, environment, agentId, client, typingSigna
767
745
  messageQueue.push(input);
768
746
  }
769
747
  };
770
- let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, artifactDir = null) => {
748
+ let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, artifactRoutingMode = 'disabled') => {
771
749
  sendInput(createClaudeInputEnvelope({
772
750
  kind: 'canon',
773
751
  msg,
774
752
  intent,
775
753
  sourceMessageId,
776
754
  markAccepted,
777
- artifactDir,
778
755
  isOwnerTurn,
756
+ artifactRoutingMode,
779
757
  }));
780
758
  };
781
759
  const inputStream = {
@@ -1073,7 +1051,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1073
1051
  environment,
1074
1052
  query: q,
1075
1053
  sendInput,
1076
- enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, artifactDir = null) => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted, isOwnerTurn, artifactDir),
1054
+ enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false, isOwnerTurn = false, artifactRoutingMode = 'disabled') => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted, isOwnerTurn, artifactRoutingMode),
1077
1055
  state: {
1078
1056
  model: undefined,
1079
1057
  permissionMode: config.permissionMode,
@@ -1254,26 +1232,31 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1254
1232
  return false;
1255
1233
  }
1256
1234
  }
1257
- async function routeSessionArtifacts(artifactDir) {
1258
- if (!artifactDir)
1235
+ function sendTurnArtifactFile(file) {
1236
+ return sendMediaFileMessage(client, conversationId, file.path, '', {
1237
+ ...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
1238
+ metadata: buildArtifactMediaMetadata(),
1239
+ });
1240
+ }
1241
+ async function routeWorkspaceGeneratedArtifacts(input) {
1242
+ if (!input.artifactBaseline)
1259
1243
  return;
1260
1244
  try {
1261
- const result = await flushTurnArtifacts({
1262
- artifactDir,
1263
- send: (file) => sendMediaFileMessage(client, conversationId, file.path, '', {
1264
- ...(session.activeSelfContextId ? { selfContextId: session.activeSelfContextId } : {}),
1265
- metadata: buildArtifactMediaMetadata(),
1266
- }),
1245
+ const result = await collectTurnArtifacts({
1246
+ cwd: session.cwd,
1247
+ baseline: input.artifactBaseline,
1267
1248
  });
1268
- for (const entry of result.sent) {
1269
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Routed artifact ${entry.file.fileName} (${entry.messageId})`
1270
- + (entry.removeFailed ? '; removal failed, so it may resend later' : ''));
1271
- }
1272
- for (const failure of result.failed) {
1273
- console.error(`[canon-host] [${conversationId.slice(0, 8)}] Artifact upload failed for ${failure.file.fileName}; left in place:`, failure.error);
1249
+ for (const file of result.files) {
1250
+ try {
1251
+ const { messageId } = await sendTurnArtifactFile(file);
1252
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Routed generated artifact ${file.relativePath ?? file.fileName} (${messageId})`);
1253
+ }
1254
+ catch (error) {
1255
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Artifact upload failed for ${file.relativePath ?? file.fileName}: ${error instanceof Error ? error.message : String(error)}`);
1256
+ }
1274
1257
  }
1275
1258
  for (const skipped of result.skipped) {
1276
- if (skipped.reason === 'too-large' || skipped.reason === 'file-cap' || skipped.reason === 'unsupported') {
1259
+ if (skipped.reason === 'too-large' || skipped.reason === 'file-cap' || skipped.reason === 'scan-limit') {
1277
1260
  console.error(`[canon-host] [${conversationId.slice(0, 8)}] Artifact skipped ${skipped.fileName} (${skipped.reason})`);
1278
1261
  }
1279
1262
  }
@@ -1282,6 +1265,11 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1282
1265
  console.error(`[canon-host] [${conversationId.slice(0, 8)}] Artifact routing failed:`, error instanceof Error ? error.message : error);
1283
1266
  }
1284
1267
  }
1268
+ async function routeArtifactsForInput(input) {
1269
+ if (input.artifactRoutingMode === 'workspace-generated') {
1270
+ await routeWorkspaceGeneratedArtifacts(input);
1271
+ }
1272
+ }
1285
1273
  function scheduleFinalDeliveryRetry(pending = session.pendingFinalDelivery) {
1286
1274
  if (!pending || session.closed || session.pendingFinalDelivery?.turnKey !== pending.turnKey)
1287
1275
  return;
@@ -1414,8 +1402,24 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1414
1402
  return;
1415
1403
  await client.updateMessageDisposition(conversationId, sourceMessageId, 'accepted_now').catch(() => { });
1416
1404
  }
1405
+ async function prepareArtifactsForInput(input) {
1406
+ if (input.kind !== 'canon')
1407
+ return;
1408
+ if (!session.currentTurnId) {
1409
+ session.currentTurnId = randomUUID();
1410
+ session.currentTurnOpenedAt = Date.now();
1411
+ }
1412
+ if (input.artifactRoutingMode === 'workspace-generated') {
1413
+ input.artifactBaseline = await captureTurnArtifactSnapshot({ cwd: session.cwd }).catch((error) => {
1414
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Artifact snapshot failed:`, error instanceof Error ? error.message : error);
1415
+ return null;
1416
+ });
1417
+ return;
1418
+ }
1419
+ }
1417
1420
  async function startCanonInput(input) {
1418
1421
  await markQueuedMessageAccepted(input.sourceMessageId, input.markAccepted);
1422
+ await prepareArtifactsForInput(input);
1419
1423
  session.state.state = 'running';
1420
1424
  sendInput(input);
1421
1425
  }
@@ -1429,7 +1433,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1429
1433
  writeTurn();
1430
1434
  void startCanonInput(next);
1431
1435
  }
1432
- enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false, isOwnerTurn = false, artifactDir = null) => {
1436
+ enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false, isOwnerTurn = false, artifactRoutingMode = 'disabled') => {
1433
1437
  session.lastActivity = Date.now();
1434
1438
  const input = createClaudeInputEnvelope({
1435
1439
  kind: 'canon',
@@ -1437,8 +1441,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1437
1441
  intent,
1438
1442
  sourceMessageId,
1439
1443
  markAccepted,
1440
- artifactDir,
1441
1444
  isOwnerTurn,
1445
+ artifactRoutingMode,
1442
1446
  });
1443
1447
  if (session.pendingFinalDelivery) {
1444
1448
  session.pendingInputs.push(input);
@@ -1575,7 +1579,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1575
1579
  // Turn artifacts land before the final text reply.
1576
1580
  if (completedInput?.kind === 'canon'
1577
1581
  && !session.interruptedTurnKeys.has(completedInput.turnKey)) {
1578
- await routeSessionArtifacts(completedInput.artifactDir);
1582
+ await routeArtifactsForInput(completedInput);
1579
1583
  }
1580
1584
  const resultText = typeof msg.result === 'string'
1581
1585
  ? msg.result.trim()
@@ -1665,7 +1669,7 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1665
1669
  type: 'user',
1666
1670
  message: {
1667
1671
  role: 'user',
1668
- content: 'You are connected to Canon messaging. Follow-up messages in this session come from Canon participants, and your text responses will be sent back to the conversation. You have full Claude Code capabilities (file read/write, bash, etc).',
1672
+ content: 'You are assisting in an ongoing chat. Reply naturally to the latest participant.',
1669
1673
  },
1670
1674
  parent_tool_use_id: null,
1671
1675
  },
@@ -2375,7 +2379,6 @@ export async function main() {
2375
2379
  return;
2376
2380
  }
2377
2381
  session.activeSelfContextId = activeSelfContextId;
2378
- const artifactDir = await ensureTurnArtifactDir(session.cwd, `claude-${m.id ?? randomUUID()}`);
2379
2382
  const promptText = buildCanonPrompt({
2380
2383
  content,
2381
2384
  conversationId: input.conversationId,
@@ -2386,13 +2389,12 @@ export async function main() {
2386
2389
  provenance: hydrated.provenance,
2387
2390
  replyContext,
2388
2391
  message: m,
2389
- session,
2390
- artifactDir,
2391
2392
  });
2392
2393
  const messageContent = await buildCanonUserContent({
2393
2394
  promptText,
2394
2395
  materialized: [...replyMedia.materialized, ...materialized],
2395
2396
  });
2397
+ const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
2396
2398
  session.enqueueInbound({
2397
2399
  type: 'user',
2398
2400
  message: {
@@ -2400,7 +2402,7 @@ export async function main() {
2400
2402
  content: messageContent,
2401
2403
  },
2402
2404
  parent_tool_use_id: null,
2403
- }, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, artifactDir);
2405
+ }, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, artifactRoutingMode);
2404
2406
  if (m.id) {
2405
2407
  saveRuntimeSessionState(runtimeId, {
2406
2408
  conversationId: input.conversationId,
@@ -2568,7 +2570,6 @@ export async function main() {
2568
2570
  return;
2569
2571
  }
2570
2572
  session.activeSelfContextId = activeSelfContextId;
2571
- const artifactDir = await ensureTurnArtifactDir(session.cwd, `claude-${m.id ?? randomUUID()}`);
2572
2573
  const promptText = buildCanonPrompt({
2573
2574
  content,
2574
2575
  conversationId: convoId,
@@ -2579,13 +2580,12 @@ export async function main() {
2579
2580
  provenance: hydrated.provenance,
2580
2581
  replyContext,
2581
2582
  message: m,
2582
- session,
2583
- artifactDir,
2584
2583
  });
2585
2584
  const messageContent = await buildCanonUserContent({
2586
2585
  promptText,
2587
2586
  materialized: [...replyMedia.materialized, ...materialized],
2588
2587
  });
2588
+ const artifactRoutingMode = resolveArtifactRoutingMode(participantContext);
2589
2589
  session.enqueueInbound({
2590
2590
  type: 'user',
2591
2591
  message: {
@@ -2593,7 +2593,7 @@ export async function main() {
2593
2593
  content: messageContent,
2594
2594
  },
2595
2595
  parent_tool_use_id: null,
2596
- }, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, artifactDir);
2596
+ }, deliveryIntent, m.id ?? null, shouldMarkAccepted, isOwner, artifactRoutingMode);
2597
2597
  if (m.id) {
2598
2598
  saveRuntimeSessionState(runtimeId, {
2599
2599
  conversationId: convoId,
@@ -1,13 +1,16 @@
1
1
  import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
2
  import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core';
3
+ import type { TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
3
4
  export type ClaudeInputKind = 'seed' | 'canon';
5
+ export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
4
6
  export interface ClaudeInputEnvelope {
5
7
  kind: ClaudeInputKind;
6
8
  msg: SDKUserMessage;
7
9
  intent: DeliveryIntent;
8
10
  sourceMessageId: string | null;
9
11
  markAccepted: boolean;
10
- artifactDir: string | null;
12
+ artifactRoutingMode: ClaudeArtifactRoutingMode;
13
+ artifactBaseline: TurnArtifactSnapshot | null;
11
14
  turnKey: string;
12
15
  isOwnerTurn: boolean;
13
16
  }
@@ -42,7 +45,8 @@ export declare function createClaudeInputEnvelope(input: {
42
45
  intent?: DeliveryIntent;
43
46
  sourceMessageId?: string | null;
44
47
  markAccepted?: boolean;
45
- artifactDir?: string | null;
48
+ artifactRoutingMode?: ClaudeArtifactRoutingMode;
49
+ artifactBaseline?: TurnArtifactSnapshot | null;
46
50
  isOwnerTurn?: boolean;
47
51
  }): ClaudeInputEnvelope;
48
52
  export declare function buildClaudeFinalMessageId(input: {
@@ -8,7 +8,8 @@ export function createClaudeInputEnvelope(input) {
8
8
  intent: input.intent ?? 'queue',
9
9
  sourceMessageId,
10
10
  markAccepted: Boolean(input.markAccepted),
11
- artifactDir: input.artifactDir ?? null,
11
+ artifactRoutingMode: input.artifactRoutingMode ?? 'disabled',
12
+ artifactBaseline: input.artifactBaseline ?? null,
12
13
  isOwnerTurn: input.kind === 'canon'
13
14
  ? input.isOwnerTurn === true
14
15
  : input.isOwnerTurn !== false,
@@ -1,6 +1,6 @@
1
- const NON_OWNER_DENY_MESSAGE = 'Only the agent owner can authorize Claude Code to use runtime tools from Canon chat.';
1
+ const NON_OWNER_DENY_MESSAGE = 'Only the agent owner can authorize runtime tool use in this chat.';
2
2
  const NATIVE_APPROVAL_UNAVAILABLE_MESSAGE = 'Runtime action approval is not available for this conversation.';
3
- const NATIVE_APPROVAL_DENIED_MESSAGE = 'The agent owner denied this action in Canon.';
3
+ const NATIVE_APPROVAL_DENIED_MESSAGE = 'The agent owner denied this action.';
4
4
  export const CLAUDE_NATIVE_APPROVAL_PERMISSION_MODE = 'default';
5
5
  const SENSITIVE_CLAUDE_TOOLS = new Set([
6
6
  'agent',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.19.2",
3
+ "version": "0.19.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",
@@ -32,8 +32,8 @@
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "^0.2.141",
34
34
  "@canonmsg/agent-sdk": "^3.2.3",
35
- "@canonmsg/coding-agent-host": "^0.2.0",
36
- "@canonmsg/core": "^2.6.0",
35
+ "@canonmsg/coding-agent-host": "^0.2.2",
36
+ "@canonmsg/core": "^2.8.0",
37
37
  "@modelcontextprotocol/sdk": "^1.29.0"
38
38
  },
39
39
  "engines": {