@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.
@@ -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 declare function mergeClaudeModelOptions(input: {
64
- previous: ReadonlyArray<ModelOption>;
65
- next: ReadonlyArray<ModelOption>;
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;
@@ -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
- export function mergeClaudeModelOptions(input) {
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
- for (const option of input.previous) {
52
- byValue.set(option.value, { ...option });
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
- for (const option of input.next) {
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 the latest Claude model probe.',
121
+ description: 'Selected for this Canon session but not returned by Claude model discovery.',
62
122
  });
63
123
  }
64
- const nextValues = new Set(input.next.map((option) => option.value));
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
- * This module is intentionally identical in packages/claude-code-plugin and
10
- * packages/codex-plugin — keep both copies in sync (future consolidation
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>>;
@@ -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
- * This module is intentionally identical in packages/claude-code-plugin and
10
- * packages/codex-plugin — keep both copies in sync (future consolidation
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 hasCursor = (messages) => input.cursor != null && messages.some((message) => message.id === input.cursor);
21
- let cursorFound = hasCursor(collected);
22
- if (input.cursor != null) {
23
- while (!cursorFound && collected.length < maxMessages) {
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
- cursorFound = hasCursor(fresh);
47
+ recordFoundCursors(fresh);
38
48
  }
39
49
  }
40
- const ascending = [...collected].sort((a, b) => String(a.createdAt ?? '').localeCompare(String(b.createdAt ?? '')));
41
- const inboundOnly = (messages) => messages.filter((message) => message.senderId !== input.agentId);
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 (cursorFound) {
45
- const cursorIndex = ascending.findIndex((message) => message.id === input.cursor);
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 (input.cursor != null && collected.length >= maxMessages) {
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 { mode, messages: missed, newestPage };
84
+ return {
85
+ mode,
86
+ cursor: resolvedCursor,
87
+ messages: missed,
88
+ newestPage,
89
+ };
59
90
  }
@@ -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 an owner approval; best-effort —
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>;
@@ -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 agent owner denied this action.';
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
- ignoreSessionRules: !input.isOwnerTurn,
202
- allowSessionRule: input.isOwnerTurn,
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.0",
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": "^0.3.200",
34
- "@canonmsg/agent-sdk": "^5.1.0",
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.0",
36
+ "@canonmsg/core": "^4.2.2",
37
37
  "@modelcontextprotocol/sdk": "^1.29.0"
38
38
  },
39
39
  "engines": {