@polycode-projects/the-mechanical-code-talker 0.2.0 → 0.3.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/ROADMAP.md +5 -2
- package/bin/tmct.mjs +253 -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/responses.jsonl +55 -0
- package/package.json +12 -3
- package/src/ask-nlp.mjs +14 -0
- package/src/ask-vocab.mjs +13 -1
- package/src/ask.mjs +92 -493
- package/src/chat.mjs +147 -45
- package/src/corpus/conceptnet-map.toml +251 -0
- package/src/corpus/conceptnet.mjs +155 -0
- package/src/corpus/templates.mjs +104 -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/index.mjs +21 -5
- package/src/interpret/fuzzy.mjs +89 -0
- package/src/interpret/merge.mjs +148 -0
- package/src/interpret/normalize.mjs +117 -0
- package/src/interpret/pipeline.mjs +112 -0
- package/src/interpret/strategies/grammar.mjs +137 -0
- package/src/interpret/strategies/keywords.mjs +185 -0
- package/src/interpret/strategies/noise-strip.mjs +114 -0
- package/src/memory/blocks.mjs +201 -0
- package/src/memory/core.mjs +292 -0
- package/src/memory/fold.mjs +105 -0
- package/src/sessions.mjs +125 -3
- package/src/source.mjs +44 -5
- package/src/tui/app.mjs +173 -0
- package/bin/cli.mjs +0 -226
package/src/tui/app.mjs
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// tui/app.mjs — the Ink full-screen chat shell (the default on a TTY).
|
|
2
|
+
//
|
|
3
|
+
// The claude-code feel: an alternate-screen, full-height layout with a scrolling
|
|
4
|
+
// transcript pane (each visitor line echoed under the prompt it was typed at,
|
|
5
|
+
// the answer below it), a bottom input line carrying the live `tmct> ` /
|
|
6
|
+
// `tmct(label)> ` focus prompt, and a thin status bar (repo · module count ·
|
|
7
|
+
// session id · an honest "no graph — starting empty" when bootstrapping).
|
|
8
|
+
//
|
|
9
|
+
// EVERY turn goes through the SAME createSession sink (src/chat.mjs) the plain
|
|
10
|
+
// readline shell uses — the transcript log, structured sidecar, per-turn graph
|
|
11
|
+
// upsert and memory side-write are byte-identical to `--plain`; only the
|
|
12
|
+
// screen drawing differs. Slash-commands work unchanged (they're session.turn's
|
|
13
|
+
// job); `/exit` and a conversational "bye" end the session; Ctrl+C exits
|
|
14
|
+
// cleanly through the same close() (Ink's exitOnCtrlC → waitUntilExit → close).
|
|
15
|
+
//
|
|
16
|
+
// Library decision (ROADMAP Phase 1 shell work): Ink 7 + React 19 — plain Node
|
|
17
|
+
// ESM, no JSX/build step (React.createElement throughout). OpenTUI was ruled
|
|
18
|
+
// out for now: @opentui/core depends on Bun FFI (bun-ffi-structs / a native Zig
|
|
19
|
+
// renderer), so it doesn't run under plain Node; revisit when it does.
|
|
20
|
+
//
|
|
21
|
+
// The view-model is PURE and exported (statusText, appendTurn, transcriptLines,
|
|
22
|
+
// wrapLines) so node:test exercises it without a terminal; the component tree
|
|
23
|
+
// is thin glue over it.
|
|
24
|
+
|
|
25
|
+
import React, { useEffect, useState } from "react";
|
|
26
|
+
import { render, Box, Text, useApp, useInput, useStdout } from "ink";
|
|
27
|
+
import { createSession } from "../chat.mjs";
|
|
28
|
+
|
|
29
|
+
const h = React.createElement;
|
|
30
|
+
|
|
31
|
+
/** The thin status-bar text: repo · module count · session id (short, like the
|
|
32
|
+
* Session graph label) — plus the honest bootstrap note when the graph is empty. */
|
|
33
|
+
export function statusText({ repo, moduleCount, sessionId, empty }) {
|
|
34
|
+
const parts = [String(repo), `${moduleCount} module(s)`, `session ${String(sessionId).slice(0, 8)}`];
|
|
35
|
+
if (empty) parts.push("no graph — starting empty");
|
|
36
|
+
return parts.join(" · ");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Append one completed turn to the transcript model (pure — returns a new array).
|
|
40
|
+
* `prompt` is the prompt the question was typed at (so a focus label is preserved
|
|
41
|
+
* in the echo, exactly like a scrolled readline session reads). */
|
|
42
|
+
export function appendTurn(items, { prompt, query, answer }) {
|
|
43
|
+
return [...items, { kind: "q", prompt: String(prompt), text: String(query) }, { kind: "a", text: String(answer) }];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Flatten the transcript model to display lines: the echoed `tmct> question`
|
|
47
|
+
* line, the answer's own lines, and a blank separator after each turn —
|
|
48
|
+
* mirroring the transcript log's visual rhythm. */
|
|
49
|
+
export function transcriptLines(items) {
|
|
50
|
+
const lines = [];
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
if (item.kind === "q") lines.push(`${item.prompt}${item.text}`);
|
|
53
|
+
else { lines.push(...String(item.text).split("\n")); lines.push(""); }
|
|
54
|
+
}
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Hard-wrap display lines at `width` columns so the pane's line budget is honest
|
|
59
|
+
* (Ink would soft-wrap and overflow the fixed-height layout otherwise). */
|
|
60
|
+
export function wrapLines(lines, width) {
|
|
61
|
+
const w = Math.max(1, Number(width) || 80);
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
let s = String(line);
|
|
65
|
+
if (s.length <= w) { out.push(s); continue; }
|
|
66
|
+
while (s.length > w) { out.push(s.slice(0, w)); s = s.slice(w); }
|
|
67
|
+
out.push(s);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Terminal size, live across resizes (falls back to 80×24 off-TTY). */
|
|
73
|
+
function useTerminalSize() {
|
|
74
|
+
const { stdout } = useStdout();
|
|
75
|
+
const size = () => ({ columns: stdout?.columns || 80, rows: stdout?.rows || 24 });
|
|
76
|
+
const [dim, setDim] = useState(size);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!stdout) return undefined;
|
|
79
|
+
const onResize = () => setDim(size());
|
|
80
|
+
stdout.on("resize", onResize);
|
|
81
|
+
return () => stdout.off("resize", onResize);
|
|
82
|
+
}, [stdout]);
|
|
83
|
+
return dim;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The Ink app over one createSession sink. Owns only VIEW state — the session
|
|
87
|
+
* (focus, files, records) lives in the sink, exactly as in the readline shell. */
|
|
88
|
+
export function App({ session }) {
|
|
89
|
+
const { exit } = useApp();
|
|
90
|
+
const { columns, rows } = useTerminalSize();
|
|
91
|
+
const [items, setItems] = useState([]);
|
|
92
|
+
const [input, setInput] = useState("");
|
|
93
|
+
const [prompt, setPrompt] = useState(session.promptFor());
|
|
94
|
+
const [busy, setBusy] = useState(false);
|
|
95
|
+
|
|
96
|
+
const submit = async (line) => {
|
|
97
|
+
if (line === "/exit") { exit(); return; }
|
|
98
|
+
setBusy(true);
|
|
99
|
+
const echoedAt = prompt; // the prompt the question was typed at, kept in the echo
|
|
100
|
+
try {
|
|
101
|
+
const { answer, end, prompt: nextPrompt } = await session.turn(line);
|
|
102
|
+
setItems((prev) => appendTurn(prev, { prompt: echoedAt, query: line, answer }));
|
|
103
|
+
setPrompt(nextPrompt);
|
|
104
|
+
if (end) { exit(); return; } // a conversational "bye" — same clean end as /exit
|
|
105
|
+
} finally {
|
|
106
|
+
setBusy(false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const trySubmit = (raw) => {
|
|
111
|
+
if (busy) return; // one turn at a time — the engine is deterministic and fast
|
|
112
|
+
const line = String(raw).trim();
|
|
113
|
+
setInput("");
|
|
114
|
+
if (line) void submit(line);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
useInput((ch, key) => {
|
|
118
|
+
if (key.return) { trySubmit(input); return; }
|
|
119
|
+
if (key.backspace || key.delete) { setInput((s) => s.slice(0, -1)); return; }
|
|
120
|
+
if (key.ctrl && ch === "u") { setInput(""); return; }
|
|
121
|
+
if (key.ctrl || key.meta || key.escape || key.tab || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) return;
|
|
122
|
+
if (!ch) return;
|
|
123
|
+
// A PASTED chunk arrives as one multi-char event; a newline inside it means
|
|
124
|
+
// "submit this line" (one line per turn — the readline shell's per-line read).
|
|
125
|
+
const nl = ch.search(/[\r\n]/);
|
|
126
|
+
if (nl === -1) { setInput((s) => s + ch); return; }
|
|
127
|
+
trySubmit(input + ch.slice(0, nl));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// The pane's line budget: full height minus the input line and the status bar.
|
|
131
|
+
const paneRows = Math.max(1, rows - 2);
|
|
132
|
+
const banner = { kind: "a", text: session.bannerLines.join("\n") }; // one block, one separator
|
|
133
|
+
const allLines = wrapLines(transcriptLines([banner, ...items]), columns);
|
|
134
|
+
const visible = allLines.slice(-paneRows);
|
|
135
|
+
|
|
136
|
+
return h(Box, { flexDirection: "column", height: rows, width: columns },
|
|
137
|
+
h(Box, { flexDirection: "column", height: paneRows, overflow: "hidden" },
|
|
138
|
+
...visible.map((line, i) =>
|
|
139
|
+
h(Text, { key: `l${i}`, wrap: "truncate-end" }, line === "" ? " " : line)),
|
|
140
|
+
),
|
|
141
|
+
h(Box, { height: 1 },
|
|
142
|
+
h(Text, { wrap: "truncate-end" },
|
|
143
|
+
h(Text, { bold: true }, prompt),
|
|
144
|
+
input,
|
|
145
|
+
busy ? h(Text, { dimColor: true }, "…") : h(Text, { inverse: true }, " "),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
h(Box, { height: 1 },
|
|
149
|
+
h(Text, { dimColor: true, wrap: "truncate-end" },
|
|
150
|
+
statusText(session), " · /help commands · /exit leaves"),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Run the full-screen TUI over a fresh session sink: alternate screen in, Ink
|
|
156
|
+
* render, wait for exit (Ctrl+C / /exit / bye), alternate screen out, then the
|
|
157
|
+
* SAME session close the readline shell performs (end lines, final upsert —
|
|
158
|
+
* which also triggers the memory fold — stream flush). Returns
|
|
159
|
+
* { logFile, sidecarFile, turns } exactly like runChat. */
|
|
160
|
+
export async function runTui({ repoPath, stdout = process.stdout, stdin = process.stdin, ...sessionOpts } = {}) {
|
|
161
|
+
const session = await createSession({ repoPath, ...sessionOpts });
|
|
162
|
+
stdout.write("\x1b[?1049h\x1b[H"); // alternate screen buffer + home — a clean full-screen canvas
|
|
163
|
+
const app = render(h(App, { session }), { stdout, stdin, exitOnCtrlC: true });
|
|
164
|
+
try {
|
|
165
|
+
await app.waitUntilExit();
|
|
166
|
+
} finally {
|
|
167
|
+
app.unmount();
|
|
168
|
+
stdout.write("\x1b[?1049l"); // restore the primary screen (shell scrollback intact)
|
|
169
|
+
await session.close();
|
|
170
|
+
stdout.write(`session ended — log ${session.logFile}\n`);
|
|
171
|
+
}
|
|
172
|
+
return { logFile: session.logFile, sidecarFile: session.sidecarFile, turns: session.turns };
|
|
173
|
+
}
|
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
|
-
});
|