@oh-my-pi/pi-coding-agent 16.0.9 → 16.0.11

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 (110) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/cli.js +3402 -3443
  3. package/dist/types/advisor/index.d.ts +1 -0
  4. package/dist/types/advisor/transcript-recorder.d.ts +52 -0
  5. package/dist/types/collab/host.d.ts +2 -2
  6. package/dist/types/collab/protocol.d.ts +4 -5
  7. package/dist/types/commit/agentic/agent.d.ts +1 -1
  8. package/dist/types/config/model-resolver.d.ts +11 -2
  9. package/dist/types/config/settings-schema.d.ts +12 -6
  10. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  11. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  12. package/dist/types/modes/components/agent-hub.d.ts +6 -1
  13. package/dist/types/modes/components/agent-transcript-viewer.d.ts +39 -0
  14. package/dist/types/modes/components/chat-transcript-builder.d.ts +42 -0
  15. package/dist/types/modes/controllers/command-controller.d.ts +3 -2
  16. package/dist/types/modes/interactive-mode.d.ts +2 -1
  17. package/dist/types/modes/types.d.ts +2 -1
  18. package/dist/types/registry/agent-registry.d.ts +10 -3
  19. package/dist/types/session/agent-session.d.ts +13 -0
  20. package/dist/types/session/compact-modes.d.ts +60 -0
  21. package/dist/types/session/streaming-output.d.ts +0 -2
  22. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  23. package/dist/types/slash-commands/helpers/collab-qrcode.d.ts +13 -0
  24. package/dist/types/tools/__tests__/json-tree.test.d.ts +1 -0
  25. package/dist/types/tools/index.d.ts +9 -1
  26. package/dist/types/utils/image-loading.d.ts +12 -0
  27. package/dist/types/utils/qrcode.d.ts +48 -0
  28. package/package.json +12 -12
  29. package/src/advisor/index.ts +1 -0
  30. package/src/advisor/transcript-recorder.ts +136 -0
  31. package/src/cli/args.ts +7 -1
  32. package/src/cli/stats-cli.ts +2 -11
  33. package/src/collab/host.ts +29 -17
  34. package/src/collab/protocol.ts +48 -15
  35. package/src/commit/agentic/agent.ts +2 -1
  36. package/src/commit/agentic/tools/git-file-diff.ts +2 -2
  37. package/src/commit/changelog/index.ts +1 -1
  38. package/src/commit/map-reduce/map-phase.ts +1 -1
  39. package/src/commit/map-reduce/utils.ts +1 -1
  40. package/src/config/config-file.ts +1 -1
  41. package/src/config/keybindings.ts +2 -2
  42. package/src/config/model-registry.ts +16 -4
  43. package/src/config/model-resolver.ts +193 -35
  44. package/src/config/settings-schema.ts +14 -7
  45. package/src/config/settings.ts +3 -9
  46. package/src/edit/file-snapshot-store.ts +1 -1
  47. package/src/edit/renderer.ts +7 -7
  48. package/src/eval/js/tool-bridge.ts +3 -2
  49. package/src/eval/py/prelude.py +3 -2
  50. package/src/export/html/tool-views.generated.js +28 -28
  51. package/src/extensibility/extensions/types.ts +7 -0
  52. package/src/hindsight/mental-models.ts +1 -1
  53. package/src/internal-urls/docs-index.generated.txt +1 -1
  54. package/src/internal-urls/history-protocol.ts +8 -3
  55. package/src/irc/bus.ts +8 -0
  56. package/src/lsp/index.ts +2 -2
  57. package/src/main.ts +6 -3
  58. package/src/modes/acp/acp-agent.ts +63 -0
  59. package/src/modes/components/agent-hub.ts +97 -920
  60. package/src/modes/components/agent-transcript-viewer.ts +461 -0
  61. package/src/modes/components/chat-transcript-builder.ts +462 -0
  62. package/src/modes/components/diff.ts +12 -35
  63. package/src/modes/components/oauth-selector.ts +31 -2
  64. package/src/modes/controllers/command-controller.ts +12 -2
  65. package/src/modes/controllers/event-controller.ts +1 -1
  66. package/src/modes/controllers/input-controller.ts +8 -1
  67. package/src/modes/controllers/selector-controller.ts +4 -1
  68. package/src/modes/interactive-mode.ts +4 -2
  69. package/src/modes/types.ts +2 -1
  70. package/src/prompts/tools/inspect-image.md +1 -1
  71. package/src/prompts/tools/read.md +1 -1
  72. package/src/registry/agent-registry.ts +13 -4
  73. package/src/sdk.ts +27 -8
  74. package/src/session/agent-session.ts +185 -17
  75. package/src/session/compact-modes.ts +105 -0
  76. package/src/session/session-dump-format.ts +1 -1
  77. package/src/session/session-history-format.ts +1 -1
  78. package/src/session/streaming-output.ts +5 -5
  79. package/src/slash-commands/builtin-registry.ts +45 -15
  80. package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
  81. package/src/task/executor.ts +1 -1
  82. package/src/task/output-manager.ts +5 -0
  83. package/src/thinking.ts +25 -5
  84. package/src/tools/__tests__/json-tree.test.ts +35 -0
  85. package/src/tools/approval.ts +1 -1
  86. package/src/tools/bash.ts +0 -1
  87. package/src/tools/browser.ts +0 -1
  88. package/src/tools/eval.ts +1 -1
  89. package/src/tools/gh.ts +1 -1
  90. package/src/tools/index.ts +10 -1
  91. package/src/tools/inspect-image.ts +72 -9
  92. package/src/tools/irc.ts +1 -1
  93. package/src/tools/json-tree.ts +22 -5
  94. package/src/tools/read.ts +5 -6
  95. package/src/utils/file-mentions.ts +5 -2
  96. package/src/utils/image-loading.ts +58 -0
  97. package/src/utils/qrcode.ts +535 -0
  98. package/src/web/scrapers/firefox-addons.ts +1 -1
  99. package/src/web/scrapers/github.ts +1 -1
  100. package/src/web/scrapers/go-pkg.ts +2 -2
  101. package/src/web/scrapers/metacpan.ts +2 -2
  102. package/src/web/scrapers/nvd.ts +2 -2
  103. package/src/web/scrapers/ollama.ts +1 -1
  104. package/src/web/scrapers/opencorporates.ts +1 -1
  105. package/src/web/scrapers/pub-dev.ts +1 -1
  106. package/src/web/scrapers/repology.ts +1 -1
  107. package/src/web/scrapers/sourcegraph.ts +1 -1
  108. package/src/web/scrapers/terraform.ts +6 -6
  109. package/src/web/scrapers/wikidata.ts +2 -2
  110. package/src/workspace-tree.ts +1 -1
