@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
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/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
package/src/c64.ts
CHANGED
|
@@ -20,6 +20,16 @@ export function formatC64Address(value: number): string {
|
|
|
20
20
|
return `$${checkAddress(value).toString(16).padStart(4, "0")}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** VIC-II state with its addresses as $xxxx; the other values stay numbers. */
|
|
24
|
+
export function formatVicii<T extends { screenAddress: number; graphicsAddress: number }>(state: T): Omit<T, "screenAddress" | "graphicsAddress"> & { screenAddress: string; graphicsAddress: string } {
|
|
25
|
+
return { ...state, screenAddress: formatC64Address(state.screenAddress), graphicsAddress: formatC64Address(state.graphicsAddress) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A sprite with its data address as $xxxx; the other values stay numbers. */
|
|
29
|
+
export function formatSprite<T extends { dataAddress: number }>(sprite: T): Omit<T, "dataAddress"> & { dataAddress: string } {
|
|
30
|
+
return { ...sprite, dataAddress: formatC64Address(sprite.dataAddress) };
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
/** A byte as $xx. Throws RangeError outside 0-255. */
|
|
24
34
|
export function formatC64Byte(value: number): string {
|
|
25
35
|
if (!Number.isInteger(value) || value < 0 || value > 0xff) throw new RangeError(`C64 byte out of range: ${value}`);
|
|
@@ -37,8 +47,25 @@ export function prgRange(prg: Uint8Array): { start: number; end: number; body: U
|
|
|
37
47
|
return { start, end: start + prg.length - 3, body: prg.subarray(2) };
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
/**
|
|
41
|
-
export const
|
|
50
|
+
/** The tables of addresses: words, or words minus 1 for an RTS dispatch; a split table has all low bytes, then all high bytes. */
|
|
51
|
+
export const POINTER_TABLE_TYPES = ["addresses", "addresses-split", "rts-addresses", "rts-addresses-split"] as const;
|
|
52
|
+
export type PointerTableType = (typeof POINTER_TABLE_TYPES)[number];
|
|
53
|
+
|
|
54
|
+
/** The entries of a table of addresses in [start, end]: the address of each low and high byte, and the target that the entry names. */
|
|
55
|
+
export function pointerTableEntries(table: { start: number; end: number; type: PointerTableType }, byteAt: (address: number) => number): Array<{ low: number; high: number; target: number }> {
|
|
56
|
+
const length = table.end - table.start + 1;
|
|
57
|
+
const split = table.type === "addresses-split" || table.type === "rts-addresses-split";
|
|
58
|
+
const minus = table.type === "rts-addresses" || table.type === "rts-addresses-split" ? 1 : 0;
|
|
59
|
+
const count = Math.floor(length / 2);
|
|
60
|
+
return Array.from({ length: count }, (_, index) => {
|
|
61
|
+
const low = split ? table.start + index : table.start + 2 * index;
|
|
62
|
+
const high = split ? table.start + count + index : low + 1;
|
|
63
|
+
return { low, high, target: ((byteAt(low) | (byteAt(high) << 8)) + minus) & 0xffff };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** How 6502 code refers to an address. A pointer is code that builds the address from immediates (D65). */
|
|
68
|
+
export const REFERENCE_KINDS = ["call", "jump", "read", "write", "reference", "pointer"] as const;
|
|
42
69
|
export type ReferenceKind = (typeof REFERENCE_KINDS)[number];
|
|
43
70
|
|
|
44
71
|
/** A symbol or label name: assembler label syntax, at most 64 characters; a leading dot marks a local label. */
|
|
@@ -128,7 +155,7 @@ export function textToPetsciiName(text: string): Uint8Array {
|
|
|
128
155
|
/** BASIC refuses higher line numbers when a line is entered. */
|
|
129
156
|
const MAX_LINE_NUMBER = 63999;
|
|
130
157
|
|
|
131
|
-
const TOKEN = { data: 0x83, rem: 0x8f, sys: 0x9e, usr: 0xb7, plus: 0xaa, minus: 0xab, times: 0xac, divide: 0xad } as const;
|
|
158
|
+
const TOKEN = { data: 0x83, rem: 0x8f, sys: 0x9e, usr: 0xb7, plus: 0xaa, minus: 0xab, times: 0xac, divide: 0xad, pi: 0xff } as const;
|
|
132
159
|
const QUOTE = 0x22;
|
|
133
160
|
const COLON = 0x3a;
|
|
134
161
|
|
|
@@ -138,31 +165,58 @@ export interface TokenizedLine {
|
|
|
138
165
|
tokens: Uint8Array;
|
|
139
166
|
}
|
|
140
167
|
|
|
141
|
-
/**
|
|
142
|
-
|
|
168
|
+
/**
|
|
169
|
+
* A machine-code handoff in BASIC program text: SYS with a constant address, a
|
|
170
|
+
* SYS whose argument needs run-time values (with the argument as text), or a USR.
|
|
171
|
+
*/
|
|
172
|
+
export type BasicHandoff =
|
|
173
|
+
| { kind: "sys"; line: number; address: number }
|
|
174
|
+
| { kind: "sys"; line: number; computed: true; expression: string }
|
|
175
|
+
| { kind: "usr"; line: number; computed: true };
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* `basicEnd` is the first address that holds no BASIC; `textBytes` is the
|
|
179
|
+
* number of file bytes (load address included) before the end of the BASIC text.
|
|
180
|
+
*/
|
|
181
|
+
export type TokenizedProgram = { loadAddress: number; basicEnd: number; textBytes: number; lines: TokenizedLine[] } | { reason: string };
|
|
143
182
|
|
|
144
|
-
|
|
183
|
+
/** BASIC text cannot start below the screen: there are the zero page, the stack and the system pages. */
|
|
184
|
+
const MIN_BASIC_LOAD = 0x0400;
|
|
145
185
|
|
|
146
186
|
/**
|
|
147
187
|
* Splits a PRG into BASIC lines the way BASIC relinks a loaded program: each
|
|
148
188
|
* line ends at a zero byte, and a link whose high byte is zero ends the
|
|
149
|
-
* program.
|
|
189
|
+
* program. A real end marker is two zero bytes; when only the high byte is
|
|
190
|
+
* zero, the "marker" is the first machine code after the BASIC text (SYS
|
|
191
|
+
* lines that jump right behind themselves). The stored links are not read:
|
|
192
|
+
* BASIC relinks a loaded program, and a crack can store odd links to hide
|
|
193
|
+
* lines from LIST. A load address below $0400 is a memory dump (F-12), and so
|
|
194
|
+
* is a file whose bytes run past $ffff: a 64 KiB dump from $0000 always does
|
|
195
|
+
* (b2/01, R3-02).
|
|
150
196
|
*/
|
|
151
197
|
export function parseTokenized(program: Uint8Array): TokenizedProgram {
|
|
152
198
|
if (program.length < 4) return { reason: "The file is too short for a BASIC program." };
|
|
153
199
|
const loadAddress = program[0]! | (program[1]! << 8);
|
|
200
|
+
if (loadAddress < MIN_BASIC_LOAD) {
|
|
201
|
+
return { reason: `The load address ${formatC64Address(loadAddress)} is below $0400, in the zero page, the stack or the system pages. The file is a memory dump or machine code, not a BASIC program.` };
|
|
202
|
+
}
|
|
203
|
+
if (loadAddress + program.length - 2 > 0x10000) {
|
|
204
|
+
return { reason: `The file holds ${program.length - 2} bytes from ${formatC64Address(loadAddress)}, which runs past $ffff. The file is a memory dump, not a BASIC program.` };
|
|
205
|
+
}
|
|
154
206
|
const lines: TokenizedLine[] = [];
|
|
155
207
|
let at = 2;
|
|
208
|
+
const ended = (basicEnd: number): TokenizedProgram => ({ loadAddress, basicEnd, textBytes: at, lines });
|
|
156
209
|
// File offset k (from 2 on) loads at loadAddress + k - 2.
|
|
157
210
|
for (;;) {
|
|
158
211
|
// A file that ends right after a line leaves the end marker to the zeros after it in memory;
|
|
159
212
|
// basicEnd is the address after that marker, as when the file holds it.
|
|
160
|
-
if (at === program.length && lines.length > 0) return
|
|
213
|
+
if (at === program.length && lines.length > 0) return ended(loadAddress + at);
|
|
161
214
|
if (at + 1 >= program.length) return { reason: "The BASIC program has no end marker." };
|
|
162
215
|
if (program[at + 1] === 0) {
|
|
163
216
|
// An end marker before any line, with more bytes after it, is how machine code at a non-BASIC address often starts.
|
|
164
217
|
if (lines.length === 0 && at + 2 < program.length) return { reason: "The file holds no BASIC line before other bytes, so it is no BASIC program." };
|
|
165
|
-
|
|
218
|
+
const marker = program[at] === 0;
|
|
219
|
+
return ended(loadAddress + (marker ? at : at - 2));
|
|
166
220
|
}
|
|
167
221
|
if (at + 4 > program.length) return { reason: "The last BASIC line is cut off." };
|
|
168
222
|
const number = program[at + 2]! | (program[at + 3]! << 8);
|
|
@@ -174,8 +228,43 @@ export function parseTokenized(program: Uint8Array): TokenizedProgram {
|
|
|
174
228
|
}
|
|
175
229
|
}
|
|
176
230
|
|
|
231
|
+
/** BASIC V2 keywords for the tokens $80 to $cb, in token order. */
|
|
232
|
+
const KEYWORDS = [
|
|
233
|
+
"END", "FOR", "NEXT", "DATA", "INPUT#", "INPUT", "DIM", "READ", "LET", "GOTO", "RUN", "IF", "RESTORE", "GOSUB", "RETURN", "REM",
|
|
234
|
+
"STOP", "ON", "WAIT", "LOAD", "SAVE", "VERIFY", "DEF", "POKE", "PRINT#", "PRINT", "CONT", "LIST", "CLR", "CMD", "SYS", "OPEN",
|
|
235
|
+
"CLOSE", "GET", "NEW", "TAB(", "TO", "FN", "SPC(", "THEN", "NOT", "STEP", "+", "-", "*", "/", "^", "AND",
|
|
236
|
+
"OR", ">", "=", "<", "SGN", "INT", "ABS", "USR", "FRE", "POS", "SQR", "RND", "LOG", "EXP", "COS", "SIN",
|
|
237
|
+
"TAN", "ATN", "PEEK", "LEN", "STR$", "VAL", "ASC", "CHR$", "LEFT$", "RIGHT$", "MID$", "GO",
|
|
238
|
+
] as const;
|
|
239
|
+
|
|
177
240
|
/**
|
|
178
|
-
*
|
|
241
|
+
* One BASIC line as text: keywords for tokens, text in quotes and after REM
|
|
242
|
+
* as it is, and {$xx} for bytes that are no character. For evidence, not a listing.
|
|
243
|
+
*/
|
|
244
|
+
export function basicLineText(line: TokenizedLine): string {
|
|
245
|
+
return `${line.number} ${basicText(line.tokens)}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Tokenized BASIC bytes as text, as for basicLineText; pi is π. */
|
|
249
|
+
function basicText(tokens: Uint8Array): string {
|
|
250
|
+
let text = "";
|
|
251
|
+
let literal = false;
|
|
252
|
+
for (const byte of tokens) {
|
|
253
|
+
if (byte === QUOTE) literal = !literal;
|
|
254
|
+
if (!literal && byte >= 0x80 && byte < 0x80 + KEYWORDS.length) {
|
|
255
|
+
text += KEYWORDS[byte - 0x80];
|
|
256
|
+
if (byte === TOKEN.rem) literal = true;
|
|
257
|
+
} else if (!literal && byte === TOKEN.pi) {
|
|
258
|
+
text += "π";
|
|
259
|
+
} else {
|
|
260
|
+
text += petsciiNameToText(Uint8Array.of(byte));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return text;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Evaluates a SYS argument that holds only numbers, π, + - * / and brackets.
|
|
179
268
|
* Returns undefined for anything that needs run-time values.
|
|
180
269
|
*/
|
|
181
270
|
export function constantExpression(bytes: Uint8Array): number | undefined {
|
|
@@ -214,6 +303,10 @@ export function constantExpression(bytes: Uint8Array): number | undefined {
|
|
|
214
303
|
if (value === undefined || tokens[at++] !== 0x29) return undefined;
|
|
215
304
|
return value;
|
|
216
305
|
}
|
|
306
|
+
if (peek() === TOKEN.pi) {
|
|
307
|
+
at++;
|
|
308
|
+
return Math.PI;
|
|
309
|
+
}
|
|
217
310
|
return number();
|
|
218
311
|
};
|
|
219
312
|
const product = (): number | undefined => {
|
|
@@ -269,8 +362,9 @@ export function findHandoffs(lines: TokenizedLine[]): BasicHandoff[] {
|
|
|
269
362
|
// The argument ends at the statement end or at a comma (extra SYS parameters).
|
|
270
363
|
let end = at + 1;
|
|
271
364
|
while (end < tokens.length && tokens[end] !== COLON && tokens[end] !== 0x2c) end++;
|
|
272
|
-
const
|
|
273
|
-
|
|
365
|
+
const argument = tokens.subarray(at + 1, end);
|
|
366
|
+
const value = constantExpression(argument);
|
|
367
|
+
if (value === undefined) handoffs.push({ kind: "sys", line: number, computed: true, expression: basicText(argument).trim() });
|
|
274
368
|
// BASIC drops the fraction; outside 0-65535 SYS stops with ILLEGAL QUANTITY and jumps nowhere.
|
|
275
369
|
else if (Math.trunc(value) >= 0 && Math.trunc(value) <= 0xffff) handoffs.push({ kind: "sys", line: number, address: Math.trunc(value) });
|
|
276
370
|
at = end - 1;
|
|
@@ -279,3 +373,137 @@ export function findHandoffs(lines: TokenizedLine[]): BasicHandoff[] {
|
|
|
279
373
|
}
|
|
280
374
|
return handoffs;
|
|
281
375
|
}
|
|
376
|
+
|
|
377
|
+
// Execution coverage (D40): the byte format of a coverage map, and the 6502
|
|
378
|
+
// instruction lengths that turn the instruction starts in it into code ranges.
|
|
379
|
+
// VICE marks only the opcode byte of an executed instruction as executed.
|
|
380
|
+
|
|
381
|
+
/** One byte per address: the access flags as VICE reports them, RAM, ROM and I/O apart. */
|
|
382
|
+
export const COVERAGE = {
|
|
383
|
+
ramRead: 1 << 0,
|
|
384
|
+
ramWrite: 1 << 1,
|
|
385
|
+
ramExecute: 1 << 2,
|
|
386
|
+
romRead: 1 << 3,
|
|
387
|
+
romExecute: 1 << 4,
|
|
388
|
+
ioRead: 1 << 5,
|
|
389
|
+
ioWrite: 1 << 6,
|
|
390
|
+
ioExecute: 1 << 7,
|
|
391
|
+
} as const;
|
|
392
|
+
|
|
393
|
+
export const EXECUTE = COVERAGE.ramExecute | COVERAGE.romExecute | COVERAGE.ioExecute;
|
|
394
|
+
export const READ = COVERAGE.ramRead | COVERAGE.romRead | COVERAGE.ioRead;
|
|
395
|
+
export const WRITE = COVERAGE.ramWrite | COVERAGE.ioWrite;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The length in bytes of the NMOS 6502/6510 instruction with this opcode, for
|
|
399
|
+
* all 256 opcodes with the undocumented ones. The opcode is aaabbbcc: bbb
|
|
400
|
+
* selects the addressing mode within a group cc. JAM (KIL) and BRK count one byte.
|
|
401
|
+
*/
|
|
402
|
+
export function instructionLength(opcode: number): 1 | 2 | 3 {
|
|
403
|
+
const group = opcode & 0b11;
|
|
404
|
+
const mode = (opcode >> 2) & 0b111;
|
|
405
|
+
if (group === 0b00) {
|
|
406
|
+
if (mode === 0b000) return opcode === 0x20 ? 3 : opcode === 0x00 || opcode === 0x40 || opcode === 0x60 ? 1 : 2;
|
|
407
|
+
return ([2, 2, 1, 3, 2, 2, 1, 3] as const)[mode]!;
|
|
408
|
+
}
|
|
409
|
+
if (group === 0b10) {
|
|
410
|
+
if (mode === 0b000) return opcode === 0x02 || opcode === 0x22 || opcode === 0x42 || opcode === 0x62 ? 1 : 2;
|
|
411
|
+
return ([2, 2, 1, 3, 1, 2, 1, 3] as const)[mode]!;
|
|
412
|
+
}
|
|
413
|
+
// Groups 01 and 11 share their addressing modes.
|
|
414
|
+
return ([2, 2, 2, 3, 2, 2, 3, 3] as const)[mode]!;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface CodeRange {
|
|
418
|
+
start: number;
|
|
419
|
+
end: number;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The executed code in RAM as ranges: each RAM instruction start in `map`
|
|
424
|
+
* with its operand bytes, by the opcode in `memory` (64 KiB from $0000).
|
|
425
|
+
* `map` starts at `base`. Adjacent and overlapping instructions merge.
|
|
426
|
+
*/
|
|
427
|
+
export function executedRamCode(map: Uint8Array, base: number, memory: Uint8Array): CodeRange[] {
|
|
428
|
+
const ranges: CodeRange[] = [];
|
|
429
|
+
for (let offset = 0; offset < map.length; offset++) {
|
|
430
|
+
if ((map[offset]! & COVERAGE.ramExecute) === 0) continue;
|
|
431
|
+
const start = base + offset;
|
|
432
|
+
const end = Math.min(0xffff, start + instructionLength(memory[start]!) - 1);
|
|
433
|
+
const last = ranges.at(-1);
|
|
434
|
+
if (last !== undefined && start <= last.end + 1) last.end = Math.max(last.end, end);
|
|
435
|
+
else ranges.push({ start, end });
|
|
436
|
+
}
|
|
437
|
+
return ranges;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Coverage flows (D44, D74): the seeds that a coverage map gives a static analysis.
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Instructions after which the next instruction starts a flow for the seeds: the
|
|
444
|
+
* ones that do not fall through, and JSR, because an analyzer that takes the
|
|
445
|
+
* called routine for one that does not return stops there (the Bruce Lee run).
|
|
446
|
+
*/
|
|
447
|
+
const FLOW_ENDS = new Set([0x00, 0x20, 0x40, 0x4c, 0x60, 0x6c]);
|
|
448
|
+
/**
|
|
449
|
+
* The executed instruction starts in RAM that begin a flow: the first after
|
|
450
|
+
* a gap, or the first after an instruction that does not fall through or is a
|
|
451
|
+
* JSR. The analyzers follow the rest of a flow themselves.
|
|
452
|
+
*/
|
|
453
|
+
export function coverageEntries(map: Uint8Array, image: (address: number) => number | undefined): number[] {
|
|
454
|
+
const entries: number[] = [];
|
|
455
|
+
let falls: number | undefined;
|
|
456
|
+
for (let address = 0; address < 0x10000; address++) {
|
|
457
|
+
if ((map[address]! & COVERAGE.ramExecute) === 0) continue;
|
|
458
|
+
const opcode = image(address);
|
|
459
|
+
if (opcode === undefined) continue;
|
|
460
|
+
if (falls !== address) entries.push(address);
|
|
461
|
+
falls = FLOW_ENDS.has(opcode) ? undefined : address + instructionLength(opcode);
|
|
462
|
+
}
|
|
463
|
+
return entries;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Every executed instruction start in RAM inside the image: VICE marks the opcode byte of an instruction as executed, not its operand. */
|
|
467
|
+
export function executedStarts(map: Uint8Array, image: (address: number) => number | undefined): number[] {
|
|
468
|
+
const starts: number[] = [];
|
|
469
|
+
for (let address = 0; address < 0x10000; address++) if ((map[address]! & COVERAGE.ramExecute) !== 0 && image(address) !== undefined) starts.push(address);
|
|
470
|
+
return starts;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const BRANCHES = new Set([0x10, 0x30, 0x50, 0x70, 0x90, 0xb0, 0xd0, 0xf0]);
|
|
474
|
+
|
|
475
|
+
/** Instructions that never fall through to the next one. */
|
|
476
|
+
const NO_FALL = new Set([0x00, 0x40, 0x4c, 0x60, 0x6c]);
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* The coverage flow starts split for Ghidra (D74). A start is a code start, a
|
|
480
|
+
* place in a routine, when it follows an executed JSR, or when an executed
|
|
481
|
+
* branch or JMP from outside its own flow reaches it (the loop of a routine
|
|
482
|
+
* that jumps back to its own start does not count). Any other start, for
|
|
483
|
+
* example a vector or table target, is a routine entry.
|
|
484
|
+
*/
|
|
485
|
+
export function coverageSeedKinds(map: Uint8Array, image: (address: number) => number | undefined): { routines: number[]; codeStarts: number[] } {
|
|
486
|
+
const entries = coverageEntries(map, image);
|
|
487
|
+
const flowOf = new Map<number, number>();
|
|
488
|
+
for (const entry of entries) {
|
|
489
|
+
// A flow runs over the executed instructions that fall through, also over a JSR.
|
|
490
|
+
for (let at = entry; ; ) {
|
|
491
|
+
const opcode = image(at);
|
|
492
|
+
if (opcode === undefined || (map[at]! & COVERAGE.ramExecute) === 0 || flowOf.has(at)) break;
|
|
493
|
+
flowOf.set(at, entry);
|
|
494
|
+
if (NO_FALL.has(opcode)) break;
|
|
495
|
+
at += instructionLength(opcode);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const inside = new Set<number>();
|
|
499
|
+
for (const address of executedStarts(map, image)) {
|
|
500
|
+
const opcode = image(address)!;
|
|
501
|
+
const low = image(address + 1);
|
|
502
|
+
const high = image(address + 2);
|
|
503
|
+
const reach = (target: number) => flowOf.get(address) !== target && inside.add(target);
|
|
504
|
+
if (BRANCHES.has(opcode) && low !== undefined) reach((address + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff);
|
|
505
|
+
if (opcode === 0x4c && low !== undefined && high !== undefined) reach(low | (high << 8));
|
|
506
|
+
if (opcode === 0x20) inside.add(address + 3);
|
|
507
|
+
}
|
|
508
|
+
return { routines: entries.filter((entry) => !inside.has(entry)), codeStarts: entries.filter((entry) => inside.has(entry)) };
|
|
509
|
+
}
|
package/src/cli/cleanup.ts
CHANGED
|
@@ -29,7 +29,7 @@ interface Manifest {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** The directory that the manifest and relative item paths belong to. */
|
|
32
|
-
function scopeRoot(scope: Scope, cwd = process.cwd()): string {
|
|
32
|
+
export function scopeRoot(scope: Scope, cwd = process.cwd()): string {
|
|
33
33
|
return scope === "global" ? homedir() : cwd;
|
|
34
34
|
}
|
|
35
35
|
|
package/src/cli/main.ts
CHANGED
|
@@ -7,14 +7,14 @@ import { dirname, join } from "node:path";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
|
|
10
|
-
import { PACKAGE } from "
|
|
10
|
+
import { PACKAGE } from "../package.ts";
|
|
11
11
|
import { localToolContext } from "../native/local.ts";
|
|
12
12
|
import { localToolStatus } from "../native/status.ts";
|
|
13
13
|
import { WireFailure } from "../protocol/messages.ts";
|
|
14
14
|
import { startTrace } from "../trace.ts";
|
|
15
15
|
import { runApSdk } from "./ap-sdk.ts";
|
|
16
16
|
import { installedItems, parseTargets, tidyAfterUninstall, undoWindsurfProjectMcp, windsurfBefore, withoutWindsurfMcp, type Scope } from "./cleanup.ts";
|
|
17
|
-
import { hostStatusLines, toolLines } from "./status.ts";
|
|
17
|
+
import { hostStatusLines, installedLines, toolLines } from "./status.ts";
|
|
18
18
|
|
|
19
19
|
const HELP = `c64-re-tools: Commodore 64 reverse engineering and development
|
|
20
20
|
|
|
@@ -27,10 +27,12 @@ Usage:
|
|
|
27
27
|
|
|
28
28
|
Harnesses: claude, codex, pi, opencode, gemini, copilot, cursor, windsurf
|
|
29
29
|
(comma-separated; default: all). Without --global the files go into the
|
|
30
|
-
current
|
|
30
|
+
current folder; with --global into your home directory.
|
|
31
31
|
|
|
32
|
-
Run it
|
|
33
|
-
|
|
32
|
+
Run it once at the root of the repository:
|
|
33
|
+
npx -y ${PACKAGE.name}@${PACKAGE.tag} install. Each project folder below
|
|
34
|
+
the root uses that install; a project is a folder with .c64-re-tools/. The
|
|
35
|
+
VICE MCP declaration starts the latest c64-re-tools-mcp through npx. The Host
|
|
34
36
|
Runtime runs on the machine with VICE: start it there with
|
|
35
37
|
npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host.
|
|
36
38
|
It runs VICE and the tools that come with it (c1541, petcat). The skill
|
|
@@ -50,10 +52,14 @@ async function status(): Promise<number> {
|
|
|
50
52
|
console.log(`c64-re-tools ${PACKAGE.version}`);
|
|
51
53
|
console.log(`Package: ${join(here, "..", "..")}`);
|
|
52
54
|
console.log(`Node: ${process.version} (${process.execPath})`);
|
|
55
|
+
for (const line of installedLines()) console.log(line);
|
|
53
56
|
// The skill scripts run these tools here, on this machine.
|
|
54
|
-
|
|
55
|
-
for (const line of
|
|
56
|
-
|
|
57
|
+
const local = await localToolStatus(localToolContext());
|
|
58
|
+
for (const line of toolLines("Tools here:", local)) console.log(line);
|
|
59
|
+
const host = await hostStatusLines();
|
|
60
|
+
for (const line of host.lines) console.log(line);
|
|
61
|
+
// A script or a CI check can tell a ready setup from a broken one.
|
|
62
|
+
return host.ready && local.every((tool) => tool.runs) ? 0 : 1;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
async function main(argv: string[]): Promise<number> {
|
package/src/cli/status.ts
CHANGED
|
@@ -1,34 +1,128 @@
|
|
|
1
|
-
// The status report of the CLI: the
|
|
2
|
-
// Runtime with its tools.
|
|
1
|
+
// The status report of the CLI: the installed skills and MCP declarations,
|
|
2
|
+
// the tools on this machine and the Host Runtime with its tools.
|
|
3
3
|
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { installedItems, scopeRoot, type InstallItem, type Scope } from "./cleanup.ts";
|
|
4
8
|
import { hostTools, type ToolCallOptions } from "../host-client/tools.ts";
|
|
5
|
-
import
|
|
9
|
+
import { viceVersionOf } from "../native/discover.ts";
|
|
10
|
+
import { PACKAGE } from "../package.ts";
|
|
11
|
+
import { STOP_AND_ASK } from "../protocol/messages.ts";
|
|
12
|
+
import type { HostInfo, ToolStatus } from "../protocol/tools.ts";
|
|
6
13
|
|
|
7
|
-
/** One line per tool under `heading`: its version, or why it does not run. */
|
|
14
|
+
/** One line per tool under `heading`: its version, or why it does not run; then each known flaw of it. */
|
|
8
15
|
export function toolLines(heading: string, tools: readonly ToolStatus[]): string[] {
|
|
9
16
|
return [
|
|
10
17
|
heading,
|
|
11
|
-
...tools.
|
|
18
|
+
...tools.flatMap((tool) => [
|
|
12
19
|
tool.runs
|
|
13
20
|
? ` ${tool.name}: ${tool.version} (${tool.path})`
|
|
14
21
|
: tool.found
|
|
15
22
|
? ` ${tool.name}: found at ${tool.path} but it does not run. ${tool.problem}`
|
|
16
23
|
: ` ${tool.name}: missing. ${tool.problem}`,
|
|
17
|
-
),
|
|
24
|
+
...(tool.flaws ?? []).map((flaw) => ` Known flaw: ${flaw}`),
|
|
25
|
+
]),
|
|
18
26
|
];
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
/**
|
|
22
30
|
* Whether the Host Runtime answers, and the tools it has. VICE and its tools
|
|
23
31
|
* live on the host; it finds and runs each one. One request shows both that
|
|
24
|
-
* the host answers and what it has.
|
|
32
|
+
* the host answers and what it has. `ready` is true when the host answers and
|
|
33
|
+
* every tool on it runs.
|
|
25
34
|
*/
|
|
26
|
-
export async function hostStatusLines(options: ToolCallOptions = {}): Promise<string[]> {
|
|
35
|
+
export async function hostStatusLines(options: ToolCallOptions = {}): Promise<{ lines: string[]; ready: boolean }> {
|
|
36
|
+
let host: HostInfo;
|
|
27
37
|
let tools: ToolStatus[];
|
|
28
38
|
try {
|
|
29
|
-
tools = await hostTools(options);
|
|
39
|
+
({ host, tools } = await hostTools(options));
|
|
30
40
|
} catch (error) {
|
|
31
|
-
|
|
41
|
+
// status is read by a person: the instruction for an agent (D37) stays out.
|
|
42
|
+
return { lines: [`Host Runtime: not reachable. ${(error as Error).message.replace(` ${STOP_AND_ASK}`, "")}`], ready: false };
|
|
43
|
+
}
|
|
44
|
+
const mixed = mixedViceBuilds(tools);
|
|
45
|
+
return {
|
|
46
|
+
lines: [
|
|
47
|
+
`Host Runtime: reachable, c64-re-tools ${host.version} on ${host.platform} ${host.release} (${host.arch})`,
|
|
48
|
+
...(host.version === PACKAGE.version ? [] : [` Warning: the Host Runtime is c64-re-tools ${host.version} and this is ${PACKAGE.version}. Start the host again so that both are the same version.`]),
|
|
49
|
+
...toolLines("Tools on the host:", tools),
|
|
50
|
+
...(mixed === undefined ? [] : [` Warning: ${mixed}`]),
|
|
51
|
+
],
|
|
52
|
+
ready: tools.every((tool) => tool.runs),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A warning when x64sc, c1541 and petcat that run come from different VICE versions or builds. */
|
|
57
|
+
export function mixedViceBuilds(tools: readonly ToolStatus[]): string | undefined {
|
|
58
|
+
const running = tools.filter((tool) => tool.runs && tool.version !== undefined);
|
|
59
|
+
const builds = running.map((tool) => {
|
|
60
|
+
const version = viceVersionOf(tool.version!);
|
|
61
|
+
return version === undefined ? tool.version! : `VICE ${version.major}.${version.minor}${version.revision === undefined ? "" : ` SVN r${version.revision}`}`;
|
|
62
|
+
});
|
|
63
|
+
if (new Set(builds).size <= 1) return undefined;
|
|
64
|
+
return `the VICE tools come from different VICE builds: ${running.map((tool) => `${tool.version} (${tool.path})`).join(", ")}. Put x64sc, c1541 and petcat of one VICE first on PATH, or set C64RT_VICE, C64RT_C1541 and C64RT_PETCAT.`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The skills and MCP declarations that install wrote for this project and
|
|
69
|
+
* globally (b's R2-01): the c64-re-tools version of each harness's skills,
|
|
70
|
+
* from their package.json, and the command that starts the MCP server. A
|
|
71
|
+
* warning names skills of another version than this one.
|
|
72
|
+
*/
|
|
73
|
+
export function installedLines(cwd = process.cwd()): string[] {
|
|
74
|
+
const lines: string[] = [];
|
|
75
|
+
for (const scope of ["project", "global"] as const satisfies readonly Scope[]) {
|
|
76
|
+
const items = installedItems(scope, undefined, cwd);
|
|
77
|
+
if (items.length === 0) continue;
|
|
78
|
+
const root = scopeRoot(scope, cwd);
|
|
79
|
+
lines.push(`Installed ${scope === "project" ? `in this project (${root})` : "globally"}:`);
|
|
80
|
+
for (const harness of [...new Set(items.map((item) => item.harness))]) {
|
|
81
|
+
const skills = items.filter((item) => item.harness === harness && item.kind === "skill");
|
|
82
|
+
const versions = [...new Set(skills.map((item) => skillVersion(item, root)).filter((version) => version !== undefined))];
|
|
83
|
+
if (skills.length > 0) {
|
|
84
|
+
lines.push(` ${harness}: ${skills.length} skills, c64-re-tools ${versions.length === 0 ? "of an unknown version" : versions.join(" and ")}`);
|
|
85
|
+
const other = versions.filter((version) => version !== PACKAGE.version);
|
|
86
|
+
if (other.length > 0) lines.push(` Warning: the ${harness} skills are c64-re-tools ${other.join(" and ")} and this is ${PACKAGE.version}. Run npx -y ${PACKAGE.name}@${PACKAGE.tag} update${scope === "global" ? " --global" : ""} to install this version.`);
|
|
87
|
+
}
|
|
88
|
+
for (const item of items.filter((candidate) => candidate.harness === harness && candidate.kind === "mcp")) {
|
|
89
|
+
lines.push(` ${harness} MCP server: ${mcpCommand(item, root) ?? `declared in ${item.files.join(", ")}`}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (lines.length === 0) return ["Installed: nothing in this project or globally."];
|
|
94
|
+
lines.push(" The MCP server gives its own version in the result of c64_status.");
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function itemPath(file: string, root: string): string {
|
|
99
|
+
return isAbsolute(file) ? file : join(root, file);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The version in the package.json of an installed skill; a skill without scripts has none. */
|
|
103
|
+
function skillVersion(item: InstallItem, root: string): string | undefined {
|
|
104
|
+
const file = item.files.find((candidate) => candidate.endsWith("/package.json") || candidate.endsWith("\\package.json"));
|
|
105
|
+
if (file === undefined) return undefined;
|
|
106
|
+
try {
|
|
107
|
+
const { version } = JSON.parse(readFileSync(itemPath(file, root), "utf8")) as { version?: unknown };
|
|
108
|
+
return typeof version === "string" ? version : undefined;
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The command of the MCP server in a JSON configuration file, or undefined for another format. */
|
|
115
|
+
function mcpCommand(item: InstallItem, root: string): string | undefined {
|
|
116
|
+
const key = item.detail?.mergeKey;
|
|
117
|
+
const [file] = item.files;
|
|
118
|
+
if (key === undefined || file === undefined) return undefined;
|
|
119
|
+
try {
|
|
120
|
+
const servers = (JSON.parse(readFileSync(itemPath(file, root), "utf8")) as Record<string, unknown>)[key] as Record<string, { command?: unknown; args?: unknown }> | undefined;
|
|
121
|
+
const server = servers?.[item.name];
|
|
122
|
+
if (typeof server?.command !== "string") return undefined;
|
|
123
|
+
const args = Array.isArray(server.args) ? server.args.filter((arg): arg is string => typeof arg === "string") : [];
|
|
124
|
+
return [server.command, ...args].join(" ");
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
32
127
|
}
|
|
33
|
-
return ["Host Runtime: reachable", ...toolLines("Tools on the host:", tools)];
|
|
34
128
|
}
|