@moxxy/plugin-channel-imessage 0.29.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/LICENSE +21 -0
- package/dist/bluebubbles-client.d.ts +79 -0
- package/dist/bluebubbles-client.d.ts.map +1 -0
- package/dist/bluebubbles-client.js +113 -0
- package/dist/bluebubbles-client.js.map +1 -0
- package/dist/channel/chunker.d.ts +66 -0
- package/dist/channel/chunker.d.ts.map +1 -0
- package/dist/channel/chunker.js +130 -0
- package/dist/channel/chunker.js.map +1 -0
- package/dist/channel/turn-runner.d.ts +32 -0
- package/dist/channel/turn-runner.d.ts.map +1 -0
- package/dist/channel/turn-runner.js +58 -0
- package/dist/channel/turn-runner.js.map +1 -0
- package/dist/channel.d.ts +87 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +264 -0
- package/dist/channel.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +211 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +70 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +111 -0
- package/dist/keys.js.map +1 -0
- package/dist/message-gate.d.ts +40 -0
- package/dist/message-gate.d.ts.map +1 -0
- package/dist/message-gate.js +54 -0
- package/dist/message-gate.js.map +1 -0
- package/dist/permission.d.ts +25 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +31 -0
- package/dist/permission.js.map +1 -0
- package/dist/schema.d.ts +96 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +41 -0
- package/dist/schema.js.map +1 -0
- package/dist/setup-wizard.d.ts +15 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +127 -0
- package/dist/setup-wizard.js.map +1 -0
- package/package.json +96 -0
- package/src/bluebubbles-client.ts +182 -0
- package/src/channel/chunker.test.ts +132 -0
- package/src/channel/chunker.ts +144 -0
- package/src/channel/turn-runner.ts +82 -0
- package/src/channel.test.ts +256 -0
- package/src/channel.ts +352 -0
- package/src/index.ts +273 -0
- package/src/keys.test.ts +86 -0
- package/src/keys.ts +109 -0
- package/src/message-gate.test.ts +126 -0
- package/src/message-gate.ts +94 -0
- package/src/permission.ts +40 -0
- package/src/schema.test.ts +51 -0
- package/src/schema.ts +47 -0
- package/src/setup-wizard.ts +155 -0
- package/src/subcommands.test.ts +194 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moxxy (moxxy.ai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The BlueBubbles transport seam (the "sidecar" equivalent for iMessage).
|
|
3
|
+
*
|
|
4
|
+
* BlueBubbles is a native macOS app running a localhost Koa + socket.io server
|
|
5
|
+
* (default port 1234). We hit it directly over `http://127.0.0.1:1234`:
|
|
6
|
+
* - inbound: a socket.io client subscribed to `new-message` (built-in
|
|
7
|
+
* reconnection; no webhook lifecycle),
|
|
8
|
+
* - outbound: `POST /api/v1/message/text` with `method: 'apple-script'`.
|
|
9
|
+
*
|
|
10
|
+
* Auth is a single password passed as a QUERY PARAM on every call (BlueBubbles
|
|
11
|
+
* never reads it from a header). There is no HMAC on the socket feed — pure
|
|
12
|
+
* localhost trust — so the consumer must stay bound to loopback.
|
|
13
|
+
*
|
|
14
|
+
* The channel drives this behind {@link BlueBubblesClientLike} so tests inject a
|
|
15
|
+
* fake and no socket.io / network is touched. `socket.io-client` is imported
|
|
16
|
+
* lazily inside {@link BlueBubblesClient.connect} so module load (and
|
|
17
|
+
* `isAvailable`) stays light.
|
|
18
|
+
*/
|
|
19
|
+
export interface BlueBubblesClientLike {
|
|
20
|
+
/** Best-effort reachability + auth probe; throws a friendly error on failure. */
|
|
21
|
+
ping(): Promise<void>;
|
|
22
|
+
/** Open the socket.io connection and begin emitting inbound messages. */
|
|
23
|
+
connect(): Promise<void>;
|
|
24
|
+
/** Subscribe to raw `new-message` payloads. Returns an unsubscribe fn. */
|
|
25
|
+
onMessage(listener: (raw: unknown) => void): () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Send a text message. `tempGuid` correlates the send with its echoed
|
|
28
|
+
* `new-message`; the returned `guid` (when the server reports it) is the
|
|
29
|
+
* permanent id. Both feed the anti-echo set.
|
|
30
|
+
*/
|
|
31
|
+
sendText(chatGuid: string, message: string): Promise<{
|
|
32
|
+
guid: string | null;
|
|
33
|
+
tempGuid: string;
|
|
34
|
+
}>;
|
|
35
|
+
/** Tear down the socket. */
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
export interface BlueBubblesClientLogger {
|
|
39
|
+
debug?(msg: string, meta?: Record<string, unknown>): void;
|
|
40
|
+
info?(msg: string, meta?: Record<string, unknown>): void;
|
|
41
|
+
warn?(msg: string, meta?: Record<string, unknown>): void;
|
|
42
|
+
}
|
|
43
|
+
/** Minimal slice of a socket.io client the channel uses (keeps typing loose). */
|
|
44
|
+
export interface SocketLike {
|
|
45
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
46
|
+
close(): void;
|
|
47
|
+
}
|
|
48
|
+
export interface BlueBubblesClientOptions {
|
|
49
|
+
readonly serverUrl: string;
|
|
50
|
+
readonly password: string;
|
|
51
|
+
readonly logger?: BlueBubblesClientLogger;
|
|
52
|
+
/** Test seam: injectable fetch (defaults to global fetch). */
|
|
53
|
+
readonly fetchImpl?: typeof fetch;
|
|
54
|
+
/** Test seam: injectable socket factory (defaults to lazy socket.io-client). */
|
|
55
|
+
readonly socketFactory?: (opts: {
|
|
56
|
+
url: string;
|
|
57
|
+
password: string;
|
|
58
|
+
}) => Promise<SocketLike>;
|
|
59
|
+
}
|
|
60
|
+
/** A fresh per-send temp guid (correlates our send with its echoed message). */
|
|
61
|
+
export declare function makeTempGuid(): string;
|
|
62
|
+
export declare class BlueBubblesClient implements BlueBubblesClientLike {
|
|
63
|
+
private readonly opts;
|
|
64
|
+
private readonly base;
|
|
65
|
+
private socket;
|
|
66
|
+
private readonly messageListeners;
|
|
67
|
+
constructor(opts: BlueBubblesClientOptions);
|
|
68
|
+
ping(): Promise<void>;
|
|
69
|
+
connect(): Promise<void>;
|
|
70
|
+
onMessage(listener: (raw: unknown) => void): () => void;
|
|
71
|
+
sendText(chatGuid: string, message: string): Promise<{
|
|
72
|
+
guid: string | null;
|
|
73
|
+
tempGuid: string;
|
|
74
|
+
}>;
|
|
75
|
+
close(): void;
|
|
76
|
+
/** Build an absolute API URL with the password query param appended. */
|
|
77
|
+
private apiUrl;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=bluebubbles-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bluebubbles-client.d.ts","sourceRoot":"","sources":["../src/bluebubbles-client.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,yEAAyE;IACzE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,0EAA0E;IAC1E,SAAS,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChG,4BAA4B;IAC5B,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1D,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1D;AAED,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAChE,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,uBAAuB,CAAC;IAC1C,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAClC,gFAAgF;IAChF,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;CAC3F;AAED,gFAAgF;AAChF,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,qBAAa,iBAAkB,YAAW,qBAAqB;IAC7D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2B;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqC;gBAE1D,IAAI,EAAE,wBAAwB;IAMpC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAqB9B,SAAS,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI;IAKjD,QAAQ,CACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAgBrD,KAAK,IAAI,IAAI;IAab,wEAAwE;IACxE,OAAO,CAAC,MAAM;CAKf"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/** A fresh per-send temp guid (correlates our send with its echoed message). */
|
|
3
|
+
export function makeTempGuid() {
|
|
4
|
+
return `temp-${randomUUID()}`;
|
|
5
|
+
}
|
|
6
|
+
export class BlueBubblesClient {
|
|
7
|
+
opts;
|
|
8
|
+
base;
|
|
9
|
+
socket = null;
|
|
10
|
+
messageListeners = new Set();
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
this.opts = opts;
|
|
13
|
+
// Normalize the base URL once (strip a trailing slash) so path joins are clean.
|
|
14
|
+
this.base = opts.serverUrl.trim().replace(/\/+$/, '');
|
|
15
|
+
}
|
|
16
|
+
async ping() {
|
|
17
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
18
|
+
let res;
|
|
19
|
+
try {
|
|
20
|
+
res = await doFetch(this.apiUrl('/api/v1/ping'));
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
throw new Error(`Cannot reach the BlueBubbles server at ${this.base}. Is it running on this Mac? ` +
|
|
24
|
+
`(${err instanceof Error ? err.message : String(err)})`);
|
|
25
|
+
}
|
|
26
|
+
if (res.status === 401 || res.status === 403) {
|
|
27
|
+
throw new Error('The BlueBubbles server rejected the password. Check the password in the BlueBubbles app matches the one stored for moxxy.');
|
|
28
|
+
}
|
|
29
|
+
// Any other HTTP response means something is listening — reachable enough.
|
|
30
|
+
}
|
|
31
|
+
async connect() {
|
|
32
|
+
const factory = this.opts.socketFactory ?? ((o) => defaultSocketFactory(o));
|
|
33
|
+
const socket = await factory({ url: this.base, password: this.opts.password });
|
|
34
|
+
this.socket = socket;
|
|
35
|
+
socket.on('new-message', (...args) => {
|
|
36
|
+
const payload = args.length > 0 ? args[0] : undefined;
|
|
37
|
+
for (const listener of this.messageListeners) {
|
|
38
|
+
try {
|
|
39
|
+
listener(payload);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
this.opts.logger?.warn?.('imessage: message listener threw', { err: String(err) });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
socket.on('disconnect', (...args) => {
|
|
47
|
+
this.opts.logger?.debug?.('imessage: socket disconnected (auto-reconnecting)', {
|
|
48
|
+
reason: String(args.length > 0 ? args[0] : ''),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
onMessage(listener) {
|
|
53
|
+
this.messageListeners.add(listener);
|
|
54
|
+
return () => this.messageListeners.delete(listener);
|
|
55
|
+
}
|
|
56
|
+
async sendText(chatGuid, message) {
|
|
57
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
58
|
+
const tempGuid = makeTempGuid();
|
|
59
|
+
const res = await doFetch(this.apiUrl('/api/v1/message/text'), {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ chatGuid, tempGuid, message, method: 'apple-script' }),
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const detail = await res.text().catch(() => '');
|
|
66
|
+
throw new Error(`BlueBubbles send failed (HTTP ${res.status})${detail ? `: ${detail}` : ''}`);
|
|
67
|
+
}
|
|
68
|
+
const guid = await extractGuid(res);
|
|
69
|
+
return { guid, tempGuid };
|
|
70
|
+
}
|
|
71
|
+
close() {
|
|
72
|
+
this.messageListeners.clear();
|
|
73
|
+
const socket = this.socket;
|
|
74
|
+
this.socket = null;
|
|
75
|
+
if (socket) {
|
|
76
|
+
try {
|
|
77
|
+
socket.close();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
this.opts.logger?.warn?.('imessage: socket close threw', { err: String(err) });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Build an absolute API URL with the password query param appended. */
|
|
85
|
+
apiUrl(path) {
|
|
86
|
+
const url = new URL(this.base + path);
|
|
87
|
+
url.searchParams.set('password', this.opts.password);
|
|
88
|
+
return url.toString();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Read the sent message's permanent guid from the send response, if present. */
|
|
92
|
+
async function extractGuid(res) {
|
|
93
|
+
const body = await res.json().catch(() => null);
|
|
94
|
+
if (!body || typeof body !== 'object')
|
|
95
|
+
return null;
|
|
96
|
+
const data = body.data;
|
|
97
|
+
if (!data || typeof data !== 'object')
|
|
98
|
+
return null;
|
|
99
|
+
const guid = data.guid;
|
|
100
|
+
return typeof guid === 'string' && guid.length > 0 ? guid : null;
|
|
101
|
+
}
|
|
102
|
+
/** Lazily import socket.io-client and open a connection to the BlueBubbles feed. */
|
|
103
|
+
async function defaultSocketFactory(opts) {
|
|
104
|
+
const { io } = await import('socket.io-client');
|
|
105
|
+
const socket = io(opts.url, {
|
|
106
|
+
query: { password: opts.password },
|
|
107
|
+
transports: ['websocket'],
|
|
108
|
+
// BlueBubbles' socket.io has built-in reconnection; keep it on.
|
|
109
|
+
reconnection: true,
|
|
110
|
+
});
|
|
111
|
+
return socket;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=bluebubbles-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bluebubbles-client.js","sourceRoot":"","sources":["../src/bluebubbles-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffered chunked sends — iMessage's streaming strategy.
|
|
3
|
+
*
|
|
4
|
+
* WHY NOT FramePump edits: editing a sent iMessage requires the BlueBubbles
|
|
5
|
+
* Private API (a Helper bundle injected into Messages, which needs SIP disabled)
|
|
6
|
+
* — out of scope for v1's stock apple-script server. With apple-script every
|
|
7
|
+
* message is immutable once sent, so instead of "send once then edit" we buffer
|
|
8
|
+
* the streamed text and send coherent chunks: nothing goes out until a
|
|
9
|
+
* paragraph-aligned chunk is ready, and the remainder goes out once, at turn
|
|
10
|
+
* end. (Copied from the Signal channel's chunker, which faces the same
|
|
11
|
+
* immutable-message constraint.)
|
|
12
|
+
*/
|
|
13
|
+
/** Buffered text below this length is held for the final flush. */
|
|
14
|
+
export declare const IMESSAGE_CHUNK_SOFT_LIMIT = 1500;
|
|
15
|
+
/**
|
|
16
|
+
* Never send a single message longer than this. iMessage itself allows long
|
|
17
|
+
* bodies, but clients render extremely long messages poorly; 2000 mirrors the
|
|
18
|
+
* Signal channel's cap for parity.
|
|
19
|
+
*/
|
|
20
|
+
export declare const IMESSAGE_CHUNK_HARD_LIMIT = 2000;
|
|
21
|
+
export interface ChunkLimits {
|
|
22
|
+
readonly softLimit?: number;
|
|
23
|
+
readonly hardLimit?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* If `text` has grown past the soft limit, split off one send-ready chunk at
|
|
27
|
+
* the best boundary (paragraph → line → word) at or below the hard limit.
|
|
28
|
+
* Returns null while the text should keep buffering.
|
|
29
|
+
*/
|
|
30
|
+
export declare function takeChunk(text: string, limits?: ChunkLimits): {
|
|
31
|
+
chunk: string;
|
|
32
|
+
rest: string;
|
|
33
|
+
} | null;
|
|
34
|
+
/** Split a final remainder into hard-limit-sized pieces at the best boundaries. */
|
|
35
|
+
export declare function splitForImessage(text: string, limits?: ChunkLimits): string[];
|
|
36
|
+
export interface ChunkedSenderOptions {
|
|
37
|
+
/** Deliver one message. Errors should be handled by the caller-supplied fn
|
|
38
|
+
* (log + swallow) — a failed chunk must never abort the turn. */
|
|
39
|
+
readonly send: (text: string) => Promise<void>;
|
|
40
|
+
readonly limits?: ChunkLimits;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Drives {@link takeChunk} over a GROWING renderer snapshot: `offer()` on every
|
|
44
|
+
* change (sends any ready chunk), `finalize()` at turn end (sends the
|
|
45
|
+
* remainder, or `emptyText` when the whole turn produced nothing). Sends are
|
|
46
|
+
* serialized on an internal queue so chunks can never interleave out of order.
|
|
47
|
+
*/
|
|
48
|
+
export declare class ChunkedSender {
|
|
49
|
+
private readonly opts;
|
|
50
|
+
/** The exact snapshot prefix already delivered (chunk boundaries included). */
|
|
51
|
+
private sentPrefix;
|
|
52
|
+
private sentAnything;
|
|
53
|
+
private queue;
|
|
54
|
+
constructor(opts: ChunkedSenderOptions);
|
|
55
|
+
/** Called with the current full snapshot whenever it changes. */
|
|
56
|
+
offer(snapshot: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Turn end: deliver whatever the final snapshot still owes. When the final
|
|
59
|
+
* text no longer extends what we already sent (a divergent final frame —
|
|
60
|
+
* rare), send the full final text so the user always receives the
|
|
61
|
+
* authoritative reply, accepting the duplication.
|
|
62
|
+
*/
|
|
63
|
+
finalize(snapshot: string, emptyText?: string): Promise<void>;
|
|
64
|
+
private deliver;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=chunker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../../src/channel/chunker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,mEAAmE;AACnE,eAAO,MAAM,yBAAyB,OAAQ,CAAC;AAC/C;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,OAAQ,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,WAAgB,GACvB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAiBxC;AAED,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,MAAM,EAAE,CAcjF;AAED,MAAM,WAAW,oBAAoB;IACnC;sEACkE;IAClE,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,+EAA+E;IAC/E,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,KAAK,CAAoC;gBAErC,IAAI,EAAE,oBAAoB;IAItC,iEAAiE;IACjE,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAiB7B;;;;;OAKG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAkBrD,OAAO;CAMtB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffered chunked sends — iMessage's streaming strategy.
|
|
3
|
+
*
|
|
4
|
+
* WHY NOT FramePump edits: editing a sent iMessage requires the BlueBubbles
|
|
5
|
+
* Private API (a Helper bundle injected into Messages, which needs SIP disabled)
|
|
6
|
+
* — out of scope for v1's stock apple-script server. With apple-script every
|
|
7
|
+
* message is immutable once sent, so instead of "send once then edit" we buffer
|
|
8
|
+
* the streamed text and send coherent chunks: nothing goes out until a
|
|
9
|
+
* paragraph-aligned chunk is ready, and the remainder goes out once, at turn
|
|
10
|
+
* end. (Copied from the Signal channel's chunker, which faces the same
|
|
11
|
+
* immutable-message constraint.)
|
|
12
|
+
*/
|
|
13
|
+
/** Buffered text below this length is held for the final flush. */
|
|
14
|
+
export const IMESSAGE_CHUNK_SOFT_LIMIT = 1_500;
|
|
15
|
+
/**
|
|
16
|
+
* Never send a single message longer than this. iMessage itself allows long
|
|
17
|
+
* bodies, but clients render extremely long messages poorly; 2000 mirrors the
|
|
18
|
+
* Signal channel's cap for parity.
|
|
19
|
+
*/
|
|
20
|
+
export const IMESSAGE_CHUNK_HARD_LIMIT = 2_000;
|
|
21
|
+
/**
|
|
22
|
+
* If `text` has grown past the soft limit, split off one send-ready chunk at
|
|
23
|
+
* the best boundary (paragraph → line → word) at or below the hard limit.
|
|
24
|
+
* Returns null while the text should keep buffering.
|
|
25
|
+
*/
|
|
26
|
+
export function takeChunk(text, limits = {}) {
|
|
27
|
+
const soft = limits.softLimit ?? IMESSAGE_CHUNK_SOFT_LIMIT;
|
|
28
|
+
const hard = limits.hardLimit ?? IMESSAGE_CHUNK_HARD_LIMIT;
|
|
29
|
+
if (text.length <= soft)
|
|
30
|
+
return null;
|
|
31
|
+
const window = text.slice(0, Math.min(text.length, hard));
|
|
32
|
+
// Prefer a paragraph break; a chunk that ends mid-sentence reads badly as a
|
|
33
|
+
// standalone message. Require the boundary to keep a meaningful chunk (≥ half
|
|
34
|
+
// the soft limit) so a leading blank line can't produce confetti.
|
|
35
|
+
const minCut = Math.floor(soft / 2);
|
|
36
|
+
for (const boundary of ['\n\n', '\n', ' ']) {
|
|
37
|
+
const at = window.lastIndexOf(boundary);
|
|
38
|
+
if (at >= minCut) {
|
|
39
|
+
return { chunk: window.slice(0, at).trimEnd(), rest: text.slice(at + boundary.length) };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (text.length <= hard)
|
|
43
|
+
return null; // no boundary yet — wait for more text
|
|
44
|
+
return { chunk: window, rest: text.slice(window.length) }; // pathological: hard cut
|
|
45
|
+
}
|
|
46
|
+
/** Split a final remainder into hard-limit-sized pieces at the best boundaries. */
|
|
47
|
+
export function splitForImessage(text, limits = {}) {
|
|
48
|
+
const hard = limits.hardLimit ?? IMESSAGE_CHUNK_HARD_LIMIT;
|
|
49
|
+
const parts = [];
|
|
50
|
+
let rest = text;
|
|
51
|
+
while (rest.length > hard) {
|
|
52
|
+
// Reuse takeChunk with soft==hard-ish so it only splits when forced.
|
|
53
|
+
const taken = takeChunk(rest, { softLimit: Math.floor(hard / 2), hardLimit: hard });
|
|
54
|
+
if (!taken)
|
|
55
|
+
break;
|
|
56
|
+
if (taken.chunk)
|
|
57
|
+
parts.push(taken.chunk);
|
|
58
|
+
rest = taken.rest;
|
|
59
|
+
}
|
|
60
|
+
const tail = rest.trim();
|
|
61
|
+
if (tail)
|
|
62
|
+
parts.push(tail);
|
|
63
|
+
return parts;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Drives {@link takeChunk} over a GROWING renderer snapshot: `offer()` on every
|
|
67
|
+
* change (sends any ready chunk), `finalize()` at turn end (sends the
|
|
68
|
+
* remainder, or `emptyText` when the whole turn produced nothing). Sends are
|
|
69
|
+
* serialized on an internal queue so chunks can never interleave out of order.
|
|
70
|
+
*/
|
|
71
|
+
export class ChunkedSender {
|
|
72
|
+
opts;
|
|
73
|
+
/** The exact snapshot prefix already delivered (chunk boundaries included). */
|
|
74
|
+
sentPrefix = '';
|
|
75
|
+
sentAnything = false;
|
|
76
|
+
queue = Promise.resolve();
|
|
77
|
+
constructor(opts) {
|
|
78
|
+
this.opts = opts;
|
|
79
|
+
}
|
|
80
|
+
/** Called with the current full snapshot whenever it changes. */
|
|
81
|
+
offer(snapshot) {
|
|
82
|
+
this.queue = this.queue.then(async () => {
|
|
83
|
+
// Streamed text only ever grows; if the snapshot diverged from what we
|
|
84
|
+
// already delivered (the final assistant_message rewrote history) hold
|
|
85
|
+
// everything for finalize(), which handles divergence explicitly.
|
|
86
|
+
if (!snapshot.startsWith(this.sentPrefix))
|
|
87
|
+
return;
|
|
88
|
+
let consumed = this.sentPrefix.length;
|
|
89
|
+
let taken = takeChunk(snapshot.slice(consumed), this.opts.limits);
|
|
90
|
+
while (taken) {
|
|
91
|
+
await this.deliver(taken.chunk);
|
|
92
|
+
consumed = snapshot.length - taken.rest.length;
|
|
93
|
+
this.sentPrefix = snapshot.slice(0, consumed);
|
|
94
|
+
taken = takeChunk(taken.rest, this.opts.limits);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Turn end: deliver whatever the final snapshot still owes. When the final
|
|
100
|
+
* text no longer extends what we already sent (a divergent final frame —
|
|
101
|
+
* rare), send the full final text so the user always receives the
|
|
102
|
+
* authoritative reply, accepting the duplication.
|
|
103
|
+
*/
|
|
104
|
+
async finalize(snapshot, emptyText) {
|
|
105
|
+
this.queue = this.queue.then(async () => {
|
|
106
|
+
const finalText = snapshot.trim();
|
|
107
|
+
if (!finalText) {
|
|
108
|
+
if (!this.sentAnything && emptyText)
|
|
109
|
+
await this.deliver(emptyText);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const remainder = snapshot.startsWith(this.sentPrefix)
|
|
113
|
+
? snapshot.slice(this.sentPrefix.length)
|
|
114
|
+
: snapshot; // diverged: resend the authoritative text in full
|
|
115
|
+
for (const part of splitForImessage(remainder, this.opts.limits)) {
|
|
116
|
+
await this.deliver(part);
|
|
117
|
+
}
|
|
118
|
+
this.sentPrefix = snapshot;
|
|
119
|
+
});
|
|
120
|
+
await this.queue;
|
|
121
|
+
}
|
|
122
|
+
async deliver(text) {
|
|
123
|
+
const trimmed = text.trim();
|
|
124
|
+
if (!trimmed)
|
|
125
|
+
return;
|
|
126
|
+
this.sentAnything = true;
|
|
127
|
+
await this.opts.send(trimmed);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=chunker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunker.js","sourceRoot":"","sources":["../../src/channel/chunker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { newTurnId } from '@moxxy/core';
|
|
2
|
+
import type { ClientSession as Session } from '@moxxy/sdk';
|
|
3
|
+
import { type ChunkLimits } from './chunker.js';
|
|
4
|
+
export interface TurnRunnerLogger {
|
|
5
|
+
warn?(msg: string, meta?: Record<string, unknown>): void;
|
|
6
|
+
}
|
|
7
|
+
export interface RunImessageTurnDeps {
|
|
8
|
+
readonly session: Session;
|
|
9
|
+
/** Deliver one outbound message to the turn's reply target. Must swallow its
|
|
10
|
+
* own transport errors (log + resolve) — a failed send never aborts the turn. */
|
|
11
|
+
readonly send: (text: string) => Promise<void>;
|
|
12
|
+
readonly chunkLimits?: ChunkLimits;
|
|
13
|
+
readonly logger?: TurnRunnerLogger;
|
|
14
|
+
}
|
|
15
|
+
export interface RunImessageTurnOptions {
|
|
16
|
+
readonly text: string;
|
|
17
|
+
readonly model?: string;
|
|
18
|
+
readonly controller: AbortController;
|
|
19
|
+
/** Pre-minted turn id; the channel records it as an own-turn id (invariant #8). */
|
|
20
|
+
readonly turnId: ReturnType<typeof newTurnId>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Drive a single iMessage turn end-to-end: subscribe to THIS turn's events
|
|
24
|
+
* (filtered by turnId — `session.log` fans out to every listener, AGENTS.md
|
|
25
|
+
* invariant #8), stream the rendered snapshot through the buffered
|
|
26
|
+
* {@link ChunkedSender} (see chunker.ts for why iMessage gets chunked sends
|
|
27
|
+
* instead of FramePump edits — apple-script messages are immutable), run the
|
|
28
|
+
* turn, flush the final remainder, and unwind in `finally`. No typing indicator:
|
|
29
|
+
* that needs the BlueBubbles Private API, which v1 deliberately omits.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runImessageTurn(deps: RunImessageTurnDeps, opts: RunImessageTurnOptions): Promise<void>;
|
|
32
|
+
//# sourceMappingURL=turn-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-runner.d.ts","sourceRoot":"","sources":["../../src/channel/turn-runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1D;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;sFACkF;IAClF,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;CACpC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;CAC/C;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CA2Cf"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { PlainTurnRenderer, driveTurn, subscribeTurn } from '@moxxy/channel-kit';
|
|
2
|
+
import { ChunkedSender } from './chunker.js';
|
|
3
|
+
/**
|
|
4
|
+
* Drive a single iMessage turn end-to-end: subscribe to THIS turn's events
|
|
5
|
+
* (filtered by turnId — `session.log` fans out to every listener, AGENTS.md
|
|
6
|
+
* invariant #8), stream the rendered snapshot through the buffered
|
|
7
|
+
* {@link ChunkedSender} (see chunker.ts for why iMessage gets chunked sends
|
|
8
|
+
* instead of FramePump edits — apple-script messages are immutable), run the
|
|
9
|
+
* turn, flush the final remainder, and unwind in `finally`. No typing indicator:
|
|
10
|
+
* that needs the BlueBubbles Private API, which v1 deliberately omits.
|
|
11
|
+
*/
|
|
12
|
+
export async function runImessageTurn(deps, opts) {
|
|
13
|
+
const { session, send, logger } = deps;
|
|
14
|
+
const { text, model, controller, turnId } = opts;
|
|
15
|
+
const renderer = new PlainTurnRenderer();
|
|
16
|
+
const sender = new ChunkedSender({
|
|
17
|
+
send: async (t) => {
|
|
18
|
+
try {
|
|
19
|
+
await send(t);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
logger?.warn?.('imessage: send failed', {
|
|
23
|
+
err: err instanceof Error ? err.message : String(err),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
...(deps.chunkLimits ? { limits: deps.chunkLimits } : {}),
|
|
28
|
+
});
|
|
29
|
+
const unsubscribe = subscribeTurn(session, turnId, (event) => {
|
|
30
|
+
if (renderer.accept(event))
|
|
31
|
+
sender.offer(renderer.snapshot());
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
await driveTurn(session, {
|
|
35
|
+
turnId,
|
|
36
|
+
prompt: text,
|
|
37
|
+
...(model ? { model } : {}),
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
});
|
|
40
|
+
await sender.finalize(renderer.snapshot(), '(no output)');
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
logger?.warn?.('imessage: turn failed', {
|
|
44
|
+
err: err instanceof Error ? err.message : String(err),
|
|
45
|
+
});
|
|
46
|
+
// Surface the failure to the sender rather than going silent.
|
|
47
|
+
try {
|
|
48
|
+
await send(`Turn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* best-effort */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
unsubscribe();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=turn-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-runner.js","sourceRoot":"","sources":["../../src/channel/turn-runner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEjF,OAAO,EAAE,aAAa,EAAoB,MAAM,cAAc,CAAC;AAuB/D;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAyB,EACzB,IAA4B;IAE5B,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACvC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAEjD,MAAM,QAAQ,GAAG,IAAI,iBAAiB,EAAE,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC;QAC/B,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;YAChB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC;YAChB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,EAAE,IAAI,EAAE,CAAC,uBAAuB,EAAE;oBACtC,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;iBACtD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1D,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3D,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,OAAO,EAAE;YACvB,MAAM;YACN,MAAM,EAAE,IAAI;YACZ,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3B,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,aAAa,CAAC,CAAC;IAC5D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,IAAI,EAAE,CAAC,uBAAuB,EAAE;YACtC,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACtD,CAAC,CAAC;QACH,8DAA8D;QAC9D,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,gBAAgB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjF,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB;QACnB,CAAC;IACH,CAAC;YAAS,CAAC;QACT,WAAW,EAAE,CAAC;IAChB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ClientSession as Session } from '@moxxy/sdk';
|
|
2
|
+
import type { Channel, ChannelHandle, ChannelStartOptsBase, PermissionResolver } from '@moxxy/sdk';
|
|
3
|
+
import type { VaultStore } from '@moxxy/plugin-vault';
|
|
4
|
+
import { type BlueBubblesClientLike, type BlueBubblesClientOptions } from './bluebubbles-client.js';
|
|
5
|
+
export interface ImessageChannelLogger {
|
|
6
|
+
debug?(msg: string, meta?: Record<string, unknown>): void;
|
|
7
|
+
info?(msg: string, meta?: Record<string, unknown>): void;
|
|
8
|
+
warn?(msg: string, meta?: Record<string, unknown>): void;
|
|
9
|
+
error?(msg: string, meta?: Record<string, unknown>): void;
|
|
10
|
+
}
|
|
11
|
+
export interface ImessageStartOpts extends ChannelStartOptsBase {
|
|
12
|
+
readonly session: Session;
|
|
13
|
+
/** Override the autonomous tool allow-list at start. */
|
|
14
|
+
readonly allowedTools?: ReadonlyArray<string>;
|
|
15
|
+
}
|
|
16
|
+
export interface ImessageChannelOptions {
|
|
17
|
+
readonly vault: VaultStore;
|
|
18
|
+
/** Server URL override; beats env + vault. */
|
|
19
|
+
readonly serverUrl?: string;
|
|
20
|
+
/** Server password override; beats env + vault. */
|
|
21
|
+
readonly password?: string;
|
|
22
|
+
/** Tools the model may call autonomously. `['*']` allows every registered tool. */
|
|
23
|
+
readonly allowedTools?: ReadonlyArray<string>;
|
|
24
|
+
/** Extra allow-listed handles, merged with the vault allow-list. */
|
|
25
|
+
readonly allowedHandles?: ReadonlyArray<string>;
|
|
26
|
+
/** Extra owner handles (self-chat identities), merged with the vault list. */
|
|
27
|
+
readonly ownerHandles?: ReadonlyArray<string>;
|
|
28
|
+
readonly logger?: ImessageChannelLogger;
|
|
29
|
+
/** Test seam: build the BlueBubbles transport (production uses the real one). */
|
|
30
|
+
readonly clientFactory?: (opts: BlueBubblesClientOptions) => BlueBubblesClientLike;
|
|
31
|
+
}
|
|
32
|
+
export declare class ImessageChannel implements Channel<ImessageStartOpts> {
|
|
33
|
+
readonly name = "imessage";
|
|
34
|
+
/**
|
|
35
|
+
* Installed on the session by the CLI dispatcher. Replaced in `start()` once
|
|
36
|
+
* the live tool registry is available for `['*']` expansion; until then it
|
|
37
|
+
* denies everything (safe default before start).
|
|
38
|
+
*/
|
|
39
|
+
permissionResolver: PermissionResolver;
|
|
40
|
+
private readonly opts;
|
|
41
|
+
private session;
|
|
42
|
+
private model;
|
|
43
|
+
private client;
|
|
44
|
+
private readonly allowedHandles;
|
|
45
|
+
private readonly ownerHandles;
|
|
46
|
+
private readonly turns;
|
|
47
|
+
private lastChatGuid;
|
|
48
|
+
private logUnsub;
|
|
49
|
+
/**
|
|
50
|
+
* guid + tempGuid of messages WE sent. BlueBubbles emits `new-message` for
|
|
51
|
+
* the account's own sends too (our replies come back `isFromMe`), so any
|
|
52
|
+
* inbound whose id is in this set is an echo of ourselves and must be dropped
|
|
53
|
+
* (loop protection).
|
|
54
|
+
*/
|
|
55
|
+
private readonly sentIds;
|
|
56
|
+
private handle;
|
|
57
|
+
private runningSettled;
|
|
58
|
+
private resolveRunning;
|
|
59
|
+
private stopping;
|
|
60
|
+
private lastDropWarnAt;
|
|
61
|
+
constructor(opts: ImessageChannelOptions);
|
|
62
|
+
start(startOpts: ImessageStartOpts): Promise<ChannelHandle>;
|
|
63
|
+
/** Every inbound `new-message` funnels through the one gate. */
|
|
64
|
+
private handleInbound;
|
|
65
|
+
/** Busy gate, then one coordinated turn. */
|
|
66
|
+
private processMessage;
|
|
67
|
+
/** Send one message and record its ids for the echo drop. */
|
|
68
|
+
private sendText;
|
|
69
|
+
private recordSent;
|
|
70
|
+
/**
|
|
71
|
+
* Post the assistant's prose for a turn this channel did not initiate.
|
|
72
|
+
* Skipped for our own turnIds (robust to async ordering / replay, invariant
|
|
73
|
+
* #8) and while a turn of ours is streaming via the chunked sender.
|
|
74
|
+
*/
|
|
75
|
+
private mirrorForeignTurn;
|
|
76
|
+
private makeClient;
|
|
77
|
+
private rebuildHandleSet;
|
|
78
|
+
/**
|
|
79
|
+
* Run a handler detached from the socket read loop (an inbound handler that
|
|
80
|
+
* awaited a whole turn would park delivery for its duration). Errors are
|
|
81
|
+
* logged here — nothing upstream awaits this promise.
|
|
82
|
+
*/
|
|
83
|
+
private dispatchInBackground;
|
|
84
|
+
private warnDrop;
|
|
85
|
+
private settleRunning;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|