@canonmsg/claude-code-plugin 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,15 @@ canon-register --name "My Claude" --description "My Claude Code agent" --phone "
15
15
  canon-claude
16
16
  ```
17
17
 
18
- Send a message to your agent in the Canon app. Claude Code responds with full capabilities (file read/write, bash, etc.).
18
+ Send a message to your agent in the Canon app. Claude Code responds with full capabilities (file read/write, bash, etc.). Keep the `canon-claude` terminal open; closing it takes this local agent offline.
19
+
20
+ After a restart or closed terminal, use the shared local manager to list and revive all recorded local agents:
21
+
22
+ ```bash
23
+ npm install -g @canonmsg/local-agents
24
+ canon-necromance
25
+ canon-necromance revive frontend
26
+ ```
19
27
 
20
28
  ## Features
21
29
 
@@ -60,6 +68,8 @@ CANON_AGENT=frontend canon-claude --cwd ~/projects/frontend
60
68
 
61
69
  For a lighter integration without session controls, run Canon as a channel plugin inside your own Claude Code session. See [full setup guide](../../docs/claude-code-setup.md#channel-mode-alternative).
62
70
 
71
+ Channel mode is embedded inside Claude Code and is shown as non-revivable by `canon-necromance`. Use `canon-claude` host mode when you want phone-controlled runtime sessions.
72
+
63
73
  ## Development
64
74
 
65
75
  ```bash
@@ -69,8 +69,6 @@ export function renderCanonHostInboundContent(message, materialized) {
69
69
  }
70
70
  function describeContactCard(card) {
71
71
  const parts = [`${card.userType} · userId: ${card.userId}`];
72
- if (card.accessLevel)
73
- parts.push(`access: ${card.accessLevel}`);
74
72
  if (card.ownerName)
75
73
  parts.push(`owner: ${card.ownerName}`);
76
74
  if (card.about)
package/dist/host.js CHANGED
@@ -26,7 +26,7 @@ import { execFileSync } from 'node:child_process';
26
26
  import { existsSync } from 'node:fs';
27
27
  import { query, } from '@anthropic-ai/claude-agent-sdk';
28
28
  import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
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';
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, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, rtdbWrite, writeRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, upsertLocalRuntimeEntry, } 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';
@@ -207,6 +207,7 @@ function buildClaudeRuntimeDescriptor(input) {
207
207
  workspaceRoots: input.workspaceRoots,
208
208
  supportsInterrupt: true,
209
209
  streamingTextMode: 'delta',
210
+ admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
210
211
  };
211
212
  }
212
213
  function normalizeClaudeEffortLevel(value) {
@@ -1158,7 +1159,13 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
1158
1159
  console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ${resumeSessionId ? 'resumed' : 'started'} `
1159
1160
  + `(mode: ${environment.mode}, cwd: ${cwd})`);
1160
1161
  };
1161
- init().catch((err) => console.error(`[canon-host] [${conversationId.slice(0, 8)}] Init failed:`, err));
1162
+ init().catch((err) => {
1163
+ console.error(`[canon-host] [${conversationId.slice(0, 8)}] Init failed:`, err);
1164
+ if (resumeSessionId) {
1165
+ session.sdkSessionId = null;
1166
+ onSessionEnd(conversationId, null);
1167
+ }
1168
+ });
1162
1169
  return session;
1163
1170
  }
1164
1171
  // ── Main ────────────────────────────────────────────────────────────
@@ -1186,10 +1193,10 @@ export async function main() {
1186
1193
  for (const warning of workspaceDiscovery.warnings) {
1187
1194
  console.error(`[canon-host] ${warning}`);
1188
1195
  }
1189
- const { apiKey, agentId: profileAgentId, profile } = resolveCanonAgent({ logPrefix: '[canon-host]' });
1196
+ const { apiKey, agentId: profileAgentId, agentName: profileAgentName, profile, baseUrl, lockHandle, } = resolveCanonAgent({ logPrefix: '[canon-host]', expectedClientType: 'claude-code' });
1190
1197
  console.error(`[canon-host] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
1191
1198
  // ── Canon API client + RTDB auth ──
1192
- const client = new CanonClient(apiKey);
1199
+ const client = new CanonClient(apiKey, baseUrl);
1193
1200
  initRTDBAuth(client);
1194
1201
  let agentId;
1195
1202
  let ownerId = null;
@@ -1209,6 +1216,34 @@ export async function main() {
1209
1216
  }
1210
1217
  console.error(`[canon-host] Authenticated as ${agentId}`);
1211
1218
  }
