@canonapp/core 0.1.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/dist/client.d.ts +51 -0
- package/dist/client.js +190 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/registration.d.ts +13 -0
- package/dist/registration.js +45 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +220 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.js +2 -0
- package/package.json +43 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type CanonMessage, type CanonConversation, type AgentContext, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus } 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
|
+
setTyping(conversationId: string, typing: boolean): Promise<void>;
|
|
37
|
+
static register(baseUrl: string | undefined, body: {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
ownerPhone: string;
|
|
41
|
+
developerInfo: string;
|
|
42
|
+
avatarUrl?: string;
|
|
43
|
+
}): Promise<{
|
|
44
|
+
requestId: string;
|
|
45
|
+
}>;
|
|
46
|
+
static checkStatus(baseUrl: string | undefined, requestId: string): Promise<RegistrationStatus>;
|
|
47
|
+
}
|
|
48
|
+
export declare class CanonApiError extends Error {
|
|
49
|
+
status: number;
|
|
50
|
+
constructor(status: number, body: string);
|
|
51
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
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 setTyping(conversationId, typing) {
|
|
155
|
+
const res = await fetch(`${this.baseUrl}/typing`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: this.authHeaders(),
|
|
158
|
+
body: JSON.stringify({ conversationId, typing }),
|
|
159
|
+
});
|
|
160
|
+
if (!res.ok)
|
|
161
|
+
throw new CanonApiError(res.status, await res.text());
|
|
162
|
+
}
|
|
163
|
+
// ── Static unauthenticated registration endpoints ────────────────────
|
|
164
|
+
static async register(baseUrl, body) {
|
|
165
|
+
const url = baseUrl || DEFAULT_BASE_URL;
|
|
166
|
+
const res = await fetch(`${url}/agents/register`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
169
|
+
body: JSON.stringify(body),
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok)
|
|
172
|
+
throw new CanonApiError(res.status, await res.text());
|
|
173
|
+
return res.json();
|
|
174
|
+
}
|
|
175
|
+
static async checkStatus(baseUrl, requestId) {
|
|
176
|
+
const url = baseUrl || DEFAULT_BASE_URL;
|
|
177
|
+
const res = await fetch(`${url}/agents/status/${requestId}`);
|
|
178
|
+
if (!res.ok)
|
|
179
|
+
throw new CanonApiError(res.status, await res.text());
|
|
180
|
+
return res.json();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export class CanonApiError extends Error {
|
|
184
|
+
status;
|
|
185
|
+
constructor(status, body) {
|
|
186
|
+
super(`Canon API error ${status}: ${body}`);
|
|
187
|
+
this.name = 'CanonApiError';
|
|
188
|
+
this.status = status;
|
|
189
|
+
}
|
|
190
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { CanonMessage, CanonConversation, AgentContext, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, } from './types.js';
|
|
2
|
+
export { CanonClient, CanonApiError } from './client.js';
|
|
3
|
+
export { CanonStream } from './stream.js';
|
|
4
|
+
export type { StreamHandler } from './stream.js';
|
|
5
|
+
export { registerAndWaitForApproval } from './registration.js';
|
|
6
|
+
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL } from './constants.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Client
|
|
2
|
+
export { CanonClient, CanonApiError } from './client.js';
|
|
3
|
+
// Stream
|
|
4
|
+
export { CanonStream } from './stream.js';
|
|
5
|
+
// Registration
|
|
6
|
+
export { registerAndWaitForApproval } from './registration.js';
|
|
7
|
+
// Constants
|
|
8
|
+
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL } 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,45 @@
|
|
|
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
|
+
});
|
|
20
|
+
callbacks?.onSubmitted?.(requestId);
|
|
21
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
22
|
+
while (Date.now() < deadline) {
|
|
23
|
+
await sleep(POLL_INTERVAL_MS);
|
|
24
|
+
const result = await CanonClient.checkStatus(input.baseUrl, requestId);
|
|
25
|
+
callbacks?.onPollUpdate?.(result);
|
|
26
|
+
if (result.status === 'approved') {
|
|
27
|
+
return {
|
|
28
|
+
status: 'approved',
|
|
29
|
+
apiKey: result.apiKey,
|
|
30
|
+
agentId: result.agentId,
|
|
31
|
+
agentName: result.agentName,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (result.status === 'rejected') {
|
|
35
|
+
return {
|
|
36
|
+
status: 'rejected',
|
|
37
|
+
agentName: result.agentName,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { status: 'timeout' };
|
|
42
|
+
}
|
|
43
|
+
function sleep(ms) {
|
|
44
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
+
}
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
onConnected?: () => void;
|
|
8
|
+
onDisconnected?: () => void;
|
|
9
|
+
onError?: (error: Error) => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Manages a persistent SSE connection to Canon's stream service.
|
|
13
|
+
*
|
|
14
|
+
* Uses native fetch + ReadableStream (Node 18+) — no EventSource polyfill.
|
|
15
|
+
* Supports Last-Event-ID for gapless replay on reconnect.
|
|
16
|
+
*/
|
|
17
|
+
export declare class CanonStream {
|
|
18
|
+
private apiKey;
|
|
19
|
+
private agentId;
|
|
20
|
+
private streamUrl;
|
|
21
|
+
private handler;
|
|
22
|
+
private running;
|
|
23
|
+
private abortController;
|
|
24
|
+
private lastEventId;
|
|
25
|
+
private reconnectAttempt;
|
|
26
|
+
private reconnectTimer;
|
|
27
|
+
constructor(opts: {
|
|
28
|
+
apiKey: string;
|
|
29
|
+
agentId: string;
|
|
30
|
+
streamUrl?: string;
|
|
31
|
+
handler: StreamHandler;
|
|
32
|
+
});
|
|
33
|
+
start(): Promise<void>;
|
|
34
|
+
stop(): void;
|
|
35
|
+
isRunning(): boolean;
|
|
36
|
+
private connect;
|
|
37
|
+
private readStream;
|
|
38
|
+
private processFrame;
|
|
39
|
+
private handleAgentContext;
|
|
40
|
+
private handleMessageCreated;
|
|
41
|
+
private handleTyping;
|
|
42
|
+
private handlePresence;
|
|
43
|
+
private scheduleReconnect;
|
|
44
|
+
}
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { DEFAULT_STREAM_URL } from './constants.js';
|
|
2
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
3
|
+
/**
|
|
4
|
+
* Manages a persistent SSE connection to Canon's stream service.
|
|
5
|
+
*
|
|
6
|
+
* Uses native fetch + ReadableStream (Node 18+) — no EventSource polyfill.
|
|
7
|
+
* Supports Last-Event-ID for gapless replay on reconnect.
|
|
8
|
+
*/
|
|
9
|
+
export class CanonStream {
|
|
10
|
+
apiKey;
|
|
11
|
+
agentId;
|
|
12
|
+
streamUrl;
|
|
13
|
+
handler;
|
|
14
|
+
running = false;
|
|
15
|
+
abortController = null;
|
|
16
|
+
lastEventId = null;
|
|
17
|
+
reconnectAttempt = 0;
|
|
18
|
+
reconnectTimer = null;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
this.apiKey = opts.apiKey;
|
|
21
|
+
this.agentId = opts.agentId;
|
|
22
|
+
this.streamUrl = opts.streamUrl || DEFAULT_STREAM_URL;
|
|
23
|
+
this.handler = opts.handler;
|
|
24
|
+
}
|
|
25
|
+
async start() {
|
|
26
|
+
this.running = true;
|
|
27
|
+
this.reconnectAttempt = 0;
|
|
28
|
+
await this.connect();
|
|
29
|
+
}
|
|
30
|
+
stop() {
|
|
31
|
+
this.running = false;
|
|
32
|
+
if (this.reconnectTimer) {
|
|
33
|
+
clearTimeout(this.reconnectTimer);
|
|
34
|
+
this.reconnectTimer = null;
|
|
35
|
+
}
|
|
36
|
+
if (this.abortController) {
|
|
37
|
+
this.abortController.abort();
|
|
38
|
+
this.abortController = null;
|
|
39
|
+
}
|
|
40
|
+
this.handler.onDisconnected?.();
|
|
41
|
+
}
|
|
42
|
+
isRunning() {
|
|
43
|
+
return this.running;
|
|
44
|
+
}
|
|
45
|
+
// ── SSE connection ────────────────────────────────────────────────────
|
|
46
|
+
async connect() {
|
|
47
|
+
if (!this.running)
|
|
48
|
+
return;
|
|
49
|
+
this.abortController = new AbortController();
|
|
50
|
+
const headers = {
|
|
51
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
52
|
+
Accept: 'text/event-stream',
|
|
53
|
+
};
|
|
54
|
+
if (this.lastEventId) {
|
|
55
|
+
headers['Last-Event-ID'] = this.lastEventId;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(`${this.streamUrl}/agents/stream`, {
|
|
59
|
+
headers,
|
|
60
|
+
signal: this.abortController.signal,
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(`SSE connect failed: ${res.status} ${res.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
this.handler.onConnected?.();
|
|
66
|
+
await this.readStream(res);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (!this.running)
|
|
70
|
+
return;
|
|
71
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
72
|
+
if (msg.includes('abort'))
|
|
73
|
+
return;
|
|
74
|
+
this.handler.onError?.(err instanceof Error ? err : new Error(msg));
|
|
75
|
+
this.scheduleReconnect();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async readStream(res) {
|
|
79
|
+
const reader = res.body.getReader();
|
|
80
|
+
const decoder = new TextDecoder();
|
|
81
|
+
let buffer = '';
|
|
82
|
+
try {
|
|
83
|
+
while (this.running) {
|
|
84
|
+
const { value, done } = await reader.read();
|
|
85
|
+
if (done)
|
|
86
|
+
break;
|
|
87
|
+
buffer += decoder.decode(value, { stream: true });
|
|
88
|
+
let boundary;
|
|
89
|
+
while ((boundary = buffer.indexOf('\n\n')) !== -1) {
|
|
90
|
+
const frame = buffer.slice(0, boundary);
|
|
91
|
+
buffer = buffer.slice(boundary + 2);
|
|
92
|
+
this.processFrame(frame);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (!this.running)
|
|
98
|
+
return;
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
if (msg.includes('abort'))
|
|
101
|
+
return;
|
|
102
|
+
this.handler.onError?.(err instanceof Error ? err : new Error(msg));
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
reader.releaseLock();
|
|
106
|
+
}
|
|
107
|
+
if (this.running) {
|
|
108
|
+
this.handler.onDisconnected?.();
|
|
109
|
+
this.scheduleReconnect();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ── SSE frame parsing ─────────────────────────────────────────────────
|
|
113
|
+
processFrame(frame) {
|
|
114
|
+
let id = null;
|
|
115
|
+
let event = null;
|
|
116
|
+
let data = '';
|
|
117
|
+
for (const line of frame.split('\n')) {
|
|
118
|
+
if (line.startsWith('id:')) {
|
|
119
|
+
id = line.slice(3).trim();
|
|
120
|
+
}
|
|
121
|
+
else if (line.startsWith('event:')) {
|
|
122
|
+
event = line.slice(6).trim();
|
|
123
|
+
}
|
|
124
|
+
else if (line.startsWith('data:')) {
|
|
125
|
+
data += (data ? '\n' : '') + line.slice(5).trim();
|
|
126
|
+
}
|
|
127
|
+
// Lines starting with ':' are comments — ignore
|
|
128
|
+
}
|
|
129
|
+
if (id) {
|
|
130
|
+
this.lastEventId = id;
|
|
131
|
+
}
|
|
132
|
+
if (!event || !data)
|
|
133
|
+
return;
|
|
134
|
+
switch (event) {
|
|
135
|
+
case 'agent.context':
|
|
136
|
+
this.reconnectAttempt = 0;
|
|
137
|
+
this.handleAgentContext(data);
|
|
138
|
+
break;
|
|
139
|
+
case 'message.created':
|
|
140
|
+
this.reconnectAttempt = 0;
|
|
141
|
+
this.handleMessageCreated(data);
|
|
142
|
+
break;
|
|
143
|
+
case 'typing':
|
|
144
|
+
this.reconnectAttempt = 0;
|
|
145
|
+
this.handleTyping(data);
|
|
146
|
+
break;
|
|
147
|
+
case 'presence':
|
|
148
|
+
this.reconnectAttempt = 0;
|
|
149
|
+
this.handlePresence(data);
|
|
150
|
+
break;
|
|
151
|
+
case 'heartbeat':
|
|
152
|
+
case 'conversation.updated':
|
|
153
|
+
case 'message.deleted':
|
|
154
|
+
this.reconnectAttempt = 0;
|
|
155
|
+
break;
|
|
156
|
+
case 'replay.expired':
|
|
157
|
+
this.reconnectAttempt = 0;
|
|
158
|
+
this.handler.onError?.(new Error('Replay expired — some messages may have been missed'));
|
|
159
|
+
break;
|
|
160
|
+
case 'error':
|
|
161
|
+
// Don't reset backoff — error events mean something is wrong
|
|
162
|
+
this.handler.onError?.(new Error(`Stream error: ${data}`));
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
handleAgentContext(raw) {
|
|
169
|
+
try {
|
|
170
|
+
const ctx = JSON.parse(raw);
|
|
171
|
+
this.handler.onAgentContext?.(ctx);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
this.handler.onError?.(new Error(`Failed to parse agent.context: ${err}`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
handleMessageCreated(raw) {
|
|
178
|
+
try {
|
|
179
|
+
const payload = JSON.parse(raw);
|
|
180
|
+
// Skip own messages
|
|
181
|
+
if (payload.message.senderId === this.agentId)
|
|
182
|
+
return;
|
|
183
|
+
this.handler.onMessage(payload);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
this.handler.onError?.(new Error(`Failed to parse message.created: ${err}`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
handleTyping(raw) {
|
|
190
|
+
try {
|
|
191
|
+
const data = JSON.parse(raw);
|
|
192
|
+
this.handler.onTyping?.(data);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Ignore parse errors for non-critical events
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
handlePresence(raw) {
|
|
199
|
+
try {
|
|
200
|
+
const data = JSON.parse(raw);
|
|
201
|
+
this.handler.onPresence?.(data);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Ignore parse errors for non-critical events
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ── Reconnect with exponential backoff + jitter ───────────────────────
|
|
208
|
+
scheduleReconnect() {
|
|
209
|
+
if (!this.running)
|
|
210
|
+
return;
|
|
211
|
+
const base = Math.min(1000 * Math.pow(2, this.reconnectAttempt), MAX_BACKOFF_MS);
|
|
212
|
+
const jitter = Math.random() * 0.25 * base;
|
|
213
|
+
const delay = base + jitter;
|
|
214
|
+
this.reconnectAttempt++;
|
|
215
|
+
this.reconnectTimer = setTimeout(() => {
|
|
216
|
+
this.reconnectTimer = null;
|
|
217
|
+
this.connect();
|
|
218
|
+
}, delay);
|
|
219
|
+
}
|
|
220
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export interface CanonMessage {
|
|
2
|
+
id: string;
|
|
3
|
+
senderId: string;
|
|
4
|
+
senderType: 'human' | 'ai_agent';
|
|
5
|
+
/** Whether the sender is this agent's owner (server-computed, trusted) */
|
|
6
|
+
isOwner: boolean;
|
|
7
|
+
contentType: 'text' | 'image' | 'audio';
|
|
8
|
+
text: string | null;
|
|
9
|
+
imageUrl: string | null;
|
|
10
|
+
audioUrl: string | null;
|
|
11
|
+
audioDurationMs: number | null;
|
|
12
|
+
mentions: string[];
|
|
13
|
+
replyTo: string | null;
|
|
14
|
+
replyToPosition: number | null;
|
|
15
|
+
status: 'sent' | 'read';
|
|
16
|
+
deleted: boolean;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CanonConversation {
|
|
20
|
+
id: string;
|
|
21
|
+
type: 'direct' | 'group';
|
|
22
|
+
name: string | null;
|
|
23
|
+
topic: string | null;
|
|
24
|
+
memberIds: string[];
|
|
25
|
+
isAgentChat: boolean;
|
|
26
|
+
lastMessage: {
|
|
27
|
+
text: string;
|
|
28
|
+
senderId: string;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
} | null;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
}
|
|
33
|
+
/** Trusted agent identity & access context, provided by the server */
|
|
34
|
+
export interface AgentContext {
|
|
35
|
+
agentId: string;
|
|
36
|
+
displayName?: string;
|
|
37
|
+
avatarUrl?: string | null;
|
|
38
|
+
description?: string | null;
|
|
39
|
+
ownerId: string;
|
|
40
|
+
ownerName: string;
|
|
41
|
+
accessLevel: 'private' | 'restricted' | 'open';
|
|
42
|
+
allowedUserIds: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface MessageCreatedPayload {
|
|
45
|
+
conversationId: string;
|
|
46
|
+
message: {
|
|
47
|
+
id: string;
|
|
48
|
+
senderId: string;
|
|
49
|
+
senderName?: string;
|
|
50
|
+
senderType?: 'human' | 'ai_agent';
|
|
51
|
+
/** Whether the sender is this agent's owner (server-computed, trusted) */
|
|
52
|
+
isOwner?: boolean;
|
|
53
|
+
text?: string;
|
|
54
|
+
contentType?: 'text' | 'image' | 'audio';
|
|
55
|
+
imageUrl?: string;
|
|
56
|
+
audioUrl?: string;
|
|
57
|
+
audioDurationMs?: number;
|
|
58
|
+
replyTo?: string;
|
|
59
|
+
replyToPosition?: number;
|
|
60
|
+
mentions?: string[];
|
|
61
|
+
createdAt?: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export interface TypingPayload {
|
|
65
|
+
conversationId: string;
|
|
66
|
+
userId: string;
|
|
67
|
+
typing: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface PresencePayload {
|
|
70
|
+
userId: string;
|
|
71
|
+
online: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface SendMessageOptions {
|
|
74
|
+
contentType?: 'text' | 'audio' | 'image';
|
|
75
|
+
replyTo?: string;
|
|
76
|
+
replyToPosition?: number;
|
|
77
|
+
audioUrl?: string;
|
|
78
|
+
audioDurationMs?: number;
|
|
79
|
+
imageUrl?: string;
|
|
80
|
+
mentions?: string[];
|
|
81
|
+
}
|
|
82
|
+
export interface CreateConversationOptions {
|
|
83
|
+
type: 'direct' | 'group';
|
|
84
|
+
targetUserId?: string;
|
|
85
|
+
memberIds?: string[];
|
|
86
|
+
name?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface RegistrationInput {
|
|
89
|
+
name: string;
|
|
90
|
+
description: string;
|
|
91
|
+
ownerPhone: string;
|
|
92
|
+
developerInfo?: string;
|
|
93
|
+
avatarUrl?: string;
|
|
94
|
+
baseUrl?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface RegistrationResult {
|
|
97
|
+
status: 'approved' | 'rejected' | 'timeout';
|
|
98
|
+
apiKey?: string;
|
|
99
|
+
agentId?: string;
|
|
100
|
+
agentName?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface RegistrationStatus {
|
|
103
|
+
status: 'pending' | 'approved' | 'rejected';
|
|
104
|
+
agentName: string;
|
|
105
|
+
agentId?: string;
|
|
106
|
+
apiKey?: string;
|
|
107
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@canonapp/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Canon core — shared types, REST client, SSE stream, and registration for Canon messaging",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"canon",
|
|
27
|
+
"messaging",
|
|
28
|
+
"ai-agents",
|
|
29
|
+
"sse",
|
|
30
|
+
"rest-client"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/HeyBobChan/canon",
|
|
35
|
+
"directory": "packages/core"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/core",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"typescript": "~5.7.0"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT"
|
|
43
|
+
}
|