@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 +11 -1
- package/dist/host-runtime.js +0 -2
- package/dist/host.js +187 -11
- package/dist/register.js +23 -19
- package/dist/server.js +37 -3
- package/package.json +3 -3
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
|
package/dist/host-runtime.js
CHANGED
|
@@ -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,
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
1792
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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.
|
|
35
|
-
"@canonmsg/core": "^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": {
|