spectrum-ts 0.0.1 → 0.1.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,418 @@
1
+ import {
2
+ mergeStreams,
3
+ stream
4
+ } from "../../chunk-3TBRO2J7.js";
5
+ import {
6
+ definePlatform
7
+ } from "../../chunk-UZ2CXPOD.js";
8
+
9
+ // src/providers/imessage/index.ts
10
+ import { createClient as createClient2, directChat } from "@photon-ai/advanced-imessage";
11
+ import { IMessageSDK as IMessageSDK2 } from "@photon-ai/imessage-kit";
12
+
13
+ // src/providers/imessage/auth.ts
14
+ import {
15
+ createClient
16
+ } from "@photon-ai/advanced-imessage";
17
+
18
+ // src/utils/cloud.ts
19
+ var SPECTRUM_CLOUD_URL = `https://${process.env.SPECTRUM_CLOUD_URL ?? "spectrum-cloud.photon.codes"}`;
20
+
21
+ // src/providers/imessage/auth.ts
22
+ var RENEWAL_RATIO = 0.8;
23
+ var EXPIRY_BUFFER_MS = 3e4;
24
+ var RETRY_DELAY_MS = 3e4;
25
+ var cloudAuthState = /* @__PURE__ */ new WeakMap();
26
+ async function fetchTokens(projectId, projectSecret) {
27
+ const url = `${SPECTRUM_CLOUD_URL}/${projectId}/imessage/tokens`;
28
+ const credentials = btoa(`${projectId}:${projectSecret}`);
29
+ const response = await fetch(url, {
30
+ method: "POST",
31
+ headers: {
32
+ Authorization: `Basic ${credentials}`
33
+ }
34
+ });
35
+ if (!response.ok) {
36
+ const body = await response.text().catch(() => "");
37
+ throw new Error(
38
+ `Spectrum Cloud authentication failed (${response.status}): ${body || response.statusText}`
39
+ );
40
+ }
41
+ const json = await response.json();
42
+ if (!json.succeed) {
43
+ throw new Error(
44
+ "Spectrum Cloud authentication failed: server returned succeed=false"
45
+ );
46
+ }
47
+ return json.data;
48
+ }
49
+ async function createCloudClients(projectId, projectSecret) {
50
+ let tokenData = await fetchTokens(projectId, projectSecret);
51
+ let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
52
+ let disposed = false;
53
+ let renewalTimer;
54
+ const scheduleRenewal = () => {
55
+ if (disposed) {
56
+ return;
57
+ }
58
+ const ttlMs = tokenData.expiresIn * 1e3;
59
+ const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
60
+ renewalTimer = setTimeout(async () => {
61
+ try {
62
+ tokenData = await fetchTokens(projectId, projectSecret);
63
+ tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
64
+ scheduleRenewal();
65
+ } catch {
66
+ renewalTimer = setTimeout(() => scheduleRenewal(), RETRY_DELAY_MS);
67
+ renewalTimer?.unref?.();
68
+ }
69
+ }, renewInMs);
70
+ renewalTimer?.unref?.();
71
+ };
72
+ scheduleRenewal();
73
+ const refreshIfNeeded = async () => {
74
+ if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) {
75
+ return;
76
+ }
77
+ tokenData = await fetchTokens(projectId, projectSecret);
78
+ tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
79
+ scheduleRenewal();
80
+ };
81
+ const buildClients = () => {
82
+ if (tokenData.type === "shared") {
83
+ const address = process.env.SPECTRUM_IMESSAGE_ADDRESS ?? "spectrum-imessage.photon.codes:443";
84
+ return [
85
+ createClient({
86
+ address,
87
+ tls: true,
88
+ token: async () => {
89
+ await refreshIfNeeded();
90
+ return tokenData.token;
91
+ }
92
+ })
93
+ ];
94
+ }
95
+ return Object.entries(tokenData.auth).map(
96
+ ([instanceId, token]) => createClient({
97
+ address: `${instanceId}.imsg.photon.codes:443`,
98
+ tls: true,
99
+ token: async () => {
100
+ await refreshIfNeeded();
101
+ const data = tokenData;
102
+ return data.auth[instanceId] ?? token;
103
+ }
104
+ })
105
+ );
106
+ };
107
+ const clients = buildClients();
108
+ cloudAuthState.set(clients, {
109
+ dispose: () => {
110
+ disposed = true;
111
+ if (renewalTimer !== void 0) {
112
+ clearTimeout(renewalTimer);
113
+ renewalTimer = void 0;
114
+ }
115
+ }
116
+ });
117
+ return clients;
118
+ }
119
+ async function disposeCloudAuth(clients) {
120
+ const auth = cloudAuthState.get(clients);
121
+ if (auth) {
122
+ auth.dispose();
123
+ cloudAuthState.delete(clients);
124
+ }
125
+ }
126
+
127
+ // src/providers/imessage/local.ts
128
+ import { unlink, writeFile } from "fs/promises";
129
+ import { tmpdir } from "os";
130
+ import { join } from "path";
131
+ var toSpace = (message) => ({
132
+ id: `${message.isGroupChat ? "any;+;" : "any;-;"}${message.chatId}`,
133
+ type: message.isGroupChat ? "group" : "dm"
134
+ });
135
+ var toMessage = (message) => ({
136
+ id: message.guid,
137
+ content: [{ type: "plain_text", text: message.text ?? "" }],
138
+ sender: { id: message.sender ?? "" },
139
+ space: toSpace(message),
140
+ timestamp: message.date ?? /* @__PURE__ */ new Date()
141
+ });
142
+ var messages = (client) => stream((emit) => {
143
+ client.startWatching({
144
+ onMessage: (message) => emit(toMessage(message))
145
+ });
146
+ return () => client.stopWatching();
147
+ });
148
+ var send = async (client, spaceId, content) => {
149
+ switch (content.type) {
150
+ case "plain_text":
151
+ await client.send(spaceId, content.text);
152
+ break;
153
+ case "attachment": {
154
+ const tmp = join(tmpdir(), `spectrum-${Date.now()}-${content.name}`);
155
+ await writeFile(tmp, content.data);
156
+ try {
157
+ await client.send(spaceId, { files: [tmp] });
158
+ } finally {
159
+ await unlink(tmp).catch(() => {
160
+ });
161
+ }
162
+ break;
163
+ }
164
+ default:
165
+ break;
166
+ }
167
+ };
168
+
169
+ // src/providers/imessage/remote.ts
170
+ import {
171
+ chatGuid,
172
+ messageGuid,
173
+ Reaction
174
+ } from "@photon-ai/advanced-imessage";
175
+ var TAPBACK_NAMES = new Set(
176
+ Object.values(Reaction).filter((r) => r !== "emoji" && r !== "sticker")
177
+ );
178
+ var toMessage2 = (event) => ({
179
+ id: event.message.guid,
180
+ content: [{ type: "plain_text", text: event.message.text ?? "" }],
181
+ sender: { id: event.message.sender?.address ?? "" },
182
+ space: {
183
+ id: event.chatGuid,
184
+ type: event.chatGuid.includes(";+;") ? "group" : "dm"
185
+ },
186
+ timestamp: event.timestamp
187
+ });
188
+ var clientStream = (client) => {
189
+ const sub = client.messages.subscribe("message.received");
190
+ return stream((emit, end) => {
191
+ (async () => {
192
+ try {
193
+ for await (const event of sub) {
194
+ emit(toMessage2(event));
195
+ }
196
+ end();
197
+ } catch (e) {
198
+ end(e);
199
+ }
200
+ })();
201
+ return () => sub.close();
202
+ });
203
+ };
204
+ var messages2 = (clients) => mergeStreams(clients.map(clientStream));
205
+ var startTyping = async (clients, spaceId) => {
206
+ const remote = clients[0];
207
+ if (!remote) {
208
+ return;
209
+ }
210
+ await remote.chats.startTyping(chatGuid(spaceId));
211
+ };
212
+ var stopTyping = async (clients, spaceId) => {
213
+ const remote = clients[0];
214
+ if (!remote) {
215
+ return;
216
+ }
217
+ await remote.chats.stopTyping(chatGuid(spaceId));
218
+ };
219
+ var send2 = async (clients, spaceId, content) => {
220
+ const remote = clients[0];
221
+ if (!remote) {
222
+ return;
223
+ }
224
+ switch (content.type) {
225
+ case "plain_text":
226
+ await remote.messages.send(chatGuid(spaceId), content.text);
227
+ break;
228
+ case "attachment": {
229
+ const attachment = await remote.attachments.upload({
230
+ data: content.data,
231
+ fileName: content.name,
232
+ mimeType: content.mimeType
233
+ });
234
+ await remote.messages.send(chatGuid(spaceId), "", {
235
+ attachment: attachment.guid
236
+ });
237
+ break;
238
+ }
239
+ default:
240
+ break;
241
+ }
242
+ };
243
+ var replyToMessage = async (clients, spaceId, msgId, content) => {
244
+ const remote = clients[0];
245
+ if (!remote) {
246
+ return;
247
+ }
248
+ const chat = chatGuid(spaceId);
249
+ const replyTo = messageGuid(msgId);
250
+ switch (content.type) {
251
+ case "plain_text":
252
+ await remote.messages.send(chat, content.text, { replyTo });
253
+ break;
254
+ case "attachment": {
255
+ const attachment = await remote.attachments.upload({
256
+ data: content.data,
257
+ fileName: content.name,
258
+ mimeType: content.mimeType
259
+ });
260
+ await remote.messages.send(chat, "", {
261
+ attachment: attachment.guid,
262
+ replyTo
263
+ });
264
+ break;
265
+ }
266
+ default:
267
+ break;
268
+ }
269
+ };
270
+ var reactToMessage = async (clients, spaceId, msgId, reaction) => {
271
+ const remote = clients[0];
272
+ if (!remote) {
273
+ return;
274
+ }
275
+ const chat = chatGuid(spaceId);
276
+ const msg = messageGuid(msgId);
277
+ if (TAPBACK_NAMES.has(reaction)) {
278
+ await remote.messages.react(chat, msg, reaction);
279
+ } else {
280
+ await remote.messages.reactEmoji(chat, msg, reaction);
281
+ }
282
+ };
283
+
284
+ // src/providers/imessage/types.ts
285
+ import { IMessageSDK } from "@photon-ai/imessage-kit";
286
+ import z from "zod";
287
+ var isLocal = (client) => client instanceof IMessageSDK;
288
+ var clientEntry = z.object({ address: z.string(), token: z.string() });
289
+ var configSchema = z.union([
290
+ z.object({ local: z.literal(true) }),
291
+ z.object({
292
+ local: z.literal(false).optional().default(false),
293
+ clients: clientEntry.or(z.array(clientEntry)).optional()
294
+ })
295
+ ]);
296
+ var userSchema = z.object({});
297
+ var spaceSchema = z.object({
298
+ id: z.string(),
299
+ type: z.enum(["dm", "group"])
300
+ });
301
+
302
+ // src/providers/imessage/index.ts
303
+ var imessage = definePlatform("iMessage", {
304
+ config: configSchema,
305
+ static: {
306
+ tapbacks: {
307
+ love: "love",
308
+ like: "like",
309
+ dislike: "dislike",
310
+ laugh: "laugh",
311
+ emphasize: "emphasize",
312
+ question: "question"
313
+ }
314
+ },
315
+ user: {
316
+ resolve: async ({ input }) => ({ id: input.userID })
317
+ },
318
+ space: {
319
+ schema: spaceSchema,
320
+ resolve: async ({ input, client }) => {
321
+ if (isLocal(client)) {
322
+ throw new Error(
323
+ "Space creation is not supported in local mode. Local mode only supports replying to messages."
324
+ );
325
+ }
326
+ if (input.users.length === 0) {
327
+ throw new Error("iMessage space creation requires at least one user");
328
+ }
329
+ const addresses = input.users.map((u) => u.id);
330
+ if (input.users.length === 1) {
331
+ return {
332
+ id: directChat(addresses[0] ?? ""),
333
+ type: "dm"
334
+ };
335
+ }
336
+ const remote = client[0];
337
+ if (!remote) {
338
+ throw new Error("No remote iMessage client available");
339
+ }
340
+ const { chat } = await remote.chats.create(addresses);
341
+ return { id: chat.guid, type: "group" };
342
+ }
343
+ },
344
+ lifecycle: {
345
+ createClient: async ({
346
+ config,
347
+ projectId,
348
+ projectSecret
349
+ }) => {
350
+ if (config.local) {
351
+ return new IMessageSDK2();
352
+ }
353
+ if (config.clients) {
354
+ const entries = Array.isArray(config.clients) ? config.clients : [config.clients];
355
+ return entries.map(
356
+ (e) => createClient2({ address: e.address, tls: true, token: e.token })
357
+ );
358
+ }
359
+ if (!(projectId && projectSecret)) {
360
+ throw new Error(
361
+ "iMessage requires projectId and projectSecret. Either pass credentials to Spectrum(), use local mode: imessage.config({ local: true }), or provide explicit client config: imessage.config({ clients: [...] })"
362
+ );
363
+ }
364
+ return await createCloudClients(projectId, projectSecret);
365
+ },
366
+ destroyClient: async ({ client }) => {
367
+ if (isLocal(client)) {
368
+ await client.close();
369
+ return;
370
+ }
371
+ await disposeCloudAuth(client);
372
+ await Promise.all(client.map((c) => c.close()));
373
+ }
374
+ },
375
+ events: {
376
+ messages: ({ client }) => isLocal(client) ? messages(client) : messages2(client)
377
+ },
378
+ actions: {
379
+ send: async ({ space, content, client }) => {
380
+ for (const item of content) {
381
+ if (isLocal(client)) {
382
+ await send(client, space.id, item);
383
+ } else {
384
+ await send2(client, space.id, item);
385
+ }
386
+ }
387
+ },
388
+ startTyping: async ({ space, client }) => {
389
+ if (isLocal(client)) {
390
+ return;
391
+ }
392
+ await startTyping(client, space.id);
393
+ },
394
+ stopTyping: async ({ space, client }) => {
395
+ if (isLocal(client)) {
396
+ return;
397
+ }
398
+ await stopTyping(client, space.id);
399
+ },
400
+ reactToMessage: async ({ space, messageId, reaction, client }) => {
401
+ if (isLocal(client)) {
402
+ return;
403
+ }
404
+ await reactToMessage(client, space.id, messageId, reaction);
405
+ },
406
+ replyToMessage: async ({ space, messageId, content, client }) => {
407
+ if (isLocal(client)) {
408
+ return;
409
+ }
410
+ for (const item of content) {
411
+ await replyToMessage(client, space.id, messageId, item);
412
+ }
413
+ }
414
+ }
415
+ });
416
+ export {
417
+ imessage
418
+ };
@@ -0,0 +1,35 @@
1
+ import { b as Platform, a as PlatformDef, P as ProviderMessage } from '../../types-eXHZpal1.js';
2
+ import * as node_readline from 'node:readline';
3
+ import z__default from 'zod';
4
+ import 'hotscript';
5
+ import 'type-fest';
6
+
7
+ declare const terminal: Platform<PlatformDef<"terminal", z__default.ZodObject<{}, z__default.core.$strip>, z__default.ZodType<object, unknown, z__default.core.$ZodTypeInternals<object, unknown>> | undefined, z__default.ZodType<object, unknown, z__default.core.$ZodTypeInternals<object, unknown>> | undefined, z__default.ZodType<object, unknown, z__default.core.$ZodTypeInternals<object, unknown>> | undefined, node_readline.Interface, {
8
+ id: string;
9
+ }, {
10
+ id: string;
11
+ }, undefined, ProviderMessage<{
12
+ id: string;
13
+ }, {
14
+ id: string;
15
+ }, Record<never, never>>, {
16
+ messages({ client }: {
17
+ client: node_readline.Interface;
18
+ config: Record<string, never>;
19
+ }): AsyncGenerator<{
20
+ id: `${string}-${string}-${string}-${string}-${string}`;
21
+ content: {
22
+ type: "plain_text";
23
+ text: string;
24
+ }[];
25
+ sender: {
26
+ id: string;
27
+ };
28
+ space: {
29
+ id: string;
30
+ };
31
+ timestamp: Date;
32
+ }, void, any>;
33
+ }>> & Readonly<Record<never, never>>;
34
+
35
+ export { terminal };
@@ -0,0 +1,61 @@
1
+ import {
2
+ definePlatform
3
+ } from "../../chunk-UZ2CXPOD.js";
4
+
5
+ // src/providers/terminal/index.ts
6
+ import { createInterface } from "readline";
7
+ import z from "zod";
8
+ var terminal = definePlatform("terminal", {
9
+ config: z.object({}),
10
+ user: {
11
+ resolve: async ({ input }) => ({
12
+ id: input.userID
13
+ })
14
+ },
15
+ space: {
16
+ resolve: async () => ({
17
+ id: "terminal"
18
+ })
19
+ },
20
+ lifecycle: {
21
+ createClient: async () => {
22
+ const client = createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout
25
+ });
26
+ client.on("SIGINT", () => {
27
+ client.close();
28
+ process.kill(process.pid, "SIGINT");
29
+ });
30
+ return client;
31
+ },
32
+ destroyClient: async ({ client }) => {
33
+ client.close();
34
+ process.stdin.unref();
35
+ }
36
+ },
37
+ events: {
38
+ async *messages({ client }) {
39
+ for await (const line of client) {
40
+ yield {
41
+ id: crypto.randomUUID(),
42
+ content: [{ type: "plain_text", text: line }],
43
+ sender: { id: "terminal-user" },
44
+ space: { id: "terminal" },
45
+ timestamp: /* @__PURE__ */ new Date()
46
+ };
47
+ }
48
+ }
49
+ },
50
+ actions: {
51
+ send: async ({ content }) => {
52
+ const outputs = content.filter((c) => c.type === "plain_text").map((c) => c.text);
53
+ for (const output of outputs) {
54
+ console.log(output);
55
+ }
56
+ }
57
+ }
58
+ });
59
+ export {
60
+ terminal
61
+ };
@@ -0,0 +1,8 @@
1
+ interface ManagedStream<T> extends AsyncIterable<T> {
2
+ close(): Promise<void>;
3
+ }
4
+ type StreamCleanup = void | (() => void | Promise<void>);
5
+ declare function stream<T>(setup: (emit: (value: T) => void, end: (error?: unknown) => void) => StreamCleanup | Promise<StreamCleanup>): ManagedStream<T>;
6
+ declare function mergeStreams<T>(streams: readonly ManagedStream<T>[]): ManagedStream<T>;
7
+
8
+ export { type ManagedStream as M, mergeStreams as m, stream as s };