@@ -38,6 +38,17 @@ export interface LoadImageInputOptions {
38
38
  excludeWebP?: boolean;
39
39
  }
40
40
 
41
+ /** Options for loading an in-memory chat image attachment as a vision-model input. */
42
+ export interface LoadImageAttachmentInputOptions {
43
+ image: ImageContent;
44
+ label: string;
45
+ uri: string;
46
+ autoResize: boolean;
47
+ maxBytes?: number;
48
+ /** Force non-WebP output (e.g. for Ollama). Leave unset to honor `OMP_NO_WEBP`. */
49
+ excludeWebP?: boolean;
50
+ }
51
+
41
52
  export interface LoadedImageInput {
42
53
  resolvedPath: string;
43
54
  mimeType: string;
@@ -161,3 +172,50 @@ export async function loadImageInput(options: LoadImageInputOptions): Promise<Lo
161
172
  bytes: outputBytes,
162
173
  };
163
174
  }
175
+
176
+ /** Loads a chat attachment image through the same size and encoder policy as file-backed image inputs. */
177
+ export async function loadImageAttachmentInput(
178
+ options: LoadImageAttachmentInputOptions,
179
+ ): Promise<LoadedImageInput | null> {
180
+ const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
181
+ if (!SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(options.image.mimeType)) {
182
+ return null;
183
+ }
184
+
185
+ const inputBytes = Buffer.byteLength(options.image.data, "base64");
186
+ if (inputBytes > maxBytes) {
187
+ throw new ImageInputTooLargeError(inputBytes, maxBytes);
188
+ }
189
+
190
+ let outputData = options.image.data;
191
+ let outputMimeType = options.image.mimeType;
192
+ let outputBytes = inputBytes;
193
+ let dimensionNote: string | undefined;
194
+
195
+ const shouldReencodeWebP = options.excludeWebP === true && options.image.mimeType === "image/webp";
196
+ if (options.autoResize || shouldReencodeWebP) {
197
+ try {
198
+ const resized = await resizeImage(options.image, { excludeWebP: options.excludeWebP });
199
+ outputData = resized.data;
200
+ outputMimeType = resized.mimeType;
201
+ outputBytes = resized.buffer.byteLength;
202
+ dimensionNote = formatDimensionNote(resized);
203
+ } catch {
204
+ // keep original image when resize fails
205
+ }
206
+ }
207
+
208
+ let textNote = `Read image attachment ${options.label} [${outputMimeType}]`;
209
+ if (dimensionNote) {
210
+ textNote += `\n${dimensionNote}`;
211
+ }
212
+
213
+ return {
214
+ resolvedPath: options.uri,
215
+ mimeType: outputMimeType,
216
+ data: outputData,
217
+ textNote,
218
+ dimensionNote,
219
+ bytes: outputBytes,
220
+ };
221
+ }
@@ -0,0 +1,535 @@
1
+ /**
2
+ * Self-contained QR Code generator (byte mode, versions 1-40, EC levels
3
+ * L/M/Q/H) with a half-block ANSI terminal renderer.
4
+ *
5
+ * Pure TypeScript, zero dependencies: the collab `/collab qrcode` command uses
6
+ * it to print scannable browser-join codes without pulling a runtime QR
7
+ * package into the bundle. The algorithm follows ISO/IEC 18004; the two
8
+ * error-correction tables below are direct transcriptions of that spec.
9
+ */
10
+
11
+ export type QrEcLevel = "L" | "M" | "Q" | "H";
12
+
13
+ /** Per-EC-level metadata: index into the spec tables and the 2-bit format code. */
14
+ const EC_LEVELS: Record<QrEcLevel, { table: number; formatBits: number }> = {
15
+ L: { table: 0, formatBits: 1 },
16
+ M: { table: 1, formatBits: 0 },
17
+ Q: { table: 2, formatBits: 3 },
18
+ H: { table: 3, formatBits: 2 },
19
+ };
20
+
21
+ const MIN_VERSION = 1;
22
+ const MAX_VERSION = 40;
23
+
24
+ // ISO/IEC 18004 Table 9 — error-correction codewords per block, indexed
25
+ // [ecTable][version]. Index 0 of each row pads the 1-based version axis.
26
+ // biome-ignore format: spec table, one row per EC level
27
+ const ECC_CODEWORDS_PER_BLOCK: readonly (readonly number[])[] = [
28
+ [-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
29
+ [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
30
+ [-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
31
+ [-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
32
+ ];
33
+
34
+ // ISO/IEC 18004 Table 9 — number of error-correction blocks, indexed
35
+ // [ecTable][version].
36
+ // biome-ignore format: spec table, one row per EC level
37
+ const NUM_EC_BLOCKS: readonly (readonly number[])[] = [
38
+ [-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
39
+ [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
40
+ [-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
41
+ [-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81],
42
+ ];
43
+
44
+ const BYTE_MODE_INDICATOR = 0x4;
45
+ const PAD_BYTES = [0xec, 0x11] as const;
46
+
47
+ const PENALTY_N1 = 3;
48
+ const PENALTY_N2 = 3;
49
+ const PENALTY_N3 = 40;
50
+ const PENALTY_N4 = 10;
51
+
52
+ function getBit(value: number, index: number): boolean {
53
+ return ((value >>> index) & 1) !== 0;
54
+ }
55
+
56
+ /** Whether mask `m` flips the module at (x, y); the 8 data-mask conditions from the spec. */
57
+ function maskBit(m: number, x: number, y: number): boolean {
58
+ switch (m) {
59
+ case 0:
60
+ return (x + y) % 2 === 0;
61
+ case 1:
62
+ return y % 2 === 0;
63
+ case 2:
64
+ return x % 3 === 0;
65
+ case 3:
66
+ return (x + y) % 3 === 0;
67
+ case 4:
68
+ return (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
69
+ case 5:
70
+ return ((x * y) % 2) + ((x * y) % 3) === 0;
71
+ case 6:
72
+ return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0;
73
+ default:
74
+ return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0;
75
+ }
76
+ }
77
+
78
+ /** GF(256) multiply under the QR primitive polynomial x^8 + x^4 + x^3 + x^2 + 1 (0x11D). */
79
+ function gfMultiply(x: number, y: number): number {
80
+ let z = 0;
81
+ for (let i = 7; i >= 0; i--) {
82
+ z = (z << 1) ^ ((z >>> 7) * 0x11d);
83
+ z ^= ((y >>> i) & 1) * x;
84
+ }
85
+ return z & 0xff;
86
+ }
87
+
88
+ /** Reed-Solomon generator polynomial coefficients for `degree` EC codewords. */
89
+ function rsDivisor(degree: number): Uint8Array {
90
+ const result = new Uint8Array(degree);
91
+ result[degree - 1] = 1;
92
+ let root = 1;
93
+ for (let i = 0; i < degree; i++) {
94
+ for (let j = 0; j < result.length; j++) {
95
+ result[j] = gfMultiply(result[j]!, root);
96
+ if (j + 1 < result.length) result[j] ^= result[j + 1]!;
97
+ }
98
+ root = gfMultiply(root, 0x02);
99
+ }
100
+ return result;
101
+ }
102
+
103
+ /** Reed-Solomon remainder (the EC codewords) of `data` divided by `divisor`. */
104
+ function rsRemainder(data: Uint8Array, divisor: Uint8Array): Uint8Array {
105
+ const result = new Uint8Array(divisor.length);
106
+ for (const b of data) {
107
+ const factor = b ^ result[0]!;
108
+ result.copyWithin(0, 1);
109
+ result[result.length - 1] = 0;
110
+ for (let i = 0; i < divisor.length; i++) result[i] ^= gfMultiply(divisor[i]!, factor);
111
+ }
112
+ return result;
113
+ }
114
+
115
+ /** Total data modules (bits available before EC) for a version. */
116
+ function rawDataModules(version: number): number {
117
+ let result = (16 * version + 128) * version + 64;
118
+ if (version >= 2) {
119
+ const numAlign = Math.floor(version / 7) + 2;
120
+ result -= (25 * numAlign - 10) * numAlign - 55;
121
+ if (version >= 7) result -= 36;
122
+ }
123
+ return result;
124
+ }
125
+
126
+ /** Number of usable data codewords (8-bit) at a given version + EC level. */
127
+ function dataCodewords(version: number, ecTable: number): number {
128
+ return (
129
+ Math.floor(rawDataModules(version) / 8) -
130
+ ECC_CODEWORDS_PER_BLOCK[ecTable]![version]! * NUM_EC_BLOCKS[ecTable]![version]!
131
+ );
132
+ }
133
+
134
+ /** Byte-mode character-count indicator width in bits for a version. */
135
+ function charCountBits(version: number): number {
136
+ return version <= 9 ? 8 : 16;
137
+ }
138
+
139
+ export interface QrEncodeOptions {
140
+ /** Lowest version to consider (default 1). */
141
+ minVersion?: number;
142
+ /** Highest version to consider (default 40). */
143
+ maxVersion?: number;
144
+ /** Force a mask 0-7; -1 (default) auto-selects the lowest-penalty mask. */
145
+ mask?: number;
146
+ }
147
+
148
+ /**
149
+ * A finished QR symbol: a square grid of dark/light modules plus the chosen
150
+ * version, EC level, and mask. `module(x, y)` is the only access path the
151
+ * renderers need.
152
+ */
153
+ export class QrCode {
154
+ readonly size: number;
155
+ /** Selected mask pattern (0-7). */
156
+ readonly mask: number;
157
+ readonly #modules: boolean[][];
158
+ readonly #isFunction: boolean[][];
159
+
160
+ private constructor(
161
+ readonly version: number,
162
+ readonly ecLevel: QrEcLevel,
163
+ dataCodewordsInterleaved: Uint8Array,
164
+ mask: number,
165
+ ) {
166
+ this.size = version * 4 + 17;
167
+ this.#modules = Array.from({ length: this.size }, () => new Array<boolean>(this.size).fill(false));
168
+ this.#isFunction = Array.from({ length: this.size }, () => new Array<boolean>(this.size).fill(false));
169
+
170
+ this.#drawFunctionPatterns();
171
+ this.#drawCodewords(dataCodewordsInterleaved);
172
+ this.mask = this.#selectMask(mask);
173
+ }
174
+
175
+ module(x: number, y: number): boolean {
176
+ return this.#modules[y]![x]!;
177
+ }
178
+
179
+ /** Encode a string in byte mode (UTF-8). Throws if it exceeds version 40. */
180
+ static encodeText(text: string, ecLevel: QrEcLevel = "M", options?: QrEncodeOptions): QrCode {
181
+ return QrCode.encodeBytes(new TextEncoder().encode(text), ecLevel, options);
182
+ }
183
+
184
+ /** Encode raw bytes in byte mode. Throws if they exceed version 40 at this EC level. */
185
+ static encodeBytes(data: Uint8Array, ecLevel: QrEcLevel = "M", options?: QrEncodeOptions): QrCode {
186
+ const ec = EC_LEVELS[ecLevel];
187
+ const minVersion = Math.max(MIN_VERSION, options?.minVersion ?? MIN_VERSION);
188
+ const maxVersion = Math.min(MAX_VERSION, options?.maxVersion ?? MAX_VERSION);
189
+
190
+ let version = minVersion;
191
+ for (; ; version++) {
192
+ const capacityBits = dataCodewords(version, ec.table) * 8;
193
+ const usedBits = 4 + charCountBits(version) + data.length * 8;
194
+ if (usedBits <= capacityBits) break;
195
+ if (version >= maxVersion) {
196
+ throw new Error(`data too long for a QR code (${data.length} bytes, EC ${ecLevel})`);
197
+ }
198
+ }
199
+
200
+ const bits = new BitBuffer();
201
+ bits.append(BYTE_MODE_INDICATOR, 4);
202
+ bits.append(data.length, charCountBits(version));
203
+ for (const b of data) bits.append(b, 8);
204
+
205
+ const capacityBits = dataCodewords(version, ec.table) * 8;
206
+ bits.append(0, Math.min(4, capacityBits - bits.length)); // terminator
207
+ bits.append(0, (8 - (bits.length % 8)) % 8); // byte-align
208
+ for (let pad = 0; bits.length < capacityBits; pad ^= 1) bits.append(PAD_BYTES[pad]!, 8);
209
+
210
+ const codewords = QrCode.#interleave(bits.toBytes(), version, ec.table);
211
+ const mask = options?.mask ?? -1;
212
+ if (mask < -1 || mask > 7) throw new Error(`invalid mask ${mask}`);
213
+ return new QrCode(version, ecLevel, codewords, mask);
214
+ }
215
+
216
+ /** Split into blocks, append Reed-Solomon EC, and interleave per the spec. */
217
+ static #interleave(data: Uint8Array, version: number, ecTable: number): Uint8Array {
218
+ const numBlocks = NUM_EC_BLOCKS[ecTable]![version]!;
219
+ const eccLen = ECC_CODEWORDS_PER_BLOCK[ecTable]![version]!;
220
+ const rawCodewords = Math.floor(rawDataModules(version) / 8);
221
+ const numShort = numBlocks - (rawCodewords % numBlocks);
222
+ const shortLen = Math.floor(rawCodewords / numBlocks);
223
+ const divisor = rsDivisor(eccLen);
224
+
225
+ const blocks: Uint8Array[] = [];
226
+ const blockLen = shortLen + 1;
227
+ for (let i = 0, offset = 0; i < numBlocks; i++) {
228
+ const datLen = shortLen - eccLen + (i < numShort ? 0 : 1);
229
+ const dat = data.subarray(offset, offset + datLen);
230
+ offset += datLen;
231
+ // Every block is padded to the longest block's length so interleaving
232
+ // stays column-aligned; short blocks leave a zero in the last data slot.
233
+ const block = new Uint8Array(blockLen);
234
+ block.set(dat, 0);
235
+ block.set(rsRemainder(dat, divisor), blockLen - eccLen);
236
+ blocks.push(block);
237
+ }
238
+
239
+ const result = new Uint8Array(rawCodewords);
240
+ let w = 0;
241
+ for (let i = 0; i < blockLen; i++) {
242
+ for (let b = 0; b < numBlocks; b++) {
243
+ // Skip the padding column at the data/EC boundary of short blocks.
244
+ if (i === shortLen - eccLen && b < numShort) continue;
245
+ result[w++] = blocks[b]![i]!;
246
+ }
247
+ }
248
+ return result;
249
+ }
250
+
251
+ // ── Module placement ──────────────────────────────────────────────────
252
+
253
+ #setFunction(x: number, y: number, dark: boolean): void {
254
+ this.#modules[y]![x] = dark;
255
+ this.#isFunction[y]![x] = true;
256
+ }
257
+
258
+ #drawFunctionPatterns(): void {
259
+ for (let i = 0; i < this.size; i++) {
260
+ this.#setFunction(6, i, i % 2 === 0);
261
+ this.#setFunction(i, 6, i % 2 === 0);
262
+ }
263
+ this.#drawFinder(3, 3);
264
+ this.#drawFinder(this.size - 4, 3);
265
+ this.#drawFinder(3, this.size - 4);
266
+
267
+ const align = this.#alignmentPositions();
268
+ const last = align.length - 1;
269
+ for (let i = 0; i <= last; i++) {
270
+ for (let j = 0; j <= last; j++) {
271
+ // Skip the three finder corners.
272
+ if ((i === 0 && j === 0) || (i === 0 && j === last) || (i === last && j === 0)) continue;
273
+ this.#drawAlignment(align[i]!, align[j]!);
274
+ }
275
+ }
276
+
277
+ this.#drawFormatBits(0); // placeholder until mask chosen
278
+ this.#drawVersion();
279
+ }
280
+
281
+ #drawFinder(cx: number, cy: number): void {
282
+ for (let dy = -4; dy <= 4; dy++) {
283
+ for (let dx = -4; dx <= 4; dx++) {
284
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
285
+ const x = cx + dx;
286
+ const y = cy + dy;
287
+ if (x >= 0 && x < this.size && y >= 0 && y < this.size) {
288
+ this.#setFunction(x, y, dist !== 2 && dist !== 4);
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ #drawAlignment(cx: number, cy: number): void {
295
+ for (let dy = -2; dy <= 2; dy++) {
296
+ for (let dx = -2; dx <= 2; dx++) {
297
+ this.#setFunction(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
298
+ }
299
+ }
300
+ }
301
+
302
+ #alignmentPositions(): number[] {
303
+ if (this.version === 1) return [];
304
+ const numAlign = Math.floor(this.version / 7) + 2;
305
+ const step = this.version === 32 ? 26 : Math.ceil((this.size - 13) / (numAlign * 2 - 2)) * 2;
306
+ const result = [6];
307
+ for (let pos = this.size - 7; result.length < numAlign; pos -= step) result.splice(1, 0, pos);
308
+ return result;
309
+ }
310
+
311
+ #drawFormatBits(mask: number): void {
312
+ const data = (EC_LEVELS[this.ecLevel].formatBits << 3) | mask;
313
+ let rem = data;
314
+ for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
315
+ const bits = ((data << 10) | rem) ^ 0x5412;
316
+
317
+ for (let i = 0; i <= 5; i++) this.#setFunction(8, i, getBit(bits, i));
318
+ this.#setFunction(8, 7, getBit(bits, 6));
319
+ this.#setFunction(8, 8, getBit(bits, 7));
320
+ this.#setFunction(7, 8, getBit(bits, 8));
321
+ for (let i = 9; i < 15; i++) this.#setFunction(14 - i, 8, getBit(bits, i));
322
+
323
+ for (let i = 0; i < 8; i++) this.#setFunction(this.size - 1 - i, 8, getBit(bits, i));
324
+ for (let i = 8; i < 15; i++) this.#setFunction(8, this.size - 15 + i, getBit(bits, i));
325
+ this.#setFunction(8, this.size - 8, true); // always-dark module
326
+ }
327
+
328
+ #drawVersion(): void {
329
+ if (this.version < 7) return;
330
+ let rem = this.version;
331
+ for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
332
+ const bits = (this.version << 12) | rem;
333
+ for (let i = 0; i < 18; i++) {
334
+ const bit = getBit(bits, i);
335
+ const a = this.size - 11 + (i % 3);
336
+ const b = Math.floor(i / 3);
337
+ this.#setFunction(a, b, bit);
338
+ this.#setFunction(b, a, bit);
339
+ }
340
+ }
341
+
342
+ #drawCodewords(data: Uint8Array): void {
343
+ let i = 0;
344
+ const totalBits = data.length * 8;
345
+ for (let right = this.size - 1; right >= 1; right -= 2) {
346
+ if (right === 6) right = 5;
347
+ for (let vert = 0; vert < this.size; vert++) {
348
+ for (let j = 0; j < 2; j++) {
349
+ const x = right - j;
350
+ const upward = ((right + 1) & 2) === 0;
351
+ const y = upward ? this.size - 1 - vert : vert;
352
+ if (!this.#isFunction[y]![x] && i < totalBits) {
353
+ this.#modules[y]![x] = getBit(data[i >>> 3]!, 7 - (i & 7));
354
+ i++;
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ // ── Masking ───────────────────────────────────────────────────────────
362
+
363
+ #applyMask(mask: number): void {
364
+ for (let y = 0; y < this.size; y++) {
365
+ for (let x = 0; x < this.size; x++) {
366
+ if (!this.#isFunction[y]![x] && maskBit(mask, x, y)) {
367
+ this.#modules[y]![x] = !this.#modules[y]![x];
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ #selectMask(forced: number): number {
374
+ let mask = forced;
375
+ if (mask === -1) {
376
+ let minPenalty = Infinity;
377
+ for (let m = 0; m < 8; m++) {
378
+ this.#applyMask(m);
379
+ this.#drawFormatBits(m);
380
+ const penalty = this.#penaltyScore();
381
+ if (penalty < minPenalty) {
382
+ mask = m;
383
+ minPenalty = penalty;
384
+ }
385
+ this.#applyMask(m); // undo (XOR mask is self-inverse)
386
+ }
387
+ }
388
+ this.#applyMask(mask);
389
+ this.#drawFormatBits(mask);
390
+ return mask;
391
+ }
392
+
393
+ #penaltyScore(): number {
394
+ let result = 0;
395
+ const size = this.size;
396
+ const mods = this.#modules;
397
+
398
+ // Rule 1 + Rule 3 — adjacent same-color runs, finder-like patterns (rows).
399
+ for (let y = 0; y < size; y++) {
400
+ let runColor = false;
401
+ let runLen = 0;
402
+ const history = [0, 0, 0, 0, 0, 0, 0];
403
+ for (let x = 0; x < size; x++) {
404
+ if (mods[y]![x] === runColor) {
405
+ runLen++;
406
+ if (runLen === 5) result += PENALTY_N1;
407
+ else if (runLen > 5) result++;
408
+ } else {
409
+ this.#finderAddHistory(runLen, history);
410
+ if (!runColor) result += this.#finderCountPatterns(history) * PENALTY_N3;
411
+ runColor = mods[y]![x]!;
412
+ runLen = 1;
413
+ }
414
+ }
415
+ result += this.#finderTerminate(runColor, runLen, history) * PENALTY_N3;
416
+ }
417
+ // Rule 1 + Rule 3 — columns.
418
+ for (let x = 0; x < size; x++) {
419
+ let runColor = false;
420
+ let runLen = 0;
421
+ const history = [0, 0, 0, 0, 0, 0, 0];
422
+ for (let y = 0; y < size; y++) {
423
+ if (mods[y]![x] === runColor) {
424
+ runLen++;
425
+ if (runLen === 5) result += PENALTY_N1;
426
+ else if (runLen > 5) result++;
427
+ } else {
428
+ this.#finderAddHistory(runLen, history);
429
+ if (!runColor) result += this.#finderCountPatterns(history) * PENALTY_N3;
430
+ runColor = mods[y]![x]!;
431
+ runLen = 1;
432
+ }
433
+ }
434
+ result += this.#finderTerminate(runColor, runLen, history) * PENALTY_N3;
435
+ }
436
+
437
+ // Rule 2 — 2x2 blocks of one color.
438
+ for (let y = 0; y < size - 1; y++) {
439
+ for (let x = 0; x < size - 1; x++) {
440
+ const c = mods[y]![x];
441
+ if (c === mods[y]![x + 1] && c === mods[y + 1]![x] && c === mods[y + 1]![x + 1]) {
442
+ result += PENALTY_N2;
443
+ }
444
+ }
445
+ }
446
+
447
+ // Rule 4 — dark/light balance.
448
+ let dark = 0;
449
+ for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (mods[y]![x]) dark++;
450
+ const total = size * size;
451
+ const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
452
+ result += k * PENALTY_N4;
453
+ return result;
454
+ }
455
+
456
+ #finderCountPatterns(history: readonly number[]): number {
457
+ const n = history[1]!;
458
+ const core = n > 0 && history[2] === n && history[3] === n * 3 && history[4] === n && history[5] === n;
459
+ return (
460
+ (core && history[0]! >= n * 4 && history[6]! >= n ? 1 : 0) +
461
+ (core && history[6]! >= n * 4 && history[0]! >= n ? 1 : 0)
462
+ );
463
+ }
464
+
465
+ #finderAddHistory(runLen: number, history: number[]): void {
466
+ if (history[0] === 0) runLen += this.size; // light border before the first run
467
+ history.pop();
468
+ history.unshift(runLen);
469
+ }
470
+
471
+ #finderTerminate(runColor: boolean, runLen: number, history: number[]): number {
472
+ if (runColor) {
473
+ this.#finderAddHistory(runLen, history);
474
+ runLen = 0;
475
+ }
476
+ runLen += this.size; // light border after the final run
477
+ this.#finderAddHistory(runLen, history);
478
+ return this.#finderCountPatterns(history);
479
+ }
480
+ }
481
+
482
+ /** Append-only MSB-first bit buffer. */
483
+ class BitBuffer {
484
+ #bits: number[] = [];
485
+
486
+ get length(): number {
487
+ return this.#bits.length;
488
+ }
489
+
490
+ append(value: number, count: number): void {
491
+ for (let i = count - 1; i >= 0; i--) this.#bits.push((value >>> i) & 1);
492
+ }
493
+
494
+ toBytes(): Uint8Array {
495
+ const out = new Uint8Array(this.#bits.length >>> 3);
496
+ for (let i = 0; i < this.#bits.length; i++) out[i >>> 3] = (out[i >>> 3]! << 1) | this.#bits[i]!;
497
+ return out;
498
+ }
499
+ }
500
+
501
+ export interface QrRenderOptions {
502
+ /** Quiet-zone width in modules on every side (default 4, per spec). */
503
+ margin?: number;
504
+ }
505
+
506
+ const ANSI_RESET = "\x1b[0m";
507
+ const ANSI_QR_ROW_PREFIX = "\x1b[47m\x1b[30m"; // white background, black foreground
508
+
509
+ /**
510
+ * Render a QR symbol as ANSI half-block rows: each text row packs two module
511
+ * rows via `▀`/`▄`/`█`, drawn black-on-white so a phone camera reads dark
512
+ * modules as data and the quiet zone as the light margin. The leading margin
513
+ * makes the symbol scannable regardless of the terminal's own background.
514
+ */
515
+ export function renderQrHalfBlocks(qr: QrCode, options?: QrRenderOptions): string[] {
516
+ const margin = Math.max(0, options?.margin ?? 4);
517
+ const dim = qr.size + margin * 2;
518
+ const dark = (gx: number, gy: number): boolean => {
519
+ const x = gx - margin;
520
+ const y = gy - margin;
521
+ return x >= 0 && x < qr.size && y >= 0 && y < qr.size && qr.module(x, y);
522
+ };
523
+
524
+ const lines: string[] = [];
525
+ for (let gy = 0; gy < dim; gy += 2) {
526
+ let row = ANSI_QR_ROW_PREFIX;
527
+ for (let gx = 0; gx < dim; gx++) {
528
+ const top = dark(gx, gy);
529
+ const bottom = gy + 1 < dim && dark(gx, gy + 1);
530
+ row += top ? (bottom ? "█" : "▀") : bottom ? "▄" : " ";
531
+ }
532
+ lines.push(row + ANSI_RESET);
533
+ }
534
+ return lines;
535
+ }
@@ -173,7 +173,7 @@ export const handleFirefoxAddons: SpecialHandler = async (
173
173
  md += `- ${permission}\n`;
174
174
  }
175
175
  if (permissions.length > preview.length) {
176
- md += `\n*...and ${permissions.length - preview.length} more*\n`;
176
+ md += `\n[…${permissions.length - preview.length} permissions elided…]\n`;
177
177
  }
178
178
  }
179
179
 
@@ -468,7 +468,7 @@ async function renderGitHubRepo(
468
468
  md += `${prefix}${item.path}\n`;
469
469
  }
470
470
  if (tree.length > 100) {
471
- md += `... and ${tree.length - 100} more files\n`;
471
+ md += `[…${tree.length - 100} files elided…]\n`;
472
472
  }
473
473
  md += "```\n\n";
474
474
  }
@@ -211,7 +211,7 @@ export const handleGoPkg: SpecialHandler = async (
211
211
  sections.push(exported.slice(0, 50).join("\n"));
212
212
  if (exported.length > 50) {
213
213
  notes.push(`showing 50 of ${exported.length} exports`);
214
- sections.push(`\n... and ${exported.length - 50} more`);
214
+ sections.push(`\n[…${exported.length - 50} exports elided…]`);
215
215
  }
216
216
  sections.push("");
217
217
  }
@@ -240,7 +240,7 @@ export const handleGoPkg: SpecialHandler = async (
240
240
  sections.push(imports.slice(0, 20).join("\n"));
241
241
  if (imports.length > 20) {
242
242
  notes.push(`showing 20 of ${imports.length} imports`);
243
- sections.push(`\n... and ${imports.length - 20} more`);
243
+ sections.push(`\n[…${imports.length - 20} imports elided…]`);
244
244
  }
245
245
  sections.push("");
246
246
  }
@@ -162,7 +162,7 @@ function formatModuleMarkdown(module: ModuleResponse, release: ReleaseResponse |
162
162
  md += "\n";
163
163
  }
164
164
  if (runtimeDeps.length > 20) {
165
- md += `\n*...and ${runtimeDeps.length - 20} more*\n`;
165
+ md += `\n[…${runtimeDeps.length - 20} dependencies elided…]\n`;
166
166
  }
167
167
  }
168
168
  }
@@ -212,7 +212,7 @@ function formatReleaseMarkdown(release: ReleaseResponse): string {
212
212
  md += "\n";
213
213
  }
214
214
  if (runtimeDeps.length > 20) {
215
- md += `\n*...and ${runtimeDeps.length - 20} more*\n`;
215
+ md += `\n[…${runtimeDeps.length - 20} dependencies elided…]\n`;
216
216
  }
217
217
  }
218
218
 
@@ -182,7 +182,7 @@ export const handleNvd: SpecialHandler = async (
182
182
  md += `- \`${cpe}\`\n`;
183
183
  }
184
184
  if (cpes.length > 20) {
185
- md += `\n*...and ${cpes.length - 20} more*\n`;
185
+ md += `\n[…${cpes.length - 20} CPEs elided…]\n`;
186
186
  }
187
187
  md += "\n";
188
188
  }
@@ -195,7 +195,7 @@ export const handleNvd: SpecialHandler = async (
195
195
  md += `- ${ref.url}${tags}\n`;
196
196
  }
197
197
  if (vuln.references.length > 15) {
198
- md += `\n*...and ${vuln.references.length - 15} more references*\n`;
198
+ md += `\n[…${vuln.references.length - 15} references elided…]\n`;
199
199
  }
200
200
  }
201
201
 
@@ -139,7 +139,7 @@ function formatTagList(tags: string[], maxItems: number): string {
139
139
  const limited = tags.slice(0, maxItems);
140
140
  const formatted = limited.map(tag => `\`${tag}\``).join(", ");
141
141
  if (tags.length > maxItems) {
142
- return `${formatted} (and ${tags.length - maxItems} more)`;
142
+ return `${formatted} […${tags.length - maxItems} tags elided…]`;
143
143
  }
144
144
  return formatted;
145
145
  }
@@ -216,7 +216,7 @@ export const handleOpenCorporates: SpecialHandler = async (
216
216
  md += "\n";
217
217
  }
218
218
  if (inactiveOfficers.length > 10) {
219
- md += `\n*...and ${inactiveOfficers.length - 10} more former officers*\n`;
219
+ md += `\n[…${inactiveOfficers.length - 10} former officers elided…]\n`;
220
220
  }
221
221
  md += "\n";
222
222
  }