topchester-ai 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ The current `topchester kb compile` command handles L1 file knowledge:
13
13
  - Requires `topchester kb init` to create the knowledge folders first.
14
14
  - Reads workspace `.gitignore` files, lists in-scope project files, and skips generated/cache folders such as `.git/`, `node_modules/`, `dist/`, `coverage/`, `topchester-kb/`, `.agents/topchester/`, and `.agents/topchester-kb-cache/`.
15
15
  - Queues L1 work at `.agents/topchester-kb-cache/l1-queue.json`.
16
- - Processes queued files with the configured `kb.summarize` model, or `fallback` when `kb.summarize` is not configured.
16
+ - Processes queued files with the configured `kb.summarize` model, or the `default` model when `kb.summarize` is not configured.
17
17
  - Writes the manifest at `topchester-kb/manifest.json`.
18
18
  - Writes current L1 file entries under `topchester-kb/l1-files/`.
19
19
  - Exits successfully only when every in-scope file has a current L1 entry.
@@ -69,11 +69,19 @@ Model settings are loaded from YAML config files and merged in this order:
69
69
 
70
70
  Example configs live in `config/example.yaml` and `config/gemini.yaml`. OpenRouter configs expect `OPENROUTER_API_KEY` in the environment; do not commit API keys or other secrets.
71
71
 
72
+ The smallest OpenRouter config uses one model for every Topchester purpose:
73
+
74
+ ```yaml
75
+ models:
76
+ default: openrouter/google/gemini-3.1-flash-lite
77
+ ```
78
+
72
79
  ## Docs
73
80
 
74
81
  - [Architecture](docs/ARCHITECTURE.md)
75
82
  - [Knowledge System](docs/KNOWLEDGE.md)
76
83
  - [CLI Commands](docs/cli.md)
84
+ - [Configuration](docs/config.md)
77
85
  - [TUI Guide](docs/tui.md)
78
86
  - [Model Configuration](docs/MODEL_CONFIG.md)
79
87
  - [Sessions](docs/SESSIONS.md)
package/dist/cli.mjs CHANGED
@@ -2018,6 +2018,7 @@ const modelPurposeSchema = z.enum([
2018
2018
  "kb.embed",
2019
2019
  "fallback"
2020
2020
  ]);