1219
+ const launchArgs = [...process.argv.slice(2)];
1220
+ if (!launchArgs.some((arg) => arg === '--cwd' || arg.startsWith('--cwd='))) {
1221
+ launchArgs.push('--cwd', workingDir);
1222
+ }
1223
+ const runtimeId = buildLocalRuntimeId({
1224
+ runtime: 'claude-code',
1225
+ profile,
1226
+ cwd: workingDir,
1227
+ launchCommand: ['canon-claude', ...launchArgs],
1228
+ });
1229
+ upsertLocalRuntimeEntry({
1230
+ id: runtimeId,
1231
+ runtime: 'claude-code',
1232
+ profile,
1233
+ agentId,
1234
+ agentName: profileAgentName,
1235
+ cwd: workingDir,
1236
+ baseCwd: workingDir,
1237
+ workspaceRoots: workspaceRoots.map((root) => root.cwd),
1238
+ workspaces: workspaceOptions.map((workspace) => workspace.cwd),
1239
+ launchCommand: ['canon-claude', ...launchArgs],
1240
+ pid: process.pid,
1241
+ status: profile ? 'running' : 'manual',
1242
+ reviveCapability: profile ? 'revivable' : 'manual',
1243
+ surfaceMode: 'host',
1244
+ lastStartedAt: new Date().toISOString(),
1245
+ lastHeartbeatAt: new Date().toISOString(),
1246
+ });
1212
1247
  let streamConnected = false;
