@vextlabs/theron-cli 0.1.0 → 0.2.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.
@@ -0,0 +1,158 @@
1
+ // project_memory — auto-load a per-project instructions file and inject
2
+ // it into every chat turn as system context. Theron's analogue to Claude
3
+ // Code's CLAUDE.md.
4
+ //
5
+ // Why this exists: a CLI that edits your repo should know the repo's
6
+ // rules (build command, code style, "never touch migrations/", the
7
+ // canonical model count) WITHOUT you re-typing them every session. Claude
8
+ // Code reads CLAUDE.md at the project root + a parent-dir cascade; this
9
+ // gives Theron the same superpower.
10
+ //
11
+ // Resolution (mirrors cap_config.ts's two-tier overlay + the verifiers'
12
+ // findTsconfig walk-up):
13
+ // 1. Global — ~/.theron/THERON.md (your cross-project house style).
14
+ // 2. Ancestors — walk UP from cwd (max 8 levels), and at each dir take
15
+ // the first of THERON.md / CLAUDE.md / AGENTS.md that exists. We stop
16
+ // at the FIRST ancestor that has a memory file so a sub-package's
17
+ // THERON.md wins over the monorepo root's (closest-wins, same spirit
18
+ // as project-local caps.json overriding the global one).
19
+ //
20
+ // THERON.md is preferred, then CLAUDE.md, then AGENTS.md — so a repo that
21
+ // already has a CLAUDE.md (most do) gets picked up with zero setup, and a
22
+ // Theron user can add a THERON.md to override it just for Theron.
23
+ //
24
+ // The loaded text rides into the request as a leading note (see repl.ts /
25
+ // api.ts `projectContext`). This is a pure client-side feature: no server
26
+ // change is required — worst case the server ignores the field and the
27
+ // note still travels in the messages array as plain context.
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import process from "node:process";
31
+ /** Filenames we recognize as a project-memory file, in priority order.
32
+ * THERON.md is ours; CLAUDE.md / AGENTS.md are the de-facto conventions
33
+ * most repos already carry, so we read them too for zero-setup pickup. */
34
+ export const MEMORY_FILENAMES = ["THERON.md", "CLAUDE.md", "AGENTS.md"];
35
+ /** Hard cap on injected memory size. A runaway CLAUDE.md (some are huge)
36
+ * shouldn't blow the context window or the request body. We keep the
37
+ * head — the top of these files is almost always the important rules —
38
+ * and mark the truncation so the model knows it didn't see all of it. */
39
+ const MAX_MEMORY_BYTES = 32 * 1024;
40
+ /** How far up the directory tree we walk looking for a memory file.
41
+ * Matches findTsconfig's depth so behavior is consistent across the CLI. */
42
+ const MAX_WALK_DEPTH = 8;
43
+ const EMPTY = { content: "", sources: [], truncated: false };
44
+ /** Read a UTF-8 file, returning null on any error (missing / unreadable /
45
+ * not a regular file). Never throws — a bad memory file must not break
46
+ * the REPL. */
47
+ function readMemorySafe(filePath) {
48
+ try {
49
+ const st = fs.statSync(filePath);
50
+ if (!st.isFile())
51
+ return null;
52
+ return fs.readFileSync(filePath, "utf8");
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ /** Truncate to MAX_MEMORY_BYTES on a UTF-8 boundary, keeping the head. */
59
+ function clampBytes(text) {
60
+ const buf = Buffer.from(text, "utf8");
61
+ if (buf.length <= MAX_MEMORY_BYTES)
62
+ return { text, truncated: false };
63
+ // Slice on a byte boundary then drop a possibly-broken trailing
64
+ // multibyte char by re-decoding loosely.
65
+ const head = buf.subarray(0, MAX_MEMORY_BYTES).toString("utf8");
66
+ return { text: head, truncated: true };
67
+ }
68
+ /**
69
+ * Find the nearest project-memory file by walking up from `startDir`.
70
+ * At each ancestor, the first existing filename in MEMORY_FILENAMES wins.
71
+ * Returns the absolute path, or null if none found within MAX_WALK_DEPTH.
72
+ *
73
+ * Closest ancestor wins (a package's THERON.md beats the repo root's), so
74
+ * a monorepo can give each package its own rules.
75
+ */
76
+ export function findProjectMemoryFile(startDir) {
77
+ let dir = path.resolve(startDir);
78
+ for (let i = 0; i < MAX_WALK_DEPTH; i++) {
79
+ for (const name of MEMORY_FILENAMES) {
80
+ const candidate = path.join(dir, name);
81
+ try {
82
+ if (fs.statSync(candidate).isFile())
83
+ return candidate;
84
+ }
85
+ catch {
86
+ // not here — keep looking
87
+ }
88
+ }
89
+ const parent = path.dirname(dir);
90
+ if (parent === dir)
91
+ break; // hit filesystem root
92
+ dir = parent;
93
+ }
94
+ return null;
95
+ }
96
+ /** Resolve the path to the global memory file, or null when HOME is unset. */
97
+ function globalMemoryPath() {
98
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
99
+ return home ? path.join(home, ".theron", "THERON.md") : null;
100
+ }
101
+ /**
102
+ * Load project memory for a given working directory.
103
+ *
104
+ * Layering (later sources are appended after earlier ones, so the
105
+ * project file — being more specific — has the model's freshest
106
+ * attention): global house style first, then the nearest project file.
107
+ *
108
+ * Always returns a value; on any failure it degrades to EMPTY so the
109
+ * REPL keeps working with no memory injected.
110
+ */
111
+ export function loadProjectMemory(cwd = process.cwd()) {
112
+ const sources = [];
113
+ const chunks = [];
114
+ let truncatedAny = false;
115
+ const add = (label, filePath) => {
116
+ const raw = readMemorySafe(filePath);
117
+ if (raw === null)
118
+ return;
119
+ const trimmed = raw.trim();
120
+ if (!trimmed)
121
+ return; // empty/whitespace file — nothing to inject
122
+ const { text, truncated } = clampBytes(trimmed);
123
+ if (truncated)
124
+ truncatedAny = true;
125
+ sources.push(filePath);
126
+ chunks.push(`--- ${label}: ${filePath}${truncated ? " (truncated)" : ""} ---\n${text}`);
127
+ };
128
+ const gp = globalMemoryPath();
129
+ if (gp)
130
+ add("global memory", gp);
131
+ const projectFile = findProjectMemoryFile(cwd);
132
+ // Don't double-load if the project file IS the global file (e.g. you're
133
+ // sitting in ~/.theron).
134
+ if (projectFile && projectFile !== gp)
135
+ add("project memory", projectFile);
136
+ if (chunks.length === 0)
137
+ return EMPTY;
138
+ return {
139
+ content: chunks.join("\n\n"),
140
+ sources,
141
+ truncated: truncatedAny,
142
+ };
143
+ }
144
+ /**
145
+ * Wrap loaded memory in a clear system-context header for injection. The
146
+ * header tells the model exactly what this block is and that it's
147
+ * authoritative project guidance, not user chat. Returns "" when there's
148
+ * nothing to inject so callers can cheaply skip.
149
+ */
150
+ export function formatProjectMemoryForRequest(mem) {
151
+ if (!mem.content)
152
+ return "";
153
+ return ("Project context (auto-loaded from a THERON.md / CLAUDE.md / AGENTS.md " +
154
+ "file — treat as authoritative instructions for this repository, " +
155
+ "the same weight as a system prompt):\n\n" +
156
+ mem.content);
157
+ }
158
+ //# sourceMappingURL=project_memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project_memory.js","sourceRoot":"","sources":["../src/project_memory.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,yEAAyE;AACzE,oBAAoB;AACpB,EAAE;AACF,qEAAqE;AACrE,mEAAmE;AACnE,0EAA0E;AAC1E,wEAAwE;AACxE,oCAAoC;AACpC,EAAE;AACF,wEAAwE;AACxE,yBAAyB;AACzB,wEAAwE;AACxE,yEAAyE;AACzE,2EAA2E;AAC3E,uEAAuE;AACvE,0EAA0E;AAC1E,8DAA8D;AAC9D,EAAE;AACF,0EAA0E;AAC1E,0EAA0E;AAC1E,kEAAkE;AAClE,EAAE;AACF,0EAA0E;AAC1E,0EAA0E;AAC1E,uEAAuE;AACvE,6DAA6D;AAE7D,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC;;2EAE2E;AAC3E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,WAAW,CAAU,CAAC;AAEjF;;;0EAG0E;AAC1E,MAAM,gBAAgB,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnC;6EAC6E;AAC7E,MAAM,cAAc,GAAG,CAAC,CAAC;AAczB,MAAM,KAAK,GAAwB,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAElF;;gBAEgB;AAChB,SAAS,cAAc,CAAC,QAAgB;IACtC,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACjC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE;YAAE,OAAO,IAAI,CAAC;QAC9B,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,IAAI,GAAG,CAAC,MAAM,IAAI,gBAAgB;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACtE,gEAAgE;IAChE,yCAAyC;IACzC,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AACzC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,KAAK,MAAM,IAAI,IAAI,gBAAgB,EAAE,CAAC;YACpC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACvC,IAAI,CAAC;gBACH,IAAI,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE;oBAAE,OAAO,SAAS,CAAC;YACxD,CAAC;YAAC,MAAM,CAAC;gBACP,0BAA0B;YAC5B,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM,CAAC,sBAAsB;QACjD,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,SAAS,gBAAgB;IACvB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC;IAC/D,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAC3D,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,YAAY,GAAG,KAAK,CAAC;IAEzB,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,QAAgB,EAAE,EAAE;QAC9C,MAAM,GAAG,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO;QACzB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,4CAA4C;QAClE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;QAChD,IAAI,SAAS;YAAE,YAAY,GAAG,IAAI,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvB,MAAM,CAAC,IAAI,CACT,OAAO,KAAK,KAAK,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,EAAE,CAC3E,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAC9B,IAAI,EAAE;QAAE,GAAG,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAEjC,MAAM,WAAW,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC;IAC/C,wEAAwE;IACxE,yBAAyB;IACzB,IAAI,WAAW,IAAI,WAAW,KAAK,EAAE;QAAE,GAAG,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;IAE1E,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEtC,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QAC5B,OAAO;QACP,SAAS,EAAE,YAAY;KACxB,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,6BAA6B,CAAC,GAAwB;IACpE,IAAI,CAAC,GAAG,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IAC5B,OAAO,CACL,wEAAwE;QACxE,kEAAkE;QAClE,0CAA0C;QAC1C,GAAG,CAAC,OAAO,CACZ,CAAC;AACJ,CAAC"}
package/dist/repl.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { renderMarkdown } from "./render.js";
2
1
  interface ReplOptions {
3
2
  apiUrl: string;
4
3
  apiKey: string | undefined;
@@ -8,6 +7,20 @@ interface ReplOptions {
8
7
  oneShot?: string;
9
8
  /** Domain profile slug. Defaults to "code". */
10
9
  profile?: string;
10
+ /** Start in plan mode — read-only tools, plan instruction, hard write deny. */
11
+ planMode?: boolean;
12
+ /** Headless mode: suppress interactive chrome; for json, buffer the
13
+ * answer and emit one JSON object at the end. Implies oneShot. */
14
+ headless?: boolean;
15
+ /** Output format for headless mode. */
16
+ outputFormat?: "text" | "json";
17
+ /** Resume the most-recent session for this cwd before the loop. */
18
+ continueSession?: boolean;
19
+ /** Resume a session (with resumeId, that one; else a picker). */
20
+ resumeSession?: boolean;
21
+ resumeId?: string;
22
+ /** Pretty-render the assistant's markdown at end of turn. */
23
+ renderMode?: boolean;
11
24
  }
12
25
  export declare function runRepl(opts: ReplOptions): Promise<number>;
13
- export { renderMarkdown };
26
+ export {};