@oooooooooo/ch0de 0.0.0-stage → 1.9.56

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.
Files changed (96) hide show
  1. package/API-TESTING.md +76 -0
  2. package/DEBTAUR-INTEGRATION.md +71 -0
  3. package/EDITING.md +80 -0
  4. package/EXPERT-PANEL.md +16 -0
  5. package/INTEGRATIONS.md +75 -0
  6. package/LICENSE +21 -0
  7. package/MODELS-DEV-LICENSE +21 -0
  8. package/PROVIDER-CONNECTIONS.md +48 -0
  9. package/PROVIDERS.md +17 -0
  10. package/README.md +523 -2
  11. package/RELIABILITY.md +95 -0
  12. package/UX-1.3.md +83 -0
  13. package/UX-1.4.1.md +13 -0
  14. package/UX-1.4.md +48 -0
  15. package/UX-1.5.md +13 -0
  16. package/UX-1.6.md +7 -0
  17. package/UX-1.7.3.md +13 -0
  18. package/UX-1.7.4.md +13 -0
  19. package/UX-1.7.md +19 -0
  20. package/acceptance-check.js +27 -0
  21. package/agent-budget.js +4 -0
  22. package/api-testing.js +140 -0
  23. package/automatic-re.js +19 -0
  24. package/benchmarks/public-baselines.json +29 -0
  25. package/benchmarks/render-1.4.0.json +8 -0
  26. package/benchmarks/targets.js +11 -0
  27. package/browser-check.js +1062 -0
  28. package/build-progress.js +10 -0
  29. package/build-quality.js +49 -0
  30. package/chode.js +7937 -0
  31. package/debtaur-engine.js +305 -0
  32. package/debtaur-orchestration.js +456 -0
  33. package/debtaur-worker.cjs +76 -0
  34. package/doctor.js +24 -0
  35. package/edit-transactions.js +79 -0
  36. package/elapsed.js +28 -0
  37. package/evidence-ledger.js +26 -0
  38. package/expert-panel.cjs +42 -0
  39. package/helios-ledger.js +405 -0
  40. package/install.cjs +25 -0
  41. package/integrations.js +30 -0
  42. package/json-output.js +8 -0
  43. package/local-memory.js +613 -0
  44. package/local-models.js +52 -0
  45. package/mcp-client.js +50 -0
  46. package/memory.js +511 -0
  47. package/model-catalog.js +46 -0
  48. package/native-tools.js +27 -0
  49. package/nosignups.js +325 -0
  50. package/observation-progress.js +33 -0
  51. package/orchestrator.js +299 -0
  52. package/package.json +160 -6
  53. package/plan-z6.js +490 -0
  54. package/project-memory.js +27 -0
  55. package/provider-capacity.js +13 -0
  56. package/provider-catalog.js +246 -0
  57. package/provider-connect.js +66 -0
  58. package/provider-credentials.js +5 -0
  59. package/provider-directory.js +8 -0
  60. package/provider-directory.json +32300 -0
  61. package/provider-errors.js +11 -0
  62. package/provider-picker.js +188 -0
  63. package/provider-policy.js +4 -0
  64. package/provider-race.js +25 -0
  65. package/provider-request.js +45 -0
  66. package/provider-store.js +13 -0
  67. package/rea-environment.js +5 -0
  68. package/read-window.js +21 -0
  69. package/recovery-context.js +3 -0
  70. package/reliability-memory.js +38 -0
  71. package/reliability-services.js +34 -0
  72. package/reliability-store.js +16 -0
  73. package/request-deadline.js +4 -0
  74. package/serve-frontend.html +172 -0
  75. package/settings-reset.js +11 -0
  76. package/skill-catalog.js +16 -0
  77. package/task-group.js +22 -0
  78. package/terminal-graphics.js +39 -0
  79. package/terminal-logo.js +76 -0
  80. package/terminal-ui.js +79 -0
  81. package/tool-call-validation.js +26 -0
  82. package/tool-health.js +22 -0
  83. package/tool-history.js +5 -0
  84. package/toolbox.js +156 -0
  85. package/tui-provider-picker.js +134 -0
  86. package/tui-worker.js +32 -0
  87. package/web-context.js +232 -0
  88. package/wolfram-math.js +6 -0
  89. package/worker-transport.js +11 -0
  90. package/workspace-commands.js +45 -0
  91. package/workspace-log.js +56 -0
  92. package/workspace-store.js +7 -0
  93. package/workspace-tui.js +113 -0
  94. package/workspace-view.js +109 -0
  95. package/worktrees.js +18 -0
  96. package/z6-integration.js +227 -0
