@canonmsg/core 0.2.2

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.
@@ -0,0 +1,54 @@
1
+ import { type CanonMessage, type CanonConversation, type AgentContext, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
2
+ /**
3
+ * Thin REST client for Canon's agent API.
4
+ * Uses native fetch — no runtime dependencies.
5
+ */
6
+ export declare class CanonClient {
7
+ private baseUrl;
8
+ private apiKey;
9
+ constructor(apiKey: string, baseUrl?: string);
10
+ private authHeaders;
11
+ getAuthToken(): Promise<{
12
+ token: string;
13
+ expiresAt: string;
14
+ agentId: string;
15
+ }>;
16
+ getAgentMe(): Promise<AgentContext>;
17
+ getConversations(): Promise<CanonConversation[]>;
18
+ getMessages(conversationId: string, limit?: number, before?: string): Promise<CanonMessage[]>;
19
+ sendMessage(conversationId: string, text: string, options?: SendMessageOptions): Promise<{
20
+ messageId: string;
21
+ }>;
22
+ createConversation(options: CreateConversationOptions): Promise<{
23
+ conversationId: string;
24
+ }>;
25
+ uploadMedia(conversationId: string, data: string, mimeType: string): Promise<{
26
+ url: string;
27
+ }>;
28
+ updateTopic(conversationId: string, topic: string): Promise<void>;
29
+ deleteMessage(conversationId: string, messageId: string): Promise<void>;
30
+ markAsRead(conversationId: string): Promise<void>;
31
+ leaveConversation(conversationId: string): Promise<void>;
32
+ react(conversationId: string, messageId: string, emoji: string): Promise<void>;
33
+ updateConversationName(conversationId: string, name: string): Promise<void>;
34
+ addMember(conversationId: string, userId: string): Promise<void>;
35
+ removeMember(conversationId: string, userId: string): Promise<void>;
36
+ setStreaming(options: SetStreamingOptions): Promise<void>;
37
+ clearStreaming(conversationId: string): Promise<void>;
38
+ setTyping(conversationId: string, typing: boolean, status?: 'thinking' | 'typing'): Promise<void>;
39
+ static register(baseUrl: string | undefined, body: {
40
+ name: string;
41
+ description: string;
42
+ ownerPhone: string;
43
+ developerInfo: string;
44
+ avatarUrl?: string;
45
+ clientType?: string;
46
+ }): Promise<{
47
+ requestId: string;
48
+ }>;
49
+ static checkStatus(baseUrl: string | undefined, requestId: string): Promise<RegistrationStatus>;
50
+ }
51
+ export declare class CanonApiError extends Error {
52
+ status: number;
53
+ constructor(status: number, body: string);
54
+ }
package/dist/client.js ADDED
@@ -0,0 +1,208 @@
1
+ import { DEFAULT_BASE_URL } from './constants.js';
2
+ /**
3
+ * Thin REST client for Canon's agent API.
4
+ * Uses native fetch — no runtime dependencies.
5
+ */
6
+ export class CanonClient {
7
+ baseUrl;
8
+ apiKey;
9
+ constructor(apiKey, baseUrl) {
10
+ this.apiKey = apiKey;
11
+ this.baseUrl = baseUrl || DEFAULT_BASE_URL;
12
+ }
13
+ authHeaders() {
14
+ return {
15
+ Authorization: `Bearer ${this.apiKey}`,
16
+ 'Content-Type': 'application/json',
17
+ };
18
+ }
19
+ // ── Authenticated endpoints ──────────────────────────────────────────
20
+ async getAuthToken() {
21
+ const res = await fetch(`${this.baseUrl}/agents/auth-token`, {
22
+ method: 'POST',
23
+ headers: this.authHeaders(),
24
+ });
25
+ if (!res.ok)
26
+ throw new CanonApiError(res.status, await res.text());
27
+ return res.json();
28
+ }
29
+ async getAgentMe() {
30
+ const res = await fetch(`${this.baseUrl}/agents/me`, {
31
+ headers: this.authHeaders(),
32
+ });
33
+ if (!res.ok)
34
+ throw new CanonApiError(res.status, await res.text());
35
+ return res.json();
36
+ }
37
+ async getConversations() {
38
+ const res = await fetch(`${this.baseUrl}/conversations`, {
39
+ headers: this.authHeaders(),
40
+ });
41
+ if (!res.ok)
42
+ throw new CanonApiError(res.status, await res.text());
43
+ const data = await res.json();
44
+ return data.conversations;
45
+ }
46
+ async getMessages(conversationId, limit = 50, before) {
47
+ const params = new URLSearchParams({ limit: String(limit) });
48
+ if (before)
49
+ params.set('before', before);
50
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, { headers: this.authHeaders() });
51
+ if (!res.ok)
52
+ throw new CanonApiError(res.status, await res.text());
53
+ const data = await res.json();
54
+ return data.messages;
55
+ }
56
+ async sendMessage(conversationId, text, options) {
57
+ const res = await fetch(`${this.baseUrl}/messages/send`, {
58
+ method: 'POST',
59
+ headers: this.authHeaders(),
60
+ body: JSON.stringify({ conversationId, text, ...(options ?? {}) }),
61
+ });
62
+ if (!res.ok)
63
+ throw new CanonApiError(res.status, await res.text());
64
+ return res.json();
65
+ }
66
+ async createConversation(options) {
67
+ const res = await fetch(`${this.baseUrl}/conversations/create`, {
68
+ method: 'POST',
69
+ headers: this.authHeaders(),
70
+ body: JSON.stringify(options),
71
+ });
72
+ if (!res.ok)
73
+ throw new CanonApiError(res.status, await res.text());
74
+ return res.json();
75
+ }
76
+ async uploadMedia(conversationId, data, mimeType) {
77
+ const res = await fetch(`${this.baseUrl}/media/upload`, {
78
+ method: 'POST',
79
+ headers: this.authHeaders(),
80
+ body: JSON.stringify({ conversationId, mimeType, data }),
81
+ });
82
+ if (!res.ok)
83
+ throw new CanonApiError(res.status, await res.text());
84
+ return res.json();
85
+ }
86
+ async updateTopic(conversationId, topic) {
87
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/topic`, {
88
+ method: 'PATCH',
89
+ headers: this.authHeaders(),
90
+ body: JSON.stringify({ topic }),
91
+ });
92
+ if (!res.ok)
93
+ throw new CanonApiError(res.status, await res.text());
94
+ }
95
+ async deleteMessage(conversationId, messageId) {
96
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages/${messageId}`, {
97
+ method: 'DELETE',
98
+ headers: this.authHeaders(),
99
+ });
100
+ if (!res.ok)
101
+ throw new CanonApiError(res.status, await res.text());
102
+ }
103
+ async markAsRead(conversationId) {
104
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/read`, {
105
+ method: 'POST',
106
+ headers: this.authHeaders(),
107
+ });
108
+ if (!res.ok)
109
+ throw new CanonApiError(res.status, await res.text());
110
+ }
111
+ async leaveConversation(conversationId) {
112
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/leave`, {
113
+ method: 'POST',
114
+ headers: this.authHeaders(),
115
+ });
116
+ if (!res.ok)
117
+ throw new CanonApiError(res.status, await res.text());
118
+ }
119
+ async react(conversationId, messageId, emoji) {
120
+ const res = await fetch(`${this.baseUrl}/messages/react`, {
121
+ method: 'POST',
122
+ headers: this.authHeaders(),
123
+ body: JSON.stringify({ conversationId, messageId, emoji }),
124
+ });
125
+ if (!res.ok)
126
+ throw new CanonApiError(res.status, await res.text());
127
+ }
128
+ async updateConversationName(conversationId, name) {
129
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/name`, {
130
+ method: 'PATCH',
131
+ headers: this.authHeaders(),
132
+ body: JSON.stringify({ name }),
133
+ });
134
+ if (!res.ok)
135
+ throw new CanonApiError(res.status, await res.text());
136
+ }
137
+ async addMember(conversationId, userId) {
138
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/members`, {
139
+ method: 'POST',
140
+ headers: this.authHeaders(),
141
+ body: JSON.stringify({ userId }),
142
+ });
143
+ if (!res.ok)
144
+ throw new CanonApiError(res.status, await res.text());
145
+ }
146
+ async removeMember(conversationId, userId) {
147
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/members/${userId}`, {
148
+ method: 'DELETE',
149
+ headers: this.authHeaders(),
150
+ });
151
+ if (!res.ok)
152
+ throw new CanonApiError(res.status, await res.text());
153
+ }
154
+ async setStreaming(options) {
155
+ const res = await fetch(`${this.baseUrl}/streaming`, {
156
+ method: 'POST',
157
+ headers: this.authHeaders(),
158
+ body: JSON.stringify(options),
159
+ });
160
+ if (!res.ok)
161
+ throw new CanonApiError(res.status, await res.text());
162
+ }
163
+ async clearStreaming(conversationId) {
164
+ const res = await fetch(`${this.baseUrl}/streaming`, {
165
+ method: 'POST',
166
+ headers: this.authHeaders(),
167
+ body: JSON.stringify({ conversationId, streaming: false }),
168
+ });
169
+ if (!res.ok)
170
+ throw new CanonApiError(res.status, await res.text());
171
+ }
172
+ async setTyping(conversationId, typing, status) {
173
+ const res = await fetch(`${this.baseUrl}/typing`, {
174
+ method: 'POST',
175
+ headers: this.authHeaders(),
176
+ body: JSON.stringify({ conversationId, typing, ...(status ? { status } : {}) }),
177
+ });
178
+ if (!res.ok)
179
+ throw new CanonApiError(res.status, await res.text());
180
+ }
181
+ // ── Static unauthenticated registration endpoints ────────────────────
182
+ static async register(baseUrl, body) {
183
+ const url = baseUrl || DEFAULT_BASE_URL;
184
+ const res = await fetch(`${url}/agents/register`, {
185
+ method: 'POST',
186
+ headers: { 'Content-Type': 'application/json' },
187
+ body: JSON.stringify(body),
188
+ });
189
+ if (!res.ok)
190
+ throw new CanonApiError(res.status, await res.text());
191
+ return res.json();
192
+ }
193
+ static async checkStatus(baseUrl, requestId) {
194
+ const url = baseUrl || DEFAULT_BASE_URL;
195
+ const res = await fetch(`${url}/agents/status/${requestId}`);
196
+ if (!res.ok)
197
+ throw new CanonApiError(res.status, await res.text());
198
+ return res.json();
199
+ }
200
+ }
201
+ export class CanonApiError extends Error {
202
+ status;
203
+ constructor(status, body) {
204
+ super(`Canon API error ${status}: ${body}`);
205
+ this.name = 'CanonApiError';
206
+ this.status = status;
207
+ }
208
+ }
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_BASE_URL = "https://api-6m6mlelskq-uc.a.run.app";
2
+ export declare const DEFAULT_STREAM_URL = "https://canon-agent-stream-195218560334.us-central1.run.app";
3
+ export declare const DEFAULT_RTDB_URL = "https://project-007a8e71-ba01-49e6-8aa-default-rtdb.firebaseio.com";
4
+ export declare const FIREBASE_WEB_API_KEY = "AIzaSyA2_NLI-WzblGsEDB2Qv8053dv7EdGE4lE";
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_BASE_URL = 'https://api-6m6mlelskq-uc.a.run.app';
2
+ export const DEFAULT_STREAM_URL = 'https://canon-agent-stream-195218560334.us-central1.run.app';
3
+ export const DEFAULT_RTDB_URL = 'https://project-007a8e71-ba01-49e6-8aa-default-rtdb.firebaseio.com';
4
+ export const FIREBASE_WEB_API_KEY = 'AIzaSyA2_NLI-WzblGsEDB2Qv8053dv7EdGE4lE';
@@ -0,0 +1,19 @@
1
+ export { AGENT_CAPABILITIES, } from './types.js';
2
+ export type { AgentCapabilities, AgentClientType, CanonMessage, CanonConversation, AgentContext, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, WorkspaceOption, } from './types.js';
3
+ export { CanonClient, CanonApiError } from './client.js';
4
+ export { CanonStream } from './stream.js';
5
+ export type { StreamHandler } from './stream.js';
6
+ export { registerAndWaitForApproval } from './registration.js';
7
+ export { ApprovalManager } from './approval-manager.js';
8
+ export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
9
+ export { DEFAULT_APPROVAL_CONFIG, } from './approval-types.js';
10
+ export type { ApprovalRequestMetadata, ApprovalReplyMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
11
+ export { createStreamingHelper } from './streaming.js';
12
+ export type { RTDBHandle, RTDBRef, ServerTimestamp, StreamingHelperOptions, StreamingNode } from './streaming.js';
13
+ export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
14
+ export type { AgentProfile } from './agent-profiles.js';
15
+ export { resolveCanonAgent, getActiveProfile } from './agent-resolver.js';
16
+ export type { ResolvedAgent } from './agent-resolver.js';
17
+ export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState } from './rtdb-rest.js';
18
+ export type { SessionStatePayload } from './rtdb-rest.js';
19
+ export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ // Types
2
+ export { AGENT_CAPABILITIES, } from './types.js';
3
+ // Client
4
+ export { CanonClient, CanonApiError } from './client.js';
5
+ // Stream
6
+ export { CanonStream } from './stream.js';
7
+ // Registration
8
+ export { registerAndWaitForApproval } from './registration.js';
9
+ // Approval
10
+ export { ApprovalManager } from './approval-manager.js';
11
+ export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
12
+ export { DEFAULT_APPROVAL_CONFIG, } from './approval-types.js';
13
+ // Streaming (RTDB helpers)
14
+ export { createStreamingHelper } from './streaming.js';
15
+ // Agent profiles (loading, locking, resolution)
16
+ export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
17
+ // Agent resolver
18
+ export { resolveCanonAgent, getActiveProfile } from './agent-resolver.js';
19
+ // RTDB REST helpers (token exchange, session state, generic read/write)
20
+ export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState } from './rtdb-rest.js';
21
+ // Constants
22
+ export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
@@ -0,0 +1,13 @@
1
+ import type { RegistrationInput, RegistrationResult, RegistrationStatus } from './types.js';
2
+ /**
3
+ * Registers a new Canon agent and polls for owner approval.
4
+ *
5
+ * Flow:
6
+ * 1. POST /agents/register -> returns requestId
7
+ * 2. Poll GET /agents/status/:requestId until approved/rejected/timeout
8
+ * 3. On approval, returns the API key
9
+ */
10
+ export declare function registerAndWaitForApproval(input: RegistrationInput, callbacks?: {
11
+ onSubmitted?: (requestId: string) => void;
12
+ onPollUpdate?: (status: RegistrationStatus) => void;
13
+ }): Promise<RegistrationResult>;
@@ -0,0 +1,46 @@
1
+ import { CanonClient } from './client.js';
2
+ const POLL_INTERVAL_MS = 3_000;
3
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
4
+ /**
5
+ * Registers a new Canon agent and polls for owner approval.
6
+ *
7
+ * Flow:
8
+ * 1. POST /agents/register -> returns requestId
9
+ * 2. Poll GET /agents/status/:requestId until approved/rejected/timeout
10
+ * 3. On approval, returns the API key
11
+ */
12
+ export async function registerAndWaitForApproval(input, callbacks) {
13
+ const { requestId } = await CanonClient.register(input.baseUrl, {
14
+ name: input.name,
15
+ description: input.description,
16
+ ownerPhone: input.ownerPhone,
17
+ developerInfo: input.developerInfo || 'Canon agent',
18
+ avatarUrl: input.avatarUrl,
19
+ clientType: input.clientType,
20
+ });
21
+ callbacks?.onSubmitted?.(requestId);
22
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
23
+ while (Date.now() < deadline) {
24
+ await sleep(POLL_INTERVAL_MS);
25
+ const result = await CanonClient.checkStatus(input.baseUrl, requestId);
26
+ callbacks?.onPollUpdate?.(result);
27
+ if (result.status === 'approved') {
28
+ return {
29
+ status: 'approved',
30
+ apiKey: result.apiKey,
31
+ agentId: result.agentId,
32
+ agentName: result.agentName,
33
+ };
34
+ }
35
+ if (result.status === 'rejected') {
36
+ return {
37
+ status: 'rejected',
38
+ agentName: result.agentName,
39
+ };
40
+ }
41
+ }
42
+ return { status: 'timeout' };
43
+ }
44
+ function sleep(ms) {
45
+ return new Promise((resolve) => setTimeout(resolve, ms));
46
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Lightweight RTDB REST helper for session state.
3
+ *
4
+ * Uses Firebase Realtime Database REST API to avoid adding firebase-admin
5
+ * as a dependency to the MCP plugin. The custom token from /agents/auth-token
6
+ * is exchanged for a Firebase ID token before use with RTDB REST.
7
+ */
8
+ import type { CanonClient } from './client.js';
9
+ export interface SessionStatePayload {
10
+ model?: string;
11
+ permissionMode?: string;
12
+ effort?: string;
13
+ cwd?: string;
14
+ hostMode?: boolean;
15
+ clientType?: string;
16
+ isActive: boolean;
17
+ state?: 'idle' | 'running' | 'requires_action';
18
+ contextUsage?: {
19
+ percentage: number;
20
+ totalTokens: number;
21
+ maxTokens: number;
22
+ };
23
+ availableModels?: Array<{
24
+ value: string;
25
+ label: string;
26
+ }>;
27
+ updatedAt: {
28
+ '.sv': 'timestamp';
29
+ };
30
+ }
31
+ /** Must be called once before any RTDB operations. */
32
+ export declare function initRTDBAuth(client: CanonClient): void;
33
+ /** Generic RTDB REST write (PUT). */
34
+ export declare function rtdbWrite(path: string, data: unknown): Promise<void>;
35
+ /** Generic RTDB REST read (GET). */
36
+ export declare function rtdbRead(path: string): Promise<unknown>;
37
+ /**
38
+ * Write session state to RTDB via REST API.
39
+ * Path: /session-state/{conversationId}/{agentId}
40
+ */
41
+ export declare function writeSessionState(conversationId: string, agentId: string, state: Omit<SessionStatePayload, 'updatedAt'>): Promise<void>;
42
+ /**
43
+ * Clear session state from RTDB (full overwrite with isActive: false).
44
+ */
45
+ export declare function clearSessionState(conversationId: string, agentId: string): Promise<void>;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Lightweight RTDB REST helper for session state.
3
+ *
4
+ * Uses Firebase Realtime Database REST API to avoid adding firebase-admin
5
+ * as a dependency to the MCP plugin. The custom token from /agents/auth-token
6
+ * is exchanged for a Firebase ID token before use with RTDB REST.
7
+ */
8
+ import { DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
9
+ const RTDB_BASE = process.env.CANON_RTDB_URL || DEFAULT_RTDB_URL;
10
+ const FIREBASE_API_KEY = process.env.CANON_FIREBASE_API_KEY || FIREBASE_WEB_API_KEY;
11
+ // ── Token management ──────────────────────────────────────────────────
12
+ let cachedIdToken = null;
13
+ let idTokenExpiresAt = 0;
14
+ let tokenClient = null;
15
+ /** Must be called once before any RTDB operations. */
16
+ export function initRTDBAuth(client) {
17
+ tokenClient = client;
18
+ }
19
+ /**
20
+ * Exchange a Firebase custom token for an ID token via the Identity Toolkit API.
21
+ */
22
+ async function exchangeCustomTokenForIdToken(customToken) {
23
+ const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`;
24
+ const res = await fetch(url, {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify({ token: customToken, returnSecureToken: true }),
28
+ });
29
+ if (!res.ok) {
30
+ const text = await res.text();
31
+ throw new Error(`Token exchange failed (${res.status}): ${text}`);
32
+ }
33
+ const data = await res.json();
34
+ return { idToken: data.idToken, expiresIn: parseInt(data.expiresIn, 10) };
35
+ }
36
+ /** Get a valid ID token for RTDB REST, refreshing if expired or expiring soon. */
37
+ async function getToken() {
38
+ // Refresh if missing, expired, or expiring within 5 minutes
39
+ if (!cachedIdToken || Date.now() > idTokenExpiresAt - 5 * 60 * 1000) {
40
+ if (!tokenClient)
41
+ return null;
42
+ try {
43
+ const auth = await tokenClient.getAuthToken();
44
+ const { idToken, expiresIn } = await exchangeCustomTokenForIdToken(auth.token);
45
+ cachedIdToken = idToken;
46
+ idTokenExpiresAt = Date.now() + expiresIn * 1000;
47
+ }
48
+ catch (err) {
49
+ console.error('[canon] RTDB token refresh failed:', err);
50
+ return cachedIdToken; // Return stale token as fallback
51
+ }
52
+ }
53
+ return cachedIdToken;
54
+ }
55
+ // ── RTDB operations ───────────────────────────────────────────────────
56
+ /** Generic RTDB REST write (PUT). */
57
+ export async function rtdbWrite(path, data) {
58
+ const token = await getToken();
59
+ if (!token)
60
+ return;
61
+ const url = `${RTDB_BASE}${path}.json?auth=${token}`;
62
+ const res = await fetch(url, {
63
+ method: 'PUT',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify(data),
66
+ });
67
+ if (!res.ok) {
68
+ const text = await res.text();
69
+ throw new Error(`RTDB write failed (${res.status}): ${text}`);
70
+ }
71
+ }
72
+ /** Generic RTDB REST read (GET). */
73
+ export async function rtdbRead(path) {
74
+ const token = await getToken();
75
+ if (!token)
76
+ return null;
77
+ const url = `${RTDB_BASE}${path}.json?auth=${token}`;
78
+ const res = await fetch(url);
79
+ if (!res.ok)
80
+ return null;
81
+ return res.json();
82
+ }
83
+ /**
84
+ * Write session state to RTDB via REST API.
85
+ * Path: /session-state/{conversationId}/{agentId}
86
+ */
87
+ export async function writeSessionState(conversationId, agentId, state) {
88
+ const token = await getToken();
89
+ if (!token)
90
+ return;
91
+ const url = `${RTDB_BASE}/session-state/${conversationId}/${agentId}.json?auth=${token}`;
92
+ const body = {
93
+ ...state,
94
+ updatedAt: { '.sv': 'timestamp' },
95
+ };
96
+ const res = await fetch(url, {
97
+ method: 'PUT',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ body: JSON.stringify(body),
100
+ });
101
+ if (!res.ok) {
102
+ const text = await res.text();
103
+ throw new Error(`RTDB write failed (${res.status}): ${text}`);
104
+ }
105
+ }
106
+ /**
107
+ * Clear session state from RTDB (full overwrite with isActive: false).
108
+ */
109
+ export async function clearSessionState(conversationId, agentId) {
110
+ const token = await getToken();
111
+ if (!token)
112
+ return;
113
+ const url = `${RTDB_BASE}/session-state/${conversationId}/${agentId}.json?auth=${token}`;
114
+ const body = {
115
+ isActive: false,
116
+ updatedAt: { '.sv': 'timestamp' },
117
+ };
118
+ // Use PUT (not PATCH) to clear stale fields like model/cwd
119
+ const res = await fetch(url, {
120
+ method: 'PUT',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify(body),
123
+ });
124
+ if (!res.ok) {
125
+ const text = await res.text();
126
+ console.error(`[canon] RTDB clear failed (${res.status}): ${text}`);
127
+ }
128
+ }
@@ -0,0 +1,54 @@
1
+ import type { AgentContext, MessageCreatedPayload, TypingPayload, PresencePayload } from './types.js';
2
+ export type StreamHandler = {
3
+ onMessage: (payload: MessageCreatedPayload) => void;
4
+ onAgentContext?: (ctx: AgentContext) => void;
5
+ onTyping?: (data: TypingPayload) => void;
6
+ onPresence?: (data: PresencePayload) => void;
7
+ onMessageDeleted?: (payload: {
8
+ conversationId: string;
9
+ messageId: string;
10
+ }) => void;
11
+ onConversationUpdated?: (payload: {
12
+ conversationId: string;
13
+ changes: Record<string, unknown>;
14
+ }) => void;
15
+ onConnected?: () => void;
16
+ onDisconnected?: () => void;
17
+ onError?: (error: Error) => void;
18
+ };
19
+ /**
20
+ * Manages a persistent SSE connection to Canon's stream service.
21
+ *
22
+ * Uses native fetch + ReadableStream (Node 18+) — no EventSource polyfill.
23
+ * Supports Last-Event-ID for gapless replay on reconnect.
24
+ */
25
+ export declare class CanonStream {
26
+ private apiKey;
27
+ private agentId;
28
+ private streamUrl;
29
+ private handler;
30
+ private running;
31
+ private abortController;
32
+ private lastEventId;
33
+ private reconnectAttempt;
34
+ private reconnectTimer;
35
+ constructor(opts: {
36
+ apiKey: string;
37
+ agentId: string;
38
+ streamUrl?: string;
39
+ handler: StreamHandler;
40
+ });
41
+ start(): Promise<void>;
42
+ stop(): void;
43
+ isRunning(): boolean;
44
+ private connect;
45
+ private readStream;
46
+ private processFrame;
47
+ private handleAgentContext;
48
+ private handleMessageCreated;
49
+ private handleTyping;
50
+ private handlePresence;
51
+ private handleMessageDeleted;
52
+ private handleConversationUpdated;
53
+ private scheduleReconnect;
54
+ }