atris 3.35.0 → 3.36.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/AGENTS.md +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +565 -265
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3029 -339
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
package/commands/engine.js
CHANGED
|
@@ -16,34 +16,66 @@
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const path = require('path');
|
|
19
|
+
const readline = require('readline');
|
|
19
20
|
const { spawnSync } = require('child_process');
|
|
21
|
+
const { Writable } = require('stream');
|
|
20
22
|
const {
|
|
21
23
|
RUNNER_PROFILE_DEFS,
|
|
22
|
-
RUNNER_PROFILE_ALIASES,
|
|
23
24
|
RUNNER_PROFILE_NAMES,
|
|
24
25
|
buildRunnerCommand,
|
|
25
26
|
} = require('../lib/runner-command');
|
|
27
|
+
const {
|
|
28
|
+
ENGINE_ROLES,
|
|
29
|
+
ENGINE_HEALTH_STATUSES,
|
|
30
|
+
binInstalled,
|
|
31
|
+
canonicalEngineName,
|
|
32
|
+
engineRegistryFile,
|
|
33
|
+
engineRegistryView,
|
|
34
|
+
readEngineRegistry,
|
|
35
|
+
resolveEngineForRole,
|
|
36
|
+
setEngineHealth,
|
|
37
|
+
} = require('../lib/engine-registry');
|
|
26
38
|
const { FLEET_CAPABLE, runDispatchFlight } = require('../lib/fleet');
|
|
39
|
+
const { ensureValidCredentials } = require('../utils/auth');
|
|
40
|
+
const { apiRequestJson } = require('../utils/api');
|
|
27
41
|
|
|
28
42
|
const HOUSE_ENGINE = 'atris-fast';
|
|
43
|
+
const MAX_ENGINE_LOGIN_FILE_BYTES = 64 * 1024;
|
|
44
|
+
const ENGINE_DEVICE_LOGIN_POLL_MS = 3000;
|
|
45
|
+
const ENGINE_DEVICE_LOGIN_TIMEOUT_MS = 16 * 60 * 1000;
|
|
46
|
+
const ENGINE_LOGIN_MANIFESTS = Object.freeze({
|
|
47
|
+
codex: Object.freeze({
|
|
48
|
+
type: 'files',
|
|
49
|
+
files: Object.freeze(['~/.codex/auth.json']),
|
|
50
|
+
missingHint: 'run codex login first',
|
|
51
|
+
}),
|
|
52
|
+
claude: Object.freeze({
|
|
53
|
+
type: 'files',
|
|
54
|
+
files: Object.freeze(['~/.claude/.credentials.json']),
|
|
55
|
+
missingHint: 'run claude login first',
|
|
56
|
+
}),
|
|
57
|
+
cursor: Object.freeze({
|
|
58
|
+
type: 'files',
|
|
59
|
+
files: Object.freeze(['~/.cursor/cli-config.json']),
|
|
60
|
+
missingHint: 'run cursor login first',
|
|
61
|
+
}),
|
|
62
|
+
devin: Object.freeze({
|
|
63
|
+
type: 'api_key',
|
|
64
|
+
missingHint: 'paste a Devin API key',
|
|
65
|
+
}),
|
|
66
|
+
grok: Object.freeze({
|
|
67
|
+
type: 'files',
|
|
68
|
+
files: Object.freeze(['~/.grok/auth.json']),
|
|
69
|
+
missingHint: 'run grok and log in with grok.com',
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
29
72
|
|
|
30
|
-
function
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function binInstalled(bin) {
|
|
35
|
-
const safe = String(bin || '').replace(/[^A-Za-z0-9_.-]/g, '');
|
|
36
|
-
if (!safe) return false;
|
|
37
|
-
const probe = spawnSync('sh', ['-c', `command -v ${safe}`], { encoding: 'utf8' });
|
|
38
|
-
return probe.status === 0 && Boolean(String(probe.stdout || '').trim());
|
|
73
|
+
function knownLoginProviders() {
|
|
74
|
+
return Object.keys(ENGINE_LOGIN_MANIFESTS);
|
|
39
75
|
}
|
|
40
76
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
if (!trimmed) return '';
|
|
44
|
-
if (RUNNER_PROFILE_DEFS[trimmed]) return trimmed;
|
|
45
|
-
if (RUNNER_PROFILE_ALIASES[trimmed]) return RUNNER_PROFILE_ALIASES[trimmed];
|
|
46
|
-
return '';
|
|
77
|
+
function engineFile(root = process.cwd()) {
|
|
78
|
+
return path.join(root, '.atris', 'engine.json');
|
|
47
79
|
}
|
|
48
80
|
|
|
49
81
|
function readSavedEngine(root = process.cwd()) {
|
|
@@ -70,11 +102,9 @@ function resolveDefaultEngine(root = process.cwd()) {
|
|
|
70
102
|
|
|
71
103
|
function roster(root = process.cwd()) {
|
|
72
104
|
const current = resolveDefaultEngine(root);
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
installed: binInstalled(RUNNER_PROFILE_DEFS[name].bin),
|
|
77
|
-
default: name === current.name,
|
|
105
|
+
return engineRegistryView(root).map((engine) => ({
|
|
106
|
+
...engine,
|
|
107
|
+
default: engine.id === current.name,
|
|
78
108
|
}));
|
|
79
109
|
}
|
|
80
110
|
|
|
@@ -93,6 +123,764 @@ function resetEngine(root = process.cwd()) {
|
|
|
93
123
|
try { fs.unlinkSync(engineFile(root)); return true; } catch { return false; }
|
|
94
124
|
}
|
|
95
125
|
|
|
126
|
+
function expandHomePath(filePath, homeDir = os.homedir()) {
|
|
127
|
+
const value = String(filePath || '');
|
|
128
|
+
if (value === '~') return homeDir;
|
|
129
|
+
if (value.startsWith('~/')) return path.join(homeDir, value.slice(2));
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeLoginProvider(name) {
|
|
134
|
+
const raw = String(name || '').trim().toLowerCase();
|
|
135
|
+
if (!raw) return '';
|
|
136
|
+
const canonical = canonicalEngineName(raw) || raw;
|
|
137
|
+
return ENGINE_LOGIN_MANIFESTS[canonical] ? canonical : '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeEngineLoginSeat(name) {
|
|
141
|
+
return String(name || '')
|
|
142
|
+
.trim()
|
|
143
|
+
.toUpperCase()
|
|
144
|
+
.replace(/[\s-]+/g, '_');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validEngineLoginSeat(name) {
|
|
148
|
+
return /^[A-Z0-9][A-Z0-9_]{0,47}$/.test(String(name || ''));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function engineLoginSeatError() {
|
|
152
|
+
return 'seat names are letters, numbers, underscores - like personal or work';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function byteLength(value) {
|
|
156
|
+
return Buffer.byteLength(String(value || ''), 'utf8');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function detectedEmailFromJsonValue(value, depth = 0) {
|
|
160
|
+
if (depth > 6 || value == null) return '';
|
|
161
|
+
if (typeof value === 'string') {
|
|
162
|
+
const match = value.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
|
|
163
|
+
return match ? match[0] : '';
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
for (const item of value) {
|
|
167
|
+
const found = detectedEmailFromJsonValue(item, depth + 1);
|
|
168
|
+
if (found) return found;
|
|
169
|
+
}
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
if (typeof value === 'object') {
|
|
173
|
+
const preferred = ['email', 'account_email', 'user_email'];
|
|
174
|
+
for (const key of preferred) {
|
|
175
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
176
|
+
const found = detectedEmailFromJsonValue(value[key], depth + 1);
|
|
177
|
+
if (found) return found;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const item of Object.values(value)) {
|
|
181
|
+
const found = detectedEmailFromJsonValue(item, depth + 1);
|
|
182
|
+
if (found) return found;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function detectedEmailFromContent(content) {
|
|
189
|
+
try {
|
|
190
|
+
return detectedEmailFromJsonValue(JSON.parse(content));
|
|
191
|
+
} catch {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readEngineLoginFiles(provider, { homeDir = os.homedir(), fsModule = fs } = {}) {
|
|
197
|
+
const manifest = ENGINE_LOGIN_MANIFESTS[provider];
|
|
198
|
+
if (!manifest || manifest.type !== 'files') {
|
|
199
|
+
throw new Error(`No file manifest for ${provider}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const files = {};
|
|
203
|
+
const summary = [];
|
|
204
|
+
for (const displayPath of manifest.files) {
|
|
205
|
+
const absolutePath = expandHomePath(displayPath, homeDir);
|
|
206
|
+
let stat;
|
|
207
|
+
try {
|
|
208
|
+
stat = fsModule.statSync(absolutePath);
|
|
209
|
+
} catch {
|
|
210
|
+
const err = new Error(`Missing ${displayPath}. ${manifest.missingHint}.`);
|
|
211
|
+
err.code = 'missing_file';
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
if (!stat.isFile()) {
|
|
215
|
+
const err = new Error(`Missing ${displayPath}. ${manifest.missingHint}.`);
|
|
216
|
+
err.code = 'missing_file';
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
if (stat.size > MAX_ENGINE_LOGIN_FILE_BYTES) {
|
|
220
|
+
const err = new Error(`${displayPath} is ${stat.size} bytes; maximum is ${MAX_ENGINE_LOGIN_FILE_BYTES} bytes.`);
|
|
221
|
+
err.code = 'file_too_large';
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
const content = fsModule.readFileSync(absolutePath, 'utf8');
|
|
225
|
+
files[displayPath] = content;
|
|
226
|
+
summary.push({
|
|
227
|
+
path: displayPath,
|
|
228
|
+
bytes: byteLength(content),
|
|
229
|
+
email: detectedEmailFromContent(content),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return { payload: { files }, summary };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseEngineLoginArgs(args = []) {
|
|
236
|
+
const options = {
|
|
237
|
+
provider: '',
|
|
238
|
+
list: false,
|
|
239
|
+
remove: '',
|
|
240
|
+
yes: false,
|
|
241
|
+
json: false,
|
|
242
|
+
computer: false,
|
|
243
|
+
business: '',
|
|
244
|
+
businessFlag: false,
|
|
245
|
+
seat: '',
|
|
246
|
+
seatFlag: false,
|
|
247
|
+
help: false,
|
|
248
|
+
};
|
|
249
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
250
|
+
const arg = String(args[i] || '');
|
|
251
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
252
|
+
options.help = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (arg === '--list' || arg === 'list') {
|
|
256
|
+
options.list = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (arg === '--remove' && args[i + 1]) {
|
|
260
|
+
options.remove = String(args[i + 1] || '').trim();
|
|
261
|
+
i += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (arg.startsWith('--remove=')) {
|
|
265
|
+
options.remove = arg.slice('--remove='.length).trim();
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (arg === '--yes' || arg === '-y') {
|
|
269
|
+
options.yes = true;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (arg === '--json') {
|
|
273
|
+
options.json = true;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (arg === '--computer') {
|
|
277
|
+
options.computer = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (arg === '--business' || arg === '-b') {
|
|
281
|
+
options.computer = true;
|
|
282
|
+
options.businessFlag = true;
|
|
283
|
+
const next = args[i + 1] === undefined ? '' : String(args[i + 1] || '');
|
|
284
|
+
if (next && !next.startsWith('--')) {
|
|
285
|
+
options.business = next.trim();
|
|
286
|
+
i += 1;
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (arg.startsWith('--business=')) {
|
|
291
|
+
options.computer = true;
|
|
292
|
+
options.businessFlag = true;
|
|
293
|
+
options.business = arg.slice('--business='.length).trim();
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (arg === '--seat') {
|
|
297
|
+
options.seatFlag = true;
|
|
298
|
+
const next = args[i + 1] === undefined ? '' : String(args[i + 1] || '');
|
|
299
|
+
if (next && !next.startsWith('--')) {
|
|
300
|
+
options.seat = normalizeEngineLoginSeat(next);
|
|
301
|
+
i += 1;
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (arg.startsWith('--seat=')) {
|
|
306
|
+
options.seatFlag = true;
|
|
307
|
+
options.seat = normalizeEngineLoginSeat(arg.slice('--seat='.length));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (arg.startsWith('--')) continue;
|
|
311
|
+
if (!options.provider) options.provider = arg;
|
|
312
|
+
}
|
|
313
|
+
return options;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function parseEngineSeedArgs(args = []) {
|
|
317
|
+
const options = {
|
|
318
|
+
provider: '',
|
|
319
|
+
business: '',
|
|
320
|
+
user: false,
|
|
321
|
+
json: false,
|
|
322
|
+
help: false,
|
|
323
|
+
};
|
|
324
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
325
|
+
const arg = String(args[i] || '');
|
|
326
|
+
if (arg === '--help' || arg === '-h' || arg === 'help') {
|
|
327
|
+
options.help = true;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if ((arg === '--business' || arg === '-b') && args[i + 1]) {
|
|
331
|
+
options.business = String(args[i + 1] || '').trim();
|
|
332
|
+
i += 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (arg.startsWith('--business=')) {
|
|
336
|
+
options.business = arg.slice('--business='.length).trim();
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (arg === '--user') {
|
|
340
|
+
options.user = true;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (arg === '--json') {
|
|
344
|
+
options.json = true;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (arg.startsWith('--')) continue;
|
|
348
|
+
if (!options.provider) options.provider = arg;
|
|
349
|
+
}
|
|
350
|
+
return options;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function printEngineLoginHelp() {
|
|
354
|
+
console.log('\n atris engine login <provider> --yes\n upload a local whitelisted engine login to the Atris vault\n atris engine login <provider> --computer [--seat <name>]\n sign in on one of your Atris computers by device flow\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on a business Atris computer by device flow\n atris engine login --list\n list vaulted engine logins\n atris engine login --remove <provider>\n remove a vaulted engine login\n providers: codex, claude, cursor, devin, grok\n');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function printEngineSeedHelp() {
|
|
358
|
+
console.log('\n atris engine seed <provider> --business <id>\n atris engine seed <provider> --user\n ask the backend to seed a vaulted login onto a computer\n');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function redactBackendResponse(value, parentKey = '') {
|
|
362
|
+
if (value == null) return value;
|
|
363
|
+
if (Array.isArray(value)) return value.map((item) => redactBackendResponse(item, parentKey));
|
|
364
|
+
if (typeof value !== 'object') {
|
|
365
|
+
if (/api[_-]?key|token|secret|credential|auth|files?/i.test(parentKey)) return '[redacted]';
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
const output = {};
|
|
369
|
+
for (const [key, item] of Object.entries(value)) {
|
|
370
|
+
if (/api[_-]?key|token|secret|credential|auth/i.test(key)) {
|
|
371
|
+
output[key] = '[redacted]';
|
|
372
|
+
} else if (key === 'files' && item && typeof item === 'object') {
|
|
373
|
+
output[key] = Object.fromEntries(Object.keys(item).map((filePath) => [filePath, '[redacted]']));
|
|
374
|
+
} else {
|
|
375
|
+
output[key] = redactBackendResponse(item, key);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return output;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function printBackendResult(result, { json = false } = {}) {
|
|
382
|
+
const data = result && result.data !== undefined && result.data !== null
|
|
383
|
+
? result.data
|
|
384
|
+
: { ok: Boolean(result && result.ok), status: result && result.status };
|
|
385
|
+
const safe = redactBackendResponse(data);
|
|
386
|
+
if (json || typeof safe === 'object') {
|
|
387
|
+
console.log(JSON.stringify(safe, null, 2));
|
|
388
|
+
} else {
|
|
389
|
+
console.log(String(safe));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function printLoginSummary(provider, summary) {
|
|
394
|
+
console.log('');
|
|
395
|
+
console.log(` engine login: ${provider}`);
|
|
396
|
+
for (const item of summary) {
|
|
397
|
+
const email = item.email ? ` email: ${item.email}` : '';
|
|
398
|
+
console.log(` ${item.path.padEnd(32)} ${String(item.bytes).padStart(6)} bytes${email}`);
|
|
399
|
+
}
|
|
400
|
+
console.log('');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function readLineNoEcho(question) {
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
if (!process.stdin.isTTY) {
|
|
406
|
+
let data = '';
|
|
407
|
+
process.stdout.write(question);
|
|
408
|
+
process.stdin.setEncoding('utf8');
|
|
409
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
410
|
+
process.stdin.on('end', () => resolve(data.trim()));
|
|
411
|
+
process.stdin.on('error', () => resolve(''));
|
|
412
|
+
process.stdin.resume();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const muted = new Writable({
|
|
417
|
+
write(chunk, encoding, callback) {
|
|
418
|
+
if (!muted.muted) process.stdout.write(chunk, encoding);
|
|
419
|
+
callback();
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
const rl = readline.createInterface({ input: process.stdin, output: muted, terminal: true });
|
|
423
|
+
rl.question(question, (answer) => {
|
|
424
|
+
rl.close();
|
|
425
|
+
process.stdout.write('\n');
|
|
426
|
+
resolve(String(answer || '').trim());
|
|
427
|
+
});
|
|
428
|
+
muted.muted = true;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function readLineVisible(question) {
|
|
433
|
+
return new Promise((resolve) => {
|
|
434
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
435
|
+
rl.question(question, (answer) => {
|
|
436
|
+
rl.close();
|
|
437
|
+
resolve(String(answer || '').trim());
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function promptApiKey(provider, deps = {}) {
|
|
443
|
+
if (typeof deps.promptSecret === 'function') {
|
|
444
|
+
return String(await deps.promptSecret(provider) || '').trim();
|
|
445
|
+
}
|
|
446
|
+
return readLineNoEcho(`Paste ${provider} API key: `);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function confirmEngineLoginUpload(provider, deps = {}) {
|
|
450
|
+
if (typeof deps.confirmUpload === 'function') {
|
|
451
|
+
return Boolean(await deps.confirmUpload(provider));
|
|
452
|
+
}
|
|
453
|
+
const answer = await readLineVisible('Upload this credential to the Atris backend vault? [y/N] ');
|
|
454
|
+
return /^y(es)?$/i.test(answer);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function authenticatedEngineApi(pathname, options, deps = {}) {
|
|
458
|
+
const apiFn = deps.apiRequestJson || apiRequestJson;
|
|
459
|
+
const ensureFn = deps.ensureValidCredentials || ensureValidCredentials;
|
|
460
|
+
const ensured = await ensureFn(apiFn);
|
|
461
|
+
if (ensured.error) {
|
|
462
|
+
return {
|
|
463
|
+
ok: false,
|
|
464
|
+
status: 0,
|
|
465
|
+
data: null,
|
|
466
|
+
error: ensured.detail || ensured.error || 'not_logged_in',
|
|
467
|
+
authError: true,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const token = ensured.credentials && ensured.credentials.token;
|
|
471
|
+
return apiFn(pathname, {
|
|
472
|
+
...options,
|
|
473
|
+
token,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function listEngineLogins(deps = {}) {
|
|
478
|
+
return authenticatedEngineApi('/engines/logins', {
|
|
479
|
+
method: 'GET',
|
|
480
|
+
timeoutMs: 15000,
|
|
481
|
+
retries: 0,
|
|
482
|
+
}, deps);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function listEngineSeats(deps = {}) {
|
|
486
|
+
return authenticatedEngineApi('/engines/logins/seats', {
|
|
487
|
+
method: 'GET',
|
|
488
|
+
timeoutMs: 15000,
|
|
489
|
+
retries: 0,
|
|
490
|
+
}, deps);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function readyCheckEngineLogin(provider, target, deps = {}) {
|
|
494
|
+
return authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}/ready-check`, {
|
|
495
|
+
method: 'POST',
|
|
496
|
+
body: { target },
|
|
497
|
+
timeoutMs: 30000,
|
|
498
|
+
retries: 0,
|
|
499
|
+
}, deps);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function formatEngineSeats(data, now = Date.now()) {
|
|
503
|
+
const seats = Array.isArray(data?.seats) ? data.seats : [];
|
|
504
|
+
if (!seats.length) return 'No accounts linked yet. Run: atris computer setup';
|
|
505
|
+
|
|
506
|
+
const nowValue = now instanceof Date ? now.getTime() : Number(now);
|
|
507
|
+
const nowSeconds = Number.isFinite(nowValue) ? nowValue / 1000 : Date.now() / 1000;
|
|
508
|
+
return seats.map((seat) => {
|
|
509
|
+
const engine = String(seat?.engine || 'engine').trim().toLowerCase();
|
|
510
|
+
const name = String(seat?.name || 'account').trim().toLowerCase();
|
|
511
|
+
const coolingUntil = Number(seat?.cooling_until);
|
|
512
|
+
if (!Number.isFinite(coolingUntil) || coolingUntil <= nowSeconds) {
|
|
513
|
+
return `${engine} ${name} - ready`;
|
|
514
|
+
}
|
|
515
|
+
const minutes = Math.ceil((coolingUntil - nowSeconds) / 60);
|
|
516
|
+
const hours = Math.floor(minutes / 60);
|
|
517
|
+
return `${engine} ${name} - cooling down, back ${hours}h ${minutes % 60}m`;
|
|
518
|
+
}).join('\n');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function runEngineSeatsCommand(deps = {}) {
|
|
522
|
+
const result = await listEngineSeats(deps);
|
|
523
|
+
if (!result.ok) {
|
|
524
|
+
console.error(result.authError ? 'Run atris login first.' : `engine seats failed: ${result.error || result.status}`);
|
|
525
|
+
return 1;
|
|
526
|
+
}
|
|
527
|
+
const now = typeof deps.now === 'function' ? deps.now() : Date.now();
|
|
528
|
+
console.log(formatEngineSeats(result.data, now));
|
|
529
|
+
return 0;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function buildEngineLoginPayload(provider, deps = {}) {
|
|
533
|
+
const manifest = ENGINE_LOGIN_MANIFESTS[provider];
|
|
534
|
+
if (!manifest) {
|
|
535
|
+
throw new Error(`Unknown engine login provider "${provider}". Known providers: ${knownLoginProviders().join(', ')}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (manifest.type === 'api_key') {
|
|
539
|
+
const apiKey = await promptApiKey(provider, deps);
|
|
540
|
+
if (!apiKey) {
|
|
541
|
+
const err = new Error('No API key provided.');
|
|
542
|
+
err.code = 'missing_api_key';
|
|
543
|
+
throw err;
|
|
544
|
+
}
|
|
545
|
+
if (byteLength(apiKey) > MAX_ENGINE_LOGIN_FILE_BYTES) {
|
|
546
|
+
const err = new Error(`API key is ${byteLength(apiKey)} bytes; maximum is ${MAX_ENGINE_LOGIN_FILE_BYTES} bytes.`);
|
|
547
|
+
err.code = 'api_key_too_large';
|
|
548
|
+
throw err;
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
payload: { api_key: apiKey },
|
|
552
|
+
summary: [{ path: 'api key', bytes: byteLength(apiKey), email: '' }],
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return readEngineLoginFiles(provider, deps);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function engineDeviceLoginTarget(options) {
|
|
560
|
+
if (options.target) return options.target;
|
|
561
|
+
if (!options.computer) return null;
|
|
562
|
+
if (options.businessFlag && !options.business) {
|
|
563
|
+
const err = new Error('usage: atris engine login <provider> --business <id>');
|
|
564
|
+
err.code = 'usage';
|
|
565
|
+
throw err;
|
|
566
|
+
}
|
|
567
|
+
if (options.business) return { type: 'business', id: options.business };
|
|
568
|
+
return { type: 'user' };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function normalizeDeviceLoginStatus(data, provider, sessionId, seat = '') {
|
|
572
|
+
const payload = data && typeof data === 'object' ? data : {};
|
|
573
|
+
return {
|
|
574
|
+
...payload,
|
|
575
|
+
session_id: payload.session_id || sessionId || '',
|
|
576
|
+
provider: payload.provider || provider,
|
|
577
|
+
...(payload.seat || seat ? { seat: payload.seat || seat } : {}),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function printDeviceLoginCode(status, { json = false } = {}) {
|
|
582
|
+
const lines = [
|
|
583
|
+
'',
|
|
584
|
+
`Sign in: ${status.verify_url}`,
|
|
585
|
+
`Code: ${status.code} (expires in 15 minutes; never share this code)`,
|
|
586
|
+
'',
|
|
587
|
+
];
|
|
588
|
+
for (const line of lines) {
|
|
589
|
+
if (json) console.error(line);
|
|
590
|
+
else console.log(line);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function printDeviceLoginCompleted(status, { json = false } = {}) {
|
|
595
|
+
if (json) {
|
|
596
|
+
console.log(JSON.stringify(status, null, 2));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const registered = status.registered === true ? 'true' : status.registered === false ? 'false' : String(status.registered ?? '');
|
|
600
|
+
console.log('');
|
|
601
|
+
console.log(`engine login ready: ${status.provider}`);
|
|
602
|
+
if (status.seat) console.log(`seat: ${status.seat}`);
|
|
603
|
+
console.log(`account_email: ${status.account_email || '(none)'}`);
|
|
604
|
+
console.log(`registered: ${registered}`);
|
|
605
|
+
console.log(`ready-check: provider=${status.provider} session_id=${status.session_id || ''} status=${status.status} registered=${registered}`);
|
|
606
|
+
console.log('');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function printDeviceLoginFinalJson(status, options) {
|
|
610
|
+
if (options.json) console.log(JSON.stringify(status, null, 2));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function waitForDeviceLoginPoll(ms, deps = {}) {
|
|
614
|
+
if (typeof deps.sleep === 'function') return deps.sleep(ms);
|
|
615
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function readDeviceLoginPasteBackCode(deps = {}) {
|
|
619
|
+
const readlineDep = deps.readline || readline;
|
|
620
|
+
const input = deps.stdin || process.stdin;
|
|
621
|
+
const output = deps.stdout || process.stdout;
|
|
622
|
+
const rl = readlineDep.createInterface({ input, output });
|
|
623
|
+
try {
|
|
624
|
+
return String(await new Promise((resolve) => rl.question('', resolve)) || '').trim();
|
|
625
|
+
} finally {
|
|
626
|
+
rl.close();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function runEngineDeviceLoginCommand(provider, options, deps = {}) {
|
|
631
|
+
let target;
|
|
632
|
+
let seat;
|
|
633
|
+
try {
|
|
634
|
+
target = engineDeviceLoginTarget(options);
|
|
635
|
+
seat = normalizeEngineLoginSeat(options.seat);
|
|
636
|
+
if ((options.seatFlag || options.seat) && !validEngineLoginSeat(seat)) {
|
|
637
|
+
const err = new Error(engineLoginSeatError());
|
|
638
|
+
err.code = 'usage';
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.error(err.message);
|
|
643
|
+
return 2;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const body = seat ? { target, seat } : { target };
|
|
647
|
+
const started = await authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}/device-login`, {
|
|
648
|
+
method: 'POST',
|
|
649
|
+
body,
|
|
650
|
+
timeoutMs: 30000,
|
|
651
|
+
retries: 0,
|
|
652
|
+
}, deps);
|
|
653
|
+
if (!started.ok) {
|
|
654
|
+
console.error(options.setup
|
|
655
|
+
? `could not connect the ${provider} account; please try again.`
|
|
656
|
+
: (started.authError ? 'Run atris login first.' : `engine login device flow failed: ${started.error || started.status}`));
|
|
657
|
+
return 1;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const startStatus = normalizeDeviceLoginStatus(started.data, provider, '', seat);
|
|
661
|
+
const sessionId = startStatus.session_id;
|
|
662
|
+
if (!sessionId) {
|
|
663
|
+
console.error(options.setup
|
|
664
|
+
? `could not connect the ${provider} account; please try again.`
|
|
665
|
+
: 'engine login device flow failed: missing session_id');
|
|
666
|
+
return 1;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let finalStatus = startStatus;
|
|
670
|
+
let codePrinted = false;
|
|
671
|
+
let pasteBackSubmitted = false;
|
|
672
|
+
const pollMs = Math.max(1, Number(deps.deviceLoginPollMs ?? ENGINE_DEVICE_LOGIN_POLL_MS));
|
|
673
|
+
const timeoutMs = Math.max(0, Number(deps.deviceLoginTimeoutMs ?? ENGINE_DEVICE_LOGIN_TIMEOUT_MS));
|
|
674
|
+
const maxPolls = Math.ceil(timeoutMs / pollMs);
|
|
675
|
+
|
|
676
|
+
for (let attempt = 0; attempt <= maxPolls; attempt += 1) {
|
|
677
|
+
const polled = await authenticatedEngineApi(`/engines/logins/device-login/${encodeURIComponent(sessionId)}`, {
|
|
678
|
+
method: 'GET',
|
|
679
|
+
timeoutMs: 30000,
|
|
680
|
+
retries: 0,
|
|
681
|
+
}, deps);
|
|
682
|
+
if (!polled.ok) {
|
|
683
|
+
console.error(options.setup
|
|
684
|
+
? `could not connect the ${provider} account; please try again.`
|
|
685
|
+
: (polled.authError ? 'Run atris login first.' : `engine login device poll failed: ${polled.error || polled.status}`));
|
|
686
|
+
return 1;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
finalStatus = normalizeDeviceLoginStatus(polled.data, provider, sessionId, seat);
|
|
690
|
+
const status = String(finalStatus.status || '').toLowerCase();
|
|
691
|
+
if (!codePrinted && finalStatus.verify_url && finalStatus.code) {
|
|
692
|
+
if (options.setup) {
|
|
693
|
+
console.log(`On your phone, open: ${finalStatus.verify_url} and type the code: ${finalStatus.code}`);
|
|
694
|
+
} else {
|
|
695
|
+
printDeviceLoginCode(finalStatus, { json: options.json });
|
|
696
|
+
}
|
|
697
|
+
codePrinted = true;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const pasteBackRequired = !pasteBackSubmitted
|
|
701
|
+
&& status === 'pending_user'
|
|
702
|
+
&& finalStatus.verify_url
|
|
703
|
+
&& !finalStatus.code
|
|
704
|
+
&& String(finalStatus.provider || provider).toLowerCase() === 'claude';
|
|
705
|
+
if (pasteBackRequired) {
|
|
706
|
+
const printPrompt = options.json ? console.error : console.log;
|
|
707
|
+
printPrompt(`On your phone, open: ${finalStatus.verify_url}`);
|
|
708
|
+
let submitted = false;
|
|
709
|
+
for (let submitAttempt = 0; submitAttempt < 2; submitAttempt += 1) {
|
|
710
|
+
printPrompt('When the browser shows you a code, paste it here:');
|
|
711
|
+
const pastedCode = await readDeviceLoginPasteBackCode(deps);
|
|
712
|
+
const codeResult = await authenticatedEngineApi(
|
|
713
|
+
`/engines/logins/device-login/${encodeURIComponent(sessionId)}/code`,
|
|
714
|
+
{
|
|
715
|
+
method: 'POST',
|
|
716
|
+
body: { code: pastedCode },
|
|
717
|
+
timeoutMs: 30000,
|
|
718
|
+
retries: 0,
|
|
719
|
+
},
|
|
720
|
+
deps
|
|
721
|
+
);
|
|
722
|
+
if (codeResult.ok) {
|
|
723
|
+
submitted = true;
|
|
724
|
+
pasteBackSubmitted = true;
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
if (submitAttempt === 0) {
|
|
728
|
+
console.error('that code did not work; paste it again.');
|
|
729
|
+
} else {
|
|
730
|
+
console.error('that code did not work; please start the login again.');
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (!submitted) return 1;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (status === 'completed') {
|
|
737
|
+
if (options.setup) console.log(`${seat} linked.`);
|
|
738
|
+
else printDeviceLoginCompleted(finalStatus, { json: options.json });
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
if (status === 'failed' || status === 'expired') {
|
|
742
|
+
if (options.setup) {
|
|
743
|
+
console.error(`could not connect the ${provider} account; please try again.`);
|
|
744
|
+
} else {
|
|
745
|
+
printDeviceLoginFinalJson(finalStatus, options);
|
|
746
|
+
console.error(`engine login device flow ended: ${status}`);
|
|
747
|
+
}
|
|
748
|
+
return 1;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (attempt < maxPolls) await waitForDeviceLoginPoll(pollMs, deps);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
finalStatus = {
|
|
755
|
+
...finalStatus,
|
|
756
|
+
status: 'timeout',
|
|
757
|
+
error: `device login did not complete within ${Math.round(timeoutMs / 60000)} minutes`,
|
|
758
|
+
};
|
|
759
|
+
if (options.setup) {
|
|
760
|
+
console.error(`could not connect the ${provider} account; please try again.`);
|
|
761
|
+
} else {
|
|
762
|
+
printDeviceLoginFinalJson(finalStatus, options);
|
|
763
|
+
console.error('engine login device flow timed out');
|
|
764
|
+
}
|
|
765
|
+
return 1;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function runEngineLoginCommand(args, root, deps = {}) {
|
|
769
|
+
const options = parseEngineLoginArgs(args);
|
|
770
|
+
if (options.help) {
|
|
771
|
+
printEngineLoginHelp();
|
|
772
|
+
return 0;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (options.list) {
|
|
776
|
+
const result = await listEngineLogins(deps);
|
|
777
|
+
if (!result.ok) {
|
|
778
|
+
console.error(result.authError ? 'Run atris login first.' : `engine login list failed: ${result.error || result.status}`);
|
|
779
|
+
return 1;
|
|
780
|
+
}
|
|
781
|
+
printBackendResult(result, { json: options.json });
|
|
782
|
+
return 0;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (options.remove) {
|
|
786
|
+
const provider = normalizeLoginProvider(options.remove);
|
|
787
|
+
if (!provider) {
|
|
788
|
+
console.error(`Unknown engine login provider "${options.remove}". Known providers: ${knownLoginProviders().join(', ')}`);
|
|
789
|
+
return 2;
|
|
790
|
+
}
|
|
791
|
+
const result = await authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}`, {
|
|
792
|
+
method: 'DELETE',
|
|
793
|
+
timeoutMs: 15000,
|
|
794
|
+
retries: 0,
|
|
795
|
+
}, deps);
|
|
796
|
+
if (!result.ok) {
|
|
797
|
+
console.error(result.authError ? 'Run atris login first.' : `engine login remove failed: ${result.error || result.status}`);
|
|
798
|
+
return 1;
|
|
799
|
+
}
|
|
800
|
+
printBackendResult(result, { json: options.json });
|
|
801
|
+
return 0;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const provider = normalizeLoginProvider(options.provider);
|
|
805
|
+
if (!provider) {
|
|
806
|
+
console.error(`usage: atris engine login <provider> --yes; providers: ${knownLoginProviders().join(', ')}`);
|
|
807
|
+
return 2;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (options.seatFlag && !options.computer) {
|
|
811
|
+
console.error('use --seat with --computer or --business');
|
|
812
|
+
return 2;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (options.computer) {
|
|
816
|
+
return runEngineDeviceLoginCommand(provider, options, deps);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
let built;
|
|
820
|
+
try {
|
|
821
|
+
built = await buildEngineLoginPayload(provider, deps);
|
|
822
|
+
} catch (err) {
|
|
823
|
+
console.error(err.message);
|
|
824
|
+
return err.code === 'missing_file' || err.code === 'missing_api_key' ? 1 : 2;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
printLoginSummary(provider, built.summary);
|
|
828
|
+
if (!options.yes) {
|
|
829
|
+
const confirmed = await confirmEngineLoginUpload(provider, deps);
|
|
830
|
+
if (!confirmed) {
|
|
831
|
+
console.error('engine login upload cancelled');
|
|
832
|
+
return 1;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const result = await authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}`, {
|
|
837
|
+
method: 'POST',
|
|
838
|
+
body: built.payload,
|
|
839
|
+
timeoutMs: 30000,
|
|
840
|
+
retries: 0,
|
|
841
|
+
}, deps);
|
|
842
|
+
if (!result.ok) {
|
|
843
|
+
console.error(result.authError ? 'Run atris login first.' : `engine login upload failed: ${result.error || result.status}`);
|
|
844
|
+
return 1;
|
|
845
|
+
}
|
|
846
|
+
printBackendResult(result, { json: options.json });
|
|
847
|
+
return 0;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function runEngineSeedCommand(args, root, deps = {}) {
|
|
851
|
+
const options = parseEngineSeedArgs(args);
|
|
852
|
+
if (options.help) {
|
|
853
|
+
printEngineSeedHelp();
|
|
854
|
+
return 0;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const provider = normalizeLoginProvider(options.provider);
|
|
858
|
+
if (!provider) {
|
|
859
|
+
console.error(`usage: atris engine seed <provider> --business <id>|--user; providers: ${knownLoginProviders().join(', ')}`);
|
|
860
|
+
return 2;
|
|
861
|
+
}
|
|
862
|
+
if ((options.business && options.user) || (!options.business && !options.user)) {
|
|
863
|
+
console.error('usage: atris engine seed <provider> --business <id>|--user');
|
|
864
|
+
return 2;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const body = options.user
|
|
868
|
+
? { target: { type: 'user' } }
|
|
869
|
+
: { target: { type: 'business', id: options.business } };
|
|
870
|
+
const result = await authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}/seed`, {
|
|
871
|
+
method: 'POST',
|
|
872
|
+
body,
|
|
873
|
+
timeoutMs: 60000,
|
|
874
|
+
retries: 0,
|
|
875
|
+
}, deps);
|
|
876
|
+
if (!result.ok) {
|
|
877
|
+
console.error(result.authError ? 'Run atris login first.' : `engine seed failed: ${result.error || result.status}`);
|
|
878
|
+
return 1;
|
|
879
|
+
}
|
|
880
|
+
printBackendResult(result, { json: options.json });
|
|
881
|
+
return 0;
|
|
882
|
+
}
|
|
883
|
+
|
|
96
884
|
function printRoster(root) {
|
|
97
885
|
const list = roster(root);
|
|
98
886
|
const found = list.filter((e) => e.installed).length;
|
|
@@ -102,8 +890,9 @@ function printRoster(root) {
|
|
|
102
890
|
console.log('');
|
|
103
891
|
for (const engine of list) {
|
|
104
892
|
const mark = engine.default ? '→' : ' ';
|
|
105
|
-
const state = engine.
|
|
106
|
-
|
|
893
|
+
const state = engine.health.status === 'not_installed' ? 'not installed' : engine.health.status.replace(/_/g, ' ');
|
|
894
|
+
const roles = engine.roles.join(',');
|
|
895
|
+
console.log(` ${mark} ${engine.id.padEnd(12)} ${state.padEnd(13)} ${engine.tier.padEnd(4)} ${roles}`);
|
|
107
896
|
}
|
|
108
897
|
console.log('');
|
|
109
898
|
console.log(` default: ${current.name}${current.source === 'saved' ? ' (set here)' : current.source === 'env' ? ' (this session)' : ''}`);
|
|
@@ -111,6 +900,80 @@ function printRoster(root) {
|
|
|
111
900
|
console.log('');
|
|
112
901
|
}
|
|
113
902
|
|
|
903
|
+
function registryPayload(root) {
|
|
904
|
+
const current = resolveDefaultEngine(root);
|
|
905
|
+
const registry = readEngineRegistry(root);
|
|
906
|
+
return {
|
|
907
|
+
default: current.name,
|
|
908
|
+
source: current.source,
|
|
909
|
+
engines: registry.engines.map((engine) => ({
|
|
910
|
+
...engine,
|
|
911
|
+
default: engine.id === current.name,
|
|
912
|
+
})),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function parseSetFlag(args) {
|
|
917
|
+
const prefix = '--set=';
|
|
918
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
919
|
+
const arg = String(args[i]);
|
|
920
|
+
if (arg === '--set') return args[i + 1] || '';
|
|
921
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
922
|
+
}
|
|
923
|
+
return '';
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function runResolveCommand(args, root) {
|
|
927
|
+
const json = args.includes('--json');
|
|
928
|
+
const role = args.filter((a) => !String(a).startsWith('--'))[0] || '';
|
|
929
|
+
if (!role) {
|
|
930
|
+
const message = `usage: atris engine resolve <role>; roles: ${ENGINE_ROLES.join(', ')}`;
|
|
931
|
+
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
932
|
+
else console.error(message);
|
|
933
|
+
return 2;
|
|
934
|
+
}
|
|
935
|
+
let engine;
|
|
936
|
+
try {
|
|
937
|
+
engine = resolveEngineForRole(role, root);
|
|
938
|
+
} catch (err) {
|
|
939
|
+
if (json) console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
|
|
940
|
+
else console.error(err.message);
|
|
941
|
+
return 2;
|
|
942
|
+
}
|
|
943
|
+
if (!engine) {
|
|
944
|
+
const message = `No ready installed engine can fill role "${role}".`;
|
|
945
|
+
if (json) console.log(JSON.stringify({ ok: false, role, error: message }, null, 2));
|
|
946
|
+
else console.error(message);
|
|
947
|
+
return 1;
|
|
948
|
+
}
|
|
949
|
+
if (json) console.log(JSON.stringify(engine, null, 2));
|
|
950
|
+
else console.log(engine.id);
|
|
951
|
+
return 0;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function runHealthCommand(args, root) {
|
|
955
|
+
const json = args.includes('--json');
|
|
956
|
+
const positional = args.filter((a) => !String(a).startsWith('--'));
|
|
957
|
+
const name = positional[0] || '';
|
|
958
|
+
const status = parseSetFlag(args);
|
|
959
|
+
if (!name || !status) {
|
|
960
|
+
const message = `usage: atris engine health <name> --set <status>; statuses: ${ENGINE_HEALTH_STATUSES.join(', ')}`;
|
|
961
|
+
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
962
|
+
else console.error(message);
|
|
963
|
+
return 2;
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
const engine = setEngineHealth(name, status, root);
|
|
967
|
+
if (json) console.log(JSON.stringify(engine, null, 2));
|
|
968
|
+
else console.log(`engine ${engine.id} health: ${engine.health.status}`);
|
|
969
|
+
return 0;
|
|
970
|
+
} catch (err) {
|
|
971
|
+
if (json) console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
|
|
972
|
+
else console.error(err.message);
|
|
973
|
+
return 2;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
114
977
|
// Preflight: run one engine CLI headless with a reply-OK prompt and report
|
|
115
978
|
// pass/fail. A dead login, missing binary, or hung spawn is a one-command
|
|
116
979
|
// diagnosis instead of a failed overnight flight. `name` is canonical.
|
|
@@ -189,6 +1052,37 @@ function probeEngine(name, { timeout = PROBE_DEFAULT_TIMEOUT_MS } = {}) {
|
|
|
189
1052
|
};
|
|
190
1053
|
}
|
|
191
1054
|
|
|
1055
|
+
// A probe is a real engine round trip on the same command shape dispatches
|
|
1056
|
+
// use, so its pass/fail and latency are routing evidence. Record one receipt
|
|
1057
|
+
// per role the engine serves so the router brain (lib/router-brain.js) can
|
|
1058
|
+
// learn from every preflight. Liveness evidence only: real build outcomes
|
|
1059
|
+
// land in the same pool via task receipts and outweigh probes over time.
|
|
1060
|
+
function writeProbeReceipt(result, root = process.cwd()) {
|
|
1061
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
1062
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
1063
|
+
let roles = [];
|
|
1064
|
+
try {
|
|
1065
|
+
const engine = engineRegistryView(root).find((row) => row.id === result.engine);
|
|
1066
|
+
roles = engine && Array.isArray(engine.roles) ? engine.roles : [];
|
|
1067
|
+
} catch { /* registry unavailable: still record the probe */ }
|
|
1068
|
+
if (!roles.length) roles = ['executor'];
|
|
1069
|
+
const at = new Date().toISOString();
|
|
1070
|
+
const stampMs = Date.now();
|
|
1071
|
+
for (const role of roles) {
|
|
1072
|
+
const receipt = {
|
|
1073
|
+
schema: 'atris.engine_probe_receipt.v1',
|
|
1074
|
+
engine: result.engine,
|
|
1075
|
+
task_type: role,
|
|
1076
|
+
verified_passed: Boolean(result.pass),
|
|
1077
|
+
duration_ms: result.durationMs,
|
|
1078
|
+
reason: result.reason,
|
|
1079
|
+
at,
|
|
1080
|
+
};
|
|
1081
|
+
const file = path.join(runsDir, `engine-probe-task-${result.engine}-${role}-${stampMs}.json`);
|
|
1082
|
+
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
192
1086
|
function runEngineTest(targets, { json, root } = {}) {
|
|
193
1087
|
let enginesToTest;
|
|
194
1088
|
if (targets && targets.length) {
|
|
@@ -213,6 +1107,14 @@ function runEngineTest(targets, { json, root } = {}) {
|
|
|
213
1107
|
const results = enginesToTest.map((name) => probeEngine(name));
|
|
214
1108
|
const failures = results.filter((r) => !r.pass);
|
|
215
1109
|
const passed = results.length - failures.length;
|
|
1110
|
+
// A live probe is the freshest health evidence there is: write it back so
|
|
1111
|
+
// a transient failure does not leave an engine stuck on error forever.
|
|
1112
|
+
for (const r of results) {
|
|
1113
|
+
try { setEngineHealth(r.engine, r.pass ? 'ready' : 'error', root); } catch { /* best-effort */ }
|
|
1114
|
+
if (r.reason !== 'not-installed') {
|
|
1115
|
+
try { writeProbeReceipt(r, root); } catch { /* best-effort */ }
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
216
1118
|
if (json) {
|
|
217
1119
|
console.log(JSON.stringify({
|
|
218
1120
|
ok: failures.length === 0,
|
|
@@ -253,6 +1155,7 @@ function parseDispatchArgs(args) {
|
|
|
253
1155
|
let promptFile = '';
|
|
254
1156
|
let base = '';
|
|
255
1157
|
let json = false;
|
|
1158
|
+
let yolo = false;
|
|
256
1159
|
for (let i = 0; i < args.length; i += 1) {
|
|
257
1160
|
const a = args[i];
|
|
258
1161
|
if (a === '--engine') { engine = args[i + 1] || ''; i += 1; continue; }
|
|
@@ -262,16 +1165,17 @@ function parseDispatchArgs(args) {
|
|
|
262
1165
|
if (a === '--base') { base = args[i + 1] || ''; i += 1; continue; }
|
|
263
1166
|
if (a.startsWith('--base=')) { base = a.slice('--base='.length); continue; }
|
|
264
1167
|
if (a === '--json') { json = true; continue; }
|
|
1168
|
+
if (a === '--yolo') { yolo = true; continue; }
|
|
265
1169
|
if (a.startsWith('--')) continue;
|
|
266
1170
|
taskIds.push(a);
|
|
267
1171
|
}
|
|
268
|
-
return { taskIds, engine, promptFile, base, json };
|
|
1172
|
+
return { taskIds, engine, promptFile, base, json, yolo };
|
|
269
1173
|
}
|
|
270
1174
|
|
|
271
1175
|
function runDispatchCommand(args, root) {
|
|
272
|
-
const { taskIds, engine, promptFile, base, json } = parseDispatchArgs(args);
|
|
1176
|
+
const { taskIds, engine, promptFile, base, json, yolo } = parseDispatchArgs(args);
|
|
273
1177
|
if (!taskIds.length || !engine) {
|
|
274
|
-
console.error('usage: atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>]');
|
|
1178
|
+
console.error('usage: atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]');
|
|
275
1179
|
return 2;
|
|
276
1180
|
}
|
|
277
1181
|
const canonical = canonicalEngineName(engine);
|
|
@@ -299,13 +1203,13 @@ function runDispatchCommand(args, root) {
|
|
|
299
1203
|
console.error(`engine dispatch: ${canonical} CLI (${def.bin}) is not installed here`);
|
|
300
1204
|
return 2;
|
|
301
1205
|
}
|
|
302
|
-
return runDispatchFlight({ root, taskIds, engine: canonical, prompt: promptOverride, ...(base ? { checkoutBase: base } : {}) }).then((flight) => {
|
|
1206
|
+
return runDispatchFlight({ root, taskIds, engine: canonical, prompt: promptOverride, yolo, ...(base ? { checkoutBase: base } : {}) }).then((flight) => {
|
|
303
1207
|
if (json) console.log(JSON.stringify(flight, null, 2));
|
|
304
1208
|
return flight.paused.length ? 1 : 0;
|
|
305
1209
|
});
|
|
306
1210
|
}
|
|
307
1211
|
|
|
308
|
-
function engineCommand(args = []) {
|
|
1212
|
+
function engineCommand(args = [], deps = {}) {
|
|
309
1213
|
const root = process.cwd();
|
|
310
1214
|
if ((args[0] || '').trim() === 'dispatch') {
|
|
311
1215
|
return runDispatchCommand(args.slice(1), root);
|
|
@@ -315,14 +1219,33 @@ function engineCommand(args = []) {
|
|
|
315
1219
|
const positional = args.filter((a) => !a.startsWith('--'));
|
|
316
1220
|
const sub = (positional[0] || '').trim();
|
|
317
1221
|
|
|
1222
|
+
if (sub === 'login') {
|
|
1223
|
+
return runEngineLoginCommand(args.slice(args.indexOf('login') + 1), root);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (sub === 'seed') {
|
|
1227
|
+
return runEngineSeedCommand(args.slice(args.indexOf('seed') + 1), root);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (sub === 'seats') {
|
|
1231
|
+
return runEngineSeatsCommand(deps);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
318
1234
|
if (sub === 'test') {
|
|
319
1235
|
return runEngineTest(positional.slice(1), { json, root });
|
|
320
1236
|
}
|
|
321
1237
|
|
|
1238
|
+
if (sub === 'resolve') {
|
|
1239
|
+
return runResolveCommand(args.slice(args.indexOf('resolve') + 1), root);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (sub === 'health') {
|
|
1243
|
+
return runHealthCommand(args.slice(args.indexOf('health') + 1), root);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
322
1246
|
if (!sub || sub === 'list' || sub === 'status') {
|
|
323
1247
|
if (json) {
|
|
324
|
-
|
|
325
|
-
console.log(JSON.stringify({ engines: roster(root), default: current.name, source: current.source }, null, 2));
|
|
1248
|
+
console.log(JSON.stringify(registryPayload(root), null, 2));
|
|
326
1249
|
return 0;
|
|
327
1250
|
}
|
|
328
1251
|
printRoster(root);
|
|
@@ -338,11 +1261,23 @@ function engineCommand(args = []) {
|
|
|
338
1261
|
}
|
|
339
1262
|
|
|
340
1263
|
if (sub === 'help') {
|
|
341
|
-
console.log('\n atris engine roster + current default\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>]\n one-command claim, worktree, build, verify, ship, ready\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
1264
|
+
console.log('\n atris engine roster + current default\n atris engine list --json full registry: default + engines with tier, roles, fallback, health\n atris engine resolve <role> [--json]\n choose the best ready engine for navigator|executor|validator\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
342
1265
|
return 0;
|
|
343
1266
|
}
|
|
344
1267
|
|
|
345
|
-
// atris engine <name> — flip the default.
|
|
1268
|
+
// atris engine <name> — flip the default. An unknown name is a plain user
|
|
1269
|
+
// typo, not a crash: report it cleanly (and as JSON on demand) instead of
|
|
1270
|
+
// letting setEngine throw a raw stack trace at a new person.
|
|
1271
|
+
if (!canonicalEngineName(sub)) {
|
|
1272
|
+
const known = RUNNER_PROFILE_NAMES.join(', ');
|
|
1273
|
+
if (json) {
|
|
1274
|
+
console.log(JSON.stringify({ ok: false, error: `Unknown engine "${sub}"`, known: RUNNER_PROFILE_NAMES }, null, 2));
|
|
1275
|
+
} else {
|
|
1276
|
+
console.error(`\n Unknown engine "${sub}". known engines: ${known}`);
|
|
1277
|
+
console.error(' run "atris engine" to see the roster, or "atris engine help".\n');
|
|
1278
|
+
}
|
|
1279
|
+
return 2;
|
|
1280
|
+
}
|
|
346
1281
|
const canonical = setEngine(sub, root);
|
|
347
1282
|
const def = RUNNER_PROFILE_DEFS[canonical];
|
|
348
1283
|
const installed = binInstalled(def.bin);
|
|
@@ -362,9 +1297,35 @@ module.exports = {
|
|
|
362
1297
|
setEngine,
|
|
363
1298
|
resetEngine,
|
|
364
1299
|
roster,
|
|
1300
|
+
registryPayload,
|
|
1301
|
+
engineRegistryFile,
|
|
1302
|
+
readEngineRegistry,
|
|
1303
|
+
resolveEngineForRole,
|
|
1304
|
+
setEngineHealth,
|
|
365
1305
|
probeEngine,
|
|
366
1306
|
runEngineTest,
|
|
367
1307
|
parseDispatchArgs,
|
|
368
1308
|
runDispatchCommand,
|
|
1309
|
+
ENGINE_LOGIN_MANIFESTS,
|
|
1310
|
+
MAX_ENGINE_LOGIN_FILE_BYTES,
|
|
1311
|
+
expandHomePath,
|
|
1312
|
+
normalizeLoginProvider,
|
|
1313
|
+
normalizeEngineLoginSeat,
|
|
1314
|
+
validEngineLoginSeat,
|
|
1315
|
+
engineLoginSeatError,
|
|
1316
|
+
detectedEmailFromContent,
|
|
1317
|
+
readEngineLoginFiles,
|
|
1318
|
+
parseEngineLoginArgs,
|
|
1319
|
+
parseEngineSeedArgs,
|
|
1320
|
+
redactBackendResponse,
|
|
1321
|
+
buildEngineLoginPayload,
|
|
1322
|
+
listEngineLogins,
|
|
1323
|
+
listEngineSeats,
|
|
1324
|
+
readyCheckEngineLogin,
|
|
1325
|
+
formatEngineSeats,
|
|
1326
|
+
runEngineSeatsCommand,
|
|
1327
|
+
runEngineDeviceLoginCommand,
|
|
1328
|
+
runEngineLoginCommand,
|
|
1329
|
+
runEngineSeedCommand,
|
|
369
1330
|
HOUSE_ENGINE,
|
|
370
1331
|
};
|