@polycode-projects/the-mechanical-code-talker 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -3
- package/ROADMAP.md +416 -3
- package/bin/tmct.mjs +308 -12
- package/corpus/README.md +52 -0
- package/corpus/conceptnet/LICENSE-NOTICE +37 -0
- package/corpus/conceptnet/README.md +103 -0
- package/corpus/conceptnet/fetch-slice.mjs +136 -0
- package/corpus/conceptnet/filter-dump.mjs +89 -0
- package/corpus/conceptnet/slice.jsonl +14258 -0
- package/data/phrasebook/software-phrases.txt +231 -0
- package/data/templates/grammar-rules.toml +89 -0
- package/data/templates/responses.jsonl +68 -0
- package/package.json +40 -3
- package/src/ask-nlp.mjs +22 -10
- package/src/ask-vocab.mjs +35 -1
- package/src/ask.mjs +171 -494
- package/src/chat.mjs +709 -81
- package/src/corpus/conceptnet-map.toml +251 -0
- package/src/corpus/conceptnet.mjs +167 -0
- package/src/corpus/templates.mjs +188 -0
- package/src/finish.mjs +443 -0
- package/src/grammar/ace.mjs +341 -0
- package/src/grammar/assert.mjs +40 -0
- package/src/grammar/lexicon-core.json +287 -0
- package/src/grammar/lexicon.mjs +202 -0
- package/src/hash.mjs +32 -0
- package/src/index.mjs +21 -5
- package/src/init.mjs +264 -0
- package/src/interpret/fuzzy.mjs +89 -0
- package/src/interpret/merge.mjs +148 -0
- package/src/interpret/normalize.mjs +151 -0
- package/src/interpret/pipeline.mjs +112 -0
- package/src/interpret/strategies/grammar.mjs +137 -0
- package/src/interpret/strategies/keywords.mjs +241 -0
- package/src/interpret/strategies/noise-strip.mjs +114 -0
- package/src/memory/blocks.mjs +221 -0
- package/src/memory/core.mjs +533 -0
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +141 -0
- package/src/memory/trust.mjs +113 -0
- package/src/prose-nlp.mjs +14 -16
- package/src/providers/bootstrap.mjs +24 -0
- package/src/providers/fixture.mjs +118 -0
- package/src/providers/graph-service.mjs +312 -0
- package/src/repository-interface.mjs +318 -0
- package/src/server.mjs +44 -28
- package/src/sessions.mjs +137 -4
- package/src/source.mjs +44 -5
- package/src/syllogise.mjs +0 -0
- package/src/toml-config.mjs +14 -0
- package/src/tui/app.mjs +173 -0
- package/src/wink-model.mjs +74 -0
- package/bin/cli.mjs +0 -226
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// wink-model.mjs — the ONE place tmct loads the wink-nlp engine + model.
|
|
2
|
+
//
|
|
3
|
+
// Two adapters sit on top of this leaf loader: ask-nlp.mjs (lemma/POS tier for the
|
|
4
|
+
// ask engine) and prose-nlp.mjs (lemma layer for the prose index). They used to
|
|
5
|
+
// each carry their own `createRequire(import.meta.url)` block — the same ~six lines
|
|
6
|
+
// twice, and both Node-only. That duplication is single-sourced here, and the
|
|
7
|
+
// Node-only limitation is lifted with a browser seam, WITHOUT eagerly bundling the
|
|
8
|
+
// ~1 MB model into anything.
|
|
9
|
+
//
|
|
10
|
+
// Why a registration seam instead of a static `import "wink-nlp"`:
|
|
11
|
+
// - The whole architecture keeps the model OUT of the base/viewer bundle; a static
|
|
12
|
+
// import would drag it in. `wink-eng-lite-web-model` is already the *browser*
|
|
13
|
+
// build, so the model can run in the page — what was missing is a load path a
|
|
14
|
+
// bundler can satisfy without a Node `require`. That path is `registerWinkModel`:
|
|
15
|
+
// a browser/bundler entry imports wink with its own `import` and hands the pair
|
|
16
|
+
// in ONCE, before any lemma/POS use. Node needs nothing — it falls back to
|
|
17
|
+
// `createRequire`. This is the Phase-8 browser-mode unblocker the dependency
|
|
18
|
+
// audit called for (a wiring fix; the model was always browser-capable).
|
|
19
|
+
//
|
|
20
|
+
// The loader stays SYNCHRONOUS (the adapters and their callers are sync): the
|
|
21
|
+
// browser host registers up front; Node resolves lazily via createRequire. Failure
|
|
22
|
+
// is cached as null — a checkout without the optional deps, or a page that never
|
|
23
|
+
// registered a model, simply runs adapter-less (lemma/POS tiers honestly off), it
|
|
24
|
+
// never throws.
|
|
25
|
+
|
|
26
|
+
import { createRequire } from "node:module";
|
|
27
|
+
|
|
28
|
+
let injected; // browser/bundler-supplied `() => ({ winkNLP, model })`, or undefined
|
|
29
|
+
let cached; // undefined = not tried yet; null = unavailable (tried once, honestly off)
|
|
30
|
+
|
|
31
|
+
/** Browser/bundler seam: register a factory returning `{ winkNLP, model }` (each the
|
|
32
|
+
* imported module) so the page's own bundler resolves wink instead of a Node
|
|
33
|
+
* `require`. Call once before any ask/prose lemma use. Resets the cache so a late
|
|
34
|
+
* registration still takes effect. */
|
|
35
|
+
export function registerWinkModel(factory) {
|
|
36
|
+
injected = factory;
|
|
37
|
+
cached = undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Load `{ winkNLP, model }` once, or null when wink isn't available. Prefers a
|
|
41
|
+
* registered browser factory; otherwise falls back to Node module resolution. */
|
|
42
|
+
export function loadWinkModel() {
|
|
43
|
+
if (cached !== undefined) return cached;
|
|
44
|
+
try {
|
|
45
|
+
const pair = injected ? injected() : nodeRequireWink();
|
|
46
|
+
cached = pair && pair.winkNLP && pair.model ? pair : null;
|
|
47
|
+
} catch {
|
|
48
|
+
cached = null;
|
|
49
|
+
}
|
|
50
|
+
return cached;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Node fallback: resolve wink through the module system (never a guessed path),
|
|
54
|
+
* exactly as the two adapters did inline before. CJS deps, so `createRequire`. */
|
|
55
|
+
function nodeRequireWink() {
|
|
56
|
+
const require = createRequire(import.meta.url);
|
|
57
|
+
return {
|
|
58
|
+
winkNLP: require("wink-nlp"),
|
|
59
|
+
model: require("wink-eng-lite-web-model"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Convenience: the constructed `nlp` instance (`winkNLP(model)`) or null. Both
|
|
64
|
+
* adapters want exactly this. Not cached here — the adapters cache their own
|
|
65
|
+
* higher-level object; constructing `nlp` is cheap next to loading the model. */
|
|
66
|
+
export function winkInstance() {
|
|
67
|
+
const loaded = loadWinkModel();
|
|
68
|
+
if (!loaded) return null;
|
|
69
|
+
try {
|
|
70
|
+
return loaded.winkNLP(loaded.model);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
package/bin/cli.mjs
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// tmct — local typed-edge code-graph tools over a pre-built graph artifact:
|
|
3
|
-
//
|
|
4
|
-
// tmct cli digest '{"repo_path":"<abs>","modules":[…]}' → architecture map + per-module
|
|
5
|
-
// context bundles to stdout
|
|
6
|
-
// tmct cli digest '{"repo_path":"<abs>","query":"<free text>"}' → same, but auto-locates +
|
|
7
|
-
// score-gap-selects the modules (R1b, the shipped default) instead of requiring an explicit
|
|
8
|
-
// `modules` list — one call from a question to a digest. `modules` wins if both are given.
|
|
9
|
-
// tmct cli <toolName> '{…args}' → invoke ANY tool via Bash
|
|
10
|
-
// (e.g. tmct cli tmct_ask '{"query":"<free text>"}' — mechanical, no-LLM NL question
|
|
11
|
-
// over the graph; no bespoke wiring needed, this fallback covers it)
|
|
12
|
-
// tmct chat [--repo <abs>] → interactive prompt over the mechanical tmct_ask engine; /exit to leave; session log → <repo>/.tmct/session-<uuidv7>.log
|
|
13
|
-
//
|
|
14
|
-
// The graph artifact lives at <repo_path>/.tmct/graph.json; the tools (run with
|
|
15
|
-
// cwd = that repo) load it by default. No flags, no config files.
|
|
16
|
-
|
|
17
|
-
import { join } from "node:path";
|
|
18
|
-
import { dispatchTool, buildContextBundle } from "../src/server.mjs";
|
|
19
|
-
import { loadConfig, DEFAULT_GRAPH_REL } from "../src/config.mjs";
|
|
20
|
-
import * as source from "../src/source.mjs";
|
|
21
|
-
import { parseEntities, rankModulesByProximity, searchModulesRanked, selectRankedModules, DEFAULT_SCORE_GAP } from "../src/codegraph.mjs";
|
|
22
|
-
|
|
23
|
-
/** Build a config pointed at a specific repo's artifact (for `cli` sub-commands that
|
|
24
|
-
* take a repo_path), or fall back to the cwd-derived default. */
|
|
25
|
-
function configFor(repoPath) {
|
|
26
|
-
return repoPath ? { graphFile: join(repoPath, DEFAULT_GRAPH_REL) } : loadConfig();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Parse the trailing JSON payload of a `cli` sub-command (best-effort). */
|
|
30
|
-
function parsePayload(payload) {
|
|
31
|
-
if (!payload) return {};
|
|
32
|
-
try { return JSON.parse(payload); }
|
|
33
|
-
catch { return null; }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const DIGEST_MODULE_CAP = 12; // bound the digest — a handful of changed modules
|
|
37
|
-
const DIGEST_SECONDARY_CAP = 2; // B2: at most this many SECONDARY modules get a (trimmed) bundle
|
|
38
|
-
const TIER_RANK = { NONE: 0, TINY: 1, MID: 2, LARGE: 3, FULL: 4 };
|
|
39
|
-
|
|
40
|
-
/** `cli digest` — print a machine-readable header + a repo architecture map + the
|
|
41
|
-
* tmct_context edit bundle for each requested module to stdout (reuses the server's exact
|
|
42
|
-
* renderer via buildContextBundle, so no render logic is duplicated). This stdout is injected
|
|
43
|
-
* into a caller's prompt.
|
|
44
|
-
*
|
|
45
|
-
* Two ways to say which modules: an explicit `modules` array (unchanged), or a `query` string —
|
|
46
|
-
* auto-locate + score-gap-select (R1b, the shipped default as of 2026-07-02) in one call, so a
|
|
47
|
-
* real caller no longer has to run `tmct_locate` and hand-pick a module themselves. `modules`
|
|
48
|
-
* wins if both are given. The header reports which modules were actually selected either way.
|
|
49
|
-
*
|
|
50
|
-
* B2: the FIRST (primary) module gets a full size-adaptive bundle; the remaining modules are
|
|
51
|
-
* RANKED by import/cochange proximity to the primary and only the top few get a TRIMMED
|
|
52
|
-
* (signatures + insertion region) bundle — so a 2-module task no longer pays for two full
|
|
53
|
-
* bundles. The leading header line lets the rig record tier/topup telemetry. */
|
|
54
|
-
async function runDigest(args) {
|
|
55
|
-
const repoPath = args.repo_path;
|
|
56
|
-
if (!repoPath) { process.stderr.write("tmct: digest requires repo_path\n"); process.exit(2); }
|
|
57
|
-
let modules = Array.isArray(args.modules) ? args.modules.slice(0, DIGEST_MODULE_CAP) : [];
|
|
58
|
-
let autoSelected = null; // for the header, when `query` drove selection
|
|
59
|
-
if (!modules.length && args.query) {
|
|
60
|
-
const graph = parseEntities(await source.fetchEntities(configFor(repoPath)));
|
|
61
|
-
// SHIPPED DEFAULT (0.5.0): the digest's query-mode auto-locate resolves literal-mention ON
|
|
62
|
-
// (a fresh invocation with no tmct.toml), disable-able via `literal_mention:false`. Kept in
|
|
63
|
-
// lockstep with the `tmct_locate` handler so `cli digest '{query}'` ≡ `cli tmct_locate` for the
|
|
64
|
-
// same query. A strict no-op unless the query carries a ≥3-component dotted path / repo-relative
|
|
65
|
-
// path; searchModulesRanked derives rawQuery from the query when literalMention is on.
|
|
66
|
-
const ranked = searchModulesRanked(graph, args.query, { literalMention: args.literal_mention !== false });
|
|
67
|
-
const scoreGapK = args.score_gap === false ? null : (Number.isFinite(args.score_gap) ? args.score_gap : DEFAULT_SCORE_GAP);
|
|
68
|
-
modules = selectRankedModules(ranked, { top_k: Number.isFinite(args.top_k) ? args.top_k : 2, scoreGapK }).slice(0, DIGEST_MODULE_CAP);
|
|
69
|
-
autoSelected = modules;
|
|
70
|
-
if (!modules.length) process.stderr.write(`tmct: digest query "${args.query}" matched no modules — empty digest\n`);
|
|
71
|
-
}
|
|
72
|
-
// Tuning-flag contract (threaded to buildContextBundle → sizeBundle): `min` → leanest TINY/no
|
|
73
|
-
// top-up; `untuned` → the earlier escalation. Neither → the tuned default. The digest header still
|
|
74
|
-
// reports the EFFECTIVE tier/topup returned per module, so rig telemetry stays correct.
|
|
75
|
-
const min = Boolean(args.min);
|
|
76
|
-
const untuned = Boolean(args.untuned);
|
|
77
|
-
// tmct-max: the injection CEILING — every requested module gets a FULL (untrimmed) bundle,
|
|
78
|
-
// not just the primary + 2 trimmed secondaries. Tests whether maximal injection re-bloats.
|
|
79
|
-
const max = Boolean(args.max);
|
|
80
|
-
const secondaryCap = max ? modules.length : DIGEST_SECONDARY_CAP;
|
|
81
|
-
const config = configFor(repoPath);
|
|
82
|
-
const body = [];
|
|
83
|
-
let effTier = "NONE"; // largest tier emitted across all modules
|
|
84
|
-
let topup = false; // whether any module's auto-sizing escalated above TINY
|
|
85
|
-
let emitted = 0; // module bundles actually emitted (primary + trimmed secondaries)
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
body.push("# Repository architecture\n" + (await dispatchTool("tmct_architecture", {}, { config })));
|
|
89
|
-
} catch (e) {
|
|
90
|
-
body.push(`# Repository architecture\n(unavailable: ${e?.message || e})`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const emit = async (m, trim) => {
|
|
94
|
-
try {
|
|
95
|
-
const { text, tier, topup: t } = await buildContextBundle({ symbol: m, min, untuned, max }, { config, source, trim });
|
|
96
|
-
body.push(`\n# Context bundle: ${m}${trim ? " (secondary, trimmed)" : ""}\n` + text);
|
|
97
|
-
if ((TIER_RANK[tier] || 0) > (TIER_RANK[effTier] || 0)) effTier = tier;
|
|
98
|
-
if (t) topup = true;
|
|
99
|
-
emitted += 1;
|
|
100
|
-
} catch (e) {
|
|
101
|
-
body.push(`\n# Context bundle: ${m}\n(no bundle: ${e?.message || e})`);
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
if (modules.length) {
|
|
106
|
-
const [primary, ...rest] = modules;
|
|
107
|
-
// rank the secondaries by proximity to the primary (best-effort: keep input order on error)
|
|
108
|
-
let ranked = rest;
|
|
109
|
-
if (rest.length) {
|
|
110
|
-
try { ranked = rankModulesByProximity(parseEntities(await source.fetchEntities(config)), primary, rest); }
|
|
111
|
-
catch { ranked = rest; }
|
|
112
|
-
}
|
|
113
|
-
const secondaries = ranked.slice(0, secondaryCap);
|
|
114
|
-
const overflow = ranked.slice(secondaryCap);
|
|
115
|
-
await emit(primary, false);
|
|
116
|
-
for (const m of secondaries) await emit(m, max ? false : true);
|
|
117
|
-
if (overflow.length) body.push(`\n# Related modules (not expanded; query tmct_context if needed): ${overflow.join(", ")}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// HARD CONTRACT: first line is the machine-readable digest header the rig greps. Fields are
|
|
121
|
-
// append-only — `selected=` is new (query mode only) and never changes the existing ones the
|
|
122
|
-
// rig's own parser depends on.
|
|
123
|
-
const header = `# tmct-digest tier=${effTier} topup=${topup} modules=${emitted}`
|
|
124
|
-
+ (autoSelected ? ` selected=${autoSelected.join(",") || "(none)"}` : "");
|
|
125
|
-
process.stdout.write([header, ...body].join("\n") + "\n");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function main() {
|
|
129
|
-
const [mode, sub, payload] = process.argv.slice(2);
|
|
130
|
-
|
|
131
|
-
if (mode === "cli") {
|
|
132
|
-
// digest mode: architecture map + per-module context bundles → stdout
|
|
133
|
-
if (sub === "digest") {
|
|
134
|
-
const args = parsePayload(payload);
|
|
135
|
-
if (args === null) {
|
|
136
|
-
process.stderr.write("tmct: digest expects a JSON arg, e.g. '{\"repo_path\":\"/abs\",\"modules\":[…]}'\n");
|
|
137
|
-
process.exit(2);
|
|
138
|
-
}
|
|
139
|
-
await runDigest(args);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// locate mode (TUNING #3): `cli tmct_locate '{"query":"…","repo_path":"<abs>"}'` emits the
|
|
144
|
-
// ranked modules as `<relpath>\t<score>`, one per line (highest first), using renderSearch's
|
|
145
|
-
// exact ranking. The rig keeps rank-1 always and rank-2 only when score2/score1 is close — it
|
|
146
|
-
// needs the raw scores, which the text renderer hides. Independent of the tuning flags.
|
|
147
|
-
if (sub === "tmct_locate") {
|
|
148
|
-
const args = parsePayload(payload);
|
|
149
|
-
if (args === null) {
|
|
150
|
-
process.stderr.write("tmct: tmct_locate expects a JSON arg, e.g. '{\"query\":\"…\",\"repo_path\":\"/abs\"}'\n");
|
|
151
|
-
process.exit(2);
|
|
152
|
-
}
|
|
153
|
-
const config = configFor(args.repo_path);
|
|
154
|
-
try {
|
|
155
|
-
const graph = parseEntities(await source.fetchEntities(config));
|
|
156
|
-
// B016 recall-lever flags (LOCATE-phase, per-arm; output byte-identical when absent).
|
|
157
|
-
const ranked = searchModulesRanked(graph, args.query || "", {
|
|
158
|
-
demoteNonProd: !!args.demote_nonprod, // R1a: demote examples//fixtures//test-* paths
|
|
159
|
-
callAdjacency: !!args.call_adjacency, // E1a: resolved-call adjacency bonus
|
|
160
|
-
implOfInterface: !!args.impl_of_interface, // E1b: C# impl-of-interface boost
|
|
161
|
-
beamSearch: !!args.beam_search, // §5.15: multi-ply discriminative expansion
|
|
162
|
-
...(Number.isFinite(Number(args.beam_width)) ? { beamWidth: Number(args.beam_width) } : {}),
|
|
163
|
-
// B018 §8.1.3 literal-mention lever: match verbatim dotted-name/path mentions in the RAW
|
|
164
|
-
// query (which the locate tokenizer destroys). searchModulesRanked derives rawQuery from the
|
|
165
|
-
// query arg when literalMention is on; the rig passes the raw problem as the query, so literal
|
|
166
|
-
// matching keys off the untokenized text. raw_query is forwarded too for callers that normalize
|
|
167
|
-
// the query arg separately from the raw problem text.
|
|
168
|
-
// SHIPPED DEFAULT (0.5.0): literal-mention is ON for a fresh invocation (no arg, no
|
|
169
|
-
// tmct.toml) — pass `literal_mention:false` to disable. It is a strict no-op on queries
|
|
170
|
-
// with no ≥3-component dotted path / repo-relative path, so it never perturbs the cells the
|
|
171
|
-
// headline B018 numbers were measured on. The low-level scoreModules default (codegraph.mjs)
|
|
172
|
-
// stays literalMention=false; the product surface opts in explicitly, right here.
|
|
173
|
-
literalMention: args.literal_mention !== false,
|
|
174
|
-
...(args.raw_query != null ? { rawQuery: String(args.raw_query) } : {}),
|
|
175
|
-
});
|
|
176
|
-
process.stdout.write(ranked.map((r) => `${r.path}\t${r.score}`).join("\n") + "\n");
|
|
177
|
-
} catch (e) {
|
|
178
|
-
process.stderr.write(`tmct: ${e?.message || e}\n`);
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// tool-query fallback: any other `cli <toolName> '{…}'` routes to dispatchTool,
|
|
185
|
-
// so "cold" tools are invokable from Bash directly.
|
|
186
|
-
if (sub) {
|
|
187
|
-
const args = parsePayload(payload);
|
|
188
|
-
if (args === null) {
|
|
189
|
-
process.stderr.write(`tmct: ${sub} expects a JSON arg, e.g. '{"symbol":"<name>"}'\n`);
|
|
190
|
-
process.exit(2);
|
|
191
|
-
}
|
|
192
|
-
const config = configFor(args.repo_path);
|
|
193
|
-
try {
|
|
194
|
-
const text = await dispatchTool(sub, args, { config });
|
|
195
|
-
process.stdout.write(text + "\n");
|
|
196
|
-
} catch (e) {
|
|
197
|
-
process.stderr.write(`tmct: ${e?.message || e}\n`);
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
process.stderr.write("tmct: `cli` needs a sub-command (digest | <toolName>)\n");
|
|
204
|
-
process.exit(2);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (mode === "chat") {
|
|
208
|
-
const { runChat } = await import("../src/chat.mjs");
|
|
209
|
-
const i = process.argv.indexOf("--repo");
|
|
210
|
-
await runChat({
|
|
211
|
-
repoPath: i !== -1 ? process.argv[i + 1] : undefined,
|
|
212
|
-
});
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Bare invocation: no server to start any more — print the usage line and exit 2.
|
|
217
|
-
// (bin/tmct.mjs splices in `chat` for bare invocations, so nothing user-facing is lost.)
|
|
218
|
-
process.stderr.write(`tmct: ${mode === undefined ? "missing mode" : `unknown invocation "${process.argv.slice(2).join(" ")}"`}. ` +
|
|
219
|
-
"Use `cli digest …`, `cli <tool> …`, or `chat`.\n");
|
|
220
|
-
process.exit(2);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
main().catch((e) => {
|
|
224
|
-
process.stderr.write(`tmct: ${e?.message || e}\n`);
|
|
225
|
-
process.exit(1);
|
|
226
|
-
});
|