@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 +3 -1
- package/dist/host.js +73 -13
- package/dist/inbound-policy.d.ts +3 -1
- package/dist/inbound-policy.js +4 -1
- package/dist/register.js +3 -1
- package/dist/server.js +3 -0
- package/package.json +3 -3
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.).
|
|
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
|
-
|
|
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
|
|
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
|
-
...(
|
|
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 =
|
|
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
|
|
1940
|
-
|
|
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
|
-
...(
|
|
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 =
|
|
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();
|
package/dist/inbound-policy.d.ts
CHANGED
|
@@ -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;
|
package/dist/inbound-policy.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
35
|
-
"@canonmsg/core": "^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": {
|