2021
+ const modelPurposes = modelPurposeSchema.options;
2021
2022
  const toolProtocolSchema = z.enum([
2022
2023
  "auto",
2023
2024
  "native",
@@ -2043,7 +2044,14 @@ const modelAssignmentSchema = z.object({
2043
2044
  provider: z.string().optional(),
2044
2045
  toolProtocol: toolProtocolSchema.optional()
2045
2046
  });
2047
+ const modelRefSchema = z.union([z.string(), modelAssignmentSchema]);
2046
2048
  const providersSchema = z.object({ default: z.string().optional() }).catchall(providerSchema.or(z.string()));
2049
+ const rawModelsSchema = z.object({
2050
+ "default": modelRefSchema.optional(),
2051
+ "fast": modelRefSchema.optional(),
2052
+ "kb.summarize": modelRefSchema.optional(),
2053
+ "providers": providersSchema.optional()
2054
+ }).strict();
2047
2055
  const ignorePathSchema = z.string().min(1).superRefine((value, context) => {
2048
2056
  const pattern = value.startsWith("!") ? value.slice(1) : value;
2049
2057
  if (!pattern) {
@@ -2071,7 +2079,11 @@ const topchesterConfigSchema = z.object({
2071
2079
  defaultPurpose: modelPurposeSchema.optional(),
2072
2080
  assignments: z.partialRecord(modelPurposeSchema, modelAssignmentSchema).optional(),
2073
2081
  providers: providersSchema.optional()
2074
- }).optional(),
2082
+ }).strict().optional(),
2083
+ ignore: z.object({ paths: z.array(ignorePathSchema).optional() }).optional()
2084
+ });
2085
+ const rawTopchesterConfigSchema = z.object({
2086
+ models: rawModelsSchema.optional(),
2075
2087
  ignore: z.object({ paths: z.array(ignorePathSchema).optional() }).optional()
2076
2088
  });
2077
2089
  function loadTopchesterConfig(options) {
@@ -2102,10 +2114,94 @@ function readConfigFile(path) {
2102
2114
  }
2103
2115
  }
2104
2116
  function parseConfigFile(path, value) {
2105
- const parsed = topchesterConfigSchema.safeParse(value ?? {});
2117
+ const raw = rawTopchesterConfigSchema.safeParse(value ?? {});
2118
+ if (!raw.success) throw new Error(`Invalid Topchester config at ${path}: ${raw.error.issues.map(formatZodIssue).join("; ")}`);
2119
+ const parsed = topchesterConfigSchema.safeParse(normalizeConfigInput(raw.data));
2106
2120
  if (!parsed.success) throw new Error(`Invalid Topchester config at ${path}: ${parsed.error.issues.map(formatZodIssue).join("; ")}`);
2107
2121
  return parsed.data;
2108
2122
  }
2123
+ function normalizeConfigInput(value) {
2124
+ if (!isPlainObject(value) || !isPlainObject(value.models)) return value;
2125
+ const models = { ...value.models };
2126
+ const providers = isPlainObject(models.providers) ? { ...models.providers } : {};
2127
+ const assignments = {};
2128
+ const defaultModelRef = normalizeModelRef(models.default, typeof providers.default === "string" ? providers.default : void 0);
2129
+ const defaultProvider = typeof providers.default === "string" ? providers.default : defaultModelRef?.provider;
2130
+ const fastModelRef = normalizeModelRef(models.fast, defaultProvider);
2131
+ const kbSummarizeModelRef = normalizeModelRef(models["kb.summarize"], defaultProvider);
2132
+ if (defaultModelRef) {
2133
+ const assignment = modelRefToAssignment(defaultModelRef);
2134
+ for (const purpose of modelPurposes) assignments[purpose] ??= assignment;
2135
+ providers.default ??= defaultModelRef.provider;
2136
+ ensureKnownProvider(providers, defaultModelRef.provider);
2137
+ delete models.default;
2138
+ }
2139
+ if (fastModelRef) {
2140
+ assignments["agent.fast"] = modelRefToAssignment(fastModelRef);
2141
+ ensureKnownProvider(providers, fastModelRef.provider);
2142
+ delete models.fast;
2143
+ }
2144
+ if (kbSummarizeModelRef) {
2145
+ assignments["kb.summarize"] = modelRefToAssignment(kbSummarizeModelRef);
2146
+ ensureKnownProvider(providers, kbSummarizeModelRef.provider);
2147
+ delete models["kb.summarize"];
2148
+ }
2149
+ return {
2150
+ ...value,
2151
+ models: {
2152
+ ...models,
2153
+ assignments,
2154
+ providers
2155
+ }
2156
+ };
2157
+ }
2158
+ function normalizeModelRef(ref, defaultProvider) {
2159
+ if (typeof ref === "string") return parseModelRef(ref, defaultProvider);
2160
+ if (!isPlainObject(ref) || typeof ref.name !== "string") return;
2161
+ return {
2162
+ model: ref.name,
2163
+ ...typeof ref.provider === "string" ? { provider: ref.provider } : defaultProvider ? { provider: defaultProvider } : {},
2164
+ ...typeof ref.toolProtocol === "string" && toolProtocolSchema.safeParse(ref.toolProtocol).success ? { toolProtocol: ref.toolProtocol } : {}
2165
+ };
2166
+ }
2167
+ function modelRefToAssignment(ref) {
2168
+ return {
2169
+ name: ref.model,
2170
+ ...ref.provider ? { provider: ref.provider } : {},
2171
+ ...ref.toolProtocol ? { toolProtocol: ref.toolProtocol } : {}
2172
+ };
2173
+ }
2174
+ function parseModelRef(ref, defaultProvider) {
2175
+ if (defaultProvider) {
2176
+ const providerPrefix = `${defaultProvider}/`;
2177
+ return ref.startsWith(providerPrefix) ? {
2178
+ provider: defaultProvider,
2179
+ model: ref.slice(providerPrefix.length)
2180
+ } : {
2181
+ provider: defaultProvider,
2182
+ model: ref
2183
+ };
2184
+ }
2185
+ const [provider, ...modelParts] = ref.split("/");
2186
+ if (provider && modelParts.length > 0) return {
2187
+ provider,
2188
+ model: modelParts.join("/")
2189
+ };
2190
+ return { model: ref };
2191
+ }
2192
+ function ensureKnownProvider(providers, provider) {
2193
+ if (provider !== "openrouter" || providers.openrouter !== void 0) return;
2194
+ providers.openrouter = {
2195
+ type: "openai-compatible",
2196
+ baseURL: "https://openrouter.ai/api/v1",
2197
+ apiKeyEnv: "OPENROUTER_API_KEY",
2198
+ supportsStructuredOutputs: true,
2199
+ headers: {
2200
+ "HTTP-Referer": "https://topchester.com",
2201
+ "X-Title": "Topchester"
2202
+ }
2203
+ };
2204
+ }
2109
2205
  function deepMerge(base, override, path = []) {
2110
2206
  if (Array.isArray(base) && Array.isArray(override)) return path.join(".") === "ignore.paths" ? [...base, ...override] : override;
2111
2207
  if (!isPlainObject(base) || !isPlainObject(override)) return override;
@@ -4435,7 +4531,6 @@ var ChatLayout = class {
4435
4531
  messages;
4436
4532
  folderName;
4437
4533
  modelLabel;
4438
- exitAgent;
4439
4534
  input = new Input();
4440
4535
  status = "ready";
4441
4536
  knowledgeStatus;
@@ -4449,12 +4544,15 @@ var ChatLayout = class {
4449
4544
  activeSlashSuggestionIndex = 0;
4450
4545
  threadScrollOffset = 0;
4451
4546
  promptHistory = new PromptHistory();
4452
- constructor(terminal, messages, folderName, modelLabel, exitAgent = () => {}) {
4547
+ exitAgent;
4548
+ transcriptMode;
4549
+ constructor(terminal, messages, folderName, modelLabel, options = {}) {
4453
4550
  this.terminal = terminal;
4454
4551
  this.messages = messages;
4455
4552
  this.folderName = folderName;
4456
4553
  this.modelLabel = modelLabel;
4457
- this.exitAgent = exitAgent;
4554
+ this.exitAgent = typeof options === "function" ? options : options.exitAgent ?? (() => {});
4555
+ this.transcriptMode = typeof options === "function" ? "viewport" : options.transcriptMode ?? "viewport";
4458
4556
  this.input.onSubmit = (value) => {
4459
4557
  if (value.trim().length > 0) {
4460
4558
  const message = value.trim();
@@ -4525,8 +4623,8 @@ var ChatLayout = class {
4525
4623
  }
4526
4624
  if (this.handleModalInput(data)) return;
4527
4625
  if (this.handleSlashSuggestionInput(data)) return;
4528
- if (this.handlePromptHistoryInput(data)) return;
4529
4626
  if (this.handleThreadScrollInput(data)) return;
4627
+ if (this.handlePromptHistoryInput(data)) return;
4530
4628
  const previousInput = this.input.getValue();
4531
4629
  this.input.handleInput(data);
4532
4630
  if (this.input.getValue() !== previousInput) this.promptHistory.resetBrowsing();
@@ -4539,6 +4637,10 @@ var ChatLayout = class {
4539
4637
  const footerLines = this.getActiveModal() ? this.renderModalHelp(safeWidth) : this.renderPrompt(safeWidth);
4540
4638
  const threadHeight = Math.max(1, this.terminal.rows - footerLines.length);
4541
4639
  const allThreadLines = this.renderThread(safeWidth);
4640
+ if (this.transcriptMode === "inline") {
4641
+ this.threadScrollOffset = 0;
4642
+ return [...allThreadLines.length < threadHeight ? padLines(allThreadLines, threadHeight, safeWidth) : allThreadLines, ...footerLines];
4643
+ }
4542
4644
  const maxScrollOffset = Math.max(0, allThreadLines.length - threadHeight);
4543
4645
  this.threadScrollOffset = Math.min(this.threadScrollOffset, maxScrollOffset);
4544
4646
  const end = allThreadLines.length - this.threadScrollOffset;
@@ -4639,6 +4741,7 @@ var ChatLayout = class {
4639
4741
  return false;
4640
4742
  }
4641
4743
  handleThreadScrollInput(data) {
4744
+ if (this.transcriptMode === "inline") return false;
4642
4745
  const pageSize = Math.max(1, Math.floor(this.terminal.rows / 2));
4643
4746
  const wheel = parseMouseWheel(data);
4644
4747
  if (wheel === "up") {
@@ -5197,15 +5300,6 @@ function formatKbPathSource(status) {
5197
5300
  return status.kbPathSource === "env" ? " (custom)" : "";
5198
5301
  }
5199
5302
  //#endregion
5200
- //#region src/tui/terminal.ts
5201
- function enterAlternateScreen(terminal) {
5202
- terminal.write("\x1B[?1049h");
5203
- terminal.clearScreen();
5204
- }
5205
- function exitAlternateScreen(terminal) {
5206
- terminal.write("\x1B[?1049l");
5207
- }
5208
- //#endregion
5209
5303
  //#region src/tui/shell.ts
5210
5304
  var TopchesterTuiShell = class {
5211
5305
  context;
@@ -5232,19 +5326,20 @@ var TopchesterTuiShell = class {
5232
5326
  return;
5233
5327
  }
5234
5328
  const terminal = new ProcessTerminal();
5235
- enterAlternateScreen(terminal);
5236
5329
  const tui = new TUI(terminal, true);
5237
5330
  let didExit = false;
5238
5331
  const exit = () => {
5239
5332
  if (didExit) return;
5240
5333
  didExit = true;
5241
5334
  tui.stop();
5242
- exitAlternateScreen(terminal);
5243
5335
  printExitBanner(session.sessionId, Date.now() - startedAt);
5244
5336
  };
5245
- const app = new ChatLayout(terminal, messages, folderName, modelLabel, () => {
5246
- exit();
5247
- process.exit(0);
5337
+ const app = new ChatLayout(terminal, messages, folderName, modelLabel, {
5338
+ transcriptMode: "inline",
5339
+ exitAgent: () => {
5340
+ exit();
5341
+ process.exit(0);
5342
+ }
5248
5343
  });
5249
5344
  app.setSubmitMessage((message) => {
5250
5345
  this.submitChatMessage(app, tui, message);