@resolflow/cli 0.0.0-stage → 0.1.0-beta.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.md +201 -2
- package/dist/adapters/antigravity.js +29 -0
- package/dist/adapters/claude-code.js +302 -0
- package/dist/adapters/codex.js +247 -0
- package/dist/adapters/coding-agent.js +278 -0
- package/dist/adapters/openai-compatible.js +60 -0
- package/dist/adapters/types.js +9 -0
- package/dist/agents/auth.js +109 -0
- package/dist/agents/daily-budget.js +170 -0
- package/dist/agents/errors.js +60 -0
- package/dist/agents/run-process.js +183 -0
- package/dist/agents/tools.js +175 -0
- package/dist/api.js +59 -0
- package/dist/commands.js +511 -0
- package/dist/config.js +104 -0
- package/dist/daemon.js +317 -0
- package/dist/index.js +88 -0
- package/dist/local-model.js +147 -0
- package/dist/local-ui.js +120 -0
- package/dist/log.js +35 -0
- package/dist/protocol.js +23 -0
- package/dist/run-log.js +78 -0
- package/dist/runtime.js +38 -0
- package/dist/secrets.js +207 -0
- package/dist/update-check.js +180 -0
- package/dist/urls.js +69 -0
- package/package.json +62 -4
package/dist/commands.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RefusedError = exports.CODEX_NOT_COUNTED = exports.platformLabel = exports.CLI_VERSION = void 0;
|
|
37
|
+
exports.makeCtx = makeCtx;
|
|
38
|
+
exports.cmdPair = cmdPair;
|
|
39
|
+
exports.describeStore = describeStore;
|
|
40
|
+
exports.promptSecret = promptSecret;
|
|
41
|
+
exports.buildAdapters = buildAdapters;
|
|
42
|
+
exports.dailyBudgetFor = dailyBudgetFor;
|
|
43
|
+
exports.describeAuth = describeAuth;
|
|
44
|
+
exports.cmdStart = cmdStart;
|
|
45
|
+
exports.cmdStatus = cmdStatus;
|
|
46
|
+
exports.cmdModels = cmdModels;
|
|
47
|
+
exports.cmdUnpair = cmdUnpair;
|
|
48
|
+
exports.cmdPause = cmdPause;
|
|
49
|
+
exports.cmdAgents = cmdAgents;
|
|
50
|
+
exports.cmdAgentAuth = cmdAgentAuth;
|
|
51
|
+
exports.cmdAgentKey = cmdAgentKey;
|
|
52
|
+
exports.cmdConfig = cmdConfig;
|
|
53
|
+
const os = __importStar(require("os"));
|
|
54
|
+
const readline = __importStar(require("readline"));
|
|
55
|
+
const api_1 = require("./api");
|
|
56
|
+
const config_1 = require("./config");
|
|
57
|
+
const daemon_1 = require("./daemon");
|
|
58
|
+
const local_model_1 = require("./local-model");
|
|
59
|
+
const local_ui_1 = require("./local-ui");
|
|
60
|
+
const log_1 = require("./log");
|
|
61
|
+
const run_log_1 = require("./run-log");
|
|
62
|
+
const secrets_1 = require("./secrets");
|
|
63
|
+
const tools_1 = require("./agents/tools");
|
|
64
|
+
const auth_1 = require("./agents/auth");
|
|
65
|
+
const coding_agent_1 = require("./adapters/coding-agent");
|
|
66
|
+
const daily_budget_1 = require("./agents/daily-budget");
|
|
67
|
+
const claude_code_1 = require("./adapters/claude-code");
|
|
68
|
+
const codex_1 = require("./adapters/codex");
|
|
69
|
+
const openai_compatible_1 = require("./adapters/openai-compatible");
|
|
70
|
+
const urls_1 = require("./urls");
|
|
71
|
+
const runtime_1 = require("./runtime");
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
73
|
+
exports.CLI_VERSION = require('../package.json').version;
|
|
74
|
+
function makeCtx(opts = {}) {
|
|
75
|
+
const config = new config_1.ConfigStore();
|
|
76
|
+
const { store, warning } = (0, secrets_1.chooseSecretStore)(config.dir);
|
|
77
|
+
return { config, secrets: store, secretWarning: warning, logger: (0, log_1.createLogger)({ verbose: opts.verbose }), out: (l) => process.stdout.write(l + '\n') };
|
|
78
|
+
}
|
|
79
|
+
const platformLabel = () => `${process.platform}-${process.arch}`;
|
|
80
|
+
exports.platformLabel = platformLabel;
|
|
81
|
+
async function prompt(question) {
|
|
82
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
83
|
+
try {
|
|
84
|
+
return await new Promise((resolve) => rl.question(question, (a) => resolve(a.trim())));
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
rl.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** `resolflow pair <code>` / `resolflow login [--pair <code>]` */
|
|
91
|
+
async function cmdPair(ctx, code, opts) {
|
|
92
|
+
const cfg = ctx.config.load();
|
|
93
|
+
const apiUrl = (0, urls_1.normaliseApiUrl)(opts.api ?? cfg.apiUrl);
|
|
94
|
+
const theCode = code || (await prompt('Pairing code (from Resolflow › Agents › Devices): '));
|
|
95
|
+
if (!theCode)
|
|
96
|
+
throw new Error('A pairing code is required');
|
|
97
|
+
const name = (opts.name ?? os.hostname()).slice(0, 60);
|
|
98
|
+
const res = await (0, api_1.pairDevice)(apiUrl, theCode, { name, platform: (0, exports.platformLabel)(), cliVersion: exports.CLI_VERSION });
|
|
99
|
+
// Replace an earlier pairing (its token is dropped locally; revoke it in the web app if it still shows)
|
|
100
|
+
await ctx.secrets.set(res.token);
|
|
101
|
+
if (ctx.secretWarning)
|
|
102
|
+
ctx.logger.warn(ctx.secretWarning);
|
|
103
|
+
ctx.config.save({
|
|
104
|
+
...cfg,
|
|
105
|
+
apiUrl,
|
|
106
|
+
device: { id: res.deviceId, name: res.name, workspaceId: res.workspaceId, workspaceName: res.workspaceName, pairedAt: new Date().toISOString() },
|
|
107
|
+
});
|
|
108
|
+
ctx.out(`Paired "${res.name}" with workspace ${res.workspaceName ?? res.workspaceId}.`);
|
|
109
|
+
ctx.out(`Token stored in: ${describeStore(ctx.secrets)}. Next: ${(0, runtime_1.cliCommand)('start')}`);
|
|
110
|
+
}
|
|
111
|
+
function describeStore(s) {
|
|
112
|
+
return { keytar: 'OS keychain (keytar)', 'macos-keychain': 'macOS keychain', libsecret: 'Secret Service (libsecret)', file: 'a 0600 file in the config directory' }[s.kind];
|
|
113
|
+
}
|
|
114
|
+
/** Read a secret without echoing it: hidden typing on a terminal, else the whole of stdin (piped). */
|
|
115
|
+
async function promptSecret(question, input = process.stdin) {
|
|
116
|
+
if (!input.isTTY) {
|
|
117
|
+
let data = '';
|
|
118
|
+
for await (const chunk of input)
|
|
119
|
+
data += chunk;
|
|
120
|
+
return data.trim();
|
|
121
|
+
}
|
|
122
|
+
const rl = readline.createInterface({ input, output: process.stderr, terminal: true });
|
|
123
|
+
const anyRl = rl;
|
|
124
|
+
let asked = false;
|
|
125
|
+
anyRl._writeToOutput = (s) => {
|
|
126
|
+
if (!asked) {
|
|
127
|
+
anyRl.output.write(s); // the question itself
|
|
128
|
+
asked = true;
|
|
129
|
+
}
|
|
130
|
+
else if (s.includes('\n') || s.includes('\r'))
|
|
131
|
+
anyRl.output.write('\n');
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
return await new Promise((resolve) => rl.question(question, (a) => resolve(a.trim())));
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
rl.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The adapters `resolflow start` offers: the local model server, plus every coding agent that is ready here — with
|
|
142
|
+
* its own API key, or (RF-90e75d6d) the person's CLI login when they chose it here. A login-mode tool is reported as
|
|
143
|
+
* such and gets no job while the administrator's switch (`policy`) is off: the server refuses it, and so does the
|
|
144
|
+
* adapter.
|
|
145
|
+
*/
|
|
146
|
+
async function buildAdapters(ctx, cfg, endpoint, env = process.env, policy) {
|
|
147
|
+
const adapters = [new openai_compatible_1.OpenAiCompatibleAdapter(endpoint)];
|
|
148
|
+
const dailyBudget = dailyBudgetFor(ctx);
|
|
149
|
+
for (const t of await (0, tools_1.detectTools)(ctx.config.dir, env, cfg)) {
|
|
150
|
+
if (!t.ready)
|
|
151
|
+
continue;
|
|
152
|
+
const opts = { configDir: ctx.config.dir, env, limits: cfg.agentLimits, authMode: t.auth, policy, dailyBudget, warn: (m) => ctx.logger.warn(m) };
|
|
153
|
+
if (t.def.id === 'claude-code')
|
|
154
|
+
adapters.push(new claude_code_1.ClaudeCodeAdapter(opts));
|
|
155
|
+
else if (t.def.id === 'codex')
|
|
156
|
+
adapters.push(new codex_1.CodexAdapter(opts));
|
|
157
|
+
}
|
|
158
|
+
return adapters;
|
|
159
|
+
}
|
|
160
|
+
/** RF-27cf99e3: the daily coding agent budget; its ceiling is read from config.json for every job (no restart). */
|
|
161
|
+
function dailyBudgetFor(ctx, now) {
|
|
162
|
+
return new daily_budget_1.DailyBudget(ctx.config.dir, { limit: () => (0, daily_budget_1.dailyLimitOf)(ctx.config.load().agentDailyBudgetUsd), now, warn: (m) => ctx.logger.warn(m) });
|
|
163
|
+
}
|
|
164
|
+
exports.CODEX_NOT_COUNTED = 'Claude Code only — Codex reports no cost and is not counted';
|
|
165
|
+
/** `api key` / `CLI login` / `CLI login — not available (disabled by the administrator)` (resolflow agents). */
|
|
166
|
+
function describeAuth(t, policy) {
|
|
167
|
+
if (t.auth !== 'login' || !(0, auth_1.isLoginTool)(t.def.id))
|
|
168
|
+
return 'api key';
|
|
169
|
+
if (policy === 'unknown')
|
|
170
|
+
return 'CLI login (could not ask Resolflow whether the administrator allows it)';
|
|
171
|
+
return policy?.[t.def.id] ? 'CLI login' : 'CLI login — not available (disabled by the administrator)';
|
|
172
|
+
}
|
|
173
|
+
/** The administrator's CLI login switch, asked with this device's token: a policy, or why it could not be asked. */
|
|
174
|
+
async function askPolicy(ctx, cfg) {
|
|
175
|
+
const token = await ctx.secrets.get().catch(() => null);
|
|
176
|
+
if (!cfg.device || !token)
|
|
177
|
+
return { unreachable: 'this machine is not paired' };
|
|
178
|
+
try {
|
|
179
|
+
const me = await (0, api_1.describeDevice)(cfg.apiUrl, token);
|
|
180
|
+
return { policy: (0, auth_1.readPolicy)(me.policy) };
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const e = err;
|
|
184
|
+
return { unreachable: e.status === 401 ? 'this device is revoked or unpaired' : `Resolflow is not reachable (${e.message})` };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function requireToken(ctx, cfg) {
|
|
188
|
+
const token = await ctx.secrets.get();
|
|
189
|
+
if (!cfg.device || !token)
|
|
190
|
+
throw new Error('This machine is not paired. Run: resolflow pair <code>');
|
|
191
|
+
return token;
|
|
192
|
+
}
|
|
193
|
+
/** `resolflow start` — runs until Ctrl+C. Returns a stop function (tests). */
|
|
194
|
+
async function cmdStart(ctx, opts) {
|
|
195
|
+
let cfg = ctx.config.load();
|
|
196
|
+
const token = await requireToken(ctx, cfg);
|
|
197
|
+
const endpoint = (0, urls_1.normaliseLocalEndpoint)(opts.endpoint ?? cfg.endpoint);
|
|
198
|
+
const runLog = new run_log_1.RunLog(ctx.config.dir);
|
|
199
|
+
if (ctx.secretWarning)
|
|
200
|
+
ctx.logger.warn(ctx.secretWarning);
|
|
201
|
+
let resolveFatal = () => undefined;
|
|
202
|
+
const fatal = new Promise((r) => (resolveFatal = r));
|
|
203
|
+
// RF-90e75d6d: none until the server says (login mode off until then)
|
|
204
|
+
const policy = { current: null };
|
|
205
|
+
const adapters = await buildAdapters(ctx, cfg, endpoint, process.env, policy);
|
|
206
|
+
const loginAdapters = adapters.filter((a) => a.authMode === 'login');
|
|
207
|
+
const daemon = new daemon_1.Daemon({
|
|
208
|
+
apiUrl: (0, urls_1.normaliseApiUrl)(cfg.apiUrl),
|
|
209
|
+
token,
|
|
210
|
+
endpoint,
|
|
211
|
+
maxConcurrency: opts.concurrency ? Number(opts.concurrency) : cfg.maxConcurrency,
|
|
212
|
+
allowBackground: opts.allowBackground ?? cfg.allowBackground,
|
|
213
|
+
paused: cfg.paused,
|
|
214
|
+
cliVersion: exports.CLI_VERSION,
|
|
215
|
+
platform: (0, exports.platformLabel)(),
|
|
216
|
+
logger: ctx.logger,
|
|
217
|
+
runLog,
|
|
218
|
+
adapters,
|
|
219
|
+
policy,
|
|
220
|
+
// RF-90e75d6d: a tool on the person's CLI login is not offered while the administrator has it off
|
|
221
|
+
onPolicy: (p) => {
|
|
222
|
+
for (const a of loginAdapters) {
|
|
223
|
+
if (!(0, auth_1.isLoginTool)(a.id))
|
|
224
|
+
continue;
|
|
225
|
+
if (p[a.id])
|
|
226
|
+
ctx.logger.info(`${tools_1.AGENT_TOOLS[a.id].label}: CLI login allowed by the Resolflow administrator.`);
|
|
227
|
+
else
|
|
228
|
+
ctx.logger.warn(`${(0, auth_1.notAvailableMessage)(tools_1.AGENT_TOOLS[a.id], a.id)} Resolflow will not send it jobs.`);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
onFatal: (m) => resolveFatal(m),
|
|
232
|
+
});
|
|
233
|
+
ctx.logger.info(`Serving local models from ${endpoint} (concurrency ${daemon.status().maxConcurrency})`);
|
|
234
|
+
const agents = adapters.filter((a) => a.id !== 'openai-compatible');
|
|
235
|
+
if (agents.length) {
|
|
236
|
+
ctx.logger.info(`Coding agents offered (Mode A, tools off): ${agents.map((a) => `${a.id} (${a.authMode === 'login' ? 'CLI login' : 'API key'})`).join(', ')}`);
|
|
237
|
+
}
|
|
238
|
+
if (loginAdapters.length)
|
|
239
|
+
ctx.logger.info('CLI login jobs use your own plan and its limits; background runs never use them.');
|
|
240
|
+
daemon.start();
|
|
241
|
+
// `resolflow pause|resume|config` edit the config file: pick changes up within 2 s (no local listener needed)
|
|
242
|
+
let mtime = ctx.config.mtimeMs();
|
|
243
|
+
const watcher = setInterval(() => {
|
|
244
|
+
const m = ctx.config.mtimeMs();
|
|
245
|
+
if (m === mtime)
|
|
246
|
+
return;
|
|
247
|
+
mtime = m;
|
|
248
|
+
cfg = ctx.config.load();
|
|
249
|
+
const before = daemon.status();
|
|
250
|
+
daemon.applySettings({ paused: cfg.paused, maxConcurrency: cfg.maxConcurrency, allowBackground: cfg.allowBackground });
|
|
251
|
+
if (before.paused !== cfg.paused)
|
|
252
|
+
ctx.logger.info(cfg.paused ? 'Paused: not serving requests' : 'Resumed serving');
|
|
253
|
+
}, 2_000);
|
|
254
|
+
watcher.unref?.();
|
|
255
|
+
let ui = null;
|
|
256
|
+
if (opts.ui) {
|
|
257
|
+
ui = await (0, local_ui_1.startLocalUi)({
|
|
258
|
+
status: () => ({ ...daemon.status(), endpoint, apiUrl: cfg.apiUrl, device: cfg.device ?? null, recentRuns: runLog.recent(10) }),
|
|
259
|
+
setPaused: (paused) => {
|
|
260
|
+
ctx.config.update({ paused });
|
|
261
|
+
daemon.applySettings({ paused });
|
|
262
|
+
},
|
|
263
|
+
}, { port: opts.uiPort ? Number(opts.uiPort) : undefined });
|
|
264
|
+
// Printed once, to the terminal only (the token is in the fragment, never sent to a server)
|
|
265
|
+
ctx.out(`Local status page (this session only): ${ui.url}`);
|
|
266
|
+
}
|
|
267
|
+
const stop = async () => {
|
|
268
|
+
clearInterval(watcher);
|
|
269
|
+
daemon.stop();
|
|
270
|
+
await ui?.close();
|
|
271
|
+
};
|
|
272
|
+
void fatal.then(async (msg) => {
|
|
273
|
+
await stop();
|
|
274
|
+
process.exitCode = 2;
|
|
275
|
+
ctx.out(`Stopped: ${msg}. If this device was revoked or unpaired, pair it again: resolflow pair <code>`);
|
|
276
|
+
});
|
|
277
|
+
return stop;
|
|
278
|
+
}
|
|
279
|
+
/** `resolflow status` */
|
|
280
|
+
async function cmdStatus(ctx) {
|
|
281
|
+
const cfg = ctx.config.load();
|
|
282
|
+
ctx.out(`API: ${cfg.apiUrl}`);
|
|
283
|
+
ctx.out(`Local endpoint: ${cfg.endpoint}`);
|
|
284
|
+
ctx.out(`Concurrency: ${cfg.maxConcurrency} Background runs: ${cfg.allowBackground ? 'allowed' : 'not allowed'} ${cfg.paused ? 'PAUSED' : 'serving'}`);
|
|
285
|
+
ctx.out(`Secrets: ${describeStore(ctx.secrets)}`);
|
|
286
|
+
const token = await ctx.secrets.get();
|
|
287
|
+
if (!cfg.device || !token) {
|
|
288
|
+
ctx.out('Device: not paired (resolflow pair <code>)');
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
try {
|
|
292
|
+
const me = await (0, api_1.describeDevice)(cfg.apiUrl, token);
|
|
293
|
+
ctx.out(`Device: ${me.name ?? cfg.device.name} (${me.deviceId}) in ${me.workspaceName ?? me.workspaceId} — ${me.online ? 'online' : `offline (run ${(0, runtime_1.cliCommand)('start')})`}`);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
const e = err;
|
|
297
|
+
ctx.out(`Device: ${cfg.device.name} (${cfg.device.id}) — ${e.status === 401 ? 'REVOKED or unpaired on the server' : `server not reachable (${e.message})`}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const models = await (0, local_model_1.listModels)(cfg.endpoint);
|
|
302
|
+
ctx.out(`Local models: ${models.length ? models.join(', ') : '(none)'}`);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
ctx.out(`Local models: unavailable (${err.message})`);
|
|
306
|
+
}
|
|
307
|
+
ctx.out(`${dailyBudgetFor(ctx).describe()} (${exports.CODEX_NOT_COUNTED}; resolflow config agent-daily-budget <USD>, 0 = no limit)`);
|
|
308
|
+
const runs = new run_log_1.RunLog(ctx.config.dir).recent(5);
|
|
309
|
+
if (runs.length) {
|
|
310
|
+
ctx.out('Recent runs:');
|
|
311
|
+
for (const r of runs)
|
|
312
|
+
ctx.out(` ${r.at} ${r.model} ${r.status}${r.errorKind ? ` (${r.errorKind})` : ''} ${(r.ms / 1000).toFixed(1)}s`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** `resolflow models` */
|
|
316
|
+
async function cmdModels(ctx, opts) {
|
|
317
|
+
const cfg = ctx.config.load();
|
|
318
|
+
const models = await (0, local_model_1.listModels)((0, urls_1.normaliseLocalEndpoint)(opts.endpoint ?? cfg.endpoint));
|
|
319
|
+
if (!models.length)
|
|
320
|
+
ctx.out('No models on the local server. Example: ollama pull qwen3:4b');
|
|
321
|
+
for (const m of models)
|
|
322
|
+
ctx.out(m);
|
|
323
|
+
}
|
|
324
|
+
/** `resolflow unpair` — revoke on the server (best effort) and forget locally. */
|
|
325
|
+
async function cmdUnpair(ctx) {
|
|
326
|
+
const cfg = ctx.config.load();
|
|
327
|
+
const token = await ctx.secrets.get();
|
|
328
|
+
if (token) {
|
|
329
|
+
try {
|
|
330
|
+
await (0, api_1.unpairDevice)(cfg.apiUrl, token);
|
|
331
|
+
ctx.out('Device revoked on Resolflow.');
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
ctx.out(`Could not revoke on the server (${err.message}); revoke it in Agents › Devices.`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
await ctx.secrets.delete();
|
|
338
|
+
const { device: _gone, ...rest } = cfg;
|
|
339
|
+
ctx.config.save({ ...rest });
|
|
340
|
+
ctx.out('Pairing removed from this machine.');
|
|
341
|
+
}
|
|
342
|
+
/** `resolflow pause` / `resolflow resume` — the kill switch; a running daemon applies it within 2 s. */
|
|
343
|
+
function cmdPause(ctx, paused) {
|
|
344
|
+
ctx.config.update({ paused });
|
|
345
|
+
ctx.out(paused ? 'Paused: this device will not serve requests (resolflow resume to undo).' : 'Resumed.');
|
|
346
|
+
}
|
|
347
|
+
/** `resolflow agents` — local coding agents: installed, version, API key configured (never the key), auth, offered. */
|
|
348
|
+
async function cmdAgents(ctx, env = process.env) {
|
|
349
|
+
const cfg = ctx.config.load();
|
|
350
|
+
const limits = { ...coding_agent_1.DEFAULT_AGENT_LIMITS, ...(cfg.agentLimits ?? {}) };
|
|
351
|
+
const tools = await (0, tools_1.detectTools)(ctx.config.dir, env, cfg);
|
|
352
|
+
// The administrator's switch is asked only when a tool is on a CLI login
|
|
353
|
+
const asked = tools.some((t) => t.auth === 'login') ? await askPolicy(ctx, cfg) : null;
|
|
354
|
+
const policy = asked ? ('policy' in asked ? asked.policy : 'unknown') : null;
|
|
355
|
+
for (const t of tools) {
|
|
356
|
+
const loginOff = t.auth === 'login' && policy !== 'unknown' && !(policy && (0, auth_1.isLoginTool)(t.def.id) && policy[t.def.id]);
|
|
357
|
+
const state = !t.def.supported
|
|
358
|
+
? t.def.note ?? 'not supported yet'
|
|
359
|
+
: t.needsOptIn
|
|
360
|
+
? `not offered — ${t.def.optInNote ?? `set ${t.def.optInEnv}=1 to offer it`}`
|
|
361
|
+
: t.ready && loginOff
|
|
362
|
+
? 'not available: CLI login is disabled by the administrator (Resolflow sends it no jobs)'
|
|
363
|
+
: t.ready
|
|
364
|
+
? 'ready (offered to Resolflow by resolflow start)'
|
|
365
|
+
: !t.bin
|
|
366
|
+
? 'not installed'
|
|
367
|
+
: 'needs an API key';
|
|
368
|
+
ctx.out(`${t.def.label} [${t.def.id}]`);
|
|
369
|
+
ctx.out(` installed: ${t.bin ? `${t.bin}${t.version ? ` (${t.version})` : ''}` : 'no'}`);
|
|
370
|
+
if (t.def.supported)
|
|
371
|
+
ctx.out(` Auth: ${describeAuth(t, policy)}`);
|
|
372
|
+
ctx.out(` API key: ${(0, tools_1.describeKeySource)(t.key)}${t.key.source === 'none' && t.def.supported && t.auth !== 'login' ? ` — resolflow config agent-key ${t.def.id} (or set ${t.def.keyEnv.join(' / ')})` : ''}`);
|
|
373
|
+
ctx.out(` status: ${state}`);
|
|
374
|
+
}
|
|
375
|
+
ctx.out(`Job caps on this device: ${limits.maxTurns} turns, ${limits.maxBudgetUsd} USD, ${Math.round(limits.timeoutMs / 60_000)} min (resolflow config agent-turns|agent-budget|agent-timeout)`);
|
|
376
|
+
ctx.out(`${dailyBudgetFor(ctx).describe()} (${exports.CODEX_NOT_COUNTED}; resolflow config agent-daily-budget <USD>, 0 = no limit)`);
|
|
377
|
+
ctx.out('API keys and CLI logins stay on this machine; Resolflow never receives them. A CLI login is used only if you chose it (resolflow config agent-auth).');
|
|
378
|
+
}
|
|
379
|
+
/** Exit code 2 (refused, nothing saved): `resolflow config agent-auth … login` while the administrator has it off. */
|
|
380
|
+
class RefusedError extends Error {
|
|
381
|
+
exitCode = 2;
|
|
382
|
+
}
|
|
383
|
+
exports.RefusedError = RefusedError;
|
|
384
|
+
/**
|
|
385
|
+
* `resolflow config agent-auth <claude|codex> <api_key|login>` (RF-90e75d6d) — how a coding agent authenticates on
|
|
386
|
+
* THIS machine. login: asks Resolflow whether the administrator allows it (refused and not saved when off; saved with
|
|
387
|
+
* a warning only when Resolflow cannot be asked), shows the terms summary and needs a typed `yes`.
|
|
388
|
+
*/
|
|
389
|
+
async function cmdAgentAuth(ctx, tool, mode, ask = (q) => prompt(q)) {
|
|
390
|
+
const raw = (tool ?? '').trim().toLowerCase();
|
|
391
|
+
if (raw === 'gemini' || raw === 'antigravity' || raw === 'agy')
|
|
392
|
+
throw new RefusedError(auth_1.GEMINI_LOGIN_MESSAGE);
|
|
393
|
+
const id = (0, tools_1.toolIdOf)(raw);
|
|
394
|
+
if (!id || !(0, auth_1.isLoginTool)(id) || (mode !== 'api_key' && mode !== 'login'))
|
|
395
|
+
throw new Error('Usage: resolflow config agent-auth <claude|codex> <api_key|login>');
|
|
396
|
+
const def = tools_1.AGENT_TOOLS[id];
|
|
397
|
+
const cfg = ctx.config.load();
|
|
398
|
+
if (mode === 'api_key') {
|
|
399
|
+
ctx.config.update({ agentAuth: { ...(cfg.agentAuth ?? {}), [id]: 'api_key' } });
|
|
400
|
+
ctx.out(`${def.label} uses an API key on this machine (resolflow config agent-key ${auth_1.TOOL_ARG[id]}). Restart ${(0, runtime_1.cliCommand)('start')} to apply.`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const asked = await askPolicy(ctx, cfg);
|
|
404
|
+
if ('policy' in asked && !asked.policy[id])
|
|
405
|
+
throw new RefusedError((0, auth_1.notAvailableMessage)(def, id));
|
|
406
|
+
for (const line of (0, auth_1.loginTermsText)(def, id))
|
|
407
|
+
ctx.out(line);
|
|
408
|
+
const answer = (await ask('Type yes to use your CLI login for this tool: ')).trim().toLowerCase();
|
|
409
|
+
if (answer !== 'yes') {
|
|
410
|
+
ctx.out('Not changed.');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const now = new Date().toISOString();
|
|
414
|
+
ctx.config.update({
|
|
415
|
+
agentAuth: { ...(cfg.agentAuth ?? {}), [id]: 'login' },
|
|
416
|
+
agentAuthTerms: { ...(cfg.agentAuthTerms ?? {}), [id]: { version: auth_1.LOGIN_TERMS_VERSION, acceptedAt: now } },
|
|
417
|
+
});
|
|
418
|
+
ctx.out(`${def.label} will use your CLI login on this machine. Restart ${(0, runtime_1.cliCommand)('start')} to apply.`);
|
|
419
|
+
if ('unreachable' in asked)
|
|
420
|
+
ctx.logger.warn(`Could not ask Resolflow (${asked.unreachable}): the CLI login will not be used until the administrator enables it.`);
|
|
421
|
+
if (id === 'codex' && process.env[tools_1.CODEX_OPT_IN_ENV] !== '1')
|
|
422
|
+
ctx.logger.warn(`Codex is offered only with ${tools_1.CODEX_OPT_IN_ENV}=1 set for resolflow start.`);
|
|
423
|
+
}
|
|
424
|
+
/** `resolflow config agent-key <tool> [--delete]` — store a coding agent's API key in the OS keychain (prompted). */
|
|
425
|
+
async function cmdAgentKey(ctx, tool, opts, readSecret = (q) => promptSecret(q)) {
|
|
426
|
+
const id = (0, tools_1.toolIdOf)(tool ?? '');
|
|
427
|
+
if (!id)
|
|
428
|
+
throw new Error('Usage: resolflow config agent-key <claude|codex> [--delete]');
|
|
429
|
+
const def = tools_1.AGENT_TOOLS[id];
|
|
430
|
+
if (!def.supported)
|
|
431
|
+
throw new Error(`${def.label} is ${def.note ?? 'not supported yet'}`);
|
|
432
|
+
const { store, warning } = (0, secrets_1.chooseSecretStore)(ctx.config.dir, process.env, (0, secrets_1.agentKeyEntry)(ctx.config.dir, id));
|
|
433
|
+
if (opts.delete) {
|
|
434
|
+
await store.delete();
|
|
435
|
+
ctx.out(`${def.label} API key removed from ${describeStore(store)}.`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const key = await readSecret(`${def.label} API key (input hidden): `);
|
|
439
|
+
const problem = (0, tools_1.validateApiKeyShape)(key);
|
|
440
|
+
if (problem)
|
|
441
|
+
throw new Error(problem);
|
|
442
|
+
await store.set(key.trim());
|
|
443
|
+
if (warning)
|
|
444
|
+
ctx.logger.warn(warning);
|
|
445
|
+
ctx.out(`${def.label} API key stored in ${describeStore(store)}. It is used only on this machine. Restart ${(0, runtime_1.cliCommand)('start')} to offer it.`);
|
|
446
|
+
}
|
|
447
|
+
/** `resolflow config [key] [value]` — endpoint, concurrency, background, api, agent caps. */
|
|
448
|
+
function cmdConfig(ctx, key, value) {
|
|
449
|
+
const cfg = ctx.config.load();
|
|
450
|
+
if (!key) {
|
|
451
|
+
const { device, ...shown } = cfg;
|
|
452
|
+
ctx.out(JSON.stringify({ ...shown, device: device ? { id: device.id, name: device.name, workspace: device.workspaceName ?? device.workspaceId } : null }, null, 2));
|
|
453
|
+
ctx.out(`(config file: ${ctx.config.file})`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (value === undefined)
|
|
457
|
+
throw new Error('Usage: resolflow config <endpoint|concurrency|background|api|agent-turns|agent-budget|agent-timeout|agent-daily-budget> <value>');
|
|
458
|
+
switch (key) {
|
|
459
|
+
case 'endpoint':
|
|
460
|
+
ctx.config.update({ endpoint: (0, urls_1.normaliseLocalEndpoint)(value) });
|
|
461
|
+
break;
|
|
462
|
+
case 'concurrency': {
|
|
463
|
+
const n = Number(value);
|
|
464
|
+
if (!Number.isInteger(n) || n < 1 || n > 4)
|
|
465
|
+
throw new Error('concurrency must be 1-4');
|
|
466
|
+
ctx.config.update({ maxConcurrency: n });
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
case 'background':
|
|
470
|
+
if (!['on', 'off'].includes(value))
|
|
471
|
+
throw new Error('background must be on or off');
|
|
472
|
+
ctx.config.update({ allowBackground: value === 'on' });
|
|
473
|
+
break;
|
|
474
|
+
case 'api':
|
|
475
|
+
if (cfg.device)
|
|
476
|
+
throw new Error('Unpair first: the pairing belongs to the current API');
|
|
477
|
+
ctx.config.update({ apiUrl: (0, urls_1.normaliseApiUrl)(value) });
|
|
478
|
+
break;
|
|
479
|
+
case 'agent-turns': {
|
|
480
|
+
const n = Number(value);
|
|
481
|
+
if (!Number.isInteger(n) || n < 1 || n > 20)
|
|
482
|
+
throw new Error('agent-turns must be 1-20');
|
|
483
|
+
ctx.config.update({ agentLimits: { ...(cfg.agentLimits ?? {}), maxTurns: n } });
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
case 'agent-budget': {
|
|
487
|
+
const n = Number(value);
|
|
488
|
+
if (!Number.isFinite(n) || n < 0.01 || n > 50)
|
|
489
|
+
throw new Error('agent-budget must be 0.01-50 (USD per job)');
|
|
490
|
+
ctx.config.update({ agentLimits: { ...(cfg.agentLimits ?? {}), maxBudgetUsd: n } });
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
case 'agent-timeout': {
|
|
494
|
+
const n = Number(value);
|
|
495
|
+
if (!Number.isFinite(n) || n < 1 || n > 30)
|
|
496
|
+
throw new Error('agent-timeout must be 1-30 (minutes per job)');
|
|
497
|
+
ctx.config.update({ agentLimits: { ...(cfg.agentLimits ?? {}), timeoutMs: Math.round(n * 60_000) } });
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case 'agent-daily-budget': {
|
|
501
|
+
const n = value.trim() === '' ? NaN : Number(value);
|
|
502
|
+
if (!Number.isFinite(n) || n < 0 || n > 10_000)
|
|
503
|
+
throw new Error('agent-daily-budget must be 0-10000 (USD per local day; 0 = no limit)');
|
|
504
|
+
ctx.config.update({ agentDailyBudgetUsd: Math.round(n * 100) / 100 });
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
default:
|
|
508
|
+
throw new Error(`Unknown setting "${key}"`);
|
|
509
|
+
}
|
|
510
|
+
ctx.out(`${key} updated.`);
|
|
511
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ConfigStore = exports.DEFAULT_CONFIG = void 0;
|
|
37
|
+
exports.configDir = configDir;
|
|
38
|
+
exports.ensurePrivateDir = ensurePrivateDir;
|
|
39
|
+
exports.writePrivateFile = writePrivateFile;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const urls_1 = require("./urls");
|
|
44
|
+
exports.DEFAULT_CONFIG = {
|
|
45
|
+
apiUrl: urls_1.DEFAULT_API_URL,
|
|
46
|
+
endpoint: urls_1.DEFAULT_LOCAL_ENDPOINT,
|
|
47
|
+
maxConcurrency: 1,
|
|
48
|
+
allowBackground: false,
|
|
49
|
+
paused: false,
|
|
50
|
+
};
|
|
51
|
+
function configDir(env = process.env) {
|
|
52
|
+
if (env.RESOLFLOW_CONFIG_DIR)
|
|
53
|
+
return path.resolve(env.RESOLFLOW_CONFIG_DIR);
|
|
54
|
+
const base = env.XDG_CONFIG_HOME ? path.resolve(env.XDG_CONFIG_HOME) : path.join(os.homedir(), '.config');
|
|
55
|
+
return path.join(base, 'resolflow');
|
|
56
|
+
}
|
|
57
|
+
function ensurePrivateDir(dir) {
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
59
|
+
if (process.platform !== 'win32')
|
|
60
|
+
fs.chmodSync(dir, 0o700);
|
|
61
|
+
}
|
|
62
|
+
/** Write a file readable by this user only (atomic rename; 0600). */
|
|
63
|
+
function writePrivateFile(file, content) {
|
|
64
|
+
ensurePrivateDir(path.dirname(file));
|
|
65
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
66
|
+
fs.writeFileSync(tmp, content, { mode: 0o600 });
|
|
67
|
+
if (process.platform !== 'win32')
|
|
68
|
+
fs.chmodSync(tmp, 0o600);
|
|
69
|
+
fs.renameSync(tmp, file);
|
|
70
|
+
}
|
|
71
|
+
class ConfigStore {
|
|
72
|
+
dir;
|
|
73
|
+
file;
|
|
74
|
+
constructor(dir = configDir()) {
|
|
75
|
+
this.dir = dir;
|
|
76
|
+
this.file = path.join(dir, 'config.json');
|
|
77
|
+
}
|
|
78
|
+
load() {
|
|
79
|
+
try {
|
|
80
|
+
const raw = JSON.parse(fs.readFileSync(this.file, 'utf8'));
|
|
81
|
+
return { ...exports.DEFAULT_CONFIG, ...raw };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { ...exports.DEFAULT_CONFIG };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
save(cfg) {
|
|
88
|
+
writePrivateFile(this.file, JSON.stringify(cfg, null, 2) + '\n');
|
|
89
|
+
}
|
|
90
|
+
update(patch) {
|
|
91
|
+
const next = { ...this.load(), ...patch };
|
|
92
|
+
this.save(next);
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
mtimeMs() {
|
|
96
|
+
try {
|
|
97
|
+
return fs.statSync(this.file).mtimeMs;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
exports.ConfigStore = ConfigStore;
|