@canonmsg/claude-code-plugin 0.25.0 → 0.25.3
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/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -0
- package/dist/host.js +426 -243
- package/dist/session-state.d.ts +21 -3
- package/dist/session-state.js +77 -16
- package/dist/startup-recovery.d.ts +6 -3
- package/dist/startup-recovery.js +45 -14
- package/dist/tool-policy.d.ts +3 -1
- package/dist/tool-policy.js +11 -3
- package/dist/user-dialog.d.ts +19 -0
- package/dist/user-dialog.js +151 -0
- package/package.json +4 -4
package/dist/session-state.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface ClaudeInputEnvelope {
|
|
|
13
13
|
artifactBaseline: TurnArtifactSnapshot | null;
|
|
14
14
|
turnKey: string;
|
|
15
15
|
isOwnerTurn: boolean;
|
|
16
|
+
requestingUserId: string | null;
|
|
16
17
|
}
|
|
17
18
|
export interface ClaudeRuntimeControlError {
|
|
18
19
|
value: string;
|
|
@@ -48,7 +49,13 @@ export declare function createClaudeInputEnvelope(input: {
|
|
|
48
49
|
artifactRoutingMode?: ClaudeArtifactRoutingMode;
|
|
49
50
|
artifactBaseline?: TurnArtifactSnapshot | null;
|
|
50
51
|
isOwnerTurn?: boolean;
|
|
52
|
+
requestingUserId?: string | null;
|
|
51
53
|
}): ClaudeInputEnvelope;
|
|
54
|
+
export declare function resolveClaudeTurnResponseRouting(turn: Pick<ClaudeInputEnvelope, 'kind' | 'isOwnerTurn' | 'requestingUserId'> | null | undefined, ownerId: string | null | undefined, ownerOnly?: boolean): {
|
|
55
|
+
isOwnerTurn: boolean;
|
|
56
|
+
responseUserId?: string;
|
|
57
|
+
allowSessionRule: boolean;
|
|
58
|
+
};
|
|
52
59
|
export declare function buildClaudeFinalMessageId(input: {
|
|
53
60
|
agentId: string;
|
|
54
61
|
conversationId: string;
|
|
@@ -60,12 +67,23 @@ export declare function shouldDeliverClaudeFinal(input: {
|
|
|
60
67
|
interruptedTurnKeys: ReadonlySet<string>;
|
|
61
68
|
}): boolean;
|
|
62
69
|
export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
export interface ClaudeModelInfoLike {
|
|
71
|
+
value: string;
|
|
72
|
+
displayName: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
}
|
|
75
|
+
export declare function claudeModelInfoToOption(model: ClaudeModelInfoLike): ModelOption;
|
|
76
|
+
export declare function isClaudeCustomModelOption(option: ModelOption | null | undefined): boolean;
|
|
77
|
+
export declare function mergeClaudeDiscoveredModelOptions(...groups: ReadonlyArray<ReadonlyArray<ModelOption>>): ModelOption[];
|
|
78
|
+
export declare function resolveClaudeModelOptions(input: {
|
|
79
|
+
discovered: ReadonlyArray<ModelOption>;
|
|
66
80
|
selectedModel?: string | null;
|
|
67
81
|
}): ModelOption[];
|
|
68
82
|
export declare function formatClaudeControlError(error: unknown): string;
|
|
83
|
+
export declare function confirmClaudeInterrupt(input: {
|
|
84
|
+
active: boolean;
|
|
85
|
+
interrupt: () => Promise<void>;
|
|
86
|
+
}): Promise<'confirmed' | 'defer'>;
|
|
69
87
|
/** Requested session-control values from the control plane (raw, pre-validation). */
|
|
70
88
|
export interface ClaudeSessionControlInput {
|
|
71
89
|
model?: string;
|
package/dist/session-state.js
CHANGED
|
@@ -13,11 +13,25 @@ export function createClaudeInputEnvelope(input) {
|
|
|
13
13
|
isOwnerTurn: input.kind === 'canon'
|
|
14
14
|
? input.isOwnerTurn === true
|
|
15
15
|
: input.isOwnerTurn !== false,
|
|
16
|
+
requestingUserId: input.kind === 'canon' && input.requestingUserId
|
|
17
|
+
? input.requestingUserId
|
|
18
|
+
: null,
|
|
16
19
|
turnKey: input.kind === 'canon' && sourceMessageId
|
|
17
20
|
? `canon:${sourceMessageId}`
|
|
18
21
|
: `${input.kind}:${fallbackId}`,
|
|
19
22
|
};
|
|
20
23
|
}
|
|
24
|
+
export function resolveClaudeTurnResponseRouting(turn, ownerId, ownerOnly = false) {
|
|
25
|
+
const isOwnerTurn = turn?.kind === 'canon' ? turn.isOwnerTurn : true;
|
|
26
|
+
const responseUserId = ownerOnly
|
|
27
|
+
? ownerId || undefined
|
|
28
|
+
: (turn?.requestingUserId ?? ownerId) || undefined;
|
|
29
|
+
return {
|
|
30
|
+
isOwnerTurn,
|
|
31
|
+
...(responseUserId ? { responseUserId } : {}),
|
|
32
|
+
allowSessionRule: Boolean(responseUserId && ownerId && responseUserId === ownerId),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
21
35
|
export function buildClaudeFinalMessageId(input) {
|
|
22
36
|
const digest = createHash('sha256')
|
|
23
37
|
.update(`${input.agentId}\n${input.conversationId}\n${input.turnKey}`)
|
|
@@ -46,32 +60,68 @@ export function resetClaudeCompletedTurnState(session) {
|
|
|
46
60
|
session.toolInProgress = false;
|
|
47
61
|
session.turnState = 'idle';
|
|
48
62
|
}
|
|
49
|
-
|
|
63
|
+
function normalizeModelDescription(value) {
|
|
64
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
65
|
+
}
|
|
66
|
+
function displayLabelWithClaudeVersion(input) {
|
|
67
|
+
const label = input.displayName.trim() || input.value;
|
|
68
|
+
const description = normalizeModelDescription(input.description);
|
|
69
|
+
if (!description || label.includes('(') || /\d/.test(label))
|
|
70
|
+
return label;
|
|
71
|
+
const [summary] = description.split('·');
|
|
72
|
+
const versionedLabel = summary?.trim();
|
|
73
|
+
return versionedLabel
|
|
74
|
+
&& versionedLabel.toLowerCase().startsWith(label.toLowerCase())
|
|
75
|
+
&& /\d/.test(versionedLabel)
|
|
76
|
+
? versionedLabel
|
|
77
|
+
: label;
|
|
78
|
+
}
|
|
79
|
+
export function claudeModelInfoToOption(model) {
|
|
80
|
+
const description = normalizeModelDescription(model.description);
|
|
81
|
+
return {
|
|
82
|
+
value: model.value,
|
|
83
|
+
label: displayLabelWithClaudeVersion(model),
|
|
84
|
+
...(description ? { description } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function isClaudeCustomModelOption(option) {
|
|
88
|
+
return option?.description?.trim().toLowerCase() === 'custom model';
|
|
89
|
+
}
|
|
90
|
+
export function mergeClaudeDiscoveredModelOptions(...groups) {
|
|
50
91
|
const byValue = new Map();
|
|
51
|
-
|
|
52
|
-
|
|
92
|
+
const valueByLabel = new Map();
|
|
93
|
+
const labelByValue = new Map();
|
|
94
|
+
for (const group of groups) {
|
|
95
|
+
for (const option of group) {
|
|
96
|
+
const previousLabel = labelByValue.get(option.value);
|
|
97
|
+
if (previousLabel && valueByLabel.get(previousLabel) === option.value) {
|
|
98
|
+
valueByLabel.delete(previousLabel);
|
|
99
|
+
}
|
|
100
|
+
const duplicateValue = valueByLabel.get(option.label);
|
|
101
|
+
if (duplicateValue && duplicateValue !== option.value && duplicateValue !== 'default') {
|
|
102
|
+
byValue.delete(duplicateValue);
|
|
103
|
+
labelByValue.delete(duplicateValue);
|
|
104
|
+
}
|
|
105
|
+
byValue.set(option.value, option);
|
|
106
|
+
valueByLabel.set(option.label, option.value);
|
|
107
|
+
labelByValue.set(option.value, option.label);
|
|
108
|
+
}
|
|
53
109
|
}
|
|
54
|
-
|
|
110
|
+
return [...byValue.values()];
|
|
111
|
+
}
|
|
112
|
+
export function resolveClaudeModelOptions(input) {
|
|
113
|
+
const byValue = new Map();
|
|
114
|
+
for (const option of input.discovered) {
|
|
55
115
|
byValue.set(option.value, { ...option });
|
|
56
116
|
}
|
|
57
117
|
if (input.selectedModel && !byValue.has(input.selectedModel)) {
|
|
58
118
|
byValue.set(input.selectedModel, {
|
|
59
119
|
value: input.selectedModel,
|
|
60
120
|
label: input.selectedModel,
|
|
61
|
-
description: 'Selected for this Canon session but not returned by
|
|
121
|
+
description: 'Selected for this Canon session but not returned by Claude model discovery.',
|
|
62
122
|
});
|
|
63
123
|
}
|
|
64
|
-
|
|
65
|
-
return [...byValue.values()].map((option) => {
|
|
66
|
-
if (nextValues.has(option.value))
|
|
67
|
-
return option;
|
|
68
|
-
return {
|
|
69
|
-
...option,
|
|
70
|
-
description: option.description
|
|
71
|
-
? `${option.description} Previously advertised; not returned by the latest Claude model probe.`
|
|
72
|
-
: 'Previously advertised; not returned by the latest Claude model probe.',
|
|
73
|
-
};
|
|
74
|
-
});
|
|
124
|
+
return [...byValue.values()];
|
|
75
125
|
}
|
|
76
126
|
export function formatClaudeControlError(error) {
|
|
77
127
|
if (error instanceof Error && error.message.trim()) {
|
|
@@ -82,6 +132,17 @@ export function formatClaudeControlError(error) {
|
|
|
82
132
|
}
|
|
83
133
|
return 'Claude Code did not apply that control.';
|
|
84
134
|
}
|
|
135
|
+
export async function confirmClaudeInterrupt(input) {
|
|
136
|
+
if (!input.active)
|
|
137
|
+
return 'confirmed';
|
|
138
|
+
try {
|
|
139
|
+
await input.interrupt();
|
|
140
|
+
return 'confirmed';
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return 'defer';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
85
146
|
/**
|
|
86
147
|
* Apply a session-control request to the live Claude session. Mirrors the host's
|
|
87
148
|
* control semantics (validation, only-if-different checks, error recording,
|
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
* its `before` message-id parameter) until the cursor is found or a hard
|
|
7
7
|
* per-conversation bound is hit, then replay everything after the cursor.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* candidate).
|
|
9
|
+
* Claude also accepts legacy cursor candidates so it can migrate old
|
|
10
|
+
* execution-mode-scoped state into one conversation-level recovery cursor.
|
|
12
11
|
*/
|
|
13
12
|
export declare const STARTUP_RECOVERY_PAGE_SIZE = 25;
|
|
14
13
|
export declare const STARTUP_RECOVERY_MAX_MESSAGES = 500;
|
|
@@ -16,6 +15,7 @@ export interface StartupRecoveryMessage {
|
|
|
16
15
|
id: string;
|
|
17
16
|
senderId: string;
|
|
18
17
|
createdAt?: string;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
19
|
}
|
|
20
20
|
export interface StartupRecoveryPage {
|
|
21
21
|
messages: StartupRecoveryMessage[];
|
|
@@ -33,6 +33,8 @@ export type StartupRecoveryMode =
|
|
|
33
33
|
| 'latest-only';
|
|
34
34
|
export interface StartupRecoveryResult<TPage extends StartupRecoveryPage> {
|
|
35
35
|
mode: StartupRecoveryMode;
|
|
36
|
+
/** Cursor found in history and used for this recovery pass. */
|
|
37
|
+
cursor: string | null;
|
|
36
38
|
/** Missed inbound messages (own messages excluded), oldest first. */
|
|
37
39
|
messages: TPage['messages'];
|
|
38
40
|
/** First page fetched — reusable as hydration context for recovered turns. */
|
|
@@ -41,6 +43,7 @@ export interface StartupRecoveryResult<TPage extends StartupRecoveryPage> {
|
|
|
41
43
|
export declare function collectMissedInboundMessages<TPage extends StartupRecoveryPage>(input: {
|
|
42
44
|
fetchPage: (before?: string) => Promise<TPage>;
|
|
43
45
|
cursor: string | null | undefined;
|
|
46
|
+
cursorCandidates?: readonly string[];
|
|
44
47
|
agentId: string;
|
|
45
48
|
maxMessages?: number;
|
|
46
49
|
}): Promise<StartupRecoveryResult<TPage>>;
|
package/dist/startup-recovery.js
CHANGED
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
* its `before` message-id parameter) until the cursor is found or a hard
|
|
7
7
|
* per-conversation bound is hit, then replay everything after the cursor.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* candidate).
|
|
9
|
+
* Claude also accepts legacy cursor candidates so it can migrate old
|
|
10
|
+
* execution-mode-scoped state into one conversation-level recovery cursor.
|
|
12
11
|
*/
|
|
13
12
|
export const STARTUP_RECOVERY_PAGE_SIZE = 25;
|
|
14
13
|
export const STARTUP_RECOVERY_MAX_MESSAGES = 500;
|
|
@@ -17,10 +16,21 @@ export async function collectMissedInboundMessages(input) {
|
|
|
17
16
|
const newestPage = await input.fetchPage();
|
|
18
17
|
const collected = [...newestPage.messages];
|
|
19
18
|
const seenIds = new Set(collected.map((message) => message.id));
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const candidateCursorIds = new Set(input.cursor
|
|
20
|
+
? [input.cursor]
|
|
21
|
+
: (input.cursorCandidates ?? []).filter(Boolean));
|
|
22
|
+
const foundCursorIds = new Set();
|
|
23
|
+
const recordFoundCursors = (messages) => {
|
|
24
|
+
for (const message of messages) {
|
|
25
|
+
if (candidateCursorIds.has(message.id))
|
|
26
|
+
foundCursorIds.add(message.id);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const cursorSearchComplete = () => foundCursorIds.size > 0;
|
|
30
|
+
const findNewestCursor = (messages) => messages.find((message) => candidateCursorIds.has(message.id))?.id ?? null;
|
|
31
|
+
recordFoundCursors(collected);
|
|
32
|
+
if (candidateCursorIds.size > 0) {
|
|
33
|
+
while (!cursorSearchComplete() && collected.length < maxMessages) {
|
|
24
34
|
// Pages are newest-first, so the last collected message is the oldest.
|
|
25
35
|
const before = collected[collected.length - 1]?.id;
|
|
26
36
|
if (!before)
|
|
@@ -34,19 +44,35 @@ export async function collectMissedInboundMessages(input) {
|
|
|
34
44
|
for (const message of fresh)
|
|
35
45
|
seenIds.add(message.id);
|
|
36
46
|
collected.push(...fresh);
|
|
37
|
-
|
|
47
|
+
recordFoundCursors(fresh);
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
50
|
+
const resolvedCursor = cursorSearchComplete()
|
|
51
|
+
? findNewestCursor(collected)
|
|
52
|
+
: null;
|
|
53
|
+
// Preserve the API's deterministic newest-first order. Timestamps are
|
|
54
|
+
// display data and may be equal or absent, so reversing wire order is the
|
|
55
|
+
// only lossless oldest-first replay sequence.
|
|
56
|
+
const ascending = [...collected].reverse();
|
|
57
|
+
const completedSourceMessageIds = new Set(collected.flatMap((message) => {
|
|
58
|
+
if (message.senderId !== input.agentId)
|
|
59
|
+
return [];
|
|
60
|
+
const metadata = message.metadata;
|
|
61
|
+
if (metadata?.turnSemantics !== 'turn_complete')
|
|
62
|
+
return [];
|
|
63
|
+
return typeof metadata.sourceMessageId === 'string'
|
|
64
|
+
? [metadata.sourceMessageId]
|
|
65
|
+
: [];
|
|
66
|
+
}));
|
|
67
|
+
const inboundOnly = (messages) => messages.filter((message) => message.senderId !== input.agentId && !completedSourceMessageIds.has(message.id));
|
|
42
68
|
let mode;
|
|
43
69
|
let missed;
|
|
44
|
-
if (
|
|
45
|
-
const cursorIndex = ascending.findIndex((message) => message.id ===
|
|
70
|
+
if (resolvedCursor) {
|
|
71
|
+
const cursorIndex = ascending.findIndex((message) => message.id === resolvedCursor);
|
|
46
72
|
mode = 'after-cursor';
|
|
47
73
|
missed = inboundOnly(ascending.slice(cursorIndex + 1));
|
|
48
74
|
}
|
|
49
|
-
else if (
|
|
75
|
+
else if (candidateCursorIds.size > 0 && collected.length >= maxMessages) {
|
|
50
76
|
mode = 'truncated-window';
|
|
51
77
|
missed = inboundOnly(ascending.slice(-maxMessages));
|
|
52
78
|
}
|
|
@@ -55,5 +81,10 @@ export async function collectMissedInboundMessages(input) {
|
|
|
55
81
|
missed = inboundOnly(ascending).slice(-1);
|
|
56
82
|
}
|
|
57
83
|
// Safe: `missed` only holds elements of pages returned by `fetchPage`.
|
|
58
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
mode,
|
|
86
|
+
cursor: resolvedCursor,
|
|
87
|
+
messages: missed,
|
|
88
|
+
newestPage,
|
|
89
|
+
};
|
|
59
90
|
}
|
package/dist/tool-policy.d.ts
CHANGED
|
@@ -24,12 +24,14 @@ export declare function decideClaudeToolPermissionForMode(input: {
|
|
|
24
24
|
runtimeId?: string;
|
|
25
25
|
turnId?: string | null;
|
|
26
26
|
isOwnerTurn: boolean;
|
|
27
|
+
responseUserId?: string | null;
|
|
28
|
+
allowSessionRule?: boolean;
|
|
27
29
|
approvalManager?: ApprovalManager | null;
|
|
28
30
|
canRequestApproval?: () => Promise<boolean>;
|
|
29
31
|
allowedNonOwnerTools?: Iterable<string>;
|
|
30
32
|
/**
|
|
31
33
|
* Lazy file-change preview for the approval card (Edit/Write-class tools).
|
|
32
|
-
* Invoked only once the call is headed for
|
|
34
|
+
* Invoked only once the call is headed for a responder approval; best-effort —
|
|
33
35
|
* a throw or undefined simply omits the diff, never blocks the approval.
|
|
34
36
|
*/
|
|
35
37
|
buildApprovalDiff?: () => Promise<CanonUnifiedDiff | undefined>;
|
package/dist/tool-policy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const NON_OWNER_DENY_MESSAGE = 'Only the agent owner can authorize runtime tool use in this chat.';
|
|
2
2
|
const NATIVE_APPROVAL_UNAVAILABLE_MESSAGE = 'Runtime action approval is not available for this conversation.';
|
|
3
|
-
const NATIVE_APPROVAL_DENIED_MESSAGE = 'The
|
|
3
|
+
const NATIVE_APPROVAL_DENIED_MESSAGE = 'The user denied this action.';
|
|
4
4
|
export const CLAUDE_NATIVE_APPROVAL_PERMISSION_MODE = 'default';
|
|
5
5
|
const SENSITIVE_CLAUDE_TOOLS = new Set([
|
|
6
6
|
'agent',
|
|
@@ -174,6 +174,12 @@ export async function decideClaudeToolPermissionForMode(input) {
|
|
|
174
174
|
message: NATIVE_APPROVAL_UNAVAILABLE_MESSAGE,
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
|
+
if (!input.isOwnerTurn && !input.responseUserId) {
|
|
178
|
+
return {
|
|
179
|
+
behavior: 'deny',
|
|
180
|
+
message: NATIVE_APPROVAL_UNAVAILABLE_MESSAGE,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
177
183
|
let diff;
|
|
178
184
|
if (input.buildApprovalDiff) {
|
|
179
185
|
try {
|
|
@@ -186,6 +192,7 @@ export async function decideClaudeToolPermissionForMode(input) {
|
|
|
186
192
|
let result;
|
|
187
193
|
try {
|
|
188
194
|
const risk = classifyClaudeApprovalRisk(input.toolName, input.toolInput);
|
|
195
|
+
const allowSessionRule = input.allowSessionRule ?? input.isOwnerTurn;
|
|
189
196
|
result = await input.approvalManager.requestApproval(input.conversationId, input.toolName, input.toolInput, {
|
|
190
197
|
category: classifyClaudeApprovalCategory(input.toolName),
|
|
191
198
|
risk,
|
|
@@ -198,8 +205,9 @@ export async function decideClaudeToolPermissionForMode(input) {
|
|
|
198
205
|
},
|
|
199
206
|
details: buildClaudeApprovalDetails(input.toolName, input.toolInput),
|
|
200
207
|
...(diff ? { diff } : {}),
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
...(input.responseUserId ? { responseUserId: input.responseUserId } : {}),
|
|
209
|
+
ignoreSessionRules: !allowSessionRule,
|
|
210
|
+
allowSessionRule,
|
|
203
211
|
});
|
|
204
212
|
}
|
|
205
213
|
catch {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PermissionResult, UserDialogRequest } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { type RuntimeInputAnswers, type RuntimeInputQuestion } from '@canonmsg/core';
|
|
3
|
+
export declare const CLAUDE_ASK_USER_DIALOG_KIND = "permission_ask_user_question";
|
|
4
|
+
export declare const CLAUDE_SUPPORTED_DIALOG_KINDS: string[];
|
|
5
|
+
export interface ClaudeAskUserDialog {
|
|
6
|
+
questions: RuntimeInputQuestion[];
|
|
7
|
+
rawQuestions: Array<Record<string, unknown>>;
|
|
8
|
+
updatedInput: Record<string, unknown>;
|
|
9
|
+
requestId?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveClaudeUserDialogRequestId(request: Pick<UserDialogRequest, 'toolUseID'>, dialog: Pick<ClaudeAskUserDialog, 'requestId'>): string | undefined;
|
|
12
|
+
export declare function createClaudeUserDialogCoordinator<T>(maxCompleted?: number): (requestId: string, run: () => Promise<T>) => Promise<T>;
|
|
13
|
+
export declare function parseClaudeAskUserDialog(request: UserDialogRequest): ClaudeAskUserDialog | null;
|
|
14
|
+
export declare function parseClaudeAskUserToolInput(input: Record<string, unknown>, requestId: string): ClaudeAskUserDialog | null;
|
|
15
|
+
export declare function buildClaudeAskUserPermissionResult(dialog: ClaudeAskUserDialog, response: {
|
|
16
|
+
value?: string;
|
|
17
|
+
answers?: RuntimeInputAnswers;
|
|
18
|
+
}): PermissionResult;
|
|
19
|
+
export declare function buildClaudeAskUserPermissionDenied(message: string): PermissionResult;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { resolveQuestionAllowOther, } from '@canonmsg/core';
|
|
2
|
+
export const CLAUDE_ASK_USER_DIALOG_KIND = 'permission_ask_user_question';
|
|
3
|
+
export const CLAUDE_SUPPORTED_DIALOG_KINDS = [CLAUDE_ASK_USER_DIALOG_KIND];
|
|
4
|
+
export function resolveClaudeUserDialogRequestId(request, dialog) {
|
|
5
|
+
const toolUseId = typeof request.toolUseID === 'string' ? request.toolUseID.trim() : '';
|
|
6
|
+
if (toolUseId)
|
|
7
|
+
return toolUseId.slice(0, 160);
|
|
8
|
+
const payloadRequestId = dialog.requestId?.trim();
|
|
9
|
+
return payloadRequestId ? payloadRequestId.slice(0, 160) : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function createClaudeUserDialogCoordinator(maxCompleted = 64) {
|
|
12
|
+
const active = new Map();
|
|
13
|
+
const completed = new Map();
|
|
14
|
+
return async (requestId, run) => {
|
|
15
|
+
if (completed.has(requestId))
|
|
16
|
+
return completed.get(requestId);
|
|
17
|
+
const activeResult = active.get(requestId);
|
|
18
|
+
if (activeResult)
|
|
19
|
+
return activeResult;
|
|
20
|
+
const pending = Promise.resolve().then(run);
|
|
21
|
+
active.set(requestId, pending);
|
|
22
|
+
try {
|
|
23
|
+
const result = await pending;
|
|
24
|
+
completed.set(requestId, result);
|
|
25
|
+
while (completed.size > maxCompleted) {
|
|
26
|
+
const oldest = completed.keys().next().value;
|
|
27
|
+
if (oldest === undefined)
|
|
28
|
+
break;
|
|
29
|
+
completed.delete(oldest);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
active.delete(requestId);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function isRecord(value) {
|
|
39
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
export function parseClaudeAskUserDialog(request) {
|
|
42
|
+
if (request.dialogKind !== CLAUDE_ASK_USER_DIALOG_KIND)
|
|
43
|
+
return null;
|
|
44
|
+
const rawQuestions = Array.isArray(request.payload.questions)
|
|
45
|
+
? request.payload.questions.filter(isRecord).slice(0, 12)
|
|
46
|
+
: [];
|
|
47
|
+
if (rawQuestions.length === 0)
|
|
48
|
+
return null;
|
|
49
|
+
const questions = rawQuestions.flatMap((record, index) => {
|
|
50
|
+
const question = typeof record.question === 'string' && record.question.trim()
|
|
51
|
+
? record.question.trim().slice(0, 1000)
|
|
52
|
+
: null;
|
|
53
|
+
if (!question)
|
|
54
|
+
return [];
|
|
55
|
+
const slug = question.slice(0, 72).replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
56
|
+
const id = `q-${index + 1}${slug ? `-${slug}` : ''}`.slice(0, 120);
|
|
57
|
+
const header = typeof record.header === 'string' && record.header.trim()
|
|
58
|
+
? record.header.trim().slice(0, 120)
|
|
59
|
+
: undefined;
|
|
60
|
+
const choices = (Array.isArray(record.options) ? record.options : [])
|
|
61
|
+
.filter(isRecord)
|
|
62
|
+
.slice(0, 12)
|
|
63
|
+
.flatMap((option) => {
|
|
64
|
+
const label = typeof option.label === 'string' && option.label.trim()
|
|
65
|
+
? option.label.trim().slice(0, 120)
|
|
66
|
+
: typeof option.value === 'string' && option.value.trim()
|
|
67
|
+
? option.value.trim().slice(0, 120)
|
|
68
|
+
: null;
|
|
69
|
+
if (!label)
|
|
70
|
+
return [];
|
|
71
|
+
const value = typeof option.value === 'string' && option.value.trim()
|
|
72
|
+
? option.value.trim().slice(0, 120)
|
|
73
|
+
: label;
|
|
74
|
+
const description = typeof option.description === 'string' && option.description.trim()
|
|
75
|
+
? option.description.trim().slice(0, 240)
|
|
76
|
+
: undefined;
|
|
77
|
+
const preview = typeof option.preview === 'string' && option.preview.trim()
|
|
78
|
+
? option.preview.trim().slice(0, 4000)
|
|
79
|
+
: undefined;
|
|
80
|
+
return [{
|
|
81
|
+
label,
|
|
82
|
+
value,
|
|
83
|
+
...(description ? { description } : {}),
|
|
84
|
+
...(preview ? { preview } : {}),
|
|
85
|
+
}];
|
|
86
|
+
});
|
|
87
|
+
return [{
|
|
88
|
+
id,
|
|
89
|
+
question,
|
|
90
|
+
...(header ? { header } : {}),
|
|
91
|
+
...(choices.length > 0 ? { choices } : {}),
|
|
92
|
+
...(resolveQuestionAllowOther(choices.length > 0, record.allowOther ?? record.isOther)
|
|
93
|
+
? { allowOther: true }
|
|
94
|
+
: {}),
|
|
95
|
+
...(record.isSecret === true ? { isSecret: true } : {}),
|
|
96
|
+
...(record.multiSelect === true ? { multiSelect: true } : {}),
|
|
97
|
+
}];
|
|
98
|
+
});
|
|
99
|
+
if (questions.length === 0)
|
|
100
|
+
return null;
|
|
101
|
+
const permissionResult = isRecord(request.payload.permissionResult)
|
|
102
|
+
? request.payload.permissionResult
|
|
103
|
+
: null;
|
|
104
|
+
const updatedInput = isRecord(request.payload.input)
|
|
105
|
+
? request.payload.input
|
|
106
|
+
: permissionResult && isRecord(permissionResult.updatedInput)
|
|
107
|
+
? permissionResult.updatedInput
|
|
108
|
+
: { questions: rawQuestions };
|
|
109
|
+
const metadataSource = typeof request.payload.metadataSource === 'string'
|
|
110
|
+
? request.payload.metadataSource.trim()
|
|
111
|
+
: '';
|
|
112
|
+
const inputWithMetadata = metadataSource && !isRecord(updatedInput.metadata)
|
|
113
|
+
? { ...updatedInput, metadata: { source: metadataSource } }
|
|
114
|
+
: updatedInput;
|
|
115
|
+
const requestId = typeof request.payload.requestId === 'string' && request.payload.requestId.trim()
|
|
116
|
+
? request.payload.requestId.trim().slice(0, 160)
|
|
117
|
+
: undefined;
|
|
118
|
+
return { questions, rawQuestions, updatedInput: inputWithMetadata, ...(requestId ? { requestId } : {}) };
|
|
119
|
+
}
|
|
120
|
+
export function parseClaudeAskUserToolInput(input, requestId) {
|
|
121
|
+
return parseClaudeAskUserDialog({
|
|
122
|
+
dialogKind: CLAUDE_ASK_USER_DIALOG_KIND,
|
|
123
|
+
payload: { requestId, questions: input.questions, input },
|
|
124
|
+
toolUseID: requestId,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
export function buildClaudeAskUserPermissionResult(dialog, response) {
|
|
128
|
+
const answers = {};
|
|
129
|
+
dialog.questions.forEach((question, index) => {
|
|
130
|
+
const answer = response.answers?.[question.id]?.answers.join(', ').trim();
|
|
131
|
+
const fallback = dialog.questions.length === 1 ? response.value?.trim() : undefined;
|
|
132
|
+
if (answer || fallback) {
|
|
133
|
+
const rawQuestion = dialog.rawQuestions[index]?.question;
|
|
134
|
+
const key = typeof rawQuestion === 'string' && rawQuestion.trim()
|
|
135
|
+
? rawQuestion
|
|
136
|
+
: question.question;
|
|
137
|
+
answers[key] = answer || fallback || '';
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
behavior: 'allow',
|
|
142
|
+
updatedInput: {
|
|
143
|
+
...dialog.updatedInput,
|
|
144
|
+
questions: dialog.rawQuestions,
|
|
145
|
+
answers,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function buildClaudeAskUserPermissionDenied(message) {
|
|
150
|
+
return { behavior: 'deny', message };
|
|
151
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.3",
|
|
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",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"test": "vitest run"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@anthropic-ai/claude-agent-sdk": "
|
|
34
|
-
"@canonmsg/agent-sdk": "^5.1.
|
|
33
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.204",
|
|
34
|
+
"@canonmsg/agent-sdk": "^5.1.1",
|
|
35
35
|
"@canonmsg/coding-agent-host": "^0.2.2",
|
|
36
|
-
"@canonmsg/core": "^4.2.
|
|
36
|
+
"@canonmsg/core": "^4.2.2",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|