@canonmsg/claude-code-plugin 0.13.1 → 0.14.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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ canon-register --name "My Claude" --description "My Claude Code agent" --phone "
15
15
  canon-claude
16
16
  ```
17
17
 
18
- Send a message to your agent in the Canon app. Claude Code responds with full capabilities (file read/write, bash, etc.). Keep the `canon-claude` terminal open; closing it takes this local agent offline.
18
+ Send a message to your agent in the Canon app. Claude Code responds with full capabilities (file read/write, bash, etc.). `canon-claude` is the local agent process. Keep that terminal open while you want Canon to reach the agent. Closing it, logging out, rebooting, or sleeping long enough to stop the process takes this local agent offline until you revive it.
19
19
 
20
20
  After a restart or closed terminal, use the shared local manager to list and revive all recorded local agents:
21
21
 
@@ -25,6 +25,8 @@ canon-necromance
25
25
  canon-necromance revive frontend
26
26
  ```
27
27
 
28
+ Public docs: <https://canonmail.com/agents/integrations>. Coding-host concepts: <https://canonmail.com/agents/coding-agents>.
29
+
28
30
  ## Features
29
31
 
30
32
  - **Two-way messaging** — Messages from Canon flow to Claude Code and back
package/dist/host.js CHANGED
@@ -26,7 +26,7 @@ import { execFileSync } from 'node:child_process';
26
26
  import { existsSync } from 'node:fs';
27
27
  import { query, } from '@anthropic-ai/claude-agent-sdk';
28
28
  import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
