@canonmsg/claude-code-plugin 0.13.1 → 0.14.0

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 = [];
@@ -1322,10 +1325,12 @@ export async function main() {
1322
1325
  initRTDBAuth(client);
1323
1326
  let agentId;
1324
1327
  let ownerId = null;
1328
+ let ownerName = null;
1325
1329
  try {
1326
1330
  const ctx = await client.getAgentMe();
1327
1331
  agentId = ctx.agentId;
1328
1332
  ownerId = ctx.ownerId;
1333
+ ownerName = ctx.ownerName;
1329
1334
  console.error(`[canon-host] Connected as ${ctx.displayName || agentId}`);
1330
1335
  }
1331
1336
  catch {
@@ -1390,6 +1395,8 @@ export async function main() {
1390
1395
  };
1391
1396
  const conversationCache = new Map();
1392
1397
  const knownConversationIds = new Set();
1398
+ const promptedGroupContextConversationIds = new Set();
1399
+ const pendingMembershipChanges = new Map();
1393
1400
  let lastKnownConversationRefreshAt = 0;
1394
1401
  const publishRuntimeHeartbeat = async () => {
1395
1402
  heartbeatLocalRuntimeEntry(runtimeId, {
@@ -1542,18 +1549,65 @@ export async function main() {
1542
1549
  }
1543
1550
  lastKnownConversationRefreshAt = Date.now();
1544
1551
  }
1552
+ function handleConversationUpdated(payload) {
1553
+ const rawMemberIds = payload.changes.memberIds;
1554
+ if (!Array.isArray(rawMemberIds))
1555
+ return;
1556
+ const memberIds = rawMemberIds.filter((id) => typeof id === 'string');
1557
+ const cached = conversationCache.get(payload.conversationId);
1558
+ const membershipChange = payload.membershipChange
1559
+ ?? (cached ? diffCanonMemberIds(cached.memberIds, memberIds) : null);
1560
+ if (cached) {
1561
+ conversationCache.set(payload.conversationId, {
1562
+ ...cached,
1563
+ memberIds,
1564
+ });
1565
+ }
1566
+ if (membershipChange) {
1567
+ pendingMembershipChanges.set(payload.conversationId, membershipChange);
1568
+ }
1569
+ if (!memberIds.includes(agentId)) {
1570
+ knownConversationIds.delete(payload.conversationId);
1571
+ conversationCache.delete(payload.conversationId);
1572
+ }
1573
+ }
1574
+ function getGroupContextMode(conversationId, conversation) {
1575
+ if (conversation?.type !== 'group')
1576
+ return undefined;
1577
+ if (pendingMembershipChanges.has(conversationId))
1578
+ return 'membership_change';
1579
+ if (!promptedGroupContextConversationIds.has(conversationId))
1580
+ return 'initial';
1581
+ return undefined;
1582
+ }
1583
+ function markGroupContextModeUsed(conversationId, mode) {
1584
+ if (!mode)
1585
+ return;
1586
+ promptedGroupContextConversationIds.add(conversationId);
1587
+ if (mode === 'membership_change') {
1588
+ pendingMembershipChanges.delete(conversationId);
1589
+ }
1590
+ }
1545
1591
  async function loadHydratedInboundContext(input) {
1546
1592
  const [conversation, page] = await Promise.all([
1547
1593
  getConversationMeta(input.conversationId),
1548
- client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
1594
+ input.hydratedPage
1595
+ ? Promise.resolve(input.hydratedPage)
1596
+ : client.getMessagesPage(input.conversationId, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT).catch(() => null),
1549
1597
  ]);
1550
1598
  return buildHydratedInboundContext({
1551
1599
  agentId,
1552
1600
  conversation,
1553
1601
  page,
1602
+ activeSelfContextId: input.activeSelfContextId,
1603
+ selfContexts: input.selfContexts,
1554
1604
  message: input.message,
1555
1605
  senderName: input.senderName,
1556
1606
  isOwner: input.isOwner,
1607
+ ownerId,
1608
+ ownerName,
1609
+ membershipChange: pendingMembershipChanges.get(input.conversationId) ?? null,
1610
+ groupContextMode: getGroupContextMode(input.conversationId, conversation),
1557
1611
  });
1558
1612
  }
1559
1613
  function closeSession(conversationId, options = {}) {
@@ -1788,15 +1842,18 @@ export async function main() {
1788
1842
  message: m,
1789
1843
  senderName: sender,
1790
1844
  isOwner,
1845
+ hydratedPage: input.hydratedPage,
1791
1846
  });
1792
1847
  const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
1793
- const selfContexts = input.hydratedPage?.selfContexts ?? hydrated.selfContexts;
1848
+ const activeSelfContextId = hydrated.activeSelfContextId;
1849
+ const selfContexts = hydrated.selfContexts;
1794
1850
  const participantContext = hydrated.participantContext;
1795
1851
  const autoReply = decideAutoReply(participantContext, behavior);
1796
1852
  if (!autoReply.allow) {
1797
1853
  console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Suppressed recovered auto-reply: ${autoReply.reason}`);
1798
1854
  return;
1799
1855
  }
1856
+ markGroupContextModeUsed(input.conversationId, participantContext.groupContextMode);
1800
1857
  let session;
1801
1858
  try {
1802
1859
  session = await getOrCreateSession(input.conversationId);
@@ -1806,7 +1863,7 @@ export async function main() {
1806
1863
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1807
1864
  console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
1808
1865
  await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1809
- ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
1866
+ ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
1810
1867
  metadata: {
1811
1868
  turnSemantics: 'turn_complete',
1812
1869
  turnComplete: true,
@@ -1816,7 +1873,7 @@ export async function main() {
1816
1873
  return;
1817
1874
  }
1818
1875
  const turnMetadata = normalizeTurnMetadata(m.metadata);
1819
- session.activeSelfContextId = selfContexts[0]?.id ?? null;
1876
+ session.activeSelfContextId = activeSelfContextId;
1820
1877
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
1821
1878
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
1822
1879
  const promptText = buildCanonPrompt({
@@ -1934,19 +1991,19 @@ export async function main() {
1934
1991
  message: m,
1935
1992
  senderName: sender,
1936
1993
  isOwner,
1994
+ activeSelfContextId: payload.activeSelfContextId,
1995
+ selfContexts: payload.selfContexts,
1937
1996
  });
1938
1997
  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;
1998
+ const activeSelfContextId = hydrated.activeSelfContextId;
1999
+ const selfContexts = hydrated.selfContexts;
1944
2000
  const participantContext = hydrated.participantContext;
1945
2001
  const autoReply = decideAutoReply(participantContext, behavior);
1946
2002
  if (!autoReply.allow) {
1947
2003
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
1948
2004
  return;
1949
2005
  }
2006
+ markGroupContextModeUsed(convoId, participantContext.groupContextMode);
1950
2007
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Message from ${sender}: "${content.slice(0, 80)}" (${autoReply.reason})`);
1951
2008
  let session;
1952
2009
  try {
@@ -1957,7 +2014,7 @@ export async function main() {
1957
2014
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1958
2015
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
1959
2016
  await client.sendMessage(convoId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1960
- ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
2017
+ ...(activeSelfContextId ? { selfContextId: activeSelfContextId } : {}),
1961
2018
  metadata: {
1962
2019
  turnSemantics: 'turn_complete',
1963
2020
  turnComplete: true,
@@ -1967,7 +2024,7 @@ export async function main() {
1967
2024
  return;
1968
2025
  }
1969
2026
  const turnMetadata = normalizeTurnMetadata(m.metadata);
1970
- session.activeSelfContextId = selfContexts[0]?.id ?? null;
2027
+ session.activeSelfContextId = activeSelfContextId;
1971
2028
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
1972
2029
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
1973
2030
  const promptText = buildCanonPrompt({
@@ -1996,6 +2053,9 @@ export async function main() {
1996
2053
  }
1997
2054
  })();
1998
2055
  },
2056
+ onConversationUpdated: (payload) => {
2057
+ handleConversationUpdated(payload);
2058
+ },
1999
2059
  onConnected: () => {
2000
2060
  streamConnected = true;
2001
2061
  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.0",
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.0",
35
+ "@canonmsg/core": "^0.17.0",
36
36
  "@modelcontextprotocol/sdk": "^1.12.0"
37
37
  },
38
38
  "engines": {