Skip to content

Commit d396320

Browse files
authored
fix: prompt for catalog provider API keys and clarify update behavior (#7)
## Related Issue No linked issue — problem explained below. ## Problem Connecting a provider from the models.dev catalog in `/login` or `/provider` failed with `Environment variable "X" is not set or is empty.` instead of asking for an API key. The same gate also broke the OpenAI (API key) and Anthropic (API key) login entries, which route through the same path, and `pythinker provider catalog add` had no way to pass a key directly. Separately, the update banner and `/update` implied Homebrew installs auto-update when they deliberately do not, and the native install scripts pointed at nonexistent release assets. ## What changed - Catalog provider login now falls back to the API key input dialog when the provider's environment variable is unset or empty, and stores the entered key in `config.toml` via the existing `api_key` provider field. When the env var is set, behavior is unchanged. - `pythinker provider catalog add` gains `--api-key <key>`, taking precedence over `--api-key-env` and the catalog's declared env var; error messages now mention the flag. - `/update` and the startup update notice state that Homebrew installs do not auto-update and show the native installer command for automatic background updates. - The native install scripts download the correct published release assets. - Site: legacy downloads popup and Python download milestone highlight. Tests: new coverage for the prompt fallback (env set / unset / undeclared / cancelled) in the TUI, and for `--api-key` precedence and Commander wiring in the CLI. Full suite green. ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/Pythoughts-labs/pythinker-code/blob/main/CONTRIBUTING.md) document. - [x] I have linked a related issue, or explained the problem above. - [x] I have added tests that prove my feature works. - [x] Ran `gen-changesets` skill, or this PR needs no changeset. - [x] Ran `gen-docs` skill, or this PR needs no doc update. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Catalog provider setup now accepts API keys interactively or through `--api-key`. - Native installers download verified, architecture-specific release packages on macOS, Linux, and Windows. - Added a dismissible community download milestone popup to the website. - Updated installation guidance, including Homebrew recommendations for automatic updates. - **Bug Fixes** - Provider login no longer fails when the configured API-key environment variable is missing or empty. - **Documentation** - Updated architecture imagery and installation messaging. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 792615a commit d396320

23 files changed

Lines changed: 725 additions & 203 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pythoughts/pythinker-code': patch
3+
---
4+
5+
Prompt for an API key when connecting a catalog provider whose environment variable is not set, instead of failing with "Environment variable is not set or is empty". Applies to `/login`, `/provider`, and `pythinker provider catalog add`, which now also accepts `--api-key <key>`.

.changeset/homebrew-update-hint.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pythoughts/pythinker-code': patch
3+
---
4+
5+
Explain in `/update` and the startup update notice that Homebrew installs do not auto-update, and point to the native installer for automatic background updates.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pythoughts/pythinker-code': patch
3+
---
4+
5+
Point the native install scripts at the published release assets.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pythoughts/pythinker-code': patch
3+
---
4+
5+
Show a clear requirement message with the native-installer alternative when the CLI is launched on Node.js older than 26.4, instead of failing with a cryptic flag error.

.github/workflows/install-smoke.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ jobs:
3636
3737
- name: Check PowerShell syntax
3838
run: |
39-
pwsh -NoProfile -Command '[void][System.Management.Automation.Language.Parser]::ParseFile("apps/pythinker-web/public/install.ps1", [ref]$null, [ref]$errs); if ($errs.Count) { $errs; exit 1 }'
39+
pwsh -NoProfile -Command '$errs = $null; [void][System.Management.Automation.Language.Parser]::ParseFile("apps/pythinker-web/public/install.ps1", [ref]$null, [ref]$errs); if ($errs.Count) { $errs; exit 1 }'

README.md

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -227,30 +227,9 @@ See the [configuration docs](https://pythoughts-labs.github.io/pythinker-code/co
227227

228228
Pythinker Code is a **pnpm monorepo**. The CLI consumes capabilities through the SDK and never depends directly on internal engine packages.
229229

230-
```mermaid
231-
flowchart LR
232-
subgraph Apps
233-
CLI["pythinker-code<br/>(CLI / TUI)"]
234-
WEB["pythinker-web<br/>(Browser UI)"]
235-
DASH["dashboard<br/>(Session replay)"]
236-
end
237-
238-
subgraph Packages
239-
SDK["node-sdk"]
240-
CORE["agent-core"]
241-
ANYLLM["Any LLM<br/>(provider abstraction)"]
242-
KAOS["kaos<br/>(Execution env)"]
243-
SERVER["server<br/>(REST + WebSocket)"]
244-
end
245-
246-
CLI --> SDK
247-
WEB --> SERVER
248-
DASH --> SERVER
249-
SDK --> CORE
250-
SERVER --> CORE
251-
CORE --> ANYLLM
252-
CORE --> KAOS
253-
```
230+
<p align="center">
231+
<img src="docs/media/Architecture.webp" alt="Pythinker Code architecture" width="836" />
232+
</p>
254233

255234
| Package | Role |
256235
|---------|------|

_typos.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ nd = "nd" # ndJsonStream, `Nd` cron interval token
2525
dows = "dows" # formatDows — days-of-week (cron)
2626
fo = "fo" # `/FO` flag of Windows schtasks
2727
pn = "pn" # "PNGs" tokenized as PN by the checker
28+
iterm = "iterm" # iTerm2 terminal app identifier

apps/pythinker-code/src/cli/sub/provider.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ interface CatalogListOptions {
6565
}
6666

6767
interface CatalogAddOptions {
68+
readonly apiKey?: string;
6869
readonly apiKeyEnv?: string;
6970
readonly defaultModel?: string;
7071
readonly url?: string;
@@ -331,19 +332,24 @@ export async function handleCatalogAdd(
331332
deps.exit(1);
332333
}
333334

335+
const literalApiKey = opts.apiKey?.trim();
334336
const apiKeyEnvVar = (opts.apiKeyEnv ?? entry.env?.[0])?.trim();
335-
if (apiKeyEnvVar === undefined || apiKeyEnvVar.length === 0) {
336-
deps.stderr.write(
337-
`Provider "${providerId}" does not declare an API key environment variable.\n`,
338-
);
339-
deps.exit(1);
340-
}
341-
const apiKey = deps.env[apiKeyEnvVar]?.trim();
342-
if (apiKey === undefined || apiKey.length === 0) {
343-
deps.stderr.write(
344-
`Environment variable "${apiKeyEnvVar}" is not set or is empty.\n`,
345-
);
346-
deps.exit(1);
337+
let useEnvVar = false;
338+
if (literalApiKey === undefined || literalApiKey.length === 0) {
339+
if (apiKeyEnvVar === undefined || apiKeyEnvVar.length === 0) {
340+
deps.stderr.write(
341+
`Provider "${providerId}" does not declare an API key environment variable. Pass --api-key <key>.\n`,
342+
);
343+
deps.exit(1);
344+
}
345+
const envValue = deps.env[apiKeyEnvVar]?.trim();
346+
if (envValue === undefined || envValue.length === 0) {
347+
deps.stderr.write(
348+
`Environment variable "${apiKeyEnvVar}" is not set or is empty. Set it or pass --api-key <key>.\n`,
349+
);
350+
deps.exit(1);
351+
}
352+
useEnvVar = true;
347353
}
348354

349355
const models = catalogProviderModels(entry);
@@ -386,7 +392,8 @@ export async function handleCatalogAdd(
386392
catalogUrl: url,
387393
wire,
388394
baseUrl,
389-
apiKeyEnvVar,
395+
apiKey: useEnvVar ? undefined : literalApiKey,
396+
apiKeyEnvVar: useEnvVar ? apiKeyEnvVar : undefined,
390397
models,
391398
selectedModelId: opts.defaultModel ?? '',
392399
thinking: false,
@@ -519,17 +526,19 @@ export function registerProviderCommand(parent: Command, deps?: Partial<Provider
519526
catalog
520527
.command('add <providerId>')
521528
.description('Import a known provider from the catalog by id.')
529+
.option('--api-key <key>', 'Provider API key to store in config.toml (takes precedence over --api-key-env).')
522530
.option('--api-key-env <name>', 'Environment variable containing the provider API key.')
523531
.option('--default-model <modelId>', 'Mark the imported model as default_model after import.')
524532
.option('--url <url>', `Override catalog URL. Defaults to ${DEFAULT_CATALOG_URL}.`)
525533
.action(
526534
async (
527535
providerId: string,
528-
options: { apiKeyEnv?: string; defaultModel?: string; url?: string },
536+
options: { apiKey?: string; apiKeyEnv?: string; defaultModel?: string; url?: string },
529537
) => {
530538
const resolved = resolveDeps(deps);
531539
await runAction(resolved, () =>
532540
handleCatalogAdd(resolved, providerId, {
541+
apiKey: options.apiKey,
533542
apiKeyEnv: options.apiKeyEnv,
534543
defaultModel: options.defaultModel,
535544
url: options.url,

apps/pythinker-code/src/cli/update/preflight.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,17 @@ export function renderManualUpdateMessage(
168168
sourceDesc = 'unsupported package manager or layout.';
169169
break;
170170
}
171+
const homebrewHint =
172+
source === 'homebrew'
173+
? `Homebrew installs do not auto-update. For automatic background updates, ` +
174+
`switch to the native installer: ${NATIVE_INSTALL_COMMAND_UNIX}\n`
175+
: '';
171176
return (
172177
`A newer version of ${NPM_PACKAGE_NAME} is available ` +
173178
`(${currentVersion} -> ${target.version}).\n` +
174179
`Detected install source: ${sourceDesc}\n` +
175-
`To update manually, run: ${installCommand}\n`
180+
`To update manually, run: ${installCommand}\n` +
181+
homebrewHint
176182
);
177183
}
178184

@@ -717,7 +723,12 @@ export type ManualUpdateResult =
717723
| { readonly status: 'check-failed'; readonly message: string }
718724
| { readonly status: 'started'; readonly version: string }
719725
| { readonly status: 'in-progress'; readonly version: string }
720-
| { readonly status: 'manual'; readonly version: string; readonly command: string };
726+
| {
727+
readonly status: 'manual';
728+
readonly version: string;
729+
readonly command: string;
730+
readonly source: InstallSource;
731+
};
721732

722733
/**
723734
* Explicit user-requested update (TUI `/update`). Unlike the passive
@@ -746,6 +757,7 @@ export async function startManualUpdate(
746757
status: 'manual',
747758
version: target.version,
748759
command: installCommandFor(source, target.version, platform),
760+
source,
749761
};
750762
}
751763

@@ -763,6 +775,7 @@ export async function startManualUpdate(
763775
status: 'manual',
764776
version: target.version,
765777
command: installCommandFor(source, target.version, platform),
778+
source,
766779
};
767780
}
768781

apps/pythinker-code/src/launcher.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ const FFI_FLAG = '--experimental-ffi';
44
const FFI_WARNING_FLAG = '--disable-warning=ExperimentalWarning';
55
const FFI_CHILD_ENV = 'PYTHINKER_CODE_FFI_CHILD';
66
const REQUIRED_RUNTIME = 'Node.js 26.4.0 or newer with experimental FFI support';
7+
const MINIMUM_NODE = [26, 4, 0] as const;
8+
const NATIVE_INSTALL_HINT =
9+
'Alternatively, use the native installer (no Node.js required): https://code.pythinker.com';
10+
11+
/**
12+
* Older Node (e.g. 24 LTS) has no `--experimental-ffi`, so the re-exec below
13+
* would die with a cryptic `bad option` error. npm installs the package on any
14+
* Node version (engines is only a warning for consumers), so guard here with
15+
* an actionable message instead.
16+
*/
17+
function isRuntimeTooOld(): boolean {
18+
const parts = process.versions.node.split('.').map(Number);
19+
const [major = 0, minor = 0, patch = 0] = parts;
20+
const [reqMajor, reqMinor, reqPatch] = MINIMUM_NODE;
21+
if (major !== reqMajor) return major < reqMajor;
22+
if (minor !== reqMinor) return minor < reqMinor;
23+
return patch < reqPatch;
24+
}
725

826
function isFfiProcess(): boolean {
927
// Only execArgv decides: a stale env marker must never bypass the FFI re-exec.
@@ -66,6 +84,15 @@ function launchWindowsFallback(
6684
}
6785

6886
async function launch(): Promise<void> {
87+
if (isRuntimeTooOld()) {
88+
process.stderr.write(
89+
`Pythinker Code requires ${REQUIRED_RUNTIME}; you are running Node.js ${process.versions.node}.\n` +
90+
`${NATIVE_INSTALL_HINT}\n`,
91+
);
92+
process.exitCode = 1;
93+
return;
94+
}
95+
6996
if (isFfiProcess()) {
7097
await import(new URL('./main.mjs', import.meta.url).href);
7198
return;

0 commit comments

Comments
 (0)