29
- import { CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, rtdbWrite, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
29
+ import { CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, rtdbWrite, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
30
30
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
31
31
  import { createStreamingBuffer } from './streaming-buffer.js';
32
32
  import { runCli } from './cli-entry.js';
@@ -51,7 +51,10 @@ EXAMPLES
51
51
  canon-claude --cwd ~/dev/canon
52
52
  canon-claude --cwd ~/dev/canon --workspace-root ~/dev
53
53
 
54
- Keep this terminal open while you want Canon to reach the agent.`;
54
+ Keep this terminal open while you want Canon to reach the agent. Closing it,
55
+ logging out, rebooting, or sleeping long enough to stop the process takes the
56
+ local agent offline until you revive it. Docs:
57
+ https://canonmail.com/agents/integrations`;
55
58
  let workingDir = process.cwd();
56
59
  let workspaceOptions = [];
57
60
  let workspaceRoots = [];
@@ -68,6 +71,103 @@ let runtimeMetadata = {
68
71
  };
69
72
  let runtimeMetadataRefresh = null;
70
73
  function buildClaudeRuntimeDescriptor(input) {
74
+ const commands = [
75
+ {
76
+ id: 'plan',
77
+ label: 'Plan first',
78
+ description: 'Switch Claude into Plan mode. Text after /plan is kept as a ready prompt while the mode applies.',
79
+ aliases: ['plan'],
80
+ category: 'plan',
81
+ placements: ['composer_slash', 'command_palette'],
82
+ availability: ['always'],
83
+ trailingTextBehavior: 'send_as_prompt',
84
+ dispatch: {
85
+ kind: 'control',
86
+ controlId: 'permissionMode',
87
+ value: 'plan',
88
+ },
89
+ },
90
+ {
91
+ id: 'runtime-status',
92
+ label: 'Runtime status',
93
+ description: 'Open Claude runtime details, including account, project, and policy status.',
94
+ primitive: 'runtime.status',
95
+ aliases: ['status'],
96
+ category: 'details',
97
+ placements: ['composer_slash', 'command_palette'],
98
+ availability: ['always'],
99
+ dispatch: { kind: 'open_details', target: 'status' },
100
+ },
101
+ {
102
+ id: 'mcp-inventory',
103
+ label: 'MCP servers',
104
+ description: 'Open the Claude MCP inventory published by the host.',
105
+ aliases: ['mcp'],
106
+ category: 'details',
107
+ placements: ['composer_slash', 'command_palette'],
108
+ availability: ['always'],
109
+ dispatch: { kind: 'open_details', target: 'mcp' },
110
+ },
111
+ {
112
+ id: 'plugin-inventory',
113
+ label: 'Plugins',
114
+ description: 'Open the Claude plugin and command inventory published by the host.',
115
+ aliases: ['plugins'],
116
+ category: 'details',
117
+ placements: ['composer_slash', 'command_palette'],
118
+ availability: ['always'],
119
+ dispatch: { kind: 'open_details', target: 'plugins' },
120
+ },
121
+ {
122
+ id: 'model-details',
123
+ label: 'Model control',
124
+ description: 'Open runtime details and use the live Model control.',
125
+ aliases: ['model'],
126
+ category: 'details',
127
+ placements: ['composer_slash', 'command_palette'],
128
+ availability: ['always'],
129
+ dispatch: { kind: 'open_details', target: 'model' },
130
+ },
131
+ {
132
+ id: 'permission-details',
133
+ label: 'Permission mode',
134
+ description: 'Open runtime details and use the live Permission mode control.',
135
+ aliases: ['permission'],
136
+ category: 'details',
137
+ placements: ['composer_slash', 'command_palette'],
138
+ availability: ['always'],
139
+ dispatch: { kind: 'open_details', target: 'permissionMode' },
140
+ },
141
+ {
142
+ id: 'effort-details',
143
+ label: 'Thinking level',
144
+ description: 'Set Claude effort using /effort or /think.',
145
+ primitive: 'runtime.reasoning.set',
146
+ aliases: ['effort', 'think'],
147
+ category: 'runtime',
148
+ placements: ['composer_slash', 'command_palette'],
149
+ availability: ['always'],
150
+ args: [{
151
+ id: 'level',
152
+ label: 'Level',
153
+ kind: 'enum',
154
+ required: true,
155
+ choices: [
156
+ { value: 'low', label: 'Low' },
157
+ { value: 'medium', label: 'Medium' },
158
+ { value: 'high', label: 'High' },
159
+ ],
160
+ }],
161
+ dispatch: {
162
+ kind: 'control',
163
+ controlId: 'effort',
164
+ },
165
+ },
166
+ {
167
+ ...RUNTIME_NEW_SESSION_ACTION,
168
+ primitive: 'session.new',
169
+ },
170
+ ];
71
171
  return buildFirstPartyCodingRuntimeDescriptor({
72
172
  clientType: 'claude-code',
73
173
  models: input.models,
@@ -78,6 +178,7 @@ function buildClaudeRuntimeDescriptor(input) {
78
178
  defaultPermissionMode: 'default',
79
179
  streamingTextMode: 'delta',
80
180
  admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
181
+ commands,
81
182
  actions: [
82
183
  {
83
184
  id: 'plan',
@@ -1322,10 +1423,12 @@ export async function main() {
1322
1423
  initRTDBAuth(client);
1323
1424
  let agentId;
1324
1425
  let ownerId = null;
1426
+ let ownerName = null;
1325
1427
  try {
1326
1428
  const ctx = await client.getAgentMe();
1327
1429
  agentId = ctx.agentId;
1328
1430
  ownerId = ctx.ownerId;
1431
+ ownerName = ctx.ownerName;
1329
1432
  console.error(`[canon-host] Connected as ${ctx.displayName || agentId}`);
1330
1433
  }
1331
1434
  catch {
@@ -1390,6 +1493,8 @@ export async function main() {
1390
1493
  };
1391
1494
  const conversationCache = new Map();
1392
1495
  const knownConversationIds = new Set();
1496
+ const promptedGroupContextConversationIds = new Set();
1497
+ const pendingMembershipChanges = new Map();
1393
1498
  let lastKnownConversationRefreshAt = 0;
1394
1499
  const publishRuntimeHeartbeat = async () => {
1395
1500
  heartbeatLocalRuntimeEntry(runtimeId, {
@@ -1542,18 +1647,65 @@ export async function main() {
1542
1647
  }
1543
1648
  lastKnownConversationRefreshAt = Date.now();
1544
1649
  }
1650
+ function handleConversationUpdated(payload) {
1651
+ const rawMemberIds = payload.changes.memberIds;
1652
+ if (!Array.isArray(rawMemberIds))
1653
+ return;
1654
+ const memberIds = rawMemberIds.filter((id) => typeof id === 'string');
1655
+ const cached = conversationCache.get(payload.conversationId);
1656
+ const membershipChange = payload.membershipChange
1657
+ ?? (cached ? diffCanonMemberIds(cached.memberIds, memberIds) : null);
1658
+ if (cached) {
1659
+ conversationCache.set(payload.conversationId, {
1660
+ ...cached,
1661
+ memberIds,
1662
+ });
1663
+ }
1664
+ if (membershipChange) {
1665
+ pendingMembershipChanges.set(payload.conversationId, membershipChange);
1666
+ }
1667
+ if (!memberIds.includes(agentId)) {
1668
+ knownConversationIds.delete(payload.conversationId);
1669
+ conversationCache.delete(payload.conversationId);
1670
+ }
1671
+ }
1672
+ function getGroupContextMode(conversationId, conversation) {
1673
+ if (conversation?.type !== 'group')
1674
+ return undefined;
1675
+ if (pendingMembershipChanges.has(conversationId))
1676
+ return 'membership_change';
1677
+ if (!promptedGroupContextConversationIds.has(conversationId))
1678
+ return 'initial';
1679
+ return undefined;
1680
+ }
1681
+ function markGroupContextModeUsed(conversationId, mode) {
1682
+ if (!mode)
1683
+ return;
1684
+ promptedGroupContextConversationIds.add(conversationId);
1685
+ if (mode === 'membership_change') {
1686
+ pendingMembershipChanges.delete(conversationId);
1687
+ }
1688
+ }
1545
1689
  async function loadHydratedInboundContext(input) {
1546
1690
  const [conversation, page] = await Promise.all([
1547
1691
  getConversationMeta(input.conversationId),
1548
- client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
1692
+ input.hydratedPage
1693
+ ? Promise.resolve(input.hydratedPage)
1694
+ : client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
1549
1695
  ]);
1550
1696
  return buildHydratedInboundContext({
1551
1697
  agentId,
1552
1698
  conversation,
1553
1699
  page,
1700
+ activeSelfContextId: input.activeSelfContextId,
1701
+ selfContexts: input.selfContexts,
1554
1702
  message: input.message,
1555
1703
  senderName: input.senderName,
1556
1704
  isOwner: input.isOwner,
1705
+ ownerId,
1706
+ ownerName,
1707
+ membershipChange: pendingMembershipChanges.get(input.conversationId) ?? null,
1708
+ groupContextMode: getGroupContextMode(input.conversationId, conversation),
1557
1709
  });
1558
1710
  }
1559
1711
  function closeSession(conversationId, options = {}) {
@@ -1788,15 +1940,18 @@ export async function main() {
1788
1940
  message: m,
1789
1941
  senderName: sender,
1790
1942
  isOwner,
1943
+ hydratedPage: input.hydratedPage,
1791
1944
  });
1792
1945
  const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
1793
- const selfContexts = input.hydratedPage?.selfContexts ?? hydrated.selfContexts;
1946
+ const activeSelfContextId = hydrated.activeSelfContextId;
1947
+ const selfContexts = hydrated.selfContexts;
1794
1948
  const participantContext = hydrated.participantContext;
1795
1949
  const autoReply = decideAutoReply(participantContext, behavior);
1796
1950
  if (!autoReply.allow) {
1797
1951
  console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Suppressed recovered auto-reply: ${autoReply.reason}`);
1798
1952
  return;
1799
1953
  }
1954
+ markGroupContextModeUsed(input.conversationId, participantContext.groupContextMode);
1800
1955
  let session;
1801
1956
  try {
1802
1957
  session = await getOrCreateSession(input.conversationId);
@@ -1806,7 +1961,7 @@ export async function main() {
1806
1961
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1807
1962
  console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
1808
1963
  await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1809
- ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
1964
+ ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
1810
1965
  metadata: {
1811
1966
  turnSemantics: 'turn_complete',
1812
1967
  turnComplete: true,
@@ -1816,7 +1971,7 @@ export async function main() {
1816
1971
  return;
1817
1972
  }
1818
1973
  const turnMetadata = normalizeTurnMetadata(m.metadata);
1819
- session.activeSelfContextId = selfContexts[0]?.id ?? null;
1974
+ session.activeSelfContextId = activeSelfContextId;
1820
1975
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
1821
1976
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
1822
1977
  const promptText = buildCanonPrompt({
@@ -1934,19 +2089,19 @@ export async function main() {
1934
2089
  message: m,
1935
2090
  senderName: sender,
1936
2091
  isOwner,
2092
+ activeSelfContextId: payload.activeSelfContextId,
2093
+ selfContexts: payload.selfContexts,
1937
2094
  });
1938
2095
  const behavior = payload.behavior ?? hydrated.behavior;
1939
- const selfContexts = hydrated.hydratedFromPage
1940
- ? hydrated.selfContexts
1941
- : Array.isArray(payload.selfContexts)
1942
- ? payload.selfContexts
1943
- : hydrated.selfContexts;
2096
+ const activeSelfContextId = hydrated.activeSelfContextId;
2097
+ const selfContexts = hydrated.selfContexts;
1944
2098
  const participantContext = hydrated.participantContext;
1945
2099
  const autoReply = decideAutoReply(participantContext, behavior);
1946
2100
  if (!autoReply.allow) {
1947
2101
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
1948
2102
  return;
1949
2103
  }
2104
+ markGroupContextModeUsed(convoId, participantContext.groupContextMode);
1950
2105
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Message from ${sender}: "${content.slice(0, 80)}" (${autoReply.reason})`);
1951
2106
  let session;
1952
2107
  try {
@@ -1957,7 +2112,7 @@ export async function main() {
1957
2112
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1958
2113
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
1959
2114
  await client.sendMessage(convoId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1960
- ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
2115
+ ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
1961
2116
  metadata: {
1962
2117
  turnSemantics: 'turn_complete',
1963
2118
  turnComplete: true,
@@ -1967,7 +2122,7 @@ export async function main() {
1967
2122
  return;
1968
2123
  }
1969
2124
  const turnMetadata = normalizeTurnMetadata(m.metadata);
1970
- session.activeSelfContextId = selfContexts[0]?.id ?? null;
2125
+ session.activeSelfContextId = activeSelfContextId;
1971
2126
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
1972
2127
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
1973
2128
  const promptText = buildCanonPrompt({
@@ -1996,6 +2151,9 @@ export async function main() {
1996
2151
  }
1997
2152
  })();
1998
2153
  },
2154
+ onConversationUpdated: (payload) => {
2155
+ handleConversationUpdated(payload);
2156
+ },
1999
2157
  onConnected: () => {
2000
2158
  streamConnected = true;
2001
2159
  void publishRuntimeHeartbeat();
@@ -1,4 +1,4 @@
1
- import { type CanonConversation, type ResolvedAgentBehaviorPolicy } from '@canonmsg/core';
1
+ import { type CanonGroupContext, type CanonGroupContextMode, type CanonConversation, type ResolvedAgentBehaviorPolicy } from '@canonmsg/core';
2
2
  export interface InboundParticipantContext {
3
3
  conversationType: CanonConversation['type'] | 'unknown';
4
4
  memberCount: number | null;
@@ -6,6 +6,8 @@ export interface InboundParticipantContext {
6
6
  senderName: string;
7
7
  isOwner: boolean;
8
8
  mentionedAgent: boolean;
9
+ groupContext?: CanonGroupContext;
10
+ groupContextMode?: CanonGroupContextMode;
9
11
  recentSenderTypes: Array<'human' | 'ai_agent'>;
10
12
  recentHumanCount: number;
11
13
  recentAgentCount: number;
@@ -1,4 +1,4 @@
1
- import { evaluateParticipationPolicy, resolveAgentBehaviorPolicy, } from '@canonmsg/core';
1
+ import { buildCompactGroupContextLines, evaluateParticipationPolicy, resolveAgentBehaviorPolicy, } from '@canonmsg/core';
2
2
  function formatRecentSenders(senderTypes) {
3
3
  if (senderTypes.length === 0)
4
4
  return 'none';
@@ -18,6 +18,9 @@ export function buildInboundContextLines(context) {
18
18
  `Latest sender name: ${context.senderName}`,
19
19
  `Latest sender type: ${context.senderType}`,
20
20
  `Conversation type: ${conversationTypeLabel}`,
21
+ ...(context.groupContext && context.groupContextMode
22
+ ? buildCompactGroupContextLines(context.groupContext, context.groupContextMode)
23
+ : []),
21
24
  `Directly addressed to this agent: ${context.mentionedAgent ? 'yes' : 'no'}`,
22
25
  `Recent sender pattern: ${formatRecentSenders(context.recentSenderTypes)}`,
23
26
  `Recent human messages: ${context.recentHumanCount}`,
package/dist/register.js CHANGED
@@ -107,7 +107,9 @@ export async function main() {
107
107
  console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
108
108
  console.log(`Saved as profile "${profileName}" in ~/.canon/agents.json`);
109
109
  console.log('Start it with: CANON_AGENT=' + profileName + ' canon-claude --cwd /path/to/project');
110
- console.log('Keep that terminal open. Closing it takes this local agent offline.');
110
+ console.log('Keep that terminal open while you want Canon to reach the agent.');
111
+ console.log('Closing it, logging out, rebooting, or sleeping long enough to stop the process takes this local agent offline until you revive it.');
112
+ console.log('Docs: https://canonmail.com/agents/integrations');
111
113
  break;
112
114
  }
113
115
  case 'rejected':
package/dist/server.js CHANGED
@@ -447,6 +447,9 @@ async function handleInboundMessage(payload) {
447
447
  }
448
448
  }
449
449
  if (Array.isArray(payload.selfContexts) && payload.selfContexts.length > 0) {
450
+ if (payload.activeSelfContextId) {
451
+ meta.active_self_context_id = payload.activeSelfContextId;
452
+ }
450
453
  meta.self_context_ids = payload.selfContexts
451
454
  .map((selfContext) => selfContext.id)
452
455
  .join('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.13.1",
3
+ "version": "0.14.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,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "^0.2.101",
34
- "@canonmsg/agent-sdk": "^1.2.1",
35
- "@canonmsg/core": "^0.16.0",
34
+ "@canonmsg/agent-sdk": "^1.3.1",
35
+ "@canonmsg/core": "^0.17.2",
36
36
  "@modelcontextprotocol/sdk": "^1.12.0"
37
37
  },
38
38
  "engines": {