chels-code 0.5.0 → 0.5.1

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/dist/src/cli.js CHANGED
@@ -2,6 +2,8 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import path from "node:path";
4
4
  import { realpathSync } from "node:fs";
5
+ import { writeFile } from "node:fs/promises";
6
+ import { createInterface } from "node:readline";
5
7
  import { stdin, stdout } from "node:process";
6
8
  import { fileURLToPath } from "node:url";
7
9
  import { render } from "ink";
@@ -74,6 +76,38 @@ function getApiKeys() {
74
76
  }
75
77
  return keys;
76
78
  }
79
+ const REQUIRED_API_KEY_VARS = [
80
+ { variable: "OPENROUTER_API_KEY", label: "OpenRouter" },
81
+ { variable: "NVIDIA_API_KEY", label: "NVIDIA" },
82
+ { variable: "GROQ_API_KEY", label: "Groq" },
83
+ ];
84
+ async function ensureApiKeys() {
85
+ const missing = REQUIRED_API_KEY_VARS.filter((item) => !process.env[item.variable]?.trim());
86
+ if (missing.length === 0)
87
+ return;
88
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
89
+ try {
90
+ const provided = {};
91
+ for (const item of missing) {
92
+ const answer = await new Promise((resolve) => {
93
+ rl.question(`\n${item.label} API key: `, (response) => resolve(response.trim()));
94
+ });
95
+ if (answer) {
96
+ process.env[item.variable] = answer;
97
+ provided[item.variable] = answer;
98
+ }
99
+ }
100
+ if (Object.keys(provided).length > 0) {
101
+ const content = Object.entries(provided)
102
+ .map(([key, value]) => `${key}=${value}`)
103
+ .join("\n") + "\n";
104
+ await writeFile(path.join(process.cwd(), ".env"), content, { encoding: "utf8" });
105
+ }
106
+ }
107
+ finally {
108
+ rl.close();
109
+ }
110
+ }
77
111
  async function main() {
78
112
  const options = parseOptions(process.argv.slice(2));
79
113
  if (options.help) {
@@ -88,6 +122,8 @@ async function main() {
88
122
  if (!stdin.isTTY || !stdout.isTTY) {
89
123
  throw new Error("ChelsCode needs an interactive terminal.");
90
124
  }
125
+ const keysMissing = REQUIRED_API_KEY_VARS.some((item) => !process.env[item.variable]?.trim());
126
+ await ensureApiKeys();
91
127
  const apiKeys = getApiKeys();
92
128
  const workspace = process.cwd();
93
129
  let resumed = null;
@@ -121,7 +157,7 @@ async function main() {
121
157
  saveSession(session);
122
158
  const restoreTitle = installTerminalTitle("ChelsCode");
123
159
  try {
124
- const instance = render(_jsx(App, { apiKeys: apiKeys, workspace: workspace, version: VERSION, sessionId: session.id, resume: resumed }), {
160
+ const instance = render(_jsx(App, { apiKeys: apiKeys, workspace: workspace, version: VERSION, sessionId: session.id, resume: resumed, missingKeys: keysMissing }), {
125
161
  exitOnCtrlC: true,
126
162
  kittyKeyboard: { mode: "auto" },
127
163
  });
@@ -62,6 +62,18 @@ export function classifyRequest(request) {
62
62
  function errorMessage(error) {
63
63
  return error instanceof Error ? error.message : String(error);
64
64
  }
65
+ function formatElapsed(ms) {
66
+ if (ms < 1000)
67
+ return `${Math.round(ms)}ms`;
68
+ if (ms < 60_000)
69
+ return `${(ms / 1000).toFixed(1)}s`;
70
+ if (ms < 3_600_000)
71
+ return `${(ms / 60_000).toFixed(1)}m`;
72
+ return `${(ms / 3_600_000).toFixed(1)}h`;
73
+ }
74
+ function statusWithElapsed(status, startMs) {
75
+ return `${status} (${formatElapsed(Date.now() - startMs)})`;
76
+ }
65
77
  function responseText(message, provider) {
66
78
  if (typeof message.content !== "string" || !message.content.trim()) {
67
79
  throw new Error(`${PROVIDERS[provider].label} returned an empty response.`);
@@ -217,7 +229,8 @@ async function runToolLoop({ provider, apiKey, model, system, user, tools, maxRo
217
229
  onMade: (item) => appendMade(made, item),
218
230
  };
219
231
  for (let toolRound = 0; toolRound < maxRounds; toolRound += 1) {
220
- hooks.onStatus(`${PROVIDERS[provider].label} is thinking…`);
232
+ const roundStart = Date.now();
233
+ hooks.onStatus(statusWithElapsed(`${PROVIDERS[provider].label} is thinking…`, roundStart));
221
234
  let assistantMessage;
222
235
  try {
223
236
  assistantMessage = await complete(hooks, {
@@ -365,7 +378,10 @@ export function createEngine({ apiKeys, models, workspace, hooks, effort }) {
365
378
  else
366
379
  failures.push(`${PROVIDERS[PROVIDER_IDS[index]].label}: ${errorMessage(result.reason)}`);
367
380
  });
368
- if (failures.length) {
381
+ if (failures.length > 0) {
382
+ hooks.onStatus(`Some AI analyses failed; continuing with available perspectives: ${failures.join("; ")}`);
383
+ }
384
+ if (perspectives.length === 0) {
369
385
  throw new Error(`All three AIs must analyze the task before it can continue.\n${failures.join("\n")}`);
370
386
  }
371
387
  hooks.onStatus(null);
@@ -27,7 +27,7 @@ function initialEntries(workspace, version, resume) {
27
27
  .map((entry, index) => ({ ...entry, id: index + 1 }));
28
28
  return [header, ...restored];
29
29
  }
30
- export function App({ apiKeys, workspace, version, sessionId, resume, sessionsDir, }) {
30
+ export function App({ apiKeys, workspace, version, sessionId, resume, sessionsDir, missingKeys = false, }) {
31
31
  const { exit } = useApp();
32
32
  const { columns, rows } = useTerminalSize();
33
33
  const [entries, setEntries] = useState(() => (initialEntries(workspace, version, resume)));
@@ -208,6 +208,13 @@ export function App({ apiKeys, workspace, version, sessionId, resume, sessionsDi
208
208
  endSession();
209
209
  return;
210
210
  }
211
+ if (missingKeys) {
212
+ pushEntry({
213
+ kind: "note",
214
+ text: "API keys are not configured. Add OPENROUTER_API_KEY, NVIDIA_API_KEY, and GROQ_API_KEY to your .env or environment before continuing.",
215
+ });
216
+ return;
217
+ }
211
218
  if (!engine)
212
219
  return;
213
220
  busyRef.current = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chels-code",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "A TypeScript + Ink terminal coding assistant for OpenRouter, NVIDIA, and Groq",
5
5
  "type": "module",
6
6
  "bin": {