chels-code 0.5.0 → 0.5.2

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,9 @@
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";
7
+ import { exec } from "node:child_process";
5
8
  import { stdin, stdout } from "node:process";
6
9
  import { fileURLToPath } from "node:url";
7
10
  import { render } from "ink";
@@ -74,6 +77,68 @@ function getApiKeys() {
74
77
  }
75
78
  return keys;
76
79
  }
80
+ const REQUIRED_API_KEY_VARS = [
81
+ { variable: "OPENROUTER_API_KEY", label: "OpenRouter" },
82
+ { variable: "NVIDIA_API_KEY", label: "NVIDIA" },
83
+ { variable: "GROQ_API_KEY", label: "Groq" },
84
+ ];
85
+ async function ensureApiKeys() {
86
+ const missing = REQUIRED_API_KEY_VARS.filter((item) => !process.env[item.variable]?.trim());
87
+ if (missing.length === 0)
88
+ return;
89
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
90
+ try {
91
+ const provided = {};
92
+ for (const item of missing) {
93
+ const answer = await new Promise((resolve) => {
94
+ rl.question(`\n${item.label} API key: `, (response) => resolve(response.trim()));
95
+ });
96
+ if (answer) {
97
+ process.env[item.variable] = answer;
98
+ provided[item.variable] = answer;
99
+ }
100
+ }
101
+ if (Object.keys(provided).length > 0) {
102
+ const content = Object.entries(provided)
103
+ .map(([key, value]) => `${key}=${value}`)
104
+ .join("\n") + "\n";
105
+ await writeFile(path.join(process.cwd(), ".env"), content, { encoding: "utf8" });
106
+ }
107
+ }
108
+ finally {
109
+ rl.close();
110
+ }
111
+ }
112
+ async function checkAndPromptUpdate() {
113
+ try {
114
+ const response = await fetch("https://registry.npmjs.org/chelscode/latest");
115
+ if (!response.ok)
116
+ return;
117
+ const data = (await response.json());
118
+ const latest = data.version;
119
+ if (!latest || latest === VERSION)
120
+ return;
121
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
122
+ const answer = await new Promise((resolve) => {
123
+ rl.question(`A new version of ChelsCode is available (${latest}). Update now? (y/n): `, (res) => resolve(res.trim()));
124
+ });
125
+ rl.close();
126
+ if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
127
+ await new Promise((resolve, reject) => {
128
+ exec(`npm install -g chelscode@${latest}`, { cwd: process.cwd() }, (error, _stdout, stderr) => {
129
+ if (error)
130
+ reject(error);
131
+ else
132
+ resolve();
133
+ });
134
+ });
135
+ process.exit(0);
136
+ }
137
+ }
138
+ catch {
139
+ // Ignore update-check failures.
140
+ }
141
+ }
77
142
  async function main() {
78
143
  const options = parseOptions(process.argv.slice(2));
79
144
  if (options.help) {
@@ -88,6 +153,8 @@ async function main() {
88
153
  if (!stdin.isTTY || !stdout.isTTY) {
89
154
  throw new Error("ChelsCode needs an interactive terminal.");
90
155
  }
156
+ const keysMissing = REQUIRED_API_KEY_VARS.some((item) => !process.env[item.variable]?.trim());
157
+ await ensureApiKeys();
91
158
  const apiKeys = getApiKeys();
92
159
  const workspace = process.cwd();
93
160
  let resumed = null;
@@ -121,7 +188,8 @@ async function main() {
121
188
  saveSession(session);
122
189
  const restoreTitle = installTerminalTitle("ChelsCode");
123
190
  try {
124
- const instance = render(_jsx(App, { apiKeys: apiKeys, workspace: workspace, version: VERSION, sessionId: session.id, resume: resumed }), {
191
+ await checkAndPromptUpdate();
192
+ const instance = render(_jsx(App, { apiKeys: apiKeys, workspace: workspace, version: VERSION, sessionId: session.id, resume: resumed, missingKeys: keysMissing }), {
125
193
  exitOnCtrlC: true,
126
194
  kittyKeyboard: { mode: "auto" },
127
195
  });
@@ -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;
@@ -1 +1 @@
1
- export const VERSION = "0.5.0";
1
+ export const VERSION = "0.5.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chels-code",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "A TypeScript + Ink terminal coding assistant for OpenRouter, NVIDIA, and Groq",
5
5
  "type": "module",
6
6
  "bin": {