@canonmsg/claude-code-plugin 0.15.0 → 0.15.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/dist/host.js +152 -31
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.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.141",
|
|
34
|
-
"@canonmsg/agent-sdk": "^3.
|
|
35
|
-
"@canonmsg/core": "^2.
|
|
34
|
+
"@canonmsg/agent-sdk": "^3.1.0",
|
|
35
|
+
"@canonmsg/core": "^2.1.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|