@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.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/dist/bin/max.d.ts +3 -0
  4. package/dist/bin/max.d.ts.map +1 -0
  5. package/dist/bin/max.js +14 -0
  6. package/dist/bin/max.js.map +1 -0
  7. package/dist/cache/driver.d.ts +38 -0
  8. package/dist/cache/driver.d.ts.map +1 -0
  9. package/dist/cache/driver.js +26 -0
  10. package/dist/cache/driver.js.map +1 -0
  11. package/dist/cache/drivers/bun-sqlite.d.ts +8 -0
  12. package/dist/cache/drivers/bun-sqlite.d.ts.map +1 -0
  13. package/dist/cache/drivers/bun-sqlite.js +23 -0
  14. package/dist/cache/drivers/bun-sqlite.js.map +1 -0
  15. package/dist/cache/drivers/node-sqlite.d.ts +8 -0
  16. package/dist/cache/drivers/node-sqlite.d.ts.map +1 -0
  17. package/dist/cache/drivers/node-sqlite.js +22 -0
  18. package/dist/cache/drivers/node-sqlite.js.map +1 -0
  19. package/dist/cache/index.d.ts +22 -0
  20. package/dist/cache/index.d.ts.map +1 -0
  21. package/dist/cache/index.js +33 -0
  22. package/dist/cache/index.js.map +1 -0
  23. package/dist/cache/open.d.ts +14 -0
  24. package/dist/cache/open.d.ts.map +1 -0
  25. package/dist/cache/open.js +26 -0
  26. package/dist/cache/open.js.map +1 -0
  27. package/dist/cache/schema.d.ts +14 -0
  28. package/dist/cache/schema.d.ts.map +1 -0
  29. package/dist/cache/schema.js +95 -0
  30. package/dist/cache/schema.js.map +1 -0
  31. package/dist/cache/store.d.ts +32 -0
  32. package/dist/cache/store.d.ts.map +1 -0
  33. package/dist/cache/store.js +153 -0
  34. package/dist/cache/store.js.map +1 -0
  35. package/dist/client.d.ts +103 -0
  36. package/dist/client.d.ts.map +1 -0
  37. package/dist/client.js +383 -0
  38. package/dist/client.js.map +1 -0
  39. package/dist/commands/account.d.ts +3 -0
  40. package/dist/commands/account.d.ts.map +1 -0
  41. package/dist/commands/account.js +24 -0
  42. package/dist/commands/account.js.map +1 -0
  43. package/dist/commands/cache.d.ts +10 -0
  44. package/dist/commands/cache.d.ts.map +1 -0
  45. package/dist/commands/cache.js +36 -0
  46. package/dist/commands/cache.js.map +1 -0
  47. package/dist/commands/chats.d.ts +3 -0
  48. package/dist/commands/chats.d.ts.map +1 -0
  49. package/dist/commands/chats.js +28 -0
  50. package/dist/commands/chats.js.map +1 -0
  51. package/dist/commands/contacts.d.ts +10 -0
  52. package/dist/commands/contacts.d.ts.map +1 -0
  53. package/dist/commands/contacts.js +33 -0
  54. package/dist/commands/contacts.js.map +1 -0
  55. package/dist/commands/context.d.ts +34 -0
  56. package/dist/commands/context.d.ts.map +1 -0
  57. package/dist/commands/context.js +40 -0
  58. package/dist/commands/context.js.map +1 -0
  59. package/dist/commands/messages.d.ts +3 -0
  60. package/dist/commands/messages.d.ts.map +1 -0
  61. package/dist/commands/messages.js +62 -0
  62. package/dist/commands/messages.js.map +1 -0
  63. package/dist/commands/runs.d.ts +9 -0
  64. package/dist/commands/runs.d.ts.map +1 -0
  65. package/dist/commands/runs.js +68 -0
  66. package/dist/commands/runs.js.map +1 -0
  67. package/dist/commands/session.d.ts +3 -0
  68. package/dist/commands/session.d.ts.map +1 -0
  69. package/dist/commands/session.js +74 -0
  70. package/dist/commands/session.js.map +1 -0
  71. package/dist/config.d.ts +53 -0
  72. package/dist/config.d.ts.map +1 -0
  73. package/dist/config.js +83 -0
  74. package/dist/config.js.map +1 -0
  75. package/dist/domain/map.d.ts +22 -0
  76. package/dist/domain/map.d.ts.map +1 -0
  77. package/dist/domain/map.js +107 -0
  78. package/dist/domain/map.js.map +1 -0
  79. package/dist/domain/models.d.ts +57 -0
  80. package/dist/domain/models.d.ts.map +1 -0
  81. package/dist/domain/models.js +7 -0
  82. package/dist/domain/models.js.map +1 -0
  83. package/dist/generated/client.generated.d.ts +30 -0
  84. package/dist/generated/client.generated.d.ts.map +1 -0
  85. package/dist/generated/client.generated.js +26 -0
  86. package/dist/generated/client.generated.js.map +1 -0
  87. package/dist/generated/opcodes.generated.d.ts +44 -0
  88. package/dist/generated/opcodes.generated.d.ts.map +1 -0
  89. package/dist/generated/opcodes.generated.js +44 -0
  90. package/dist/generated/opcodes.generated.js.map +1 -0
  91. package/dist/generated/operations.generated.d.ts +76 -0
  92. package/dist/generated/operations.generated.d.ts.map +1 -0
  93. package/dist/generated/operations.generated.js +15 -0
  94. package/dist/generated/operations.generated.js.map +1 -0
  95. package/dist/output.d.ts +22 -0
  96. package/dist/output.d.ts.map +1 -0
  97. package/dist/output.js +34 -0
  98. package/dist/output.js.map +1 -0
  99. package/dist/profile.d.ts +33 -0
  100. package/dist/profile.d.ts.map +1 -0
  101. package/dist/profile.js +60 -0
  102. package/dist/profile.js.map +1 -0
  103. package/dist/program.d.ts +29 -0
  104. package/dist/program.d.ts.map +1 -0
  105. package/dist/program.js +130 -0
  106. package/dist/program.js.map +1 -0
  107. package/dist/protocol/connection.d.ts +60 -0
  108. package/dist/protocol/connection.d.ts.map +1 -0
  109. package/dist/protocol/connection.js +142 -0
  110. package/dist/protocol/connection.js.map +1 -0
  111. package/dist/protocol/frame.d.ts +44 -0
  112. package/dist/protocol/frame.d.ts.map +1 -0
  113. package/dist/protocol/frame.js +68 -0
  114. package/dist/protocol/frame.js.map +1 -0
  115. package/dist/runs/events.d.ts +92 -0
  116. package/dist/runs/events.d.ts.map +1 -0
  117. package/dist/runs/events.js +85 -0
  118. package/dist/runs/events.js.map +1 -0
  119. package/dist/runs/recording.d.ts +35 -0
  120. package/dist/runs/recording.d.ts.map +1 -0
  121. package/dist/runs/recording.js +53 -0
  122. package/dist/runs/recording.js.map +1 -0
  123. package/dist/runs/run.d.ts +81 -0
  124. package/dist/runs/run.d.ts.map +1 -0
  125. package/dist/runs/run.js +175 -0
  126. package/dist/runs/run.js.map +1 -0
  127. package/dist/session/handshake.d.ts +26 -0
  128. package/dist/session/handshake.d.ts.map +1 -0
  129. package/dist/session/handshake.js +32 -0
  130. package/dist/session/handshake.js.map +1 -0
  131. package/dist/session/prompt.d.ts +18 -0
  132. package/dist/session/prompt.d.ts.map +1 -0
  133. package/dist/session/prompt.js +41 -0
  134. package/dist/session/prompt.js.map +1 -0
  135. package/dist/session/store.d.ts +46 -0
  136. package/dist/session/store.d.ts.map +1 -0
  137. package/dist/session/store.js +80 -0
  138. package/dist/session/store.js.map +1 -0
  139. package/dist/spec/check.d.ts +16 -0
  140. package/dist/spec/check.d.ts.map +1 -0
  141. package/dist/spec/check.js +44 -0
  142. package/dist/spec/check.js.map +1 -0
  143. package/dist/spec/define.d.ts +95 -0
  144. package/dist/spec/define.d.ts.map +1 -0
  145. package/dist/spec/define.js +34 -0
  146. package/dist/spec/define.js.map +1 -0
  147. package/dist/spec/identity.d.ts +23 -0
  148. package/dist/spec/identity.d.ts.map +1 -0
  149. package/dist/spec/identity.js +23 -0
  150. package/dist/spec/identity.js.map +1 -0
  151. package/dist/spec/index.d.ts +15 -0
  152. package/dist/spec/index.d.ts.map +1 -0
  153. package/dist/spec/index.js +30 -0
  154. package/dist/spec/index.js.map +1 -0
  155. package/dist/spec/operations/chats.d.ts +25 -0
  156. package/dist/spec/operations/chats.d.ts.map +1 -0
  157. package/dist/spec/operations/chats.js +49 -0
  158. package/dist/spec/operations/chats.js.map +1 -0
  159. package/dist/spec/operations/contacts.d.ts +9 -0
  160. package/dist/spec/operations/contacts.d.ts.map +1 -0
  161. package/dist/spec/operations/contacts.js +39 -0
  162. package/dist/spec/operations/contacts.js.map +1 -0
  163. package/dist/spec/operations/messages.d.ts +19 -0
  164. package/dist/spec/operations/messages.d.ts.map +1 -0
  165. package/dist/spec/operations/messages.js +37 -0
  166. package/dist/spec/operations/messages.js.map +1 -0
  167. package/dist/spec/operations/session.d.ts +69 -0
  168. package/dist/spec/operations/session.d.ts.map +1 -0
  169. package/dist/spec/operations/session.js +106 -0
  170. package/dist/spec/operations/session.js.map +1 -0
  171. package/dist/spec/scalars.d.ts +16 -0
  172. package/dist/spec/scalars.d.ts.map +1 -0
  173. package/dist/spec/scalars.js +28 -0
  174. package/dist/spec/scalars.js.map +1 -0
  175. package/dist/testing/mock-max.d.ts +29 -0
  176. package/dist/testing/mock-max.d.ts.map +1 -0
  177. package/dist/testing/mock-max.js +46 -0
  178. package/dist/testing/mock-max.js.map +1 -0
  179. package/dist/version.d.ts +3 -0
  180. package/dist/version.d.ts.map +1 -0
  181. package/dist/version.js +3 -0
  182. package/dist/version.js.map +1 -0
  183. package/package.json +58 -0
