set-agent-provider 0.1.0
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/LICENSE +21 -0
- package/README.en.md +164 -0
- package/README.md +157 -0
- package/bin/set-agent-provider.js +7 -0
- package/package.json +30 -0
- package/src/adapters/claude.js +64 -0
- package/src/adapters/codex.js +171 -0
- package/src/adapters/dsh.js +158 -0
- package/src/adapters/index.js +15 -0
- package/src/adapters/opencode.js +78 -0
- package/src/adapters/pi.js +73 -0
- package/src/apply.js +68 -0
- package/src/backup.js +28 -0
- package/src/cli/args.js +43 -0
- package/src/cli/help.js +33 -0
- package/src/cli/prompt.js +69 -0
- package/src/config/baseurl.js +49 -0
- package/src/config/load.js +50 -0
- package/src/config/name.js +18 -0
- package/src/config/resolve.js +58 -0
- package/src/config/schema.js +47 -0
- package/src/discover.js +34 -0
- package/src/domain/resolve.js +99 -0
- package/src/index.js +48 -0
- package/src/models/fetch.js +43 -0
- package/src/models/select.js +120 -0
- package/src/output.js +47 -0
- package/src/status.js +51 -0
- package/src/util/fs.js +40 -0
- package/src/util/http.js +62 -0
- package/src/util/paths.js +18 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { home } = require('../util/paths');
|
|
5
|
+
const { exists, readText, writeText } = require('../util/fs');
|
|
6
|
+
|
|
7
|
+
const ID = 'dsh';
|
|
8
|
+
const NAMESPACE = 'llm-pi-ai';
|
|
9
|
+
|
|
10
|
+
function settingsFile() { return path.join(home(), '.dsh', 'settings.yaml'); }
|
|
11
|
+
function credsFile() { return path.join(home(), '.dsh', '.credentials.yaml'); }
|
|
12
|
+
|
|
13
|
+
function yq(v) {
|
|
14
|
+
return "'" + String(v).replace(/'/g, "''") + "'";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function credentialRef(name) {
|
|
18
|
+
let n = String(name).replace(/[^A-Za-z0-9_]/g, '_').toUpperCase();
|
|
19
|
+
if (!/^[A-Za-z_]/.test(n)) n = 'P_' + n;
|
|
20
|
+
return n + '_API_KEY';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function topKey(line) {
|
|
24
|
+
const m = /^([^\s:#][^:]*):(\s|$)/.exec(line);
|
|
25
|
+
return m ? m[1] : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function upsertNamespace(text, key, innerYaml) {
|
|
29
|
+
const lines = (text || '').split(/\r?\n/);
|
|
30
|
+
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
|
|
31
|
+
|
|
32
|
+
const result = [];
|
|
33
|
+
let done = false;
|
|
34
|
+
let i = 0;
|
|
35
|
+
while (i < lines.length) {
|
|
36
|
+
if (topKey(lines[i]) === key) {
|
|
37
|
+
result.push(key + ':');
|
|
38
|
+
for (const bl of innerYaml.split('\n')) result.push(bl === '' ? '' : ' ' + bl);
|
|
39
|
+
i++;
|
|
40
|
+
while (i < lines.length && topKey(lines[i]) === null) i++;
|
|
41
|
+
done = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
result.push(lines[i]);
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
if (!done) {
|
|
48
|
+
if (result.length) result.push('');
|
|
49
|
+
result.push(key + ':');
|
|
50
|
+
for (const bl of innerYaml.split('\n')) result.push(bl === '' ? '' : ' ' + bl);
|
|
51
|
+
}
|
|
52
|
+
return result.join('\n') + '\n';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildProviderYaml(ctx, ref) {
|
|
56
|
+
const out = [];
|
|
57
|
+
out.push('providers:');
|
|
58
|
+
out.push(' ' + ctx.name + ':');
|
|
59
|
+
out.push(' displayName: ' + yq(ctx.name));
|
|
60
|
+
out.push(' api: ' + yq('openai-completions'));
|
|
61
|
+
out.push(' baseURL: ' + yq(String(ctx.baseUrl).replace(/\/+$/, '')));
|
|
62
|
+
out.push(' apiKeyEnv: ' + yq(ref));
|
|
63
|
+
out.push(' models:');
|
|
64
|
+
for (const m of ctx.models) {
|
|
65
|
+
out.push(' - id: ' + yq(m.id));
|
|
66
|
+
out.push(' name: ' + yq(m.name));
|
|
67
|
+
if (m.limit && m.limit.context != null) out.push(' contextWindow: ' + Number(m.limit.context));
|
|
68
|
+
if (m.limit && m.limit.output != null) out.push(' maxTokens: ' + Number(m.limit.output));
|
|
69
|
+
}
|
|
70
|
+
return out.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildDefaultModelYaml(ctx) {
|
|
74
|
+
return ['provider: ' + yq(ctx.name), 'model: ' + yq(ctx.defaultModel)].join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function unquoteScalar(s) {
|
|
78
|
+
s = String(s).trim();
|
|
79
|
+
if (s.length >= 2 && s[0] === "'" && s[s.length - 1] === "'") return s.slice(1, -1).replace(/''/g, "'");
|
|
80
|
+
if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') return s.slice(1, -1);
|
|
81
|
+
return s;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseCreds(text) {
|
|
85
|
+
const refs = {};
|
|
86
|
+
const records = [];
|
|
87
|
+
let section = null;
|
|
88
|
+
for (const line of (text || '').split(/\r?\n/)) {
|
|
89
|
+
const k = topKey(line);
|
|
90
|
+
if (k) {
|
|
91
|
+
section = k;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (section === 'refs') {
|
|
95
|
+
const m = /^\s+([A-Za-z0-9_]+)\s*:\s*(.+?)\s*$/.exec(line);
|
|
96
|
+
if (m) refs[m[1]] = unquoteScalar(m[2]);
|
|
97
|
+
} else if (section === 'records') {
|
|
98
|
+
records.push(line);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { refs: refs, recordsRaw: records.join('\n').replace(/^\n+|\s+$/g, '') };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function writeCreds(refs, recordsRaw) {
|
|
105
|
+
let out = 'version: 1\nrefs:\n';
|
|
106
|
+
for (const k of Object.keys(refs)) out += ' ' + k + ': ' + yq(refs[k]) + '\n';
|
|
107
|
+
if (recordsRaw) out += 'records:\n' + recordsRaw + '\n';
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function write(ctx) {
|
|
112
|
+
const sf = settingsFile();
|
|
113
|
+
const cf = credsFile();
|
|
114
|
+
const ref = credentialRef(ctx.name);
|
|
115
|
+
|
|
116
|
+
let settings = exists(sf) ? readText(sf) : '';
|
|
117
|
+
settings = upsertNamespace(settings, NAMESPACE, buildProviderYaml(ctx, ref));
|
|
118
|
+
settings = upsertNamespace(settings, 'agent-default-model', buildDefaultModelYaml(ctx));
|
|
119
|
+
let changed = writeText(sf, settings);
|
|
120
|
+
|
|
121
|
+
if (ctx.apiKey) {
|
|
122
|
+
let credsText = exists(cf) ? readText(cf) : '';
|
|
123
|
+
const parsed = parseCreds(credsText);
|
|
124
|
+
parsed.refs[ref] = ctx.apiKey;
|
|
125
|
+
if (writeText(cf, writeCreds(parsed.refs, parsed.recordsRaw))) changed = true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return changed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readStatus() {
|
|
132
|
+
const sf = settingsFile();
|
|
133
|
+
if (!exists(sf)) return null;
|
|
134
|
+
const text = readText(sf);
|
|
135
|
+
const dmMatch = text.match(/agent-default-model:\s*\n\s+provider:\s*'?([^'\n]+)'?\s*\n\s+model:\s*'?([^'\n]+)'?/);
|
|
136
|
+
if (!dmMatch) return null;
|
|
137
|
+
const provider = dmMatch[1].trim();
|
|
138
|
+
const model = dmMatch[2].trim();
|
|
139
|
+
const baseMatch = text.match(new RegExp(provider.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ':\\s*\\n(?:\\s+.*\\n)*?\\s+baseURL:\\s*\'?([^\'\\n]+)'));
|
|
140
|
+
return {
|
|
141
|
+
configured: true,
|
|
142
|
+
name: provider,
|
|
143
|
+
baseUrl: baseMatch ? baseMatch[1].trim() : null,
|
|
144
|
+
defaultModel: model,
|
|
145
|
+
modelCount: 1
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
id: ID,
|
|
151
|
+
name: 'dsh',
|
|
152
|
+
configPaths: ['~/.dsh/settings.yaml', '~/.dsh'],
|
|
153
|
+
supports: { reasoning: false, limit: true },
|
|
154
|
+
writeFiles: function () { return [settingsFile(), credsFile()]; },
|
|
155
|
+
write: write,
|
|
156
|
+
readStatus: readStatus,
|
|
157
|
+
credentialRef: credentialRef
|
|
158
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const opencode = require('./opencode');
|
|
4
|
+
const claude = require('./claude');
|
|
5
|
+
const codex = require('./codex');
|
|
6
|
+
const pi = require('./pi');
|
|
7
|
+
const dsh = require('./dsh');
|
|
8
|
+
|
|
9
|
+
const adapters = [claude, codex, opencode, pi, dsh];
|
|
10
|
+
|
|
11
|
+
function getAdapter(id) {
|
|
12
|
+
return adapters.find(function (a) { return a.id === id; }) || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { adapters, getAdapter };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { home } = require('../util/paths');
|
|
5
|
+
const { exists, readJson, writeJson } = require('../util/fs');
|
|
6
|
+
|
|
7
|
+
const ID = 'opencode';
|
|
8
|
+
|
|
9
|
+
function file() {
|
|
10
|
+
return path.join(home(), '.config', 'opencode', 'opencode.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function write(ctx) {
|
|
14
|
+
const f = file();
|
|
15
|
+
let cfg = {};
|
|
16
|
+
if (exists(f)) {
|
|
17
|
+
try { cfg = readJson(f); } catch (e) { cfg = {}; }
|
|
18
|
+
}
|
|
19
|
+
if (!cfg.provider || typeof cfg.provider !== 'object') cfg.provider = {};
|
|
20
|
+
|
|
21
|
+
const provider = {
|
|
22
|
+
name: ctx.name,
|
|
23
|
+
npm: '@ai-sdk/openai-compatible',
|
|
24
|
+
options: { baseURL: trimTrailing(ctx.baseUrl) }
|
|
25
|
+
};
|
|
26
|
+
if (ctx.apiKey) provider.options.apiKey = ctx.apiKey;
|
|
27
|
+
|
|
28
|
+
if (ctx.models && ctx.models.length) {
|
|
29
|
+
const models = {};
|
|
30
|
+
for (const m of ctx.models) {
|
|
31
|
+
const entry = { name: m.name };
|
|
32
|
+
if (m.reasoning) entry.reasoning = true;
|
|
33
|
+
if (m.limit && (m.limit.context != null || m.limit.output != null)) {
|
|
34
|
+
entry.limit = {};
|
|
35
|
+
if (m.limit.context != null) entry.limit.context = m.limit.context;
|
|
36
|
+
if (m.limit.output != null) entry.limit.output = m.limit.output;
|
|
37
|
+
}
|
|
38
|
+
models[m.id] = entry;
|
|
39
|
+
}
|
|
40
|
+
provider.models = models;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
cfg.provider[ctx.name] = provider;
|
|
44
|
+
if (ctx.defaultModel) cfg.model = ctx.name + '/' + ctx.defaultModel;
|
|
45
|
+
return writeJson(f, cfg);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readStatus() {
|
|
49
|
+
const f = file();
|
|
50
|
+
if (!exists(f)) return null;
|
|
51
|
+
let cfg;
|
|
52
|
+
try { cfg = readJson(f); } catch (e) { return null; }
|
|
53
|
+
if (!cfg.model || typeof cfg.model !== 'string') return null;
|
|
54
|
+
const name = cfg.model.split('/')[0];
|
|
55
|
+
const p = cfg.provider && cfg.provider[name];
|
|
56
|
+
if (!p) return null;
|
|
57
|
+
return {
|
|
58
|
+
configured: true,
|
|
59
|
+
name: name,
|
|
60
|
+
baseUrl: p.options && p.options.baseURL,
|
|
61
|
+
defaultModel: cfg.model.split('/').slice(1).join('/'),
|
|
62
|
+
modelCount: p.models ? Object.keys(p.models).length : 0
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function trimTrailing(s) {
|
|
67
|
+
return String(s).replace(/\/+$/, '');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
id: ID,
|
|
72
|
+
name: 'OpenCode',
|
|
73
|
+
configPaths: ['~/.config/opencode/opencode.json', '~/.config/opencode'],
|
|
74
|
+
supports: { reasoning: true, limit: true },
|
|
75
|
+
writeFiles: function () { return [file()]; },
|
|
76
|
+
write: write,
|
|
77
|
+
readStatus: readStatus
|
|
78
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { home } = require('../util/paths');
|
|
5
|
+
const { exists, readJson, writeJson } = require('../util/fs');
|
|
6
|
+
|
|
7
|
+
const ID = 'pi';
|
|
8
|
+
|
|
9
|
+
function file() {
|
|
10
|
+
return path.join(home(), '.pi', 'agent', 'models.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function trim(s) {
|
|
14
|
+
return String(s).replace(/\/+$/, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function write(ctx) {
|
|
18
|
+
const f = file();
|
|
19
|
+
let cfg = {};
|
|
20
|
+
if (exists(f)) {
|
|
21
|
+
try { cfg = readJson(f); } catch (e) { cfg = {}; }
|
|
22
|
+
}
|
|
23
|
+
if (!cfg.providers || typeof cfg.providers !== 'object') cfg.providers = {};
|
|
24
|
+
|
|
25
|
+
const provider = {
|
|
26
|
+
name: ctx.name,
|
|
27
|
+
baseUrl: trim(ctx.baseUrl),
|
|
28
|
+
api: 'openai-completions'
|
|
29
|
+
};
|
|
30
|
+
if (ctx.apiKey) provider.apiKey = ctx.apiKey;
|
|
31
|
+
|
|
32
|
+
if (ctx.models && ctx.models.length) {
|
|
33
|
+
provider.models = ctx.models.map(function (m) {
|
|
34
|
+
const entry = { id: m.id, name: m.name };
|
|
35
|
+
if (m.reasoning) entry.reasoning = true;
|
|
36
|
+
if (m.limit && m.limit.context != null) entry.contextWindow = m.limit.context;
|
|
37
|
+
if (m.limit && m.limit.output != null) entry.maxTokens = m.limit.output;
|
|
38
|
+
return entry;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
cfg.providers[ctx.name] = provider;
|
|
43
|
+
return writeJson(f, cfg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readStatus() {
|
|
47
|
+
const f = file();
|
|
48
|
+
if (!exists(f)) return null;
|
|
49
|
+
let cfg;
|
|
50
|
+
try { cfg = readJson(f); } catch (e) { return null; }
|
|
51
|
+
const providers = (cfg && cfg.providers) || {};
|
|
52
|
+
const names = Object.keys(providers);
|
|
53
|
+
if (!names.length) return null;
|
|
54
|
+
const name = names[names.length - 1];
|
|
55
|
+
const p = providers[name];
|
|
56
|
+
return {
|
|
57
|
+
configured: true,
|
|
58
|
+
name: name,
|
|
59
|
+
baseUrl: p.baseUrl,
|
|
60
|
+
defaultModel: null,
|
|
61
|
+
modelCount: p.models ? p.models.length : 0
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
id: ID,
|
|
67
|
+
name: 'pi',
|
|
68
|
+
configPaths: ['~/.pi/agent/models.json', '~/.pi'],
|
|
69
|
+
supports: { reasoning: true, limit: true },
|
|
70
|
+
writeFiles: function () { return [file()]; },
|
|
71
|
+
write: write,
|
|
72
|
+
readStatus: readStatus
|
|
73
|
+
};
|
package/src/apply.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveTargets } = require('./discover');
|
|
4
|
+
const { backupFile, localTimestamp } = require('./backup');
|
|
5
|
+
const { setBackupHook } = require('./util/fs');
|
|
6
|
+
|
|
7
|
+
async function applyProvider(cfg, opts) {
|
|
8
|
+
opts = opts || {};
|
|
9
|
+
const resolved = resolveTargets(opts.targetArg);
|
|
10
|
+
const interactive = Boolean(opts.interactive);
|
|
11
|
+
const results = [];
|
|
12
|
+
|
|
13
|
+
for (const adapter of resolved.selected) {
|
|
14
|
+
try {
|
|
15
|
+
let extra = {};
|
|
16
|
+
if (typeof adapter.resolveOptions === 'function') {
|
|
17
|
+
extra = (await adapter.resolveOptions({
|
|
18
|
+
baseUrl: cfg.baseUrl,
|
|
19
|
+
apiKey: cfg.apiKey,
|
|
20
|
+
models: cfg.models,
|
|
21
|
+
defaultModel: opts.defaultModel,
|
|
22
|
+
interactive: interactive
|
|
23
|
+
})) || {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ts = localTimestamp(new Date());
|
|
27
|
+
const backedUp = [];
|
|
28
|
+
setBackupHook(function (file) {
|
|
29
|
+
const dest = backupFile(file, ts);
|
|
30
|
+
if (dest) backedUp.push(dest);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let changed;
|
|
34
|
+
try {
|
|
35
|
+
changed = adapter.write(Object.assign({
|
|
36
|
+
name: cfg.name,
|
|
37
|
+
baseUrl: cfg.baseUrl,
|
|
38
|
+
apiKey: cfg.apiKey,
|
|
39
|
+
models: cfg.models,
|
|
40
|
+
defaultModel: opts.defaultModel
|
|
41
|
+
}, extra));
|
|
42
|
+
} finally {
|
|
43
|
+
setBackupHook(null);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
results.push({
|
|
47
|
+
id: adapter.id,
|
|
48
|
+
name: adapter.name,
|
|
49
|
+
ok: true,
|
|
50
|
+
changed: changed !== false,
|
|
51
|
+
backedUp: backedUp,
|
|
52
|
+
detection: extra.detection,
|
|
53
|
+
wireApi: extra.wireApi
|
|
54
|
+
});
|
|
55
|
+
} catch (e) {
|
|
56
|
+
setBackupHook(null);
|
|
57
|
+
results.push({ id: adapter.id, name: adapter.name, ok: false, error: e.message });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const m of resolved.missing) {
|
|
62
|
+
results.push({ id: m, name: m, ok: false, skipped: true, error: 'Not installed / unknown target' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { results: results, selected: resolved.selected, missing: resolved.missing };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { applyProvider };
|
package/src/backup.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
function localTimestamp(d) {
|
|
6
|
+
const p = function (n) { return String(n).padStart(2, '0'); };
|
|
7
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) +
|
|
8
|
+
'_' + p(d.getHours()) + '-' + p(d.getMinutes()) + '-' + p(d.getSeconds());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function backupFile(file, ts) {
|
|
12
|
+
if (!fs.existsSync(file)) return null;
|
|
13
|
+
const dest = file + '.bak-' + ts;
|
|
14
|
+
fs.copyFileSync(file, dest);
|
|
15
|
+
return dest;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function backupFiles(files) {
|
|
19
|
+
const ts = localTimestamp(new Date());
|
|
20
|
+
const backedUp = [];
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const dest = backupFile(file, ts);
|
|
23
|
+
if (dest) backedUp.push(dest);
|
|
24
|
+
}
|
|
25
|
+
return backedUp;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { backupFiles, backupFile, localTimestamp };
|
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SPEC = {
|
|
4
|
+
'--name': 'name', '--baseUrl': 'baseUrl', '--baseurl': 'baseUrl',
|
|
5
|
+
'--apiKey': 'apiKey', '--apikey': 'apiKey',
|
|
6
|
+
'--models': 'models', '--target': 'target', '--config': 'config',
|
|
7
|
+
'--domain': 'domain', '--status': 'status', '--help': 'help',
|
|
8
|
+
'--version': 'version',
|
|
9
|
+
'-N': 'name', '-U': 'baseUrl', '-K': 'apiKey', '-M': 'models',
|
|
10
|
+
'-T': 'target', '-C': 'config', '-D': 'domain', '-h': 'help', '-v': 'version'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const FLAGS = { status: true, help: true, version: true };
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const tok = argv[i];
|
|
19
|
+
let key = tok;
|
|
20
|
+
let val = null;
|
|
21
|
+
if (tok.indexOf('--') === 0 && tok.indexOf('=') !== -1) {
|
|
22
|
+
const idx = tok.indexOf('=');
|
|
23
|
+
key = tok.slice(0, idx);
|
|
24
|
+
val = tok.slice(idx + 1);
|
|
25
|
+
}
|
|
26
|
+
const field = SPEC[key];
|
|
27
|
+
if (!field) {
|
|
28
|
+
throw new Error('Unknown option: ' + tok);
|
|
29
|
+
}
|
|
30
|
+
if (FLAGS[field]) {
|
|
31
|
+
out[field] = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (val == null) val = argv[++i];
|
|
35
|
+
if (val == null || val === '') {
|
|
36
|
+
throw new Error('Missing value for ' + key);
|
|
37
|
+
}
|
|
38
|
+
out[field] = val;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { parseArgs };
|
package/src/cli/help.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function printHelp() {
|
|
4
|
+
const text = [
|
|
5
|
+
'set-agent-provider — configure a model provider across all installed AI CLIs',
|
|
6
|
+
'',
|
|
7
|
+
'Usage:',
|
|
8
|
+
' set-agent-provider [options]',
|
|
9
|
+
'',
|
|
10
|
+
'Options:',
|
|
11
|
+
' -N, --name <name> Provider name (default: hostname of baseUrl)',
|
|
12
|
+
' -U, --baseUrl <url> Provider base URL (required; prompted if omitted interactively)',
|
|
13
|
+
' -K, --apiKey <key> Provider API key (optional; prompted if omitted, may be blank)',
|
|
14
|
+
' -M, --models <json> Models as strict JSON array',
|
|
15
|
+
' -T, --target <list> Comma-separated targets (claude,codex,opencode,pi,dsh)',
|
|
16
|
+
' -C, --config <source> Config source: file path, URL, or bare domain',
|
|
17
|
+
' -D, --domain <domain> Auto-detect from a bare domain (config, then POST /v1/chat/completions)',
|
|
18
|
+
' --status Show current provider config for each installed CLI',
|
|
19
|
+
' -h, --help Show this help',
|
|
20
|
+
' -v, --version Show version',
|
|
21
|
+
'',
|
|
22
|
+
'Examples:',
|
|
23
|
+
' set-agent-provider -N foo -U https://api.example.com/v1 -K sk-xxx \\',
|
|
24
|
+
' -M \'[{"id":"gpt-5.6-sol","name":"GPT-5.6 SOL","reasoning":true,"limit":{"context":272000,"output":128000}}]\'',
|
|
25
|
+
' set-agent-provider -C ./config.json',
|
|
26
|
+
' set-agent-provider -C https://example.com/set-agent-provider-config.json -T opencode',
|
|
27
|
+
' set-agent-provider -D api.deepseek.com -K sk-xxx',
|
|
28
|
+
''
|
|
29
|
+
].join('\n');
|
|
30
|
+
process.stdout.write(text + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { printHelp: printHelp };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { maskKey } = require('../output');
|
|
5
|
+
|
|
6
|
+
function isInteractive() {
|
|
7
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ask(question, defaultValue) {
|
|
11
|
+
return new Promise(function (resolve) {
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
const q = defaultValue ? question + ' [' + defaultValue + ']: ' : question + ': ';
|
|
14
|
+
rl.question(q, function (ans) {
|
|
15
|
+
rl.close();
|
|
16
|
+
const v = String(ans).trim();
|
|
17
|
+
resolve(v || defaultValue || '');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function askYes(question, def) {
|
|
23
|
+
return new Promise(function (resolve) {
|
|
24
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
25
|
+
rl.question(question + ' [' + (def ? 'Y/n' : 'y/N') + ']: ', function (ans) {
|
|
26
|
+
rl.close();
|
|
27
|
+
const v = String(ans).trim().toLowerCase();
|
|
28
|
+
if (!v) return resolve(Boolean(def));
|
|
29
|
+
resolve(v === 'y' || v === 'yes');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function askBaseUrl() {
|
|
35
|
+
if (!isInteractive()) return null;
|
|
36
|
+
let baseUrl = '';
|
|
37
|
+
while (!baseUrl) {
|
|
38
|
+
baseUrl = await ask('Base URL (e.g. https://api.example.com/v1)');
|
|
39
|
+
if (!baseUrl) process.stdout.write('Base URL is required.\n');
|
|
40
|
+
}
|
|
41
|
+
return baseUrl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function askApiKey(reason) {
|
|
45
|
+
if (!isInteractive()) return undefined;
|
|
46
|
+
const label = reason
|
|
47
|
+
? 'API key (' + reason + ', optional, Enter to skip)'
|
|
48
|
+
: 'API key (optional, Enter to skip)';
|
|
49
|
+
const value = await ask(label);
|
|
50
|
+
return value || undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function confirm(cfg) {
|
|
54
|
+
if (!isInteractive()) return true;
|
|
55
|
+
process.stdout.write('\n');
|
|
56
|
+
process.stdout.write(' Name : ' + cfg.name + '\n');
|
|
57
|
+
process.stdout.write(' BaseUrl: ' + cfg.baseUrl + '\n');
|
|
58
|
+
process.stdout.write(' API Key: ' + (cfg.apiKey ? maskKey(cfg.apiKey) : '(none)') + '\n\n');
|
|
59
|
+
return askYes('Apply this configuration?', true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
isInteractive: isInteractive,
|
|
64
|
+
ask: ask,
|
|
65
|
+
askYes: askYes,
|
|
66
|
+
askBaseUrl: askBaseUrl,
|
|
67
|
+
askApiKey: askApiKey,
|
|
68
|
+
confirm: confirm
|
|
69
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadConfigSource } = require('./load');
|
|
4
|
+
const { resolveDomain } = require('../domain/resolve');
|
|
5
|
+
const { isInteractive, askBaseUrl } = require('../cli/prompt');
|
|
6
|
+
|
|
7
|
+
async function resolveBaseUrl(args) {
|
|
8
|
+
if (args.config && args.domain) {
|
|
9
|
+
throw new Error('Use either -C/--config or -D/--domain, not both.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let config = null;
|
|
13
|
+
let domainBase = null;
|
|
14
|
+
|
|
15
|
+
if (args.config) {
|
|
16
|
+
config = await loadConfigSource(args.config);
|
|
17
|
+
} else if (args.domain) {
|
|
18
|
+
const resolved = await resolveDomain(args.domain, {
|
|
19
|
+
probe: args.baseUrl == null,
|
|
20
|
+
apiKey: args.apiKey
|
|
21
|
+
});
|
|
22
|
+
config = resolved.config;
|
|
23
|
+
domainBase = resolved.baseUrl;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let baseUrl = null;
|
|
27
|
+
let source = null;
|
|
28
|
+
if (args.baseUrl != null) {
|
|
29
|
+
baseUrl = String(args.baseUrl).trim();
|
|
30
|
+
source = 'flag';
|
|
31
|
+
} else if (config && config.baseUrl) {
|
|
32
|
+
baseUrl = String(config.baseUrl).trim();
|
|
33
|
+
source = 'config';
|
|
34
|
+
} else if (domainBase) {
|
|
35
|
+
baseUrl = domainBase;
|
|
36
|
+
source = 'domain';
|
|
37
|
+
} else if (isInteractive()) {
|
|
38
|
+
baseUrl = await askBaseUrl();
|
|
39
|
+
source = 'prompt';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!baseUrl) {
|
|
43
|
+
throw new Error('baseUrl is required. Provide -U/--baseUrl, a config source, or run interactively.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { baseUrl: baseUrl, config: config, source: source };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { resolveBaseUrl: resolveBaseUrl };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { get } = require('../util/http');
|
|
5
|
+
const { normalizeConfig } = require('./schema');
|
|
6
|
+
|
|
7
|
+
const CONFIG_PATH = '/set-agent-provider-config.json';
|
|
8
|
+
|
|
9
|
+
async function loadConfigSource(source) {
|
|
10
|
+
if (!source) return {};
|
|
11
|
+
if (/^https?:\/\//i.test(source)) {
|
|
12
|
+
return normalizeConfig(await fetchConfigUrl(source));
|
|
13
|
+
}
|
|
14
|
+
if (fs.existsSync(source)) {
|
|
15
|
+
return normalizeConfig(parseJson(fs.readFileSync(source, 'utf8'), source));
|
|
16
|
+
}
|
|
17
|
+
const domain = String(source).replace(/\/+$/, '');
|
|
18
|
+
let lastErr;
|
|
19
|
+
for (const scheme of ['https', 'http']) {
|
|
20
|
+
try {
|
|
21
|
+
return normalizeConfig(await fetchConfigUrl(scheme + '://' + domain + CONFIG_PATH));
|
|
22
|
+
} catch (e) {
|
|
23
|
+
lastErr = e;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error('Could not load config from "' + source + '"' + (lastErr ? ' (' + lastErr.message + ')' : ''));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function fetchConfigUrl(url) {
|
|
30
|
+
let res;
|
|
31
|
+
try {
|
|
32
|
+
res = await get(url, { headers: { Accept: 'application/json' } });
|
|
33
|
+
} catch (e) {
|
|
34
|
+
throw new Error('Failed to fetch ' + url + ': ' + e.message);
|
|
35
|
+
}
|
|
36
|
+
if (res.status < 200 || res.status >= 300) {
|
|
37
|
+
throw new Error('Config fetch returned HTTP ' + res.status + ' for ' + url);
|
|
38
|
+
}
|
|
39
|
+
return parseJson(res.body, url);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseJson(text, label) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(text);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
throw new Error('Invalid JSON' + (label ? ' in ' + label : '') + ': ' + e.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { loadConfigSource: loadConfigSource, CONFIG_PATH: CONFIG_PATH };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function hostOf(baseUrl) {
|
|
4
|
+
if (!baseUrl) return null;
|
|
5
|
+
try {
|
|
6
|
+
return new URL(baseUrl).hostname || null;
|
|
7
|
+
} catch (e) {
|
|
8
|
+
return String(baseUrl).replace(/\/+$/, '') || null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveName(args, config, baseUrl) {
|
|
13
|
+
if (args.name != null) return args.name;
|
|
14
|
+
if (config && config.name) return config.name;
|
|
15
|
+
return hostOf(baseUrl) || 'local';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { resolveName: resolveName, hostOf: hostOf };
|