@canonmsg/claude-code-plugin 0.15.0 → 0.15.2
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 +152 -31
- package/dist/mcp-args.d.ts +2 -1
- package/dist/mcp-args.js +26 -0
- package/dist/server.js +7 -1
- package/package.json +3 -3
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, materializeReplyContextMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
|
|
29
|
-
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,
|
|
29
|
+
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, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, 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, rtdbRead, rtdbWrite, sendMessageWithRetry, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCanonTurnBriefPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
30
30
|
import { decideAutoReply, } from './inbound-policy.js';
|
|
31
31
|
import { runCli } from './cli-entry.js';
|
|
32
32
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
@@ -87,21 +87,6 @@ let runtimeMetadata = {
|
|
|
87
87
|
let runtimeMetadataRefresh = null;
|
|
88
88
|
function buildClaudeRuntimeDescriptor(input) {
|
|
89
89
|
const commands = [
|
|
90
|
-
{
|
|
91
|
-
id: 'plan',
|
|
92
|
-
label: 'Plan first',
|
|
93
|
-
description: 'Switch Claude into Plan mode. Text after /plan is kept as a ready prompt while the mode applies.',
|
|
94
|
-
aliases: ['plan'],
|
|
95
|
-
category: 'plan',
|
|
96
|
-
placements: ['composer_slash', 'command_palette'],
|
|
97
|
-
availability: ['always'],
|
|
98
|
-
trailingTextBehavior: 'send_as_prompt',
|
|
99
|
-
dispatch: {
|
|
100
|
-
kind: 'control',
|
|
101
|
-
controlId: 'permissionMode',
|
|
102
|
-
value: 'plan',
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
90
|
{
|
|
106
91
|
id: 'runtime-status',
|
|
107
92
|
label: 'Runtime status',
|
|
@@ -190,6 +175,25 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
190
175
|
permissionModes: CLAUDE_PERMISSION_MODE_OPTIONS,
|
|
191
176
|
defaultPermissionMode: 'default',
|
|
192
177
|
streamingTextMode: 'delta',
|
|
178
|
+
turnModes: [
|
|
179
|
+
{
|
|
180
|
+
id: 'normal',
|
|
181
|
+
label: 'Normal',
|
|
182
|
+
description: 'Let Claude answer or act normally.',
|
|
183
|
+
scope: 'session',
|
|
184
|
+
default: true,
|
|
185
|
+
activation: { kind: 'control', controlId: 'permissionMode', value: 'default' },
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'plan',
|
|
189
|
+
label: 'Plan',
|
|
190
|
+
description: 'Switch Claude Code into Plan mode.',
|
|
191
|
+
scope: 'session',
|
|
192
|
+
ownerOnly: true,
|
|
193
|
+
aliases: ['plan'],
|
|
194
|
+
activation: { kind: 'control', controlId: 'permissionMode', value: 'plan' },
|
|
195
|
+
},
|
|
196
|
+
],
|
|
193
197
|
admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
|
|
194
198
|
presentation: input.presentation,
|
|
195
199
|
commands,
|
|
@@ -380,6 +384,71 @@ async function refreshClaudeRuntimeMetadata(input) {
|
|
|
380
384
|
});
|
|
381
385
|
return runtimeMetadataRefresh;
|
|
382
386
|
}
|
|
387
|
+
function sleep(ms) {
|
|
388
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
389
|
+
}
|
|
390
|
+
function mapClaudeQuestions(value) {
|
|
391
|
+
if (!Array.isArray(value))
|
|
392
|
+
return undefined;
|
|
393
|
+
const questions = value.slice(0, 12).flatMap((entry) => {
|
|
394
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
395
|
+
return [];
|
|
396
|
+
const record = entry;
|
|
397
|
+
const id = typeof record.id === 'string' && record.id.trim()
|
|
398
|
+
? record.id.trim().slice(0, 120)
|
|
399
|
+
: typeof record.question === 'string' && record.question.trim()
|
|
400
|
+
? record.question.trim().slice(0, 80).replace(/[^A-Za-z0-9_.:-]+/g, '-')
|
|
401
|
+
: null;
|
|
402
|
+
const question = typeof record.question === 'string' && record.question.trim()
|
|
403
|
+
? record.question.trim().slice(0, 1000)
|
|
404
|
+
: null;
|
|
405
|
+
if (!id || !/^[A-Za-z0-9_.:-]{1,120}$/.test(id) || !question)
|
|
406
|
+
return [];
|
|
407
|
+
const header = typeof record.header === 'string' && record.header.trim()
|
|
408
|
+
? record.header.trim().slice(0, 120)
|
|
409
|
+
: undefined;
|
|
410
|
+
const rawOptions = Array.isArray(record.options) ? record.options : [];
|
|
411
|
+
const choices = rawOptions.slice(0, 12).flatMap((option) => {
|
|
412
|
+
if (!option || typeof option !== 'object' || Array.isArray(option))
|
|
413
|
+
return [];
|
|
414
|
+
const optionRecord = option;
|
|
415
|
+
const label = typeof optionRecord.label === 'string' && optionRecord.label.trim()
|
|
416
|
+
? optionRecord.label.trim().slice(0, 120)
|
|
417
|
+
: typeof optionRecord.value === 'string' && optionRecord.value.trim()
|
|
418
|
+
? optionRecord.value.trim().slice(0, 120)
|
|
419
|
+
: null;
|
|
420
|
+
if (!label)
|
|
421
|
+
return [];
|
|
422
|
+
const optionValue = typeof optionRecord.value === 'string' && optionRecord.value.trim()
|
|
423
|
+
? optionRecord.value.trim().slice(0, 120)
|
|
424
|
+
: label;
|
|
425
|
+
return [{
|
|
426
|
+
label,
|
|
427
|
+
value: optionValue,
|
|
428
|
+
...(typeof optionRecord.description === 'string' && optionRecord.description.trim()
|
|
429
|
+
? { description: optionRecord.description.trim().slice(0, 240) }
|
|
430
|
+
: {}),
|
|
431
|
+
}];
|
|
432
|
+
});
|
|
433
|
+
return [{
|
|
434
|
+
id,
|
|
435
|
+
question,
|
|
436
|
+
...(header ? { header } : {}),
|
|
437
|
+
...(choices.length ? { choices } : {}),
|
|
438
|
+
...(record.isOther === true || record.allowOther === true ? { allowOther: true } : {}),
|
|
439
|
+
...(record.isSecret === true ? { isSecret: true } : {}),
|
|
440
|
+
...(record.multiSelect === true ? { multiSelect: true } : {}),
|
|
441
|
+
}];
|
|
442
|
+
});
|
|
443
|
+
return questions.length ? questions : undefined;
|
|
444
|
+
}
|
|
445
|
+
function flattenRuntimeInputAnswers(answers) {
|
|
446
|
+
const flattened = {};
|
|
447
|
+
for (const [questionId, answer] of Object.entries(answers ?? {})) {
|
|
448
|
+
flattened[questionId] = answer.answers.join(', ');
|
|
449
|
+
}
|
|
450
|
+
return flattened;
|
|
451
|
+
}
|
|
383
452
|
function mapClaudeMcpStatus(status) {
|
|
384
453
|
switch (status) {
|
|
385
454
|
case 'connected':
|
|
@@ -692,6 +761,30 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
692
761
|
};
|
|
693
762
|
});
|
|
694
763
|
}
|
|
764
|
+
async function waitForRuntimeInputResponse(input) {
|
|
765
|
+
session.lastActivity = Date.now();
|
|
766
|
+
while (Date.now() < input.expiresAt) {
|
|
767
|
+
const response = await client.consumeRuntimeInputResponse({
|
|
768
|
+
conversationId,
|
|
769
|
+
inputId: input.inputId,
|
|
770
|
+
}).catch(() => null);
|
|
771
|
+
if (response?.status === 'submitted') {
|
|
772
|
+
return { status: 'submitted', answers: response.answers, value: response.value };
|
|
773
|
+
}
|
|
774
|
+
if (response?.status === 'cancelled' || response?.status === 'timeout') {
|
|
775
|
+
return { status: response.status };
|
|
776
|
+
}
|
|
777
|
+
await sleep(1_000);
|
|
778
|
+
}
|
|
779
|
+
const response = await client.consumeRuntimeInputResponse({
|
|
780
|
+
conversationId,
|
|
781
|
+
inputId: input.inputId,
|
|
782
|
+
}).catch(() => null);
|
|
783
|
+
if (response?.status === 'submitted') {
|
|
784
|
+
return { status: 'submitted', answers: response.answers, value: response.value };
|
|
785
|
+
}
|
|
786
|
+
return { status: 'timeout' };
|
|
787
|
+
}
|
|
695
788
|
const claudeCliPath = resolveClaudeCliPath();
|
|
696
789
|
const queryOptions = {
|
|
697
790
|
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
@@ -715,24 +808,43 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
715
808
|
if (toolName === 'AskUserQuestion') {
|
|
716
809
|
try {
|
|
717
810
|
const questionId = randomUUID();
|
|
718
|
-
const questions = input.questions
|
|
719
|
-
const
|
|
720
|
-
await
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
811
|
+
const questions = mapClaudeQuestions(input.questions);
|
|
812
|
+
const expiresAt = Date.now() + REPLY_TIMEOUT_MS;
|
|
813
|
+
await client.createRuntimeInputRequest({
|
|
814
|
+
conversationId,
|
|
815
|
+
inputId: questionId,
|
|
816
|
+
kind: 'clarify',
|
|
817
|
+
expiresAt,
|
|
818
|
+
title: 'Claude needs input',
|
|
819
|
+
prompt: questions?.length
|
|
820
|
+
? 'Claude needs your input to continue.'
|
|
821
|
+
: 'Claude needs input to continue.',
|
|
822
|
+
...(questions ? { questions } : {}),
|
|
823
|
+
sensitive: Boolean(questions?.some((question) => question.isSecret)),
|
|
824
|
+
...(cardResponseUserId ? { responseUserId: cardResponseUserId } : {}),
|
|
825
|
+
native: {
|
|
826
|
+
runtime: 'claude-code',
|
|
827
|
+
method: 'AskUserQuestion',
|
|
828
|
+
requestId: questionId,
|
|
829
|
+
turnId: session.currentTurnId ?? undefined,
|
|
729
830
|
},
|
|
831
|
+
turnId: session.currentTurnId ?? undefined,
|
|
730
832
|
});
|
|
731
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}]
|
|
732
|
-
const reply = await
|
|
833
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Runtime input question sent (${questionId.slice(0, 8)})`);
|
|
834
|
+
const reply = await waitForRuntimeInputResponse({ inputId: questionId, expiresAt });
|
|
835
|
+
if (reply.status !== 'submitted') {
|
|
836
|
+
return { behavior: 'deny', message: 'No reply received (timed out)' };
|
|
837
|
+
}
|
|
733
838
|
return {
|
|
734
839
|
behavior: 'allow',
|
|
735
|
-
updatedInput: {
|
|
840
|
+
updatedInput: {
|
|
841
|
+
...input,
|
|
842
|
+
answers: reply.answers
|
|
843
|
+
? flattenRuntimeInputAnswers(reply.answers)
|
|
844
|
+
: reply.value
|
|
845
|
+
? { answer: reply.value }
|
|
846
|
+
: {},
|
|
847
|
+
},
|
|
736
848
|
};
|
|
737
849
|
}
|
|
738
850
|
catch {
|
|
@@ -2184,6 +2296,10 @@ export async function main() {
|
|
|
2184
2296
|
}
|
|
2185
2297
|
const sender = m.senderName || m.senderId;
|
|
2186
2298
|
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
2299
|
+
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
2300
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2187
2303
|
void (async () => {
|
|
2188
2304
|
let materialized = [];
|
|
2189
2305
|
if (m.id) {
|
|
@@ -2218,7 +2334,12 @@ export async function main() {
|
|
|
2218
2334
|
});
|
|
2219
2335
|
const replyContext = replyMedia.replyContext;
|
|
2220
2336
|
const participantContext = hydrated.participantContext;
|
|
2221
|
-
const autoReply =
|
|
2337
|
+
const autoReply = payload.turnDispatch?.kind === 'run_turn'
|
|
2338
|
+
? {
|
|
2339
|
+
allow: true,
|
|
2340
|
+
reason: payload.turnDispatch.reason || 'server dispatch allowed this turn',
|
|
2341
|
+
}
|
|
2342
|
+
: decideAutoReply(participantContext, behavior);
|
|
2222
2343
|
if (!autoReply.allow) {
|
|
2223
2344
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
2224
2345
|
return;
|
package/dist/mcp-args.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SendContextualMessageOptions } from '@canonmsg/core';
|
|
1
|
+
import type { DirectSessionSelection, SendContextualMessageOptions } from '@canonmsg/core';
|
|
2
2
|
export type McpParseResult<T> = {
|
|
3
3
|
ok: true;
|
|
4
4
|
value: T;
|
|
@@ -32,6 +32,7 @@ export interface SendContextualMessageArgs {
|
|
|
32
32
|
targetUserId?: string;
|
|
33
33
|
text: string;
|
|
34
34
|
selfContext: SendContextualMessageOptions['selfContext'];
|
|
35
|
+
sessionSelection?: DirectSessionSelection;
|
|
35
36
|
messageOptions?: SendContextualMessageOptions['messageOptions'];
|
|
36
37
|
}
|
|
37
38
|
export declare function parseSendContextualMessageArgs(value: unknown): McpParseResult<SendContextualMessageArgs>;
|
package/dist/mcp-args.js
CHANGED
|
@@ -87,6 +87,28 @@ export function parseSendMessageArgs(value) {
|
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
function parseSessionSelection(value) {
|
|
91
|
+
if (value == null)
|
|
92
|
+
return { ok: true, value: undefined };
|
|
93
|
+
const raw = typeof value === 'string'
|
|
94
|
+
? value
|
|
95
|
+
: isRecord(value) && typeof value.mode === 'string'
|
|
96
|
+
? value.mode
|
|
97
|
+
: null;
|
|
98
|
+
const mode = raw?.trim().toLowerCase().replace(/-/g, '_');
|
|
99
|
+
if (!mode)
|
|
100
|
+
return { ok: false, error: 'Invalid session_selection' };
|
|
101
|
+
if (mode === 'new' || mode === 'fresh' || mode === 'start_new') {
|
|
102
|
+
return { ok: true, value: { mode: 'new' } };
|
|
103
|
+
}
|
|
104
|
+
if (mode === 'continue_latest' || mode === 'latest' || mode === 'continue') {
|
|
105
|
+
return { ok: true, value: { mode: 'continue_latest' } };
|
|
106
|
+
}
|
|
107
|
+
if (mode === 'continue_or_create' || mode === 'auto' || mode === 'reuse_or_create') {
|
|
108
|
+
return { ok: true, value: { mode: 'continue_or_create' } };
|
|
109
|
+
}
|
|
110
|
+
return { ok: false, error: 'session_selection must be new, continue_latest, or continue_or_create' };
|
|
111
|
+
}
|
|
90
112
|
export function parseSendContextualMessageArgs(value) {
|
|
91
113
|
if (!isRecord(value))
|
|
92
114
|
return { ok: false, error: 'Arguments must be an object' };
|
|
@@ -111,6 +133,9 @@ export function parseSendContextualMessageArgs(value) {
|
|
|
111
133
|
if (type !== 'cross_session') {
|
|
112
134
|
return { ok: false, error: 'self_context.type must be cross_session' };
|
|
113
135
|
}
|
|
136
|
+
const sessionSelection = parseSessionSelection(value.session_selection);
|
|
137
|
+
if (!sessionSelection.ok)
|
|
138
|
+
return sessionSelection;
|
|
114
139
|
const messageOptions = isRecord(value.message_options)
|
|
115
140
|
? value.message_options
|
|
116
141
|
: undefined;
|
|
@@ -125,6 +150,7 @@ export function parseSendContextualMessageArgs(value) {
|
|
|
125
150
|
type: 'cross_session',
|
|
126
151
|
context: context.value,
|
|
127
152
|
},
|
|
153
|
+
...(sessionSelection.value ? { sessionSelection: sessionSelection.value } : {}),
|
|
128
154
|
...(messageOptions ? { messageOptions } : {}),
|
|
129
155
|
},
|
|
130
156
|
};
|
package/dist/server.js
CHANGED
|
@@ -163,6 +163,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
163
163
|
type: 'object',
|
|
164
164
|
description: 'Required { type: "cross_session", context: "why I am saying this here" }',
|
|
165
165
|
},
|
|
166
|
+
session_selection: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
enum: ['new', 'continue_latest', 'continue_or_create'],
|
|
169
|
+
description: 'Optional direct-agent session behavior when target_user_id is used',
|
|
170
|
+
},
|
|
166
171
|
message_options: {
|
|
167
172
|
type: 'object',
|
|
168
173
|
description: 'Optional Canon send options for the contextual outbound message',
|
|
@@ -321,7 +326,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
321
326
|
const parsed = parseSendContextualMessageArgs(args);
|
|
322
327
|
if (!parsed.ok)
|
|
323
328
|
return toolArgumentError(parsed.error);
|
|
324
|
-
const { sourceConversationId, targetConversationId, targetUserId, text, selfContext, messageOptions, } = parsed.value;
|
|
329
|
+
const { sourceConversationId, targetConversationId, targetUserId, text, selfContext, sessionSelection, messageOptions, } = parsed.value;
|
|
325
330
|
const contextualMessageOptions = messageOptions;
|
|
326
331
|
const result = await client.sendContextualMessage({
|
|
327
332
|
sourceConversationId,
|
|
@@ -329,6 +334,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
329
334
|
...(targetUserId ? { targetUserId } : {}),
|
|
330
335
|
text,
|
|
331
336
|
selfContext,
|
|
337
|
+
...(sessionSelection ? { sessionSelection } : {}),
|
|
332
338
|
...(contextualMessageOptions
|
|
333
339
|
? {
|
|
334
340
|
messageOptions: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
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.141",
|
|
34
|
-
"@canonmsg/agent-sdk": "^3.
|
|
35
|
-
"@canonmsg/core": "^2.
|
|
34
|
+
"@canonmsg/agent-sdk": "^3.1.1",
|
|
35
|
+
"@canonmsg/core": "^2.1.1",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|