@mmmbuto/nexuscrew 0.8.0 → 0.8.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/README.md +43 -51
- package/bin/nexuscrew.js +10 -4
- package/frontend/dist/assets/index-COsoJxXQ.css +32 -0
- package/frontend/dist/assets/index-Dey9rdY-.js +90 -0
- package/frontend/dist/assets/qr-scanner-worker.min-D85Z9gVD.js +98 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +175 -100
- package/lib/cli/init.js +32 -7
- package/lib/cli/service.js +0 -5
- package/lib/decks/store.js +8 -2
- package/lib/files/routes.js +3 -1
- package/lib/fleet/builtin.js +15 -5
- package/lib/fleet/managed.js +277 -107
- package/lib/mcp/server.js +24 -5
- package/lib/nodes/commands.js +27 -12
- package/lib/nodes/peering.js +116 -0
- package/lib/nodes/store.js +91 -24
- package/lib/nodes/tunnel.js +22 -7
- package/lib/proxy/federation.js +217 -0
- package/lib/proxy/node-proxy.js +21 -8
- package/lib/server.js +90 -7
- package/lib/settings/routes.js +216 -4
- package/package.json +2 -2
- package/frontend/dist/assets/index-BBOgTCoO.css +0 -32
- package/frontend/dist/assets/index-DQrDBogT.js +0 -83
package/lib/cli/service.js
CHANGED
|
@@ -70,7 +70,6 @@ After=network-online.target
|
|
|
70
70
|
[Service]
|
|
71
71
|
Type=simple
|
|
72
72
|
WorkingDirectory=${repo}
|
|
73
|
-
Environment=NEXUSCREW_PORT=${ctx.port}
|
|
74
73
|
Environment=PATH=${nodeDir}:/usr/local/bin:/usr/bin:/bin
|
|
75
74
|
ExecStart=${node} ${repoBin} serve
|
|
76
75
|
Restart=on-failure
|
|
@@ -86,7 +85,6 @@ function generateMac(ctx) {
|
|
|
86
85
|
const repoBinXml = escapeXml(path.join(ctx.repoRoot, 'bin', 'nexuscrew.js'));
|
|
87
86
|
const repoXml = escapeXml(ctx.repoRoot);
|
|
88
87
|
const homeXml = escapeXml(ctx.home);
|
|
89
|
-
const port = ctx.port;
|
|
90
88
|
const launchPath = [...new Set([
|
|
91
89
|
path.dirname(ctx.nodeBin), '/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin',
|
|
92
90
|
])].join(':');
|
|
@@ -107,8 +105,6 @@ function generateMac(ctx) {
|
|
|
107
105
|
<string>${repoXml}</string>
|
|
108
106
|
<key>EnvironmentVariables</key>
|
|
109
107
|
<dict>
|
|
110
|
-
<key>NEXUSCREW_PORT</key>
|
|
111
|
-
<string>${port}</string>
|
|
112
108
|
<key>PATH</key>
|
|
113
109
|
<string>${launchPathXml}</string>
|
|
114
110
|
</dict>
|
|
@@ -136,7 +132,6 @@ function generateTermux(ctx) {
|
|
|
136
132
|
# NexusCrew boot (Termux) - loopback, localhost del telefono
|
|
137
133
|
export PATH=/data/data/com.termux/files/usr/bin:$PATH
|
|
138
134
|
export HOME=/data/data/com.termux/files/home
|
|
139
|
-
export NEXUSCREW_PORT=${ctx.port}
|
|
140
135
|
cd -- ${repoQ}
|
|
141
136
|
termux-wake-lock 2>/dev/null || true
|
|
142
137
|
mkdir -p "$HOME/.nexuscrew"
|
package/lib/decks/store.js
CHANGED
|
@@ -8,7 +8,13 @@ const SCHEMA_VERSION = 1;
|
|
|
8
8
|
const MAX_DECKS = 24;
|
|
9
9
|
const MAX_TILES = 9;
|
|
10
10
|
const NAME_RE = /^[a-z0-9-]{1,32}$/;
|
|
11
|
-
const NODE_RE = /^[a-z0-9-]{1,32}$/;
|
|
11
|
+
const NODE_RE = /^[a-z0-9-]{1,32}(?:\/[a-z0-9-]{1,32}){0,3}$/;
|
|
12
|
+
|
|
13
|
+
function validNodeRoute(node) {
|
|
14
|
+
if (!NODE_RE.test(node)) return false;
|
|
15
|
+
const parts = node.split('/');
|
|
16
|
+
return new Set(parts).size === parts.length;
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
function defaultDecksPath(home) {
|
|
14
20
|
return path.join(home || os.homedir(), '.nexuscrew', 'decks.json');
|
|
@@ -41,7 +47,7 @@ function parseLayout(raw) {
|
|
|
41
47
|
const tiles = [];
|
|
42
48
|
for (const t of c.tiles) {
|
|
43
49
|
if (!t || typeof t !== 'object' || Array.isArray(t) || !validText(t.session, 128)) return null;
|
|
44
|
-
if (t.node !== undefined && (typeof t.node !== 'string' || !
|
|
50
|
+
if (t.node !== undefined && (typeof t.node !== 'string' || !validNodeRoute(t.node))) return null;
|
|
45
51
|
const height = Number(t.height);
|
|
46
52
|
const fontSize = Number(t.fontSize);
|
|
47
53
|
if (!Number.isFinite(height) || height < 0.2 || height > 100) return null;
|
package/lib/files/routes.js
CHANGED
|
@@ -9,7 +9,7 @@ const store = require('./store.js');
|
|
|
9
9
|
|
|
10
10
|
// Router file-exchange. Nessuno stato: tutto deriva da cfg + filesystem.
|
|
11
11
|
// notifier (opzionale, MCP bridge): emette la notify di consegna file outbox.
|
|
12
|
-
function filesRoutes({ cfg, sessionExists, paste, notifier }) {
|
|
12
|
+
function filesRoutes({ cfg, sessionExists, paste, notifier, readonly = () => false }) {
|
|
13
13
|
const router = Router();
|
|
14
14
|
const upload = multer({
|
|
15
15
|
storage: multer.memoryStorage(),
|
|
@@ -17,6 +17,7 @@ function filesRoutes({ cfg, sessionExists, paste, notifier }) {
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
router.post('/upload', (req, res) => {
|
|
20
|
+
if (readonly()) return res.status(403).json({ error: 'READONLY: upload bloccato' });
|
|
20
21
|
upload.single('file')(req, res, async (err) => {
|
|
21
22
|
if (err) {
|
|
22
23
|
const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
|
|
@@ -104,6 +105,7 @@ function filesRoutes({ cfg, sessionExists, paste, notifier }) {
|
|
|
104
105
|
});
|
|
105
106
|
|
|
106
107
|
router.delete('/', (req, res) => {
|
|
108
|
+
if (readonly()) return res.status(403).json({ error: 'READONLY: eliminazione file bloccata' });
|
|
107
109
|
const ok = store.removeFile(
|
|
108
110
|
cfg.filesRoot, String(req.query.session || ''), String(req.query.box || ''), String(req.query.name || ''),
|
|
109
111
|
);
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -29,7 +29,7 @@ const {
|
|
|
29
29
|
loadDefinitions, atomicWrite, CAPS,
|
|
30
30
|
} = require('./definitions.js');
|
|
31
31
|
const {
|
|
32
|
-
|
|
32
|
+
publicCatalog, describeManaged, resolveManagedEngine, discoverOllamaModels, discoverPiModels,
|
|
33
33
|
} = require('./managed.js');
|
|
34
34
|
|
|
35
35
|
const STATUS_TTL_MS = 2000;
|
|
@@ -273,6 +273,8 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
273
273
|
});
|
|
274
274
|
const needsOllama = cache.defs.engines.some((e) => e.managed?.provider === 'ollama-cloud');
|
|
275
275
|
const ollamaModels = needsOllama ? await discoverOllamaModels({ ...cfg, home }) : [];
|
|
276
|
+
const needsPi = cache.defs.engines.some((e) => e.managed?.client === 'pi');
|
|
277
|
+
const piModels = needsPi ? await discoverPiModels({ ...cfg, home }) : {};
|
|
276
278
|
const engines = cache.defs.engines.map((e) => {
|
|
277
279
|
const managed = e.managed ? describeManaged(e.managed, { ...cfg, home }) : null;
|
|
278
280
|
return {
|
|
@@ -280,7 +282,8 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
280
282
|
...(managed ? {
|
|
281
283
|
kind: 'managed', client: managed.client, provider: managed.provider,
|
|
282
284
|
model: e.managed.model || managed.defaultModel || '',
|
|
283
|
-
models: managed.provider === 'ollama-cloud' ? ollamaModels
|
|
285
|
+
models: managed.provider === 'ollama-cloud' ? ollamaModels
|
|
286
|
+
: (managed.client === 'pi' ? (e.managed.provider === 'custom' ? [e.managed.model] : (piModels[e.managed.provider] || [])) : managed.models),
|
|
284
287
|
configured: managed.configured, reason: managed.reason,
|
|
285
288
|
} : { kind: 'custom', configured: true, model: e.model?.value || '', models: [] }),
|
|
286
289
|
};
|
|
@@ -504,7 +507,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
504
507
|
return out;
|
|
505
508
|
}),
|
|
506
509
|
cells: defs.cells.map((c) => ({ ...c, ...(c.models ? { models: { ...c.models } } : {}) })),
|
|
507
|
-
managedCatalog:
|
|
510
|
+
managedCatalog: publicCatalog(),
|
|
508
511
|
managedConfig: { providerSecretsPath: cfg.providerSecretsPath || path.join(home, '.nexuscrew', 'providers.env') },
|
|
509
512
|
};
|
|
510
513
|
}
|
|
@@ -536,9 +539,16 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
536
539
|
rc: { type: 'boolean', required: false, default: false },
|
|
537
540
|
managed: {
|
|
538
541
|
type: 'object', requiredFor: 'managed',
|
|
539
|
-
client: { type: 'enum', values: ['claude', 'codex-vl'] },
|
|
540
|
-
provider: { type: '
|
|
542
|
+
client: { type: 'enum', values: ['claude', 'codex', 'codex-vl', 'pi'] },
|
|
543
|
+
provider: { type: 'catalog', source: 'managedCatalog' },
|
|
544
|
+
credentialProfile: { type: 'string', required: false, max: 32 },
|
|
541
545
|
model: { type: 'string', required: false, max: CAPS.MAX_MODEL_VAL_LEN },
|
|
546
|
+
permissionPolicy: { type: 'enum', values: ['standard', 'unsafe'], default: 'standard' },
|
|
547
|
+
displayName: { type: 'string', requiredFor: 'provider=custom', max: CAPS.MAX_LABEL_LEN },
|
|
548
|
+
protocol: { type: 'catalog', source: 'managedCatalog.protocol' },
|
|
549
|
+
baseUrl: { type: 'string', requiredFor: 'provider=custom', max: CAPS.MAX_COMMAND_LEN },
|
|
550
|
+
envKey: { type: 'string', requiredFor: 'provider=custom', pattern: '^[A-Za-z_][A-Za-z0-9_]{0,63}$' },
|
|
551
|
+
providerId: { type: 'string', requiredFor: 'provider=custom', pattern: '^[a-z][a-z0-9_-]{0,31}$' },
|
|
542
552
|
},
|
|
543
553
|
command: { type: 'string', requiredFor: 'custom', max: CAPS.MAX_COMMAND_LEN, absolute: true },
|
|
544
554
|
args: { type: 'array', of: 'string', required: false, max: CAPS.MAX_ARGS, itemMax: CAPS.MAX_ARG_LEN },
|
package/lib/fleet/managed.js
CHANGED
|
@@ -1,53 +1,124 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Managed AI engine registry. Definitions contain adapter/provider metadata but
|
|
3
|
+
// never secret values. Commands are direct argv; no shell or chat-protocol
|
|
4
|
+
// compatibility fallback is allowed.
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { execFile } = require('node:child_process');
|
|
7
9
|
|
|
8
10
|
const OLLAMA_CLOUD_MODELS = Object.freeze([
|
|
9
11
|
'glm-5.2', 'kimi-k2.7-code', 'deepseek-v4-pro', 'minimax-m3',
|
|
10
12
|
'qwen3.5:397b', 'deepseek-v4-flash', 'mistral-large-3:675b', 'gemma4:31b',
|
|
11
13
|
]);
|
|
12
14
|
const OLLAMA_CONTEXT = Object.freeze({
|
|
13
|
-
'glm-5.2': 1000000,
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'minimax-m3': 524288,
|
|
17
|
-
'qwen3.5:397b': 262144,
|
|
18
|
-
'deepseek-v4-flash': 1048576,
|
|
19
|
-
'mistral-large-3:675b': 262144,
|
|
20
|
-
'gemma4:31b': 262144,
|
|
15
|
+
'glm-5.2': 1000000, 'kimi-k2.7-code': 262144, 'deepseek-v4-pro': 524288,
|
|
16
|
+
'minimax-m3': 524288, 'qwen3.5:397b': 262144, 'deepseek-v4-flash': 1048576,
|
|
17
|
+
'mistral-large-3:675b': 262144, 'gemma4:31b': 262144,
|
|
21
18
|
});
|
|
22
19
|
|
|
20
|
+
const CUSTOM_KEYS = ['displayName', 'protocol', 'baseUrl', 'envKey', 'providerId'];
|
|
21
|
+
const MANAGED_KEYS = new Set(['client', 'provider', 'credentialProfile', 'model', 'permissionPolicy', ...CUSTOM_KEYS]);
|
|
22
|
+
const CLIENT_LABELS = Object.freeze({ claude: 'Claude Code', codex: 'Codex', 'codex-vl': 'Codex-VL', pi: 'Pi' });
|
|
23
|
+
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
24
|
+
const PROVIDER_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
25
|
+
|
|
26
|
+
function validBaseUrl(value) {
|
|
27
|
+
if (typeof value !== 'string' || value.length > 512 || /\s|[\x00-\x1f\x7f]/.test(value)) return false;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(value);
|
|
30
|
+
return (parsed.protocol === 'http:' || parsed.protocol === 'https:')
|
|
31
|
+
&& !parsed.username && !parsed.password && !parsed.hash;
|
|
32
|
+
} catch (_) { return false; }
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
const CATALOG = Object.freeze([
|
|
24
|
-
|
|
25
|
-
{ id: '
|
|
26
|
-
{ id: 'claude.ollama-cloud', client: 'claude', provider: 'ollama-cloud', label: 'Claude · Ollama Cloud
|
|
27
|
-
{ id: '
|
|
28
|
-
{ id: 'claude.zai-a', client: 'claude', provider: 'zai
|
|
29
|
-
{ id: 'claude.zai-p', client: 'claude', provider: 'zai
|
|
36
|
+
// Claude Code
|
|
37
|
+
{ id: 'claude.native', client: 'claude', provider: 'native', label: 'Claude · Anthropic', auth: 'login', endpoint: 'Anthropic account', protocol: 'anthropic_messages', rc: true, default: true },
|
|
38
|
+
{ id: 'claude.ollama-cloud', client: 'claude', provider: 'ollama-cloud', label: 'Claude · Ollama Cloud', auth: 'OLLAMA_API_KEY', endpoint: 'https://ollama.com', protocol: 'anthropic_messages', model: 'glm-5.2', models: OLLAMA_CLOUD_MODELS, legacySecrets: true },
|
|
39
|
+
{ id: 'claude.ollama', client: 'claude', provider: 'ollama', label: 'Claude · Ollama local', auth: 'none', endpoint: 'http://127.0.0.1:11434', protocol: 'anthropic_messages' },
|
|
40
|
+
{ id: 'claude.zai-a', client: 'claude', provider: 'zai', credentialProfile: 'a', label: 'Claude · Z.AI · profile A', auth: 'ZAI_API_KEY_A', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.2[1m]', models: ['glm-5.2[1m]'], legacySecrets: true, legacyProvider: 'zai-a' },
|
|
41
|
+
{ id: 'claude.zai-p', client: 'claude', provider: 'zai', credentialProfile: 'p', label: 'Claude · Z.AI · profile P', auth: 'ZAI_API_KEY_P', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.2[1m]', models: ['glm-5.2[1m]'], legacySecrets: true, legacyProvider: 'zai-p' },
|
|
42
|
+
{ id: 'claude.custom', client: 'claude', provider: 'custom', label: 'Claude · Custom', auth: 'dynamic', protocol: 'anthropic_messages', protocols: ['anthropic_messages'], custom: true },
|
|
43
|
+
|
|
44
|
+
// Codex family. OpenAI Responses is the only remote custom wire API.
|
|
45
|
+
{ id: 'codex.native', client: 'codex', provider: 'native', label: 'Codex · OpenAI', auth: 'login', endpoint: 'OpenAI account', protocol: 'openai_responses', default: true },
|
|
46
|
+
{ id: 'codex-vl.native', client: 'codex-vl', provider: 'native', label: 'Codex-VL · OpenAI', auth: 'login', endpoint: 'OpenAI account', protocol: 'openai_responses', default: true },
|
|
47
|
+
{ id: 'codex.ollama', client: 'codex', provider: 'ollama', label: 'Codex · Ollama local', auth: 'none', endpoint: 'local provider', protocol: 'openai_responses', localProvider: 'ollama' },
|
|
48
|
+
{ id: 'codex-vl.ollama', client: 'codex-vl', provider: 'ollama', label: 'Codex-VL · Ollama local', auth: 'none', endpoint: 'local provider', protocol: 'openai_responses', localProvider: 'ollama' },
|
|
49
|
+
{ id: 'codex.lmstudio', client: 'codex', provider: 'lmstudio', label: 'Codex · LM Studio', auth: 'none', endpoint: 'local provider', protocol: 'openai_responses', localProvider: 'lmstudio' },
|
|
50
|
+
{ id: 'codex-vl.lmstudio', client: 'codex-vl', provider: 'lmstudio', label: 'Codex-VL · LM Studio', auth: 'none', endpoint: 'local provider', protocol: 'openai_responses', localProvider: 'lmstudio' },
|
|
51
|
+
{ id: 'codex.ollama-cloud', client: 'codex', provider: 'ollama-cloud', label: 'Codex · Ollama Cloud', auth: 'OLLAMA_API_KEY', endpoint: 'https://ollama.com/v1', protocol: 'openai_responses', model: 'glm-5.2', models: OLLAMA_CLOUD_MODELS, legacySecrets: true },
|
|
52
|
+
{ id: 'codex-vl.ollama-cloud', client: 'codex-vl', provider: 'ollama-cloud', label: 'Codex-VL · Ollama Cloud', auth: 'OLLAMA_API_KEY', endpoint: 'https://ollama.com/v1', protocol: 'openai_responses', model: 'glm-5.2', models: OLLAMA_CLOUD_MODELS, legacySecrets: true },
|
|
53
|
+
{ id: 'codex.custom', client: 'codex', provider: 'custom', label: 'Codex · Custom Responses', auth: 'dynamic', protocol: 'openai_responses', protocols: ['openai_responses'], custom: true },
|
|
54
|
+
{ id: 'codex-vl.custom', client: 'codex-vl', provider: 'custom', label: 'Codex-VL · Custom Responses', auth: 'dynamic', protocol: 'openai_responses', protocols: ['openai_responses'], custom: true },
|
|
55
|
+
|
|
56
|
+
// Pi uses its real provider IDs directly. OAuth providers do not need env keys.
|
|
57
|
+
{ id: 'pi.anthropic', client: 'pi', provider: 'anthropic', label: 'Pi · Anthropic', auth: 'ANTHROPIC_API_KEY', protocol: 'pi_native', piProvider: 'anthropic' },
|
|
58
|
+
{ id: 'pi.openai', client: 'pi', provider: 'openai', label: 'Pi · OpenAI', auth: 'OPENAI_API_KEY', protocol: 'pi_native', piProvider: 'openai' },
|
|
59
|
+
{ id: 'pi.google', client: 'pi', provider: 'google', label: 'Pi · Google Gemini', auth: 'GEMINI_API_KEY', protocol: 'pi_native', piProvider: 'google' },
|
|
60
|
+
{ id: 'pi.openrouter', client: 'pi', provider: 'openrouter', label: 'Pi · OpenRouter', auth: 'OPENROUTER_API_KEY', protocol: 'pi_native', piProvider: 'openrouter' },
|
|
61
|
+
{ id: 'pi.github-copilot', client: 'pi', provider: 'github-copilot', label: 'Pi · GitHub Copilot', auth: 'login', protocol: 'pi_native', piProvider: 'github-copilot' },
|
|
62
|
+
{ id: 'pi.fireworks', client: 'pi', provider: 'fireworks', label: 'Pi · Fireworks AI', auth: 'FIREWORKS_API_KEY', protocol: 'pi_native', piProvider: 'fireworks' },
|
|
63
|
+
{ id: 'pi.huggingface', client: 'pi', provider: 'huggingface', label: 'Pi · Hugging Face', auth: 'HF_TOKEN', protocol: 'pi_native', piProvider: 'huggingface' },
|
|
64
|
+
{ id: 'pi.minimax', client: 'pi', provider: 'minimax', label: 'Pi · MiniMax', auth: 'MINIMAX_API_KEY', protocol: 'pi_native', piProvider: 'minimax' },
|
|
65
|
+
{ id: 'pi.deepseek', client: 'pi', provider: 'deepseek', label: 'Pi · DeepSeek', auth: 'DEEPSEEK_API_KEY', protocol: 'pi_native', piProvider: 'deepseek' },
|
|
66
|
+
{ id: 'pi.kimi-coding', client: 'pi', provider: 'kimi-coding', label: 'Pi · Kimi For Coding', auth: 'KIMI_API_KEY', protocol: 'pi_native', piProvider: 'kimi-coding' },
|
|
67
|
+
{ id: 'pi.mistral', client: 'pi', provider: 'mistral', label: 'Pi · Mistral', auth: 'MISTRAL_API_KEY', protocol: 'pi_native', piProvider: 'mistral' },
|
|
68
|
+
{ id: 'pi.together', client: 'pi', provider: 'together', label: 'Pi · Together AI', auth: 'TOGETHER_API_KEY', protocol: 'pi_native', piProvider: 'together' },
|
|
69
|
+
{ id: 'pi.ollama', client: 'pi', provider: 'ollama', label: 'Pi · Ollama local', auth: 'none', protocol: 'openai-completions', piProvider: 'ollama', requiresModel: true, piExtension: { baseUrl: 'http://127.0.0.1:11434/v1', apiKey: 'ollama' } },
|
|
70
|
+
{ id: 'pi.zai', client: 'pi', provider: 'zai', label: 'Pi · Z.AI', auth: 'ZAI_API_KEY', protocol: 'pi_native', piProvider: 'zai' },
|
|
71
|
+
{ id: 'pi.custom', client: 'pi', provider: 'custom', label: 'Pi · Custom provider', auth: 'dynamic', protocol: 'openai-responses', protocols: ['openai-responses', 'anthropic-messages', 'openai-completions', 'google-generative-ai'], custom: true },
|
|
30
72
|
]);
|
|
31
73
|
|
|
32
|
-
|
|
74
|
+
function profileFor(client, provider, credentialProfile) {
|
|
75
|
+
return CATALOG.find((p) => p.client === client && p.provider === provider
|
|
76
|
+
&& (p.credentialProfile || '') === (credentialProfile || '')) || null;
|
|
77
|
+
}
|
|
33
78
|
|
|
34
79
|
function normalizeManagedSpec(value) {
|
|
35
80
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
81
|
+
if (Object.keys(value).some((k) => !MANAGED_KEYS.has(k))) return null;
|
|
82
|
+
if (!CLIENT_LABELS[value.client]) return null;
|
|
83
|
+
let provider = value.provider;
|
|
84
|
+
let credentialProfile = value.credentialProfile || '';
|
|
85
|
+
// 0.8.0 compatibility: zai-a/zai-p were encoded as providers.
|
|
86
|
+
if (value.client === 'claude' && (provider === 'zai-a' || provider === 'zai-p')) {
|
|
87
|
+
credentialProfile = provider.slice(-1);
|
|
88
|
+
provider = 'zai';
|
|
89
|
+
}
|
|
90
|
+
if (typeof provider !== 'string') return null;
|
|
91
|
+
const profile = profileFor(value.client, provider, credentialProfile);
|
|
39
92
|
if (!profile) return null;
|
|
40
93
|
const model = value.model === undefined ? (profile.model || '') : value.model;
|
|
41
|
-
if (typeof model !== 'string' || model.length > 128) return null;
|
|
42
|
-
|
|
94
|
+
if (typeof model !== 'string' || model.length > 128 || /[\x00-\x1f\x7f]/.test(model)) return null;
|
|
95
|
+
if (profile.requiresModel && !model) return null;
|
|
96
|
+
const permissionPolicy = value.permissionPolicy === undefined ? 'standard' : value.permissionPolicy;
|
|
97
|
+
if (permissionPolicy !== 'standard' && permissionPolicy !== 'unsafe') return null;
|
|
98
|
+
if (value.client === 'pi' && permissionPolicy !== 'standard') return null;
|
|
99
|
+
const out = { client: profile.client, provider: profile.provider, model, permissionPolicy };
|
|
100
|
+
if (profile.credentialProfile) out.credentialProfile = profile.credentialProfile;
|
|
101
|
+
if (profile.custom) {
|
|
102
|
+
const displayName = typeof value.displayName === 'string' ? value.displayName.trim() : '';
|
|
103
|
+
const baseUrl = typeof value.baseUrl === 'string' ? value.baseUrl.trim() : '';
|
|
104
|
+
const envKey = typeof value.envKey === 'string' ? value.envKey.trim() : '';
|
|
105
|
+
const protocol = value.protocol || profile.protocol;
|
|
106
|
+
const providerId = typeof value.providerId === 'string' && value.providerId ? value.providerId : 'nexuscrew-custom';
|
|
107
|
+
if (!displayName || displayName.length > 64 || /[\x00-\x1f\x7f]/.test(displayName)) return null;
|
|
108
|
+
if (!validBaseUrl(baseUrl)) return null;
|
|
109
|
+
if (!ENV_KEY_RE.test(envKey) || !PROVIDER_ID_RE.test(providerId)) return null;
|
|
110
|
+
if (!model || !(profile.protocols || [profile.protocol]).includes(protocol)) return null;
|
|
111
|
+
Object.assign(out, { displayName, baseUrl, envKey, protocol, providerId });
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
43
114
|
}
|
|
44
115
|
|
|
45
116
|
function defaultDefinitions() {
|
|
46
117
|
return {
|
|
47
118
|
schemaVersion: 1,
|
|
48
119
|
engines: CATALOG.filter((p) => p.default).map((p) => ({
|
|
49
|
-
id: p.id, label: p.label, rc: p.rc,
|
|
50
|
-
managed: { client: p.client, provider: p.provider, model: p.model || '' },
|
|
120
|
+
id: p.id, label: p.label, rc: !!p.rc,
|
|
121
|
+
managed: { client: p.client, provider: p.provider, model: p.model || '', permissionPolicy: 'standard' },
|
|
51
122
|
})),
|
|
52
123
|
cells: [],
|
|
53
124
|
};
|
|
@@ -64,24 +135,27 @@ function parseEnvFile(file) {
|
|
|
64
135
|
for (const line of raw.split(/\r?\n/)) {
|
|
65
136
|
const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
66
137
|
if (!m) continue;
|
|
67
|
-
let
|
|
68
|
-
if ((
|
|
69
|
-
out[m[1]] =
|
|
138
|
+
let v = m[2];
|
|
139
|
+
if ((v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))) v = v.slice(1, -1);
|
|
140
|
+
out[m[1]] = v;
|
|
70
141
|
}
|
|
71
142
|
return out;
|
|
72
143
|
}
|
|
73
144
|
|
|
74
145
|
function binaryCandidates(client, home) {
|
|
75
146
|
const prefix = process.env.PREFIX || '';
|
|
76
|
-
|
|
77
|
-
return [
|
|
147
|
+
const bin = client;
|
|
148
|
+
return [...new Set([
|
|
149
|
+
path.join(home, '.local', 'bin', bin), path.join(path.dirname(process.execPath), bin),
|
|
150
|
+
`/usr/local/bin/${bin}`, `/opt/homebrew/bin/${bin}`,
|
|
151
|
+
prefix && path.join(prefix, 'bin', bin),
|
|
152
|
+
].filter(Boolean))];
|
|
78
153
|
}
|
|
79
154
|
|
|
80
155
|
function findBinary(client, home) {
|
|
81
156
|
for (const candidate of binaryCandidates(client, home)) {
|
|
82
157
|
try {
|
|
83
|
-
const real = fs.realpathSync(candidate);
|
|
84
|
-
const st = fs.lstatSync(real);
|
|
158
|
+
const real = fs.realpathSync(candidate); const st = fs.lstatSync(real);
|
|
85
159
|
if (!st.isFile() || !(st.mode & 0o100) || (st.mode & 0o002)) continue;
|
|
86
160
|
if (typeof process.getuid === 'function' && st.uid !== process.getuid() && st.uid !== 0) continue;
|
|
87
161
|
return real;
|
|
@@ -91,121 +165,217 @@ function findBinary(client, home) {
|
|
|
91
165
|
}
|
|
92
166
|
|
|
93
167
|
function secretsPath(cfg, home) {
|
|
94
|
-
return cfg.providerSecretsPath || process.env.NEXUSCREW_PROVIDER_SECRETS
|
|
95
|
-
|
|
168
|
+
return cfg.providerSecretsPath || process.env.NEXUSCREW_PROVIDER_SECRETS || path.join(home, '.nexuscrew', 'providers.env');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function credential(profile, spec, cfg, home) {
|
|
172
|
+
if (profile.auth === 'login' || profile.auth === 'none') return { envKey: profile.auth, value: '' };
|
|
173
|
+
const envKey = profile.auth === 'dynamic' ? spec.envKey : profile.auth;
|
|
174
|
+
const runtimeEnv = cfg.env || process.env;
|
|
175
|
+
let value = runtimeEnv[envKey] || '';
|
|
176
|
+
// Preserve 0.8.0 Z.AI/Ollama Cloud behavior, but new Custom never reads disk.
|
|
177
|
+
if (!value && profile.legacySecrets) value = parseEnvFile(secretsPath(cfg, home))[envKey] || '';
|
|
178
|
+
return { envKey, value };
|
|
96
179
|
}
|
|
97
180
|
|
|
98
181
|
let ollamaCache = { at: 0, models: [] };
|
|
99
182
|
async function discoverOllamaModels(opts = {}) {
|
|
100
|
-
const now = Date.now();
|
|
101
|
-
const ttl = opts.ttlMs === undefined ? 30000 : opts.ttlMs;
|
|
183
|
+
const now = Date.now(); const ttl = opts.ttlMs === undefined ? 30000 : opts.ttlMs;
|
|
102
184
|
if (!opts.noCache && ollamaCache.models.length && now - ollamaCache.at < ttl) return [...ollamaCache.models];
|
|
103
185
|
const fetchImpl = opts.fetchImpl || globalThis.fetch;
|
|
104
186
|
if (typeof fetchImpl !== 'function') return [...OLLAMA_CLOUD_MODELS];
|
|
105
187
|
try {
|
|
106
188
|
const home = opts.home || require('node:os').homedir();
|
|
107
|
-
const apiKey = opts.apiKey || parseEnvFile(secretsPath(opts, home)).OLLAMA_API_KEY;
|
|
108
|
-
if (!apiKey) throw new Error('OLLAMA_API_KEY
|
|
109
|
-
const response = await fetchImpl('https://ollama.com/api/tags', {
|
|
110
|
-
headers: { authorization: `Bearer ${apiKey}` },
|
|
111
|
-
signal: typeof AbortSignal !== 'undefined' && AbortSignal.timeout ? AbortSignal.timeout(2500) : undefined,
|
|
112
|
-
});
|
|
189
|
+
const apiKey = opts.apiKey || (opts.env || process.env).OLLAMA_API_KEY || parseEnvFile(secretsPath(opts, home)).OLLAMA_API_KEY;
|
|
190
|
+
if (!apiKey) throw new Error('OLLAMA_API_KEY missing');
|
|
191
|
+
const response = await fetchImpl('https://ollama.com/api/tags', { headers: { authorization: `Bearer ${apiKey}` }, signal: AbortSignal.timeout?.(2500) });
|
|
113
192
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
114
|
-
const body = await response.json();
|
|
115
|
-
const available = new Set();
|
|
193
|
+
const body = await response.json(); const available = new Set();
|
|
116
194
|
for (const item of Array.isArray(body.models) ? body.models : []) {
|
|
117
195
|
const name = typeof item?.name === 'string' ? item.name : '';
|
|
118
|
-
if (
|
|
119
|
-
available.add(name);
|
|
196
|
+
if (/^[A-Za-z0-9._-]+(?::[A-Za-z0-9._-]+)?$/.test(name) && name.length <= 128) available.add(name);
|
|
120
197
|
}
|
|
121
|
-
// La API diretta espone molti modelli, inclusi modelli in deprecazione. La UI
|
|
122
|
-
// mantiene la shortlist TOP curata, ma solo se ancora realmente disponibile.
|
|
123
198
|
const models = OLLAMA_CLOUD_MODELS.filter((name) => available.has(name));
|
|
124
|
-
if (!models.length) throw new Error('
|
|
125
|
-
ollamaCache = { at: now, models };
|
|
126
|
-
return [...models];
|
|
199
|
+
if (!models.length) throw new Error('no cloud models');
|
|
200
|
+
ollamaCache = { at: now, models }; return [...models];
|
|
127
201
|
} catch (_) {
|
|
128
|
-
ollamaCache = { at: now, models: [...OLLAMA_CLOUD_MODELS] };
|
|
129
|
-
|
|
202
|
+
ollamaCache = { at: now, models: [...OLLAMA_CLOUD_MODELS] }; return [...OLLAMA_CLOUD_MODELS];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let piCache = { at: 0, providers: {} };
|
|
207
|
+
let piInFlight = null;
|
|
208
|
+
async function discoverPiModels(opts = {}) {
|
|
209
|
+
const now = Date.now(); const ttl = opts.ttlMs === undefined ? 300000 : opts.ttlMs;
|
|
210
|
+
if (!opts.noCache && Object.keys(piCache.providers).length && now - piCache.at < ttl) {
|
|
211
|
+
return Object.fromEntries(Object.entries(piCache.providers).map(([k, v]) => [k, [...v]]));
|
|
212
|
+
}
|
|
213
|
+
const home = opts.home || require('node:os').homedir();
|
|
214
|
+
const binary = opts.binary || findBinary('pi', home);
|
|
215
|
+
if (!binary) return {};
|
|
216
|
+
if (!opts.noCache && piInFlight) return piInFlight;
|
|
217
|
+
const execFileImpl = opts.execFileImpl || execFile;
|
|
218
|
+
const load = async () => {
|
|
219
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
220
|
+
execFileImpl(binary, ['--list-models'], { encoding: 'utf8', timeout: 15000, maxBuffer: 1024 * 1024 }, (err, out) => {
|
|
221
|
+
if (err) reject(err); else resolve(String(out || ''));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
const providers = {};
|
|
225
|
+
for (const line of stdout.split(/\r?\n/).slice(1)) {
|
|
226
|
+
const [provider, model] = line.trim().split(/\s+/);
|
|
227
|
+
if (!PROVIDER_ID_RE.test(provider || '') || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(model || '')) continue;
|
|
228
|
+
(providers[provider] ||= []).push(model);
|
|
229
|
+
}
|
|
230
|
+
for (const key of Object.keys(providers)) providers[key] = [...new Set(providers[key])];
|
|
231
|
+
piCache = { at: now, providers };
|
|
232
|
+
return Object.fromEntries(Object.entries(providers).map(([k, v]) => [k, [...v]]));
|
|
233
|
+
};
|
|
234
|
+
if (opts.noCache) {
|
|
235
|
+
try { return await load(); } catch (_) { return {}; }
|
|
130
236
|
}
|
|
237
|
+
piInFlight = load().catch(() => ({})).finally(() => { piInFlight = null; });
|
|
238
|
+
return piInFlight;
|
|
131
239
|
}
|
|
132
240
|
|
|
133
241
|
function describeManaged(spec, cfg = {}) {
|
|
134
242
|
const normalized = normalizeManagedSpec(spec);
|
|
135
|
-
if (!normalized) return { configured: false, reason: '
|
|
243
|
+
if (!normalized) return { configured: false, reason: 'invalid managed profile' };
|
|
136
244
|
const home = cfg.home || require('node:os').homedir();
|
|
137
|
-
const profile =
|
|
245
|
+
const profile = profileFor(normalized.client, normalized.provider, normalized.credentialProfile || '');
|
|
138
246
|
const binary = findBinary(normalized.client, home);
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
247
|
+
const cred = credential(profile, normalized, cfg, home);
|
|
248
|
+
// Pi can resolve credentials from its own documented /login auth store. Do
|
|
249
|
+
// not inspect or copy that store; delegate native-provider auth to Pi.
|
|
250
|
+
const delegatedPiAuth = profile.client === 'pi' && profile.provider !== 'custom';
|
|
251
|
+
const authConfigured = delegatedPiAuth || profile.auth === 'login' || profile.auth === 'none' || !!cred.value;
|
|
143
252
|
return {
|
|
144
|
-
client: profile.client,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
253
|
+
client: profile.client, clientLabel: CLIENT_LABELS[profile.client], provider: profile.provider,
|
|
254
|
+
credentialProfile: normalized.credentialProfile || '', model: normalized.model,
|
|
255
|
+
permissionPolicy: normalized.permissionPolicy, protocol: normalized.protocol || profile.protocol,
|
|
256
|
+
endpoint: normalized.baseUrl || profile.endpoint || '', auth: cred.envKey, authConfigured,
|
|
257
|
+
configured: !!binary && authConfigured, models: [...(profile.models || [])], defaultModel: profile.model || '',
|
|
258
|
+
binary: binary || '', displayName: normalized.displayName || profile.label,
|
|
259
|
+
reason: !binary ? `client ${profile.client} not found` : (!authConfigured ? `environment variable ${cred.envKey} missing` : 'ready'),
|
|
149
260
|
};
|
|
150
261
|
}
|
|
151
262
|
|
|
263
|
+
function codexProviderArgs(id, name, baseUrl, envKey) {
|
|
264
|
+
return [
|
|
265
|
+
'-c', `model_provider=${JSON.stringify(id)}`, '-c', `model_providers.${id}.name=${JSON.stringify(name)}`,
|
|
266
|
+
'-c', `model_providers.${id}.base_url=${JSON.stringify(baseUrl)}`, '-c', `model_providers.${id}.env_key=${JSON.stringify(envKey)}`,
|
|
267
|
+
'-c', `model_providers.${id}.wire_api="responses"`,
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function writePiProviderExtension(spec, home) {
|
|
272
|
+
const dir = path.join(home, '.nexuscrew', 'pi-providers');
|
|
273
|
+
try {
|
|
274
|
+
const st = fs.lstatSync(dir);
|
|
275
|
+
if (st.isSymbolicLink() || !st.isDirectory()) throw new Error('unsafe Pi provider extension directory');
|
|
276
|
+
} catch (e) {
|
|
277
|
+
if (e.code === 'ENOENT') fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
278
|
+
else throw e;
|
|
279
|
+
}
|
|
280
|
+
fs.chmodSync(dir, 0o700);
|
|
281
|
+
const target = path.join(dir, `${spec.providerId}.ts`);
|
|
282
|
+
try {
|
|
283
|
+
if (fs.lstatSync(target).isSymbolicLink()) throw new Error('refusing symlink Pi provider extension');
|
|
284
|
+
} catch (e) {
|
|
285
|
+
if (e.code !== 'ENOENT') throw e;
|
|
286
|
+
}
|
|
287
|
+
const definition = {
|
|
288
|
+
name: spec.displayName,
|
|
289
|
+
baseUrl: spec.baseUrl,
|
|
290
|
+
apiKey: spec.apiKey || `$${spec.envKey}`,
|
|
291
|
+
authHeader: true,
|
|
292
|
+
api: spec.protocol,
|
|
293
|
+
models: [{
|
|
294
|
+
id: spec.model, name: spec.model, reasoning: false, input: ['text'],
|
|
295
|
+
contextWindow: 128000, maxTokens: 16384,
|
|
296
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
297
|
+
}],
|
|
298
|
+
};
|
|
299
|
+
const source = `// Generated by NexusCrew. Contains environment references only, never secret values.\nexport default function (pi) {\n pi.registerProvider(${JSON.stringify(spec.providerId)}, ${JSON.stringify(definition, null, 2)});\n}\n`;
|
|
300
|
+
const tmp = path.join(dir, `.${spec.providerId}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
301
|
+
try {
|
|
302
|
+
fs.writeFileSync(tmp, source, { mode: 0o600 });
|
|
303
|
+
fs.chmodSync(tmp, 0o600);
|
|
304
|
+
fs.renameSync(tmp, target);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
307
|
+
throw e;
|
|
308
|
+
}
|
|
309
|
+
return target;
|
|
310
|
+
}
|
|
311
|
+
|
|
152
312
|
function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
153
|
-
const
|
|
154
|
-
|
|
313
|
+
const spec = normalizeManagedSpec(engine.managed);
|
|
314
|
+
const info = describeManaged(spec, cfg);
|
|
315
|
+
if (!spec || !info.configured) return { ok: false, reason: info.reason, info };
|
|
155
316
|
const home = cfg.home || require('node:os').homedir();
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
args.push('--dangerously-skip-permissions');
|
|
161
|
-
if (
|
|
317
|
+
const profile = profileFor(spec.client, spec.provider, spec.credentialProfile || '');
|
|
318
|
+
const cred = credential(profile, spec, cfg, home);
|
|
319
|
+
const env = {}; const args = []; const model = cell?.model || spec.model;
|
|
320
|
+
if (spec.permissionPolicy === 'unsafe') {
|
|
321
|
+
if (spec.client === 'claude') args.push('--dangerously-skip-permissions');
|
|
322
|
+
if (spec.client === 'codex' || spec.client === 'codex-vl') args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
323
|
+
}
|
|
324
|
+
if (spec.client === 'claude') {
|
|
325
|
+
if (spec.provider === 'native') {
|
|
162
326
|
if (engine.rc !== false) args.push('--remote-control', `Cloud_${cell.id}`);
|
|
163
|
-
} else if (info.provider === 'ollama-cloud') {
|
|
164
|
-
const secrets = parseEnvFile(secretsPath(cfg, home));
|
|
165
|
-
Object.assign(env, {
|
|
166
|
-
ANTHROPIC_BASE_URL: 'https://ollama.com', ANTHROPIC_AUTH_TOKEN: secrets.OLLAMA_API_KEY,
|
|
167
|
-
ANTHROPIC_API_KEY: '',
|
|
168
|
-
ANTHROPIC_MODEL: model, ANTHROPIC_SMALL_FAST_MODEL: model, API_TIMEOUT_MS: '3000000',
|
|
169
|
-
CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(OLLAMA_CONTEXT[model] || 200000),
|
|
170
|
-
});
|
|
171
327
|
} else {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
328
|
+
const endpoint = spec.baseUrl || profile.endpoint;
|
|
329
|
+
const token = profile.auth === 'none' ? 'ollama' : cred.value;
|
|
174
330
|
Object.assign(env, {
|
|
175
|
-
ANTHROPIC_BASE_URL:
|
|
176
|
-
ANTHROPIC_MODEL: model,
|
|
177
|
-
|
|
331
|
+
ANTHROPIC_BASE_URL: endpoint, ANTHROPIC_AUTH_TOKEN: token, ANTHROPIC_API_KEY: '',
|
|
332
|
+
ANTHROPIC_MODEL: model, ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
|
333
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: model, ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
|
|
334
|
+
CLAUDE_CODE_SUBAGENT_MODEL: model, API_TIMEOUT_MS: '3000000',
|
|
335
|
+
CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(OLLAMA_CONTEXT[model] || (spec.provider === 'zai' ? 1000000 : 200000)),
|
|
178
336
|
});
|
|
179
337
|
}
|
|
180
338
|
if (model) args.push('--model', model);
|
|
181
|
-
} else {
|
|
182
|
-
args.push('--
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
339
|
+
} else if (spec.client === 'codex' || spec.client === 'codex-vl') {
|
|
340
|
+
if (profile.localProvider) args.push('--oss', '--local-provider', profile.localProvider);
|
|
341
|
+
else if (spec.provider === 'ollama-cloud') {
|
|
342
|
+
env.OPENAI_API_KEY = cred.value;
|
|
343
|
+
args.push(...codexProviderArgs('ollama_cloud', 'Ollama Cloud', profile.endpoint, 'OPENAI_API_KEY'));
|
|
344
|
+
args.push('-c', 'model_providers.ollama_cloud.stream_idle_timeout_ms=600000', '-c', `model_context_window=${OLLAMA_CONTEXT[model] || 200000}`);
|
|
187
345
|
const localCatalog = path.join(home, '.codex', 'ollama_cloud_model_catalog.json');
|
|
188
|
-
args.push(
|
|
189
|
-
'-c', 'model_provider="ollama_cloud"',
|
|
190
|
-
'-c', 'model_providers.ollama_cloud.name="Ollama Cloud"',
|
|
191
|
-
'-c', 'model_providers.ollama_cloud.base_url="https://ollama.com/v1"',
|
|
192
|
-
'-c', 'model_providers.ollama_cloud.wire_api="responses"',
|
|
193
|
-
'-c', 'model_providers.ollama_cloud.stream_idle_timeout_ms=600000',
|
|
194
|
-
'-c', `model_context_window=${OLLAMA_CONTEXT[model] || 200000}`,
|
|
195
|
-
);
|
|
196
|
-
// Codex carica il catalogo solo all'avvio. Il file e' locale e opzionale:
|
|
197
|
-
// sui device che non lo hanno resta valido il fallback context-window sopra.
|
|
198
346
|
if (fs.existsSync(localCatalog)) args.push('-c', `model_catalog_json="${localCatalog}"`);
|
|
347
|
+
} else if (spec.provider === 'custom') {
|
|
348
|
+
env[spec.envKey] = cred.value;
|
|
349
|
+
args.push(...codexProviderArgs(spec.providerId, spec.displayName, spec.baseUrl, spec.envKey));
|
|
199
350
|
}
|
|
351
|
+
if (model) args.push('-m', model);
|
|
352
|
+
} else if (spec.client === 'pi') {
|
|
353
|
+
if (profile.auth !== 'none' && profile.auth !== 'login' && cred.value) env[cred.envKey] = cred.value;
|
|
354
|
+
if (spec.provider === 'custom') args.push('--extension', writePiProviderExtension(spec, home));
|
|
355
|
+
else if (profile.piExtension) args.push('--extension', writePiProviderExtension({
|
|
356
|
+
providerId: profile.piProvider, displayName: profile.label.replace(/^Pi · /, ''),
|
|
357
|
+
baseUrl: profile.piExtension.baseUrl, apiKey: profile.piExtension.apiKey,
|
|
358
|
+
protocol: profile.protocol, model,
|
|
359
|
+
}, home));
|
|
360
|
+
args.push('--provider', spec.provider === 'custom' ? spec.providerId : profile.piProvider);
|
|
361
|
+
if (model) args.push('--model', model);
|
|
200
362
|
}
|
|
201
|
-
|
|
202
|
-
// evita di digitare nella TUI prima che sia pronta; argv e' diretto, mai shell.
|
|
203
|
-
if (cell && cell.prompt) args.push(cell.prompt);
|
|
363
|
+
if (cell?.prompt) args.push(cell.prompt);
|
|
204
364
|
return { ok: true, info, engine: { ...engine, command: info.binary, args, env, promptMode: 'managed-argv' } };
|
|
205
365
|
}
|
|
206
366
|
|
|
367
|
+
function publicCatalog() {
|
|
368
|
+
return CATALOG.map((p) => ({
|
|
369
|
+
id: p.id, client: p.client, clientLabel: CLIENT_LABELS[p.client], provider: p.provider,
|
|
370
|
+
credentialProfile: p.credentialProfile || '', label: p.label, protocol: p.protocol,
|
|
371
|
+
auth: p.auth, endpoint: p.endpoint || '', model: p.model || '', models: [...(p.models || [])],
|
|
372
|
+
protocols: [...(p.protocols || [p.protocol])], supportsUnsafe: p.client !== 'pi', requiresModel: !!p.requiresModel || !!p.custom,
|
|
373
|
+
rc: !!p.rc, custom: !!p.custom, default: !!p.default,
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
|
|
207
377
|
module.exports = {
|
|
208
|
-
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT,
|
|
209
|
-
discoverOllamaModels,
|
|
210
|
-
|
|
378
|
+
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, CLIENT_LABELS, normalizeManagedSpec,
|
|
379
|
+
defaultDefinitions, describeManaged, discoverOllamaModels, resolveManagedEngine,
|
|
380
|
+
discoverPiModels, parseEnvFile, findBinary, publicCatalog, writePiProviderExtension,
|
|
211
381
|
};
|