@@ -0,0 +1,130 @@
1
+ import { CliError, exitCodeFor, GENERIC_FAILURE, processStreams } from "@leemour/cli-core";
2
+ import { Command, CommanderError } from "commander";
3
+ import { accountCommand } from "./commands/account.js";
4
+ import { cacheCommand } from "./commands/cache.js";
5
+ import { chatsCommand } from "./commands/chats.js";
6
+ import { contactsCommand } from "./commands/contacts.js";
7
+ import { messagesCommand } from "./commands/messages.js";
8
+ import { runsCommand } from "./commands/runs.js";
9
+ import { sessionCommand } from "./commands/session.js";
10
+ import { commandWords, liftProfile } from "./profile.js";
11
+ import { VERSION } from "./version.js";
12
+ /**
13
+ * The command tree, built fresh on each call.
14
+ *
15
+ * Commander is global state by default — `exitOverride` and the write hooks below turn it into a
16
+ * value a test can drive. A command that calls `process.exit` cannot be tested, and a CLI whose
17
+ * argument parsing is untested is a CLI that breaks on the day someone adds an option.
18
+ */
19
+ export const createProgram = ({ out, err } = {}) => {
20
+ const program = new Command();
21
+ program
22
+ .name("max")
23
+ .usage("[profile] [options] <command>")
24
+ .description("A local command line interface for a personal MAX Messenger account\n\n" +
25
+ "The first word is the profile whenever it is not a command — `max personal chats list`.\n" +
26
+ "`MAX_PROFILE` says the same thing for a whole shell session; without either it is `default`.")
27
+ .version(VERSION, "-v, --version")
28
+ .option("--json", "machine-readable output: one JSON value on stdout, nothing else")
29
+ .option("--quiet", "diagnostics off")
30
+ .option("--verbose", "diagnostics on: ids and timings on stderr, never message content")
31
+ .option("--offline", "answer from what was recorded and never connect; fails if nothing was")
32
+ .option("--record", "keep this run under `max runs` — ids and timings, never message content")
33
+ .option("--no-record", "do not keep it, whatever the configuration says")
34
+ .showHelpAfterError();
35
+ // One resource per command, one action per subcommand — `max chats list`, `max messages send`.
36
+ // The shape `braze-cli` uses, and the reason there is no `max send`: an action is never a
37
+ // top-level command, so there is one rule instead of a list of exceptions.
38
+ program.addCommand(sessionCommand());
39
+ program.addCommand(accountCommand());
40
+ program.addCommand(chatsCommand());
41
+ program.addCommand(contactsCommand());
42
+ program.addCommand(messagesCommand());
43
+ program.addCommand(cacheCommand());
44
+ program.addCommand(runsCommand());
45
+ // Depth-first: Commander does not pass `configureOutput` down to a command added with
46
+ // `addCommand`, so `max messages --help` would write to the real terminal while the top level
47
+ // wrote to the injected streams — and a test reading stdout would see nothing at all.
48
+ if (out || err) {
49
+ forEachCommand(program, (command) => command.configureOutput({
50
+ writeOut: (text) => out?.(text),
51
+ writeErr: (text) => err?.(text),
52
+ }));
53
+ }
54
+ return program;
55
+ };
56
+ const forEachCommand = (command, apply) => {
57
+ apply(command);
58
+ for (const child of command.commands)
59
+ forEachCommand(child, apply);
60
+ };
61
+ /**
62
+ * Runs the program and **returns an exit code instead of throwing**.
63
+ *
64
+ * A stack trace is not an error message: it puts Node internals on stderr, tells a script nothing
65
+ * it can branch on, and exits 1 for every kind of failure alike. Here a failure becomes a code an
66
+ * agent can switch on, and a sentence a person can act on.
67
+ */
68
+ export const run = async (argv, options = {}) => {
69
+ const streams = options.streams ?? processStreams;
70
+ const program = createProgram({
71
+ out: (text) => streams.data(text.replace(/\n$/, "")),
72
+ err: (text) => streams.diagnostic(text.replace(/\n$/, "")),
73
+ });
74
+ // Commander calls process.exit for --help and --version. A library that kills the process cannot
75
+ // be tested and cannot be embedded, so it throws instead and `run` decides the exit code.
76
+ //
77
+ // Depth-first, not one level: `max chats list --nonsense` is handled by the subcommand, and a
78
+ // subcommand left with the default behaviour kills the process from inside a test.
79
+ forEachCommand(program, (command) => command.exitOverride());
80
+ // Before commander, not inside it: a custom argument parser would still have to be told that
81
+ // the first word is sometimes a command, and commander has no hook that runs before it decides
82
+ // which subcommand it is looking at.
83
+ const { profile, rest } = liftProfile(argv, commandWords(program));
84
+ if (profile !== undefined)
85
+ program.setOptionValue("profile", profile);
86
+ // `max me` — a command that was renamed away — is now a profile with nothing after it, and
87
+ // commander answers a missing command by printing help **on stdout**. That breaks the one
88
+ // contract this program has, and it tells the person nothing about why their command vanished.
89
+ if (profile !== undefined && rest.length === 0) {
90
+ report(streams, options, {
91
+ code: "validation_error",
92
+ message: `"${profile}" is not a command, so it was read as a profile name — and no command followed it. ` +
93
+ `Run \`max --help\` for the commands, or \`max ${profile} account show\` if "${profile}" is your profile.`,
94
+ });
95
+ return exitCodeFor("validation_error");
96
+ }
97
+ try {
98
+ await program.parseAsync(rest, { from: "user" });
99
+ return process.exitCode === undefined ? 0 : Number(process.exitCode);
100
+ }
101
+ catch (error) {
102
+ if (error instanceof CommanderError) {
103
+ // `max chat list` — one letter short of `chats` — now reports an unknown command `list`,
104
+ // which is baffling on its own. This is the everyday cost of the first word being a profile.
105
+ if (profile !== undefined && error.code === "commander.unknownCommand") {
106
+ streams.diagnostic(`"${profile}" is not a command, so it was read as a profile name — which left "${rest[0]}" to be one.`);
107
+ }
108
+ return error.exitCode;
109
+ }
110
+ if (error instanceof CliError) {
111
+ report(streams, options, { code: error.code, message: error.message, ...error.details });
112
+ return exitCodeFor(error.code);
113
+ }
114
+ report(streams, options, {
115
+ code: "generic_failure",
116
+ message: error instanceof Error ? error.message : String(error),
117
+ });
118
+ return GENERIC_FAILURE;
119
+ }
120
+ };
121
+ /**
122
+ * **A failure never reaches stdout.** An agent reading stdout must not be able to mistake a refusal
123
+ * for a result, so the error goes to stderr — as JSON when nobody is watching, because an exit code
124
+ * says which kind of thing went wrong and nothing about which chat or how long to wait.
125
+ */
126
+ const report = (streams, options, error) => {
127
+ const interactive = options.tty ?? process.stdout.isTTY === true;
128
+ streams.diagnostic(interactive ? `\u2717 ${error.message}` : JSON.stringify({ error }));
129
+ };
130
+ //# sourceMappingURL=program.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"program.js","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { WebSocket } from "ws";
2
+ import { type InboundFrame, type Payload } from "./frame.js";
3
+ export declare const MAX_WEBSOCKET_URL = "wss://ws-api.oneme.ru/websocket";
4
+ /** MAX's web client sends this; we send what it sends rather than announcing ourselves (§34). */
5
+ export declare const WEB_ORIGIN = "https://web.max.ru";
6
+ /**
7
+ * What one frame cost, reported as it happens.
8
+ *
9
+ * The transport is the only layer that knows the `seq` it allocated and how many bytes actually
10
+ * went over the socket, and it is the only one that can time the wait. It reports those and
11
+ * nothing else — **who is asking, and why, stays above it** (`ARCHITECTURE.md` §2).
12
+ */
13
+ export interface WireEvent {
14
+ phase: "sent" | "received";
15
+ seq: number;
16
+ opcode: number;
17
+ bytes: number;
18
+ }
19
+ export interface ConnectionOptions {
20
+ url?: string;
21
+ origin?: string;
22
+ /** Per request, not per connection. */
23
+ timeoutMs?: number;
24
+ /** Events MAX pushes on its own — messages arriving, presence. Ignored unless a caller cares. */
25
+ onEvent?: (frame: InboundFrame) => void;
26
+ /** Injected in tests; defaults to the real `ws` client. */
27
+ createSocket?: (url: string, origin: string) => WebSocket;
28
+ }
29
+ export declare class ProtocolError extends Error {
30
+ readonly opcode: number;
31
+ readonly payload: Payload | null;
32
+ constructor(message: string, opcode: number, payload: Payload | null);
33
+ }
34
+ /**
35
+ * One WebSocket, one command's worth of work, then closed.
36
+ *
37
+ * **Everything that could keep the process alive is owned here**: the socket, the per-request
38
+ * timers and the listeners. `close()` clears all three, and a command that opens a connection
39
+ * closes it in a `finally` — a CLI that prints its result and then hangs is a defect, not a rough
40
+ * edge (REQUIREMENTS §18).
41
+ */
42
+ export declare class Connection {
43
+ #private;
44
+ constructor(options?: ConnectionOptions);
45
+ open(): Promise<void>;
46
+ /**
47
+ * Sends one request and waits for the response carrying the same `seq`.
48
+ *
49
+ * MAX interleaves pushed events with responses on one socket, so the `seq` is the only thing
50
+ * tying an answer to its question — a reader that takes the next frame as its answer will
51
+ * eventually read somebody's incoming message instead.
52
+ *
53
+ * `watch` is told what left and what came back, for the caller that is keeping a diagnostic. It
54
+ * is called synchronously on the way out, so a request that never gets an answer is still on
55
+ * record — which is the run somebody actually wants to read.
56
+ */
57
+ invoke(opcode: number, payload?: Payload, watch?: (event: WireEvent) => void): Promise<Payload>;
58
+ close(): Promise<void>;
59
+ }
60
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/protocol/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAA;AAC9B,OAAO,EAAqC,KAAK,YAAY,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAA;AAE/F,eAAO,MAAM,iBAAiB,oCAAoC,CAAA;AAClE,iGAAiG;AACjG,eAAO,MAAM,UAAU,uBAAuB,CAAA;AAE9C;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,GAAG,UAAU,CAAA;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iGAAiG;IACjG,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IACvC,2DAA2D;IAC3D,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,CAAA;CAC1D;AAED,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;gBAEpB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;CAMrE;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU;;gBAmBT,OAAO,GAAE,iBAAsB;IAQrC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB3B;;;;;;;;;;OAUG;IACG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,OAAY,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAoCnG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAuC7B"}
@@ -0,0 +1,142 @@
1
+ import { WebSocket } from "ws";
2
+ import { Command, decodeFrame, encodeFrame } from "./frame.js";
3
+ export const MAX_WEBSOCKET_URL = "wss://ws-api.oneme.ru/websocket";
4
+ /** MAX's web client sends this; we send what it sends rather than announcing ourselves (§34). */
5
+ export const WEB_ORIGIN = "https://web.max.ru";
6
+ export class ProtocolError extends Error {
7
+ opcode;
8
+ payload;
9
+ constructor(message, opcode, payload) {
10
+ super(message);
11
+ this.name = "ProtocolError";
12
+ this.opcode = opcode;
13
+ this.payload = payload;
14
+ }
15
+ }
16
+ /**
17
+ * One WebSocket, one command's worth of work, then closed.
18
+ *
19
+ * **Everything that could keep the process alive is owned here**: the socket, the per-request
20
+ * timers and the listeners. `close()` clears all three, and a command that opens a connection
21
+ * closes it in a `finally` — a CLI that prints its result and then hangs is a defect, not a rough
22
+ * edge (REQUIREMENTS §18).
23
+ */
24
+ export class Connection {
25
+ #url;
26
+ #origin;
27
+ #timeoutMs;
28
+ #onEvent;
29
+ #createSocket;
30
+ #pending = new Map();
31
+ #socket;
32
+ #seq = 0;
33
+ #closed = false;
34
+ constructor(options = {}) {
35
+ this.#url = options.url ?? MAX_WEBSOCKET_URL;
36
+ this.#origin = options.origin ?? WEB_ORIGIN;
37
+ this.#timeoutMs = options.timeoutMs ?? 30_000;
38
+ this.#onEvent = options.onEvent;
39
+ this.#createSocket = options.createSocket ?? ((url, origin) => new WebSocket(url, { headers: { Origin: origin } }));
40
+ }
41
+ async open() {
42
+ if (this.#socket)
43
+ return;
44
+ const socket = this.#createSocket(this.#url, this.#origin);
45
+ this.#socket = socket;
46
+ await new Promise((resolve, reject) => {
47
+ const onOpen = () => {
48
+ socket.off("error", onError);
49
+ resolve();
50
+ };
51
+ const onError = (error) => {
52
+ socket.off("open", onOpen);
53
+ reject(error);
54
+ };
55
+ socket.once("open", onOpen);
56
+ socket.once("error", onError);
57
+ });
58
+ socket.on("message", (data) => this.#receive(String(data)));
59
+ socket.on("close", () => this.#failAll(new Error("MAX closed the connection")));
60
+ socket.on("error", (error) => this.#failAll(error));
61
+ }
62
+ /**
63
+ * Sends one request and waits for the response carrying the same `seq`.
64
+ *
65
+ * MAX interleaves pushed events with responses on one socket, so the `seq` is the only thing
66
+ * tying an answer to its question — a reader that takes the next frame as its answer will
67
+ * eventually read somebody's incoming message instead.
68
+ *
69
+ * `watch` is told what left and what came back, for the caller that is keeping a diagnostic. It
70
+ * is called synchronously on the way out, so a request that never gets an answer is still on
71
+ * record — which is the run somebody actually wants to read.
72
+ */
73
+ async invoke(opcode, payload = {}, watch) {
74
+ if (!Number.isInteger(opcode)) {
75
+ // A typo in an opcode constant is otherwise a round trip to MAX that comes back
76
+ // "неизвестный opcode", which reads as the protocol's fault rather than ours.
77
+ throw new Error(`refusing to send a frame with a non-integer opcode: ${String(opcode)}`);
78
+ }
79
+ if (!this.#socket)
80
+ throw new Error("the connection is not open");
81
+ if (this.#closed)
82
+ throw new Error("the connection is closed");
83
+ this.#seq += 1;
84
+ const seq = this.#seq;
85
+ const socket = this.#socket;
86
+ const answer = await new Promise((resolve, reject) => {
87
+ const timer = setTimeout(() => {
88
+ this.#pending.delete(seq);
89
+ reject(new Error(`MAX did not answer opcode ${opcode} within ${this.#timeoutMs}ms`));
90
+ }, this.#timeoutMs);
91
+ this.#pending.set(seq, { resolve, reject, timer });
92
+ const sent = encodeFrame({ seq, opcode, payload });
93
+ socket.send(sent);
94
+ watch?.({ phase: "sent", seq, opcode, bytes: Buffer.byteLength(sent) });
95
+ });
96
+ const frame = answer.frame;
97
+ watch?.({ phase: "received", seq, opcode, bytes: answer.bytes });
98
+ if (frame.cmd === Command.ERROR) {
99
+ const reason = typeof frame.payload?.error === "string" ? frame.payload.error : "an error with no reason given";
100
+ throw new ProtocolError(`MAX refused opcode ${opcode}: ${reason}`, opcode, frame.payload);
101
+ }
102
+ return frame.payload ?? {};
103
+ }
104
+ async close() {
105
+ this.#closed = true;
106
+ const socket = this.#socket;
107
+ this.#socket = undefined;
108
+ for (const { timer } of this.#pending.values())
109
+ clearTimeout(timer);
110
+ this.#pending.clear();
111
+ if (!socket)
112
+ return;
113
+ socket.removeAllListeners();
114
+ if (socket.readyState === socket.OPEN || socket.readyState === socket.CONNECTING)
115
+ socket.close();
116
+ }
117
+ #receive(raw) {
118
+ let frame;
119
+ try {
120
+ frame = decodeFrame(raw);
121
+ }
122
+ catch {
123
+ return; // A frame we cannot read is not a reason to fail a request we can.
124
+ }
125
+ const waiting = frame.seq === null ? undefined : this.#pending.get(frame.seq);
126
+ if (!waiting) {
127
+ this.#onEvent?.(frame);
128
+ return;
129
+ }
130
+ clearTimeout(waiting.timer);
131
+ this.#pending.delete(frame.seq);
132
+ waiting.resolve({ frame, bytes: Buffer.byteLength(raw) });
133
+ }
134
+ #failAll(error) {
135
+ for (const { reject, timer } of this.#pending.values()) {
136
+ clearTimeout(timer);
137
+ reject(error);
138
+ }
139
+ this.#pending.clear();
140
+ }
141
+ }
142
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/protocol/connection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /** The protocol version the web client pins. Confirmed in tsmax, maxrs and max-api-docs. */
2
+ export declare const PROTOCOL_VERSION = 11;
3
+ export declare const Command: {
4
+ readonly REQUEST: 0;
5
+ readonly RESPONSE: 1;
6
+ readonly EVENT: 2;
7
+ readonly ERROR: 3;
8
+ };
9
+ export type Command = (typeof Command)[keyof typeof Command];
10
+ export type Payload = Record<string, unknown>;
11
+ export interface OutboundFrame {
12
+ seq: number;
13
+ opcode: number;
14
+ payload: Payload;
15
+ cmd?: Command;
16
+ }
17
+ export interface InboundFrame {
18
+ ver: number;
19
+ cmd: number;
20
+ seq: number | null;
21
+ opcode: number;
22
+ payload: Payload | null;
23
+ }
24
+ export declare const encodeFrame: (frame: OutboundFrame) => string;
25
+ /**
26
+ * **Never `JSON.parse`.** Message ids are 64-bit and past `Number.MAX_SAFE_INTEGER` — 18 digits,
27
+ * measured — so the built-in parser rounds them and two different messages arrive as one id.
28
+ * `lossless-json` hands back a bigint for any integer that would not survive, and every id leaves
29
+ * this layer as a string.
30
+ *
31
+ * ⚠ Measured 2026-09-19: **chat ids on the owner's account reach 14 digits, not 19**, and contact
32
+ * ids reach 9 — all comfortably inside a number. The hazard is real and it is the message ids that
33
+ * carry it; a 19-digit chat id is an illustration, not something seen.
34
+ */
35
+ export declare const decodeFrame: (raw: string) => InboundFrame;
36
+ export declare class FrameError extends Error {
37
+ constructor(what: string);
38
+ }
39
+ /**
40
+ * Ids arrive as `number` when small and `bigint` when not, and both have to render identically.
41
+ * A domain id is a string from here on — it is an identifier, never arithmetic.
42
+ */
43
+ export declare const asId: (value: unknown) => string | undefined;
44
+ //# sourceMappingURL=frame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame.d.ts","sourceRoot":"","sources":["../../src/protocol/frame.ts"],"names":[],"mappings":"AAEA,4FAA4F;AAC5F,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAElC,eAAO,MAAM,OAAO;;;;;CAKV,CAAA;AACV,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,OAAO,OAAO,CAAC,CAAA;AAE5D,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAE7C,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;CACxB;AAED,eAAO,MAAM,WAAW,GAAI,OAAO,aAAa,KAAG,MAOrC,CAAA;AAEd;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,YAczC,CAAA;AAED,qBAAa,UAAW,SAAQ,KAAK;gBACvB,IAAI,EAAE,MAAM;CAIzB;AAED;;;GAGG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAK9C,CAAA"}
@@ -0,0 +1,68 @@
1
+ import { isInteger, isSafeNumber, parse, stringify } from "lossless-json";
2
+ /** The protocol version the web client pins. Confirmed in tsmax, maxrs and max-api-docs. */
3
+ export const PROTOCOL_VERSION = 11;
4
+ export const Command = {
5
+ REQUEST: 0,
6
+ RESPONSE: 1,
7
+ EVENT: 2,
8
+ ERROR: 3,
9
+ };
10
+ export const encodeFrame = (frame) => stringify({
11
+ ver: PROTOCOL_VERSION,
12
+ cmd: frame.cmd ?? Command.REQUEST,
13
+ seq: frame.seq,
14
+ opcode: frame.opcode,
15
+ payload: frame.payload,
16
+ });
17
+ /**
18
+ * **Never `JSON.parse`.** Message ids are 64-bit and past `Number.MAX_SAFE_INTEGER` — 18 digits,
19
+ * measured — so the built-in parser rounds them and two different messages arrive as one id.
20
+ * `lossless-json` hands back a bigint for any integer that would not survive, and every id leaves
21
+ * this layer as a string.
22
+ *
23
+ * ⚠ Measured 2026-09-19: **chat ids on the owner's account reach 14 digits, not 19**, and contact
24
+ * ids reach 9 — all comfortably inside a number. The hazard is real and it is the message ids that
25
+ * carry it; a 19-digit chat id is an illustration, not something seen.
26
+ */
27
+ export const decodeFrame = (raw) => {
28
+ const value = parse(raw, undefined, {
29
+ parseNumber: (text) => (isInteger(text) && !isSafeNumber(text) ? BigInt(text) : Number(text)),
30
+ });
31
+ if (!isRecord(value))
32
+ throw new FrameError("a frame that is not an object");
33
+ return {
34
+ ver: header(value.ver, "ver"),
35
+ cmd: header(value.cmd ?? Command.RESPONSE, "cmd"),
36
+ seq: value.seq === null || value.seq === undefined ? null : header(value.seq, "seq"),
37
+ opcode: header(value.opcode, "opcode"),
38
+ payload: isRecord(value.payload) ? value.payload : null,
39
+ };
40
+ };
41
+ export class FrameError extends Error {
42
+ constructor(what) {
43
+ super(`MAX sent ${what}`);
44
+ this.name = "FrameError";
45
+ }
46
+ }
47
+ /**
48
+ * Ids arrive as `number` when small and `bigint` when not, and both have to render identically.
49
+ * A domain id is a string from here on — it is an identifier, never arithmetic.
50
+ */
51
+ export const asId = (value) => {
52
+ if (typeof value === "bigint")
53
+ return value.toString();
54
+ if (typeof value === "number" && Number.isFinite(value))
55
+ return String(value);
56
+ if (typeof value === "string" && value !== "")
57
+ return value;
58
+ return undefined;
59
+ };
60
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
61
+ const header = (value, field) => {
62
+ const asNumber = typeof value === "bigint" ? Number(value) : value;
63
+ if (typeof asNumber !== "number" || !Number.isSafeInteger(asNumber)) {
64
+ throw new FrameError(`a frame whose \`${field}\` is not a whole number`);
65
+ }
66
+ return asNumber;
67
+ };
68
+ //# sourceMappingURL=frame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame.js","sourceRoot":"","sources":["../../src/protocol/frame.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import type { Payload } from "../protocol/frame.js";
2
+ /**
3
+ * One request, as both sinks see it: `--verbose` renders it, the run log writes it as a line of
4
+ * JSON. **One object, two sinks** — a second shape for one of them is how the two start disagreeing
5
+ * about what happened.
6
+ *
7
+ * ⚠ **Nothing here may carry content.** The operation, the opcode, the `seq`, the ids a request
8
+ * named, byte counts and durations are safe: an id is opaque and can only be correlated by whoever
9
+ * already has the session. A chat title, a person's name, a message body, a phone number or a token
10
+ * are not — not truncated and not hashed (`REQUIREMENTS.md` §14, §24). The fields below are the
11
+ * whole vocabulary, and `idsOf` builds them from named fields rather than filtering a copy of the
12
+ * payload, so there is no path by which an unforeseen field arrives here.
13
+ */
14
+ export interface RequestEvent {
15
+ event: "request" | "response";
16
+ /** Ours, not MAX's: `chats.history`, not `CHAT_HISTORY`. */
17
+ operation: string;
18
+ opcode: number;
19
+ seq: number;
20
+ /** The frame that went out, or the one that came back. Absent when nothing came back. */
21
+ bytes?: number;
22
+ /** Which things the request named — `{ chat: "0" }`. Never how they are called. */
23
+ ids?: Record<string, string>;
24
+ /** How many things, per field: `{ messages: 3 }`. A field name and a length carry no content. */
25
+ counts?: Record<string, number>;
26
+ durationMs?: number;
27
+ outcome?: "ok" | "error";
28
+ /** `cli-core`'s code — `timeout`, `provider_error`. Never the message: MAX quotes our payload. */
29
+ errorCode?: string;
30
+ }
31
+ /**
32
+ * A read that never reached MAX.
33
+ *
34
+ * It is not a request and must not be shaped like one — there is no opcode, no `seq` and no frame,
35
+ * and calling a local answer "request 0 bytes" would put a fiction in the record. The call sites
36
+ * belong to the cache (`MAX-7`); what lives here is the vocabulary, so that the two of them cannot
37
+ * drift into two shapes for one diagnostic.
38
+ */
39
+ export interface CacheEvent {
40
+ event: "cache";
41
+ /** Ours: `chats.list`, `messages.list`. */
42
+ operation: string;
43
+ reason: CacheReason;
44
+ ids?: Record<string, string>;
45
+ counts?: Record<string, number>;
46
+ /** How old the local answer was. The number a person wants when a list looks wrong. */
47
+ ageMs?: number;
48
+ }
49
+ /**
50
+ * Why MAX was not asked. **"We did not ask" and "there was nothing to ask" are different events**,
51
+ * and only the second is the record doing its job rather than standing in for a connection.
52
+ *
53
+ * Written out as two words rather than a free string so that the vocabulary has one home and a
54
+ * third case has to be added here, deliberately, instead of appearing in a log nobody can group.
55
+ */
56
+ export type CacheReason =
57
+ /** The caller said never connect, so the record is all there is. */
58
+ "offline"
59
+ /** The window asked for is older than a fetch would return, so the record is authoritative. */
60
+ | "history";
61
+ /** What either sink is handed. One object, two sinks — see `RequestEvent`. */
62
+ export type DiagnosticEvent = RequestEvent | CacheEvent;
63
+ /**
64
+ * The ids a request named, by name, from a list of fields that is written out here.
65
+ *
66
+ * **An allowlist, never a filter.** Copying the request and removing what looks dangerous means a
67
+ * field nobody has seen yet arrives in the log by default; this way it cannot. `token` is the case
68
+ * that matters: it is a field of `session.login` and there is no branch here that could reach it.
69
+ */
70
+ export declare const idsOf: (request: unknown) => {
71
+ ids?: Record<string, string>;
72
+ counts?: Record<string, number>;
73
+ };
74
+ /**
75
+ * How many of each list MAX sent back — `{ chats: 25, contacts: 6 }`.
76
+ *
77
+ * Every array at the top of the answer, counted. Generic on purpose: a per-operation table of
78
+ * "which field is the interesting one" is a second registry to keep in step with `src/spec/`, and
79
+ * a field name with a length beside it is content-free whatever MAX adds next.
80
+ */
81
+ export declare const countsIn: (payload: Payload) => Record<string, number> | undefined;
82
+ /**
83
+ * The line a person reads. `→` is what we asked, `←` what came back, `•` what never left.
84
+ *
85
+ * ```text
86
+ * → chats.history op 49 seq 3 chat 0
87
+ * ← chats.history op 49 seq 3 118ms 4.2 kB 3 messages
88
+ * • chats.list offline cached 42s 25 chats
89
+ * ```
90
+ */
91
+ export declare const renderEvent: (event: DiagnosticEvent) => string;
92
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/runs/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAEnD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,SAAS,GAAG,UAAU,CAAA;IAC7B,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mFAAmF;IACnF,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,IAAI,GAAG,OAAO,CAAA;IACxB,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,OAAO,CAAA;IACd,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;GAMG;AACH,MAAM,MAAM,WAAW;AACrB,oEAAoE;AAClE,SAAS;AACX,+FAA+F;GAC7F,SAAS,CAAA;AAEb,8EAA8E;AAC9E,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,UAAU,CAAA;AAEvD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,GAAI,SAAS,OAAO,KAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAqBvG,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,GAAI,SAAS,OAAO,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAIpE,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,eAAe,KAAG,MAqBpD,CAAA"}