atris 3.53.0 → 3.56.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/FOR_AGENTS.md +7 -0
- package/README.md +1 -1
- package/atris/policies/ANTISLOP.md +2 -1
- package/atris/skills/copy-editor/SKILL.md +2 -1
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +82 -33
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/bin/atris +21 -0
- package/bin/atris.js +563 -373
- package/commands/api-key.js +170 -0
- package/commands/auth.js +453 -43
- package/commands/autoland.js +3 -1
- package/commands/autopilot-front.js +58 -11
- package/commands/avail.js +5 -4
- package/commands/balance.js +55 -0
- package/commands/bench.js +29 -1
- package/commands/brain.js +63 -12
- package/commands/brainstorm.js +18 -0
- package/commands/business.js +149 -1
- package/commands/chat-scan.js +17 -4
- package/commands/close.js +5 -1
- package/commands/doctor.js +174 -0
- package/commands/dream.js +7 -0
- package/commands/drill.js +34 -2
- package/commands/engine.js +47 -35
- package/commands/errors.js +58 -8
- package/commands/experiments.js +14 -0
- package/commands/feedback.js +61 -2
- package/commands/fleet-report.js +27 -7
- package/commands/fleet.js +98 -11
- package/commands/founder.js +60 -21
- package/commands/human-missions.js +79 -4
- package/commands/improve.js +54 -4
- package/commands/init.js +68 -92
- package/commands/install.js +40 -0
- package/commands/integrations.js +565 -30
- package/commands/interview.js +8 -0
- package/commands/land.js +48 -17
- package/commands/learn.js +29 -4
- package/commands/log.js +51 -0
- package/commands/member.js +68 -10
- package/commands/mission.js +287 -50
- package/commands/now.js +15 -3
- package/commands/orb.js +97 -2
- package/commands/pack.js +83 -9
- package/commands/playbook.js +381 -0
- package/commands/plugin.js +16 -0
- package/commands/radar.js +31 -7
- package/commands/recap.js +128 -21
- package/commands/release.js +102 -2
- package/commands/review.js +5 -10
- package/commands/revisions.js +55 -0
- package/commands/run-front.js +12 -4
- package/commands/scout.js +12 -1
- package/commands/search.js +123 -28
- package/commands/sign.js +2 -2
- package/commands/signup.js +12 -3
- package/commands/site-deploy.js +17 -4
- package/commands/site.js +2 -2
- package/commands/skill-eval.js +289 -0
- package/commands/skill.js +56 -6
- package/commands/social.js +603 -0
- package/commands/spaceship.js +69 -6
- package/commands/stream.js +8 -1
- package/commands/study.js +32 -19
- package/commands/sync.js +28 -22
- package/commands/task.js +435 -60
- package/commands/teach.js +72 -63
- package/commands/terminal.js +25 -9
- package/commands/topup.js +88 -0
- package/commands/truth.js +52 -6
- package/commands/usage.js +91 -0
- package/commands/verify.js +3 -2
- package/commands/version.js +3 -1
- package/commands/voice.js +5 -1
- package/commands/who.js +69 -11
- package/commands/wiki.js +7 -4
- package/commands/wish.js +76 -5
- package/commands/workflow.js +327 -145
- package/commands/worktree.js +1 -0
- package/commands/write.js +5 -0
- package/commands/x-search.js +340 -0
- package/commands/xp.js +5 -1
- package/commands/youtube.js +551 -25
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +8 -1
- package/lib/auto-lessons.js +186 -0
- package/lib/autoland.js +4 -2
- package/lib/chat-log-scan.js +7 -4
- package/lib/cli-json.js +77 -0
- package/lib/cli-scope.js +26 -0
- package/lib/codex-flight.js +1 -0
- package/lib/conductor-artifacts.js +1 -1
- package/lib/context-gatherer.js +11 -0
- package/lib/developer-api.js +116 -0
- package/lib/feedback-triage.js +274 -0
- package/lib/first-minute.js +398 -0
- package/lib/fleet.js +115 -34
- package/lib/functional-owner.js +22 -0
- package/lib/known-commands.js +31 -5
- package/lib/member-scaffold.js +197 -0
- package/lib/mission-root.js +4 -2
- package/lib/noninteractive.js +88 -0
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +4 -0
- package/lib/task-proof.js +69 -8
- package/lib/verifier-quality.js +69 -0
- package/lib/wish-audit.js +2 -2
- package/lib/wish-delegate.js +10 -1
- package/lib/workspace-scaffold.js +270 -0
- package/package.json +3 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/utils/update-check.js +15 -0
package/commands/auth.js
CHANGED
|
@@ -1,22 +1,361 @@
|
|
|
1
|
-
const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
|
|
1
|
+
const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
|
|
2
2
|
const { getAppBaseUrl, apiRequestJson } = require('../utils/api');
|
|
3
|
+
const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
|
|
4
|
+
const { hasFlag, readFlag } = require('../lib/arg-parser');
|
|
3
5
|
const fs = require('fs');
|
|
4
6
|
const path = require('path');
|
|
5
7
|
|
|
8
|
+
const AGENT_TOKEN_PATH = '/auth/agent-token';
|
|
9
|
+
const DEFAULT_AGENT_SCOPES = ['x-search', 'youtube'];
|
|
10
|
+
const DEFAULT_DAILY_CREDIT_CAP = 50;
|
|
11
|
+
const NO_STORED_JWT_MESSAGE = 'not signed in. run atris login first.';
|
|
12
|
+
|
|
13
|
+
function firstNonEmptyString(...values) {
|
|
14
|
+
for (const value of values) {
|
|
15
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function wantsAgentToken(args = []) {
|
|
21
|
+
if (hasFlag(args, '--agent')) return true;
|
|
22
|
+
const first = args.find((arg) => arg && !String(arg).startsWith('-'));
|
|
23
|
+
return first === 'agent-token';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseScopeList(raw) {
|
|
27
|
+
if (!raw) return [...DEFAULT_AGENT_SCOPES];
|
|
28
|
+
const scopes = String(raw).split(',').map((part) => part.trim()).filter(Boolean);
|
|
29
|
+
if (scopes.length === 0) {
|
|
30
|
+
throw new Error('scopes must list at least one value');
|
|
31
|
+
}
|
|
32
|
+
return scopes;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseDailyCreditCap(args) {
|
|
36
|
+
const present = args.some((arg) => arg === '--daily-credit-cap' || String(arg).startsWith('--daily-credit-cap='));
|
|
37
|
+
const raw = readFlag(args, '--daily-credit-cap', '');
|
|
38
|
+
if (!present) return DEFAULT_DAILY_CREDIT_CAP;
|
|
39
|
+
const value = Number.parseInt(raw, 10);
|
|
40
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
41
|
+
throw new Error('daily credit cap must be a positive integer');
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseAgentTokenArgs(args = []) {
|
|
47
|
+
return {
|
|
48
|
+
agent: wantsAgentToken(args),
|
|
49
|
+
scopes: parseScopeList(readFlag(args, '--scopes', '')),
|
|
50
|
+
dailyCreditCap: parseDailyCreditCap(args),
|
|
51
|
+
json: wantsJson(args),
|
|
52
|
+
help: hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractAgentAccessToken(data) {
|
|
57
|
+
if (!data || typeof data !== 'object') return null;
|
|
58
|
+
return firstNonEmptyString(
|
|
59
|
+
data.access_token,
|
|
60
|
+
data.token,
|
|
61
|
+
data.agent_access,
|
|
62
|
+
data.agent_token,
|
|
63
|
+
data.agent_access_token,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function expiryFromValue(value) {
|
|
68
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
69
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
70
|
+
const ms = value > 1e12 ? value : value * 1000;
|
|
71
|
+
return new Date(ms).toISOString();
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractAgentTokenMeta(data, requested, token) {
|
|
77
|
+
const payload = data && typeof data === 'object' ? data : {};
|
|
78
|
+
const scopes = Array.isArray(payload.scopes) && payload.scopes.length
|
|
79
|
+
? payload.scopes.map(String)
|
|
80
|
+
: requested.scopes;
|
|
81
|
+
const rawCap = payload.daily_credit_cap;
|
|
82
|
+
const dailyCreditCap = Number.isFinite(Number(rawCap)) ? Number(rawCap) : requested.dailyCreditCap;
|
|
83
|
+
const expiresAt = expiryFromValue(payload.expires_at ?? payload.expiry ?? payload.expires ?? payload.exp)
|
|
84
|
+
|| expiryFromValue(getTokenExpiryEpochSeconds(token));
|
|
85
|
+
return { scopes, dailyCreditCap, expiresAt };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function persistMintedAgentToken(credentials, token, extras = {}) {
|
|
89
|
+
const next = {
|
|
90
|
+
token,
|
|
91
|
+
refresh_token: extras.refresh_token || credentials.refresh_token || null,
|
|
92
|
+
email: credentials.email || null,
|
|
93
|
+
user_id: credentials.user_id || null,
|
|
94
|
+
provider: credentials.provider || null,
|
|
95
|
+
saved_at: extras.saved_at || new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
if (credentials.source_profile) {
|
|
98
|
+
saveProfile(credentials.source_profile, next);
|
|
99
|
+
return next;
|
|
100
|
+
}
|
|
101
|
+
saveCredentials(next.token, next.refresh_token, next.email, next.user_id, next.provider);
|
|
102
|
+
return next;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function printAgentTokenMint(meta, output) {
|
|
106
|
+
output('minted scoped agent token');
|
|
107
|
+
output(`scopes: ${meta.scopes.join(', ')}`);
|
|
108
|
+
output(`daily credit cap: ${meta.dailyCreditCap}`);
|
|
109
|
+
if (meta.expiresAt) output(`expires: ${meta.expiresAt}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function redactSecret(text, secret) {
|
|
113
|
+
if (!secret || !text) return text;
|
|
114
|
+
return String(text).split(secret).join('[redacted]');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function postAgentToken(api, token, body) {
|
|
118
|
+
return api(AGENT_TOKEN_PATH, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
token,
|
|
121
|
+
body,
|
|
122
|
+
retries: 0,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function mintScopedAgentToken(requested = {}, deps = {}) {
|
|
127
|
+
const api = deps.apiRequestJson || apiRequestJson;
|
|
128
|
+
const load = deps.loadCredentials || loadCredentials;
|
|
129
|
+
const persist = deps.persistMintedAgentToken || persistMintedAgentToken;
|
|
130
|
+
const now = deps.now || (() => new Date().toISOString());
|
|
131
|
+
|
|
132
|
+
const scopes = Array.isArray(requested.scopes)
|
|
133
|
+
? requested.scopes.map((part) => String(part).trim()).filter(Boolean)
|
|
134
|
+
: parseScopeList(requested.scopes);
|
|
135
|
+
if (scopes.length === 0) {
|
|
136
|
+
return { ok: false, code: 'invalid_scopes', error: 'scopes must list at least one value' };
|
|
137
|
+
}
|
|
138
|
+
const dailyCreditCap = Number.isInteger(requested.dailyCreditCap) && requested.dailyCreditCap > 0
|
|
139
|
+
? requested.dailyCreditCap
|
|
140
|
+
: DEFAULT_DAILY_CREDIT_CAP;
|
|
141
|
+
|
|
142
|
+
const credentials = load() || {};
|
|
143
|
+
const accessToken = firstNonEmptyString(credentials.token);
|
|
144
|
+
const refreshToken = firstNonEmptyString(credentials.refresh_token);
|
|
145
|
+
if (!accessToken && !refreshToken) {
|
|
146
|
+
return { ok: false, code: 'not_logged_in', error: NO_STORED_JWT_MESSAGE };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const body = {
|
|
150
|
+
scopes,
|
|
151
|
+
daily_credit_cap: dailyCreditCap,
|
|
152
|
+
};
|
|
153
|
+
let authToken = accessToken || refreshToken;
|
|
154
|
+
let result = await postAgentToken(api, authToken, body);
|
|
155
|
+
if (!result.ok && result.status === 401 && refreshToken && authToken !== refreshToken) {
|
|
156
|
+
authToken = refreshToken;
|
|
157
|
+
result = await postAgentToken(api, authToken, body);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!result.ok) {
|
|
161
|
+
const detail = redactSecret(result.error || 'agent token request failed', accessToken);
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
code: 'mint_failed',
|
|
165
|
+
status: result.status || 0,
|
|
166
|
+
error: redactSecret(detail, refreshToken),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const minted = extractAgentAccessToken(result.data);
|
|
171
|
+
if (!minted) {
|
|
172
|
+
return { ok: false, code: 'missing_token', error: 'backend did not return an agent token' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
persist(credentials, minted, {
|
|
176
|
+
refresh_token: firstNonEmptyString(result.data && result.data.refresh_token) || refreshToken,
|
|
177
|
+
saved_at: now(),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
token: minted,
|
|
183
|
+
meta: extractAgentTokenMeta(result.data, { scopes, dailyCreditCap }, minted),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function ensureBilledCommandAuth(scope, deps = {}) {
|
|
188
|
+
const wanted = String(scope || '').trim();
|
|
189
|
+
if (!wanted) {
|
|
190
|
+
return { ok: false, error: NO_STORED_JWT_MESSAGE };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const api = deps.apiRequestJson || apiRequestJson;
|
|
194
|
+
const ensureFn = deps.ensureValidCredentials || ensureValidCredentials;
|
|
195
|
+
const load = deps.loadCredentials || loadCredentials;
|
|
196
|
+
const mint = deps.mintScopedAgentToken || mintScopedAgentToken;
|
|
197
|
+
|
|
198
|
+
if (!deps.forceMint) {
|
|
199
|
+
const ensured = await ensureFn(api);
|
|
200
|
+
if (ensured && ensured.credentials && ensured.credentials.token) {
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
token: ensured.credentials.token,
|
|
204
|
+
minted: false,
|
|
205
|
+
credentials: ensured.credentials,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const credentials = load() || {};
|
|
211
|
+
if (!firstNonEmptyString(credentials.token, credentials.refresh_token)) {
|
|
212
|
+
return { ok: false, error: NO_STORED_JWT_MESSAGE };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const minted = await mint({ scopes: [wanted] }, deps);
|
|
216
|
+
if (!minted.ok) {
|
|
217
|
+
return { ok: false, error: minted.error || NO_STORED_JWT_MESSAGE };
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
token: minted.token,
|
|
222
|
+
minted: true,
|
|
223
|
+
credentials: load() || credentials,
|
|
224
|
+
meta: minted.meta,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function mintAgentToken(args = [], deps = {}) {
|
|
229
|
+
const output = deps.output || console.log;
|
|
230
|
+
const outputError = deps.outputError || console.error;
|
|
231
|
+
|
|
232
|
+
let options;
|
|
233
|
+
try {
|
|
234
|
+
options = parseAgentTokenArgs(args);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const message = error.message || String(error);
|
|
237
|
+
if (wantsJson(args)) {
|
|
238
|
+
output(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
239
|
+
} else {
|
|
240
|
+
outputError(message);
|
|
241
|
+
}
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const minted = await mintScopedAgentToken({
|
|
246
|
+
scopes: options.scopes,
|
|
247
|
+
dailyCreditCap: options.dailyCreditCap,
|
|
248
|
+
}, deps);
|
|
249
|
+
|
|
250
|
+
if (!minted.ok) {
|
|
251
|
+
if (options.json) {
|
|
252
|
+
output(JSON.stringify({
|
|
253
|
+
ok: false,
|
|
254
|
+
error: minted.code === 'not_logged_in' ? 'not_logged_in' : minted.error,
|
|
255
|
+
next: minted.code === 'not_logged_in' ? 'atris login' : undefined,
|
|
256
|
+
status: minted.status || undefined,
|
|
257
|
+
}, null, 2));
|
|
258
|
+
} else if (minted.code === 'not_logged_in') {
|
|
259
|
+
output('not signed in. run atris login first, then atris login --agent.');
|
|
260
|
+
} else {
|
|
261
|
+
outputError(minted.error);
|
|
262
|
+
}
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (options.json) {
|
|
267
|
+
output(JSON.stringify({
|
|
268
|
+
ok: true,
|
|
269
|
+
minted: true,
|
|
270
|
+
scopes: minted.meta.scopes,
|
|
271
|
+
daily_credit_cap: minted.meta.dailyCreditCap,
|
|
272
|
+
expires_at: minted.meta.expiresAt,
|
|
273
|
+
}, null, 2));
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
printAgentTokenMint(minted.meta, output);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function printWhoamiPayload(asJson) {
|
|
282
|
+
const ensured = await ensureValidCredentials(apiRequestJson);
|
|
283
|
+
if (ensured.error) {
|
|
284
|
+
if (asJson) {
|
|
285
|
+
console.log(JSON.stringify({
|
|
286
|
+
ok: false,
|
|
287
|
+
logged_in: false,
|
|
288
|
+
error: ensured.error,
|
|
289
|
+
detail: ensured.detail || null,
|
|
290
|
+
}, null, 2));
|
|
291
|
+
} else {
|
|
292
|
+
console.log('Status: Not logged in');
|
|
293
|
+
if (ensured.detail) console.log(`Reason: ${ensured.detail}`);
|
|
294
|
+
console.log('\nRun "atris login" to sign in.');
|
|
295
|
+
}
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const { credentials, user } = ensured;
|
|
300
|
+
const email = user?.email || credentials?.email || 'unknown';
|
|
301
|
+
const userId = user?.id || credentials?.user_id || 'unknown';
|
|
302
|
+
const provider = user?.provider || credentials?.provider || 'unknown';
|
|
303
|
+
const savedAt = credentials?.saved_at || 'unknown';
|
|
304
|
+
const payload = {
|
|
305
|
+
ok: true,
|
|
306
|
+
logged_in: true,
|
|
307
|
+
email,
|
|
308
|
+
user_id: userId,
|
|
309
|
+
provider,
|
|
310
|
+
credentials_saved: savedAt,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (asJson) {
|
|
314
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
315
|
+
process.exit(0);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const summary = await displayAccountSummary(apiRequestJson);
|
|
319
|
+
if (summary.error) {
|
|
320
|
+
console.log('\nRun "atris login" to sign in.');
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
|
|
6
326
|
async function loginAtris(options = {}) {
|
|
7
327
|
// Support: atris login --token <token> --force
|
|
8
328
|
const args = process.argv.slice(3);
|
|
9
329
|
const forceFlag = args.includes('--force') || args.includes('-f') || options.force;
|
|
10
330
|
const tokenIndex = args.indexOf('--token');
|
|
11
331
|
const directToken = tokenIndex !== -1 ? args[tokenIndex + 1] : options.token;
|
|
332
|
+
const asJson = wantsJson(args);
|
|
333
|
+
const nonInteractive = isNonInteractive(args);
|
|
12
334
|
|
|
13
335
|
try {
|
|
14
336
|
const existing = loadCredentials();
|
|
15
337
|
|
|
338
|
+
if (wantsAgentToken(args)) {
|
|
339
|
+
if (directToken) {
|
|
340
|
+
saveCredentials(
|
|
341
|
+
String(directToken).trim(),
|
|
342
|
+
existing?.refresh_token || null,
|
|
343
|
+
existing?.email || null,
|
|
344
|
+
existing?.user_id || null,
|
|
345
|
+
existing?.provider || 'manual',
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const code = await mintAgentToken(args);
|
|
349
|
+
process.exit(code);
|
|
350
|
+
}
|
|
351
|
+
|
|
16
352
|
// Direct token mode (non-interactive)
|
|
17
353
|
if (directToken) {
|
|
18
354
|
const trimmed = directToken.trim();
|
|
19
355
|
saveCredentials(trimmed, null, existing?.email || null, existing?.user_id || null, existing?.provider || 'manual');
|
|
356
|
+
if (asJson) {
|
|
357
|
+
return printWhoamiPayload(true);
|
|
358
|
+
}
|
|
20
359
|
console.log('Token saved. Validating…\n');
|
|
21
360
|
const summary = await displayAccountSummary(apiRequestJson);
|
|
22
361
|
if (summary.error) {
|
|
@@ -27,24 +366,48 @@ async function loginAtris(options = {}) {
|
|
|
27
366
|
process.exit(0);
|
|
28
367
|
}
|
|
29
368
|
|
|
369
|
+
// Already signed in without --force: behave like whoami (no menu).
|
|
370
|
+
// Print local identity without waiting on the network so headless agents
|
|
371
|
+
// never hang on a stale token validation.
|
|
30
372
|
if (existing && !forceFlag) {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
console.log(
|
|
373
|
+
const email = existing.email || null;
|
|
374
|
+
const userId = existing.user_id || null;
|
|
375
|
+
const provider = existing.provider || null;
|
|
376
|
+
if (asJson) {
|
|
377
|
+
console.log(JSON.stringify({
|
|
378
|
+
ok: true,
|
|
379
|
+
logged_in: true,
|
|
380
|
+
email,
|
|
381
|
+
user_id: userId,
|
|
382
|
+
provider,
|
|
383
|
+
credentials_saved: existing.saved_at || null,
|
|
384
|
+
next: 'atris whoami --json',
|
|
385
|
+
}, null, 2));
|
|
386
|
+
process.exit(0);
|
|
36
387
|
}
|
|
37
|
-
console.log(
|
|
38
|
-
console.log(
|
|
39
|
-
console.log('
|
|
388
|
+
console.log(`Currently signed in as: ${email || userId || 'unknown'}`);
|
|
389
|
+
if (provider) console.log(`Provider: ${provider}`);
|
|
390
|
+
console.log('Next: atris whoami');
|
|
391
|
+
process.exit(0);
|
|
392
|
+
}
|
|
40
393
|
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
console.log(
|
|
44
|
-
|
|
394
|
+
if (nonInteractive) {
|
|
395
|
+
if (asJson) {
|
|
396
|
+
console.log(JSON.stringify({
|
|
397
|
+
ok: false,
|
|
398
|
+
logged_in: false,
|
|
399
|
+
error: 'login requires --token in non-interactive mode',
|
|
400
|
+
next: 'atris login --token <token>',
|
|
401
|
+
}, null, 2));
|
|
402
|
+
} else {
|
|
403
|
+
console.log('Not signed in.');
|
|
404
|
+
console.log('login needs a terminal for browser OAuth, or pass a token.');
|
|
405
|
+
console.log('Next: atris login --token <token>');
|
|
45
406
|
}
|
|
46
|
-
|
|
47
|
-
}
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!existing) {
|
|
48
411
|
console.log('Welcome to Atris! Let\'s get you signed in.\n');
|
|
49
412
|
}
|
|
50
413
|
|
|
@@ -154,17 +517,21 @@ function logoutAtris() {
|
|
|
154
517
|
}
|
|
155
518
|
|
|
156
519
|
async function whoamiAtris() {
|
|
157
|
-
const
|
|
520
|
+
const args = process.argv.slice(3);
|
|
521
|
+
const asJson = wantsJson(args);
|
|
158
522
|
|
|
159
523
|
try {
|
|
160
|
-
|
|
161
|
-
if (summary.error) {
|
|
162
|
-
console.log('\nRun "atris login" to sign in.');
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
process.exit(0);
|
|
524
|
+
return await printWhoamiPayload(asJson);
|
|
166
525
|
} catch (error) {
|
|
167
|
-
|
|
526
|
+
if (asJson) {
|
|
527
|
+
console.log(JSON.stringify({
|
|
528
|
+
ok: false,
|
|
529
|
+
logged_in: false,
|
|
530
|
+
error: error.message || String(error),
|
|
531
|
+
}, null, 2));
|
|
532
|
+
} else {
|
|
533
|
+
console.error(`Failed to fetch account: ${error.message || error}`);
|
|
534
|
+
}
|
|
168
535
|
process.exit(1);
|
|
169
536
|
}
|
|
170
537
|
}
|
|
@@ -318,16 +685,18 @@ function useAccount() {
|
|
|
318
685
|
}
|
|
319
686
|
}
|
|
320
687
|
|
|
321
|
-
async function accountsCmd() {
|
|
322
|
-
const args = process.argv.slice(3);
|
|
323
|
-
const
|
|
688
|
+
async function accountsCmd(argv = null) {
|
|
689
|
+
const args = Array.isArray(argv) ? argv : process.argv.slice(3);
|
|
690
|
+
const { parseScopeFlag } = require('../lib/cli-scope');
|
|
691
|
+
const scope = parseScopeFlag(args);
|
|
692
|
+
const subCmd = scope.args[0];
|
|
324
693
|
|
|
325
694
|
if (subCmd === 'add' || subCmd === 'login') {
|
|
326
695
|
return loginAtris({ force: true });
|
|
327
696
|
}
|
|
328
697
|
|
|
329
698
|
if (subCmd === 'remove' || subCmd === 'rm') {
|
|
330
|
-
const target = args[1];
|
|
699
|
+
const target = scope.args[1];
|
|
331
700
|
if (target === '--all') {
|
|
332
701
|
const profiles = listProfiles();
|
|
333
702
|
if (profiles.length === 0) {
|
|
@@ -386,34 +755,59 @@ async function accountsCmd() {
|
|
|
386
755
|
process.exit(0);
|
|
387
756
|
}
|
|
388
757
|
|
|
389
|
-
// Default: list accounts
|
|
390
|
-
return listAccountsCmd();
|
|
758
|
+
// Default: list accounts (workspace = active only; --global = all profiles)
|
|
759
|
+
return listAccountsCmd({ global: scope.global, json: scope.args.includes('--json') || args.includes('--json') });
|
|
391
760
|
}
|
|
392
761
|
|
|
393
|
-
function listAccountsCmd() {
|
|
762
|
+
function listAccountsCmd(options = {}) {
|
|
394
763
|
const profiles = listProfiles();
|
|
395
|
-
if (profiles.length === 0) {
|
|
396
|
-
console.log('No accounts saved. Run "atris login" to add one.');
|
|
397
|
-
process.exit(0);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
764
|
const current = loadCredentials();
|
|
401
765
|
const currentUid = current?.user_id;
|
|
402
|
-
// Also check which profile name is active (env var or session)
|
|
403
766
|
const envProfile = process.env.ATRIS_PROFILE;
|
|
404
767
|
const sessionProfile = getSessionProfile();
|
|
768
|
+
const scopeKind = options.global ? 'global' : 'workspace';
|
|
405
769
|
|
|
406
|
-
|
|
407
|
-
profiles.forEach(name => {
|
|
770
|
+
const rows = profiles.map((name) => {
|
|
408
771
|
const profile = loadProfile(name);
|
|
409
772
|
const email = profile?.email || 'unknown';
|
|
410
|
-
const isActive = profile?.user_id === currentUid
|
|
411
|
-
|
|
412
|
-
|
|
773
|
+
const isActive = profile?.user_id === currentUid
|
|
774
|
+
|| name === envProfile
|
|
775
|
+
|| name === sessionProfile;
|
|
776
|
+
return { name, email, active: Boolean(isActive) };
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
const visible = options.global ? rows : rows.filter((row) => row.active);
|
|
780
|
+
|
|
781
|
+
if (options.json) {
|
|
782
|
+
console.log(JSON.stringify({
|
|
783
|
+
scope: scopeKind,
|
|
784
|
+
accounts: visible,
|
|
785
|
+
total_profiles: profiles.length,
|
|
786
|
+
}, null, 2));
|
|
787
|
+
process.exit(0);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (visible.length === 0) {
|
|
791
|
+
if (profiles.length === 0) {
|
|
792
|
+
console.log('No accounts saved. Run "atris login" to add one.');
|
|
413
793
|
} else {
|
|
414
|
-
console.log(
|
|
794
|
+
console.log('No active account in this workspace.');
|
|
795
|
+
console.log('Pass --global to list all saved profiles, or atris switch <name>.');
|
|
796
|
+
}
|
|
797
|
+
process.exit(0);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
console.log(`\n Accounts (${scopeKind})\n`);
|
|
801
|
+
visible.forEach((row) => {
|
|
802
|
+
if (row.active) {
|
|
803
|
+
console.log(` ● ${row.name} ${row.email}`);
|
|
804
|
+
} else {
|
|
805
|
+
console.log(` ${row.name} ${row.email}`);
|
|
415
806
|
}
|
|
416
807
|
});
|
|
808
|
+
if (!options.global && profiles.length > visible.length) {
|
|
809
|
+
console.log(`\n ${profiles.length - visible.length} more profile(s) hidden. Pass --global to list them.`);
|
|
810
|
+
}
|
|
417
811
|
console.log(`\n Switch: atris switch <name>`);
|
|
418
812
|
console.log(` Add: atris accounts add`);
|
|
419
813
|
console.log(` Remove: atris accounts remove <name>\n`);
|
|
@@ -569,4 +963,20 @@ function shellInit() {
|
|
|
569
963
|
console.log(lines.join('\n'));
|
|
570
964
|
}
|
|
571
965
|
|
|
572
|
-
module.exports = {
|
|
966
|
+
module.exports = {
|
|
967
|
+
loginAtris,
|
|
968
|
+
logoutAtris,
|
|
969
|
+
whoamiAtris,
|
|
970
|
+
switchAccount,
|
|
971
|
+
useAccount,
|
|
972
|
+
accountsCmd,
|
|
973
|
+
resolveProfile,
|
|
974
|
+
profileEmail,
|
|
975
|
+
activateGlobal,
|
|
976
|
+
switchSession,
|
|
977
|
+
shellInit,
|
|
978
|
+
parseAgentTokenArgs,
|
|
979
|
+
mintAgentToken,
|
|
980
|
+
ensureBilledCommandAuth,
|
|
981
|
+
wantsAgentToken,
|
|
982
|
+
};
|
package/commands/autoland.js
CHANGED
|
@@ -282,8 +282,10 @@ function verifyClosedTaskMissions(root, tasks, { limit = MAX_MISSION_AUTO_VERIFY
|
|
|
282
282
|
'tick',
|
|
283
283
|
mission.id,
|
|
284
284
|
'--verify',
|
|
285
|
+
'--yes',
|
|
286
|
+
'--full',
|
|
285
287
|
'--summary',
|
|
286
|
-
'
|
|
288
|
+
'The linked repair tasks are closed, so the mission check ran again.',
|
|
287
289
|
'--json',
|
|
288
290
|
]);
|
|
289
291
|
const outcome = parseMissionVerifyResult(mission.id, result);
|
|
@@ -8,6 +8,7 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { spawn } = require('child_process');
|
|
10
10
|
const { pickRunnableMission, runBudgetSeconds } = require('./run-front');
|
|
11
|
+
const { refuseHeadlessUnless, wantsJson, hasYesFlag } = require('../lib/noninteractive');
|
|
11
12
|
|
|
12
13
|
const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
13
14
|
const DEFAULT_LEG_WALL_SECONDS = 3600;
|
|
@@ -120,19 +121,40 @@ function legPlan(root, legWallSeconds) {
|
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
function
|
|
124
|
+
function watchStopFile(root, child, { intervalMs = 2000 } = {}) {
|
|
125
|
+
const poll = setInterval(() => {
|
|
126
|
+
if (stopRequested(root)) {
|
|
127
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
128
|
+
}
|
|
129
|
+
}, intervalMs);
|
|
130
|
+
if (poll.unref) poll.unref();
|
|
131
|
+
return () => clearInterval(poll);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function recordChildPid(root, state, pid) {
|
|
135
|
+
if (!state) return;
|
|
136
|
+
if (Number.isFinite(pid) && pid > 0) state.child_pid = pid;
|
|
137
|
+
else delete state.child_pid;
|
|
138
|
+
writeState(root, state);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function killPid(pid) {
|
|
142
|
+
if (!pidAlive(pid)) return false;
|
|
143
|
+
try { process.kill(pid, 'SIGTERM'); return true; } catch { return false; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function driveLeg(root, legArgs, current, state) {
|
|
124
147
|
return new Promise((resolve) => {
|
|
125
148
|
const child = spawn(process.execPath, [CLI_PATH, ...legArgs], { cwd: root, stdio: 'inherit' });
|
|
126
149
|
current.child = child;
|
|
150
|
+
recordChildPid(root, state, child.pid);
|
|
127
151
|
// Stop fast: a stop file written by `atris autopilot stop` in another
|
|
128
152
|
// terminal terminates the running leg, not just the loop between legs.
|
|
129
|
-
const
|
|
130
|
-
if (stopRequested(root)) { try { child.kill('SIGTERM'); } catch {} }
|
|
131
|
-
}, 2000);
|
|
132
|
-
if (poll.unref) poll.unref();
|
|
153
|
+
const stopWatch = watchStopFile(root, child);
|
|
133
154
|
const finish = (code) => {
|
|
134
|
-
|
|
155
|
+
stopWatch();
|
|
135
156
|
current.child = null;
|
|
157
|
+
recordChildPid(root, state, null);
|
|
136
158
|
resolve(Number.isFinite(code) ? code : 1);
|
|
137
159
|
};
|
|
138
160
|
child.on('error', () => finish(1));
|
|
@@ -144,10 +166,14 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
|
144
166
|
|
|
145
167
|
function autopilotStop(root = process.cwd()) {
|
|
146
168
|
requestStop(root);
|
|
147
|
-
const state = readState(root);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
const state = readState(root) || {};
|
|
170
|
+
const wrapperPid = Number(state.pid);
|
|
171
|
+
const childPid = Number(state.child_pid);
|
|
172
|
+
const killedChild = killPid(childPid);
|
|
173
|
+
const killedWrapper = wrapperPid !== childPid && killPid(wrapperPid);
|
|
174
|
+
if (killedChild || killedWrapper) {
|
|
175
|
+
const extra = killedChild && Number.isFinite(childPid) ? `, leg ${childPid}` : '';
|
|
176
|
+
console.log(`Stopping autopilot (pid ${Number.isFinite(wrapperPid) ? wrapperPid : childPid}${extra}).`);
|
|
151
177
|
} else {
|
|
152
178
|
console.log('No live autopilot process; stop marker written so the next start clears it.');
|
|
153
179
|
}
|
|
@@ -194,6 +220,26 @@ async function autopilotFront(args = []) {
|
|
|
194
220
|
if (args[0] === 'stop') return autopilotStop(root);
|
|
195
221
|
if (args[0] === 'status') return autopilotStatus(root);
|
|
196
222
|
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') { showFrontHelp(); return 0; }
|
|
223
|
+
if (wantsJson(args) && !hasYesFlag(args)) {
|
|
224
|
+
const state = readState(root);
|
|
225
|
+
const alive = Boolean(state && pidAlive(state.pid));
|
|
226
|
+
console.log(JSON.stringify({
|
|
227
|
+
ok: false,
|
|
228
|
+
command: 'autopilot',
|
|
229
|
+
running: alive,
|
|
230
|
+
error: 'pass --yes to start',
|
|
231
|
+
usage: 'atris autopilot [--yes|--auto] [--once]',
|
|
232
|
+
}, null, 2));
|
|
233
|
+
return 2;
|
|
234
|
+
}
|
|
235
|
+
// Bare headless invoke used to start a mission loop and hang. --auto is
|
|
236
|
+
// the existing proceed flag pulse and spaceship already pass. --once is
|
|
237
|
+
// a duration, not consent: it still needs --yes off a TTY.
|
|
238
|
+
if (!args.includes('--auto') && refuseHeadlessUnless(args, {
|
|
239
|
+
allowYes: true,
|
|
240
|
+
allowOnce: false,
|
|
241
|
+
usage: 'Usage: atris autopilot [--yes|--auto] [--once]',
|
|
242
|
+
})) return 2;
|
|
197
243
|
|
|
198
244
|
const existing = readState(root);
|
|
199
245
|
if (existing && pidAlive(existing.pid) && existing.pid !== process.pid) {
|
|
@@ -245,7 +291,7 @@ async function autopilotFront(args = []) {
|
|
|
245
291
|
console.log(`\nautopilot leg ${state.legs}: ${plan.label}`);
|
|
246
292
|
|
|
247
293
|
const legStarted = Date.now();
|
|
248
|
-
const code = await driveLeg(root, plan.args, current);
|
|
294
|
+
const code = await driveLeg(root, plan.args, current, state);
|
|
249
295
|
state.last_exit = code;
|
|
250
296
|
writeState(root, state);
|
|
251
297
|
|
|
@@ -284,4 +330,5 @@ module.exports = {
|
|
|
284
330
|
clearStop,
|
|
285
331
|
readState,
|
|
286
332
|
writeState,
|
|
333
|
+
watchStopFile,
|
|
287
334
|
};
|