@canonmsg/claude-code-plugin 0.12.8 → 0.13.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 +12 -2
- package/dist/host.js +66 -36
- package/dist/session-state.d.ts +15 -1
- package/dist/session-state.js +11 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -66,9 +66,19 @@ CANON_AGENT=frontend canon-claude --cwd ~/projects/frontend
|
|
|
66
66
|
|
|
67
67
|
## Channel mode (alternative)
|
|
68
68
|
|
|
69
|
-
For a lighter integration without session controls, run Canon as a channel plugin inside your own Claude Code session.
|
|
69
|
+
For a lighter integration without session controls, run Canon as a channel plugin inside your own Claude Code session. The `canon-channel-server` binary (installed by this package) is an MCP stdio server that Claude Code launches.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Register the channel server with Claude Code's MCP config and provide your Canon API key via the `CANON_API_KEY` environment variable:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Register once per Claude Code project
|
|
75
|
+
claude mcp add canon-channel canon-channel-server
|
|
76
|
+
|
|
77
|
+
# Then start Claude Code with your Canon key in the environment
|
|
78
|
+
CANON_API_KEY=agk_live_... claude
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Channel mode forwards messages between Canon conversations and the embedded Claude Code session without managing session lifecycle. It is shown as non-revivable by `canon-necromance`. Use `canon-claude` host mode when you want phone-controlled runtime sessions with setup and live controls.
|
|
72
82
|
|
|
73
83
|
## Development
|
|
74
84
|
|
package/dist/host.js
CHANGED
|
@@ -26,11 +26,11 @@ 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, 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, 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';
|
|
33
|
-
import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
33
|
+
import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
34
34
|
const HELP = `canon-claude — run a local Claude Code agent host for Canon
|
|
35
35
|
|
|
36
36
|
USAGE
|
|
@@ -154,26 +154,9 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
154
154
|
availability: ['always'],
|
|
155
155
|
dispatch: { kind: 'open_details', target: 'effort' },
|
|
156
156
|
},
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
description: 'Interrupt the current Claude turn.',
|
|
161
|
-
aliases: ['stop'],
|
|
162
|
-
category: 'turn',
|
|
163
|
-
placements: ['composer_slash', 'command_palette'],
|
|
164
|
-
availability: ['busy'],
|
|
165
|
-
dispatch: { kind: 'signal', signal: 'interrupt' },
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
id: 'stop-and-clear-queue',
|
|
169
|
-
label: 'Stop & clear queue',
|
|
170
|
-
description: 'Interrupt the current Claude turn and drop queued Canon messages.',
|
|
171
|
-
aliases: ['stop-clear', 'clear-queue'],
|
|
172
|
-
category: 'turn',
|
|
173
|
-
placements: ['composer_slash', 'command_palette', 'session_strip'],
|
|
174
|
-
availability: ['busy_with_queue'],
|
|
175
|
-
dispatch: { kind: 'signal', signal: 'stop_and_drop' },
|
|
176
|
-
},
|
|
157
|
+
RUNTIME_STOP_ACTION,
|
|
158
|
+
RUNTIME_STOP_AND_DROP_ACTION,
|
|
159
|
+
RUNTIME_NEW_SESSION_ACTION,
|
|
177
160
|
],
|
|
178
161
|
});
|
|
179
162
|
}
|
|
@@ -802,6 +785,10 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
802
785
|
function onStreamDelta(text) {
|
|
803
786
|
streamingBuffer.append(text);
|
|
804
787
|
}
|
|
788
|
+
function isActiveTurnInterrupted() {
|
|
789
|
+
const turnKey = session.activeInput?.turnKey;
|
|
790
|
+
return Boolean(turnKey && session.interruptedTurnKeys.has(turnKey));
|
|
791
|
+
}
|
|
805
792
|
function clearStreaming() {
|
|
806
793
|
streamingBuffer.clear();
|
|
807
794
|
runtimeState.clearStreaming(conversationId).catch(() => { });
|
|
@@ -822,15 +809,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
822
809
|
if (session.closed)
|
|
823
810
|
return;
|
|
824
811
|
clearFinalDeliveryTimer();
|
|
825
|
-
session
|
|
826
|
-
session.currentTurnOpenedAt = null;
|
|
827
|
-
session.lastAcceptedIntent = null;
|
|
828
|
-
session.pendingFinalText = null;
|
|
829
|
-
session.pendingFinalDelivery = null;
|
|
830
|
-
session.activeInput = null;
|
|
831
|
-
session.toolInProgress = false;
|
|
832
|
-
session.turnState = 'idle';
|
|
812
|
+
resetClaudeCompletedTurnState(session);
|
|
833
813
|
stopVisibleWorkSignal();
|
|
814
|
+
writeState();
|
|
834
815
|
writeTurn();
|
|
835
816
|
drainPendingInput();
|
|
836
817
|
}
|
|
@@ -1100,6 +1081,11 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
1100
1081
|
}
|
|
1101
1082
|
case 'stream_event': {
|
|
1102
1083
|
const event = msg.event;
|
|
1084
|
+
if (isActiveTurnInterrupted()) {
|
|
1085
|
+
stopVisibleWorkSignal();
|
|
1086
|
+
client.setTyping(conversationId, false).catch(() => { });
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1103
1089
|
if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
|
|
1104
1090
|
session.toolInProgress = true;
|
|
1105
1091
|
session.turnState = 'tool';
|
|
@@ -1570,19 +1556,20 @@ export async function main() {
|
|
|
1570
1556
|
isOwner: input.isOwner,
|
|
1571
1557
|
});
|
|
1572
1558
|
}
|
|
1573
|
-
function closeSession(conversationId) {
|
|
1559
|
+
function closeSession(conversationId, options = {}) {
|
|
1574
1560
|
const session = sessions.get(conversationId);
|
|
1575
1561
|
if (!session)
|
|
1576
1562
|
return;
|
|
1563
|
+
const saveResume = options.saveResume !== false;
|
|
1577
1564
|
session.closed = true;
|
|
1578
1565
|
if (session.typingKeepaliveTimer) {
|
|
1579
1566
|
clearInterval(session.typingKeepaliveTimer);
|
|
1580
1567
|
session.typingKeepaliveTimer = null;
|
|
1581
1568
|
}
|
|
1582
1569
|
releaseConversationEnvironment(session.environment);
|
|
1583
|
-
|
|
1584
|
-
if (session.sdkSessionId) {
|
|
1585
|
-
savedSessionIds.set(
|
|
1570
|
+
const resumeKey = buildConversationEnvironmentKey(conversationId, session.environment.baseCwd);
|
|
1571
|
+
if (saveResume && session.sdkSessionId) {
|
|
1572
|
+
savedSessionIds.set(resumeKey, session.sdkSessionId);
|
|
1586
1573
|
saveRuntimeSessionState(runtimeId, {
|
|
1587
1574
|
conversationId,
|
|
1588
1575
|
baseCwd: session.environment.baseCwd,
|
|
@@ -1590,6 +1577,27 @@ export async function main() {
|
|
|
1590
1577
|
claudeSessionId: session.sdkSessionId,
|
|
1591
1578
|
});
|
|
1592
1579
|
}
|
|
1580
|
+
else {
|
|
1581
|
+
const existingState = loadRuntimeSessionState(runtimeId, {
|
|
1582
|
+
conversationId,
|
|
1583
|
+
baseCwd: session.environment.baseCwd,
|
|
1584
|
+
executionMode: session.environment.mode,
|
|
1585
|
+
});
|
|
1586
|
+
savedSessionIds.delete(resumeKey);
|
|
1587
|
+
clearRuntimeSessionState(runtimeId, {
|
|
1588
|
+
conversationId,
|
|
1589
|
+
baseCwd: session.environment.baseCwd,
|
|
1590
|
+
executionMode: session.environment.mode,
|
|
1591
|
+
});
|
|
1592
|
+
if (existingState?.lastInboundMessageId) {
|
|
1593
|
+
saveRuntimeSessionState(runtimeId, {
|
|
1594
|
+
conversationId,
|
|
1595
|
+
baseCwd: session.environment.baseCwd,
|
|
1596
|
+
executionMode: session.environment.mode,
|
|
1597
|
+
lastInboundMessageId: existingState.lastInboundMessageId,
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1593
1601
|
if (session.streamingTimer)
|
|
1594
1602
|
clearTimeout(session.streamingTimer);
|
|
1595
1603
|
session.streamingText = '';
|
|
@@ -1610,7 +1618,22 @@ export async function main() {
|
|
|
1610
1618
|
}
|
|
1611
1619
|
catch { }
|
|
1612
1620
|
sessions.delete(conversationId);
|
|
1613
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session suspended (sessionId saved for resume)`);
|
|
1621
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ${options.reason ?? (saveResume ? 'suspended (sessionId saved for resume)' : 'cleared')}`);
|
|
1622
|
+
}
|
|
1623
|
+
async function resetRuntimeSession(session) {
|
|
1624
|
+
const conversationId = session.conversationId;
|
|
1625
|
+
const droppedInputs = session.pendingInputs.splice(0);
|
|
1626
|
+
await markSessionPendingInputsRejected(conversationId, droppedInputs);
|
|
1627
|
+
if (session.activeInput) {
|
|
1628
|
+
session.interruptedTurnKeys.add(session.activeInput.turnKey);
|
|
1629
|
+
}
|
|
1630
|
+
session.pendingFinalText = null;
|
|
1631
|
+
session.pendingFinalDelivery = null;
|
|
1632
|
+
session.turnState = 'interrupted';
|
|
1633
|
+
if (session.state.state === 'running') {
|
|
1634
|
+
await session.query.interrupt().catch(() => { });
|
|
1635
|
+
}
|
|
1636
|
+
closeSession(conversationId, { saveResume: false, reason: 'reset for new Canon session' });
|
|
1614
1637
|
}
|
|
1615
1638
|
async function markSessionPendingInputsRejected(conversationId, inputs) {
|
|
1616
1639
|
await Promise.all(inputs.map((input) => {
|
|
@@ -2054,10 +2077,17 @@ export async function main() {
|
|
|
2054
2077
|
if (sig && typeof sig === 'object') {
|
|
2055
2078
|
const s = sig;
|
|
2056
2079
|
const ts = s.updatedAt ?? 0;
|
|
2057
|
-
if ((s.type === 'interrupt' || s.type === 'stop_and_drop'
|
|
2080
|
+
if ((s.type === 'interrupt' || s.type === 'stop_and_drop' || s.type === 'new_session')
|
|
2081
|
+
&& ts > (lastSeenSignal.get(convoId) ?? 0)) {
|
|
2058
2082
|
lastSeenSignal.set(convoId, ts);
|
|
2059
2083
|
const session = sessions.get(convoId);
|
|
2060
2084
|
if (session && !session.closed) {
|
|
2085
|
+
if (s.type === 'new_session') {
|
|
2086
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] new_session signal`);
|
|
2087
|
+
await resetRuntimeSession(session);
|
|
2088
|
+
await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2061
2091
|
const isActive = session.state.state === 'running';
|
|
2062
2092
|
if (!isActive && (s.type !== 'stop_and_drop' || session.pendingInputs.length === 0)) {
|
|
2063
2093
|
await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
|
package/dist/session-state.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import type { DeliveryIntent, ModelOption } from '@canonmsg/core';
|
|
2
|
+
import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core';
|
|
3
3
|
export type ClaudeInputKind = 'seed' | 'canon';
|
|
4
4
|
export interface ClaudeInputEnvelope {
|
|
5
5
|
kind: ClaudeInputKind;
|
|
@@ -19,6 +19,19 @@ export interface ClaudePendingFinalDelivery {
|
|
|
19
19
|
text: string;
|
|
20
20
|
messageId: string;
|
|
21
21
|
}
|
|
22
|
+
export interface ClaudeCompletedTurnState {
|
|
23
|
+
state: {
|
|
24
|
+
state?: 'idle' | 'running' | 'requires_action';
|
|
25
|
+
};
|
|
26
|
+
pendingFinalText: string | null;
|
|
27
|
+
pendingFinalDelivery: ClaudePendingFinalDelivery | null;
|
|
28
|
+
activeInput: ClaudeInputEnvelope | null;
|
|
29
|
+
toolInProgress: boolean;
|
|
30
|
+
turnState: TurnLifecycleState;
|
|
31
|
+
currentTurnId: string | null;
|
|
32
|
+
currentTurnOpenedAt: number | null;
|
|
33
|
+
lastAcceptedIntent: DeliveryIntent | null;
|
|
34
|
+
}
|
|
22
35
|
export declare function createClaudeInputEnvelope(input: {
|
|
23
36
|
kind: ClaudeInputKind;
|
|
24
37
|
msg: SDKUserMessage;
|
|
@@ -36,6 +49,7 @@ export declare function shouldDeliverClaudeFinal(input: {
|
|
|
36
49
|
finalizedTurnKeys: ReadonlySet<string>;
|
|
37
50
|
interruptedTurnKeys: ReadonlySet<string>;
|
|
38
51
|
}): boolean;
|
|
52
|
+
export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
|
|
39
53
|
export declare function mergeClaudeModelOptions(input: {
|
|
40
54
|
previous: ReadonlyArray<ModelOption>;
|
|
41
55
|
next: ReadonlyArray<ModelOption>;
|
package/dist/session-state.js
CHANGED
|
@@ -29,6 +29,17 @@ export function shouldDeliverClaudeFinal(input) {
|
|
|
29
29
|
return false;
|
|
30
30
|
return true;
|
|
31
31
|
}
|
|
32
|
+
export function resetClaudeCompletedTurnState(session) {
|
|
33
|
+
session.state.state = 'idle';
|
|
34
|
+
session.currentTurnId = null;
|
|
35
|
+
session.currentTurnOpenedAt = null;
|
|
36
|
+
session.lastAcceptedIntent = null;
|
|
37
|
+
session.pendingFinalText = null;
|
|
38
|
+
session.pendingFinalDelivery = null;
|
|
39
|
+
session.activeInput = null;
|
|
40
|
+
session.toolInProgress = false;
|
|
41
|
+
session.turnState = 'idle';
|
|
42
|
+
}
|
|
32
43
|
export function mergeClaudeModelOptions(input) {
|
|
33
44
|
const byValue = new Map();
|
|
34
45
|
for (const option of input.previous) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.1
|
|
35
|
-
"@canonmsg/core": "^0.
|
|
34
|
+
"@canonmsg/agent-sdk": "^1.2.1",
|
|
35
|
+
"@canonmsg/core": "^0.16.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|