@leemour/max-cli 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/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/bin/max.d.ts +3 -0
- package/dist/bin/max.d.ts.map +1 -0
- package/dist/bin/max.js +14 -0
- package/dist/bin/max.js.map +1 -0
- package/dist/cache/driver.d.ts +38 -0
- package/dist/cache/driver.d.ts.map +1 -0
- package/dist/cache/driver.js +26 -0
- package/dist/cache/driver.js.map +1 -0
- package/dist/cache/drivers/bun-sqlite.d.ts +8 -0
- package/dist/cache/drivers/bun-sqlite.d.ts.map +1 -0
- package/dist/cache/drivers/bun-sqlite.js +23 -0
- package/dist/cache/drivers/bun-sqlite.js.map +1 -0
- package/dist/cache/drivers/node-sqlite.d.ts +8 -0
- package/dist/cache/drivers/node-sqlite.d.ts.map +1 -0
- package/dist/cache/drivers/node-sqlite.js +22 -0
- package/dist/cache/drivers/node-sqlite.js.map +1 -0
- package/dist/cache/index.d.ts +22 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +33 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/open.d.ts +14 -0
- package/dist/cache/open.d.ts.map +1 -0
- package/dist/cache/open.js +26 -0
- package/dist/cache/open.js.map +1 -0
- package/dist/cache/schema.d.ts +14 -0
- package/dist/cache/schema.d.ts.map +1 -0
- package/dist/cache/schema.js +95 -0
- package/dist/cache/schema.js.map +1 -0
- package/dist/cache/store.d.ts +32 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/store.js +153 -0
- package/dist/cache/store.js.map +1 -0
- package/dist/client.d.ts +103 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +383 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.d.ts.map +1 -0
- package/dist/commands/account.js +24 -0
- package/dist/commands/account.js.map +1 -0
- package/dist/commands/cache.d.ts +10 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +36 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/chats.d.ts +3 -0
- package/dist/commands/chats.d.ts.map +1 -0
- package/dist/commands/chats.js +28 -0
- package/dist/commands/chats.js.map +1 -0
- package/dist/commands/contacts.d.ts +10 -0
- package/dist/commands/contacts.d.ts.map +1 -0
- package/dist/commands/contacts.js +33 -0
- package/dist/commands/contacts.js.map +1 -0
- package/dist/commands/context.d.ts +34 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +40 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/messages.d.ts +3 -0
- package/dist/commands/messages.d.ts.map +1 -0
- package/dist/commands/messages.js +62 -0
- package/dist/commands/messages.js.map +1 -0
- package/dist/commands/runs.d.ts +9 -0
- package/dist/commands/runs.d.ts.map +1 -0
- package/dist/commands/runs.js +68 -0
- package/dist/commands/runs.js.map +1 -0
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +74 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +83 -0
- package/dist/config.js.map +1 -0
- package/dist/domain/map.d.ts +22 -0
- package/dist/domain/map.d.ts.map +1 -0
- package/dist/domain/map.js +107 -0
- package/dist/domain/map.js.map +1 -0
- package/dist/domain/models.d.ts +57 -0
- package/dist/domain/models.d.ts.map +1 -0
- package/dist/domain/models.js +7 -0
- package/dist/domain/models.js.map +1 -0
- package/dist/generated/client.generated.d.ts +30 -0
- package/dist/generated/client.generated.d.ts.map +1 -0
- package/dist/generated/client.generated.js +26 -0
- package/dist/generated/client.generated.js.map +1 -0
- package/dist/generated/opcodes.generated.d.ts +44 -0
- package/dist/generated/opcodes.generated.d.ts.map +1 -0
- package/dist/generated/opcodes.generated.js +44 -0
- package/dist/generated/opcodes.generated.js.map +1 -0
- package/dist/generated/operations.generated.d.ts +76 -0
- package/dist/generated/operations.generated.d.ts.map +1 -0
- package/dist/generated/operations.generated.js +15 -0
- package/dist/generated/operations.generated.js.map +1 -0
- package/dist/output.d.ts +22 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +34 -0
- package/dist/output.js.map +1 -0
- package/dist/profile.d.ts +33 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +60 -0
- package/dist/profile.js.map +1 -0
- package/dist/program.d.ts +29 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +130 -0
- package/dist/program.js.map +1 -0
- package/dist/protocol/connection.d.ts +60 -0
- package/dist/protocol/connection.d.ts.map +1 -0
- package/dist/protocol/connection.js +142 -0
- package/dist/protocol/connection.js.map +1 -0
- package/dist/protocol/frame.d.ts +44 -0
- package/dist/protocol/frame.d.ts.map +1 -0
- package/dist/protocol/frame.js +68 -0
- package/dist/protocol/frame.js.map +1 -0
- package/dist/runs/events.d.ts +92 -0
- package/dist/runs/events.d.ts.map +1 -0
- package/dist/runs/events.js +85 -0
- package/dist/runs/events.js.map +1 -0
- package/dist/runs/recording.d.ts +35 -0
- package/dist/runs/recording.d.ts.map +1 -0
- package/dist/runs/recording.js +53 -0
- package/dist/runs/recording.js.map +1 -0
- package/dist/runs/run.d.ts +81 -0
- package/dist/runs/run.d.ts.map +1 -0
- package/dist/runs/run.js +175 -0
- package/dist/runs/run.js.map +1 -0
- package/dist/session/handshake.d.ts +26 -0
- package/dist/session/handshake.d.ts.map +1 -0
- package/dist/session/handshake.js +32 -0
- package/dist/session/handshake.js.map +1 -0
- package/dist/session/prompt.d.ts +18 -0
- package/dist/session/prompt.d.ts.map +1 -0
- package/dist/session/prompt.js +41 -0
- package/dist/session/prompt.js.map +1 -0
- package/dist/session/store.d.ts +46 -0
- package/dist/session/store.d.ts.map +1 -0
- package/dist/session/store.js +80 -0
- package/dist/session/store.js.map +1 -0
- package/dist/spec/check.d.ts +16 -0
- package/dist/spec/check.d.ts.map +1 -0
- package/dist/spec/check.js +44 -0
- package/dist/spec/check.js.map +1 -0
- package/dist/spec/define.d.ts +95 -0
- package/dist/spec/define.d.ts.map +1 -0
- package/dist/spec/define.js +34 -0
- package/dist/spec/define.js.map +1 -0
- package/dist/spec/identity.d.ts +23 -0
- package/dist/spec/identity.d.ts.map +1 -0
- package/dist/spec/identity.js +23 -0
- package/dist/spec/identity.js.map +1 -0
- package/dist/spec/index.d.ts +15 -0
- package/dist/spec/index.d.ts.map +1 -0
- package/dist/spec/index.js +30 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/spec/operations/chats.d.ts +25 -0
- package/dist/spec/operations/chats.d.ts.map +1 -0
- package/dist/spec/operations/chats.js +49 -0
- package/dist/spec/operations/chats.js.map +1 -0
- package/dist/spec/operations/contacts.d.ts +9 -0
- package/dist/spec/operations/contacts.d.ts.map +1 -0
- package/dist/spec/operations/contacts.js +39 -0
- package/dist/spec/operations/contacts.js.map +1 -0
- package/dist/spec/operations/messages.d.ts +19 -0
- package/dist/spec/operations/messages.d.ts.map +1 -0
- package/dist/spec/operations/messages.js +37 -0
- package/dist/spec/operations/messages.js.map +1 -0
- package/dist/spec/operations/session.d.ts +69 -0
- package/dist/spec/operations/session.d.ts.map +1 -0
- package/dist/spec/operations/session.js +106 -0
- package/dist/spec/operations/session.js.map +1 -0
- package/dist/spec/scalars.d.ts +16 -0
- package/dist/spec/scalars.d.ts.map +1 -0
- package/dist/spec/scalars.js +28 -0
- package/dist/spec/scalars.js.map +1 -0
- package/dist/testing/mock-max.d.ts +29 -0
- package/dist/testing/mock-max.d.ts.map +1 -0
- package/dist/testing/mock-max.js +46 -0
- package/dist/testing/mock-max.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Chat, Contact, Id, Message } from "../domain/models.js";
|
|
2
|
+
import type { CacheDatabase } from "./driver.js";
|
|
3
|
+
export interface CacheOptions {
|
|
4
|
+
database: CacheDatabase;
|
|
5
|
+
/** Injected so a freshness test costs nothing and does not wait. */
|
|
6
|
+
now?: () => number;
|
|
7
|
+
}
|
|
8
|
+
/** Nothing here ever throws on missing data: absent is a cache miss, not an error. */
|
|
9
|
+
export interface CacheStore {
|
|
10
|
+
chats: {
|
|
11
|
+
read(freshForMs: number): Chat[] | undefined;
|
|
12
|
+
write(chats: Chat[]): void;
|
|
13
|
+
};
|
|
14
|
+
contacts: {
|
|
15
|
+
read(freshForMs: number): Contact[] | undefined;
|
|
16
|
+
write(contacts: Contact[]): void;
|
|
17
|
+
};
|
|
18
|
+
messages: {
|
|
19
|
+
/** The newest `limit` messages, but only if the window we hold is both fresh and contiguous. */
|
|
20
|
+
read(chatId: Id, limit: number, freshForMs: number): Message[] | undefined;
|
|
21
|
+
write(chatId: Id, messages: Message[]): void;
|
|
22
|
+
/** After a send, what we hold for that chat is missing the message we just added. */
|
|
23
|
+
invalidate(chatId: Id): void;
|
|
24
|
+
};
|
|
25
|
+
/** True when this process may go to MAX for that window; false when somebody else already is. */
|
|
26
|
+
claim(chatId: Id, anchor: string, holder: string, forMs: number): boolean;
|
|
27
|
+
release(chatId: Id, anchor: string): void;
|
|
28
|
+
clear(): void;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
export declare const openStore: ({ database, now }: CacheOptions) => CacheStore;
|
|
32
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/cache/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAGhD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,CAAA;IACvB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,sFAAsF;AACtF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE;QACL,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,SAAS,CAAA;QAC5C,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAAA;KAC3B,CAAA;IACD,QAAQ,EAAE;QACR,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,SAAS,CAAA;QAC/C,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;KACjC,CAAA;IACD,QAAQ,EAAE;QACR,gGAAgG;QAChG,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,SAAS,CAAA;QAC1E,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;QAC5C,qFAAqF;QACrF,UAAU,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,CAAA;KAC7B,CAAA;IACD,iGAAiG;IACjG,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;IACzE,OAAO,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACzC,KAAK,IAAI,IAAI,CAAA;IACb,KAAK,IAAI,IAAI,CAAA;CACd;AAED,eAAO,MAAM,SAAS,GAAI,mBAAsC,YAAY,KAAG,UA8L9E,CAAA"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { migrate } from "./schema.js";
|
|
2
|
+
export const openStore = ({ database, now = () => Date.now() }) => {
|
|
3
|
+
migrate(database);
|
|
4
|
+
const markFetched = database.prepare("INSERT INTO fetched (kind, at) VALUES (?, ?) ON CONFLICT(kind) DO UPDATE SET at = excluded.at");
|
|
5
|
+
const fetchedAt = database.prepare("SELECT at FROM fetched WHERE kind = ?");
|
|
6
|
+
const isFresh = (kind, freshForMs) => {
|
|
7
|
+
const at = fetchedAt.get(kind)?.at;
|
|
8
|
+
return at !== undefined && now() - at <= freshForMs;
|
|
9
|
+
};
|
|
10
|
+
const putChat = database.prepare(`
|
|
11
|
+
INSERT INTO chats (id, title, kind, unread_count, last_message_at, participants_count, fetched_at)
|
|
12
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
13
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
14
|
+
title = excluded.title, kind = excluded.kind, unread_count = excluded.unread_count,
|
|
15
|
+
last_message_at = excluded.last_message_at, participants_count = excluded.participants_count,
|
|
16
|
+
fetched_at = excluded.fetched_at, generation = chats.generation + 1`);
|
|
17
|
+
const putContact = database.prepare(`
|
|
18
|
+
INSERT INTO contacts (id, name, username, description, fetched_at) VALUES (?, ?, ?, ?, ?)
|
|
19
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
20
|
+
name = excluded.name, username = excluded.username,
|
|
21
|
+
description = excluded.description, fetched_at = excluded.fetched_at`);
|
|
22
|
+
/**
|
|
23
|
+
* **Compares MAX's clock, never ours.** `update_time` is what MAX sets on an edit and is `NULL`
|
|
24
|
+
* on a message nobody has changed (measured 2026-09-20).
|
|
25
|
+
*
|
|
26
|
+
* The comparison has to be on that value rather than on when we fetched, and the difference is
|
|
27
|
+
* not academic: agent A reads a chat after an edit, agent B read it before but finishes writing
|
|
28
|
+
* second. Ordering on fetch time puts B's pre-edit text back over A's correct one. Ordering on
|
|
29
|
+
* MAX's `update_time`, an edit always beats a version that never saw it, whoever writes last.
|
|
30
|
+
*
|
|
31
|
+
* `>=` rather than `>` so that two ordinary re-fetches of the same unedited message both go
|
|
32
|
+
* through — they carry identical text, so the write is harmless and refreshing `fetched_at` is
|
|
33
|
+
* the point of it.
|
|
34
|
+
*/
|
|
35
|
+
const putMessage = database.prepare(`
|
|
36
|
+
INSERT INTO messages (chat_id, id, sender_id, sender_name, time, update_time, text, outgoing, attachments, fetched_at)
|
|
37
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
38
|
+
ON CONFLICT(chat_id, id) DO UPDATE SET
|
|
39
|
+
sender_name = excluded.sender_name, update_time = excluded.update_time, text = excluded.text,
|
|
40
|
+
outgoing = excluded.outgoing, attachments = excluded.attachments, fetched_at = excluded.fetched_at
|
|
41
|
+
WHERE coalesce(excluded.update_time, 0) >= coalesce(messages.update_time, 0)`);
|
|
42
|
+
const toChat = (row) => ({
|
|
43
|
+
id: String(row.id),
|
|
44
|
+
title: row.title === null ? null : String(row.title),
|
|
45
|
+
kind: String(row.kind),
|
|
46
|
+
unreadCount: row.unread_count === null ? null : Number(row.unread_count),
|
|
47
|
+
lastMessageAt: row.last_message_at === null ? null : new Date(Number(row.last_message_at)).toISOString(),
|
|
48
|
+
participantsCount: row.participants_count === null ? null : Number(row.participants_count),
|
|
49
|
+
});
|
|
50
|
+
const toMessage = (row) => ({
|
|
51
|
+
id: String(row.id),
|
|
52
|
+
chatId: String(row.chat_id),
|
|
53
|
+
senderId: row.sender_id === null ? null : String(row.sender_id),
|
|
54
|
+
senderName: row.sender_name === null ? null : String(row.sender_name),
|
|
55
|
+
timestamp: new Date(Number(row.time)).toISOString(),
|
|
56
|
+
editedAt: row.update_time === null ? null : new Date(Number(row.update_time)).toISOString(),
|
|
57
|
+
text: String(row.text),
|
|
58
|
+
outgoing: row.outgoing === null ? null : row.outgoing === 1,
|
|
59
|
+
attachments: JSON.parse(String(row.attachments)),
|
|
60
|
+
});
|
|
61
|
+
const epoch = (iso) => (iso === null ? null : new Date(iso).getTime());
|
|
62
|
+
return {
|
|
63
|
+
chats: {
|
|
64
|
+
read: (freshForMs) => {
|
|
65
|
+
if (!isFresh("chats", freshForMs))
|
|
66
|
+
return undefined;
|
|
67
|
+
return database.prepare("SELECT * FROM chats ORDER BY last_message_at DESC").all().map(toChat);
|
|
68
|
+
},
|
|
69
|
+
write: (chats) => {
|
|
70
|
+
const at = now();
|
|
71
|
+
for (const chat of chats) {
|
|
72
|
+
putChat.run(chat.id, chat.title, chat.kind, chat.unreadCount, epoch(chat.lastMessageAt), chat.participantsCount, at);
|
|
73
|
+
}
|
|
74
|
+
markFetched.run("chats", at);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
contacts: {
|
|
78
|
+
read: (freshForMs) => {
|
|
79
|
+
if (!isFresh("contacts", freshForMs))
|
|
80
|
+
return undefined;
|
|
81
|
+
return database
|
|
82
|
+
.prepare("SELECT * FROM contacts ORDER BY name")
|
|
83
|
+
.all()
|
|
84
|
+
.map((row) => ({
|
|
85
|
+
id: String(row.id),
|
|
86
|
+
name: row.name === null ? null : String(row.name),
|
|
87
|
+
username: row.username === null ? null : String(row.username),
|
|
88
|
+
description: row.description === null ? null : String(row.description),
|
|
89
|
+
}));
|
|
90
|
+
},
|
|
91
|
+
write: (contacts) => {
|
|
92
|
+
const at = now();
|
|
93
|
+
for (const contact of contacts) {
|
|
94
|
+
putContact.run(contact.id, contact.name, contact.username, contact.description, at);
|
|
95
|
+
}
|
|
96
|
+
markFetched.run("contacts", at);
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
messages: {
|
|
100
|
+
read: (chatId, limit, freshForMs) => {
|
|
101
|
+
if (!isFresh(`messages:${chatId}`, freshForMs))
|
|
102
|
+
return undefined;
|
|
103
|
+
const rows = database
|
|
104
|
+
.prepare("SELECT * FROM messages WHERE chat_id = ? ORDER BY time DESC LIMIT ?")
|
|
105
|
+
.all(chatId, limit);
|
|
106
|
+
// A window we never claimed to hold completely cannot answer "the newest twenty": we have
|
|
107
|
+
// no way to know whether a twenty-first arrived between the fetch and now.
|
|
108
|
+
const range = database
|
|
109
|
+
.prepare("SELECT to_time FROM ranges WHERE chat_id = ? ORDER BY to_time DESC LIMIT 1")
|
|
110
|
+
.get(chatId);
|
|
111
|
+
if (!range)
|
|
112
|
+
return undefined;
|
|
113
|
+
return rows.map(toMessage).reverse();
|
|
114
|
+
},
|
|
115
|
+
write: (chatId, messages) => {
|
|
116
|
+
if (messages.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
const at = now();
|
|
119
|
+
for (const message of messages) {
|
|
120
|
+
putMessage.run(chatId, message.id, message.senderId, message.senderName, epoch(message.timestamp) ?? 0, epoch(message.editedAt), message.text, message.outgoing === null ? null : Number(message.outgoing), JSON.stringify(message.attachments), at);
|
|
121
|
+
}
|
|
122
|
+
const times = messages.map((message) => epoch(message.timestamp) ?? 0);
|
|
123
|
+
database
|
|
124
|
+
.prepare(`INSERT INTO ranges (chat_id, from_time, to_time) VALUES (?, ?, ?)
|
|
125
|
+
ON CONFLICT(chat_id, from_time) DO UPDATE SET to_time = max(ranges.to_time, excluded.to_time)`)
|
|
126
|
+
.run(chatId, Math.min(...times), Math.max(...times));
|
|
127
|
+
markFetched.run(`messages:${chatId}`, at);
|
|
128
|
+
},
|
|
129
|
+
invalidate: (chatId) => {
|
|
130
|
+
database.prepare("DELETE FROM fetched WHERE kind = ?").run(`messages:${chatId}`);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
claim: (chatId, anchor, holder, forMs) => {
|
|
134
|
+
const at = now();
|
|
135
|
+
return (database
|
|
136
|
+
.prepare(`INSERT INTO fetch_lease (chat_id, anchor, holder, expires_at) VALUES (?, ?, ?, ?)
|
|
137
|
+
ON CONFLICT(chat_id, anchor) DO UPDATE
|
|
138
|
+
SET holder = excluded.holder, expires_at = excluded.expires_at
|
|
139
|
+
WHERE fetch_lease.expires_at <= ?`)
|
|
140
|
+
.run(chatId, anchor, holder, at + forMs, at).changes === 1);
|
|
141
|
+
},
|
|
142
|
+
release: (chatId, anchor) => {
|
|
143
|
+
database.prepare("DELETE FROM fetch_lease WHERE chat_id = ? AND anchor = ?").run(chatId, anchor);
|
|
144
|
+
},
|
|
145
|
+
clear: () => {
|
|
146
|
+
for (const table of ["messages", "chats", "contacts", "ranges", "fetched", "fetch_lease"]) {
|
|
147
|
+
database.exec(`DELETE FROM ${table}`);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
close: () => database.close(),
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/cache/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAgCrC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,EAAE,QAAQ,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAgB,EAAc,EAAE;IAC1F,OAAO,CAAC,QAAQ,CAAC,CAAA;IAEjB,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAClC,+FAA+F,CAChG,CAAA;IACD,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,uCAAuC,CAAC,CAAA;IAE3E,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,UAAkB,EAAW,EAAE;QAC5D,MAAM,EAAE,GAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAiC,EAAE,EAAE,CAAA;QACnE,OAAO,EAAE,KAAK,SAAS,IAAI,GAAG,EAAE,GAAG,EAAE,IAAI,UAAU,CAAA;IACrD,CAAC,CAAA;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;;;;;;0EAMuC,CAAC,CAAA;IAEzE,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC;;;;2EAIqC,CAAC,CAAA;IAE1E;;;;;;;;;;;;OAYG;IACH,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC;;;;;;iFAM2C,CAAC,CAAA;IAEhF,MAAM,MAAM,GAAG,CAAC,GAA4B,EAAQ,EAAE,CAAC,CAAC;QACtD,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QAClB,KAAK,EAAE,GAAG,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;QACpD,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAiB;QACtC,WAAW,EAAE,GAAG,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC;QACxE,aAAa,EAAE,GAAG,CAAC,eAAe,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,WAAW,EAAE;QACxG,iBAAiB,EAAE,GAAG,CAAC,kBAAkB,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC;KAC3F,CAAC,CAAA;IAEF,MAAM,SAAS,GAAG,CAAC,GAA4B,EAAW,EAAE,CAAC,CAAC;QAC5D,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;QAC3B,QAAQ,EAAE,GAAG,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;QAC/D,UAAU,EAAE,GAAG,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC;QACrE,SAAS,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE;QACnD,QAAQ,EAAE,GAAG,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,EAAE;QAC3F,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;QACtB,QAAQ,EAAE,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,CAAC;QAC3D,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAA2B;KAC3E,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,CAAC,GAAkB,EAAiB,EAAE,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,CAAA;IAEpG,OAAO;QACL,KAAK,EAAE;YACL,IAAI,EAAE,CAAC,UAAU,EAAE,EAAE;gBACnB,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC;oBAAE,OAAO,SAAS,CAAA;gBACnD,OAAO,QAAQ,CAAC,OAAO,CAAC,mDAAmD,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAChG,CAAC;YACD,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE;gBACf,MAAM,EAAE,GAAG,GAAG,EAAE,CAAA;gBAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACzB,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,EAAE,EACP,IAAI,CAAC,KAAK,EACV,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,WAAW,EAChB,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,EACzB,IAAI,CAAC,iBAAiB,EACtB,EAAE,CACH,CAAA;gBACH,CAAC;gBACD,WAAW,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;YAC9B,CAAC;SACF;QAED,QAAQ,EAAE;YACR,IAAI,EAAE,CAAC,UAAU,EAAE,EAAE;gBACnB,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,UAAU,CAAC;oBAAE,OAAO,SAAS,CAAA;gBACtD,OAAO,QAAQ;qBACZ,OAAO,CAAC,sCAAsC,CAAC;qBAC/C,GAAG,EAAE;qBACL,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;oBACb,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;oBAClB,IAAI,EAAE,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;oBACjD,QAAQ,EAAE,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;oBAC7D,WAAW,EAAE,GAAG,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC;iBACvE,CAAC,CAAC,CAAA;YACP,CAAC;YACD,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE;gBAClB,MAAM,EAAE,GAAG,GAAG,EAAE,CAAA;gBAChB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;oBAC/B,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;gBACrF,CAAC;gBACD,WAAW,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;YACjC,CAAC;SACF;QAED,QAAQ,EAAE;YACR,IAAI,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;gBAClC,IAAI,CAAC,OAAO,CAAC,YAAY,MAAM,EAAE,EAAE,UAAU,CAAC;oBAAE,OAAO,SAAS,CAAA;gBAChE,MAAM,IAAI,GAAG,QAAQ;qBAClB,OAAO,CAAC,qEAAqE,CAAC;qBAC9E,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;gBAErB,0FAA0F;gBAC1F,2EAA2E;gBAC3E,MAAM,KAAK,GAAG,QAAQ;qBACnB,OAAO,CAAC,4EAA4E,CAAC;qBACrF,GAAG,CAAC,MAAM,CAAC,CAAA;gBACd,IAAI,CAAC,KAAK;oBAAE,OAAO,SAAS,CAAA;gBAE5B,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAA;YACtC,CAAC;YACD,KAAK,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;gBAC1B,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;oBAAE,OAAM;gBACjC,MAAM,EAAE,GAAG,GAAG,EAAE,CAAA;gBAChB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;oBAC/B,UAAU,CAAC,GAAG,CACZ,MAAM,EACN,OAAO,CAAC,EAAE,EACV,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,UAAU,EAClB,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,EAC7B,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EACvB,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAC3D,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,EACnC,EAAE,CACH,CAAA;gBACH,CAAC;gBAED,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;gBACtE,QAAQ;qBACL,OAAO,CACN;2GAC+F,CAChG;qBACA,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAA;gBACtD,WAAW,CAAC,GAAG,CAAC,YAAY,MAAM,EAAE,EAAE,EAAE,CAAC,CAAA;YAC3C,CAAC;YACD,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE;gBACrB,QAAQ,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC,GAAG,CAAC,YAAY,MAAM,EAAE,CAAC,CAAA;YAClF,CAAC;SACF;QAED,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;YACvC,MAAM,EAAE,GAAG,GAAG,EAAE,CAAA;YAChB,OAAO,CACL,QAAQ;iBACL,OAAO,CACN;;;iDAGqC,CACtC;iBACA,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,KAAK,CAAC,CAC7D,CAAA;QACH,CAAC;QAED,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;YAC1B,QAAQ,CAAC,OAAO,CAAC,0DAA0D,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAClG,CAAC;QAED,KAAK,EAAE,GAAG,EAAE;YACV,KAAK,MAAM,KAAK,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,EAAE,CAAC;gBAC1F,QAAQ,CAAC,IAAI,CAAC,eAAe,KAAK,EAAE,CAAC,CAAA;YACvC,CAAC;QACH,CAAC;QAED,KAAK,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE;KAC9B,CAAA;AACH,CAAC,CAAA"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { CacheStore } from "./cache/store.js";
|
|
2
|
+
import type { Chat, Contact, Id, Message, Profile } from "./domain/models.js";
|
|
3
|
+
import { Connection } from "./protocol/connection.js";
|
|
4
|
+
import { type DiagnosticEvent } from "./runs/events.js";
|
|
5
|
+
import type { SessionStore } from "./session/store.js";
|
|
6
|
+
export interface MaxClientOptions {
|
|
7
|
+
store: SessionStore;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
/** Injected by tests; defaults to a real WebSocket connection. */
|
|
10
|
+
connection?: Connection;
|
|
11
|
+
/**
|
|
12
|
+
* Where a protocol note goes. Never stdout: in machine mode that stream carries one JSON value
|
|
13
|
+
* and nothing else.
|
|
14
|
+
*/
|
|
15
|
+
warn?: (message: string) => void;
|
|
16
|
+
/** Where reads are recorded. It is a record, not a shortcut — see `offline`. */
|
|
17
|
+
cache?: CacheStore;
|
|
18
|
+
/**
|
|
19
|
+
* Answer from what was recorded and **never connect**.
|
|
20
|
+
*
|
|
21
|
+
* Off by default, and deliberately: `LOGIN` already returns the chats, the contacts and recent
|
|
22
|
+
* messages, so once a command connects at all those are fresh for free. Serving them from disk
|
|
23
|
+
* instead buys only the right to skip connecting — which is the same act as deciding not to find
|
|
24
|
+
* out what changed. For a messenger that is backwards (§3.4z of the cache plan).
|
|
25
|
+
*/
|
|
26
|
+
offline?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* One event per request, for whoever is keeping a diagnostic. Injected the way `warn` is: the
|
|
29
|
+
* client reports what it did and never decides where that goes. Absent means nothing is kept.
|
|
30
|
+
*/
|
|
31
|
+
events?: (event: DiagnosticEvent) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* **The only thing above this line that knows MAX exists.** Commands speak the domain model; the
|
|
35
|
+
* opcodes and the frames stop here, so a different transport underneath changes this file and
|
|
36
|
+
* nothing else (REQUIREMENTS §25).
|
|
37
|
+
*
|
|
38
|
+
* The methods are grouped by subject — `client.chats.list()`, `client.messages.send()` — which is
|
|
39
|
+
* the shape §8 sketched. **There is only this door.** The generated per-operation wrappers sit
|
|
40
|
+
* underneath and stay internal: they return raw MAX payloads, without the resend rule, the name
|
|
41
|
+
* filling or the id conversion that make this safe to point at a real account (`NEED-34`).
|
|
42
|
+
*
|
|
43
|
+
* One instance is one connection. `open` then `close`, always in a `finally`.
|
|
44
|
+
*/
|
|
45
|
+
export declare class MaxClient {
|
|
46
|
+
#private;
|
|
47
|
+
constructor({ store, timeoutMs, connection, warn, cache, offline, events }: MaxClientOptions);
|
|
48
|
+
readonly account: {
|
|
49
|
+
me: () => Profile;
|
|
50
|
+
};
|
|
51
|
+
readonly chats: {
|
|
52
|
+
/**
|
|
53
|
+
* Chats come with the login; a limit trims rather than fetching more.
|
|
54
|
+
*
|
|
55
|
+
* **A one-to-one chat has no title of its own** — its name is the other person's, which MAX
|
|
56
|
+
* does not put in the chat object. So the partner is looked up once, for every dialog at a
|
|
57
|
+
* time, and the name is filled in. Without this, `max chats` shows a column of blanks for
|
|
58
|
+
* exactly the chats a person recognises by name.
|
|
59
|
+
*/
|
|
60
|
+
list: (limit?: number) => Promise<Chat[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Turns what the person typed into a chat id.
|
|
63
|
+
*
|
|
64
|
+
* A number is taken as an id. Anything else is matched against chat titles, exactly first and
|
|
65
|
+
* then as a fragment — and **an ambiguous name is an error, not a guess**: sending to the wrong
|
|
66
|
+
* conversation is not undoable, so the caller is shown the candidates and asked to be specific.
|
|
67
|
+
*/
|
|
68
|
+
resolve: (reference: string) => Promise<Id>;
|
|
69
|
+
};
|
|
70
|
+
readonly contacts: {
|
|
71
|
+
/** The people this account has a one-to-one chat with, named. */
|
|
72
|
+
list: () => Promise<Contact[]>;
|
|
73
|
+
};
|
|
74
|
+
readonly messages: {
|
|
75
|
+
list: (chatId: Id, limit?: number) => Promise<Message[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Sends one message, retrying **only with the same `cid`**.
|
|
78
|
+
*
|
|
79
|
+
* §17 allows a send to be retried when the protocol has a verified deduplication handle. It has
|
|
80
|
+
* one, measured against MAX on 2026-09-19: sending the same `cid` twice returned the same
|
|
81
|
+
* message id and left **one** copy in the chat — and it held across two separate connections
|
|
82
|
+
* and logins, which is the case a retry actually faces.
|
|
83
|
+
*
|
|
84
|
+
* So one retry, same `cid`, and nothing beyond that. What is still unmeasured is how long the
|
|
85
|
+
* server remembers a `cid`; the two probes were seconds apart. If the retry also fails the
|
|
86
|
+
* answer is `outcome_unknown` — never failed, never sent — and it names the `cid`, because
|
|
87
|
+
* `max send --cid <n>` can then repeat the attempt without risking a second message.
|
|
88
|
+
*/
|
|
89
|
+
send: (chatId: Id, text: string, options?: {
|
|
90
|
+
cid?: number;
|
|
91
|
+
notify?: boolean;
|
|
92
|
+
}) => Promise<Message>;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Opens the connection and logs in with the stored token.
|
|
96
|
+
*
|
|
97
|
+
* The login response carries the profile, the chats, the contacts and recent messages, so most
|
|
98
|
+
* commands need no further request — measured against MAX on 2026-09-19.
|
|
99
|
+
*/
|
|
100
|
+
connect(): Promise<void>;
|
|
101
|
+
close(): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AAG7E,OAAO,EAAE,UAAU,EAAiB,MAAM,0BAA0B,CAAA;AAEpE,OAAO,EAAY,KAAK,eAAe,EAAS,MAAM,kBAAkB,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAGtD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kEAAkE;IAClE,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB;;;OAGG;IACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,gFAAgF;IAChF,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;CAC1C;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,SAAS;;gBAaR,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,OAAe,EAAE,MAAM,EAAE,EAAE,gBAAgB;IASpG,QAAQ,CAAC,OAAO;kBACN,OAAO;MAChB;IAED,QAAQ,CAAC,KAAK;QACZ;;;;;;;WAOG;uBACkB,MAAM,KAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAuB7C;;;;;;WAMG;6BACwB,MAAM,KAAG,OAAO,CAAC,EAAE,CAAC;MAgBhD;IAED,QAAQ,CAAC,QAAQ;QACf,iEAAiE;oBACjD,OAAO,CAAC,OAAO,EAAE,CAAC;MASnC;IAED,QAAQ,CAAC,QAAQ;uBACM,EAAE,qBAAe,OAAO,CAAC,OAAO,EAAE,CAAC;QA0BxD;;;;;;;;;;;;WAYG;uBACkB,EAAE,QAAQ,MAAM,YAAW;YAAE,GAAG,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,OAAO,CAAA;SAAE,KAAQ,OAAO,CAAC,OAAO,CAAC;MAuC3G;IAED;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA6BxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAyJ7B"}
|