@henols/c64-re-tools 1.0.0 → 2.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +190 -37
- package/distribution/package.ts +36 -0
- package/distribution/plugin.ts +120 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +73 -0
- package/skills/c64-assembler/scripts/assemble.ts +103 -0
- package/skills/c64-basic/SKILL.md +67 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +79 -0
- package/skills/c64-disk/scripts/disk.ts +65 -0
- package/skills/c64-emulator/SKILL.md +124 -0
- package/skills/c64-knowledge/SKILL.md +102 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
- package/skills/c64-memory-map/SKILL.md +76 -0
- package/skills/c64-memory-map/scripts/memmap.ts +81 -0
- package/skills/c64-memory-map/scripts/platform.ts +424 -0
- package/skills/c64-provenance/SKILL.md +76 -0
- package/skills/c64-provenance/scripts/compare.ts +92 -0
- package/skills/c64-provenance/scripts/provenance.ts +83 -0
- package/skills/c64-reverse-engineering/SKILL.md +90 -0
- package/skills/c64-static-analysis/SKILL.md +96 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
- package/skills/c64-static-analysis/scripts/findings.ts +73 -0
- package/skills/c64-testing/SKILL.md +79 -0
- package/skills/c64-testing/references/scenario-format.md +76 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/scenario.ts +473 -0
- package/skills/c64-testing/scripts/test.ts +55 -0
- package/skills/c64-unpacker/SKILL.md +82 -0
- package/skills/c64-unpacker/scripts/evidence.ts +135 -0
- package/skills/c64-unpacker/scripts/unpack.ts +216 -0
- package/src/c64.ts +281 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +122 -0
- package/src/cli/status.ts +34 -0
- package/src/host/main.ts +121 -0
- package/src/host/server.ts +409 -0
- package/src/host/tools/c1541.ts +255 -0
- package/src/host/tools/index.ts +31 -0
- package/src/host/tools/petcat.ts +70 -0
- package/src/host/tools/status.ts +30 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +761 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +108 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +402 -0
- package/src/host/vice/screen.ts +183 -0
- package/src/host/vice/session.ts +1316 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +100 -0
- package/src/host-client/vice-session.ts +254 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +128 -0
- package/src/knowledge/import.ts +344 -0
- package/src/knowledge/read.ts +191 -0
- package/src/knowledge/schema.ts +140 -0
- package/src/knowledge/write.ts +348 -0
- package/src/mcp/main.ts +109 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/tools/debug.ts +237 -0
- package/src/mcp/tools/execution.ts +116 -0
- package/src/mcp/tools/input.ts +69 -0
- package/src/mcp/tools/machine.ts +97 -0
- package/src/mcp/tools/media.ts +106 -0
- package/src/mcp/tools/memory.ts +203 -0
- package/src/mcp/tools/video.ts +260 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +141 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +157 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +242 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +101 -0
- package/src/native/types.ts +228 -0
- package/src/native/watchdog.ts +78 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +252 -0
- package/src/protocol/tools.ts +247 -0
- package/src/protocol/vice.ts +1235 -0
- package/src/script.ts +59 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +162 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
package/src/script.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The one error envelope for the skill scripts and the CLI: a script prints one JSON line,
|
|
2
|
+
// a refusal prints {"error":{code,message}} with exit status 1, and a usage error prints the
|
|
3
|
+
// usage text as an invalid-input error with exit status 2.
|
|
4
|
+
|
|
5
|
+
import { WireFailure } from "./protocol/messages.ts";
|
|
6
|
+
import { elapsedMs, startTrace, trace, type TraceFields } from "./trace.ts";
|
|
7
|
+
|
|
8
|
+
/** A wrong command line. The message names the problem; an empty message asks for the usage text alone. */
|
|
9
|
+
export class UsageError extends Error {}
|
|
10
|
+
|
|
11
|
+
/** A refusal class: an Error with a stable `code` (WireFailure, KnowledgeError, ...). */
|
|
12
|
+
export type RefusalClass = abstract new (...args: never[]) => Error & { code: string };
|
|
13
|
+
|
|
14
|
+
/** True for an error that node:util parseArgs throws for a wrong command line. */
|
|
15
|
+
function isParseArgsError(error: unknown): error is Error {
|
|
16
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
17
|
+
return error instanceof Error && typeof code === "string" && code.startsWith("ERR_PARSE_ARGS_");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printLine(value: unknown): void {
|
|
21
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Runs a script body. Prints its result as one JSON line when it returns a value (undefined prints nothing).
|
|
26
|
+
* A refusal (WireFailure always, plus `refusals`) prints {"error":{code,message}} with exit status 1.
|
|
27
|
+
* A UsageError or a node:util parseArgs error (code ERR_PARSE_ARGS_*) prints {"error":{"code":"invalid-input",
|
|
28
|
+
* "message": message === "" ? usage : message + "\n\n" + usage}} with exit status 2. Anything else is rethrown.
|
|
29
|
+
* The body may set process.exitCode itself for an extra exit status; a value it returns is printed as usual.
|
|
30
|
+
*/
|
|
31
|
+
export async function runScript(body: () => unknown, options: { usage: string; refusals?: readonly RefusalClass[] }): Promise<void> {
|
|
32
|
+
// The trace of the script, when C64RT_TRACE is set; its start event carries the script and its arguments.
|
|
33
|
+
startTrace("script", { warn: (line) => process.stderr.write(`${line}\n`) });
|
|
34
|
+
const started = performance.now();
|
|
35
|
+
const end = (fields: TraceFields) => trace().event("script.end", { ms: elapsedMs(started), status: process.exitCode ?? 0, ...fields });
|
|
36
|
+
let result: unknown;
|
|
37
|
+
try {
|
|
38
|
+
result = await body();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if ([WireFailure, ...(options.refusals ?? [])].some((refusal) => error instanceof refusal)) {
|
|
41
|
+
const { code, message } = error as Error & { code: string };
|
|
42
|
+
printLine({ error: { code, message } });
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
end({ error: { code, message } });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (error instanceof UsageError || isParseArgsError(error)) {
|
|
48
|
+
const { message } = error;
|
|
49
|
+
printLine({ error: { code: "invalid-input", message: message === "" ? options.usage : `${message}\n\n${options.usage}` } });
|
|
50
|
+
process.exitCode = 2;
|
|
51
|
+
end({ error: { code: "invalid-input", message } });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
end({ status: 1, error });
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
if (result !== undefined) printLine(result);
|
|
58
|
+
end({ result });
|
|
59
|
+
}
|
package/src/time.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Every date and time the code writes is ISO 8601 at the fixed CET offset
|
|
2
|
+
// +01:00, also in summer: no timestamp repeats when the clocks go back, and
|
|
3
|
+
// the text of two timestamps sorts in time order.
|
|
4
|
+
|
|
5
|
+
const CET_OFFSET_MS = 60 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
/** `time` as `2026-10-09T14:03:21.123+01:00`. */
|
|
8
|
+
export function isoCet(time: Date = new Date()): string {
|
|
9
|
+
return `${new Date(time.getTime() + CET_OFFSET_MS).toISOString().slice(0, -1)}+01:00`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `time` to the second, for a file name: `2026-10-09T14-03-21`. A colon is not allowed in a name on Windows. */
|
|
13
|
+
export function fileStampCet(time: Date = new Date()): string {
|
|
14
|
+
return isoCet(time).slice(0, 19).replace(/:/g, "-");
|
|
15
|
+
}
|
package/src/trace.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// An opt-in trace for the person who debugs a run. With C64RT_TRACE=<directory>
|
|
2
|
+
// in the environment, each process appends its events as JSON lines to a file
|
|
3
|
+
// of its own in that directory; unset, every call does nothing. The trace is
|
|
4
|
+
// for people: nothing of it reaches the agent, and no result depends on it.
|
|
5
|
+
|
|
6
|
+
import { appendFileSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { fileStampCet, isoCet } from "./time.ts";
|
|
10
|
+
|
|
11
|
+
export const TRACE_VARIABLE = "C64RT_TRACE";
|
|
12
|
+
|
|
13
|
+
/** The programs that write a trace, each to a file named after it. */
|
|
14
|
+
export type TraceKind = "host" | "mcp" | "script" | "cli";
|
|
15
|
+
|
|
16
|
+
/** The fields of one event. `t`, `kind` and `event` are set by the trace; a field that is undefined is left out. */
|
|
17
|
+
export type TraceFields = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
const TEXT_LIMIT = 2_000;
|
|
20
|
+
const ITEM_LIMIT = 32;
|
|
21
|
+
const DEPTH_LIMIT = 4;
|
|
22
|
+
const RESERVED = new Set(["t", "kind", "event"]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A value as the trace records it: bytes become their count, text and arrays
|
|
26
|
+
* beyond a limit are cut and say so, an Error keeps its name, message and
|
|
27
|
+
* code, and an object deeper than four levels ends in "…".
|
|
28
|
+
*/
|
|
29
|
+
export function summarize(value: unknown, depth = 0): unknown {
|
|
30
|
+
if (value instanceof Uint8Array) return { bytes: value.length };
|
|
31
|
+
if (typeof value === "string") return value.length > TEXT_LIMIT ? `${value.slice(0, TEXT_LIMIT)}… (${value.length} characters)` : value;
|
|
32
|
+
if (typeof value === "bigint") return value.toString();
|
|
33
|
+
if (value instanceof Error) {
|
|
34
|
+
const code = (value as { code?: unknown }).code;
|
|
35
|
+
return { name: value.name, message: value.message, ...(typeof code === "string" ? { code } : {}) };
|
|
36
|
+
}
|
|
37
|
+
if (typeof value !== "object" || value === null) return value;
|
|
38
|
+
if (depth >= DEPTH_LIMIT) return "…";
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
const items = value.slice(0, ITEM_LIMIT).map((item) => summarize(item, depth + 1));
|
|
41
|
+
return value.length > ITEM_LIMIT ? [...items, `… (${value.length} items)`] : items;
|
|
42
|
+
}
|
|
43
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, summarize(item, depth + 1)]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The version of c64-re-tools that runs, from the package.json next to src/:
|
|
48
|
+
* the package's own, or the one that install writes into a skill. Undefined
|
|
49
|
+
* when there is none.
|
|
50
|
+
*/
|
|
51
|
+
function runningVersion(): string | undefined {
|
|
52
|
+
try {
|
|
53
|
+
const { version } = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8")) as { version?: unknown };
|
|
54
|
+
return typeof version === "string" ? version : undefined;
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Milliseconds since `since`, a performance.now() reading, to a tenth. */
|
|
61
|
+
export function elapsedMs(since: number): number {
|
|
62
|
+
return Math.round((performance.now() - since) * 10) / 10;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class Trace {
|
|
66
|
+
readonly kind: TraceKind;
|
|
67
|
+
/** The directory of the trace, or undefined when the trace is off. */
|
|
68
|
+
readonly dir: string | undefined;
|
|
69
|
+
/** The file this trace appends to, or undefined when the trace is off. */
|
|
70
|
+
readonly file: string | undefined;
|
|
71
|
+
#broken = false;
|
|
72
|
+
|
|
73
|
+
private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined) {
|
|
74
|
+
this.kind = kind;
|
|
75
|
+
this.dir = dir;
|
|
76
|
+
this.file = file;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A trace that records nothing. */
|
|
80
|
+
static off(kind: TraceKind): Trace {
|
|
81
|
+
return new Trace(kind, undefined, undefined);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A trace into `dir`, created when it is missing. The file is
|
|
86
|
+
* `<kind>-<CET time>-<pid>.jsonl`, and its first event is `trace.start`.
|
|
87
|
+
* Throws when the directory cannot be used.
|
|
88
|
+
*/
|
|
89
|
+
static open(kind: TraceKind, dir: string, now = new Date()): Trace {
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`));
|
|
92
|
+
trace.event("trace.start", { version: runningVersion(), pid: process.pid, argv: process.argv.slice(1), cwd: process.cwd(), node: process.version, platform: process.platform });
|
|
93
|
+
return trace;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get on(): boolean {
|
|
97
|
+
return this.file !== undefined && !this.#broken;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Appends one event. A write that fails turns the trace off; nothing throws. */
|
|
101
|
+
event(name: string, fields: TraceFields = {}): void {
|
|
102
|
+
if (!this.on) return;
|
|
103
|
+
const line: Record<string, unknown> = { t: isoCet(), kind: this.kind, event: name };
|
|
104
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
105
|
+
if (!RESERVED.has(key) && value !== undefined) line[key] = summarize(value);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
appendFileSync(this.file!, `${JSON.stringify(line)}\n`);
|
|
109
|
+
} catch {
|
|
110
|
+
this.#broken = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A free-text line, as the programs print to stderr, recorded as the event `log`. */
|
|
115
|
+
log(line: string): void {
|
|
116
|
+
this.event("log", { line });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Copies a file that would otherwise go away (VICE's own log) next to the
|
|
121
|
+
* trace, as `<kind>-<pid>-<name>`. A copy that fails is recorded, not thrown.
|
|
122
|
+
*/
|
|
123
|
+
keep(source: string, name: string): void {
|
|
124
|
+
if (!this.on) return;
|
|
125
|
+
const target = join(this.dir!, `${this.kind}-${process.pid}-${name}`);
|
|
126
|
+
try {
|
|
127
|
+
copyFileSync(source, target);
|
|
128
|
+
this.event("trace.kept", { source, file: target });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.event("trace.keep-failed", { source, error });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Off until a program calls startTrace; the kind is a placeholder until then.
|
|
136
|
+
let current = Trace.off("script");
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Switches the trace of this process on when `env` (the environment by
|
|
140
|
+
* default) has C64RT_TRACE, and off otherwise. A directory that cannot be used
|
|
141
|
+
* leaves the trace off and reports that through `warn`. Called once, at the
|
|
142
|
+
* start of a program.
|
|
143
|
+
*/
|
|
144
|
+
export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; warn?: (line: string) => void } = {}): Trace {
|
|
145
|
+
const dir = (options.env ?? process.env)[TRACE_VARIABLE];
|
|
146
|
+
if (dir === undefined || dir === "") {
|
|
147
|
+
current = Trace.off(kind);
|
|
148
|
+
return current;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
current = Trace.open(kind, dir);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
current = Trace.off(kind);
|
|
154
|
+
options.warn?.(`${TRACE_VARIABLE}=${dir} cannot be used (${error instanceof Error ? error.message : String(error)}); the trace is off.`);
|
|
155
|
+
}
|
|
156
|
+
return current;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The trace of this process: off until startTrace switches it on. */
|
|
160
|
+
export function trace(): Trace {
|
|
161
|
+
return current;
|
|
162
|
+
}
|
package/THIRD-PARTY-NOTICES.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Third-Party Notices for `@henols/c64-re-tools`
|
|
2
|
-
|
|
3
|
-
This package is MIT-licensed, copyright Henrik Olsson (see `LICENSE` at the
|
|
4
|
-
repository root). It ships the C64 reverse-engineering **skill playbooks**
|
|
5
|
-
under `skills/` plus the installer CLI under `bin/`, and it declares one
|
|
6
|
-
runtime dependency, `@henols/vice-mcp`, which carries its own notices file.
|
|
7
|
-
|
|
8
|
-
This document covers only the third-party material that ships **in this
|
|
9
|
-
package**. It is deliberately not a second, competing inventory: the canonical
|
|
10
|
-
third-party notices for the whole repository — including the cc65 opcode-table
|
|
11
|
-
transcription and the ACME/VICE not-incorporated scope notes, none of which
|
|
12
|
-
ship here — live in the `@henols/vice-mcp` package at
|
|
13
|
-
`src/mcp/vice/THIRD-PARTY-NOTICES.md` in the repository, and inside that
|
|
14
|
-
package's own tarball as `THIRD-PARTY-NOTICES.md`. Read that file for the full
|
|
15
|
-
inventory; read this one for what `@henols/c64-re-tools` itself carries.
|
|
16
|
-
|
|
17
|
-
## Incorporated material
|
|
18
|
-
|
|
19
|
-
None. The skill playbooks under `skills/` are this project's own prose,
|
|
20
|
-
written against this project's own curated tool surface.
|
|
21
|
-
|
|
22
|
-
## Runtime dependency
|
|
23
|
-
|
|
24
|
-
- **`@henols/vice-mcp`** — this repository's own MCP server package, MIT. Its
|
|
25
|
-
own third-party notices ship inside it as `THIRD-PARTY-NOTICES.md`, and that
|
|
26
|
-
file is the canonical inventory referred to at the top of this document.
|
package/bin/cli.mjs
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @henols/c64-re-tools installer.
|
|
3
|
-
//
|
|
4
|
-
// Installs the C64 reverse-engineering skills and wires the VICE MCP server into
|
|
5
|
-
// a target project:
|
|
6
|
-
// npx @henols/c64-re-tools [targetDir] [--force] [--dry-run] [--vendor]
|
|
7
|
-
//
|
|
8
|
-
// * copies the bundled skills into <target>/.claude/skills/
|
|
9
|
-
// * merges a `vice` server entry into <target>/.mcp.json (never clobbering
|
|
10
|
-
// other servers), launching it via `npx -y @henols/vice-mcp`
|
|
11
|
-
// * with --vendor, also `npm install`s @henols/vice-mcp into the project and
|
|
12
|
-
// wires .mcp.json to the local copy (pinned/offline use)
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { dirname, join, resolve } from "node:path";
|
|
16
|
-
// Entry-point dispatch guard (see the bottom of this file): the same idiom
|
|
17
|
-
// `src/skills/c64-memory-mapping/scripts/driver.mjs` already uses, so a bare
|
|
18
|
-
// `import` from a test file does not also execute the CLI.
|
|
19
|
-
import {
|
|
20
|
-
existsSync,
|
|
21
|
-
readFileSync,
|
|
22
|
-
writeFileSync,
|
|
23
|
-
mkdirSync,
|
|
24
|
-
readdirSync,
|
|
25
|
-
cpSync,
|
|
26
|
-
} from "node:fs";
|
|
27
|
-
|
|
28
|
-
const HERE = dirname(fileURLToPath(import.meta.url)); // installer/bin (packed) or repo installer/bin (dev)
|
|
29
|
-
const PKG_ROOT = dirname(HERE);
|
|
30
|
-
const SKILLS_SRC = join(PKG_ROOT, "skills");
|
|
31
|
-
|
|
32
|
-
// The single version-resolution seam this repo maintains is
|
|
33
|
-
// `src/mcp/vice/version.ts` -- one algorithm for resolving a VERSION
|
|
34
|
-
// template against a published version, kept in exactly one place after
|
|
35
|
-
// this repo once carried several independent copies of it. This package
|
|
36
|
-
// deliberately does NOT import it: it ships without the seam file (its
|
|
37
|
-
// `files[]` is `bin/`, `skills/`, `README.md`) and targets node >= 18, which
|
|
38
|
-
// cannot type-strip the seam's `.ts` the way the vice-mcp package's own
|
|
39
|
-
// node >= 24 runtime can. What follows is exactly the seam's own
|
|
40
|
-
// precedence step 1 -- "read my own package.json's `.version`, trust it
|
|
41
|
-
// when it is a real published number" -- not a second, independent
|
|
42
|
-
// implementation of the resolution algorithm; there is no template/`-`
|
|
43
|
-
// handling here because this package is never resolved from `VERSION`,
|
|
44
|
-
// only ever published with a concrete version already stamped in. That
|
|
45
|
-
// number is PRODUCED elsewhere: `scripts/version.mjs stamp` writes the
|
|
46
|
-
// working-tree placeholder, and CI's `npm version` writes the real one at
|
|
47
|
-
// publish time. Do not reimplement the seam's own VERSION-template
|
|
48
|
-
// parsing and prefix-resolution rules here.
|
|
49
|
-
const SELF = readJson(join(PKG_ROOT, "package.json")) ?? {};
|
|
50
|
-
const SELF_VERSION = typeof SELF.version === "string" ? SELF.version : "0.0.0";
|
|
51
|
-
const MCP_PKG = "@henols/vice-mcp";
|
|
52
|
-
// The dev placeholder every derived, publishable version string carries in
|
|
53
|
-
// the working tree outside a stamped release. Defined authoritatively as
|
|
54
|
-
// `DEV_PLACEHOLDER` in `src/mcp/vice/version.ts` -- repeated here as a
|
|
55
|
-
// literal, NOT imported, because this package deliberately ships without
|
|
56
|
-
// that seam file (see the comment above) and targets node >= 18, which
|
|
57
|
-
// cannot type-strip a `.ts` import the way the vice-mcp package's own
|
|
58
|
-
// node >= 24 runtime can. This is the same disclosed divergence as
|
|
59
|
-
// `SELF_VERSION` above: one literal, kept in sync by hand, documented here
|
|
60
|
-
// so a future edit to the seam's placeholder is not missed.
|
|
61
|
-
const MCP_DEV_PLACEHOLDER = "0.0.0-dev";
|
|
62
|
-
// Wire the project to the exact vice-mcp version this installer was built
|
|
63
|
-
// against -- EXCEPT when run from an unstamped dev checkout:
|
|
64
|
-
// `installer/package.json`'s dependency pin is the permanent working-tree
|
|
65
|
-
// placeholder outside a CI-stamped publish job, and `@henols/vice-mcp` at
|
|
66
|
-
// that literal version will never exist on the npm registry. Silently
|
|
67
|
-
// writing it into a consumer's .mcp.json would 404 every time Claude Code
|
|
68
|
-
// tries to launch the server, with no error until the user actually tries
|
|
69
|
-
// to use it -- strictly worse than falling back to "latest". Fall back AND
|
|
70
|
-
// warn loudly, so a developer testing the installer locally knows their
|
|
71
|
-
// .mcp.json is unpinned rather than discovering it via a silent 404 later.
|
|
72
|
-
const MCP_VERSION_RAW =
|
|
73
|
-
SELF.dependencies && typeof SELF.dependencies[MCP_PKG] === "string"
|
|
74
|
-
? SELF.dependencies[MCP_PKG].replace(/^[\^~]/, "")
|
|
75
|
-
: SELF_VERSION;
|
|
76
|
-
if (MCP_VERSION_RAW === MCP_DEV_PLACEHOLDER) {
|
|
77
|
-
console.error(
|
|
78
|
-
`c64-re-tools: WARNING -- running from an unstamped dev checkout (installer/package.json pins ` +
|
|
79
|
-
`${MCP_PKG}@${MCP_DEV_PLACEHOLDER}, the dev placeholder, not a real published version). ` +
|
|
80
|
-
`Falling back to "${MCP_PKG}@latest" in the generated .mcp.json instead of writing an ` +
|
|
81
|
-
`unresolvable pin. If you intended to test against a specific version, pass --vendor or fix ` +
|
|
82
|
-
`installer/package.json's dependency pin before packaging a release.`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
const MCP_VERSION = MCP_VERSION_RAW === MCP_DEV_PLACEHOLDER ? "latest" : MCP_VERSION_RAW;
|
|
86
|
-
|
|
87
|
-
function readJson(path) {
|
|
88
|
-
try {
|
|
89
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
90
|
-
} catch {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function parseArgs(argv) {
|
|
96
|
-
const opts = { force: false, dryRun: false, vendor: false, help: false, target: undefined };
|
|
97
|
-
for (const arg of argv) {
|
|
98
|
-
if (arg === "--force") opts.force = true;
|
|
99
|
-
else if (arg === "--dry-run" || arg === "-n") opts.dryRun = true;
|
|
100
|
-
else if (arg === "--vendor") opts.vendor = true;
|
|
101
|
-
else if (arg === "--help" || arg === "-h") opts.help = true;
|
|
102
|
-
else if (arg.startsWith("-")) {
|
|
103
|
-
console.error(`c64-re-tools: unknown flag ${JSON.stringify(arg)} (try --help)`);
|
|
104
|
-
process.exit(2);
|
|
105
|
-
} else if (opts.target === undefined) opts.target = arg;
|
|
106
|
-
else {
|
|
107
|
-
console.error(`c64-re-tools: unexpected extra argument ${JSON.stringify(arg)} (try --help)`);
|
|
108
|
-
process.exit(2);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return opts;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const HELP = `c64-re-tools -- install the C64 reverse-engineering skills + VICE MCP server into a project
|
|
115
|
-
|
|
116
|
-
Usage:
|
|
117
|
-
npx @henols/c64-re-tools [targetDir] [options]
|
|
118
|
-
|
|
119
|
-
Arguments:
|
|
120
|
-
targetDir Project to install into (default: current directory)
|
|
121
|
-
|
|
122
|
-
Options:
|
|
123
|
-
--force Overwrite existing skills and an existing 'vice' MCP entry
|
|
124
|
-
--vendor Also 'npm install -D ${MCP_PKG}' into the project and wire
|
|
125
|
-
.mcp.json to the local copy (pinned/offline), instead of npx
|
|
126
|
-
--dry-run, -n Show what would change without writing anything
|
|
127
|
-
--help, -h Show this help
|
|
128
|
-
|
|
129
|
-
What it does:
|
|
130
|
-
1. Copies bundled skills into <target>/.claude/skills/
|
|
131
|
-
2. Adds a 'vice' server to <target>/.mcp.json (other servers are preserved)
|
|
132
|
-
|
|
133
|
-
Requires Node >= 24 to RUN the vice MCP server (this installer runs on Node >= 18).`;
|
|
134
|
-
|
|
135
|
-
function viceServerEntry(vendor) {
|
|
136
|
-
return {
|
|
137
|
-
command: "npx",
|
|
138
|
-
args: vendor ? [MCP_PKG] : ["-y", `${MCP_PKG}@${MCP_VERSION}`],
|
|
139
|
-
timeout: 150000,
|
|
140
|
-
env: { MASTRA_TELEMETRY_DISABLED: "1" },
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function installSkills(target, { force, dryRun }) {
|
|
145
|
-
if (!existsSync(SKILLS_SRC)) {
|
|
146
|
-
console.error(
|
|
147
|
-
`c64-re-tools: FAIL -- bundled skills not found at ${SKILLS_SRC}. ` +
|
|
148
|
-
`(In a dev checkout, run 'node scripts/sync-skills.mjs' first.)`
|
|
149
|
-
);
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
const names = readdirSync(SKILLS_SRC, { withFileTypes: true })
|
|
153
|
-
.filter((d) => d.isDirectory())
|
|
154
|
-
.map((d) => d.name)
|
|
155
|
-
.sort();
|
|
156
|
-
const destRoot = join(target, ".claude", "skills");
|
|
157
|
-
const installed = [];
|
|
158
|
-
const skipped = [];
|
|
159
|
-
for (const name of names) {
|
|
160
|
-
const dest = join(destRoot, name);
|
|
161
|
-
if (existsSync(dest) && !force) {
|
|
162
|
-
skipped.push(name);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (!dryRun) {
|
|
166
|
-
mkdirSync(destRoot, { recursive: true });
|
|
167
|
-
cpSync(join(SKILLS_SRC, name), dest, { recursive: true, force: true });
|
|
168
|
-
}
|
|
169
|
-
installed.push(name);
|
|
170
|
-
}
|
|
171
|
-
return { destRoot, installed, skipped, total: names.length };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function wireMcp(target, { force, dryRun, vendor }) {
|
|
175
|
-
const mcpPath = join(target, ".mcp.json");
|
|
176
|
-
let config = { mcpServers: {} };
|
|
177
|
-
if (existsSync(mcpPath)) {
|
|
178
|
-
const parsed = readJson(mcpPath);
|
|
179
|
-
if (parsed === undefined) {
|
|
180
|
-
console.error(
|
|
181
|
-
`c64-re-tools: FAIL -- ${mcpPath} exists but is not valid JSON. ` +
|
|
182
|
-
`Refusing to overwrite it; fix or remove it and re-run.`
|
|
183
|
-
);
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
config = parsed;
|
|
187
|
-
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
188
|
-
console.error(`c64-re-tools: FAIL -- ${mcpPath} is not a JSON object.`);
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
if (typeof config.mcpServers !== "object" || config.mcpServers === null) {
|
|
192
|
-
config.mcpServers = {};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const existed = Object.prototype.hasOwnProperty.call(config.mcpServers, "vice");
|
|
196
|
-
let action;
|
|
197
|
-
if (existed && !force) {
|
|
198
|
-
action = "kept"; // leave the user's existing entry alone
|
|
199
|
-
} else {
|
|
200
|
-
action = existed ? "updated" : "added";
|
|
201
|
-
if (!dryRun) {
|
|
202
|
-
config.mcpServers.vice = viceServerEntry(vendor);
|
|
203
|
-
mkdirSync(dirname(mcpPath), { recursive: true });
|
|
204
|
-
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return { mcpPath, action };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function vendorInstall(target, { dryRun }) {
|
|
211
|
-
const spec = `${MCP_PKG}@${MCP_VERSION}`;
|
|
212
|
-
if (dryRun) return { ran: false, spec };
|
|
213
|
-
const res = spawnSync("npm", ["install", "--save-dev", spec], {
|
|
214
|
-
cwd: target,
|
|
215
|
-
stdio: "inherit",
|
|
216
|
-
});
|
|
217
|
-
if (res.status !== 0) {
|
|
218
|
-
console.error(
|
|
219
|
-
`c64-re-tools: WARN -- 'npm install --save-dev ${spec}' exited ${res.status}. ` +
|
|
220
|
-
`Skills and .mcp.json were still written; install the package manually if needed.`
|
|
221
|
-
);
|
|
222
|
-
return { ran: true, ok: false, spec };
|
|
223
|
-
}
|
|
224
|
-
return { ran: true, ok: true, spec };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function main() {
|
|
228
|
-
const opts = parseArgs(process.argv.slice(2));
|
|
229
|
-
if (opts.help) {
|
|
230
|
-
console.log(HELP);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const target = resolve(opts.target ?? process.cwd());
|
|
234
|
-
if (!existsSync(target)) {
|
|
235
|
-
console.error(`c64-re-tools: FAIL -- target directory does not exist: ${target}`);
|
|
236
|
-
process.exit(1);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
console.error(`c64-re-tools ${SELF_VERSION} -> ${target}${opts.dryRun ? " (dry run)" : ""}`);
|
|
240
|
-
|
|
241
|
-
const skills = installSkills(target, opts);
|
|
242
|
-
if (opts.vendor) vendorInstall(target, opts);
|
|
243
|
-
const mcp = wireMcp(target, opts);
|
|
244
|
-
|
|
245
|
-
// Summary
|
|
246
|
-
console.error("");
|
|
247
|
-
console.error(` skills -> ${skills.destRoot}`);
|
|
248
|
-
console.error(
|
|
249
|
-
` ${skills.installed.length} installed${
|
|
250
|
-
opts.force ? "" : `, ${skills.skipped.length} already present (use --force to overwrite)`
|
|
251
|
-
} of ${skills.total}`
|
|
252
|
-
);
|
|
253
|
-
if (skills.installed.length) console.error(` + ${skills.installed.join(", ")}`);
|
|
254
|
-
if (skills.skipped.length && !opts.force)
|
|
255
|
-
console.error(` = ${skills.skipped.join(", ")} (kept)`);
|
|
256
|
-
console.error(` mcp -> ${mcp.mcpPath}`);
|
|
257
|
-
if (mcp.action === "kept") {
|
|
258
|
-
console.error(` 'vice' already configured -- kept (use --force to overwrite)`);
|
|
259
|
-
} else {
|
|
260
|
-
console.error(
|
|
261
|
-
` 'vice' ${mcp.action}${
|
|
262
|
-
opts.vendor ? ` (local ${MCP_PKG})` : ` (npx -y ${MCP_PKG}@${MCP_VERSION})`
|
|
263
|
-
}`
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
console.error("");
|
|
267
|
-
if (opts.dryRun) {
|
|
268
|
-
console.error("Dry run -- nothing was written.");
|
|
269
|
-
} else {
|
|
270
|
-
console.error("Done. Restart Claude Code in this project so it picks up the skills and MCP server.");
|
|
271
|
-
console.error("Note: running the vice MCP server requires Node >= 24.");
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Only dispatch when this module is the process entry point -- i.e. run as
|
|
276
|
-
// `node bin/cli.mjs ...` or via the `c64-re-tools` bin shim -- not when
|
|
277
|
-
// imported by a test (installer/wire-mcp.test.mjs imports `wireMcp`/`readJson`
|
|
278
|
-
// below and must not trigger a real install as a side effect).
|
|
279
|
-
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
280
|
-
main();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export { wireMcp, readJson };
|