@namzu/cli 2.5.0 → 2.6.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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Typing a credential into a running namzu, for this session only.
3
+ *
4
+ * Someone who has just installed namzu and has no key reaches a screen that
5
+ * lists three sources and says "then restart namzu". That is accurate and it is
6
+ * a cliff: the product tells them to leave it in order to use it.
7
+ *
8
+ * ## Why session-only, stated as a decision rather than a limitation
9
+ *
10
+ * The obvious durable home is the OS keychain, and here it does not exist. The
11
+ * keychain helper in this package is macOS-only, and it reads and writes a
12
+ * DIFFERENT product's credential store — namzu borrows an OAuth token from it.
13
+ * Writing a namzu key into that envelope would put our secret under their name.
14
+ * On Windows, which is where this was asked for, there is no keychain path at
15
+ * all.
16
+ *
17
+ * So the durable options were a plaintext file or a per-platform credential
18
+ * store nobody has written. Keeping the key in memory for the session is the
19
+ * honest third answer: nothing lands on disk, it works everywhere, and the
20
+ * screen names the environment variable that makes it durable. A secret at rest
21
+ * should be something the operator chose, not something that arrived because
22
+ * they typed into a text field.
23
+ *
24
+ * Everything decidable lives here rather than in the component, because this
25
+ * package has no component tests and a secret is the worst thing to leave
26
+ * unverifiable.
27
+ */
28
+ /**
29
+ * What a key looks like on screen. Never the key.
30
+ *
31
+ * Enough tail to tell two keys apart when you have pasted the wrong one, and
32
+ * never enough to reconstruct one from a screen recording or a scrollback that
33
+ * outlives the session. The length is not revealed either: a fixed-width mask
34
+ * would leak it, and key length distinguishes vendors and sometimes tiers.
35
+ */
36
+ export function maskKey(key) {
37
+ const trimmed = key.trim();
38
+ if (trimmed.length === 0)
39
+ return '';
40
+ const tail = trimmed.slice(-4);
41
+ return trimmed.length <= 4 ? '••••' : `••••••••${tail}`;
42
+ }
43
+ /**
44
+ * Whether a typed key is worth trying at all.
45
+ *
46
+ * Deliberately shallow. Anything beyond "not empty, no whitespace, not obviously
47
+ * a shell fragment" is guessing at vendor formats that change, and a validator
48
+ * that rejects a NEW valid key format is worse than one that lets the provider
49
+ * answer. The provider is the authority; this only catches the paste that
50
+ * plainly went wrong.
51
+ */
52
+ export function keyLooksUsable(key) {
53
+ const trimmed = key.trim();
54
+ if (trimmed.length === 0)
55
+ return { ok: false, reason: 'No key entered.' };
56
+ if (/\s/.test(trimmed)) {
57
+ return { ok: false, reason: 'That contains a space — check for a truncated or wrapped paste.' };
58
+ }
59
+ if (trimmed.startsWith('$') || trimmed.includes('=')) {
60
+ return {
61
+ ok: false,
62
+ // The classic: pasting `ANTHROPIC_API_KEY=sk-…` or `$MY_KEY` from a
63
+ // shell profile rather than the value.
64
+ reason: 'That looks like a shell assignment or variable. Paste the key itself.',
65
+ };
66
+ }
67
+ return { ok: true };
68
+ }
69
+ /**
70
+ * What namzu tells the operator it did with what they typed.
71
+ *
72
+ * Every branch says three things: that it worked or did not, that the key is
73
+ * held for this session only, and the environment variable that persists it.
74
+ * The middle one is the part a person is most likely to assume otherwise —
75
+ * typing a credential into an application usually means it was saved.
76
+ */
77
+ export function describeDisposition(entry, verification) {
78
+ const persist = entry.envVars[0];
79
+ const durable = persist
80
+ ? `To keep it, set ${persist} in your shell and restart.`
81
+ : 'To keep it, configure the credential in your environment and restart.';
82
+ if (verification.kind === 'rejected') {
83
+ return `${entry.label} rejected that key: ${verification.reason}\nNothing was stored.`;
84
+ }
85
+ const checked = verification.kind === 'verified'
86
+ ? `${entry.label} accepted the key.`
87
+ : // Said plainly rather than implied. A driver with no cheap check
88
+ // cannot confirm a key without spending a turn, and claiming it is
89
+ // good would be inventing a verification that did not happen.
90
+ `Key accepted. ${entry.label} offers no way to check it without a request, so the first message will be the test.`;
91
+ return `${checked}\nHeld in memory for this session only — it is not written anywhere. ${durable}`;
92
+ }
93
+ /**
94
+ * The detected-provider record a typed key produces.
95
+ *
96
+ * Shaped exactly like a discovered one so every downstream path — session
97
+ * construction, the model picker, `/provider` — treats it identically. The only
98
+ * difference is `source`, so a surface can say where it came from.
99
+ */
100
+ export function sessionCredential(entry, key) {
101
+ return {
102
+ entry,
103
+ source: { kind: 'session' },
104
+ apiKey: key.trim(),
105
+ ...(entry.defaultBaseUrl !== undefined ? { baseUrl: entry.defaultBaseUrl } : {}),
106
+ alternatives: [],
107
+ };
108
+ }
109
+ //# sourceMappingURL=credential-entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-entry.js","sourceRoot":"","sources":["../../src/tui/credential-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,GAAW;IAClC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC1B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACnC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9B,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAA;AACxD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACzC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC1B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAA;IACzE,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iEAAiE,EAAE,CAAA;IAChG,CAAC;IACD,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACtD,OAAO;YACN,EAAE,EAAE,KAAK;YACT,oEAAoE;YACpE,uCAAuC;YACvC,MAAM,EAAE,uEAAuE;SAC/E,CAAA;IACF,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;AACpB,CAAC;AASD;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAClC,KAA4B,EAC5B,YAA0B;IAE1B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IAChC,MAAM,OAAO,GAAG,OAAO;QACtB,CAAC,CAAC,mBAAmB,OAAO,6BAA6B;QACzD,CAAC,CAAC,uEAAuE,CAAA;IAE1E,IAAI,YAAY,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QACtC,OAAO,GAAG,KAAK,CAAC,KAAK,uBAAuB,YAAY,CAAC,MAAM,uBAAuB,CAAA;IACvF,CAAC;IAED,MAAM,OAAO,GACZ,YAAY,CAAC,IAAI,KAAK,UAAU;QAC/B,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,oBAAoB;QACpC,CAAC,CAAC,iEAAiE;YAClE,mEAAmE;YACnE,8DAA8D;YAC9D,iBAAiB,KAAK,CAAC,KAAK,sFAAsF,CAAA;IAErH,OAAO,GAAG,OAAO,wEAAwE,OAAO,EAAE,CAAA;AACnG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAA4B,EAAE,GAAW;IAC1E,OAAO;QACN,KAAK;QACL,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;QAC3B,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE;QAClB,GAAG,CAAC,KAAK,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChF,YAAY,EAAE,EAAE;KAChB,CAAA;AACF,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What the model step of the picker offers, and what it says about the list.
3
+ *
4
+ * Pure on purpose. The TUI has no component tests, so logic that lives inside a
5
+ * React component is logic nobody can check — and the owner is currently the
6
+ * only person able to see this screen at all. Everything decidable is decided
7
+ * here, and `Picker` is left as a thin caller.
8
+ *
9
+ * ## The default is always offered
10
+ *
11
+ * Every branch returns at least the provider's `defaultModel`. A screen that
12
+ * can end with nothing selectable is a dead end for someone who came here to
13
+ * get on with a task, and the default is exactly the value they would have got
14
+ * by not visiting this step.
15
+ *
16
+ * ## The notice says which case it is
17
+ *
18
+ * "No models" is the same words for four different facts — the driver does not
19
+ * implement listing, the provider publishes none, it timed out, it errored —
20
+ * and three of them are not "this provider has no models". The timeout matters
21
+ * most: the honest line is "it did not answer in time", because the operator
22
+ * can retry that, and cannot retry a provider that genuinely has one model.
23
+ */
24
+ import type { ModelListing } from './agent.js';
25
+ export interface ModelChoice {
26
+ readonly id: string;
27
+ readonly label: string;
28
+ /** Shown beside the row. `(default)` for the provider's own default. */
29
+ readonly note?: string;
30
+ }
31
+ export interface ModelStep {
32
+ readonly choices: readonly ModelChoice[];
33
+ /**
34
+ * Why the list is short or absent, or `null` when it is a real full list.
35
+ *
36
+ * Rendered above the choices. `null` means the list is exactly what the
37
+ * provider said it was, so a notice would be noise.
38
+ */
39
+ readonly notice: string | null;
40
+ /** Index of the row to start on: the current model, else the default. */
41
+ readonly initialIndex: number;
42
+ }
43
+ /**
44
+ * Build the model step for one provider.
45
+ *
46
+ * @param defaultModel The provider's own default, always offered.
47
+ * @param listing What asking the provider produced.
48
+ * @param currentModel The model in force now, if any — so re-opening the picker
49
+ * starts on what is already selected rather than resetting to the default.
50
+ */
51
+ export declare function modelStep(defaultModel: string, listing: ModelListing, currentModel?: string): ModelStep;
52
+ //# sourceMappingURL=model-choices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-choices.d.ts","sourceRoot":"","sources":["../../src/tui/model-choices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,wEAAwE;IACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAA;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,YAAY,EACrB,YAAY,CAAC,EAAE,MAAM,GACnB,SAAS,CA4CX"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * What the model step of the picker offers, and what it says about the list.
3
+ *
4
+ * Pure on purpose. The TUI has no component tests, so logic that lives inside a
5
+ * React component is logic nobody can check — and the owner is currently the
6
+ * only person able to see this screen at all. Everything decidable is decided
7
+ * here, and `Picker` is left as a thin caller.
8
+ *
9
+ * ## The default is always offered
10
+ *
11
+ * Every branch returns at least the provider's `defaultModel`. A screen that
12
+ * can end with nothing selectable is a dead end for someone who came here to
13
+ * get on with a task, and the default is exactly the value they would have got
14
+ * by not visiting this step.
15
+ *
16
+ * ## The notice says which case it is
17
+ *
18
+ * "No models" is the same words for four different facts — the driver does not
19
+ * implement listing, the provider publishes none, it timed out, it errored —
20
+ * and three of them are not "this provider has no models". The timeout matters
21
+ * most: the honest line is "it did not answer in time", because the operator
22
+ * can retry that, and cannot retry a provider that genuinely has one model.
23
+ */
24
+ /**
25
+ * Build the model step for one provider.
26
+ *
27
+ * @param defaultModel The provider's own default, always offered.
28
+ * @param listing What asking the provider produced.
29
+ * @param currentModel The model in force now, if any — so re-opening the picker
30
+ * starts on what is already selected rather than resetting to the default.
31
+ */
32
+ export function modelStep(defaultModel, listing, currentModel) {
33
+ const fallback = (notice) => ({
34
+ choices: [{ id: defaultModel, label: defaultModel, note: '(default)' }],
35
+ notice,
36
+ initialIndex: 0,
37
+ });
38
+ if (listing.kind === 'unsupported') {
39
+ return fallback('This provider does not publish a model list. Showing its default.');
40
+ }
41
+ if (listing.kind === 'timeout') {
42
+ // Deliberately not "no models". The provider may have hundreds; it just
43
+ // did not answer inside 3s, and that is a retryable condition.
44
+ return fallback('The provider did not answer in time. Showing its default — try again to list.');
45
+ }
46
+ if (listing.kind === 'failed') {
47
+ return fallback(`Could not list models: ${listing.reason}. Showing the default.`);
48
+ }
49
+ if (listing.models.length === 0) {
50
+ return fallback('The provider returned no models. Showing its default.');
51
+ }
52
+ // A real list. Make sure the default is in it and marked, because a list
53
+ // that omits the value the user would otherwise get is missing the one entry
54
+ // they can be sure works.
55
+ const seen = new Set();
56
+ const choices = [];
57
+ for (const m of listing.models) {
58
+ if (seen.has(m.id))
59
+ continue;
60
+ seen.add(m.id);
61
+ choices.push({
62
+ id: m.id,
63
+ label: m.name,
64
+ ...(m.id === defaultModel ? { note: '(default)' } : {}),
65
+ });
66
+ }
67
+ if (!seen.has(defaultModel)) {
68
+ choices.unshift({ id: defaultModel, label: defaultModel, note: '(default)' });
69
+ }
70
+ const wanted = currentModel ?? defaultModel;
71
+ const idx = choices.findIndex((c) => c.id === wanted);
72
+ return { choices, notice: null, initialIndex: idx >= 0 ? idx : 0 };
73
+ }
74
+ //# sourceMappingURL=model-choices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-choices.js","sourceRoot":"","sources":["../../src/tui/model-choices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAwBH;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CACxB,YAAoB,EACpB,OAAqB,EACrB,YAAqB;IAErB,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAa,EAAE,CAAC,CAAC;QAChD,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QACvE,MAAM;QACN,YAAY,EAAE,CAAC;KACf,CAAC,CAAA;IAEF,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QACpC,OAAO,QAAQ,CAAC,mEAAmE,CAAC,CAAA;IACrF,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAChC,wEAAwE;QACxE,+DAA+D;QAC/D,OAAO,QAAQ,CAAC,+EAA+E,CAAC,CAAA;IACjG,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,QAAQ,CAAC,0BAA0B,OAAO,CAAC,MAAM,wBAAwB,CAAC,CAAA;IAClF,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,QAAQ,CAAC,uDAAuD,CAAC,CAAA;IACzE,CAAC;IAED,yEAAyE;IACzE,6EAA6E;IAC7E,0BAA0B;IAC1B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,MAAM,OAAO,GAAkB,EAAE,CAAA;IACjC,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,SAAQ;QAC5B,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QACd,OAAO,CAAC,IAAI,CAAC;YACZ,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,KAAK,EAAE,CAAC,CAAC,IAAI;YACb,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvD,CAAC,CAAA;IACH,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;IAC9E,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,IAAI,YAAY,CAAA;IAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAA;IAErD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AACnE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/cli",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "Operator CLI for the Namzu agent platform — namzu doctor + future commands. Dual-purpose: standalone bin (`namzu doctor`) and library (`import { runDoctor } from '@namzu/cli'`).",
5
5
  "keywords": [
6
6
  "namzu",
@@ -45,14 +45,15 @@
45
45
  "react": "^19.2.6",
46
46
  "yaml": "^2.9.0",
47
47
  "@namzu/ollama": "2.0.1",
48
- "@namzu/anthropic": "3.1.1",
49
48
  "@namzu/openai": "1.1.1",
49
+ "@namzu/sdk": "^16.0.0",
50
50
  "@namzu/openrouter": "2.0.1",
51
- "@namzu/sdk": "^15.1.0"
51
+ "@namzu/anthropic": "3.1.1"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@biomejs/biome": "^1.9.4",
55
55
  "@types/react": "^19.2.15",
56
+ "ink-testing-library": "^4.0.0",
56
57
  "typescript": "^5.5.0",
57
58
  "vitest": "^3.2.6"
58
59
  },