@canonmsg/claude-code-plugin 0.9.6 → 0.12.0
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 +19 -2
- package/dist/host-runtime.d.ts +8 -2
- package/dist/host-runtime.js +14 -1
- package/dist/host.js +550 -17
- package/dist/server.js +21 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -20,8 +20,8 @@ Send a message to your agent in the Canon app. Claude Code responds with full ca
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
22
22
|
- **Two-way messaging** — Messages from Canon flow to Claude Code and back
|
|
23
|
-
- **Session controls** —
|
|
24
|
-
- **Live
|
|
23
|
+
- **Session controls** — Canon renders setup and live controls from the runtime descriptor Claude publishes
|
|
24
|
+
- **Live preview** — See Claude's current live preview/status in the app
|
|
25
25
|
- **Interrupt** — Stop Claude mid-response from the app
|
|
26
26
|
- **Context meter** — See context window usage in the app
|
|
27
27
|
- **Max plan auth** — Uses your Claude subscription, no API key billing
|
|
@@ -32,6 +32,23 @@ Send a message to your agent in the Canon app. Claude Code responds with full ca
|
|
|
32
32
|
canon-claude --cwd /path/to/project
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
Advertise multiple project choices to the Canon app:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
canon-claude --cwd ~/dev --workspace-root ~/dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`--cwd` is the default workspace. Each `--workspace-root` value is an approved local root; the host discovers immediate child projects with common markers such as `.git`, `package.json`, `pyproject.toml`, `Cargo.toml`, or `go.mod` and publishes them as selectable projects during session creation. Use repeated `--workspace /path/to/project` entries to advertise specific projects outside those roots. Worktree mode creates a per-conversation git worktree under `~/.canon/conversation-worktrees`; shared-project mode runs directly in the selected directory.
|
|
42
|
+
|
|
43
|
+
Current Canon truth for Claude host mode:
|
|
44
|
+
|
|
45
|
+
- model is live-editable
|
|
46
|
+
- thinking level / effort is live-editable when the host reports it
|
|
47
|
+
- workspace selection is setup-only
|
|
48
|
+
- execution mode selection is setup-only
|
|
49
|
+
- permission modes are rendered exactly as the Claude runtime reports them; Canon does not flatten them to a smaller hardcoded set
|
|
50
|
+
- if worktree creation is unavailable for the selected project, Canon may fall back to shared-project execution and surface the fallback reason in session details
|
|
51
|
+
|
|
35
52
|
## Multiple agents
|
|
36
53
|
|
|
37
54
|
```bash
|
package/dist/host-runtime.d.ts
CHANGED
|
@@ -98,17 +98,23 @@ export declare function publishHostSessionSnapshots(input: {
|
|
|
98
98
|
liveSessionConfigByConversation?: ReadonlyMap<string, {
|
|
99
99
|
model?: string;
|
|
100
100
|
permissionMode?: string;
|
|
101
|
+
effort?: string;
|
|
102
|
+
runtimeControlValues?: Record<string, string>;
|
|
101
103
|
workspaceId?: string;
|
|
102
104
|
executionMode?: SessionWorkspaceConfig['executionMode'];
|
|
103
105
|
executionBranch?: string | null;
|
|
104
106
|
}>;
|
|
105
107
|
}): Promise<void>;
|
|
106
|
-
export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>>
|
|
108
|
+
export declare function readHostSessionConfig<TExtra extends string = never>(raw: unknown, extraStringFields?: readonly TExtra[]): (SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
|
|
109
|
+
runtimeControlValues?: Record<string, string>;
|
|
110
|
+
}) | null;
|
|
107
111
|
export declare function loadHostSessionConfig<TExtra extends string = never>(input: {
|
|
108
112
|
conversationId: string;
|
|
109
113
|
agentId: string;
|
|
110
114
|
extraStringFields?: readonly TExtra[];
|
|
111
|
-
}): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>>
|
|
115
|
+
}): Promise<(SessionWorkspaceConfig & Partial<Record<TExtra, string>> & {
|
|
116
|
+
runtimeControlValues?: Record<string, string>;
|
|
117
|
+
}) | null>;
|
|
112
118
|
export declare function resolveHostWorkspaceCwd(input: {
|
|
113
119
|
workspaceOptions: HostWorkspaceResolverOption[];
|
|
114
120
|
config: {
|
package/dist/host-runtime.js
CHANGED
|
@@ -145,7 +145,7 @@ export async function publishHostSessionSnapshots(input) {
|
|
|
145
145
|
const persistedConfig = await loadHostSessionConfig({
|
|
146
146
|
conversationId,
|
|
147
147
|
agentId: input.agentId,
|
|
148
|
-
extraStringFields: ['permissionMode'],
|
|
148
|
+
extraStringFields: ['permissionMode', 'effort'],
|
|
149
149
|
});
|
|
150
150
|
const liveConfig = input.liveSessionConfigByConversation?.get(conversationId) ?? null;
|
|
151
151
|
const mergedConfig = {
|
|
@@ -163,6 +163,10 @@ export async function publishHostSessionSnapshots(input) {
|
|
|
163
163
|
sessionConfig: {
|
|
164
164
|
...(mergedConfig.model ? { model: mergedConfig.model } : {}),
|
|
165
165
|
...(mergedConfig.permissionMode ? { permissionMode: mergedConfig.permissionMode } : {}),
|
|
166
|
+
...(mergedConfig.effort ? { effort: mergedConfig.effort } : {}),
|
|
167
|
+
...(mergedConfig.runtimeControlValues
|
|
168
|
+
? { runtimeControlValues: mergedConfig.runtimeControlValues }
|
|
169
|
+
: {}),
|
|
166
170
|
...(mergedConfig.workspaceId ? { workspaceId: mergedConfig.workspaceId } : {}),
|
|
167
171
|
...(mergedConfig.executionMode ? { executionMode: mergedConfig.executionMode } : {}),
|
|
168
172
|
},
|
|
@@ -184,6 +188,8 @@ export async function publishHostSessionSnapshots(input) {
|
|
|
184
188
|
hostMode: true,
|
|
185
189
|
model: snapshot.model ?? null,
|
|
186
190
|
permissionMode: snapshot.permissionMode ?? null,
|
|
191
|
+
effort: snapshot.effort ?? null,
|
|
192
|
+
runtimeControlValues: snapshot.runtimeControlValues ?? null,
|
|
187
193
|
workspaceId: snapshot.workspaceId ?? null,
|
|
188
194
|
executionMode: snapshot.executionMode ?? null,
|
|
189
195
|
executionBranch,
|
|
@@ -205,9 +211,16 @@ export function readHostSessionConfig(raw, extraStringFields = []) {
|
|
|
205
211
|
const value = normalizeOptionalString(data[field]);
|
|
206
212
|
return value ? [[field, value]] : [];
|
|
207
213
|
}));
|
|
214
|
+
const runtimeControlValues = Object.fromEntries(Object.entries(data.runtimeControlValues && typeof data.runtimeControlValues === 'object'
|
|
215
|
+
? data.runtimeControlValues
|
|
216
|
+
: {}).flatMap(([key, value]) => {
|
|
217
|
+
const normalizedValue = normalizeOptionalString(value);
|
|
218
|
+
return normalizedValue ? [[key, normalizedValue]] : [];
|
|
219
|
+
}));
|
|
208
220
|
return {
|
|
209
221
|
...(baseConfig ?? {}),
|
|
210
222
|
...extraConfig,
|
|
223
|
+
...(Object.keys(runtimeControlValues).length > 0 ? { runtimeControlValues } : {}),
|
|
211
224
|
};
|
|
212
225
|
}
|
|
213
226
|
export async function loadHostSessionConfig(input) {
|
package/dist/host.js
CHANGED
|
@@ -24,16 +24,196 @@ import { parseArgs } from 'node:util';
|
|
|
24
24
|
import { randomUUID } from 'node:crypto';
|
|
25
25
|
import { execFileSync } from 'node:child_process';
|
|
26
26
|
import { existsSync } from 'node:fs';
|
|
27
|
-
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
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,
|
|
29
|
+
import { CLAUDE_PERMISSION_MODE_OPTIONS, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, getActiveProfile, releaseLock, initRTDBAuth, rtdbRead, rtdbWrite, writeRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, releaseConversationEnvironment, } from '@canonmsg/core';
|
|
30
30
|
import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
|
|
31
31
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
32
32
|
import { createStreamingBuffer } from './streaming-buffer.js';
|
|
33
33
|
import { runCli } from './cli-entry.js';
|
|
34
34
|
let workingDir = process.cwd();
|
|
35
35
|
let workspaceOptions = [];
|
|
36
|
+
let workspaceRoots = [];
|
|
37
|
+
let workspaceRootMetadata = [];
|
|
36
38
|
let runtimeModels = [];
|
|
39
|
+
const CLAUDE_METADATA_TTL_MS = 5 * 60 * 1000;
|
|
40
|
+
let runtimeMetadata = {
|
|
41
|
+
account: null,
|
|
42
|
+
mcpServers: [],
|
|
43
|
+
commands: [],
|
|
44
|
+
agents: [],
|
|
45
|
+
plugins: [],
|
|
46
|
+
loadedAt: 0,
|
|
47
|
+
};
|
|
48
|
+
let runtimeMetadataRefresh = null;
|
|
49
|
+
function buildClaudeRuntimeDescriptor(input) {
|
|
50
|
+
return {
|
|
51
|
+
coreControls: [
|
|
52
|
+
{
|
|
53
|
+
id: 'model',
|
|
54
|
+
label: 'Model',
|
|
55
|
+
options: input.models,
|
|
56
|
+
defaultValue: input.models[0]?.value ?? null,
|
|
57
|
+
availability: 'setup_and_live',
|
|
58
|
+
liveBehavior: 'immediate',
|
|
59
|
+
selectionPolicy: 'inherit',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'workspace',
|
|
63
|
+
label: 'Project',
|
|
64
|
+
options: input.workspaces.map((workspace) => ({
|
|
65
|
+
value: workspace.id,
|
|
66
|
+
label: workspace.label,
|
|
67
|
+
...(workspace.description ? { description: workspace.description } : {}),
|
|
68
|
+
...(workspace.workspaceRootId ? { workspaceRootId: workspace.workspaceRootId } : {}),
|
|
69
|
+
...(workspace.workspaceRelativePath ? { workspaceRelativePath: workspace.workspaceRelativePath } : {}),
|
|
70
|
+
...(workspace.source ? { source: workspace.source } : {}),
|
|
71
|
+
})),
|
|
72
|
+
defaultValue: input.workspaces[0]?.id ?? null,
|
|
73
|
+
availability: 'setup',
|
|
74
|
+
liveBehavior: 'none',
|
|
75
|
+
selectionPolicy: 'inherit',
|
|
76
|
+
description: input.workspaceRoots?.length
|
|
77
|
+
? 'Choose one of the projects discovered inside the approved local roots for this host.'
|
|
78
|
+
: 'Choose one of the local projects advertised by this host.',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'executionMode',
|
|
82
|
+
label: 'Execution mode',
|
|
83
|
+
options: input.executionModes.map((mode) => ({
|
|
84
|
+
value: mode,
|
|
85
|
+
label: mode === 'worktree' ? 'Isolated worktree' : 'Use shared project',
|
|
86
|
+
description: mode === 'worktree'
|
|
87
|
+
? 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected project is a git repo.'
|
|
88
|
+
: 'Runs directly in the selected project folder. Changes happen there.',
|
|
89
|
+
})),
|
|
90
|
+
defaultValue: null,
|
|
91
|
+
availability: 'setup',
|
|
92
|
+
liveBehavior: 'none',
|
|
93
|
+
selectionPolicy: 'required_explicit',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
runtimeControls: [
|
|
97
|
+
{
|
|
98
|
+
id: 'permissionMode',
|
|
99
|
+
label: 'Permission mode',
|
|
100
|
+
options: [...CLAUDE_PERMISSION_MODE_OPTIONS],
|
|
101
|
+
defaultValue: 'default',
|
|
102
|
+
availability: 'setup_and_live',
|
|
103
|
+
liveBehavior: 'immediate',
|
|
104
|
+
selectionPolicy: 'inherit',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'effort',
|
|
108
|
+
label: 'Thinking level',
|
|
109
|
+
options: [
|
|
110
|
+
{ value: 'low', label: 'Low' },
|
|
111
|
+
{ value: 'medium', label: 'Medium' },
|
|
112
|
+
{ value: 'high', label: 'High' },
|
|
113
|
+
],
|
|
114
|
+
defaultValue: 'medium',
|
|
115
|
+
availability: 'setup_and_live',
|
|
116
|
+
liveBehavior: 'immediate',
|
|
117
|
+
selectionPolicy: 'inherit',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
actions: [
|
|
121
|
+
{
|
|
122
|
+
id: 'plan',
|
|
123
|
+
label: 'Plan first',
|
|
124
|
+
description: 'Switch Claude into Plan mode. Text after /plan is kept as a ready prompt while the mode applies.',
|
|
125
|
+
aliases: ['plan'],
|
|
126
|
+
category: 'plan',
|
|
127
|
+
placements: ['composer_slash', 'command_palette'],
|
|
128
|
+
availability: ['always'],
|
|
129
|
+
trailingTextBehavior: 'send_as_prompt',
|
|
130
|
+
dispatch: {
|
|
131
|
+
kind: 'control',
|
|
132
|
+
controlId: 'permissionMode',
|
|
133
|
+
value: 'plan',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'runtime-status',
|
|
138
|
+
label: 'Runtime status',
|
|
139
|
+
description: 'Open Claude runtime details, including account, project, and policy status.',
|
|
140
|
+
aliases: ['status'],
|
|
141
|
+
category: 'details',
|
|
142
|
+
placements: ['composer_slash', 'command_palette'],
|
|
143
|
+
availability: ['always'],
|
|
144
|
+
dispatch: { kind: 'open_details', target: 'status' },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'mcp-inventory',
|
|
148
|
+
label: 'MCP servers',
|
|
149
|
+
description: 'Open the Claude MCP inventory published by the host.',
|
|
150
|
+
aliases: ['mcp'],
|
|
151
|
+
category: 'details',
|
|
152
|
+
placements: ['composer_slash', 'command_palette'],
|
|
153
|
+
availability: ['always'],
|
|
154
|
+
dispatch: { kind: 'open_details', target: 'mcp' },
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'plugin-inventory',
|
|
158
|
+
label: 'Plugins',
|
|
159
|
+
description: 'Open the Claude plugin and command inventory published by the host.',
|
|
160
|
+
aliases: ['plugins'],
|
|
161
|
+
category: 'details',
|
|
162
|
+
placements: ['composer_slash', 'command_palette'],
|
|
163
|
+
availability: ['always'],
|
|
164
|
+
dispatch: { kind: 'open_details', target: 'plugins' },
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: 'model-details',
|
|
168
|
+
label: 'Model control',
|
|
169
|
+
description: 'Open runtime details and use the live Model control.',
|
|
170
|
+
aliases: ['model'],
|
|
171
|
+
category: 'details',
|
|
172
|
+
placements: ['composer_slash', 'command_palette'],
|
|
173
|
+
availability: ['always'],
|
|
174
|
+
dispatch: { kind: 'open_details', target: 'model' },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'permission-details',
|
|
178
|
+
label: 'Permission mode',
|
|
179
|
+
description: 'Open runtime details and use the live Permission mode control.',
|
|
180
|
+
aliases: ['permission'],
|
|
181
|
+
category: 'details',
|
|
182
|
+
placements: ['composer_slash', 'command_palette'],
|
|
183
|
+
availability: ['always'],
|
|
184
|
+
dispatch: { kind: 'open_details', target: 'permissionMode' },
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: 'effort-details',
|
|
188
|
+
label: 'Thinking level',
|
|
189
|
+
description: 'Open runtime details and use the live Thinking level control.',
|
|
190
|
+
aliases: ['effort'],
|
|
191
|
+
category: 'details',
|
|
192
|
+
placements: ['composer_slash', 'command_palette'],
|
|
193
|
+
availability: ['always'],
|
|
194
|
+
dispatch: { kind: 'open_details', target: 'effort' },
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'stop',
|
|
198
|
+
label: 'Stop',
|
|
199
|
+
description: 'Interrupt the current Claude turn.',
|
|
200
|
+
aliases: ['stop'],
|
|
201
|
+
category: 'turn',
|
|
202
|
+
placements: ['composer_slash', 'command_palette'],
|
|
203
|
+
availability: ['busy'],
|
|
204
|
+
dispatch: { kind: 'signal', signal: 'interrupt' },
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
workspaceRoots: input.workspaceRoots,
|
|
208
|
+
supportsInterrupt: true,
|
|
209
|
+
streamingTextMode: 'delta',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function normalizeClaudeEffortLevel(value) {
|
|
213
|
+
return value === 'low' || value === 'medium' || value === 'high'
|
|
214
|
+
? value
|
|
215
|
+
: undefined;
|
|
216
|
+
}
|
|
37
217
|
let cachedClaudeCliPath;
|
|
38
218
|
// The SDK bundles a Bun-compiled `claude` Mach-O binary next to
|
|
39
219
|
// claude-agent-sdk-darwin-arm64. On some macOS setups that exact binary gets
|
|
@@ -83,18 +263,18 @@ async function loadSessionConfig(conversationId, agentId) {
|
|
|
83
263
|
const config = await loadHostSessionConfig({
|
|
84
264
|
conversationId,
|
|
85
265
|
agentId,
|
|
86
|
-
extraStringFields: ['permissionMode'],
|
|
266
|
+
extraStringFields: ['permissionMode', 'effort'],
|
|
87
267
|
});
|
|
88
268
|
return {
|
|
89
269
|
...config,
|
|
90
270
|
permissionMode: config?.permissionMode,
|
|
271
|
+
effort: normalizeClaudeEffortLevel(config?.effort),
|
|
91
272
|
};
|
|
92
273
|
}
|
|
93
|
-
// Default to 'locked' (shared workspace) when no mode has been picked. The
|
|
94
|
-
// UI still lets owners flip to 'worktree'; this just stops sessions from
|
|
95
|
-
// failing closed when the mode has never been written.
|
|
96
274
|
function resolveSessionExecutionMode(config) {
|
|
97
|
-
|
|
275
|
+
if (config?.executionMode)
|
|
276
|
+
return config.executionMode;
|
|
277
|
+
throw new ExecutionEnvironmentError('Session config is missing an execution mode.', 'Choose Isolated worktree or Use shared project before starting this coding session.');
|
|
98
278
|
}
|
|
99
279
|
function resolveWorkspaceCwd(config) {
|
|
100
280
|
return resolveHostWorkspaceCwd({
|
|
@@ -103,6 +283,14 @@ function resolveWorkspaceCwd(config) {
|
|
|
103
283
|
defaultCwd: workingDir,
|
|
104
284
|
});
|
|
105
285
|
}
|
|
286
|
+
function resolveExecutionFallbackReason(environment) {
|
|
287
|
+
if (!environment?.reason || environment.mode !== 'locked') {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
return environment.reason === 'Sharing the base workspace (locked mode)'
|
|
291
|
+
? null
|
|
292
|
+
: environment.reason;
|
|
293
|
+
}
|
|
106
294
|
async function detectRuntimeModels(cwd) {
|
|
107
295
|
const emptyPrompt = {
|
|
108
296
|
async *[Symbol.asyncIterator]() { },
|
|
@@ -130,6 +318,190 @@ async function detectRuntimeModels(cwd) {
|
|
|
130
318
|
catch { }
|
|
131
319
|
}
|
|
132
320
|
}
|
|
321
|
+
async function inspectClaudeRuntimeMetadata(cwd, activeQuery) {
|
|
322
|
+
const emptyPrompt = {
|
|
323
|
+
async *[Symbol.asyncIterator]() { },
|
|
324
|
+
};
|
|
325
|
+
const claudeCliPath = resolveClaudeCliPath();
|
|
326
|
+
const probe = activeQuery ?? query({
|
|
327
|
+
prompt: emptyPrompt,
|
|
328
|
+
options: {
|
|
329
|
+
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
330
|
+
tools: { type: 'preset', preset: 'claude_code' },
|
|
331
|
+
permissionMode: 'default',
|
|
332
|
+
cwd,
|
|
333
|
+
settings: { forceLoginMethod: 'claudeai' },
|
|
334
|
+
...(claudeCliPath ? { pathToClaudeCodeExecutable: claudeCliPath } : {}),
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
try {
|
|
338
|
+
const [init, mcpServers, pluginInfo] = await Promise.all([
|
|
339
|
+
probe.initializationResult(),
|
|
340
|
+
probe.mcpServerStatus().catch(() => []),
|
|
341
|
+
probe.reloadPlugins().catch(() => null),
|
|
342
|
+
]);
|
|
343
|
+
return {
|
|
344
|
+
account: init.account ?? null,
|
|
345
|
+
mcpServers: pluginInfo?.mcpServers ?? mcpServers,
|
|
346
|
+
commands: pluginInfo?.commands ?? init.commands ?? [],
|
|
347
|
+
agents: pluginInfo?.agents ?? init.agents ?? [],
|
|
348
|
+
plugins: pluginInfo?.plugins ?? [],
|
|
349
|
+
loadedAt: Date.now(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
if (!activeQuery) {
|
|
354
|
+
try {
|
|
355
|
+
probe.close();
|
|
356
|
+
}
|
|
357
|
+
catch { }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function refreshClaudeRuntimeMetadata(input) {
|
|
362
|
+
if (!input.force
|
|
363
|
+
&& runtimeMetadata.loadedAt > 0
|
|
364
|
+
&& Date.now() - runtimeMetadata.loadedAt < CLAUDE_METADATA_TTL_MS) {
|
|
365
|
+
return runtimeMetadata;
|
|
366
|
+
}
|
|
367
|
+
if (runtimeMetadataRefresh) {
|
|
368
|
+
return runtimeMetadataRefresh;
|
|
369
|
+
}
|
|
370
|
+
runtimeMetadataRefresh = inspectClaudeRuntimeMetadata(input.cwd, input.activeQuery ?? null)
|
|
371
|
+
.then((metadata) => {
|
|
372
|
+
runtimeMetadata = metadata;
|
|
373
|
+
return metadata;
|
|
374
|
+
})
|
|
375
|
+
.finally(() => {
|
|
376
|
+
runtimeMetadataRefresh = null;
|
|
377
|
+
});
|
|
378
|
+
return runtimeMetadataRefresh;
|
|
379
|
+
}
|
|
380
|
+
function mapClaudeMcpStatus(status) {
|
|
381
|
+
switch (status) {
|
|
382
|
+
case 'connected':
|
|
383
|
+
return 'ready';
|
|
384
|
+
case 'needs-auth':
|
|
385
|
+
return 'auth_needed';
|
|
386
|
+
case 'disabled':
|
|
387
|
+
return 'configured';
|
|
388
|
+
case 'failed':
|
|
389
|
+
return 'error';
|
|
390
|
+
default:
|
|
391
|
+
return 'unknown';
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function buildClaudeRuntimeInfo(input) {
|
|
395
|
+
const { descriptor, metadata } = input;
|
|
396
|
+
const statusItems = [
|
|
397
|
+
metadata.account?.email
|
|
398
|
+
? {
|
|
399
|
+
id: 'account',
|
|
400
|
+
label: 'Account',
|
|
401
|
+
value: metadata.account.email,
|
|
402
|
+
tone: 'success',
|
|
403
|
+
}
|
|
404
|
+
: null,
|
|
405
|
+
metadata.account?.subscriptionType
|
|
406
|
+
? {
|
|
407
|
+
id: 'subscription',
|
|
408
|
+
label: 'Plan',
|
|
409
|
+
value: metadata.account.subscriptionType,
|
|
410
|
+
}
|
|
411
|
+
: null,
|
|
412
|
+
metadata.account?.apiProvider
|
|
413
|
+
? {
|
|
414
|
+
id: 'provider',
|
|
415
|
+
label: 'Provider',
|
|
416
|
+
value: metadata.account.apiProvider,
|
|
417
|
+
}
|
|
418
|
+
: null,
|
|
419
|
+
metadata.account?.tokenSource || metadata.account?.apiKeySource
|
|
420
|
+
? {
|
|
421
|
+
id: 'auth',
|
|
422
|
+
label: 'Auth',
|
|
423
|
+
value: metadata.account.tokenSource ?? metadata.account?.apiKeySource ?? 'connected',
|
|
424
|
+
}
|
|
425
|
+
: null,
|
|
426
|
+
runtimeModels[0]
|
|
427
|
+
? {
|
|
428
|
+
id: 'fallbackModel',
|
|
429
|
+
label: 'Fallback model',
|
|
430
|
+
value: runtimeModels[0].label,
|
|
431
|
+
}
|
|
432
|
+
: null,
|
|
433
|
+
{
|
|
434
|
+
id: 'toolPolicy',
|
|
435
|
+
label: 'Tool policy',
|
|
436
|
+
value: 'Claude Code preset',
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: 'browser',
|
|
440
|
+
label: 'Browser tools',
|
|
441
|
+
value: 'Runtime policy',
|
|
442
|
+
},
|
|
443
|
+
].filter((item) => Boolean(item));
|
|
444
|
+
return {
|
|
445
|
+
descriptor,
|
|
446
|
+
surfaceMode: 'host',
|
|
447
|
+
statusItems,
|
|
448
|
+
inventories: [
|
|
449
|
+
{
|
|
450
|
+
id: 'mcp',
|
|
451
|
+
label: 'MCP',
|
|
452
|
+
entries: metadata.mcpServers.map((server) => ({
|
|
453
|
+
id: server.name,
|
|
454
|
+
label: server.name,
|
|
455
|
+
status: mapClaudeMcpStatus(server.status),
|
|
456
|
+
description: server.error
|
|
457
|
+
?? server.serverInfo?.version
|
|
458
|
+
?? server.scope
|
|
459
|
+
?? undefined,
|
|
460
|
+
})),
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: 'plugins',
|
|
464
|
+
label: 'Plugins',
|
|
465
|
+
entries: metadata.plugins.map((plugin) => ({
|
|
466
|
+
id: plugin.name,
|
|
467
|
+
label: plugin.name,
|
|
468
|
+
status: 'configured',
|
|
469
|
+
description: plugin.source ?? plugin.path,
|
|
470
|
+
})),
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
id: 'commands',
|
|
474
|
+
label: 'Commands',
|
|
475
|
+
entries: metadata.commands.map((command) => ({
|
|
476
|
+
id: command.name,
|
|
477
|
+
label: command.name.startsWith('/') ? command.name : `/${command.name}`,
|
|
478
|
+
status: 'configured',
|
|
479
|
+
description: command.description ?? command.argumentHint ?? undefined,
|
|
480
|
+
})),
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: 'agents',
|
|
484
|
+
label: 'Agents',
|
|
485
|
+
entries: metadata.agents.map((agent) => ({
|
|
486
|
+
id: agent.name,
|
|
487
|
+
label: agent.name,
|
|
488
|
+
status: 'configured',
|
|
489
|
+
description: agent.description ?? agent.model ?? undefined,
|
|
490
|
+
})),
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
execution: {
|
|
494
|
+
resolvedWorkspaceLabel: input.workspaceLabel,
|
|
495
|
+
resolvedCwd: input.resolvedCwd,
|
|
496
|
+
workspaceRootId: input.workspaceRootId ?? null,
|
|
497
|
+
workspaceRelativePath: input.workspaceRelativePath ?? null,
|
|
498
|
+
executionMode: input.executionMode,
|
|
499
|
+
executionBranch: input.executionBranch,
|
|
500
|
+
worktreePath: input.worktreePath,
|
|
501
|
+
fallbackReason: input.fallbackReason,
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
133
505
|
// ── Constants ───────────────────────────────────────────────────────
|
|
134
506
|
const MAX_SESSIONS = 5;
|
|
135
507
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
@@ -245,6 +617,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
245
617
|
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
246
618
|
tools: { type: 'preset', preset: 'claude_code' },
|
|
247
619
|
permissionMode: config.permissionMode,
|
|
620
|
+
allowDangerouslySkipPermissions: true,
|
|
248
621
|
cwd,
|
|
249
622
|
includePartialMessages: true,
|
|
250
623
|
maxTurns: 200,
|
|
@@ -284,7 +657,30 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
284
657
|
if (toolName === 'ExitPlanMode') {
|
|
285
658
|
try {
|
|
286
659
|
const planId = randomUUID();
|
|
287
|
-
const
|
|
660
|
+
const allowedPrompts = Array.isArray(input.allowedPrompts)
|
|
661
|
+
? input.allowedPrompts
|
|
662
|
+
.map((entry) => {
|
|
663
|
+
if (!entry || typeof entry !== 'object') {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
const prompt = entry.prompt;
|
|
667
|
+
const tool = entry.tool;
|
|
668
|
+
if (typeof prompt !== 'string' || !prompt.trim()) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
tool: typeof tool === 'string' && tool.trim() ? tool : 'Tool',
|
|
673
|
+
prompt,
|
|
674
|
+
};
|
|
675
|
+
})
|
|
676
|
+
.filter((entry) => Boolean(entry))
|
|
677
|
+
: [];
|
|
678
|
+
const planApproval = buildPlanApprovalRequest(planId, 'Plan ready for review.', allowedPrompts.length > 0 ? {
|
|
679
|
+
summary: 'Claude has prepared a plan and is asking permission to continue.',
|
|
680
|
+
allowedPrompts,
|
|
681
|
+
} : {
|
|
682
|
+
summary: 'Claude has prepared a plan and is asking permission to continue.',
|
|
683
|
+
});
|
|
288
684
|
await client.sendMessage(conversationId, planApproval.text, {
|
|
289
685
|
metadata: {
|
|
290
686
|
...planApproval.metadata,
|
|
@@ -344,6 +740,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
344
740
|
currentTurnOpenedAt: null,
|
|
345
741
|
lastAcceptedIntent: null,
|
|
346
742
|
lastActivity: Date.now(),
|
|
743
|
+
typingKeepaliveTimer: null,
|
|
347
744
|
closed: false,
|
|
348
745
|
sdkSessionId: resumeSessionId || null,
|
|
349
746
|
pendingReply: null,
|
|
@@ -400,6 +797,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
400
797
|
session.pendingFinalDeliveryText = null;
|
|
401
798
|
session.toolInProgress = false;
|
|
402
799
|
session.turnState = 'idle';
|
|
800
|
+
stopVisibleWorkSignal();
|
|
403
801
|
writeTurn();
|
|
404
802
|
drainPendingInput();
|
|
405
803
|
}
|
|
@@ -418,6 +816,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
418
816
|
session.pendingFinalDeliveryText = finalText;
|
|
419
817
|
session.state.state = 'running';
|
|
420
818
|
session.turnState = 'streaming';
|
|
819
|
+
stopVisibleWorkSignal();
|
|
421
820
|
streamingBuffer.replace(finalText);
|
|
422
821
|
writeState();
|
|
423
822
|
writeTurn();
|
|
@@ -468,6 +867,10 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
468
867
|
cwd,
|
|
469
868
|
executionMode: session.environment.mode,
|
|
470
869
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
870
|
+
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
871
|
+
...(resolveExecutionFallbackReason(session.environment)
|
|
872
|
+
? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
|
|
873
|
+
: {}),
|
|
471
874
|
hostMode: true,
|
|
472
875
|
isActive: true,
|
|
473
876
|
}).catch(() => { });
|
|
@@ -486,6 +889,25 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
486
889
|
: {}),
|
|
487
890
|
}).catch(() => { });
|
|
488
891
|
}
|
|
892
|
+
function refreshVisibleWorkSignal() {
|
|
893
|
+
if (session.closed)
|
|
894
|
+
return;
|
|
895
|
+
if (session.turnState !== 'thinking' && session.turnState !== 'tool')
|
|
896
|
+
return;
|
|
897
|
+
client.setTyping(conversationId, true, 'thinking').catch(() => { });
|
|
898
|
+
}
|
|
899
|
+
function startVisibleWorkSignal() {
|
|
900
|
+
refreshVisibleWorkSignal();
|
|
901
|
+
if (session.typingKeepaliveTimer)
|
|
902
|
+
return;
|
|
903
|
+
session.typingKeepaliveTimer = setInterval(refreshVisibleWorkSignal, 3500);
|
|
904
|
+
}
|
|
905
|
+
function stopVisibleWorkSignal() {
|
|
906
|
+
if (session.typingKeepaliveTimer) {
|
|
907
|
+
clearInterval(session.typingKeepaliveTimer);
|
|
908
|
+
session.typingKeepaliveTimer = null;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
489
911
|
async function markQueuedMessageAccepted(sourceMessageId, markAccepted) {
|
|
490
912
|
if (!markAccepted || !sourceMessageId)
|
|
491
913
|
return;
|
|
@@ -517,6 +939,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
517
939
|
if (session.state.state === 'running' && intent === 'interrupt') {
|
|
518
940
|
session.pendingInputs.unshift({ msg, intent, sourceMessageId, markAccepted });
|
|
519
941
|
session.turnState = 'interrupted';
|
|
942
|
+
stopVisibleWorkSignal();
|
|
520
943
|
writeTurn();
|
|
521
944
|
void session.query.interrupt().catch(() => { });
|
|
522
945
|
return;
|
|
@@ -557,18 +980,22 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
557
980
|
if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
|
|
558
981
|
session.toolInProgress = true;
|
|
559
982
|
session.turnState = 'tool';
|
|
983
|
+
startVisibleWorkSignal();
|
|
560
984
|
writeTurn();
|
|
561
985
|
}
|
|
562
986
|
else if (event?.type === 'content_block_stop' && session.toolInProgress) {
|
|
563
987
|
session.toolInProgress = false;
|
|
564
988
|
if (session.state.state === 'running') {
|
|
565
989
|
session.turnState = 'thinking';
|
|
990
|
+
startVisibleWorkSignal();
|
|
566
991
|
writeTurn();
|
|
567
992
|
}
|
|
568
993
|
}
|
|
569
994
|
else if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
570
995
|
session.toolInProgress = false;
|
|
571
996
|
session.turnState = 'streaming';
|
|
997
|
+
stopVisibleWorkSignal();
|
|
998
|
+
client.setTyping(conversationId, false).catch(() => { });
|
|
572
999
|
writeTurn();
|
|
573
1000
|
onStreamDelta(event.delta.text);
|
|
574
1001
|
}
|
|
@@ -589,9 +1016,10 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
589
1016
|
session.pendingFinalText = null;
|
|
590
1017
|
session.toolInProgress = false;
|
|
591
1018
|
session.turnState = 'thinking';
|
|
592
|
-
|
|
1019
|
+
startVisibleWorkSignal();
|
|
593
1020
|
}
|
|
594
1021
|
else if (state === 'idle') {
|
|
1022
|
+
stopVisibleWorkSignal();
|
|
595
1023
|
client.setTyping(conversationId, false).catch(() => { });
|
|
596
1024
|
}
|
|
597
1025
|
else if (state === 'requires_action') {
|
|
@@ -602,6 +1030,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
602
1030
|
}
|
|
603
1031
|
session.toolInProgress = false;
|
|
604
1032
|
session.turnState = 'waiting_input';
|
|
1033
|
+
stopVisibleWorkSignal();
|
|
605
1034
|
client.setTyping(conversationId, false).catch(() => { });
|
|
606
1035
|
}
|
|
607
1036
|
writeState();
|
|
@@ -668,6 +1097,15 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
668
1097
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to set initial model:`, err);
|
|
669
1098
|
}
|
|
670
1099
|
}
|
|
1100
|
+
if (config.effort) {
|
|
1101
|
+
try {
|
|
1102
|
+
await q.applyFlagSettings({ effortLevel: config.effort });
|
|
1103
|
+
session.state.effort = config.effort;
|
|
1104
|
+
}
|
|
1105
|
+
catch (err) {
|
|
1106
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to set initial effort:`, err);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
671
1109
|
if (!resumeSessionId) {
|
|
672
1110
|
// New session — send seed message to activate streaming input mode
|
|
673
1111
|
sendInput({
|
|
@@ -692,6 +1130,12 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
692
1130
|
availableModels: modelList,
|
|
693
1131
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
694
1132
|
availableExecutionModes: config.availableExecutionModes,
|
|
1133
|
+
runtimeDescriptor: buildClaudeRuntimeDescriptor({
|
|
1134
|
+
models: modelList,
|
|
1135
|
+
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
1136
|
+
workspaceRoots: workspaceRootMetadata,
|
|
1137
|
+
executionModes: config.availableExecutionModes,
|
|
1138
|
+
}),
|
|
695
1139
|
});
|
|
696
1140
|
writeSessionState(conversationId, agentId, {
|
|
697
1141
|
...session.state,
|
|
@@ -699,6 +1143,10 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
|
|
|
699
1143
|
cwd,
|
|
700
1144
|
executionMode: session.environment.mode,
|
|
701
1145
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1146
|
+
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1147
|
+
...(resolveExecutionFallbackReason(session.environment)
|
|
1148
|
+
? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
|
|
1149
|
+
: {}),
|
|
702
1150
|
hostMode: true,
|
|
703
1151
|
isActive: true,
|
|
704
1152
|
}).catch(() => { });
|
|
@@ -720,12 +1168,24 @@ export async function main() {
|
|
|
720
1168
|
options: {
|
|
721
1169
|
cwd: { type: 'string' },
|
|
722
1170
|
workspace: { type: 'string', multiple: true },
|
|
1171
|
+
'workspace-root': { type: 'string', multiple: true },
|
|
723
1172
|
},
|
|
724
1173
|
strict: false,
|
|
725
1174
|
});
|
|
726
1175
|
workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
|
|
727
1176
|
const configuredWorkspaces = (args.workspace ?? []).filter((value) => typeof value === 'string');
|
|
728
|
-
|
|
1177
|
+
const configuredWorkspaceRoots = (args['workspace-root'] ?? []).filter((value) => typeof value === 'string');
|
|
1178
|
+
const workspaceDiscovery = buildConfiguredWorkspaceOptionsWithRoots({
|
|
1179
|
+
primaryCwd: workingDir,
|
|
1180
|
+
configuredWorkspaces,
|
|
1181
|
+
workspaceRoots: configuredWorkspaceRoots,
|
|
1182
|
+
});
|
|
1183
|
+
workspaceOptions = workspaceDiscovery.workspaceOptions;
|
|
1184
|
+
workspaceRoots = workspaceDiscovery.workspaceRoots;
|
|
1185
|
+
workspaceRootMetadata = buildPublicWorkspaceRoots(workspaceRoots);
|
|
1186
|
+
for (const warning of workspaceDiscovery.warnings) {
|
|
1187
|
+
console.error(`[canon-host] ${warning}`);
|
|
1188
|
+
}
|
|
729
1189
|
const { apiKey, agentId: profileAgentId, profile } = resolveCanonAgent({ logPrefix: '[canon-host]' });
|
|
730
1190
|
console.error(`[canon-host] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
|
|
731
1191
|
// ── Canon API client + RTDB auth ──
|
|
@@ -759,6 +1219,12 @@ export async function main() {
|
|
|
759
1219
|
availablePermissionModes: [...CLAUDE_PERMISSION_MODE_OPTIONS],
|
|
760
1220
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
761
1221
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
1222
|
+
runtimeDescriptor: buildClaudeRuntimeDescriptor({
|
|
1223
|
+
models: runtimeModels,
|
|
1224
|
+
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
1225
|
+
workspaceRoots: workspaceRootMetadata,
|
|
1226
|
+
executionModes: hostAvailableExecutionModes,
|
|
1227
|
+
}),
|
|
762
1228
|
};
|
|
763
1229
|
const conversationCache = new Map();
|
|
764
1230
|
const knownConversationIds = new Set();
|
|
@@ -769,6 +1235,14 @@ export async function main() {
|
|
|
769
1235
|
await refreshKnownConversationIds().catch((error) => {
|
|
770
1236
|
console.error('[canon-host] Failed to refresh known conversations:', error);
|
|
771
1237
|
});
|
|
1238
|
+
const activeSessionQuery = sessions.values().next().value?.query ?? null;
|
|
1239
|
+
const metadata = await refreshClaudeRuntimeMetadata({
|
|
1240
|
+
cwd: workingDir,
|
|
1241
|
+
activeQuery: activeSessionQuery,
|
|
1242
|
+
}).catch((error) => {
|
|
1243
|
+
console.error('[canon-host] Failed to refresh Claude runtime metadata:', error);
|
|
1244
|
+
return runtimeMetadata;
|
|
1245
|
+
});
|
|
772
1246
|
await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
|
|
773
1247
|
console.error('[canon-host] Failed to publish agent runtime:', error);
|
|
774
1248
|
});
|
|
@@ -786,6 +1260,7 @@ export async function main() {
|
|
|
786
1260
|
{
|
|
787
1261
|
...(session.state.model ? { model: session.state.model } : {}),
|
|
788
1262
|
...(session.state.permissionMode ? { permissionMode: session.state.permissionMode } : {}),
|
|
1263
|
+
...(session.state.effort ? { effort: session.state.effort } : {}),
|
|
789
1264
|
...(workspaceId ? { workspaceId } : {}),
|
|
790
1265
|
executionMode: session.environment.mode,
|
|
791
1266
|
executionBranch: session.environment.branch ?? null,
|
|
@@ -795,6 +1270,33 @@ export async function main() {
|
|
|
795
1270
|
}).catch((error) => {
|
|
796
1271
|
console.error('[canon-host] Failed to publish session snapshots:', error);
|
|
797
1272
|
});
|
|
1273
|
+
await Promise.all(Array.from(knownConversationIds).map(async (conversationId) => {
|
|
1274
|
+
const session = sessions.get(conversationId);
|
|
1275
|
+
const workspaceId = session
|
|
1276
|
+
? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
|
|
1277
|
+
: runtimeDescriptor.defaultWorkspaceId;
|
|
1278
|
+
const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
|
|
1279
|
+
const payload = buildClaudeRuntimeInfo({
|
|
1280
|
+
descriptor: runtimeDescriptor.runtimeDescriptor ?? buildClaudeRuntimeDescriptor({
|
|
1281
|
+
models: runtimeModels,
|
|
1282
|
+
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
1283
|
+
workspaceRoots: workspaceRootMetadata,
|
|
1284
|
+
executionModes: hostAvailableExecutionModes,
|
|
1285
|
+
}),
|
|
1286
|
+
metadata,
|
|
1287
|
+
workspaceLabel: workspace?.label ?? workspaceId ?? null,
|
|
1288
|
+
resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
|
|
1289
|
+
workspaceRootId: workspace?.workspaceRootId ?? null,
|
|
1290
|
+
workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
|
|
1291
|
+
executionMode: session?.environment.mode ?? null,
|
|
1292
|
+
executionBranch: session?.environment.branch ?? null,
|
|
1293
|
+
worktreePath: session?.environment.worktreePath ?? null,
|
|
1294
|
+
fallbackReason: resolveExecutionFallbackReason(session?.environment),
|
|
1295
|
+
});
|
|
1296
|
+
await writeRuntimeInfo(conversationId, agentId, payload);
|
|
1297
|
+
})).catch((error) => {
|
|
1298
|
+
console.error('[canon-host] Failed to publish runtime info:', error);
|
|
1299
|
+
});
|
|
798
1300
|
};
|
|
799
1301
|
// ── Load conversations (informational — sessions created on demand) ──
|
|
800
1302
|
try {
|
|
@@ -807,6 +1309,12 @@ export async function main() {
|
|
|
807
1309
|
availableModels: runtimeModels,
|
|
808
1310
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
809
1311
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
1312
|
+
runtimeDescriptor: buildClaudeRuntimeDescriptor({
|
|
1313
|
+
models: runtimeModels,
|
|
1314
|
+
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
1315
|
+
workspaceRoots: workspaceRootMetadata,
|
|
1316
|
+
executionModes: hostAvailableExecutionModes,
|
|
1317
|
+
}),
|
|
810
1318
|
};
|
|
811
1319
|
}
|
|
812
1320
|
catch {
|
|
@@ -816,6 +1324,12 @@ export async function main() {
|
|
|
816
1324
|
availablePermissionModes: [...CLAUDE_PERMISSION_MODE_OPTIONS],
|
|
817
1325
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
818
1326
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
1327
|
+
runtimeDescriptor: buildClaudeRuntimeDescriptor({
|
|
1328
|
+
models: runtimeModels,
|
|
1329
|
+
workspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
1330
|
+
workspaceRoots: workspaceRootMetadata,
|
|
1331
|
+
executionModes: hostAvailableExecutionModes,
|
|
1332
|
+
}),
|
|
819
1333
|
};
|
|
820
1334
|
}
|
|
821
1335
|
try {
|
|
@@ -873,6 +1387,10 @@ export async function main() {
|
|
|
873
1387
|
if (!session)
|
|
874
1388
|
return;
|
|
875
1389
|
session.closed = true;
|
|
1390
|
+
if (session.typingKeepaliveTimer) {
|
|
1391
|
+
clearInterval(session.typingKeepaliveTimer);
|
|
1392
|
+
session.typingKeepaliveTimer = null;
|
|
1393
|
+
}
|
|
876
1394
|
releaseConversationEnvironment(session.environment);
|
|
877
1395
|
// Save SDK session ID for future resume
|
|
878
1396
|
if (session.sdkSessionId) {
|
|
@@ -958,6 +1476,7 @@ export async function main() {
|
|
|
958
1476
|
}, {
|
|
959
1477
|
model: config?.model ?? runtimeModels[0]?.value,
|
|
960
1478
|
permissionMode: config?.permissionMode ?? 'default',
|
|
1479
|
+
effort: config?.effort,
|
|
961
1480
|
availableModels: runtimeModels,
|
|
962
1481
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
963
1482
|
}, resumeId);
|
|
@@ -1060,7 +1579,12 @@ export async function main() {
|
|
|
1060
1579
|
const message = error instanceof Error ? error.message : String(error);
|
|
1061
1580
|
const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
|
|
1062
1581
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
|
|
1063
|
-
await client.sendMessage(convoId, `I couldn't start a coding session for this workspace: ${userMessage}
|
|
1582
|
+
await client.sendMessage(convoId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
|
|
1583
|
+
metadata: {
|
|
1584
|
+
turnSemantics: 'turn_complete',
|
|
1585
|
+
turnComplete: true,
|
|
1586
|
+
},
|
|
1587
|
+
}).catch(() => { });
|
|
1064
1588
|
return;
|
|
1065
1589
|
}
|
|
1066
1590
|
const turnMetadata = normalizeTurnMetadata(m.metadata);
|
|
@@ -1139,12 +1663,16 @@ export async function main() {
|
|
|
1139
1663
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set mode:`, err);
|
|
1140
1664
|
}
|
|
1141
1665
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
effortLevel:
|
|
1146
|
-
|
|
1147
|
-
|
|
1666
|
+
const nextEffort = normalizeClaudeEffortLevel(ctrl.effort);
|
|
1667
|
+
if (nextEffort && nextEffort !== session.state.effort) {
|
|
1668
|
+
try {
|
|
1669
|
+
await session.query.applyFlagSettings({ effortLevel: nextEffort });
|
|
1670
|
+
session.state.effort = nextEffort;
|
|
1671
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Effort → ${nextEffort}`);
|
|
1672
|
+
}
|
|
1673
|
+
catch (err) {
|
|
1674
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set effort:`, err);
|
|
1675
|
+
}
|
|
1148
1676
|
}
|
|
1149
1677
|
writeSessionState(convoId, agentId, {
|
|
1150
1678
|
...session.state,
|
|
@@ -1152,6 +1680,10 @@ export async function main() {
|
|
|
1152
1680
|
cwd: session.cwd,
|
|
1153
1681
|
executionMode: session.environment.mode,
|
|
1154
1682
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1683
|
+
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1684
|
+
...(resolveExecutionFallbackReason(session.environment)
|
|
1685
|
+
? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
|
|
1686
|
+
: {}),
|
|
1155
1687
|
hostMode: true,
|
|
1156
1688
|
isActive: true,
|
|
1157
1689
|
}).catch(() => { });
|
|
@@ -1213,6 +1745,7 @@ export async function main() {
|
|
|
1213
1745
|
cwd: session.cwd,
|
|
1214
1746
|
executionMode: session.environment.mode,
|
|
1215
1747
|
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1748
|
+
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1216
1749
|
hostMode: true,
|
|
1217
1750
|
isActive: true,
|
|
1218
1751
|
}).catch(() => { });
|
package/dist/server.js
CHANGED
|
@@ -26,6 +26,10 @@ let agentContext = null;
|
|
|
26
26
|
let approvalManager = null;
|
|
27
27
|
let approvalServer = null;
|
|
28
28
|
const conversationCache = new Map();
|
|
29
|
+
const TURN_COMPLETE_METADATA = {
|
|
30
|
+
turnSemantics: 'turn_complete',
|
|
31
|
+
turnComplete: true,
|
|
32
|
+
};
|
|
29
33
|
/** The conversation that most recently triggered work (for approval routing) */
|
|
30
34
|
let triggerConversationId = null;
|
|
31
35
|
function getPrimaryAttachment(message) {
|
|
@@ -221,7 +225,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
221
225
|
case 'reply': {
|
|
222
226
|
const conversationId = args.conversation_id;
|
|
223
227
|
const text = args.text;
|
|
224
|
-
const result = await client.sendMessage(conversationId, text
|
|
228
|
+
const result = await client.sendMessage(conversationId, text, {
|
|
229
|
+
metadata: TURN_COMPLETE_METADATA,
|
|
230
|
+
});
|
|
225
231
|
return {
|
|
226
232
|
content: [
|
|
227
233
|
{
|
|
@@ -261,6 +267,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
261
267
|
else {
|
|
262
268
|
opts.contentType = contentType;
|
|
263
269
|
}
|
|
270
|
+
opts.metadata = {
|
|
271
|
+
...(opts.metadata ?? {}),
|
|
272
|
+
...TURN_COMPLETE_METADATA,
|
|
273
|
+
};
|
|
264
274
|
const result = await client.sendMessage(conversationId, text, opts);
|
|
265
275
|
return {
|
|
266
276
|
content: [
|
|
@@ -288,6 +298,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
288
298
|
const messageOptions = typeof args.message_options === 'object' && args.message_options != null
|
|
289
299
|
? args.message_options
|
|
290
300
|
: undefined;
|
|
301
|
+
const linkedMessageOptions = messageOptions;
|
|
291
302
|
const result = await client.sendLinkedMessage({
|
|
292
303
|
sourceConversationId,
|
|
293
304
|
targetConversationId,
|
|
@@ -308,11 +319,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
308
319
|
targetContext: targetContext,
|
|
309
320
|
}
|
|
310
321
|
: {}),
|
|
311
|
-
...(
|
|
322
|
+
...(linkedMessageOptions
|
|
312
323
|
? {
|
|
313
|
-
messageOptions:
|
|
324
|
+
messageOptions: {
|
|
325
|
+
...linkedMessageOptions,
|
|
326
|
+
metadata: {
|
|
327
|
+
...(linkedMessageOptions.metadata ?? {}),
|
|
328
|
+
...TURN_COMPLETE_METADATA,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
314
331
|
}
|
|
315
|
-
: {}),
|
|
332
|
+
: { messageOptions: { metadata: TURN_COMPLETE_METADATA } }),
|
|
316
333
|
});
|
|
317
334
|
return {
|
|
318
335
|
content: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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": "^0.
|
|
35
|
-
"@canonmsg/core": "^0.
|
|
34
|
+
"@canonmsg/agent-sdk": "^0.9.2",
|
|
35
|
+
"@canonmsg/core": "^0.10.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|