1213
1248
  const hostAvailableExecutionModes = [
1214
1249
  ...EXECUTION_ENVIRONMENT_MODES,
@@ -1230,6 +1265,12 @@ export async function main() {
1230
1265
  const knownConversationIds = new Set();
1231
1266
  let lastKnownConversationRefreshAt = 0;
1232
1267
  const publishRuntimeHeartbeat = async () => {
1268
+ heartbeatLocalRuntimeEntry(runtimeId, {
1269
+ agentId,
1270
+ agentName: profileAgentName,
1271
+ cwd: workingDir,
1272
+ baseCwd: workingDir,
1273
+ });
1233
1274
  if (!streamConnected)
1234
1275
  return;
1235
1276
  await refreshKnownConversationIds().catch((error) => {
@@ -1395,6 +1436,12 @@ export async function main() {
1395
1436
  // Save SDK session ID for future resume
1396
1437
  if (session.sdkSessionId) {
1397
1438
  savedSessionIds.set(buildConversationEnvironmentKey(conversationId, session.environment.baseCwd), session.sdkSessionId);
1439
+ saveRuntimeSessionState(runtimeId, {
1440
+ conversationId,
1441
+ baseCwd: session.environment.baseCwd,
1442
+ executionMode: session.environment.mode,
1443
+ claudeSessionId: session.sdkSessionId,
1444
+ });
1398
1445
  }
1399
1446
  if (session.streamingTimer)
1400
1447
  clearTimeout(session.streamingTimer);
@@ -1457,7 +1504,12 @@ export async function main() {
1457
1504
  });
1458
1505
  try {
1459
1506
  const resumeKey = buildConversationEnvironmentKey(conversationId, environment.baseCwd);
1460
- const resumeId = savedSessionIds.get(resumeKey);
1507
+ const persisted = loadRuntimeSessionState(runtimeId, {
1508
+ conversationId,
1509
+ baseCwd: environment.baseCwd,
1510
+ executionMode: environment.mode,
1511
+ });
1512
+ const resumeId = savedSessionIds.get(resumeKey) ?? persisted?.claudeSessionId;
1461
1513
  const session = createSession(conversationId, environment, agentId, client, (convoId, sdkSessionId) => {
1462
1514
  // Called when session processing loop ends — save session ID for resume
1463
1515
  const existing = sessions.get(convoId);
@@ -1465,6 +1517,19 @@ export async function main() {
1465
1517
  releaseConversationEnvironment(existing.environment);
1466
1518
  if (sdkSessionId) {
1467
1519
  savedSessionIds.set(buildConversationEnvironmentKey(convoId, existing.environment.baseCwd), sdkSessionId);
1520
+ saveRuntimeSessionState(runtimeId, {
1521
+ conversationId: convoId,
1522
+ baseCwd: existing.environment.baseCwd,
1523
+ executionMode: existing.environment.mode,
1524
+ claudeSessionId: sdkSessionId,
1525
+ });
1526
+ }
1527
+ else {
1528
+ clearRuntimeSessionState(runtimeId, {
1529
+ conversationId: convoId,
1530
+ baseCwd: existing.environment.baseCwd,
1531
+ executionMode: existing.environment.mode,
1532
+ });
1468
1533
  }
1469
1534
  }
1470
1535
  sessions.delete(convoId);
@@ -1496,6 +1561,112 @@ export async function main() {
1496
1561
  pendingSessionCreations.delete(conversationId);
1497
1562
  }
1498
1563
  }
1564
+ async function enqueueRecoveredInboundMessage(input) {
1565
+ const m = input.message;
1566
+ if (m.senderId === agentId)
1567
+ return;
1568
+ let materialized = [];
1569
+ if (m.id) {
1570
+ try {
1571
+ materialized = await materializeMessageMedia({
1572
+ id: m.id,
1573
+ attachments: m.attachments ?? [],
1574
+ }, { agentId, conversationId: input.conversationId });
1575
+ }
1576
+ catch (error) {
1577
+ console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to materialize recovered media:`, error instanceof Error ? error.message : error);
1578
+ }
1579
+ }
1580
+ const sender = m.senderName || m.senderId;
1581
+ const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
1582
+ const content = renderInboundContent(m, materialized);
1583
+ const hydrated = await loadHydratedInboundContext({
1584
+ conversationId: input.conversationId,
1585
+ message: m,
1586
+ senderName: sender,
1587
+ isOwner,
1588
+ });
1589
+ const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
1590
+ const workSessions = input.hydratedPage?.workSessions ?? hydrated.workSessions;
1591
+ const participantContext = hydrated.participantContext;
1592
+ const autoReply = decideAutoReply(participantContext, behavior);
1593
+ if (!autoReply.allow) {
1594
+ console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Suppressed recovered auto-reply: ${autoReply.reason}`);
1595
+ return;
1596
+ }
1597
+ let session;
1598
+ try {
1599
+ session = await getOrCreateSession(input.conversationId);
1600
+ }
1601
+ catch (error) {
1602
+ const message = error instanceof Error ? error.message : String(error);
1603
+ const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1604
+ console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
1605
+ await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1606
+ metadata: {
1607
+ turnSemantics: 'turn_complete',
1608
+ turnComplete: true,
1609
+ },
1610
+ }).catch(() => { });
1611
+ return;
1612
+ }
1613
+ const turnMetadata = normalizeTurnMetadata(m.metadata);
1614
+ const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
1615
+ const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
1616
+ const promptText = buildCanonPrompt({
1617
+ content,
1618
+ conversationId: input.conversationId,
1619
+ participantContext,
1620
+ behavior,
1621
+ workSession: m.workSession,
1622
+ workSessions,
1623
+ });
1624
+ const messageContent = await buildCanonUserContent({ promptText, materialized });
1625
+ session.enqueueInbound({
1626
+ type: 'user',
1627
+ message: {
1628
+ role: 'user',
1629
+ content: messageContent,
1630
+ },
1631
+ parent_tool_use_id: null,
1632
+ }, deliveryIntent, m.id ?? null, shouldMarkAccepted);
1633
+ if (m.id) {
1634
+ saveRuntimeSessionState(runtimeId, {
1635
+ conversationId: input.conversationId,
1636
+ baseCwd: workingDir,
1637
+ lastInboundMessageId: m.id,
1638
+ });
1639
+ }
1640
+ }
1641
+ try {
1642
+ for (const conversation of conversationCache.values()) {
1643
+ const cursor = loadRuntimeSessionState(runtimeId, {
1644
+ conversationId: conversation.id,
1645
+ baseCwd: workingDir,
1646
+ })?.lastInboundMessageId;
1647
+ const page = await client.getMessagesPage(conversation.id, 25);
1648
+ const inboundMessages = page.messages
1649
+ .filter((message) => message.senderId !== agentId)
1650
+ .sort((a, b) => String(a.createdAt ?? '').localeCompare(String(b.createdAt ?? '')));
1651
+ const cursorIndex = cursor
1652
+ ? inboundMessages.findIndex((message) => message.id === cursor)
1653
+ : -1;
1654
+ const messagesToRecover = cursorIndex >= 0
1655
+ ? inboundMessages.slice(cursorIndex + 1)
1656
+ : inboundMessages.slice(-1);
1657
+ for (const message of messagesToRecover) {
1658
+ console.error(`[canon-host] [${conversation.id.slice(0, 8)}] Recovering inbound message on startup`);
1659
+ await enqueueRecoveredInboundMessage({
1660
+ conversationId: conversation.id,
1661
+ message: message,
1662
+ hydratedPage: page,
1663
+ });
1664
+ }
1665
+ }
1666
+ }
1667
+ catch (error) {
1668
+ console.error('[canon-host] Failed startup recovery:', error instanceof Error ? error.message : error);
1669
+ }
1499
1670
  // ── Connect SSE stream for inbound Canon messages ──
1500
1671
  const stream = new CanonStream({
1501
1672
  apiKey,
@@ -1607,6 +1778,13 @@ export async function main() {
1607
1778
  },
1608
1779
  parent_tool_use_id: null,
1609
1780
  }, deliveryIntent, m.id ?? null, shouldMarkAccepted);
1781
+ if (m.id) {
1782
+ saveRuntimeSessionState(runtimeId, {
1783
+ conversationId: convoId,
1784
+ baseCwd: workingDir,
1785
+ lastInboundMessageId: m.id,
1786
+ });
1787
+ }
1610
1788
  })();
1611
1789
  },
1612
1790
  onConnected: () => {
@@ -1788,13 +1966,13 @@ export async function main() {
1788
1966
  for (const convoId of [...sessions.keys()]) {
1789
1967
  closeSession(convoId);
1790
1968
  }
1791
- const ap = getActiveProfile();
1792
- if (ap)
1793
- releaseLock(ap);
1969
+ markLocalRuntimeStopped(runtimeId);
1970
+ (lockHandle ?? getActiveProfileLock())?.release();
1794
1971
  process.exit(0);
1795
1972
  };
1796
1973
  process.on('SIGINT', shutdown);
1797
1974
  process.on('SIGTERM', shutdown);
1975
+ process.on('SIGHUP', shutdown);
1798
1976
  console.error('[canon-host] Ready — sessions created on demand');
1799
1977
  // Keep alive
1800
1978
  await new Promise(() => { });
@@ -1802,8 +1980,6 @@ export async function main() {
1802
1980
  // ── Entry ───────────────────────────────────────────────────────────
1803
1981
  runCli(import.meta.url, main, (err) => {
1804
1982
  console.error('[canon-host] Fatal error:', err);
1805
- const ap = getActiveProfile();
1806
- if (ap)
1807
- releaseLock(ap);
1983
+ getActiveProfileLock()?.release();
1808
1984
  process.exit(1);
1809
1985
  });
package/dist/register.js CHANGED
@@ -9,14 +9,10 @@ import { setDefaultResultOrder } from 'node:dns';
9
9
  *
10
10
  * On approval, saves the agent to ~/.canon/agents.json under the profile name.
11
11
  */
12
- import { registerAndWaitForApproval } from '@canonmsg/core';
13
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
14
- import { join } from 'node:path';
15
- import { homedir } from 'node:os';
12
+ import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core';
13
+ import { readFileSync } from 'node:fs';
16
14
  import { parseArgs } from 'node:util';
17
15
  import { runCli } from './cli-entry.js';
18
- const CANON_DIR = join(homedir(), '.canon');
19
- const AGENTS_PATH = join(CANON_DIR, 'agents.json');
20
16
  export async function main() {
21
17
  setDefaultResultOrder('ipv4first');
22
18
  const { values } = parseArgs({
@@ -44,6 +40,7 @@ export async function main() {
44
40
  // File doesn't exist yet
45
41
  }
46
42
  console.log(`Registering agent "${values.name}" (profile: ${profileName})...`);
43
+ const pending = getOrCreatePendingRegistration(profileName, 'claude-code');
47
44
  const result = await registerAndWaitForApproval({
48
45
  name: values.name,
49
46
  description: values.description,
@@ -52,8 +49,14 @@ export async function main() {
52
49
  clientType: 'claude-code',
53
50
  baseUrl: values['base-url'],
54
51
  requestedAgentId: existingAgentId,
52
+ localRegistrationId: pending.localRegistrationId,
55
53
  }, {
56
- onSubmitted: (requestId) => {
54
+ onSubmitted: (requestId, pollToken) => {
55
+ updatePendingRegistration(profileName, {
56
+ requestId,
57
+ pollToken,
58
+ clientType: 'claude-code',
59
+ });
57
60
  console.log(`Registration submitted (request ID: ${requestId}).`);
58
61
  console.log('Waiting for approval in Canon app...');
59
62
  },
@@ -64,25 +67,26 @@ export async function main() {
64
67
  console.log(''); // newline after dots
65
68
  switch (result.status) {
66
69
  case 'approved': {
67
- // Save to ~/.canon/agents.json
68
- mkdirSync(CANON_DIR, { recursive: true });
69
- let profiles = {};
70
- try {
71
- profiles = JSON.parse(readFileSync(AGENTS_PATH, 'utf-8'));
70
+ if (!result.apiKey || !result.agentId || !result.agentName) {
71
+ console.error('Approval completed but Canon did not return a usable API key. Run this command again to resume key pickup.');
72
+ process.exit(1);
72
73
  }
73
- catch {
74
- // File doesn't exist yet
75
- }
76
- profiles[profileName] = {
74
+ upsertAgentProfile(profileName, {
77
75
  apiKey: result.apiKey,
78
76
  agentId: result.agentId,
79
77
  agentName: result.agentName,
80
78
  registeredAt: new Date().toISOString(),
81
- };
82
- writeFileSync(AGENTS_PATH, JSON.stringify(profiles, null, 2));
79
+ clientType: 'claude-code',
80
+ ...(typeof values['base-url'] === 'string' ? { baseUrl: values['base-url'] } : {}),
81
+ });
82
+ if (result.requestId) {
83
+ await ackRegistrationApproval(values['base-url'], result.requestId, result.pollToken);
84
+ }
85
+ clearPendingRegistration(profileName);
83
86
  console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
84
87
  console.log(`Saved as profile "${profileName}" in ~/.canon/agents.json`);
85
- console.log('Restart Claude Code to connect.');
88
+ console.log('Start it with: CANON_AGENT=' + profileName + ' canon-claude --cwd /path/to/project');
89
+ console.log('Keep that terminal open. Closing it takes this local agent offline.');
86
90
  break;
87
91
  }
88
92
  case 'rejected':
package/dist/server.js CHANGED
@@ -10,7 +10,7 @@ import { setDefaultResultOrder } from 'node:dns';
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
- import { CanonClient, CanonStream, ApprovalManager, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, initRTDBAuth, writeSessionState, clearSessionState, } from '@canonmsg/core';
13
+ import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, writeSessionState, clearSessionState, } from '@canonmsg/core';
14
14
  import { ApprovalHttpServer } from './approval-server.js';
15
15
  import { runCli } from './cli-entry.js';
16
16
  // ── MCP server ─────────────────────────────────────────────────────────
@@ -525,10 +525,14 @@ async function handleInboundMessage(payload) {
525
525
  async function startChannel() {
526
526
  let apiKey;
527
527
  let profile;
528
+ let baseUrl;
529
+ let profileAgentName;
528
530
  try {
529
- const resolved = resolveCanonAgent({ logPrefix: '[canon]' });
531
+ const resolved = resolveCanonAgent({ logPrefix: '[canon]', expectedClientType: 'claude-code' });
530
532
  apiKey = resolved.apiKey;
531
533
  profile = resolved.profile;
534
+ baseUrl = resolved.baseUrl;
535
+ profileAgentName = resolved.agentName;
532
536
  }
533
537
  catch (err) {
534
538
  console.error(err instanceof Error ? err.message : err);
@@ -537,7 +541,7 @@ async function startChannel() {
537
541
  if (profile) {
538
542
  console.error(`[canon] Using agent profile: ${profile}`);
539
543
  }
540
- client = new CanonClient(apiKey);
544
+ client = new CanonClient(apiKey, baseUrl);
541
545
  // Get agent identity — try /agents/me first, fall back to /agents/auth-token
542
546
  let agentId;
543
547
  try {
@@ -560,6 +564,27 @@ async function startChannel() {
560
564
  }
561
565
  // Initialize RTDB auth for session state reporting
562
566
  initRTDBAuth(client);
567
+ const runtimeId = buildLocalRuntimeId({
568
+ runtime: 'claude-code',
569
+ profile,
570
+ cwd: process.cwd(),
571
+ launchCommand: ['canon-channel-server'],
572
+ });
573
+ upsertLocalRuntimeEntry({
574
+ id: runtimeId,
575
+ runtime: 'claude-code',
576
+ profile,
577
+ agentId,
578
+ agentName: agentContext?.displayName ?? profileAgentName,
579
+ cwd: process.cwd(),
580
+ launchCommand: ['canon-channel-server'],
581
+ pid: process.pid,
582
+ status: 'embedded',
583
+ reviveCapability: 'embedded',
584
+ surfaceMode: 'channel',
585
+ lastStartedAt: new Date().toISOString(),
586
+ lastHeartbeatAt: new Date().toISOString(),
587
+ });
563
588
  // Populate conversation cache
564
589
  try {
565
590
  const convos = await client.getConversations();
@@ -627,13 +652,22 @@ export async function main() {
627
652
  approvalManager?.dispose();
628
653
  approvalServer?.stop().catch(() => { });
629
654
  stream?.stop();
655
+ const runtimeId = buildLocalRuntimeId({
656
+ runtime: 'claude-code',
657
+ profile: getActiveProfile(),
658
+ cwd: process.cwd(),
659
+ launchCommand: ['canon-channel-server'],
660
+ });
661
+ markLocalRuntimeStopped(runtimeId);
630
662
  const ap = getActiveProfile();
631
663
  if (ap)
632
664
  releaseLock(ap);
665
+ getActiveProfileLock()?.release();
633
666
  process.exit(0);
634
667
  };
635
668
  process.on('SIGINT', shutdown);
636
669
  process.on('SIGTERM', shutdown);
670
+ process.on('SIGHUP', shutdown);
637
671
  }
638
672
  runCli(import.meta.url, main, (err) => {
639
673
  console.error('[canon] Fatal error:', err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "^0.2.101",
34
- "@canonmsg/agent-sdk": "^0.9.2",
35
- "@canonmsg/core": "^0.10.0",
34
+ "@canonmsg/agent-sdk": "^0.10.1",
35
+ "@canonmsg/core": "^0.13.0",
36
36
  "@modelcontextprotocol/sdk": "^1.12.0"
37
37
  },
38
38
  "engines": {