@polycode-projects/the-mechanical-code-talker 1.5.5 → 1.8.3
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 +123 -14
- package/ROADMAP.md +233 -1392
- package/bin/tmct.mjs +479 -98
- package/corpus/README.md +3 -0
- package/corpus/generated/README.md +43 -0
- package/corpus/generated/ace-surface-variants.jsonl +17 -0
- package/corpus/generated/manifest.json +9 -0
- package/corpus/tier2/generate.mjs +14668 -0
- package/corpus/tier2/human-examples-large.jsonl +1928 -0
- package/corpus/tier2/human-examples-medium.jsonl +356 -0
- package/corpus/tier2/human-examples.jsonl +120 -0
- package/corpus/tier2/human-large.jsonl +12001 -0
- package/corpus/tier2/human-medium.jsonl +944 -0
- package/corpus/tier2/human.jsonl +664 -0
- package/corpus/tier2/manifest.json +42 -0
- package/package.json +14 -8
- package/src/answer-variants.json +47 -0
- package/src/answer-variants.mjs +67 -0
- package/src/ask-browser-entry.mjs +34 -0
- package/src/ask-browser.bundle.js +5095 -0
- package/src/ask-vocab.mjs +93 -8
- package/src/ask.mjs +451 -49
- package/src/chat.mjs +1273 -137
- package/src/cli-args.mjs +164 -0
- package/src/codegraph.mjs +170 -32
- package/src/extensions.mjs +100 -19
- package/src/grammar/ace.mjs +85 -3
- package/src/grammar/lexicon-core.json +9531 -63
- package/src/grammar/lexicon.mjs +58 -8
- package/src/graph-merge.mjs +114 -0
- package/src/index.mjs +14 -0
- package/src/init.mjs +40 -14
- package/src/interpret/normalize.mjs +75 -1
- package/src/interpret/strategies/grammar.mjs +10 -0
- package/src/interpret/strategies/keywords.mjs +20 -0
- package/src/interpret/strategies/noise-strip.mjs +73 -4
- package/src/memory/core.mjs +466 -8
- package/src/router/goal-reasoner.mjs +41 -7
- package/src/router/guardrail.mjs +37 -7
- package/src/router/resolver.mjs +50 -4
- package/src/sessions.mjs +5 -1
- package/src/source.mjs +54 -1
- package/src/syllogise.mjs +398 -27
- package/src/toml-config.mjs +13 -4
- package/src/viz.mjs +541 -0
package/bin/tmct.mjs
CHANGED
|
@@ -34,6 +34,9 @@ software repository. No model calls; no codebase index of its own.
|
|
|
34
34
|
Usage:
|
|
35
35
|
tmct interactive chat (the headline surface)
|
|
36
36
|
tmct chat [--repo <abs>] chat over a specific repo's graph
|
|
37
|
+
[--graph <path>] explicit graph file (repeatable — multiple graphs merge;
|
|
38
|
+
see src/graph-merge.mjs); wins over --repo/TMCT_GRAPH_FILE/tmct.toml
|
|
39
|
+
[--config <path>] an alternate tmct.toml location (a file or a directory)
|
|
37
40
|
[--ephemeral] read the graph but write nothing back (demo/read-only)
|
|
38
41
|
[--narrate] start with narrate mode on — a verbose, developer-facing
|
|
39
42
|
trace of decision points/matched pattern/results/goal per
|
|
@@ -42,31 +45,57 @@ Usage:
|
|
|
42
45
|
[--plain] force the plain readline shell (the default when
|
|
43
46
|
stdin/stdout is not a terminal)
|
|
44
47
|
tmct memory [--repo <abs>] what tmct remembers: facts, utterances, sessions,
|
|
45
|
-
[--
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
[--
|
|
49
|
-
|
|
48
|
+
[--config <path>] folded blocks (the /memory chat command, from the shell)
|
|
49
|
+
[--verbose]
|
|
50
|
+
tmct init [--repo <abs>] initialize a repo for tmct (default: cwd): .tmct/,
|
|
51
|
+
[--force] tmct.toml, tier-1 corpus seed, provenance record
|
|
52
|
+
[--corpus <id|path>] also seed a corpus — a tier-2 manifest id (aws|python|java|
|
|
53
|
+
general) or a jsonl file path — opt-in, offline, $0
|
|
54
|
+
[--ontology <name|path>] activate+seed an ontology bundle (a recognized name or a path)
|
|
55
|
+
[--lexicon <name|path>] activate a lexicon bundle (recognized name or a path;
|
|
56
|
+
merged read-time, never seeded — see mergedLexiconExtra)
|
|
57
|
+
[--graph <path>] set graph_file/graph_files in tmct.toml (repeatable)
|
|
58
|
+
[--config <path>] write to an alternate tmct.toml location
|
|
50
59
|
[--detect] suggest a tier-2 corpus from the repo's manifests
|
|
51
60
|
(pyproject.toml → python, pom.xml → java); never seeds unasked
|
|
52
61
|
[--with-persona <name>] write an explicit [extensions]/[bias] preset into tmct.toml
|
|
53
62
|
("code" — today's implicit default, made explicit)
|
|
63
|
+
[--persona-size <medium|large>] grow the default "human" persona's fact count
|
|
64
|
+
beyond Small (the default): "medium" activates
|
|
65
|
+
human-medium.jsonl (~1,608 facts total), "large" also
|
|
66
|
+
activates human-large.jsonl (~13,600 facts total,
|
|
67
|
+
with genuine multi-hop hypernym chains) — additive
|
|
68
|
+
size tiers of the SAME bundle, not separate personas
|
|
69
|
+
tmct import [--repo <abs>] activate+seed into an ALREADY-initialized repo (any
|
|
70
|
+
[--corpus <id|path>] combination of these flags in one call). --graph is a
|
|
71
|
+
[--ontology <name|path>] DIFFERENT operation from the others: it APPENDS to
|
|
72
|
+
[--lexicon <name|path>] tmct.toml's graph_files array (multi-graph growth),
|
|
73
|
+
[--graph <path>] never an extensions-bundle activation.
|
|
74
|
+
[--config <path>]
|
|
54
75
|
tmct extend --validate <dir> validate a third-party extension pack's declared
|
|
55
|
-
|
|
76
|
+
[--config <path>] resources (corpus/lexicon/templates) before activating
|
|
56
77
|
it in any repo's tmct.toml; exits non-zero on failure
|
|
57
78
|
tmct syllogise [--repo <abs>] speculative inference (offline maintenance job): forward-
|
|
58
79
|
[--depth <n>] [--budget <n>] chain the memory's rdfs:subClassOf closure, materialising
|
|
59
|
-
|
|
80
|
+
[--config <path>] bounded, low-trust, retractable entailed facts (never on the chat path)
|
|
81
|
+
tmct viz [--repo <abs>] write one self-contained, navigable HTML file rendering the
|
|
82
|
+
[--focus <id>] memory graph: pan/zoom, click a node for its label/class/
|
|
83
|
+
[--output <path>] timestamps. Seeds from the most recently created individual
|
|
84
|
+
[--config <path>] by default; --output defaults to graph.html in the cwd.
|
|
60
85
|
tmct serve [--repo <abs>] run the Anthropic Messages API-compatible endpoint
|
|
61
86
|
[--host <h>] [--port <n>] (POST /v1/messages) over the graph — a deterministic,
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
[--graph <path>] no-LLM "model" a tool-loop client can call; $0 usage.
|
|
88
|
+
[--config <path>] Defaults: host 127.0.0.1, port 8787. Ctrl+C to stop.
|
|
64
89
|
tmct cli <tool> '{…}' invoke a graph tool directly (carry-over, de-emphasized)
|
|
65
90
|
tmct cli digest '{…}' architecture map + per-module context bundles
|
|
66
91
|
tmct --help show this help
|
|
67
92
|
|
|
68
93
|
On a terminal, chat opens the full-screen TUI; piped input gets the plain shell.
|
|
69
94
|
In chat: /help lists slash-commands; /exit leaves. Session log → <repo>/.tmct/session-<id>.log.
|
|
95
|
+
|
|
96
|
+
Shared graph-path precedence (chat/serve; see src/cli-args.mjs): --graph flag(s) >
|
|
97
|
+
TMCT_GRAPH_FILE env > tmct.toml graph_file/graph_files > --repo-derived
|
|
98
|
+
<repo>/.tmct/graph.json > git-root/cwd default.
|
|
70
99
|
`;
|
|
71
100
|
|
|
72
101
|
const argv = process.argv.slice(2);
|
|
@@ -274,6 +303,207 @@ async function runCliMode() {
|
|
|
274
303
|
process.exit(2);
|
|
275
304
|
}
|
|
276
305
|
|
|
306
|
+
// ---- shared `init`/`import` pluggable-input helpers ---------------------------
|
|
307
|
+
//
|
|
308
|
+
// Both `tmct init --corpus/--ontology/--lexicon` and the new `tmct import` verb
|
|
309
|
+
// funnel through the SAME two-step seam: resolvePluggableInput (name/id/path →
|
|
310
|
+
// a resolved descriptor) then activatePluggableInput (write tmct.toml + seed via
|
|
311
|
+
// src/extensions.mjs's unified loop) — one seam instead of three near-duplicate
|
|
312
|
+
// hand-rolled call sites.
|
|
313
|
+
|
|
314
|
+
/** Read `repoRoot`'s current tmct.toml (if any) into the shape init.mjs's
|
|
315
|
+
* renderTomlConfig/defaultConfig expect, so a caller can add ONE key and
|
|
316
|
+
* write it straight back without losing every other already-written key.
|
|
317
|
+
* Mirrors `tmct init --corpus`'s original inline config-merge exactly. */
|
|
318
|
+
async function readConfigForRewrite(repoRoot) {
|
|
319
|
+
const { defaultConfig } = await import("../src/init.mjs");
|
|
320
|
+
const { loadTomlConfig } = await import("../src/toml-config.mjs");
|
|
321
|
+
const raw = await loadTomlConfig(repoRoot);
|
|
322
|
+
const cfg = { ...defaultConfig() };
|
|
323
|
+
if (raw?.graph_file !== undefined) cfg.graphFile = String(raw.graph_file);
|
|
324
|
+
if (Array.isArray(raw?.graph_files)) cfg.graphFiles = raw.graph_files.slice();
|
|
325
|
+
if (raw?.corpus?.tier !== undefined) cfg.corpus = { tier: raw.corpus.tier };
|
|
326
|
+
if (raw?.seed) {
|
|
327
|
+
cfg.seed = { ...cfg.seed };
|
|
328
|
+
if (raw.seed.enabled !== undefined) cfg.seed.enabled = Boolean(raw.seed.enabled);
|
|
329
|
+
if (raw.seed.limit !== undefined) cfg.seed.limit = Number(raw.seed.limit);
|
|
330
|
+
}
|
|
331
|
+
if (raw?.extensions !== undefined) cfg.extensions = raw.extensions;
|
|
332
|
+
if (raw?.bias !== undefined) cfg.bias = raw.bias;
|
|
333
|
+
return { raw, cfg };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function writeConfig(repoRoot, cfg) {
|
|
337
|
+
const { renderTomlConfig, CONFIG_FILE } = await import("../src/init.mjs");
|
|
338
|
+
const { writeFile } = await import("node:fs/promises");
|
|
339
|
+
const { join: joinPath } = await import("node:path");
|
|
340
|
+
await writeFile(joinPath(repoRoot, CONFIG_FILE), renderTomlConfig(cfg));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** realpathSync `absPath`, tolerating that it (or some trailing segment of
|
|
344
|
+
* it) may not exist yet: walks up to the longest EXISTING ancestor,
|
|
345
|
+
* realpath's THAT, then rejoins the non-existent remainder untouched.
|
|
346
|
+
* Equivalent to `fs.realpathSync(absPath)` when `absPath` already exists.
|
|
347
|
+
* Needed because `--graph <path>` etc. deliberately name not-yet-written
|
|
348
|
+
* files (a fresh graph a future indexer will create) — so we can't just
|
|
349
|
+
* realpathSync() the candidate path outright. */
|
|
350
|
+
function realpathTolerant(absPath, { realpathSync, dirname, basename, join: joinPath }) {
|
|
351
|
+
let dir = absPath;
|
|
352
|
+
const remainder = [];
|
|
353
|
+
while (true) {
|
|
354
|
+
try {
|
|
355
|
+
const real = realpathSync(dir);
|
|
356
|
+
return remainder.length ? joinPath(real, ...remainder) : real;
|
|
357
|
+
} catch {
|
|
358
|
+
const parent = dirname(dir);
|
|
359
|
+
if (parent === dir) return absPath; // hit the filesystem root; nothing on this path exists — give up as-is
|
|
360
|
+
remainder.unshift(basename(dir));
|
|
361
|
+
dir = parent;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Render `p` (resolved against `repoRoot` if not already absolute) as a
|
|
367
|
+
* path relative to `repoRoot` for tmct.toml — whose own header documents
|
|
368
|
+
* paths as "relative to this file" — falling back to the absolute form
|
|
369
|
+
* when `p` lands outside `repoRoot`. Both sides are realpath'd (tolerantly
|
|
370
|
+
* — `p` need not exist yet) before computing relative(), so the
|
|
371
|
+
* inside/outside-repoRoot decision doesn't depend on whether the OS
|
|
372
|
+
* happens to route a path through a symlink: macOS's os.tmpdir() resolves
|
|
373
|
+
* under /var/folders/..., and /var is itself a symlink to /private/var, so
|
|
374
|
+
* a spawned child's process.cwd() (kernel-realpath'd) and a test's raw
|
|
375
|
+
* mkdtemp() string (unresolved) can name the same real directory while
|
|
376
|
+
* disagreeing textually — Linux's /tmp has no such symlink, which is why
|
|
377
|
+
* this only showed up on CI. Comparing realpath'd forms on both sides
|
|
378
|
+
* makes the decision symlink-invariant on every platform. */
|
|
379
|
+
async function repoRelative(repoRoot, p) {
|
|
380
|
+
const { resolve: resolvePath, relative, dirname, basename, join: joinPath, isAbsolute } = await import("node:path");
|
|
381
|
+
const { realpathSync } = await import("node:fs");
|
|
382
|
+
const abs = isAbsolute(p) ? p : resolvePath(repoRoot, p);
|
|
383
|
+
const helpers = { realpathSync, dirname, basename, join: joinPath };
|
|
384
|
+
const repoReal = realpathTolerant(repoRoot, helpers);
|
|
385
|
+
const absReal = realpathTolerant(abs, helpers);
|
|
386
|
+
const rel = relative(repoReal, absReal);
|
|
387
|
+
return rel && !rel.startsWith("..") ? rel : abs;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Resolve a `--corpus`/`--ontology`/`--lexicon` value to something
|
|
391
|
+
* activatePluggableInput can act on: either a RECOGNIZED name (a
|
|
392
|
+
* BUILTIN_EXTENSIONS entry of the matching kind; for `--corpus` specifically,
|
|
393
|
+
* also a tier-2 manifest id like "aws" — today's `--corpus <id>` contract,
|
|
394
|
+
* preserved byte-for-byte) or a filesystem PATH, to be declared as a brand
|
|
395
|
+
* new `[extensions.<slug>]` entry. Throws the same clear "unknown --corpus"
|
|
396
|
+
* error the original tier-2-only implementation gave (naming the available
|
|
397
|
+
* ids), generalized to name a checked file path too. Validates before any
|
|
398
|
+
* disk write — callers resolve every pluggable input BEFORE calling
|
|
399
|
+
* initRepo, so a bad name/path touches nothing. */
|
|
400
|
+
async function resolvePluggableInput(kind, nameOrPath, { repoRoot }) {
|
|
401
|
+
const { BUILTIN_EXTENSIONS } = await import("../src/extensions.mjs");
|
|
402
|
+
if (Object.prototype.hasOwnProperty.call(BUILTIN_EXTENSIONS, nameOrPath) && BUILTIN_EXTENSIONS[nameOrPath].kind === kind) {
|
|
403
|
+
return { known: true, name: nameOrPath };
|
|
404
|
+
}
|
|
405
|
+
let manifestIds = null;
|
|
406
|
+
if (kind === "corpus") {
|
|
407
|
+
const { readFile } = await import("node:fs/promises");
|
|
408
|
+
const { TIER2_MANIFEST_FILE } = await import("../src/corpus/conceptnet.mjs");
|
|
409
|
+
try {
|
|
410
|
+
const manifest = JSON.parse(await readFile(TIER2_MANIFEST_FILE, "utf8"));
|
|
411
|
+
const corpuses = manifest.corpuses || [];
|
|
412
|
+
manifestIds = corpuses.map((c) => c.id);
|
|
413
|
+
const manifestEntry = corpuses.find((c) => c.id === nameOrPath);
|
|
414
|
+
if (manifestEntry) return { known: true, name: `tier2-${manifestEntry.id}`, manifestEntry };
|
|
415
|
+
} catch { /* manifest unreadable — fall through to path resolution */ }
|
|
416
|
+
}
|
|
417
|
+
const { access } = await import("node:fs/promises");
|
|
418
|
+
const { resolve: resolvePath, basename, isAbsolute } = await import("node:path");
|
|
419
|
+
const abs = isAbsolute(nameOrPath) ? nameOrPath : resolvePath(repoRoot, nameOrPath);
|
|
420
|
+
try {
|
|
421
|
+
await access(abs);
|
|
422
|
+
} catch {
|
|
423
|
+
if (manifestIds) {
|
|
424
|
+
throw new Error(`unknown --corpus "${nameOrPath}". Available tier-2 corpuses: ${manifestIds.join(", ")}.`);
|
|
425
|
+
}
|
|
426
|
+
throw new Error(`unknown --${kind} "${nameOrPath}" — not a recognized name, and no file found at ${abs}`);
|
|
427
|
+
}
|
|
428
|
+
const slug = basename(abs).replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]+/g, "-").toLowerCase() || kind;
|
|
429
|
+
return { known: false, name: slug, kind, path: abs };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Activate one resolvePluggableInput() result in `repoRoot`'s tmct.toml and —
|
|
433
|
+
* for corpus/ontology-kind entries (or a pack-kind entry with a corpusPath) —
|
|
434
|
+
* seed it via the SAME unified loop every other bundle uses
|
|
435
|
+
* (src/extensions.mjs's seedActiveCorpusEntries). A lexicon/templates-kind
|
|
436
|
+
* entry only activates (merged read-time via mergedLexiconExtra, never
|
|
437
|
+
* seeded — matching today's behavior for those kinds). Returns a
|
|
438
|
+
* human-readable status line for stdout. */
|
|
439
|
+
async function activatePluggableInput(repoRoot, resolved) {
|
|
440
|
+
const { raw, cfg } = await readConfigForRewrite(repoRoot);
|
|
441
|
+
const name = resolved.name;
|
|
442
|
+
const existingEntry = raw?.extensions?.[name] || {};
|
|
443
|
+
const newEntry = { ...existingEntry, active: true };
|
|
444
|
+
if (!resolved.known) {
|
|
445
|
+
newEntry.kind = resolved.kind;
|
|
446
|
+
const pathKey = resolved.kind === "ontology" ? "ontology_path"
|
|
447
|
+
: resolved.kind === "lexicon" ? "lexicon_path"
|
|
448
|
+
: resolved.kind === "templates" ? "templates_path"
|
|
449
|
+
: "corpus_path";
|
|
450
|
+
newEntry[pathKey] = await repoRelative(repoRoot, resolved.path);
|
|
451
|
+
}
|
|
452
|
+
cfg.extensions = { ...(cfg.extensions || {}), [name]: newEntry };
|
|
453
|
+
await writeConfig(repoRoot, cfg);
|
|
454
|
+
|
|
455
|
+
const { resolveExtensions, seedActiveCorpusEntries } = await import("../src/extensions.mjs");
|
|
456
|
+
const { entries } = await resolveExtensions(repoRoot);
|
|
457
|
+
const entry = entries.get(name);
|
|
458
|
+
const seedable = entry.kind === "corpus" || entry.kind === "ontology" || (entry.kind === "pack" && entry.corpusPath);
|
|
459
|
+
if (!seedable) {
|
|
460
|
+
return `activated "${name}" (${entry.kind}) in tmct.toml — no corpus facts to seed for this kind.\n`;
|
|
461
|
+
}
|
|
462
|
+
const { perBundle } = await seedActiveCorpusEntries(repoRoot, new Map([[name, entry]]));
|
|
463
|
+
const seeded = perBundle[name];
|
|
464
|
+
if (seeded.error) {
|
|
465
|
+
throw new Error(`could not seed "${name}" — ${seeded.error}`);
|
|
466
|
+
}
|
|
467
|
+
if (resolved.manifestEntry) {
|
|
468
|
+
// Preserve the ORIGINAL `--corpus <tier2-id>` wording byte-for-byte
|
|
469
|
+
// (test/init-cli.test.mjs asserts on this exact shape).
|
|
470
|
+
return `seeded tier-2 corpus "${resolved.manifestEntry.id}" (${resolved.manifestEntry.kind}) — ${seeded.appended} fact(s) added`
|
|
471
|
+
+ `${seeded.skipped ? `, ${seeded.skipped} already present` : ""}. Source: corpus/tier2/${resolved.manifestEntry.file} (${resolved.manifestEntry.license}). `
|
|
472
|
+
+ `Activated in tmct.toml — future \`tmct init\`/chat sessions seed it automatically.\n`;
|
|
473
|
+
}
|
|
474
|
+
return `seeded "${name}" (${entry.kind}) — ${seeded.appended} fact(s) added`
|
|
475
|
+
+ `${seeded.skipped ? `, ${seeded.skipped} already present` : ""}. `
|
|
476
|
+
+ `Activated in tmct.toml — future \`tmct init\`/chat sessions seed it automatically.\n`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** `--graph <path>` (repeatable) APPENDS to `repoRoot`'s tmct.toml
|
|
480
|
+
* `graph_files` array — a DIFFERENT, purely additive operation from
|
|
481
|
+
* activatePluggableInput (no extensions-bundle activation). Each path is
|
|
482
|
+
* sanity-checked (readable, valid JSON, has an "individuals" array) before
|
|
483
|
+
* being recorded — `tmct import` targets an ALREADY-initialized repo, so the
|
|
484
|
+
* named graph is expected to genuinely exist. */
|
|
485
|
+
async function appendGraphFiles(repoRoot, graphPaths) {
|
|
486
|
+
const { readFile } = await import("node:fs/promises");
|
|
487
|
+
const { resolve: resolvePath } = await import("node:path");
|
|
488
|
+
for (const p of graphPaths) {
|
|
489
|
+
const abs = resolvePath(repoRoot, p);
|
|
490
|
+
let text;
|
|
491
|
+
try { text = await readFile(abs, "utf8"); }
|
|
492
|
+
catch (e) { throw new Error(`--graph ${p}: cannot read ${abs} (${e?.message || e})`); }
|
|
493
|
+
let payload;
|
|
494
|
+
try { payload = JSON.parse(text); }
|
|
495
|
+
catch (e) { throw new Error(`--graph ${p}: ${abs} is not valid JSON (${e?.message || e})`); }
|
|
496
|
+
if (typeof payload !== "object" || payload === null || !Array.isArray(payload.individuals)) {
|
|
497
|
+
throw new Error(`--graph ${p}: ${abs} doesn't look like a graph entities payload (missing an "individuals" array)`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const { cfg } = await readConfigForRewrite(repoRoot);
|
|
501
|
+
const additions = [];
|
|
502
|
+
for (const p of graphPaths) additions.push(await repoRelative(repoRoot, p));
|
|
503
|
+
cfg.graphFiles = [...(cfg.graphFiles || []), ...additions];
|
|
504
|
+
await writeConfig(repoRoot, cfg);
|
|
505
|
+
}
|
|
506
|
+
|
|
277
507
|
async function main() {
|
|
278
508
|
const mode = process.argv[2];
|
|
279
509
|
|
|
@@ -291,30 +521,42 @@ async function main() {
|
|
|
291
521
|
// session with the verbose developer/debug narrate mode already on. Default
|
|
292
522
|
// OFF; `/narrate on`/`/narrate off` also toggles it mid-session.
|
|
293
523
|
const narrate = rest.includes("--narrate");
|
|
524
|
+
// `--graph <path>` (repeatable) / `--config <path>` (src/cli-args.mjs): threaded
|
|
525
|
+
// through as the new top tier of createSession's graph-resolution order (above
|
|
526
|
+
// TMCT_GRAPH_FILE — see chat.mjs's own docblock at createSession). Omitted from
|
|
527
|
+
// the call entirely when absent, so a plain `tmct chat [--repo]` invocation is
|
|
528
|
+
// byte-identical to before these flags existed.
|
|
529
|
+
const { repeatedFlag, strFlag } = await import("../src/cli-args.mjs");
|
|
530
|
+
const graphPaths = repeatedFlag(rest, ["--graph"]);
|
|
531
|
+
const configPath = strFlag(rest, ["--config"]);
|
|
532
|
+
const extra = {};
|
|
533
|
+
if (graphPaths.length) extra.graphPaths = graphPaths;
|
|
534
|
+
if (configPath) extra.configPath = configPath;
|
|
294
535
|
// The shell gate: a real terminal gets the full-screen Ink TUI; `--plain` or a
|
|
295
536
|
// non-TTY stream (pipes, scripts, the test suite) gets the readline shell. Both
|
|
296
537
|
// drive the same createSession sink — only the drawing differs.
|
|
297
538
|
const plain = rest.includes("--plain") || !process.stdin.isTTY || !process.stdout.isTTY;
|
|
298
539
|
if (plain) {
|
|
299
540
|
const { runChat } = await import("../src/chat.mjs");
|
|
300
|
-
await runChat({ repoPath, ephemeral, narrate });
|
|
541
|
+
await runChat({ repoPath, ephemeral, narrate, ...extra });
|
|
301
542
|
} else {
|
|
302
543
|
const { runTui } = await import("../src/tui/app.mjs");
|
|
303
|
-
await runTui({ repoPath, ephemeral, narrate });
|
|
544
|
+
await runTui({ repoPath, ephemeral, narrate, ...extra });
|
|
304
545
|
}
|
|
305
546
|
return;
|
|
306
547
|
}
|
|
307
548
|
|
|
308
549
|
if (mode === "memory") {
|
|
309
550
|
// `tmct memory` — the /memory chat command from the shell: same renderer
|
|
310
|
-
// (src/memory/inspect.mjs)
|
|
551
|
+
// (src/memory/inspect.mjs). Repo resolution now goes through the shared
|
|
552
|
+
// resolveRuntimeConfig (src/cli-args.mjs) — --repo > git root > cwd, same
|
|
553
|
+
// as before, plus a (currently inert but accepted) `--config` for symmetry
|
|
554
|
+
// with every other subcommand. No `--graph`: memory reads no code graph.
|
|
311
555
|
const rest = process.argv.slice(3);
|
|
312
|
-
const i = rest.indexOf("--repo");
|
|
313
|
-
const repoPath = i !== -1 ? rest[i + 1] : undefined;
|
|
314
556
|
const verbose = rest.includes("--verbose") || rest.includes("-v");
|
|
315
|
-
const {
|
|
557
|
+
const { resolveRuntimeConfig } = await import("../src/cli-args.mjs");
|
|
316
558
|
const { inspectMemory } = await import("../src/memory/inspect.mjs");
|
|
317
|
-
const repo =
|
|
559
|
+
const { repo } = await resolveRuntimeConfig({ argv: rest });
|
|
318
560
|
process.stdout.write(await inspectMemory(repo, { verbose }) + "\n");
|
|
319
561
|
return;
|
|
320
562
|
}
|
|
@@ -330,34 +572,40 @@ async function main() {
|
|
|
330
572
|
// explicitly asked via `--corpus <id>`. The `--detect` auto-detect is a
|
|
331
573
|
// documented STUB: it inspects the repo's manifests (pyproject.toml → python,
|
|
332
574
|
// pom.xml → java) and SUGGESTS the matching corpus, but never seeds it unasked.
|
|
575
|
+
//
|
|
576
|
+
// `--repo <abs>` (NEW): init used to always hardcode process.cwd() — the only
|
|
577
|
+
// subcommand without a --repo flag. It now takes one like every other
|
|
578
|
+
// subcommand, defaulting to cwd exactly as before when absent.
|
|
333
579
|
const rest = process.argv.slice(3);
|
|
334
|
-
const {
|
|
335
|
-
const {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
580
|
+
const { strFlag, repeatedFlag } = await import("../src/cli-args.mjs");
|
|
581
|
+
const { resolve: resolvePath } = await import("node:path");
|
|
582
|
+
const { initRepo, PERSONA_PRESETS } = await import("../src/init.mjs");
|
|
583
|
+
|
|
584
|
+
const repoFlag = strFlag(rest, ["--repo"]);
|
|
585
|
+
const repoRoot = repoFlag ? resolvePath(process.cwd(), repoFlag) : process.cwd();
|
|
586
|
+
|
|
587
|
+
const corpusVal = strFlag(rest, ["--corpus"]);
|
|
588
|
+
const ontologyVal = strFlag(rest, ["--ontology"]);
|
|
589
|
+
const lexiconVal = strFlag(rest, ["--lexicon"]);
|
|
590
|
+
const graphFlags = repeatedFlag(rest, ["--graph"]);
|
|
591
|
+
|
|
592
|
+
// Resolve + validate EVERY pluggable input BEFORE touching disk — mirrors
|
|
593
|
+
// `--with-persona`'s own "validate before scaffolding" discipline, and the
|
|
594
|
+
// original `--corpus`'s "unknown id touches nothing" contract.
|
|
595
|
+
let corpusResolved = null, ontologyResolved = null, lexiconResolved = null;
|
|
596
|
+
try {
|
|
597
|
+
if (corpusVal) corpusResolved = await resolvePluggableInput("corpus", corpusVal, { repoRoot });
|
|
598
|
+
if (ontologyVal) ontologyResolved = await resolvePluggableInput("ontology", ontologyVal, { repoRoot });
|
|
599
|
+
if (lexiconVal) lexiconResolved = await resolvePluggableInput("lexicon", lexiconVal, { repoRoot });
|
|
600
|
+
} catch (e) {
|
|
601
|
+
process.stderr.write(`tmct init: ${e?.message || e}\n`);
|
|
602
|
+
process.exit(2);
|
|
354
603
|
}
|
|
355
604
|
|
|
356
605
|
// `--with-persona <name>` (Part 7): resolve + validate BEFORE touching
|
|
357
606
|
// disk, mirroring `--corpus`'s own unknown-id error handling — a bad
|
|
358
607
|
// persona name never scaffolds anything.
|
|
359
|
-
const
|
|
360
|
-
const personaName = pi !== -1 ? rest[pi + 1] : undefined;
|
|
608
|
+
const personaName = strFlag(rest, ["--with-persona"]);
|
|
361
609
|
let personaPreset = null;
|
|
362
610
|
if (personaName) {
|
|
363
611
|
if (!Object.prototype.hasOwnProperty.call(PERSONA_PRESETS, personaName)) {
|
|
@@ -368,60 +616,87 @@ async function main() {
|
|
|
368
616
|
personaPreset = PERSONA_PRESETS[personaName];
|
|
369
617
|
}
|
|
370
618
|
|
|
371
|
-
|
|
619
|
+
// `--persona-size <medium|large>` (PLAN_SEED.md §3): Small/Medium/Large are
|
|
620
|
+
// SIZES of the one `human` bundle, not separate corpus ids — human.jsonl
|
|
621
|
+
// (Small, the default) stays exactly as-is; human-medium.jsonl/
|
|
622
|
+
// human-large.jsonl hold ONLY the facts each size adds beyond the previous
|
|
623
|
+
// one (built by scripts/build-persona-tiers.mjs). "medium" activates just
|
|
624
|
+
// human-medium; "large" activates BOTH human-medium AND human-large (Large
|
|
625
|
+
// is Medium's facts plus its own — both incremental bundles must be active
|
|
626
|
+
// to reach the full ~13,600-fact total). Reuses the EXACT SAME
|
|
627
|
+
// resolvePluggableInput/activatePluggableInput seam `--corpus <id>` already
|
|
628
|
+
// uses (human-medium/human-large are ordinary `kind: "corpus"`
|
|
629
|
+
// BUILTIN_EXTENSIONS entries, recognized directly, no manifest needed) —
|
|
630
|
+
// this is sugar over "activate these specific corpus ids", not a new
|
|
631
|
+
// activation mechanism. Works whether the repo is fresh (this same
|
|
632
|
+
// `tmct init` call) or already initialized (a later re-run), exactly like
|
|
633
|
+
// `--corpus` does.
|
|
634
|
+
const PERSONA_SIZE_BUNDLES = { medium: ["human-medium"], large: ["human-medium", "human-large"] };
|
|
635
|
+
const personaSizeVal = strFlag(rest, ["--persona-size"]);
|
|
636
|
+
let personaSizeResolved = [];
|
|
637
|
+
if (personaSizeVal) {
|
|
638
|
+
if (!Object.prototype.hasOwnProperty.call(PERSONA_SIZE_BUNDLES, personaSizeVal)) {
|
|
639
|
+
const sizes = Object.keys(PERSONA_SIZE_BUNDLES).join(", ");
|
|
640
|
+
process.stderr.write(`tmct init: unknown --persona-size "${personaSizeVal}". Available sizes: ${sizes}.\n`);
|
|
641
|
+
process.exit(2);
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
for (const id of PERSONA_SIZE_BUNDLES[personaSizeVal]) {
|
|
645
|
+
personaSizeResolved.push(await resolvePluggableInput("corpus", id, { repoRoot }));
|
|
646
|
+
}
|
|
647
|
+
} catch (e) {
|
|
648
|
+
process.stderr.write(`tmct init: ${e?.message || e}\n`);
|
|
649
|
+
process.exit(2);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const res = await initRepo(repoRoot, { force: rest.includes("--force"), persona: personaPreset });
|
|
372
654
|
process.stdout.write(res.message + "\n");
|
|
373
655
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
cfg.seed = { ...cfg.seed };
|
|
390
|
-
if (raw.seed.enabled !== undefined) cfg.seed.enabled = Boolean(raw.seed.enabled);
|
|
391
|
-
if (raw.seed.limit !== undefined) cfg.seed.limit = Number(raw.seed.limit);
|
|
392
|
-
}
|
|
393
|
-
cfg.extensions = { ...(raw?.extensions || {}), [extName]: { ...(raw?.extensions?.[extName] || {}), active: true } };
|
|
394
|
-
if (raw?.bias !== undefined) cfg.bias = raw.bias;
|
|
395
|
-
const { writeFile } = await import("node:fs/promises");
|
|
396
|
-
const { join } = await import("node:path");
|
|
397
|
-
await writeFile(join(process.cwd(), CONFIG_FILE), renderTomlConfig(cfg));
|
|
398
|
-
|
|
399
|
-
// Seed it now too — through the SAME unified corpus loader every other
|
|
400
|
-
// bundle goes through (src/extensions.mjs), not a bespoke seedMemory call.
|
|
401
|
-
const { resolveExtensions, seedActiveCorpusEntries } = await import("../src/extensions.mjs");
|
|
402
|
-
const { entries } = await resolveExtensions(process.cwd());
|
|
403
|
-
const entry = entries.get(extName);
|
|
404
|
-
const { perBundle } = await seedActiveCorpusEntries(process.cwd(), new Map([[extName, entry]]));
|
|
405
|
-
const seeded = perBundle[extName];
|
|
406
|
-
if (seeded.error) {
|
|
407
|
-
process.stderr.write(`tmct init: could not seed tier-2 corpus "${manifestEntry.id}" — ${seeded.error}\n`);
|
|
656
|
+
// `--corpus`/`--ontology`/`--lexicon` now mean "activate this bundle and
|
|
657
|
+
// PERSIST that into tmct.toml" — so a second `tmct init` (or the next chat
|
|
658
|
+
// bootstrap) remembers the choice, unlike the old ad hoc path, which had
|
|
659
|
+
// to be repeated every time. (For a tier-2 --corpus id this changes the
|
|
660
|
+
// provenance tag from the old colon-separated "corpus:tier2:<id>" to the
|
|
661
|
+
// hyphenated "corpus:tier2-<id>" — a deliberate, low-risk rename predating
|
|
662
|
+
// this batch; nothing in chat.mjs's runtime logic keys on the old string.)
|
|
663
|
+
let anyActivation = false;
|
|
664
|
+
for (const resolved of [corpusResolved, ontologyResolved, lexiconResolved, ...personaSizeResolved]) {
|
|
665
|
+
if (!resolved) continue;
|
|
666
|
+
anyActivation = true;
|
|
667
|
+
try {
|
|
668
|
+
process.stdout.write(await activatePluggableInput(repoRoot, resolved));
|
|
669
|
+
} catch (e) {
|
|
670
|
+
process.stderr.write(`tmct init: ${e?.message || e}\n`);
|
|
408
671
|
process.exit(1);
|
|
409
672
|
}
|
|
410
|
-
process.stdout.write(
|
|
411
|
-
`seeded tier-2 corpus "${manifestEntry.id}" (${manifestEntry.kind}) — ${seeded.appended} fact(s) added`
|
|
412
|
-
+ `${seeded.skipped ? `, ${seeded.skipped} already present` : ""}. Source: corpus/tier2/${manifestEntry.file} (${manifestEntry.license}). `
|
|
413
|
-
+ `Activated in tmct.toml — future \`tmct init\`/chat sessions seed it automatically.\n`,
|
|
414
|
-
);
|
|
415
|
-
return;
|
|
416
673
|
}
|
|
417
674
|
|
|
675
|
+
// `--graph <path>` (repeatable): SETS (not appends — that's `tmct import`'s
|
|
676
|
+
// job) the fresh scaffold's graph pointer(s). No existence check: like
|
|
677
|
+
// init's own default graph_file, this just names where a future indexer
|
|
678
|
+
// will write, and a brand new repo's graph typically doesn't exist yet.
|
|
679
|
+
if (graphFlags.length) {
|
|
680
|
+
const { cfg } = await readConfigForRewrite(repoRoot);
|
|
681
|
+
if (graphFlags.length === 1) {
|
|
682
|
+
cfg.graphFile = await repoRelative(repoRoot, graphFlags[0]);
|
|
683
|
+
delete cfg.graphFiles;
|
|
684
|
+
} else {
|
|
685
|
+
cfg.graphFiles = await Promise.all(graphFlags.map((p) => repoRelative(repoRoot, p)));
|
|
686
|
+
}
|
|
687
|
+
await writeConfig(repoRoot, cfg);
|
|
688
|
+
process.stdout.write(`graph path${graphFlags.length > 1 ? "s" : ""} set in tmct.toml: ${graphFlags.join(", ")}\n`);
|
|
689
|
+
anyActivation = true;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (anyActivation) return;
|
|
693
|
+
|
|
418
694
|
if (rest.includes("--detect")) {
|
|
419
695
|
// AUTO-DETECT STUB (documented, non-seeding): map a build manifest to the
|
|
420
696
|
// tier-2 corpus that fits, and tell the operator how to add it. Kept a stub on
|
|
421
697
|
// purpose — the $0/offline default never expands the corpus without an ask.
|
|
422
698
|
const { access } = await import("node:fs/promises");
|
|
423
|
-
const
|
|
424
|
-
const has = (f) => access(join(process.cwd(), f)).then(() => true, () => false);
|
|
699
|
+
const has = (f) => access(resolvePath(repoRoot, f)).then(() => true, () => false);
|
|
425
700
|
const DETECT = [["pyproject.toml", "python"], ["pom.xml", "java"]];
|
|
426
701
|
const found = [];
|
|
427
702
|
for (const [file, id] of DETECT) if (await has(file)) found.push([file, id]);
|
|
@@ -437,6 +712,61 @@ async function main() {
|
|
|
437
712
|
return;
|
|
438
713
|
}
|
|
439
714
|
|
|
715
|
+
if (mode === "import") {
|
|
716
|
+
// `tmct import` — activate+seed into an ALREADY-initialized repo, reusing
|
|
717
|
+
// the SAME resolvePluggableInput/activatePluggableInput seam `init`'s own
|
|
718
|
+
// --corpus/--ontology/--lexicon flags use. `--graph` is a DIFFERENT, purely
|
|
719
|
+
// additive operation (appendGraphFiles) — it grows tmct.toml's graph_files
|
|
720
|
+
// array, never activates an extensions bundle.
|
|
721
|
+
const rest = process.argv.slice(3);
|
|
722
|
+
const { strFlag, repeatedFlag } = await import("../src/cli-args.mjs");
|
|
723
|
+
const { resolve: resolvePath } = await import("node:path");
|
|
724
|
+
|
|
725
|
+
const repoFlag = strFlag(rest, ["--repo"]);
|
|
726
|
+
const repoRoot = repoFlag ? resolvePath(process.cwd(), repoFlag) : process.cwd();
|
|
727
|
+
|
|
728
|
+
const corpusVal = strFlag(rest, ["--corpus"]);
|
|
729
|
+
const ontologyVal = strFlag(rest, ["--ontology"]);
|
|
730
|
+
const lexiconVal = strFlag(rest, ["--lexicon"]);
|
|
731
|
+
const graphFlags = repeatedFlag(rest, ["--graph"]);
|
|
732
|
+
|
|
733
|
+
if (!corpusVal && !ontologyVal && !lexiconVal && !graphFlags.length) {
|
|
734
|
+
process.stderr.write("tmct import: needs at least one of --corpus/--ontology/--lexicon/--graph\n");
|
|
735
|
+
process.exit(2);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let corpusResolved = null, ontologyResolved = null, lexiconResolved = null;
|
|
739
|
+
try {
|
|
740
|
+
if (corpusVal) corpusResolved = await resolvePluggableInput("corpus", corpusVal, { repoRoot });
|
|
741
|
+
if (ontologyVal) ontologyResolved = await resolvePluggableInput("ontology", ontologyVal, { repoRoot });
|
|
742
|
+
if (lexiconVal) lexiconResolved = await resolvePluggableInput("lexicon", lexiconVal, { repoRoot });
|
|
743
|
+
} catch (e) {
|
|
744
|
+
process.stderr.write(`tmct import: ${e?.message || e}\n`);
|
|
745
|
+
process.exit(2);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
for (const resolved of [corpusResolved, ontologyResolved, lexiconResolved]) {
|
|
749
|
+
if (!resolved) continue;
|
|
750
|
+
try {
|
|
751
|
+
process.stdout.write(await activatePluggableInput(repoRoot, resolved));
|
|
752
|
+
} catch (e) {
|
|
753
|
+
process.stderr.write(`tmct import: ${e?.message || e}\n`);
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (graphFlags.length) {
|
|
759
|
+
try {
|
|
760
|
+
await appendGraphFiles(repoRoot, graphFlags);
|
|
761
|
+
process.stdout.write(`added ${graphFlags.length} graph file(s) to tmct.toml's graph_files: ${graphFlags.join(", ")}\n`);
|
|
762
|
+
} catch (e) {
|
|
763
|
+
process.stderr.write(`tmct import: ${e?.message || e}\n`);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
440
770
|
if (mode === "extend") {
|
|
441
771
|
// `tmct extend --validate <dir>` — validate a THIRD-PARTY extension pack
|
|
442
772
|
// (the shape a package like seonix/marginalia ships) BEFORE it's activated
|
|
@@ -456,11 +786,26 @@ async function main() {
|
|
|
456
786
|
process.exit(2);
|
|
457
787
|
}
|
|
458
788
|
const { resolve: resolvePath } = await import("node:path");
|
|
789
|
+
const { strFlag } = await import("../src/cli-args.mjs");
|
|
459
790
|
const target = resolvePath(process.cwd(), dirArg);
|
|
791
|
+
// `--config <path>` (optional): an alternate tmct.toml to validate against,
|
|
792
|
+
// INSTEAD of `<target>/tmct.toml` — `target` still anchors every resource
|
|
793
|
+
// path (a pure validator stays that way; this never mutates anything).
|
|
794
|
+
// Accepts either a FILE (the tmct.toml itself) or a DIRECTORY (look for
|
|
795
|
+
// tmct.toml under it), same as every other subcommand's --config.
|
|
796
|
+
const configFlag = strFlag(rest, ["--config"]);
|
|
797
|
+
let configFile;
|
|
798
|
+
if (configFlag) {
|
|
799
|
+
const { stat } = await import("node:fs/promises");
|
|
800
|
+
const abs = resolvePath(process.cwd(), configFlag);
|
|
801
|
+
let isDir = false;
|
|
802
|
+
try { isDir = (await stat(abs)).isDirectory(); } catch { /* missing path — treat as a file target */ }
|
|
803
|
+
configFile = isDir ? resolvePath(abs, "tmct.toml") : abs;
|
|
804
|
+
}
|
|
460
805
|
const { resolveExtensions, BUILTIN_EXTENSIONS, validateExtensionPack } = await import("../src/extensions.mjs");
|
|
461
806
|
let entries;
|
|
462
807
|
try {
|
|
463
|
-
({ entries } = await resolveExtensions(target));
|
|
808
|
+
({ entries } = await resolveExtensions(target, configFile ? { configFile } : {}));
|
|
464
809
|
} catch (e) {
|
|
465
810
|
process.stderr.write(`tmct extend --validate: ${e?.message || e}\n`);
|
|
466
811
|
process.exit(1);
|
|
@@ -490,18 +835,18 @@ async function main() {
|
|
|
490
835
|
if (mode === "syllogise") {
|
|
491
836
|
// `tmct syllogise` — the explicit speculative-inference batch (never on the chat
|
|
492
837
|
// hot path): forward-chain the memory's rdfs:subClassOf closure into bounded,
|
|
493
|
-
// low-trust, retractable entailed facts. Same repo resolution as `memory
|
|
838
|
+
// low-trust, retractable entailed facts. Same repo resolution as `memory` —
|
|
839
|
+
// resolveRuntimeConfig (src/cli-args.mjs): --repo > git root > cwd. Also
|
|
840
|
+
// accepts `--config` for symmetry (syllogise reads no code graph either).
|
|
494
841
|
const rest = process.argv.slice(3);
|
|
495
|
-
const i = rest.indexOf("--repo");
|
|
496
|
-
const repoPath = i !== -1 ? rest[i + 1] : undefined;
|
|
497
842
|
const numFlag = (name, dflt) => {
|
|
498
843
|
const j = rest.indexOf(name);
|
|
499
844
|
const v = j !== -1 ? Number(rest[j + 1]) : NaN;
|
|
500
845
|
return Number.isFinite(v) ? v : dflt;
|
|
501
846
|
};
|
|
502
|
-
const {
|
|
847
|
+
const { resolveRuntimeConfig } = await import("../src/cli-args.mjs");
|
|
503
848
|
const { syllogise } = await import("../src/syllogise.mjs");
|
|
504
|
-
const repo =
|
|
849
|
+
const { repo } = await resolveRuntimeConfig({ argv: rest });
|
|
505
850
|
const res = await syllogise(repo, { depth: numFlag("--depth", 32), budget: numFlag("--budget", 50) });
|
|
506
851
|
process.stdout.write(
|
|
507
852
|
`tmct syllogise — derived ${res.count} entailed fact(s) (subClassOf closure, depth ${res.depth}, budget ${res.budget})`
|
|
@@ -510,6 +855,36 @@ async function main() {
|
|
|
510
855
|
return;
|
|
511
856
|
}
|
|
512
857
|
|
|
858
|
+
if (mode === "viz") {
|
|
859
|
+
// `tmct viz` — one self-contained, navigable HTML file rendering the
|
|
860
|
+
// memory graph (PLAN_BREADTH_FIRST_NLU.md §5, PLAN_VIZ.md's design):
|
|
861
|
+
// pan/zoom, click-a-node, a concentric ring layout keyed on hop with a
|
|
862
|
+
// depth/age falloff. Same repo resolution as `memory`/`syllogise` —
|
|
863
|
+
// resolveRuntimeConfig: --repo > git root > cwd.
|
|
864
|
+
const rest = process.argv.slice(3);
|
|
865
|
+
const { strFlag, resolveRuntimeConfig } = await import("../src/cli-args.mjs");
|
|
866
|
+
const { computeVizGraph, renderVizHtml, readAskBundle } = await import("../src/viz.mjs");
|
|
867
|
+
const { writeFile } = await import("node:fs/promises");
|
|
868
|
+
const { resolve } = await import("node:path");
|
|
869
|
+
const focus = strFlag(rest, ["--focus"]);
|
|
870
|
+
const outPath = resolve(process.cwd(), strFlag(rest, ["--output", "--out"], "graph.html"));
|
|
871
|
+
const { repo } = await resolveRuntimeConfig({ argv: rest });
|
|
872
|
+
const vizGraph = await computeVizGraph(repo, focus ? { focus } : {});
|
|
873
|
+
// The embedded "Ask the graph" chat panel — the real ask.mjs engine,
|
|
874
|
+
// bundled for the browser (scripts/build-ask-bundle.mjs's checked-in
|
|
875
|
+
// output). readAskBundle() never throws; an empty string renders a
|
|
876
|
+
// graph-only page with an honest "chat unavailable" note instead of a
|
|
877
|
+
// broken one (e.g. a fresh checkout before the bundle's first build).
|
|
878
|
+
const askBundle = await readAskBundle();
|
|
879
|
+
const html = renderVizHtml({ ...vizGraph, askBundle });
|
|
880
|
+
await writeFile(outPath, html, "utf8");
|
|
881
|
+
process.stdout.write(
|
|
882
|
+
`tmct viz — wrote ${vizGraph.nodes.length} node(s), ${vizGraph.edges.length} edge(s) to ${outPath}`
|
|
883
|
+
+ `${askBundle ? " (with the embedded ask-the-graph chat panel)" : " (no chat panel — run `npm run build:ask-bundle` first)"}\n`,
|
|
884
|
+
);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
513
888
|
if (mode === "serve") {
|
|
514
889
|
// `tmct serve` — the Phase-A capability-router interface: an Anthropic
|
|
515
890
|
// Messages API-compatible HTTP endpoint (POST /v1/messages) over the graph.
|
|
@@ -521,8 +896,11 @@ async function main() {
|
|
|
521
896
|
process.stdout.write(
|
|
522
897
|
"tmct serve — Anthropic Messages API-compatible endpoint (POST /v1/messages)\n\n" +
|
|
523
898
|
"Usage:\n" +
|
|
524
|
-
" tmct serve [--repo <abs>] [--host <h>] [--port <n>]\n\n" +
|
|
525
|
-
" --repo <abs> target a repo's graph (<abs>/.tmct/graph.json); default: cwd
|
|
899
|
+
" tmct serve [--repo <abs>] [--graph <path>] [--config <path>] [--host <h>] [--port <n>]\n\n" +
|
|
900
|
+
" --repo <abs> target a repo's graph (<abs>/.tmct/graph.json); default: git root/cwd\n" +
|
|
901
|
+
" --graph <path> explicit graph file (repeatable — multiple graphs merge; wins\n" +
|
|
902
|
+
" over --repo/TMCT_GRAPH_FILE/tmct.toml)\n" +
|
|
903
|
+
" --config <path> an alternate tmct.toml location (a file or a directory)\n" +
|
|
526
904
|
" --host <h> bind address (default 127.0.0.1)\n" +
|
|
527
905
|
" --port <n> TCP port (default 8787; 0 picks an ephemeral port)\n\n" +
|
|
528
906
|
"Request: { model, messages:[...], tools:[...], max_tokens, system? }\n" +
|
|
@@ -531,16 +909,19 @@ async function main() {
|
|
|
531
909
|
);
|
|
532
910
|
return;
|
|
533
911
|
}
|
|
534
|
-
const strFlag
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const portRaw = strFlag("--port", undefined);
|
|
912
|
+
const { strFlag, resolveRuntimeConfig } = await import("../src/cli-args.mjs");
|
|
913
|
+
const host = strFlag(rest, ["--host"], "127.0.0.1");
|
|
914
|
+
const portRaw = strFlag(rest, ["--port"]);
|
|
538
915
|
const port = portRaw !== undefined && Number.isFinite(Number(portRaw)) ? Number(portRaw) : 8787;
|
|
539
|
-
const { join } = await import("node:path");
|
|
540
916
|
const { startServer } = await import("../src/server-http.mjs");
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
917
|
+
// Graph-path precedence (src/cli-args.mjs, shared with `chat`): --graph
|
|
918
|
+
// flag(s) > TMCT_GRAPH_FILE env > tmct.toml graph_file/graph_files >
|
|
919
|
+
// --repo-derived <repo>/.tmct/graph.json > git-root/cwd default. This
|
|
920
|
+
// REPLACES serve's old cwd-only default (loadConfig had no git-root
|
|
921
|
+
// fallback) with the same git-root-aware default every other subcommand
|
|
922
|
+
// now shares — a deliberate, documented unification, not a regression.
|
|
923
|
+
const { config } = await resolveRuntimeConfig({ argv: rest });
|
|
924
|
+
const srv = await startServer({ config, host, port });
|
|
544
925
|
process.stdout.write(
|
|
545
926
|
`tmct serve — Anthropic Messages API at ${srv.url}/v1/messages (POST) — ` +
|
|
546
927
|
`graph ${srv.config.graphFile} — usage billed $0 — Ctrl+C to stop\n`,
|
|
@@ -559,7 +940,7 @@ async function main() {
|
|
|
559
940
|
// An unknown mode gets the instructive usage line and exit 2. (A bare invocation
|
|
560
941
|
// never lands here — the argv splice above rewrote it to `chat`.)
|
|
561
942
|
process.stderr.write(`tmct: unknown invocation "${process.argv.slice(2).join(" ")}". ` +
|
|
562
|
-
"Use `cli digest …`, `cli <tool>
|
|
943
|
+
"Use `chat`, `memory`, `init`, `import`, `extend --validate`, `syllogise`, `serve`, `cli digest …`, or `cli <tool> …`.\n");
|
|
563
944
|
process.exit(2);
|
|
564
945
|
}
|
|
565
946
|
|