@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,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ids a request named, by name, from a list of fields that is written out here.
|
|
3
|
+
*
|
|
4
|
+
* **An allowlist, never a filter.** Copying the request and removing what looks dangerous means a
|
|
5
|
+
* field nobody has seen yet arrives in the log by default; this way it cannot. `token` is the case
|
|
6
|
+
* that matters: it is a field of `session.login` and there is no branch here that could reach it.
|
|
7
|
+
*/
|
|
8
|
+
export const idsOf = (request) => {
|
|
9
|
+
if (!isRecord(request))
|
|
10
|
+
return {};
|
|
11
|
+
const ids = {};
|
|
12
|
+
const counts = {};
|
|
13
|
+
if (isId(request.chatId))
|
|
14
|
+
ids.chat = String(request.chatId);
|
|
15
|
+
if (isId(request.messageId))
|
|
16
|
+
ids.message = String(request.messageId);
|
|
17
|
+
if (typeof request.cid === "number")
|
|
18
|
+
ids.cid = String(request.cid);
|
|
19
|
+
// `messages.send` carries its client id one level down, and the `cid` is the one thing that makes
|
|
20
|
+
// an ambiguous send repeatable — see `MaxClient.messages.send`.
|
|
21
|
+
const message = isRecord(request.message) ? request.message : undefined;
|
|
22
|
+
if (message && typeof message.cid === "number")
|
|
23
|
+
ids.cid = String(message.cid);
|
|
24
|
+
if (Array.isArray(request.contactIds))
|
|
25
|
+
counts.contacts = request.contactIds.length;
|
|
26
|
+
return {
|
|
27
|
+
...(Object.keys(ids).length > 0 ? { ids } : {}),
|
|
28
|
+
...(Object.keys(counts).length > 0 ? { counts } : {}),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* How many of each list MAX sent back — `{ chats: 25, contacts: 6 }`.
|
|
33
|
+
*
|
|
34
|
+
* Every array at the top of the answer, counted. Generic on purpose: a per-operation table of
|
|
35
|
+
* "which field is the interesting one" is a second registry to keep in step with `src/spec/`, and
|
|
36
|
+
* a field name with a length beside it is content-free whatever MAX adds next.
|
|
37
|
+
*/
|
|
38
|
+
export const countsIn = (payload) => {
|
|
39
|
+
const counts = {};
|
|
40
|
+
for (const [field, value] of Object.entries(payload))
|
|
41
|
+
if (Array.isArray(value))
|
|
42
|
+
counts[field] = value.length;
|
|
43
|
+
return Object.keys(counts).length > 0 ? counts : undefined;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The line a person reads. `→` is what we asked, `←` what came back, `•` what never left.
|
|
47
|
+
*
|
|
48
|
+
* ```text
|
|
49
|
+
* → chats.history op 49 seq 3 chat 0
|
|
50
|
+
* ← chats.history op 49 seq 3 118ms 4.2 kB 3 messages
|
|
51
|
+
* • chats.list offline cached 42s 25 chats
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export const renderEvent = (event) => {
|
|
55
|
+
const parts = [];
|
|
56
|
+
if (event.event === "cache") {
|
|
57
|
+
parts.push(event.reason);
|
|
58
|
+
if (event.ageMs !== undefined)
|
|
59
|
+
parts.push(`cached ${age(event.ageMs)}`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
parts.push(`op ${event.opcode}`, `seq ${event.seq}`);
|
|
63
|
+
}
|
|
64
|
+
if (event.ids)
|
|
65
|
+
for (const [name, value] of Object.entries(event.ids))
|
|
66
|
+
parts.push(`${name} ${value}`);
|
|
67
|
+
if (event.event !== "cache") {
|
|
68
|
+
if (event.durationMs !== undefined)
|
|
69
|
+
parts.push(`${event.durationMs}ms`);
|
|
70
|
+
if (event.bytes !== undefined)
|
|
71
|
+
parts.push(size(event.bytes));
|
|
72
|
+
}
|
|
73
|
+
if (event.counts)
|
|
74
|
+
for (const [field, count] of Object.entries(event.counts))
|
|
75
|
+
parts.push(`${count} ${field}`);
|
|
76
|
+
if (event.event !== "cache" && event.errorCode)
|
|
77
|
+
parts.push(event.errorCode);
|
|
78
|
+
return `${MARK[event.event]} ${event.operation.padEnd(16)} ${parts.join(" ")}`;
|
|
79
|
+
};
|
|
80
|
+
const MARK = { request: "\u2192", response: "\u2190", cache: "\u2022" };
|
|
81
|
+
const age = (ms) => (ms < 1000 ? `${ms}ms` : `${Math.round(ms / 1000)}s`);
|
|
82
|
+
const size = (bytes) => (bytes < 1000 ? `${bytes} B` : `${(bytes / 1000).toFixed(1)} kB`);
|
|
83
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
84
|
+
const isId = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint";
|
|
85
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/runs/events.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type RenderFormat, type Streams } from "@leemour/cli-core";
|
|
2
|
+
import { type DiagnosticEvent } from "./events.js";
|
|
3
|
+
export interface RecordingOptions {
|
|
4
|
+
/** As typed, without the arguments: `chats list`. It names the run directory. */
|
|
5
|
+
command: string;
|
|
6
|
+
profile: string;
|
|
7
|
+
/**
|
|
8
|
+
* What `commander` parsed. `record` is three-valued on purpose: `--record`, `--no-record`, and
|
|
9
|
+
* absent — the third is where the configuration file's `"record": true` plugs in (thread C).
|
|
10
|
+
*/
|
|
11
|
+
options: {
|
|
12
|
+
record?: boolean | undefined;
|
|
13
|
+
verbose?: boolean | undefined;
|
|
14
|
+
};
|
|
15
|
+
/** What `resolveOutput` decided for the data stream. Diagnostics follow it (`NEED-53`). */
|
|
16
|
+
format: RenderFormat;
|
|
17
|
+
streams?: Streams;
|
|
18
|
+
/** Tests point this at a temporary directory. */
|
|
19
|
+
runsDir?: string;
|
|
20
|
+
keepDays?: number;
|
|
21
|
+
now?: () => Date;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Runs one command with the diagnostics turned on, and **finalizes on every path**.
|
|
25
|
+
*
|
|
26
|
+
* Two sinks, one event object (plan §3.1, §3.7): `--verbose` shows it as it happens and keeps
|
|
27
|
+
* nothing, `--record` keeps it and shows nothing. Either, both, or — by default — neither, in
|
|
28
|
+
* which case this costs a counter.
|
|
29
|
+
*
|
|
30
|
+
* The body gets the emitter to hand to `MaxClient`. It is a wrapper rather than three lines in
|
|
31
|
+
* each command because the failure it prevents is the quiet one: a run directory left saying
|
|
32
|
+
* `running` because the command that owned it threw on the way out.
|
|
33
|
+
*/
|
|
34
|
+
export declare const recorded: <T>(options: RecordingOptions, body: (events: (event: DiagnosticEvent) => void) => Promise<T>) => Promise<T>;
|
|
35
|
+
//# sourceMappingURL=recording.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording.d.ts","sourceRoot":"","sources":["../../src/runs/recording.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,YAAY,EAAE,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAE7F,OAAO,EAAE,KAAK,eAAe,EAAe,MAAM,aAAa,CAAA;AAG/D,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAAE,CAAA;IACxE,2FAA2F;IAC3F,MAAM,EAAE,YAAY,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,GAAU,CAAC,EAC9B,SAAS,gBAAgB,EACzB,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAC7D,OAAO,CAAC,CAAC,CAsCX,CAAA"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CliError, processStreams } from "@leemour/cli-core";
|
|
2
|
+
import { VERSION } from "../version.js";
|
|
3
|
+
import { renderEvent } from "./events.js";
|
|
4
|
+
import { runsDirFor, startRun } from "./run.js";
|
|
5
|
+
/**
|
|
6
|
+
* Runs one command with the diagnostics turned on, and **finalizes on every path**.
|
|
7
|
+
*
|
|
8
|
+
* Two sinks, one event object (plan §3.1, §3.7): `--verbose` shows it as it happens and keeps
|
|
9
|
+
* nothing, `--record` keeps it and shows nothing. Either, both, or — by default — neither, in
|
|
10
|
+
* which case this costs a counter.
|
|
11
|
+
*
|
|
12
|
+
* The body gets the emitter to hand to `MaxClient`. It is a wrapper rather than three lines in
|
|
13
|
+
* each command because the failure it prevents is the quiet one: a run directory left saying
|
|
14
|
+
* `running` because the command that owned it threw on the way out.
|
|
15
|
+
*/
|
|
16
|
+
export const recorded = async (options, body) => {
|
|
17
|
+
const streams = options.streams ?? processStreams;
|
|
18
|
+
const verbose = options.options.verbose === true;
|
|
19
|
+
const run = options.options.record === true
|
|
20
|
+
? startRun({
|
|
21
|
+
runsDir: options.runsDir ?? runsDirFor(),
|
|
22
|
+
command: options.command,
|
|
23
|
+
profile: options.profile,
|
|
24
|
+
cliVersion: VERSION,
|
|
25
|
+
...(options.keepDays === undefined ? {} : { keepDays: options.keepDays }),
|
|
26
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
27
|
+
})
|
|
28
|
+
: undefined;
|
|
29
|
+
let requests = 0;
|
|
30
|
+
const events = (event) => {
|
|
31
|
+
if (event.event === "request")
|
|
32
|
+
requests += 1;
|
|
33
|
+
// Text for a person, one JSON object per line for anything else — the rule `resolveOutput`
|
|
34
|
+
// already applies to the data stream (`NEED-53`). Never stdout, in any mode.
|
|
35
|
+
if (verbose)
|
|
36
|
+
streams.diagnostic(options.format === "pretty" ? renderEvent(event) : JSON.stringify(event));
|
|
37
|
+
run?.logger.info(event);
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
const answer = await body(events);
|
|
41
|
+
await run?.finish("success", { requests });
|
|
42
|
+
return answer;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
await run?.finish("failed", {
|
|
46
|
+
requests,
|
|
47
|
+
// The code a script branches on, never the sentence: MAX's refusals quote our payload back.
|
|
48
|
+
...(error instanceof CliError ? { errorCode: error.code } : {}),
|
|
49
|
+
});
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=recording.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording.js","sourceRoot":"","sources":["../../src/runs/recording.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAmC,MAAM,mBAAmB,CAAA;AAC7F,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAwB,WAAW,EAAE,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAY,UAAU,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAoBzD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAC3B,OAAyB,EACzB,IAA8D,EAClD,EAAE;IACd,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,CAAA;IACjD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,KAAK,IAAI,CAAA;IAEhD,MAAM,GAAG,GACP,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,IAAI;QAC7B,CAAC,CAAC,QAAQ,CAAC;YACP,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,UAAU,EAAE;YACxC,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,UAAU,EAAE,OAAO;YACnB,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;YACzE,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;SAC3D,CAAC;QACJ,CAAC,CAAC,SAAS,CAAA;IAEf,IAAI,QAAQ,GAAG,CAAC,CAAA;IAEhB,MAAM,MAAM,GAAG,CAAC,KAAsB,EAAQ,EAAE;QAC9C,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,QAAQ,IAAI,CAAC,CAAA;QAC5C,2FAA2F;QAC3F,6EAA6E;QAC7E,IAAI,OAAO;YAAE,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;QACzG,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACzB,CAAC,CAAA;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,CAAA;QACjC,MAAM,GAAG,EAAE,MAAM,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC1C,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,EAAE,MAAM,CAAC,QAAQ,EAAE;YAC1B,QAAQ;YACR,4FAA4F;YAC5F,GAAG,CAAC,KAAK,YAAY,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAChE,CAAC,CAAA;QACF,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type FileLogger } from "@leemour/cli-core";
|
|
2
|
+
export type RunStatus = "success" | "failed";
|
|
3
|
+
/**
|
|
4
|
+
* What a run was, and how it ended. **It never says what was read or sent** — a command, a profile
|
|
5
|
+
* and a count, and the `errorCode` a script branches on rather than the sentence a person reads,
|
|
6
|
+
* because MAX's refusals quote the payload back at us.
|
|
7
|
+
*/
|
|
8
|
+
export interface RunMetadata {
|
|
9
|
+
runId: string;
|
|
10
|
+
/** As typed, without the arguments: `chats list`, `messages send`. */
|
|
11
|
+
command: string;
|
|
12
|
+
profile: string;
|
|
13
|
+
startedAt: string;
|
|
14
|
+
completedAt?: string;
|
|
15
|
+
status: RunStatus | "running";
|
|
16
|
+
/** How many requests reached MAX. A read the cache answers makes none at all (`MAX-7`). */
|
|
17
|
+
requests?: number;
|
|
18
|
+
durationMs?: number;
|
|
19
|
+
cliVersion: string;
|
|
20
|
+
errorCode?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface StartRunOptions {
|
|
23
|
+
runsDir: string;
|
|
24
|
+
command: string;
|
|
25
|
+
profile: string;
|
|
26
|
+
cliVersion: string;
|
|
27
|
+
/** Older runs are removed as this one starts (`NEED-49`). */
|
|
28
|
+
keepDays?: number;
|
|
29
|
+
runId?: string;
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
}
|
|
32
|
+
export interface Run {
|
|
33
|
+
id: string;
|
|
34
|
+
dir: string;
|
|
35
|
+
logger: FileLogger;
|
|
36
|
+
/**
|
|
37
|
+
* Writes the final `run.json` and closes the log. **Must run on every path** — the answer, a
|
|
38
|
+
* refusal, a failure before the connection was even open. Safe to call twice.
|
|
39
|
+
*/
|
|
40
|
+
finish(status: RunStatus, extra?: Partial<RunMetadata>): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/** 30 days (`NEED-49`). Time is what you think in when asking what you did last week. */
|
|
43
|
+
export declare const KEEP_RUNS_FOR_DAYS = 30;
|
|
44
|
+
/**
|
|
45
|
+
* `<state dir>/runs`. Beside the profiles and never in the cache directory: a cache is deleted to
|
|
46
|
+
* fix a cache, and the log is the one thing here that cannot be rebuilt from MAX (`NEED-51`).
|
|
47
|
+
*/
|
|
48
|
+
export declare const runsDirFor: (env?: NodeJS.ProcessEnv) => string;
|
|
49
|
+
/**
|
|
50
|
+
* Opens a run directory, the shape `braze-cli` arrived at
|
|
51
|
+
* (`../braze-cli/packages/cli/src/runs/run.ts:74`).
|
|
52
|
+
*
|
|
53
|
+
* ```text
|
|
54
|
+
* <state dir>/runs/2026-09-20/20260920T084431Z-chats-list-a81f2c/
|
|
55
|
+
* run.json what it was, when, which profile, how long, the outcome
|
|
56
|
+
* events.jsonl one JSON object per request, written by Pino, no ANSI ever
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* `run.json` is written **now**, with `status: "running"`, and again by `finish`. A directory
|
|
60
|
+
* holding events and no metadata is a special case `max runs list` would carry forever.
|
|
61
|
+
*/
|
|
62
|
+
export declare const startRun: (options: StartRunOptions) => Run;
|
|
63
|
+
/** Newest first, which is the order anyone asking "what did I just do" wants. */
|
|
64
|
+
export declare const listRuns: (runsDir: string) => RunMetadata[];
|
|
65
|
+
export declare const findRun: (runsDir: string, id: string) => {
|
|
66
|
+
dir: string;
|
|
67
|
+
metadata: RunMetadata;
|
|
68
|
+
} | undefined;
|
|
69
|
+
/** The events of one run, in the order they happened. A line that will not parse is skipped. */
|
|
70
|
+
export declare const readEvents: (dir: string) => Record<string, unknown>[];
|
|
71
|
+
/**
|
|
72
|
+
* Removes every day older than `keepDays`, and answers how many days went.
|
|
73
|
+
*
|
|
74
|
+
* A whole day at a time, by the directory name, so nothing has to be opened to decide — and a
|
|
75
|
+
* directory that is not a date is left alone rather than guessed at.
|
|
76
|
+
*/
|
|
77
|
+
export declare const pruneRuns: (runsDir: string, { keepDays, now }: {
|
|
78
|
+
keepDays: number;
|
|
79
|
+
now: Date;
|
|
80
|
+
}) => number;
|
|
81
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/runs/run.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoB,KAAK,UAAU,EAA+B,MAAM,mBAAmB,CAAA;AAElG,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAA;AAE5C;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,SAAS,GAAG,SAAS,CAAA;IAC7B,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,UAAU,CAAA;IAClB;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvE;AAED,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,MACY,CAAA;AAE9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ,GAAI,SAAS,eAAe,KAAG,GAyDnD,CAAA;AAkBD,iFAAiF;AACjF,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,KAAG,WAAW,EAUrD,CAAA;AAED,eAAO,MAAM,OAAO,GAAI,SAAS,MAAM,EAAE,IAAI,MAAM,KAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,GAAG,SAO9F,CAAA;AAED,gGAAgG;AAChG,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAoB/D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS,MAAM,EAAE,mBAAmB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,IAAI,CAAA;CAAE,KAAG,MAc/F,CAAA"}
|
package/dist/runs/run.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createFileLogger, resolvePaths, writeSecurely } from "@leemour/cli-core";
|
|
5
|
+
/** 30 days (`NEED-49`). Time is what you think in when asking what you did last week. */
|
|
6
|
+
export const KEEP_RUNS_FOR_DAYS = 30;
|
|
7
|
+
/**
|
|
8
|
+
* `<state dir>/runs`. Beside the profiles and never in the cache directory: a cache is deleted to
|
|
9
|
+
* fix a cache, and the log is the one thing here that cannot be rebuilt from MAX (`NEED-51`).
|
|
10
|
+
*/
|
|
11
|
+
export const runsDirFor = (env = process.env) => join(resolvePaths({ appName: "max-cli", prefix: "MAX", env }).state, "runs");
|
|
12
|
+
/**
|
|
13
|
+
* Opens a run directory, the shape `braze-cli` arrived at
|
|
14
|
+
* (`../braze-cli/packages/cli/src/runs/run.ts:74`).
|
|
15
|
+
*
|
|
16
|
+
* ```text
|
|
17
|
+
* <state dir>/runs/2026-09-20/20260920T084431Z-chats-list-a81f2c/
|
|
18
|
+
* run.json what it was, when, which profile, how long, the outcome
|
|
19
|
+
* events.jsonl one JSON object per request, written by Pino, no ANSI ever
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* `run.json` is written **now**, with `status: "running"`, and again by `finish`. A directory
|
|
23
|
+
* holding events and no metadata is a special case `max runs list` would carry forever.
|
|
24
|
+
*/
|
|
25
|
+
export const startRun = (options) => {
|
|
26
|
+
const now = options.now ?? (() => new Date());
|
|
27
|
+
const startedAt = now();
|
|
28
|
+
const id = options.runId ?? runId(startedAt, options.command);
|
|
29
|
+
const dir = join(options.runsDir, day(startedAt), id);
|
|
30
|
+
// Only while something is being recorded: a tool that is not recording has no business walking
|
|
31
|
+
// that directory (`NEED-49`).
|
|
32
|
+
pruneRuns(options.runsDir, { keepDays: options.keepDays ?? KEEP_RUNS_FOR_DAYS, now: startedAt });
|
|
33
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
34
|
+
// Created here rather than by the logger, which opens with whatever the umask allows. The
|
|
35
|
+
// directory is already 0700, so this is the second lock rather than the first — but a log file
|
|
36
|
+
// that outlives its directory, or is copied out of it, should not be the one that is readable.
|
|
37
|
+
const events = join(dir, "events.jsonl");
|
|
38
|
+
writeFileSync(events, "", { flag: "a", mode: 0o600 });
|
|
39
|
+
const logger = createFileLogger({
|
|
40
|
+
path: events,
|
|
41
|
+
base: { runId: id, command: options.command, profile: options.profile },
|
|
42
|
+
});
|
|
43
|
+
const metadata = {
|
|
44
|
+
runId: id,
|
|
45
|
+
command: options.command,
|
|
46
|
+
profile: options.profile,
|
|
47
|
+
startedAt: startedAt.toISOString(),
|
|
48
|
+
status: "running",
|
|
49
|
+
cliVersion: options.cliVersion,
|
|
50
|
+
};
|
|
51
|
+
writeRunFile(dir, metadata);
|
|
52
|
+
let finished = false;
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
dir,
|
|
56
|
+
logger,
|
|
57
|
+
finish: async (status, extra = {}) => {
|
|
58
|
+
if (finished)
|
|
59
|
+
return;
|
|
60
|
+
finished = true;
|
|
61
|
+
const completedAt = now();
|
|
62
|
+
writeRunFile(dir, {
|
|
63
|
+
...metadata,
|
|
64
|
+
...extra,
|
|
65
|
+
status,
|
|
66
|
+
completedAt: completedAt.toISOString(),
|
|
67
|
+
durationMs: completedAt.getTime() - startedAt.getTime(),
|
|
68
|
+
});
|
|
69
|
+
// Pino writes through a plain append stream, and a process that exits before it flushes
|
|
70
|
+
// loses the tail — which is exactly the run somebody wanted to read.
|
|
71
|
+
await logger.close();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/** `20260920T084431Z-chats-list-a81f2c` — sortable, and readable without opening it. */
|
|
76
|
+
const runId = (startedAt, command) => {
|
|
77
|
+
const stamp = startedAt
|
|
78
|
+
.toISOString()
|
|
79
|
+
.replace(/[-:]/g, "")
|
|
80
|
+
.replace(/\.\d+Z$/, "Z");
|
|
81
|
+
const slug = command.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
|
|
82
|
+
return `${stamp}-${slug}-${randomUUID().slice(0, 6)}`;
|
|
83
|
+
};
|
|
84
|
+
const writeRunFile = (dir, metadata) => {
|
|
85
|
+
// Atomic, so a reader never sees half of one — and 0600, because it names a profile and a
|
|
86
|
+
// command even though it carries nothing of what was said.
|
|
87
|
+
writeSecurely(join(dir, "run.json"), `${JSON.stringify(metadata, null, 2)}\n`, 0o600);
|
|
88
|
+
};
|
|
89
|
+
/** Newest first, which is the order anyone asking "what did I just do" wants. */
|
|
90
|
+
export const listRuns = (runsDir) => {
|
|
91
|
+
const runs = [];
|
|
92
|
+
for (const date of days(runsDir).sort().reverse()) {
|
|
93
|
+
for (const entry of safeReaddir(join(runsDir, date)).sort().reverse()) {
|
|
94
|
+
const metadata = readRun(join(runsDir, date, entry));
|
|
95
|
+
if (metadata)
|
|
96
|
+
runs.push(metadata);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return runs;
|
|
100
|
+
};
|
|
101
|
+
export const findRun = (runsDir, id) => {
|
|
102
|
+
for (const date of days(runsDir)) {
|
|
103
|
+
const dir = join(runsDir, date, id);
|
|
104
|
+
const metadata = readRun(dir);
|
|
105
|
+
if (metadata)
|
|
106
|
+
return { dir, metadata };
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
};
|
|
110
|
+
/** The events of one run, in the order they happened. A line that will not parse is skipped. */
|
|
111
|
+
export const readEvents = (dir) => {
|
|
112
|
+
let raw;
|
|
113
|
+
try {
|
|
114
|
+
raw = readFileSync(join(dir, "events.jsonl"), "utf8");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const events = [];
|
|
120
|
+
for (const line of raw.split("\n")) {
|
|
121
|
+
if (line.trim() === "")
|
|
122
|
+
continue;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(line);
|
|
125
|
+
if (parsed !== null && typeof parsed === "object")
|
|
126
|
+
events.push(parsed);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// A half-written last line is what a killed process leaves behind. The rest of the file is
|
|
130
|
+
// still the run somebody wants to read.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return events;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Removes every day older than `keepDays`, and answers how many days went.
|
|
137
|
+
*
|
|
138
|
+
* A whole day at a time, by the directory name, so nothing has to be opened to decide — and a
|
|
139
|
+
* directory that is not a date is left alone rather than guessed at.
|
|
140
|
+
*/
|
|
141
|
+
export const pruneRuns = (runsDir, { keepDays, now }) => {
|
|
142
|
+
const cutoff = day(new Date(now.getTime() - keepDays * 24 * 60 * 60 * 1000));
|
|
143
|
+
let removed = 0;
|
|
144
|
+
for (const date of days(runsDir)) {
|
|
145
|
+
if (date >= cutoff)
|
|
146
|
+
continue;
|
|
147
|
+
try {
|
|
148
|
+
rmSync(join(runsDir, date), { recursive: true, force: true });
|
|
149
|
+
removed += 1;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// A day we cannot remove is not a reason to fail the command that was only starting.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return removed;
|
|
156
|
+
};
|
|
157
|
+
const day = (date) => date.toISOString().slice(0, 10);
|
|
158
|
+
const days = (runsDir) => safeReaddir(runsDir).filter((entry) => /^\d{4}-\d{2}-\d{2}$/.test(entry));
|
|
159
|
+
const readRun = (dir) => {
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(readFileSync(join(dir, "run.json"), "utf8"));
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const safeReaddir = (dir) => {
|
|
168
|
+
try {
|
|
169
|
+
return readdirSync(dir);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/runs/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Invoke } from "../generated/client.generated.js";
|
|
2
|
+
import type { Payload } from "../protocol/frame.js";
|
|
3
|
+
export interface SessionOptions {
|
|
4
|
+
token: string;
|
|
5
|
+
deviceId: string;
|
|
6
|
+
/** How many chats LOGIN should return with the profile. The specification caps it at 100. */
|
|
7
|
+
chatsCount?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* INIT then LOGIN, in that order, before anything else will be answered
|
|
11
|
+
* (`max-api-docs/protocol/auth.md`).
|
|
12
|
+
*
|
|
13
|
+
* **Handwritten, and staying that way** (`REQUIREMENTS.md` §9): a socket problem has to be
|
|
14
|
+
* debuggable without reading a generator. What the specification supplies is the two payloads, so
|
|
15
|
+
* the field names have one home rather than two.
|
|
16
|
+
*
|
|
17
|
+
* It lives here rather than under `protocol/` because it is not transport: the transport moves
|
|
18
|
+
* frames and does not care who is asking. Establishing a session is this layer's job.
|
|
19
|
+
*
|
|
20
|
+
* **It is handed the client's own `invoke` rather than the socket**, so INIT and LOGIN are built,
|
|
21
|
+
* checked and reported exactly like every other request. They were not, until 2026-09-20: a
|
|
22
|
+
* diagnostic hook in `MaxClient` saw nothing of the two requests every single invocation makes,
|
|
23
|
+
* and `max chats list` answers from the LOGIN response without sending anything else.
|
|
24
|
+
*/
|
|
25
|
+
export declare const startSession: (invoke: Invoke, { token, deviceId, chatsCount }: SessionOptions) => Promise<Payload>;
|
|
26
|
+
//# sourceMappingURL=handshake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.d.ts","sourceRoot":"","sources":["../../src/session/handshake.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAA;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAInD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,GACvB,QAAQ,MAAM,EACd,iCAAsC,cAAc,KACnD,OAAO,CAAC,OAAO,CAajB,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { WEB_USER_AGENT } from "../spec/identity.js";
|
|
2
|
+
import { sessionInit, sessionLogin } from "../spec/operations/session.js";
|
|
3
|
+
/**
|
|
4
|
+
* INIT then LOGIN, in that order, before anything else will be answered
|
|
5
|
+
* (`max-api-docs/protocol/auth.md`).
|
|
6
|
+
*
|
|
7
|
+
* **Handwritten, and staying that way** (`REQUIREMENTS.md` §9): a socket problem has to be
|
|
8
|
+
* debuggable without reading a generator. What the specification supplies is the two payloads, so
|
|
9
|
+
* the field names have one home rather than two.
|
|
10
|
+
*
|
|
11
|
+
* It lives here rather than under `protocol/` because it is not transport: the transport moves
|
|
12
|
+
* frames and does not care who is asking. Establishing a session is this layer's job.
|
|
13
|
+
*
|
|
14
|
+
* **It is handed the client's own `invoke` rather than the socket**, so INIT and LOGIN are built,
|
|
15
|
+
* checked and reported exactly like every other request. They were not, until 2026-09-20: a
|
|
16
|
+
* diagnostic hook in `MaxClient` saw nothing of the two requests every single invocation makes,
|
|
17
|
+
* and `max chats list` answers from the LOGIN response without sending anything else.
|
|
18
|
+
*/
|
|
19
|
+
export const startSession = async (invoke, { token, deviceId, chatsCount = 40 }) => {
|
|
20
|
+
await invoke(sessionInit, { userAgent: WEB_USER_AGENT, deviceId });
|
|
21
|
+
return await invoke(sessionLogin, {
|
|
22
|
+
token,
|
|
23
|
+
// A script reading is not a person looking; see the specification for why this is never true.
|
|
24
|
+
interactive: false,
|
|
25
|
+
chatsCount,
|
|
26
|
+
chatsSync: 0,
|
|
27
|
+
contactsSync: 0,
|
|
28
|
+
presenceSync: 0,
|
|
29
|
+
draftsSync: 0,
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=handshake.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.js","sourceRoot":"","sources":["../../src/session/handshake.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AASzE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAC/B,MAAc,EACd,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,GAAG,EAAE,EAAkB,EAClC,EAAE;IACpB,MAAM,MAAM,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,CAAA;IAElE,OAAO,MAAM,MAAM,CAAC,YAAY,EAAE;QAChC,KAAK;QACL,8FAA8F;QAC9F,WAAW,EAAE,KAAK;QAClB,UAAU;QACV,SAAS,EAAE,CAAC;QACZ,YAAY,EAAE,CAAC;QACf,YAAY,EAAE,CAAC;QACf,UAAU,EAAE,CAAC;KACd,CAAC,CAAA;AACJ,CAAC,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SecretInput {
|
|
2
|
+
input?: NodeJS.ReadableStream & {
|
|
3
|
+
isTTY?: boolean;
|
|
4
|
+
};
|
|
5
|
+
output?: NodeJS.WritableStream;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* One line of secret, taken without a file and without argv.
|
|
9
|
+
*
|
|
10
|
+
* A token on a command line is read by `ps` and kept by shell history; a token in a file is a
|
|
11
|
+
* second copy nobody remembers to delete. Both are avoided by never letting it land anywhere: it
|
|
12
|
+
* is typed or piped, goes straight to the keyring, and is never echoed.
|
|
13
|
+
*
|
|
14
|
+
* The prompt goes to **stderr**, not stdout, so that `max session start --json` still writes one JSON
|
|
15
|
+
* value and nothing else.
|
|
16
|
+
*/
|
|
17
|
+
export declare const readSecret: (prompt: string, { input, output }?: SecretInput) => Promise<string>;
|
|
18
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/session/prompt.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,GAAG;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAA;CAC/B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,GACrB,QAAQ,MAAM,EACd,oBAAoD,WAAgB,KACnE,OAAO,CAAC,MAAM,CA0BhB,CAAA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { Writable } from "node:stream";
|
|
3
|
+
/**
|
|
4
|
+
* One line of secret, taken without a file and without argv.
|
|
5
|
+
*
|
|
6
|
+
* A token on a command line is read by `ps` and kept by shell history; a token in a file is a
|
|
7
|
+
* second copy nobody remembers to delete. Both are avoided by never letting it land anywhere: it
|
|
8
|
+
* is typed or piped, goes straight to the keyring, and is never echoed.
|
|
9
|
+
*
|
|
10
|
+
* The prompt goes to **stderr**, not stdout, so that `max session start --json` still writes one JSON
|
|
11
|
+
* value and nothing else.
|
|
12
|
+
*/
|
|
13
|
+
export const readSecret = async (prompt, { input = process.stdin, output = process.stderr } = {}) => {
|
|
14
|
+
if (!input.isTTY) {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
for await (const chunk of input)
|
|
17
|
+
chunks.push(Buffer.from(chunk));
|
|
18
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
19
|
+
}
|
|
20
|
+
let muted = false;
|
|
21
|
+
const shim = new Writable({
|
|
22
|
+
write(chunk, _encoding, done) {
|
|
23
|
+
if (!muted)
|
|
24
|
+
output.write(chunk);
|
|
25
|
+
done();
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const reader = createInterface({ input, output: shim, terminal: true });
|
|
29
|
+
try {
|
|
30
|
+
const answer = await new Promise((resolve) => {
|
|
31
|
+
reader.question(prompt, resolve);
|
|
32
|
+
muted = true;
|
|
33
|
+
});
|
|
34
|
+
return answer.trim();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
reader.close();
|
|
38
|
+
output.write("\n");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/session/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAOtC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC7B,MAAc,EACd,EAAE,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,KAAkB,EAAE,EACnD,EAAE;IACnB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACjB,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,KAAK;YAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;QAChE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA;IACtD,CAAC;IAED,IAAI,KAAK,GAAG,KAAK,CAAA;IACjB,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC;QACxB,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI;YAC1B,IAAI,CAAC,KAAK;gBAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAC/B,IAAI,EAAE,CAAA;QACR,CAAC;KACF,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACvE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;YACnD,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YAChC,KAAK,GAAG,IAAI,CAAA;QACd,CAAC,CAAC,CAAA;QACF,OAAO,MAAM,CAAC,IAAI,EAAE,CAAA;IACtB,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACpB,CAAC;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type KeyringStore } from "@leemour/cli-core";
|
|
2
|
+
/**
|
|
3
|
+
* What a MAX session actually is: a token, and an identity for this installation.
|
|
4
|
+
*
|
|
5
|
+
* **The token lives in the OS keyring; the rest lives in a file.** The split is not decoration —
|
|
6
|
+
* the token is the credential, and the device identity is merely identifying, but it must be
|
|
7
|
+
* *stable*: a client that presents a new device on every command is not imitating a real client
|
|
8
|
+
* (REQUIREMENTS §34), and MAX ties sessions to it.
|
|
9
|
+
*/
|
|
10
|
+
export interface SessionState {
|
|
11
|
+
deviceId: string;
|
|
12
|
+
/** Set once the account is known; only used to tell our own messages from other people's. */
|
|
13
|
+
viewerId?: string;
|
|
14
|
+
/** Counts logins so a token that stops working can be explained rather than guessed at (NEED-8). */
|
|
15
|
+
logins: number;
|
|
16
|
+
lastLoginAt?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SessionStoreOptions {
|
|
19
|
+
profile?: string;
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
keyring?: KeyringStore;
|
|
22
|
+
/** Overrides the config and state directories; tests pass a temporary one. */
|
|
23
|
+
configDir?: string;
|
|
24
|
+
stateDir?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class SessionStore {
|
|
27
|
+
#private;
|
|
28
|
+
readonly profile: string;
|
|
29
|
+
constructor({ profile, env, keyring, configDir, stateDir }?: SessionStoreOptions);
|
|
30
|
+
/** `undefined` means nobody has logged in on this profile. */
|
|
31
|
+
readToken(): string | undefined;
|
|
32
|
+
writeToken(token: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Reads the state, inventing a device identity the first time and keeping it thereafter.
|
|
35
|
+
*
|
|
36
|
+
* **A new identity is written out immediately, before it is used for anything.** Returning one
|
|
37
|
+
* without saving it looks harmless and is not: a login that fails, or simply a second call,
|
|
38
|
+
* would then present MAX with a different device every time — which is the "new device on every
|
|
39
|
+
* command" that §34 exists to prevent. Found by a test, not by a user.
|
|
40
|
+
*/
|
|
41
|
+
readState(): SessionState;
|
|
42
|
+
writeState(state: SessionState): void;
|
|
43
|
+
/** Forgets everything about this profile. Returns whether there was a token to forget. */
|
|
44
|
+
forget(): boolean;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=store.d.ts.map
|