@@ -0,0 +1,246 @@
1
+ 'use strict';
2
+ /**
3
+ * provider-catalog.js
4
+ *
5
+ * A single data-driven view of every provider chode can connect to.
6
+ *
7
+ * It unifies three sources into one browsable list:
8
+ * 1. the built-in registry (PROVIDERS, including saved "setup" customs),
9
+ * 2. the community directory profiles (provider-directory.json), and
10
+ * 3. saved custom connections (already folded into PROVIDERS by reloadConnections).
11
+ *
12
+ * Each entry carries live status (is it configured? is a key resolvable? how
13
+ * many models? which tier?) and, for providers that offer more than one way to
14
+ * authenticate, an ordered list of access `variants` (e.g. OVH: "free, no key"
15
+ * plus "API key / credits").
16
+ *
17
+ * The module is dependency-light and synchronous so it can drive both the
18
+ * terminal picker and the TUI picker. It never performs network I/O: callers
19
+ * pass in already-discovered data (e.g. the Ollama model list) when they want
20
+ * richer local status.
21
+ */
22
+
23
+ const directory = require('./provider-directory');
24
+ const credentials = require('./provider-credentials');
25
+
26
+ // Canonical tier labels, in display order.
27
+ const TIERS = ['free', 'keyless', 'local', 'paid', 'configured'];
28
+
29
+ function categoryToTier(category) {
30
+ switch (category) {
31
+ case 'free_tier': return 'free';
32
+ case 'keyless': return 'keyless';
33
+ case 'free_local': return 'local';
34
+ case 'paid': return 'paid';
35
+ case 'configured': return 'configured';
36
+ default: return 'configured';
37
+ }
38
+ }
39
+
40
+ function normalizeBase(url) {
41
+ try { return String(url || '').replace(/\/+$/, '').replace(/\/chat\/completions$/i, ''); }
42
+ catch (e) { return String(url || ''); }
43
+ }
44
+
45
+ function baseOfBuiltIn(p) {
46
+ const ep = p && p.endpoints && p.endpoints[0];
47
+ const u = typeof ep && ep.url === 'function' ? null : ep && ep.url;
48
+ return u ? normalizeBase(u) : null;
49
+ }
50
+
51
+ function baseOfProfile(profile) {
52
+ return profile && profile.baseUrl ? normalizeBase(profile.baseUrl) : null;
53
+ }
54
+
55
+ // Build the variant list for a provider. Free/no-key variants sort first so the
56
+ // zero-setup path is always the default, which is what "free tiers first" means.
57
+ function variantsFor({ requiresKey, keyEnv, tier, modelCount, freeTier, pricedModels }) {
58
+ const variants = [];
59
+ if (!requiresKey) {
60
+ variants.push({
61
+ label: tier === 'local' ? 'Local (no key)' : 'No key (free)',
62
+ tier: tier === 'local' ? 'local' : 'keyless',
63
+ requiresKey: false,
64
+ keyEnv: null,
65
+ note: freeTier || (tier === 'local' ? 'Uses your running local server.' : 'Anonymous / keyless access.'),
66
+ modelCount: modelCount
67
+ });
68
+ } else {
69
+ variants.push({
70
+ label: 'API key (' + (keyEnv || 'key') + ')',
71
+ tier: tier === 'free' ? 'free' : 'paid',
72
+ requiresKey: true,
73
+ keyEnv: keyEnv || null,
74
+ note: pricedModels ? 'Uses account credits / pricing.' : 'Authenticated account access.',
75
+ modelCount: modelCount
76
+ });
77
+ }
78
+ return variants;
79
+ }
80
+
81
+ /**
82
+ * Build the unified catalog.
83
+ *
84
+ * @param {object} engine the chode module (for PROVIDERS, loadConfig, loadProviderSettings)
85
+ * @param {object} [opts]
86
+ * @param {object} [opts.env] env map used for key resolution (default process.env)
87
+ * @param {Array} [opts.localModels] discovered local model ids (e.g. Ollama)
88
+ * @param {boolean}[opts.probeLocal] set true to mark the local server as up (caller verified)
89
+ * @returns {Array} catalog entries
90
+ */
91
+ function all(engine, opts = {}) {
92
+ const env = opts.env || process.env;
93
+ const providers = (engine && engine.PROVIDERS) || {};
94
+ const config = (engine && typeof engine.loadConfig === 'function') ? engine.loadConfig(true) : {};
95
+ const settingsFor = (id) => (engine && typeof engine.loadProviderSettings === 'function')
96
+ ? engine.loadProviderSettings(id, false)
97
+ : ((config.providers && config.providers[id]) || {});
98
+
99
+ const byId = new Map();
100
+
101
+ // 1) built-in registry + saved customs (source 'setup')
102
+ for (const [id, p] of Object.entries(providers)) {
103
+ if (!p || p.category === 'bootstrap') continue;
104
+ const tier = categoryToTier(p.category);
105
+ const keyEnv = p.requiresKey || null;
106
+ const key = credentials.resolve(p, settingsFor(id), env);
107
+ const isLocal = p.category === 'free_local';
108
+ const modelCount = (p.endpoints && p.endpoints.length) || 1;
109
+ byId.set(id, {
110
+ id,
111
+ name: p.name || id,
112
+ source: p.source === 'setup' ? 'setup' : 'builtin',
113
+ category: p.category,
114
+ tier,
115
+ keyEnv,
116
+ requiresKey: !!keyEnv,
117
+ configured: !!(config.providers && config.providers[id]) || p.source === 'setup',
118
+ hasKey: !!key,
119
+ modelCount,
120
+ hasFreeModels: tier === 'keyless' || tier === 'local',
121
+ doc: p.signupUrl || null,
122
+ signupUrl: p.signupUrl || null,
123
+ baseUrl: baseOfBuiltIn(p),
124
+ local: isLocal,
125
+ localUp: isLocal ? (opts.probeLocal === true || (Array.isArray(opts.localModels) && opts.localModels.length > 0)) : null,
126
+ verification: p.source === 'setup' ? 'Saved connection; account access is yours to verify.' : 'Built-in route; free tier / keyless as labelled.',
127
+ _models: null,
128
+ variants: variantsFor({ requiresKey: !!keyEnv, keyEnv, tier, modelCount, freeTier: p.freeTier })
129
+ });
130
+ }
131
+
132
+ // 2) community directory profiles
133
+ const merged = new Set();
134
+ for (const profile of directory.list()) {
135
+ const base = baseOfProfile(profile);
136
+ // Merge a keyed directory twin into a matching keyless built-in as a variant
137
+ // (e.g. ovh + ovhcloud, agnes + agnes). Key on identical base URL.
138
+ if (base && profile.env && !profile.local) {
139
+ for (const existing of byId.values()) {
140
+ if (!existing.requiresKey && existing.baseUrl && existing.baseUrl === base) {
141
+ const models = (profile.models || []);
142
+ existing.variants.push({
143
+ label: 'API key (' + (profile.env || 'key') + ')',
144
+ tier: 'paid',
145
+ requiresKey: true,
146
+ keyEnv: profile.env || null,
147
+ note: 'Authenticated account access; uses account credits / pricing.',
148
+ modelCount: models.length,
149
+ profileId: profile.id
150
+ });
151
+ existing.hasPaidVariant = true;
152
+ existing._models = models;
153
+ if (!existing.doc) existing.doc = profile.doc;
154
+ merged.add(profile.id);
155
+ break;
156
+ }
157
+ }
158
+ }
159
+ if (merged.has(profile.id)) continue; // folded in as a variant above
160
+ if (byId.has(profile.id)) {
161
+ // Same id as a built-in: keep the built-in entry, but fold in the richer
162
+ // directory model list + doc so the picker can search/filter models.
163
+ const existing = byId.get(profile.id);
164
+ const models = (profile.models || []);
165
+ existing._models = models.length ? models : existing._models;
166
+ if (models.length) existing.modelCount = models.length;
167
+ if (models.some(m => m && m.zeroTokenPrice)) existing.hasFreeModels = true;
168
+ if (profile.doc && !existing.doc) existing.doc = profile.doc;
169
+ continue;
170
+ }
171
+ const keyEnv = profile.env || null;
172
+ const tier = profile.local ? 'local' : (keyEnv ? 'configured' : 'keyless');
173
+ const models = (profile.models || []);
174
+ byId.set(profile.id, {
175
+ id: profile.id,
176
+ name: profile.name || profile.id,
177
+ source: 'directory',
178
+ category: profile.local ? 'free_local' : (keyEnv ? 'configured' : 'keyless'),
179
+ tier,
180
+ keyEnv,
181
+ requiresKey: !!keyEnv && !profile.local,
182
+ configured: !!(config.providers && config.providers[profile.id]),
183
+ hasKey: keyEnv ? !!credentials.resolve({ requiresKey: keyEnv }, settingsFor(profile.id), env) : false,
184
+ modelCount: models.length,
185
+ hasFreeModels: models.some(m => m && m.zeroTokenPrice) || !!profile.local,
186
+ doc: profile.doc || null,
187
+ signupUrl: null,
188
+ baseUrl: base,
189
+ local: !!profile.local,
190
+ localUp: profile.local ? (opts.probeLocal === true || (Array.isArray(opts.localModels) && opts.localModels.length > 0)) : null,
191
+ verification: profile.verification || 'community-listed; authenticated access not independently verified',
192
+ _models: models,
193
+ variants: variantsFor({
194
+ requiresKey: !!keyEnv && !profile.local,
195
+ keyEnv: profile.local ? null : keyEnv,
196
+ tier: profile.local ? 'local' : (keyEnv ? 'paid' : 'keyless'),
197
+ modelCount: models.length,
198
+ freeTier: profile.local ? 'Local server.' : undefined,
199
+ pricedModels: models.some(m => m && (m.inputPrice || m.outputPrice))
200
+ })
201
+ });
202
+ }
203
+
204
+ const entries = [...byId.values()];
205
+ // Stable, useful ordering: by tier (free/keyless/local first), then name.
206
+ const tierRank = Object.fromEntries(TIERS.map((t, i) => [t, i]));
207
+ entries.sort((a, b) => ((tierRank[a.tier] ?? 99) - (tierRank[b.tier] ?? 99)) || a.name.localeCompare(b.name));
208
+ return entries;
209
+ }
210
+
211
+ /**
212
+ * Filter catalog entries.
213
+ * @param {Array} entries
214
+ * @param {object} [f]
215
+ * @param {string} [f.query] case-insensitive match on id/name/model ids
216
+ * @param {string} [f.tier] one of TIERS
217
+ * @param {boolean}[f.configured] only configured
218
+ * @param {boolean}[f.hasKey] only those with a resolvable key
219
+ * @param {boolean}[f.free] only free/keyless/local tiers
220
+ */
221
+ function filter(entries, f = {}) {
222
+ const q = String(f.query || '').toLowerCase();
223
+ const freeTiers = new Set(['free', 'keyless', 'local']);
224
+ return entries.filter(e => {
225
+ if (f.tier && e.tier !== f.tier) return false;
226
+ if (f.free && !freeTiers.has(e.tier)) return false;
227
+ if (f.configured && !e.configured) return false;
228
+ if (f.hasKey && !e.hasKey) return false;
229
+ if (q) {
230
+ const models = (e._models || []).map(m => m && m.id).join(' ');
231
+ const hay = (e.id + ' ' + e.name + ' ' + (e.keyEnv || '') + ' ' + models).toLowerCase();
232
+ if (!hay.includes(q)) return false;
233
+ }
234
+ return true;
235
+ });
236
+ }
237
+
238
+ /** Find an entry by exact id or case-insensitive id/name. */
239
+ function find(entries, idOrName) {
240
+ const q = String(idOrName || '').toLowerCase();
241
+ if (!q) return null;
242
+ return entries.find(e => e.id === q) || entries.find(e => e.id.toLowerCase() === q) ||
243
+ entries.find(e => e.name.toLowerCase() === q) || null;
244
+ }
245
+
246
+ module.exports = { all, filter, find, TIERS, categoryToTier, variantsFor };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+ const crypto=require('crypto'),{spawn}=require('child_process');
3
+ function endpoint(original,settings={}){const ep={...original},model=settings.model;if(model){const body=ep.body;ep.body=(key,messages)=>{const data=JSON.parse(body(key,messages));if(Object.hasOwn(data,'model'))data.model=model;return JSON.stringify(data);};const url=ep.url;ep.url=key=>{let value=typeof url==='function'?url(key):url;if(/generativelanguage\.googleapis\.com/.test(value))value=value.replace(/\/models\/[^:]+:generateContent/,'/models/'+encodeURIComponent(model.replace(/^models\//,''))+':generateContent');if(/api\.cloudflare\.com/.test(value)){if(settings.accountId)value=value.replace(/\/accounts\/[^/]*/,'/accounts/'+encodeURIComponent(settings.accountId));value=value.replace(/\/ai\/run\/.*/,'/ai/run/'+model);}return value;};}return ep;}
4
+ function url(value){const u=new URL(value);if(u.username||u.password||u.search||u.hash)throw Error('Use a base URL without credentials, query parameters or fragments.');if(u.protocol!=='https:'&&!(u.protocol==='http:'&&['localhost','127.0.0.1','[::1]'].includes(u.hostname)))throw Error('Use HTTPS, or HTTP for a local server.');return u.href.replace(/\/$/,'');}
5
+ function loadConnections(config,options={}){const out={};for(const [id,c]of Object.entries(config.connections||{})){try{if(!/^[a-z][a-z0-9_-]{1,39}$/.test(id))continue;const base=url(c.baseUrl);if(c.local&&!['localhost','127.0.0.1','[::1]'].includes(new URL(base).hostname))continue;const entry={name:String(c.name||id),source:'setup',credentialScope:options.scope||'user',category:c.local?'free_local':c.auth==='none'?'keyless':'configured',requiresKey:c.auth==='none'?null:(/^[A-Z_][A-Z0-9_]*$/.test(c.keyEnv||'')?c.keyEnv:'CHODE_'+id.toUpperCase()+'_KEY'),endpoints:[{type:'chat',url:base+'/chat/completions',headers:key=>({'Content-Type':'application/json',...(c.auth==='none'?{}:{Authorization:'Bearer '+key})}),body:(key,messages)=>JSON.stringify({model:c.model,messages,max_tokens:4096}),parse:data=>data.choices?.[0]?.message?.content}]};if(options.scope==='project'){entry.requiresKey=c.auth==='none'?null:'CHODE_'+id.toUpperCase()+'_KEY';const loopback=['localhost','127.0.0.1','[::1]'].includes(new URL(base).hostname);entry.needsConfirmation=!(typeof config.providers?.[id]?.key==='string'&&config.providers[id].key)&&!(c.auth==='none'&&loopback);}if(!require('./provider-policy').isRetired(id,entry))out[id]=entry;}catch{}}return out;}
6
+ function openBrowser(link){const args=process.platform==='win32'?['rundll32.exe',['url.dll,FileProtocolHandler',link]]:process.platform==='darwin'?['open',[link]]:['xdg-open',[link]];try{const child=spawn(...args,{windowsHide:true,stdio:'ignore'});child.on('error',()=>{});child.unref();}catch{}}
7
+ const {request}=require('./provider-request');
8
+ function choice(value,items){const n=Number(value);return Number.isInteger(n)&&n>=1&&n<=items.length?items[n-1]:items.find(x=>String(x).toLowerCase()===String(value).toLowerCase());}
9
+ async function run(engine,options={}){const ask=options.ask,notice=options.notice||(()=>{}),fetcher=options.fetch||fetch,signal=options.signal;async function prompt(title,extra={}){if(signal?.aborted)throw Error('Setup cancelled.');const result=await ask({title,...extra});if(result==null||signal?.aborted)throw Error('Setup cancelled.');return String(result).trim();}async function pick(title,items){for(let i=0;i<3;i++){const found=choice(await prompt(title,{options:items}),items);if(found)return found;notice('Choose a listed number or name.');}throw Error('No selection made.');}
10
+ const cfg=engine.loadConfig(true),wasFree=engine.defaultFreeMode?.(),ids=Object.keys(engine.PROVIDERS),directory=require('./provider-directory');let idInput=options.provider;
11
+ if(!idInput){let page=0,query='',entries;for(let attempt=0;attempt<40;attempt++){entries=[...new Set([...ids,...directory.list(query).map(p=>p.id)])].filter(id=>!query||(id+' '+(engine.PROVIDERS[id]?.name||directory.find(id)?.name||'')).toLowerCase().includes(query));const pageItems=entries.slice(page*15,(page+1)*15);const value=await prompt('Connect a provider (page '+(page+1)+'/'+Math.max(1,Math.ceil(entries.length/15))+'). Enter an ID/name, next, previous, search <text>, or custom.',{options:[...pageItems,'custom']});if(value.toLowerCase()==='next'){if((page+1)*15<entries.length)page++;else notice('No more providers.');continue;}if(value.toLowerCase()==='previous'){page=Math.max(0,page-1);continue;}if(/^search(?:\s|$)/i.test(value)){query=value.slice(6).trim();page=0;continue;}idInput=choice(value,[...pageItems,'custom'])||value;break;}}
12
+ let id=choice(idInput,[...ids,'custom']);let profile=null;const dirProfile=directory.find(idInput);const exactDirId=!!(dirProfile&&dirProfile.id.toLowerCase()===String(idInput).toLowerCase());if(!id){if(exactDirId){id=dirProfile.id;profile=dirProfile;}else{const matches=ids.filter(key=>engine.PROVIDERS[key].name.split('(')[0].trim().toLowerCase()===String(idInput).toLowerCase());if(matches.length===1)id=matches[0];else if(dirProfile){id=dirProfile.id;profile=dirProfile;}}}if(!id)throw Error('Unknown provider. Use search <name> in /connect, or choose custom.');let provider=engine.PROVIDERS[id],connection=null,settings={},key='';
13
+ if(profile){const consent=await pick('Connect '+profile.name+'? Endpoint: '+profile.baseUrl+'. '+(profile.local?'Requires your running local server and installed model; API key authentication is not enabled in this preset.':'Key environment: '+profile.env+'. Community catalog, not an authenticated verification. Free pricing does not guarantee free account access.')+' Review '+profile.doc,['Continue to setup','Cancel']);if(consent==='Cancel')throw Error('Setup cancelled.');connection=directory.connection(profile);provider=loadConnections({connections:{[id]:connection}})[id];if(!provider)throw Error('Provider profile is unsupported.');}
14
+
15
+ if(id==='custom'){id=(await prompt('Provider ID (lowercase letters, numbers, hyphens; e.g. my-server)')).toLowerCase();if(!/^[a-z][a-z0-9_-]{1,39}$/.test(id)||engine.PROVIDERS[id])throw Error('Choose a new provider ID with 2–40 characters.');connection={name:await prompt('Display name'),baseUrl:url(await prompt('OpenAI-compatible API base URL, including /v1 if required')),auth:await pick('Authentication',['API key','No key (local or public endpoint)']),model:'default'};connection.auth=connection.auth==='API key'?'bearer':'none';provider=loadConnections({connections:{[id]:connection}})[id];if(!provider)throw Error('This provider is unavailable or permanently retired.');}
16
+ if(provider?.needsConfirmation){const confirmed=await pick('Confirm this project connection before using account credentials: '+provider.endpoints[0].url,['Continue to setup','Cancel']);if(confirmed==='Cancel')throw Error('Setup cancelled.');connection={...cfg.connections[id]};}
17
+ const saved=engine.loadProviderSettings?engine.loadProviderSettings(id,true):(cfg.providers?.[id]||{});settings={...saved};
18
+ if(provider.requiresKey){const methods=[...(require('./provider-credentials').resolve(provider,saved)?['Use existing key']:[]),'Paste API key',...(id==='openrouter'?['Browser login (OAuth PKCE)']:[])];const method=methods.length===1?methods[0]:await pick('How would you like to connect?',methods);if(method==='Use existing key')key=require('./provider-credentials').resolve(provider,saved);
19
+ else if(method==='Paste API key')key=await prompt('Paste API key. Input is hidden and never added to chat history.',{secret:true});
20
+ else{const verifier=crypto.randomBytes(48).toString('base64url'),challenge=crypto.createHash('sha256').update(verifier).digest('base64url'),auth=new URL('https://openrouter.ai/auth');auth.searchParams.set('code_challenge',challenge);auth.searchParams.set('code_challenge_method','S256');auth.searchParams.set('key_label','Chode');notice('Open this URL to sign in and authorize Chode. Works from SSH too:\n'+auth.href);(options.open||openBrowser)(auth.href);const code=await prompt('After authorizing, paste the single-use authorization code shown by OpenRouter.',{secret:true});const response=await request(fetcher,'https://openrouter.ai/api/v1/auth/keys',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({code,code_verifier:verifier,code_challenge_method:'S256'})},signal);if(!response.ok)throw Error('Browser login failed or expired. Restart /connect; no credentials were saved.');const data=await response.json();key=data.key;}
21
+ key=require('./provider-credentials').normalize(key);settings.key=key;settings.keySource='setup';
22
+ }
23
+ if(id==='cloudflare'){settings.accountId=await prompt('Cloudflare account ID (not the API token)');if(!/^[a-zA-Z0-9_-]{1,128}$/.test(settings.accountId))throw Error('Invalid account ID.');}
24
+ let ep=endpoint(provider.endpoints[0],settings),defaultModel='';try{defaultModel=JSON.parse(ep.body(key,[])).model||'';}catch{}if(!defaultModel){const value=typeof ep.url==='function'?ep.url(key):ep.url;defaultModel=(/\/models\/([^:]+):/.exec(value)||/\/ai\/run\/(.+)$/.exec(value)||[])[1]||'';}
25
+ let models=profile?directory.models(profile):[],discoveryAttempted=false;try{let target=typeof ep.url==='function'?ep.url(key):ep.url;if(/\/chat\/completions$/.test(target))target=target.replace(/\/chat\/completions$/,'/models');else if(id==='ollama')target=target.replace(/\/api\/chat$/,'/api/tags');else target=null;if(target){discoveryAttempted=true;const response=await request(fetcher,target,{headers:ep.headers(key)},signal);if(response.ok){const data=await response.json();const discovered=require('./model-catalog').normalize(data,{local:id==='ollama'||!!profile?.local});if(discovered.length)models=discovered;}}}catch{if(signal?.aborted)throw Error('Setup cancelled.');notice('Model discovery unavailable. Enter a model ID manually.');}
26
+ if(discoveryAttempted&&!models.length)notice('Model list came back empty. Enter the exact model ID manually.');
27
+ if(models.length)notice('Model metadata is reported by the provider, not a quality benchmark. Listed zero prices do not remove account limits; changing models does not reset shared quotas.');
28
+ let model=await require('./model-catalog').select(models,defaultModel,prompt,notice);settings.model=model;if(connection)connection.model=model;ep=endpoint(provider.endpoints[0],settings);
29
+ const action=await pick('Check this connection? A test sends a small model request and may use provider credits.',['Test connection','Save without testing','Cancel']);if(action==='Cancel')throw Error('Setup cancelled.');let tested=false,testFailure=null;if(action==='Test connection'){for(let attempt=0;attempt<3;attempt++){try{const data=JSON.parse(ep.body(key,[{role:'user',content:'Reply with OK.'}]));if(data.max_tokens)data.max_tokens=16;if(data.generationConfig)data.generationConfig.maxOutputTokens=16;const response=await request(fetcher,typeof ep.url==='function'?ep.url(key):ep.url,{method:'POST',headers:ep.headers(key),body:JSON.stringify(data)},signal);const payload=await response.json();if(!response.ok){testFailure=require('./provider-errors').classify(response.status,payload);throw Object.assign(Error('HTTP '+response.status+': '+testFailure.message),{providerFailure:testFailure});}if(!ep.parse(payload))throw Error('Empty model response');tested=true;testFailure=null;break;}catch(err){if(signal?.aborted)throw Error('Setup cancelled.');if(err.providerFailure&&['quota','permission','model'].includes(err.providerFailure.category)){notice(err.message);const next=await pick('The connection is not ready. How should setup continue?',['Choose another model','Save without a successful test','Cancel']);if(next==='Cancel')throw Error('Setup cancelled; no settings or credentials were saved.');if(next==='Save without a successful test')break;if(attempt===2)throw Error('Model test limit reached. No settings saved.');model=await require('./model-catalog').select(models,model,prompt,notice);settings.model=model;if(connection)connection.model=model;ep=endpoint(provider.endpoints[0],settings);continue;}const clean=String(err.message||'unknown error').split(key||'\0').join('[redacted]');throw Error('Connection test failed ('+clean+'). No settings or credentials were saved. Check the key, model and provider access, then reconnect.');}}}
30
+
31
+ const save=await pick('Save '+provider.name+' / '+model+' locally? API keys are stored unencrypted in local config, with restricted file permissions where supported; they never enter chat or exports.',['Save and select provider (leave strict free mode if a key is required)','Save only','Cancel']);if(save==='Cancel')throw Error('Setup cancelled.');cfg.providers=cfg.providers||{};const shared=options.scope!=='project'&&typeof engine.saveProviderSettings==='function';if(shared){engine.saveProviderSettings(id,settings,connection);const metadata={...settings,keySource:'global'};delete metadata.key;cfg.providers[id]=metadata;notice('Credentials saved for your user account across workspaces; project config contains no copied API key.');}else cfg.providers[id]=settings;if(connection){cfg.connections=cfg.connections||{};cfg.connections[id]=connection;}if(signal?.aborted)throw Error('Setup cancelled.');if(save.startsWith('Save and select')){cfg.defaultProvider=id;cfg.freeOnly=require('./provider-capacity').free(provider);}else if(cfg.freeOnly===undefined&&typeof wasFree==='boolean')cfg.freeOnly=wasFree;engine.saveConfig(cfg);engine.reloadConnections?.();engine.clearAuthFailure?.(id);notice('Saved '+provider.name+' / '+model+(tested?' · connection tested.':testFailure?' · test failed: '+testFailure.category+'; not ready.':' · not tested.'));return {provider:id,model,tested,testFailure,select:save.startsWith('Save and select'),freeOnly:require('./provider-capacity').free(provider)};
32
+ }
33
+ async function cli(engine,id,opts={}){
34
+ const input=opts.input||process.stdin,output=opts.output||process.stderr;
35
+ if(!input.isTTY||!output.isTTY)throw Error('Provider setup requires an interactive terminal. Run chode connect, or /connect inside Chode.');
36
+ const {Writable}=require('stream');
37
+ const ask=q=>new Promise((resolve,reject)=>{output.write('\n'+q.title+'\n'+(q.options||[]).map((x,i)=>(i+1)+'. '+x+'\n').join('')+'› ');const out=new Writable({write(chunk,enc,done){if(!q.secret)output.write(chunk);done();}});out.isTTY=true;out.columns=output.columns||80;const rl=require('readline').createInterface({input:input,output:out,terminal:true,historySize:0});let settled=false;rl.on('SIGINT',()=>{settled=true;rl.close();resolve(null);});rl.on('close',()=>{if(!settled)resolve(null);});rl.question('',answer=>{settled=true;rl.close();if(q.secret)output.write('\n');resolve(answer);});});
38
+ const notice=text=>output.write(text+'\n');
39
+ const catalog=require('./provider-catalog');const picker=require('./provider-picker');
40
+ let entries=[];try{entries=catalog.all(engine,{env:process.env});}catch(e){}
41
+ let providerId=id,variantIndex=0;
42
+ if(!providerId){
43
+ const sel=await picker.select(input,output,{entries});
44
+ if(!sel){notice('No provider selected.');return {cancelled:true};}
45
+ providerId=sel.id;variantIndex=sel.variantIndex;
46
+ }else{
47
+ const entry=catalog.find(entries,providerId);
48
+ if(entry&&entry.variants.length>1){
49
+ const labels=entry.variants.map((v,i)=>v.label+(v.note?' — '+v.note:''));
50
+ const ans=await ask({title:'Select '+entry.name+' access mode:',options:labels});
51
+ const n=Number(ans);variantIndex=(Number.isInteger(n)&&n>=1&&n<=entry.variants.length)?n-1:0;
52
+ }
53
+ }
54
+ const wizardId=resolveWizardId(entries,providerId,variantIndex);
55
+ return run(engine,{provider:wizardId,ask,notice});}
56
+ // Map a catalog selection (provider id + variant index) to the concrete
57
+ // provider id the setup wizard should run against. A keyed "variant" that came
58
+ // from a merged directory twin (e.g. OVH paid -> ovhcloud) routes to that
59
+ // profile; everything else uses the id as-is.
60
+ function resolveWizardId(entries, providerId, variantIndex = 0) {
61
+ const catalog = require('./provider-catalog');
62
+ const entry = catalog.find(entries || [], providerId);
63
+ const variant = entry && entry.variants[variantIndex];
64
+ return (variant && variant.profileId) ? variant.profileId : providerId;
65
+ }
66
+ module.exports={run,cli,endpoint,loadConnections,url,resolveWizardId};
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+ function environment(provider,env=process.env){for(const name of [provider?.requiresKey,...(provider?.keyAliases||[])])if(name&&typeof env[name]==='string'&&env[name].trim())return env[name].trim();return null;}
3
+ function resolve(provider,settings={},env=process.env){if(!provider?.requiresKey)return null;return settings.keySource==='setup'&&settings.key?settings.key:environment(provider,env)||settings.key||null;}
4
+ function normalize(value){const key=String(value||'').trim().replace(/^(?:Authorization:\s*)?Bearer\s+/i,'');if(!key||key.length>4096||/[\s\x00-\x1f\x7f]/.test(key))throw Error('Paste only the API key, without quotes or additional header text. Nothing was saved.');return key;}
5
+ module.exports={environment,resolve,normalize};
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+ const data=require('./provider-directory.json');
3
+ function list(query=''){const q=String(query).toLowerCase();return data.profiles.filter(p=>!q||(p.id+' '+p.name+' '+p.models.map(m=>m.id).join(' ')).toLowerCase().includes(q));}
4
+ function find(id){const q=String(id||'').toLowerCase();return data.profiles.find(p=>p.id===q||p.name.toLowerCase()===q);}
5
+ function connection(p){return {name:p.name,baseUrl:p.baseUrl,auth:p.local?'none':'bearer',local:!!p.local,keyEnv:p.env,model:p.models[0]?.id||'',directoryId:p.id};}
6
+ function models(p){return p.models.map(m=>({id:m.id,zeroTokenPrice:m.zeroTokenPrice,local:false,tools:m.tools,structured:false,context:m.context}));}
7
+ function summary(query,providers={}){const q=String(query||'').toLowerCase(),built=Object.entries(providers).filter(([id,p])=>!q||(id+' '+p.name).toLowerCase().includes(q)).map(([id,p])=>({id,name:p.name,builtIn:true,local:p.category==='free_local',listedZeroPrice:false,verification:'Configured/built-in route; test account and model access',doc:p.signupUrl}));return built.concat(list(query).filter(p=>!providers[p.id]).map(p=>({id:p.id,name:p.name,baseUrl:p.baseUrl,keyEnv:p.env,models:p.models.length,listedZeroPrice:p.models.some(m=>m.zeroTokenPrice),local:!!p.local,verification:p.verification,doc:p.doc})));}
8
+ module.exports={list,find,connection,models,summary,metadata:()=>({source:data.source,retrievedAt:data.retrievedAt,sha256:data.sourceSha256})};