TOOLS
uses node:path
import { SLOT_WAIT_LINE } from "#codebase/tcmts/cli/budget"; import { sessionDirFor } from "#codebase/tcmts/cli/codebase"; import { background, briefOutput, fence, GUIDE, jobIds, jobsDirFor, LANG, lastJobId, mergeSummary, message, messageArgs, names, next, patchImports, readJob, schema, stopJob, str, strs, viewMarkdown, } from "#codebase/tcmts/cli/mcp"; import type { Job, JobState, Tool } from "#codebase/tcmts/cli/mcp"; import fs from "node:fs"; import path from "node:path"; export const TOOLS: Tool[] = [ { name: "tcm_status", description: "Where things stand: the branch you're on (and what it has that main doesn't: in a session, your work, until it lands), the codebase by namespace (with how many definitions each), scratch files holding unsaved work (and what saving them would do), what needs fixing (todo), other branches with work main doesn't have, and whether a mirror's files are in step. Start here.", inputSchema: schema({}), argv: () => ["status"], }, { name: "tcm_find", description: 'Search the codebase: words match names, docs and signatures; a query starting with ":" is a type, matched up to renaming, argument order, async and generality (":(xs: T[]) => T").', inputSchema: schema({ query: { type: "string", description: 'words, or ":<type>"' } }, [ "query", ]), argv: (a) => [ "find", ...(str(a.query).trim().startsWith(":") ? [str(a.query).trim()] : str(a.query).split(/\s+/).filter(Boolean)), ], }, { name: "tcm_search", description: "Find text in the codebase's code and pages, as find in files does: each line with it, by definition, numbered as tcm_view's `lines` counts them. The text is as written, or a regular expression written /…/, as tcm_view's grep takes it. tcm_find searches names, docs and signatures instead.", inputSchema: schema( { text: { type: "string", description: "what to find: text, as written (`a|b` is that text), or a regular expression written /…/ (`/a|b/`, `/x/i`: its case as its flags say)", }, regex: { type: "boolean", description: "the old way: all the text is a regular expression, without slashes (write /…/ instead)", }, case: { type: "boolean", description: "match case" }, word: { type: "boolean", description: "whole words only" }, under: { type: "string", description: "only definitions under this namespace, or this definition", }, limit: { type: "number", description: "lines to show at most (default 200)" }, }, ["text"], ), argv: (a) => [ "search", ...(a.regex ? ["--regex"] : []), ...(a.case ? ["--case"] : []), ...(a.word ? ["--word"] : []), ...(a.under ? ["--under", str(a.under)] : []), ...(a.limit ? ["--limit", String(a.limit)] : []), // (after `--`, so text that looks like a flag is looked for) "--", str(a.text), ], }, { name: "tcm_view", description: "Show definitions as code, with current names, where each runs (server, browser, anywhere) and its hash. For a large one, `lines` or `grep` shows only some of it, numbered.", inputSchema: schema( { names, lines: { type: "string", description: "only these lines: 120, or 120-180" }, grep: { type: "string", description: "only the lines with this text, as written (`a|b` is that text), or matching a regular expression written /…/ (`/a|b/`, `/x/i`); and two either side", }, }, ["names"], ), run: async (a, ctx) => { const r = await ctx.tcm([ "view", ...strs(a.names), ...(a.lines ? ["--lines", str(a.lines)] : []), ...(a.grep ? ["--grep", str(a.grep)] : []), ]); // what it found is shown, with what it didn't: an error only if it found nothing const text = viewMarkdown(r.out); return r.code === 0 || text !== r.out ? { text } : { text: r.out, isError: true }; }, }, { name: "tcm_deps", description: "What a definition uses (codebase definitions, packages).", inputSchema: schema({ name: { type: "string" } }, ["name"]), argv: (a) => ["deps", str(a.name)], }, { name: "tcm_dependents", description: "What uses a definition: what a change to it re-hashes.", inputSchema: schema({ name: { type: "string" } }, ["name"]), argv: (a) => ["dependents", str(a.name)], }, { name: "tcm_docs", description: "A definition's documentation, with its examples evaluated.", inputSchema: schema({ name: { type: "string" } }, ["name"]), argv: (a) => ["docs", str(a.name)], }, { name: "tcm_edit", description: "Write definitions to a scratch file for editing (default scratch.ts, or scratch.tsx for JSX), with lines saying where each goes back. A scratch file holding unsaved work keeps it: the definitions are added to it. A conflicted name (after a merge) gets a file of its own, named after it (lib.k.ts), holding its two sides merged from the base, with conflict markers (lines of their own: <<<<<<< ours, ||||||| base, =======, >>>>>>> theirs) only where both changed the same lines; it says what each side changed and where, and whether the merge typechecks. Conflicted names given together (one using another) share a file. Settle the markers, take them out, and tcm_update that file resolves the conflict. Returns the file's path and contents: edit it with your file tools, then tcm_check and tcm_update.", inputSchema: schema( { names, file: { type: "string", description: "the scratch file: a name alone (scratch.ts) is in a session's own directory (as tcm_status says), a path is relative to the project", }, force: { type: "boolean", description: "replace what a scratch file holds, even definitions that aren't saved, instead of adding to it", }, }, ["names"], ), run: async (a, ctx) => { const argv = (file?: string) => [ "edit", ...strs(a.names), ...(file ? ["--file", file] : []), ...(a.force ? ["--force"] : []), ]; const r = await ctx.tcm(argv(a.file ? str(a.file) : undefined)); if (r.code !== 0) return { text: r.out, isError: true }; // where it went: the rest of the line that says so (a path can have spaces in it), whole const said = /^((?:wrote|added) \S+(?:, \S+)* to )(.+?)((?: \((?:took out|saved with) .*\))?)$/m.exec( r.out, ) ?? /^(\S+(?:, \S+)* (?:is|are) in )(.+)( already)$/m.exec(r.out); const file = said?.[2] ?? (a.file ? str(a.file) : undefined); const abs = file && path.resolve(ctx.root, file); if (!abs || !fs.existsSync(abs)) return { text: `${r.out}\n\nEdit the file, then tcm_check and tcm_update.` }; const head = said ? r.out.replace(said[0], `${said[1]}\`${abs}\`${said[3]}`) : `${r.out}\n\n\`${abs}\``; const body = fence( fs.readFileSync(abs, "utf8").trimEnd(), LANG[path.extname(abs).slice(1)] ?? "", ); // (a file other than the default, as a conflict's is: which one to check and save) const named = /^scratch\.tsx?$/.test(path.basename(abs)) ? "" : ` with file "${ctx.branch && path.dirname(abs) === sessionDirFor(ctx.root, ctx.branch) ? path.basename(abs) : path.relative(ctx.root, abs)}"`; // (a file kept as written: the namespace it's saved into) const into = /--into (\S+?)`/.exec(said?.[3] ?? "")?.[1]; return { text: `${head}\n\n${body}\n\nEdit it, then tcm_check and tcm_update${named}${into ? ` and into "${into}"` : ""}.`, }; }, }, { name: "tcm_patch", description: "Change a definition by find-and-replace on its code, as tcm_view shows it (current names), and save it as tcm_update does: checked, its dependents re-hashed, the tests it reaches named. Each `old` must be in it exactly once (or set `all`). Imports follow the code: use a name and it's imported, stop and it goes; one that can't be found that way (an alias, say) is given in `imports`. Nothing is saved if an edit doesn't apply or the result doesn't typecheck (code that doesn't parse: its first syntax error alone). Inside a template literal, old and new are as the source writes them, escapes and all (in JSON, a backslash is doubled): an old found only with its escapes taken the other way (a line break for backslash-n, a bare backtick for an escaped one) is matched so when that finds it once, and said; a new that would end or split the literal (a bare backtick or ${) is refused. The quick way to make a small change; tcm_edit is for bigger ones. To change several definitions together (a signature and its callers, or code and the page documenting it), give `patches` instead of `name` and `edits`: they're saved as one update and one history entry, each edit matched against its definition as tcm_view shows it.", inputSchema: schema( { patches: { type: "array", description: "several definitions (code, or pages and other files kept as written), each with its edits, saved together (instead of name and edits)", items: schema( { name: { type: "string" }, edits: { type: "array", items: schema({ old: { type: "string" }, new: { type: "string" } }, [ "old", "new", ]), }, all: { type: "boolean" }, imports: patchImports, }, ["name", "edits"], ), }, name: { type: "string", description: "the definition" }, edits: { type: "array", items: schema({ old: { type: "string" }, new: { type: "string" } }, ["old", "new"]), description: "what to find, and what replaces it, in order", }, all: { type: "boolean", description: "replace every place each old text is" }, imports: patchImports, message, }, [], ), argv: (a) => Array.isArray(a.patches) ? ["patch", "--patches", JSON.stringify(a.patches), ...messageArgs(a)] : [ "patch", ...messageArgs(a), str(a.name), "--edits", JSON.stringify(a.edits ?? []), ...(a.all ? ["--all"] : []), ...(Array.isArray(a.imports) && a.imports.length ? ["--imports", JSON.stringify(a.imports)] : []), ], }, { name: "tcm_at", description: "Which definition is at a line of a file (a scratch file, a definition's document (its branch's views: .tcm/src, or a session's in its own directory), a generated module in .tcm/cache as stack traces name them, a mirrored file); without a line, every definition in the file with its lines.", inputSchema: schema( { place: { type: "string", description: "<file>[:<line>], relative to the project" } }, ["place"], ), argv: (a) => ["at", str(a.place)], }, { name: "tcm_check", description: "Typecheck a scratch file and say what saving it would do (new, update, identical, resolves: a conflicted name it settles); watch expressions and tests in it are run. Names it uses and doesn't import are imported first, as saving would, written into the file (the lines said); an import nothing uses is left, and said (saving takes it out).", inputSchema: schema({ file: { type: "string", description: "the scratch file (default scratch.ts): a name alone is in a session's own directory, a path is relative to the project", }, into: { type: "string", description: "the namespace it goes in (default: its `// tcm: namespace` line); for a file kept as written, the namespace holding it", }, }), argv: (a) => [ "check", ...(a.file ? [str(a.file)] : []), ...(a.into ? ["--into", str(a.into)] : []), ], }, { name: "tcm_update", description: "Save a scratch file: names it uses and doesn't import are imported, and imports nothing in it uses taken out (as tcm_patch does); new definitions are added, changed ones replace the old, and everything depending on them is re-hashed. Dependents that stop typechecking go to tcm_todo. A definition saved at a conflicted name resolves the conflict to it (what used either side's is re-derived once): no tcm_resolve first; history names it so (resolve lib.k (merged)). Once it's saved, the file is deleted (what it held is in the codebase), unless `keep`.", inputSchema: schema({ file: { type: "string", description: "the scratch file (default scratch.ts): a name alone is in a session's own directory, a path is relative to the project", }, into: { type: "string", description: "the namespace it goes in (default: its `// tcm: namespace` line); for a file kept as written (as tcm_edit says), the namespace holding it", }, keep: { type: "boolean", description: "keep the file once it's saved" }, message, }), argv: (a) => [ "update", ...(a.file ? [str(a.file)] : []), ...(a.into ? ["--into", str(a.into)] : []), ...(a.keep ? [] : ["--clear"]), ...messageArgs(a), ], }, { name: "tcm_add", description: 'Save code given here, without writing a scratch file: TypeScript, as a scratch file holds it (imports from "#codebase/…" and packages, definitions), into `namespace`. New definitions only, unless `update` (then it replaces the ones of the same names, as tcm_update does).', inputSchema: schema( { code: { type: "string", description: "the definitions, with their imports (a name used and not imported is imported, as tcm_patch does)", }, namespace: { type: "string", description: "where they go (or a `// tcm: namespace a.b` line in the code)", }, update: { type: "boolean", description: "replace definitions of the same names" }, jsx: { type: "boolean", description: "the code is TSX" }, message, }, ["code"], ), run: async (a, ctx) => { // a scratch file of its own, where scratch files go (a session's own directory), gone afterwards const dir = ctx.branch ? sessionDirFor(ctx.root, ctx.branch) : ctx.root; fs.mkdirSync(dir, { recursive: true }); const file = path.join(dir, `add-${process.pid}-${Date.now()}.${a.jsx ? "tsx" : "ts"}`); fs.writeFileSync(file, str(a.code).endsWith("\n") ? str(a.code) : `${str(a.code)}\n`); try { const argv = [ a.update ? "update" : "add", file, ...(a.namespace ? ["--into", str(a.namespace)] : []), ...messageArgs(a), ]; const r = await ctx.tcm(argv); return { text: [r.out.replaceAll(file, "(the code)"), next(argv, r.out, r.code)] .filter(Boolean) .join("\n\n"), isError: r.code !== 0, }; } finally { fs.rmSync(file, { force: true }); } }, }, { name: "tcm_todo", description: "What needs fixing: conflicted names (each definition labelled with its side of the merge, ours or theirs), and dependents that stopped typechecking (with the error: the first 20, as fixing those often fixes the rest, unless `all`).", inputSchema: schema({ all: { type: "boolean", description: "every dependent that needs fixing, not the first 20" }, }), argv: (a) => ["todo", ...(a.all ? ["--all"] : [])], }, { name: "tcm_resolve", description: 'Settle a conflicted name by keeping one of its definitions whole: `side` "ours" (this branch\'s side of the merge) or "theirs" (the merged branch\'s), as tcm_todo labels them, or a `hash`; what used the others is re-derived against it. To combine the two instead, tcm_edit the name (its sides merged, conflict markers where they overlap) and tcm_update it; tcm_cli ["diff", "<name>"] shows the base against each side.', inputSchema: schema( { name: { type: "string", description: "the conflicted name" }, side: { type: "string", enum: ["ours", "theirs"], description: "keep this side's definition", }, hash: { type: "string", description: "or the #hash to keep" }, message, }, ["name"], ), argv: (a) => [ "resolve", str(a.name), ...(a.hash ? [str(a.hash)] : []), ...(a.side === "ours" || a.side === "theirs" ? [`--${a.side}`] : []), ...messageArgs(a), ], }, { name: "tcm_test", description: "Run tests (definitions named test…, or under a tests namespace), optionally only some (by name or namespace), and the suites packages declare (test files running a package's command). Passes are cached by hash: only tests whose code or dependencies changed run again, and only suites that ran code that changed; a test that failed always runs again. It names the tests that ran (and, when you name tests or one fails, the cached ones).", inputSchema: schema({ names: { type: "array", items: { type: "string" }, description: "only these tests, or the tests under these namespaces", }, fresh: { type: "boolean", description: "run everything again, ignoring cached results" }, definitions: { type: "boolean", description: "only the test definitions, not the suites (which can take minutes when much changed)", }, suites: { type: "boolean", description: "only the suites, not the test definitions" }, bin: { type: "object", additionalProperties: { type: "string" }, description: 'run the suites against another build of their command: command → its file, as {"tcm": "<a tcm.js>"}, or for tcm a kept build\'s name (`tcm builds` lists them), as {"tcm": "previous"} or {"tcm": "known-good"}: to see an end-to-end test fail on the tcm from before a fix (a build without the code a test ran runs it again)', }, background, }), argv: (a) => [ "test", ...(Array.isArray(a.names) ? strs(a.names) : []), ...(a.fresh ? ["--fresh"] : []), ...(a.definitions ? ["--definitions"] : []), ...(a.suites ? ["--suites"] : []), ...(a.bin && typeof a.bin === "object" && Object.keys(a.bin).length ? [ "--bin", Object.entries(a.bin as Record<string, unknown>) .map(([cmd, file]) => `${cmd}=${String(file)}`) .join(","), ] : []), ], }, { name: "tcm_run", description: "Run a definition: a function is called with the arguments; what it prints, and a number it returns as the exit code.", inputSchema: schema( { name: { type: "string" }, args: { type: "array", items: { type: "string" } }, background }, ["name"], ), argv: (a) => ["run", str(a.name), "--", ...strs(a.args)], }, { name: "tcm_job", description: "How a command started in the background (`background: true`) is going: what it has printed so far (a test run: how many have finished, and what failed), or how it ended. `wait` waits for it to end, up to that many seconds, rather than asking again and again; `stop` stops it. Without an id, this session's jobs. A job is a process of its own, its output kept in the session's directory: when this server is started again (a new tcm), the job runs on, and the new server follows it.", inputSchema: schema({ id: { type: "number" }, wait: { type: "number", description: "seconds to wait for it to end (at most 600), answering as soon as it does", }, stop: { type: "boolean", description: "stop it, and what it runs: a run you no longer want (it says how far it got)", }, }), run: async (a, ctx) => { const dir = jobsDirFor(ctx.root, ctx.branch); const here = ctx.branch ? "in this session" : "here"; const secs = (from: number, to = Date.now()) => `${Math.round((to - from) / 1000)}s`; const whatOf = (j: Job) => j.tool ?? `tcm ${j.argv.join(" ")}`; // (one started by a server that has stopped since, tcm replaced: it ran on, and is followed all the same) const earlier = (j: Job) => j.server !== process.pid ? ", started by an earlier server" : ""; if (a.id === undefined) { const all = jobIds(dir).flatMap((id) => readJob(dir, id) ?? []); if (!all.length) return { text: `no jobs ${here} (a tool with \`background: true\` starts one)` }; return { text: all .map( (j) => `${j.id} ${whatOf(j)} ${j.done ? (j.done.stopped ? `stopped after ${secs(j.started, j.done.at)}` : `ended (${j.done.code === 0 ? "ok" : `exit ${j.done.code}`}) after ${secs(j.started, j.done.at)}`) : j.lost ? "gone without saying how it ended" : `running for ${secs(j.started)}`}${earlier(j)}`, ) .join("\n"), }; } const id = Number(a.id); let job = readJob(dir, id); if (!job) { const last = lastJobId(dir); return { text: Number.isInteger(id) && id >= 1 && id <= last ? `job ${id}'s files are gone: a finished job's are cleared away a day after it ends (tcm_job without an id lists the jobs ${here})` : `no job ${String(a.id)} ${here}: ${last ? `its jobs are numbered 1 to ${last} (tcm_job without an id lists those kept)` : "it has none (a tool with `background: true` starts one)"}`, isError: true, }; } const what = whatOf(job); const tail = (s: string) => s.split("\n").length > 60 ? `…\n${s.split("\n").slice(-60).join("\n")}` : s; const log = path.relative(ctx.root, job.log); const printed = (j: JobState) => `it had printed (all of it in ${log}):\n\n${tail(j.out.trim()) || "(nothing)"}`; if (a.stop === true && !job.done && !job.lost) { job = (await stopJob(dir, id)) ?? job; return { text: `stopped job ${id} (${what}) after ${secs(job.started)}, and what it ran${earlier(job)}; ${printed(job)}`, }; } // waited for, if asked: answered when it ends const until = Date.now() + Math.min(Math.max(Number(a.wait) || 0, 0), 600) * 1000; while (!job.done && !job.lost && Date.now() < until) { await new Promise((r) => setTimeout(r, 500)); job = readJob(dir, id) ?? job; } if (job.lost) return { text: `job ${id} (${what})${earlier(job)} is gone without saying how it ended: its process (pid ${job.pid}) was stopped from outside, or with the machine. ${printed(job)}`, isError: true, }; if (!job.done) { // a test run so far: how many finished, and what failed, not a line per test const lines = job.out.split("\n"); const passed = lines.filter((l) => /^\s*✓ /.test(l)).length; const failed = lines.filter((l) => /^\s*✗ /.test(l)).length; // (and what it waits on, if it's waiting for the machine's suite slots now) const wait = lines .filter((l) => SLOT_WAIT_LINE.test(l)) .at(-1) ?.trim(); const sofar = job.argv[0] === "test" ? `${passed + failed} finished so far, ${failed} failed${wait && !wait.startsWith("got ") ? `; ${wait}` : ""}${failed ? `:\n\n${tail(briefOutput(job.argv, job.out))}` : ""}` : `it has printed:\n\n${tail(job.out.trim()) || "(nothing yet)"}`; return { text: `job ${id} (${what}) is running, for ${secs(job.started)} so far${earlier(job)}; ${sofar}`, }; } if (job.done.stopped) return { text: `job ${id} (${what}) was stopped after ${secs(job.started, job.done.at)}; ${printed(job)}`, isError: true, }; // (a tool's run says what it did itself) const out = job.out.trim(); const hint = job.tool ? undefined : next(job.argv, out, job.done.code); return { text: [ `${a.stop === true ? "(it had ended already) " : ""}job ${id} (${what}) ${job.done.code === 0 ? "ended" : `failed (exit ${job.done.code})`} after ${secs(job.started, job.done.at)}${earlier(job)}:`, (job.tool ? (job.done.text ?? out) : briefOutput(job.argv, out)) || "(it printed nothing)", hint, ] .filter(Boolean) .join("\n\n"), isError: job.done.code !== 0, }; }, }, { name: "tcm_mv", description: "Rename a definition or namespace. References are by hash, so no code changes.", inputSchema: schema({ from: { type: "string" }, to: { type: "string" }, message }, [ "from", "to", ]), argv: (a) => ["mv", str(a.from), str(a.to), ...messageArgs(a)], }, { name: "tcm_rm", description: "Remove names (refused while something depends on them).", inputSchema: schema({ names, message }, ["names"]), argv: (a) => ["rm", ...strs(a.names), ...messageArgs(a)], }, { name: "tcm_history", description: 'This branch\'s history: an entry per command and what it changed, by name, with the definitions only re-hashed because a dependency changed counted (`all` lists them). tcm_cli ["reset", "<n>"] goes back.', inputSchema: schema({ n: { type: "number", description: "how many entries, newest first (default 10)" }, all: { type: "boolean", description: "list the definitions re-hashed because a dependency changed too", }, }), argv: (a) => ["history", String(a.n ?? 10), ...(a.all ? ["--all"] : [])], }, { name: "tcm_merge", description: "Land a branch (this session's, or `branch`: an earlier session's) on main (or `into`), ONLY when the user has told you to land it: finishing a change, or its tests passing, isn't that; until they say so, the work stays on its branch, as on a git branch, and you tell them it's ready. main is merged into the branch first (conflicts and broken dependents are to fix there, then tcm_merge again), then the tests the change reaches run, and only if they pass does the branch land on main (as one history entry, which `tcm revert` undoes, called by your `message`), which the repository's files follow. The tests can take minutes: `background` runs it as a job (tcm_job).", inputSchema: schema( { approved: { type: "boolean", description: "true only if the user has told you, in this conversation, to land this branch now; nothing lands without it", }, branch: { type: "string", description: "the branch to land, if not this session's: an earlier session's, say (tcm_status lists those holding work main doesn't have)", }, into: { type: "string", description: "the branch to land on (default main)" }, message: { type: "string", description: "what the branch's work is for, as the history of the branch it lands on will call it: a line saying what and why, then, after a blank line, what a reader of that history should know (what changed for users, what was decided, what's left)", }, background, }, ["approved", "message"], ), run: async (a, ctx) => { const source = str(a.branch) || ctx.branch; if (!source) return { text: "this server works on the codebase's own branch (--direct): there's no session branch to land; name one with `branch`", isError: true, }; const into = str(a.into) || "main"; if (into === source) return { text: `${source} is ${into}: nothing to land`, isError: true }; // landing is the user's to decide: an agent lands only what it's been told to if (a.approved !== true) return { text: `Not landed: tcm_merge lands a branch only when the user has told you to. Tell them ${source} is ready (tcm_test says whether its tests pass), and call tcm_merge with approved: true once they say to land it.`, isError: true, }; // what the landing is for: the target's history calls it so if (!str(a.message).trim()) return { text: `Not landed: say what ${source}'s work is for in \`message\` (a line saying what and why, then more after a blank line): ${into}'s history calls the landing by it.`, isError: true, }; // landing is `tcm merge --squash` on the target, as anyone lands a branch: the target merged into it first, there // (a merge that leaves something to settle waits there, the target untouched), what needs fixing and the tests the // change reaches, then the branch on the target as one entry (`tcm revert` undoes it whole; squashed, so the // session's own entries stay its own, and pushing the target sends only what it names; unless it made releases, // which are entries of its history, so that comes too) const land = await ctx.tcm(["merge", source, "--squash", "-m", str(a.message)], undefined, { TCM_BRANCH: into, }); if (land.code !== 0) return { text: `${briefOutput(["test"], land.out)}\n\nNothing landed on ${into}. Settle what's said above on ${source} (tcm_todo says what's left; the merge wrote files holding each conflict's sides merged, and what stopped typechecking: change them, then tcm_update; tcm_resolve keeps one side), then tcm_merge again.`, isError: true, }; // (what it changed, by name and count: the session made it, and `tcm diff` has the code) return { text: `${mergeSummary(land.out)}\n\n${source} is on ${into}, as one history entry: \`tcm revert 0\` on ${into} undoes it (and only it), while it's the latest; \`tcm history\` names it after.`, }; }, }, { name: "tcm_cli", description: 'Any tcm command, as its arguments (["help"] lists them; ["<command>", "--help"] explains one): branches, merges, apps, packages, releases, lint, fmt, verify.', inputSchema: schema( { args: { type: "array", items: { type: "string" } }, live: { type: "boolean", description: "run the codebase's own tcm (a package of it whose command is `tcm`), as it is now, rather than this server's: to try a change to tcm before it's released or verified", }, cwd: { type: "string", description: "run it in this directory (relative to the project), on the codebase there, rather than this one (`tcm -C <dir>`): with `live`, to try this branch's tcm on a codebase made for a repro", }, build: { type: "string", description: "run it with another tcm (`tcm --build`): a kept build, known-good (main's last verified), previous (the one before it) or an id `tcm builds` lists, or a tcm.js; with `cwd`, on a codebase made for a repro: to show a bug is in an older tcm, or a new test's case fails before the fix", }, background, }, ["args"], ), argv: (a) => { const command = [...(a.cwd ? ["-C", str(a.cwd)] : []), ...strs(a.args)]; if (a.build && a.live) throw new Error( "`build` or `live`: one tcm runs it (live is the codebase's own, build a kept one)", ); if (a.build) return ["--build", str(a.build), ...command]; return a.live ? ["run", "--bin", "tcm", "--", ...command] : command; }, }, { name: "tcm_guide", description: `How to work in a tcm codebase, by topic: ${Object.keys(GUIDE).join(", ")}.`, inputSchema: schema({ topic: { type: "string", enum: Object.keys(GUIDE) } }, ["topic"]), run: async (a) => ({ text: GUIDE[str(a.topic)] ?? `topics: ${Object.keys(GUIDE).join(", ")}` }), }, ]
uses
- tcmts.cli.budget.SLOT_WAIT_LINE
- tcmts.cli.codebase.sessionDirFor
- tcmts.cli.mcp.GUIDE
- tcmts.cli.mcp.Job
- tcmts.cli.mcp.JobState
- tcmts.cli.mcp.LANG
- tcmts.cli.mcp.Tool
- tcmts.cli.mcp.background
- tcmts.cli.mcp.briefOutput
- tcmts.cli.mcp.fence
- tcmts.cli.mcp.jobIds
- tcmts.cli.mcp.jobsDirFor
- tcmts.cli.mcp.lastJobId
- tcmts.cli.mcp.mergeSummary
- tcmts.cli.mcp.message
- tcmts.cli.mcp.messageArgs
- tcmts.cli.mcp.names
- tcmts.cli.mcp.next
- tcmts.cli.mcp.patchImports
- tcmts.cli.mcp.readJob
- tcmts.cli.mcp.schema
- tcmts.cli.mcp.stopJob
- tcmts.cli.mcp.str
- tcmts.cli.mcp.strs
- tcmts.cli.mcp.viewMarkdown