@wirecat/cli-core 0.360.0 → 0.362.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/CHANGELOG.md +31 -0
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/messenger/context.js +13 -12
- package/dist/cli/messenger/serve-notice.d.ts +3 -2
- package/dist/cli/messenger/serve-notice.js +6 -4
- package/dist/cli/messenger/stored.d.ts +2 -2
- package/dist/cli/messenger/stored.js +14 -6
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/runs/events.d.ts +2 -0
- package/dist/terminal/login-qr.d.ts +15 -11
- package/dist/terminal/login-qr.js +30 -42
- package/dist/terminal/qr.d.ts +7 -5
- package/dist/terminal/qr.js +21 -47
- package/package.json +1 -1
- package/parity.json +1 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.362.0 — 19.10.2026
|
|
11
|
+
|
|
12
|
+
### Changed — may break callers
|
|
13
|
+
|
|
14
|
+
- **Without a terminal, `loginQr` prints the QR code as text an agent pastes into the chat; the PNG is gone.** The
|
|
15
|
+
owner signed in through a PNG an agent opened in an image viewer, a window the person can lose. Now, when the
|
|
16
|
+
diagnostic stream is not a terminal (`terminal: false`, by default whether stderr is one), `show` prints
|
|
17
|
+
`plainQr(link)` on it: half blocks with no colours, the light modules and the quiet zone drawn, so the code reads
|
|
18
|
+
dark on light in a dark terminal theme and every line starts and ends with a block. A WhatsApp-sized code stays
|
|
19
|
+
under 80 columns. Then it notes `PASTE_QR` (paste the block in a code block, exactly as printed; paste a renewed one
|
|
20
|
+
and say the old one expired) and where to scan. With `json: true` each code is also one line on stderr,
|
|
21
|
+
`{"event":"qr","qr":{"text","expires_at","scan"}}`; stdout keeps only the answer. `show` takes the code's expiry as
|
|
22
|
+
a third argument. In a terminal the code is drawn as before.
|
|
23
|
+
- Removed: `loginQr`'s `qrFile` option, `LoginQr.path` and `LoginQr.close()`, `qrPng` and `qrNeedsTerminal`. A CLI
|
|
24
|
+
drops `--qr-file` and its refusal of a QR login without a terminal. `parity.json` no longer expects `--qr-file` on
|
|
25
|
+
`session start` or `setup`.
|
|
26
|
+
|
|
27
|
+
## 0.361.0 — 19.10.2026
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **A warning in `serve`'s log now says which warning it was.** Off a terminal, a re-key given up at exit, a write
|
|
32
|
+
the store did not take, a passive listen that failed and a stored-chats call that failed each wrote a bare
|
|
33
|
+
`serve.warning` record, so the log could not tell them apart. The record now carries the warning's `code`,
|
|
34
|
+
`operation` and `counts` (`rekeys_not_applied` with `counts.rekeys`), never its sentence, which can quote an
|
|
35
|
+
error or a chat. Warnings that have no code yet still log a bare record.
|
|
36
|
+
- **The adapter test example in [ADAPTERS.md](docs/dev/ADAPTERS.md#running-the-contract-cases) no longer gives up
|
|
37
|
+
re-keys.** It handed `run()` `rekeyPacing: { lockMs: 0 }`, one re-key per transaction, so a few hundred re-keys
|
|
38
|
+
outlasted `closeWaitMs: 10` and were given up. It now hands `{ lockMs: 1000, pauseMs: 0 }` and says what each
|
|
39
|
+
field does.
|
|
40
|
+
|
|
10
41
|
## 0.360.0 — 19.10.2026
|
|
11
42
|
|
|
12
43
|
### Added
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface BaseEnvironment {
|
|
|
37
37
|
storeLockWaitMs?: number;
|
|
38
38
|
/** How long a connection's close waits on its saves and passive listens, 5 s unless a test hands one in. */
|
|
39
39
|
closeWaitMs?: number;
|
|
40
|
-
/** How long one pass of chat re-keys holds the store's write lock and the pause after it; tests hand in
|
|
40
|
+
/** How long one pass of chat re-keys holds the store's write lock and the pause after it; tests hand in no pause. */
|
|
41
41
|
rekeyPacing?: {
|
|
42
42
|
lockMs: number;
|
|
43
43
|
pauseMs: number;
|
|
@@ -126,8 +126,9 @@ const listened = ({ run, operation, what }, events, warn, waitMs) => {
|
|
|
126
126
|
.catch((error) => {
|
|
127
127
|
if (closing)
|
|
128
128
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const warning = { event: "warning", code: "listen_failed", operation };
|
|
130
|
+
events(warning);
|
|
131
|
+
warn(`${what} arriving during this command are not saved: ${error instanceof Error ? error.message : String(error)}`, warning);
|
|
131
132
|
});
|
|
132
133
|
return {
|
|
133
134
|
operation,
|
|
@@ -230,22 +231,22 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
|
|
|
230
231
|
finally {
|
|
231
232
|
try {
|
|
232
233
|
const ended = await Promise.all(passives.map((passive) => passive.stop()));
|
|
234
|
+
const warn = renderer.warn;
|
|
235
|
+
const warned = (warning, text) => {
|
|
236
|
+
events(warning);
|
|
237
|
+
warn(text, warning);
|
|
238
|
+
};
|
|
233
239
|
passives.forEach(({ operation, what }, at) => {
|
|
234
240
|
if (ended[at])
|
|
235
241
|
return;
|
|
236
|
-
|
|
237
|
-
renderer.warn(`not saved to the local store: the messenger was still handing on ${what} at exit`);
|
|
242
|
+
warned({ event: "warning", code: "store_not_written", operation }, `not saved to the local store: the messenger was still handing on ${what} at exit`);
|
|
238
243
|
});
|
|
239
244
|
await settled(new Set([...pending, ...announcing]), closeWaitMs);
|
|
240
245
|
closing.abort();
|
|
241
|
-
if (pending.size > 0)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (announcing.size > 0) {
|
|
246
|
-
events({ event: "warning", code: "stored_chats_failed", operation: "close" });
|
|
247
|
-
renderer.warn("the messenger's stored-chats hook did not finish and was abandoned at exit; nothing is lost");
|
|
248
|
-
}
|
|
246
|
+
if (pending.size > 0)
|
|
247
|
+
warned({ event: "warning", code: "store_not_written", operation: "close" }, "not saved to the local store: the last messages were still being written at exit");
|
|
248
|
+
if (announcing.size > 0)
|
|
249
|
+
warned({ event: "warning", code: "stored_chats_failed", operation: "close" }, "the messenger's stored-chats hook did not finish and was abandoned at exit; nothing is lost");
|
|
249
250
|
}
|
|
250
251
|
finally {
|
|
251
252
|
await store.close();
|
|
@@ -13,8 +13,9 @@ export declare const noticeFor: (context: MessengerContext, app: AppIdentity, se
|
|
|
13
13
|
/**
|
|
14
14
|
* A serve left running writes its diagnostics to a log file, so off a terminal each one is a log record with
|
|
15
15
|
* its time and run. A sentence handed to the renderer may quote an error or a chat, so the record keeps only
|
|
16
|
-
* which kind of line it was
|
|
17
|
-
*
|
|
16
|
+
* which kind of line it was, and a warning's code, operation and counts when the warning event comes with it
|
|
17
|
+
* (`Warn`); serve's own reports go through `noticeFor` with a code. The renderer is changed in place, so the
|
|
18
|
+
* send guard, bound to it before serve started, writes records too.
|
|
18
19
|
*/
|
|
19
20
|
export declare const recordDiagnostics: (context: MessengerContext, app: AppIdentity) => MessengerContext;
|
|
20
21
|
export {};
|
|
@@ -19,16 +19,18 @@ export const noticeFor = (context, app, severity = "warn") => ({ text, ...attrib
|
|
|
19
19
|
/**
|
|
20
20
|
* A serve left running writes its diagnostics to a log file, so off a terminal each one is a log record with
|
|
21
21
|
* its time and run. A sentence handed to the renderer may quote an error or a chat, so the record keeps only
|
|
22
|
-
* which kind of line it was
|
|
23
|
-
*
|
|
22
|
+
* which kind of line it was, and a warning's code, operation and counts when the warning event comes with it
|
|
23
|
+
* (`Warn`); serve's own reports go through `noticeFor` with a code. The renderer is changed in place, so the
|
|
24
|
+
* send guard, bound to it before serve started, writes records too.
|
|
24
25
|
*/
|
|
25
26
|
export const recordDiagnostics = (context, app) => {
|
|
26
27
|
if (context.format === "pretty")
|
|
27
28
|
return context;
|
|
28
|
-
const say = (severity, event, always = false) => (_text) => {
|
|
29
|
+
const say = (severity, event, always = false) => (_text, warning) => {
|
|
29
30
|
if (context.settings.quiet && !always)
|
|
30
31
|
return;
|
|
31
|
-
|
|
32
|
+
const { event: _warning, ...fields } = warning ?? {};
|
|
33
|
+
record(context, app, severity, { event, ...fields });
|
|
32
34
|
};
|
|
33
35
|
const renderer = context.renderer;
|
|
34
36
|
renderer.note = say("info", "serve.note");
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Id } from "../../domain/models.js";
|
|
2
2
|
import { type MessengerAdapter } from "../../ports/messenger.js";
|
|
3
3
|
import type { MessengerTraits } from "../../ports/traits.js";
|
|
4
|
-
import type { EventSink } from "../../runs/events.js";
|
|
4
|
+
import type { EventSink, Warn } from "../../runs/events.js";
|
|
5
5
|
import type { AccountKey, DeletionScope, MessageStore } from "../../store/store.js";
|
|
6
6
|
export interface Saving {
|
|
7
7
|
account: AccountKey;
|
|
8
8
|
/** Opened on the first save, not before: a command that reads nothing has no reason to touch the file. */
|
|
9
9
|
store: () => Promise<MessageStore | undefined>;
|
|
10
|
-
warn:
|
|
10
|
+
warn: Warn;
|
|
11
11
|
events: EventSink;
|
|
12
12
|
/** Saves still writing, for the closer to wait on: `watch` does not hold the next event for one. */
|
|
13
13
|
pending?: Set<Promise<void>>;
|
|
@@ -16,9 +16,10 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
16
16
|
let warned = false;
|
|
17
17
|
let recorded = profile === undefined;
|
|
18
18
|
const notWritten = (operation, error) => {
|
|
19
|
-
|
|
19
|
+
const warning = { event: "warning", code: "store_not_written", operation };
|
|
20
|
+
events(warning);
|
|
20
21
|
if (!warned)
|
|
21
|
-
warn(`not saved to the local store: ${error instanceof Error ? error.message : String(error)}
|
|
22
|
+
warn(`not saved to the local store: ${error instanceof Error ? error.message : String(error)}`, warning);
|
|
22
23
|
warned = true;
|
|
23
24
|
};
|
|
24
25
|
const attempt = async (operation, write) => {
|
|
@@ -92,8 +93,14 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
92
93
|
pending?.delete(saving);
|
|
93
94
|
if (count === 0)
|
|
94
95
|
return;
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const warning = {
|
|
97
|
+
event: "warning",
|
|
98
|
+
code: "rekeys_not_applied",
|
|
99
|
+
operation: "history.rekey",
|
|
100
|
+
counts: { rekeys: count },
|
|
101
|
+
};
|
|
102
|
+
events(warning);
|
|
103
|
+
warn(`${count} chat re-key${count === 1 ? "" : "s"} not applied yet; the next serve applies them`, warning);
|
|
97
104
|
};
|
|
98
105
|
closing?.addEventListener("abort", giveUpRekeys, { once: true });
|
|
99
106
|
const saveRekeys = async (pairs, round) => {
|
|
@@ -466,8 +473,9 @@ const announceStoredChats = async (messenger, store, account, events, warn, reci
|
|
|
466
473
|
await messenger.storedChats?.(held);
|
|
467
474
|
}
|
|
468
475
|
catch (error) {
|
|
469
|
-
|
|
470
|
-
|
|
476
|
+
const warning = { event: "warning", code: "stored_chats_failed", operation: "history.stored_chats" };
|
|
477
|
+
events(warning);
|
|
478
|
+
warn(`the messenger was not told the stored chats: ${error instanceof Error ? error.message : String(error)}`, warning);
|
|
471
479
|
}
|
|
472
480
|
};
|
|
473
481
|
/** Read once when `watch` starts: a store read per event would let a later event overtake an earlier one. */
|
package/dist/cli/program.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface ProgramOptions {
|
|
|
29
29
|
export declare const createProgram: ({ app, commands, configure, firstScreen }: ProgramDefinition, { out, err }?: ProgramOptions) => Command;
|
|
30
30
|
export type RunOptions = BaseEnvironment & Record<string, unknown>;
|
|
31
31
|
/**
|
|
32
|
-
* An interactive command run without a terminal — an agent's `session start
|
|
32
|
+
* An interactive command run without a terminal — an agent's `session start qr` — waits on a person scanning a
|
|
33
33
|
* code somewhere else, which 30 s does not cover.
|
|
34
34
|
*/
|
|
35
35
|
export declare const HEADLESS_INTERACTIVE_MS: number;
|
package/dist/cli/program.js
CHANGED
|
@@ -93,7 +93,7 @@ class Relayed extends Error {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
|
-
* An interactive command run without a terminal — an agent's `session start
|
|
96
|
+
* An interactive command run without a terminal — an agent's `session start qr` — waits on a person scanning a
|
|
97
97
|
* code somewhere else, which 30 s does not cover.
|
|
98
98
|
*/
|
|
99
99
|
export const HEADLESS_INTERACTIVE_MS = 5 * 60_000;
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,6 @@ export { defaultRules, ModerationRules, moderationPathFor } from "./moderation/r
|
|
|
22
22
|
export { type OutputOptions, resolveOutput } from "./output.js";
|
|
23
23
|
export { pollLines, type RenderOptions, renderMessage, renderMessages } from "./render/messages.js";
|
|
24
24
|
export { isId, type PeopleLookup, pickChat, pickPerson } from "./resolve.js";
|
|
25
|
-
export { atTerminal, type LoginQr, type LoginQrOptions, loginQr,
|
|
25
|
+
export { atTerminal, type LoginQr, type LoginQrOptions, loginQr, PASTE_QR, } from "./terminal/login-qr.js";
|
|
26
26
|
export { readSecret, type SecretInput } from "./terminal/prompt.js";
|
|
27
|
-
export {
|
|
27
|
+
export { plainQr, terminalQr } from "./terminal/qr.js";
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,6 @@ export { defaultRules, ModerationRules, moderationPathFor } from "./moderation/r
|
|
|
15
15
|
export { resolveOutput } from "./output.js";
|
|
16
16
|
export { pollLines, renderMessage, renderMessages } from "./render/messages.js";
|
|
17
17
|
export { isId, pickChat, pickPerson } from "./resolve.js";
|
|
18
|
-
export { atTerminal, loginQr,
|
|
18
|
+
export { atTerminal, loginQr, PASTE_QR, } from "./terminal/login-qr.js";
|
|
19
19
|
export { readSecret } from "./terminal/prompt.js";
|
|
20
|
-
export {
|
|
20
|
+
export { plainQr, terminalQr } from "./terminal/qr.js";
|
package/dist/runs/events.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface CacheEvent {
|
|
|
49
49
|
}
|
|
50
50
|
export type DiagnosticEvent = RequestEvent | CacheEvent | WarningEvent;
|
|
51
51
|
export type EventSink = (event: DiagnosticEvent) => void;
|
|
52
|
+
/** A sentence for a person, and the warning event it stands for when there is one: a log record keeps only the event. */
|
|
53
|
+
export type Warn = (message: string, warning?: WarningEvent) => void;
|
|
52
54
|
/** `FLOOD_WAIT`, `PEER_ID_INVALID`, `login.token` — or nothing. A sentence is dropped, not trimmed. */
|
|
53
55
|
export declare const providerErrorKey: (value: unknown) => string | undefined;
|
|
54
56
|
/**
|
|
@@ -1,30 +1,34 @@
|
|
|
1
|
-
import { CliError } from "@wirecat/cli-base";
|
|
2
1
|
/** Whether a person can scan a code drawn here and answer what the login asks next. */
|
|
3
2
|
export declare const atTerminal: (input: NodeJS.ReadableStream & {
|
|
4
3
|
isTTY?: boolean;
|
|
5
4
|
}) => boolean;
|
|
6
|
-
/**
|
|
7
|
-
export declare const
|
|
5
|
+
/** What an agent reads off the plain-text code: the same words a person reads under the drawn one. */
|
|
6
|
+
export declare const PASTE_QR: string;
|
|
8
7
|
export interface LoginQrOptions {
|
|
9
|
-
/** `--qr-file`: the PNG to write instead of drawing the code. */
|
|
10
|
-
qrFile?: string;
|
|
11
8
|
/** Where the code and the narrow-terminal hint go; `--quiet` must not hide either. */
|
|
12
9
|
diagnostic: (text: string) => void;
|
|
13
|
-
/** Where the
|
|
10
|
+
/** Where where-to-scan and the agent's instruction go. */
|
|
14
11
|
note: (text: string) => void;
|
|
15
12
|
/** How far the code sits right, to line up under a setup step. */
|
|
16
13
|
indent?: number;
|
|
17
14
|
/** Read on every code, so a widened terminal draws the next one. Unknown draws the code. */
|
|
18
15
|
columns?: () => number | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Whether a person reads the diagnostic stream in a terminal. Off one, the code is plain text for an agent to
|
|
18
|
+
* paste into the chat (`plainQr`). Defaults to whether stderr is a terminal.
|
|
19
|
+
*/
|
|
20
|
+
terminal?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Machine output (`--json`, `--jsonl`, or no terminal): each code is also one JSON line on the diagnostic stream,
|
|
23
|
+
* `{"event":"qr","qr":{"text","expires_at","scan"}}`. stdout keeps only the answer, which comes after the scan.
|
|
24
|
+
*/
|
|
25
|
+
json?: boolean;
|
|
19
26
|
}
|
|
20
27
|
export interface LoginQr {
|
|
21
|
-
readonly path: string | undefined;
|
|
22
28
|
/**
|
|
23
29
|
* `scan` says where to scan; it follows the code, and is not said when the terminal is too narrow to draw one.
|
|
24
30
|
* Never throws for a code that does not fit: the messengers call it from inside their own event loops.
|
|
25
31
|
*/
|
|
26
|
-
show: (link: string, scan?: string) => void;
|
|
27
|
-
/** The image logs the account in while it is valid; call this in `finally`. */
|
|
28
|
-
close: () => void;
|
|
32
|
+
show: (link: string, scan?: string, expiresAt?: Date) => void;
|
|
29
33
|
}
|
|
30
|
-
export declare const loginQr: ({
|
|
34
|
+
export declare const loginQr: ({ diagnostic, note, indent: pad, columns, terminal, json, }: LoginQrOptions) => LoginQr;
|
|
@@ -1,48 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { CliError, indent, writeSecurely } from "@wirecat/cli-base";
|
|
1
|
+
import { indent } from "@wirecat/cli-base";
|
|
4
2
|
import { inputPolicy } from "../cli/input-policy.js";
|
|
5
|
-
import {
|
|
3
|
+
import { plainQr, terminalQr } from "./qr.js";
|
|
6
4
|
const MARGIN = 2;
|
|
7
5
|
/** Whether a person can scan a code drawn here and answer what the login asks next. */
|
|
8
6
|
export const atTerminal = (input) => input.isTTY === true && !inputPolicy(input).noInput;
|
|
9
|
-
/**
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
written = true;
|
|
24
|
-
note(`the QR code is in ${path} — it is replaced when the code renews`);
|
|
25
|
-
if (scan)
|
|
26
|
-
note(scan);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const drawn = terminalQr(link);
|
|
30
|
-
const need = drawn.width + pad + MARGIN;
|
|
31
|
-
const width = columns();
|
|
32
|
-
if (width !== undefined && need > width) {
|
|
33
|
-
diagnostic(`the terminal is ${width} columns wide and the QR code needs ${need} — widen it, ` +
|
|
34
|
-
"or run again with --qr-file <png>");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
diagnostic(`\n${indent(drawn.text, pad + MARGIN)}`);
|
|
7
|
+
/** What an agent reads off the plain-text code: the same words a person reads under the drawn one. */
|
|
8
|
+
export const PASTE_QR = "agents: paste the QR code above into the chat as a code block, exactly as printed; " +
|
|
9
|
+
"when a new one is printed, paste it and say the old one expired";
|
|
10
|
+
export const loginQr = ({ diagnostic, note, indent: pad = 0, columns = () => process.stderr.columns, terminal = process.stderr.isTTY === true, json = false, }) => ({
|
|
11
|
+
show: (link, scan, expiresAt) => {
|
|
12
|
+
if (!terminal) {
|
|
13
|
+
const text = plainQr(link);
|
|
14
|
+
diagnostic(`\n${text}\n`);
|
|
15
|
+
if (json)
|
|
16
|
+
diagnostic(JSON.stringify({
|
|
17
|
+
event: "qr",
|
|
18
|
+
qr: { text, expires_at: expiresAt?.toISOString() ?? null, scan: scan ?? null },
|
|
19
|
+
}));
|
|
20
|
+
note(PASTE_QR);
|
|
38
21
|
if (scan)
|
|
39
22
|
note(scan);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const drawn = terminalQr(link);
|
|
26
|
+
const need = drawn.width + pad + MARGIN;
|
|
27
|
+
const width = columns();
|
|
28
|
+
if (width !== undefined && need > width) {
|
|
29
|
+
diagnostic(`the terminal is ${width} columns wide and the QR code needs ${need} — widen it`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
diagnostic(`\n${indent(drawn.text, pad + MARGIN)}`);
|
|
33
|
+
if (scan)
|
|
34
|
+
note(scan);
|
|
35
|
+
},
|
|
36
|
+
});
|
package/dist/terminal/qr.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The same code as a PNG, `scale` pixels a module, for a person or an agent to pass on as an image.
|
|
3
|
-
* 8-bit greyscale, one filter-free row per pixel line: the smallest PNG any decoder reads.
|
|
4
|
-
*/
|
|
5
|
-
export declare const qrPng: (link: string, scale?: number) => Uint8Array;
|
|
6
1
|
/**
|
|
7
2
|
* A QR code as text, two modules per character cell with half blocks, so it fits a terminal
|
|
8
3
|
* without being twice as tall as it is wide. `width` is in columns, for deciding whether it fits.
|
|
@@ -11,3 +6,10 @@ export declare const terminalQr: (link: string) => {
|
|
|
11
6
|
text: string;
|
|
12
7
|
width: number;
|
|
13
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* The same code as plain text, for an agent to paste into a chat where no colour survives. Without colours a glyph
|
|
11
|
+
* takes the foreground colour, so the light modules and the quiet zone are the glyphs: on a dark background the code
|
|
12
|
+
* reads dark on light, and every line starts and ends with a glyph, so nothing trims it. Half blocks keep a long
|
|
13
|
+
* WhatsApp code under 80 columns; full blocks would need about 140 and wrap.
|
|
14
|
+
*/
|
|
15
|
+
export declare const plainQr: (link: string) => string;
|
package/dist/terminal/qr.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { deflateSync } from "node:zlib";
|
|
2
1
|
import qrcode from "qrcode-generator";
|
|
3
2
|
const QUIET_ZONE = 4;
|
|
4
3
|
// Dark modules on a light ground whatever the terminal's theme: a phone camera reads an inverted
|
|
@@ -18,52 +17,6 @@ const modules = (link) => {
|
|
|
18
17
|
};
|
|
19
18
|
return { size, dark };
|
|
20
19
|
};
|
|
21
|
-
/**
|
|
22
|
-
* The same code as a PNG, `scale` pixels a module, for a person or an agent to pass on as an image.
|
|
23
|
-
* 8-bit greyscale, one filter-free row per pixel line: the smallest PNG any decoder reads.
|
|
24
|
-
*/
|
|
25
|
-
export const qrPng = (link, scale = 8) => {
|
|
26
|
-
const { size, dark } = modules(link);
|
|
27
|
-
const side = size * scale;
|
|
28
|
-
const pixels = Buffer.alloc((side + 1) * side, 0xff);
|
|
29
|
-
for (let y = 0; y < side; y++) {
|
|
30
|
-
pixels[y * (side + 1)] = 0;
|
|
31
|
-
for (let x = 0; x < side; x++) {
|
|
32
|
-
if (dark(Math.floor(y / scale), Math.floor(x / scale)))
|
|
33
|
-
pixels[y * (side + 1) + 1 + x] = 0;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const header = Buffer.alloc(13);
|
|
37
|
-
header.writeUInt32BE(side, 0);
|
|
38
|
-
header.writeUInt32BE(side, 4);
|
|
39
|
-
header.set([8, 0, 0, 0, 0], 8);
|
|
40
|
-
return Buffer.concat([
|
|
41
|
-
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
42
|
-
chunk("IHDR", header),
|
|
43
|
-
chunk("IDAT", deflateSync(pixels)),
|
|
44
|
-
chunk("IEND", Buffer.alloc(0)),
|
|
45
|
-
]);
|
|
46
|
-
};
|
|
47
|
-
const chunk = (type, data) => {
|
|
48
|
-
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
49
|
-
const out = Buffer.alloc(body.length + 8);
|
|
50
|
-
out.writeUInt32BE(data.length, 0);
|
|
51
|
-
body.copy(out, 4);
|
|
52
|
-
out.writeUInt32BE(crc32(body), body.length + 4);
|
|
53
|
-
return out;
|
|
54
|
-
};
|
|
55
|
-
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
56
|
-
let c = n;
|
|
57
|
-
for (let k = 0; k < 8; k++)
|
|
58
|
-
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
59
|
-
return c >>> 0;
|
|
60
|
-
});
|
|
61
|
-
const crc32 = (bytes) => {
|
|
62
|
-
let c = 0xffffffff;
|
|
63
|
-
for (const byte of bytes)
|
|
64
|
-
c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
|
|
65
|
-
return (c ^ 0xffffffff) >>> 0;
|
|
66
|
-
};
|
|
67
20
|
/**
|
|
68
21
|
* A QR code as text, two modules per character cell with half blocks, so it fits a terminal
|
|
69
22
|
* without being twice as tall as it is wide. `width` is in columns, for deciding whether it fits.
|
|
@@ -82,3 +35,24 @@ export const terminalQr = (link) => {
|
|
|
82
35
|
}
|
|
83
36
|
return { text: lines.join("\n"), width: size };
|
|
84
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* The same code as plain text, for an agent to paste into a chat where no colour survives. Without colours a glyph
|
|
40
|
+
* takes the foreground colour, so the light modules and the quiet zone are the glyphs: on a dark background the code
|
|
41
|
+
* reads dark on light, and every line starts and ends with a glyph, so nothing trims it. Half blocks keep a long
|
|
42
|
+
* WhatsApp code under 80 columns; full blocks would need about 140 and wrap.
|
|
43
|
+
*/
|
|
44
|
+
export const plainQr = (link) => {
|
|
45
|
+
const { size, dark } = modules(link);
|
|
46
|
+
const light = (row, col) => row < size && !dark(row, col);
|
|
47
|
+
const lines = [];
|
|
48
|
+
for (let row = 0; row < size; row += 2) {
|
|
49
|
+
let line = "";
|
|
50
|
+
for (let col = 0; col < size; col++) {
|
|
51
|
+
const top = light(row, col);
|
|
52
|
+
const bottom = light(row + 1, col);
|
|
53
|
+
line += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " ";
|
|
54
|
+
}
|
|
55
|
+
lines.push(line);
|
|
56
|
+
}
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.362.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
package/parity.json
CHANGED
|
@@ -810,10 +810,6 @@
|
|
|
810
810
|
"value": "<url>",
|
|
811
811
|
"meaning": "the tunnel's https address the browser apps use"
|
|
812
812
|
},
|
|
813
|
-
"--qr-file": {
|
|
814
|
-
"value": "<png>",
|
|
815
|
-
"meaning": "write the QR code to this PNG instead of drawing it, for an agent to pass on"
|
|
816
|
-
},
|
|
817
813
|
"--question": {
|
|
818
814
|
"meaning": "match only questions"
|
|
819
815
|
},
|
|
@@ -4053,7 +4049,6 @@
|
|
|
4053
4049
|
"in": ["tg"],
|
|
4054
4050
|
"reason": "Telegram asks each client for its own app from my.telegram.org; MAX has no such step; WhatsApp has no application registration"
|
|
4055
4051
|
},
|
|
4056
|
-
"--qr-file": "all",
|
|
4057
4052
|
"--sms": {
|
|
4058
4053
|
"in": ["tg"],
|
|
4059
4054
|
"reason": "asks Telegram to resend its login code by SMS; MAX has no such choice; WhatsApp links a device by QR or pairing code, never by an SMS code"
|
|
@@ -4068,8 +4063,7 @@
|
|
|
4068
4063
|
"in": ["tg"],
|
|
4069
4064
|
"reason": "Telegram requires an application from my.telegram.org; MAX has no application registration step; WhatsApp has no application registration"
|
|
4070
4065
|
},
|
|
4071
|
-
"--method": "all"
|
|
4072
|
-
"--qr-file": "all"
|
|
4066
|
+
"--method": "all"
|
|
4073
4067
|
}
|
|
4074
4068
|
},
|
|
4075
4069
|
"skill": {
|