atris 3.58.5 → 3.58.6
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/atris/skills/x-search/SKILL.md +2 -2
- package/atris/skills/youtube/SKILL.md +44 -28
- package/commands/auth.js +52 -24
- package/commands/learn.js +205 -39
- package/commands/workflow.js +4 -8
- package/commands/x-search.js +9 -10
- package/commands/youtube.js +444 -101
- package/package.json +1 -1
- package/scripts/det/ytnotes +82 -5
- package/utils/auth.js +47 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: x-search
|
|
3
3
|
description: "X/Twitter search via xAI Grok API. Use when user wants to search tweets, monitor topics, find viral posts, or run social listening. Costs 5 credits per search. Triggers on x search, tweet search, twitter search, social listening, revenue intel, viral tweets."
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.1
|
|
5
5
|
tags:
|
|
6
6
|
- x-search
|
|
7
7
|
- social
|
|
@@ -33,7 +33,7 @@ login
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
1. Login: stored login, then `atris login --agent` if needed. Never paste tokens. Never `/auth/cli`.
|
|
36
|
-
2. Search: `atris x-search "QUERY"`. Always billed (5 credits). Prints to stdout. Rich ephemeral prints one apply next-step, one inferred check plus `score: 0`, then one `next: atris youtube search "<query>"` (person mode uses `--name`), and writes nothing. Thin ephemeral prints `check: fill this` then that youtube-search next, and writes nothing. Empty successful pull (no tweets) prints one `next: atris youtube search " "`. Explicit refund or failed search writes nothing extra.
|
|
36
|
+
2. Search: `atris x-search "QUERY"`. Always billed (5 credits). Prints to stdout. Rich ephemeral prints one apply next-step, one inferred check plus `score: 0`, then one `next: atris youtube search "<query>"` (person mode uses `--name`), and writes nothing. Thin ephemeral prints `check: fill this` then that youtube-search next, and writes nothing. Empty successful pull (no tweets) prints one `next: atris youtube search " "`. Explicit refund or failed search writes nothing extra. Empty, 502, 401, or 402 with unused credits print Credits when present and do not say refunded unless credits_refunded is explicit. A 401 that remints and retries prints those same credit lines from the first payload before the retry. `--json` stays quiet.
|
|
37
37
|
3. `--save` only when you want a brief. Rich results (a number-with-units or a named mechanism) file the brief, mint `atris/experiments/x-search-<slug>/`, write one Apply that names that pack and the keep rule, and prove that fixture starts at score 0. Thin `--save` prints `thin: no number or named mechanism. no brief.` and writes nothing. `--save` does not print a youtube-search next-step.
|
|
38
38
|
4. Stop. `--json`, explicit refund, or failed search does not print apply, check, score, or youtube-search next-step.
|
|
39
39
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: youtube
|
|
3
|
-
description: "YouTube discovery and learning. Get watch permalinks with atris youtube search QUERY (free, local ytsearch/yt-dlp). On 429 the CLI
|
|
4
|
-
version: 2.
|
|
3
|
+
description: "YouTube discovery and learning. Get watch permalinks with atris youtube search QUERY (free, local ytsearch/yt-dlp). On 429 use printed rows if any; else the CLI retries once, then cached rows if printed, else STOP. Never run --paid after a 429. --paid only when the user explicitly asked to buy permalinks (5 credits). After a URL is picked, atris youtube notes URL (free, ephemeral unless --save). atris youtube process only to store knowledge (5 credits). Never paste tokens. Never /auth/cli. Mint with atris login --agent from a stored login. Never summarize a video from model memory. Triggers on: youtube search, find videos, paid youtube search, any youtube.com or youtu.be link, youtube, video, watch this, notes on this."
|
|
4
|
+
version: 2.18.18
|
|
5
5
|
tags:
|
|
6
6
|
- youtube
|
|
7
7
|
- research
|
|
@@ -18,12 +18,13 @@ Hard path. Follow in order. Soft memory does not override this.
|
|
|
18
18
|
```
|
|
19
19
|
search QUERY (free)
|
|
20
20
|
|
|
|
21
|
-
rows --> rich: one failing
|
|
22
|
-
--> next: atris youtube teach <first-url>
|
|
21
|
+
rows --> rich: one pack-named apply + failing measure.py (score 0), then next: experiments keep
|
|
22
|
+
--> thin: check: fill this, then next: atris youtube teach <first-url>
|
|
23
23
|
--> --json stays quiet
|
|
24
24
|
|
|
|
25
|
-
429 -->
|
|
26
|
-
-->
|
|
25
|
+
429 --> printed rows? use them (no retry)
|
|
26
|
+
--> else CLI retries once
|
|
27
|
+
--> cached rows printed? use them (same rich mint or thin check as live rows)
|
|
27
28
|
--> rate-limit sentence printed? STOP
|
|
28
29
|
do not run --paid
|
|
29
30
|
|
|
|
@@ -33,22 +34,34 @@ search QUERY (free)
|
|
|
33
34
|
| then next: atris youtube teach <same-url>
|
|
34
35
|
| --json stays quiet
|
|
35
36
|
| --save files brief + pack-named apply when notes have a number or named mechanism; a multi-url --save batch proves the first saved pack the same way single-url --save does; thin --save refuses
|
|
37
|
+
| playlist expand keeps printed yt-dlp rows on 429
|
|
38
|
+
| notes keep a written yt_<id>.md (and ytnotes keeps a written manual or auto en / en-orig / en-US / en-GB VTT) when yt-dlp exits 429 or a later error, or --print is empty
|
|
39
|
+
| watch, youtu.be, shorts, embed, live, /e/, and youtube-nocookie embed urls all resolve the same video id for that keep
|
|
40
|
+
| a copied #t= timestamp still finds yt_<id>.en.vtt
|
|
41
|
+
| a watch?v=&list= copy still finds yt_<video>.en.vtt when -J dumps playlist JSON
|
|
36
42
|
| or teach URL [--section N] (one chapter: claim numbers, named mechanisms, one check; free unless --save)
|
|
43
|
+
| printed yt-dlp metadata is a hit even on 429
|
|
44
|
+
| a written VTT or clean.txt is used when the caption URL fetch fails, `-J` stdout is empty, or `-J` dumps a playlist for a watch?v=&list= URL
|
|
37
45
|
| a taught section that is not last prints next: recap TEXT or skip
|
|
38
46
|
| last section: rich ephemeral apply, failing check, and score 0, then next: atris youtube watch tick; save pack keep stays; no recap next
|
|
39
47
|
| next --section refuses until recap/skip
|
|
40
48
|
| owed prints unpaid check; successful unlock prints next section command
|
|
41
49
|
| bare teach resumes owed, or prints a start command if nothing is owed
|
|
42
50
|
|
|
|
51
|
+
persist a rich lesson? --> learn log '{"type":"pattern","key":"...","insight":"..."}'
|
|
52
|
+
--> rich: one pack-named apply + failing measure.py (score 0), then next: experiments keep
|
|
53
|
+
--> thin: jsonl only
|
|
54
|
+
|
|
43
55
|
write one Apply (claimable) before process
|
|
44
56
|
|
|
|
45
57
|
store knowledge? --> process URL (5 credits)
|
|
46
|
-
--> rich: one failing
|
|
58
|
+
--> rich: one pack-named apply + failing measure.py (score 0), then next: experiments keep
|
|
59
|
+
--> thin: check: fill this; --json stays quiet
|
|
47
60
|
|
|
48
61
|
watch add --> next: atris youtube watch tick
|
|
49
62
|
watch tick --> briefs new videos
|
|
50
|
-
--> if briefed: rich pack-named apply + failing measure.py (score 0), then next: experiments keep
|
|
51
|
-
--> thin: check: fill this, then next: atris youtube teach <first-briefed-url>
|
|
63
|
+
--> if briefed: first rich brief mints pack-named apply + failing measure.py (score 0), then next: experiments keep
|
|
64
|
+
--> all thin: check: fill this, then next: atris youtube teach <first-briefed-url>
|
|
52
65
|
--> 0 briefed, no channels: next: atris youtube watch add <channel-url-or-@handle>
|
|
53
66
|
--> 0 briefed, channels exist: next: atris youtube search " "
|
|
54
67
|
|
|
@@ -58,18 +71,18 @@ digest --> files brief + claimable journal
|
|
|
58
71
|
|
|
59
72
|
--paid QUERY only if the user asked to buy permalinks
|
|
60
73
|
|
|
|
61
|
-
rows --> rich: one failing
|
|
62
|
-
--> next: atris youtube teach <first-url>
|
|
63
|
-
--> --json stays quiet
|
|
74
|
+
rows --> rich: one pack-named apply + failing measure.py (score 0), then next: experiments keep
|
|
75
|
+
--> thin: check: fill this, then next: atris youtube teach <first-url>
|
|
76
|
+
--> --json stays quiet and writes no pack
|
|
64
77
|
login: atris login --agent from a stored login
|
|
65
78
|
never paste tokens, never /auth/cli
|
|
66
79
|
```
|
|
67
80
|
|
|
68
|
-
1. Get watch permalinks: `atris youtube search QUERY` (free). A rich hit
|
|
69
|
-
2. If 429: wait/retry is already in the CLI. If it prints cached rows, use those. If it prints `youtube rate-limited local search. do not use --paid as a fallback; retry later.`, STOP. Do not run `--paid`.
|
|
81
|
+
1. Get watch permalinks: `atris youtube search QUERY` (free). A rich hit mints `atris/experiments/search-<query>/`, writes one pack-named Apply, and prints `score: 0` only when that Apply starts failing. A thin hit prints `check: fill this`, then one next teach command. `--json` stays quiet and writes no pack.
|
|
82
|
+
2. If 429: use any rows already printed. If none, wait/retry is already in the CLI. If it prints cached rows, use those. Cached rows mint or print the same rich or thin gate as a live hit. If it prints `youtube rate-limited local search. do not use --paid as a fallback; retry later.`, STOP. Do not run `--paid`.
|
|
70
83
|
3. `--paid` only when the user explicitly asked to buy permalinks. The CLI hard-refuses `--paid` when the free cache still has a fresh same-query hit.
|
|
71
|
-
4. `atris youtube notes URL` after a URL is picked (free). Notes is ephemeral unless `--save`. Rich ephemeral prints one apply next-step and one failing check (`score: 0`), then one `next: atris youtube teach <same-url>`, and writes no files. Thin ephemeral prints `check: fill this` instead of inventing a check. A playlist or multi-url batch does the same for the first successful item only. `--json` stays quiet on the check and the teach next-step. Rich `--save` files the brief, mints `atris/experiments/notes-<id>/`, writes one Apply, and prints `score: 0` only when that Apply starts failing. A rich multi-url `--save` batch proves that failing baseline for the first saved pack only; thin `--save` (no number-with-units and no named mechanism) refuses with no brief and exit 2. Do not auto `--paid`.
|
|
72
|
-
5. Write one Apply (change + receipt) before `atris youtube process`. Process still requires a filled Apply (so you `--save` a rich brief, fill Apply, then process).
|
|
84
|
+
4. `atris youtube notes URL` after a URL is picked (free). Notes is ephemeral unless `--save`. Rich ephemeral prints one apply next-step and one failing check (`score: 0`), then one `next: atris youtube teach <same-url>`, and writes no files. Thin ephemeral prints `check: fill this` instead of inventing a check. A playlist or multi-url batch does the same for the first successful item only. A playlist expand that prints video rows keeps them even when yt-dlp exits 429. A notes run that already wrote `yt_<id>.md` keeps that lesson even when the runner exits 429 or a later error, so the learner gate and rich `--save` mint still run. Watch, youtu.be, shorts, embed, live, /e/, and youtube-nocookie embed urls all resolve the same video id, so empty-JSON teach/process and notes keep still find that file. A copied watch?v=&list= URL still finds yt_<video>.en.vtt when -J dumps playlist JSON. A copied #t= timestamp still finds the same yt_<id> file. The bundled ytnotes script does the same for a written manual or auto en, en-orig, en-US, or en-GB VTT plus printed metadata. `--json` stays quiet on the check and the teach next-step. Rich `--save` files the brief, mints `atris/experiments/notes-<id>/`, writes one Apply, and prints `score: 0` only when that Apply starts failing. A rich multi-url `--save` batch proves that failing baseline for the first saved pack only; thin `--save` (no number-with-units and no named mechanism) refuses with no brief and exit 2. Do not auto `--paid`.
|
|
85
|
+
5. Write one Apply (change + receipt) before `atris youtube process`. Process still requires a filled Apply (so you `--save` a rich brief, fill Apply, then process). A rich analysis then mints `atris/experiments/process-<id>/`, writes one pack-named Apply, and prints `score: 0` only when that Apply starts failing. Thin analysis prints `check: fill this`. `--json` stays quiet. 401, 402, or 502 print Credits when present and say credits refunded only when the server marks a refund. A local-transcript 502 that then retries cloud prints those same credit lines from the first payload before the retry. A 401 that remints and retries prints those same credit lines from the first payload before the retry.
|
|
73
86
|
6. Never paste tokens. Never `/auth/cli`. Mint with `atris login --agent` from a stored login.
|
|
74
87
|
|
|
75
88
|
If the user says "find videos", "search youtube", or "get youtube links" → search. If they say "learn from", "notes on", "alpha", or "rabbit hole" → notes. If they say "process", "store", "add to knowledge" → process. "Buy permalinks" or "paid search" is the only ask that unlocks `--paid`.
|
|
@@ -89,7 +102,7 @@ if ! command -v atris &> /dev/null; then
|
|
|
89
102
|
npm install -g atris
|
|
90
103
|
fi
|
|
91
104
|
|
|
92
|
-
echo "Ready. Feature map: free search, 429 cache-or-stop, notes, then process."
|
|
105
|
+
echo "Ready. Feature map: free search, 429 printed-rows or cache-or-stop, notes, then process."
|
|
93
106
|
```
|
|
94
107
|
|
|
95
108
|
---
|
|
@@ -104,7 +117,7 @@ atris youtube search "MCP agents" --json
|
|
|
104
117
|
|
|
105
118
|
Uses `ytsearch` on PATH when present, else bundled `scripts/det/ytsearch`, else `yt-dlp --flat-playlist --print` with `ytsearchN:`. No credits. No `/agent/process_youtube` call. A rich hit prints one inferred check plus `score: 0`. A thin hit prints `check: fill this`. Then one next: `atris youtube teach <first-url>`. `--json` stays quiet.
|
|
106
119
|
|
|
107
|
-
On 429 the CLI retries once, then serves `~/.atris/youtube-search-cache.json` if the same query is younger than one hour. If it prints the rate-limit sentence, stop. Do not run `--paid`.
|
|
120
|
+
On 429, printed rows are a hit (no retry). If stdout is empty the CLI retries once, then serves `~/.atris/youtube-search-cache.json` if the same query is younger than one hour. A cache reprint prints the same rich or thin check as a live hit. If it prints the rate-limit sentence, stop. Do not run `--paid`.
|
|
108
121
|
|
|
109
122
|
## Paid search (5 credits, opt-in buy only)
|
|
110
123
|
|
|
@@ -115,17 +128,18 @@ atris youtube search --paid "MCP agents 2026"
|
|
|
115
128
|
atris youtube search --paid "MCP agents" --limit 10
|
|
116
129
|
```
|
|
117
130
|
|
|
118
|
-
Requires a stored login, then `atris login --agent`. The CLI mints a youtube-scope agent token from disk the same way as `atris youtube process` and `atris x-search`. Never `/auth/cli`. Never paste tokens. Prints `title | watch permalink` plus credits. A rich hit
|
|
131
|
+
Requires a stored login, then `atris login --agent`. The CLI mints a youtube-scope agent token from disk the same way as `atris youtube process` and `atris x-search`. Never `/auth/cli`. Never paste tokens. Prints `title | watch permalink` plus credits. A rich hit mints `atris/experiments/search-<query>/`, writes one pack-named Apply, and prints `score: 0` only when that Apply starts failing. A thin hit prints `check: fill this`, then one next teach command. `--json` stays quiet and writes no pack. Empty, 502, 401, or 402 with unused credits do not claim a refund unless credits_refunded is explicit.
|
|
119
132
|
|
|
120
133
|
Line contract:
|
|
121
134
|
|
|
122
135
|
```text
|
|
123
|
-
title |
|
|
124
|
-
|
|
125
|
-
next: atris
|
|
136
|
+
title | https://www.youtube.com/watch?v=ID
|
|
137
|
+
Credits: N used, M remaining
|
|
138
|
+
next: atris experiments keep search-<query>
|
|
139
|
+
score: 0
|
|
126
140
|
```
|
|
127
141
|
|
|
128
|
-
|
|
142
|
+
A thin paid hit prints `check: fill this` and `next: atris youtube teach "<first-url>"` instead of the keep next. `--json` stays machine-quiet. After the user picks a URL, run notes (free) or process (5 credits).
|
|
129
143
|
|
|
130
144
|
---
|
|
131
145
|
|
|
@@ -246,18 +260,18 @@ Two layers, never mixed. The reply the person reads is flowing prose: ideas, spe
|
|
|
246
260
|
|
|
247
261
|
`atris youtube search --paid` posts `{query, limit}` to `/youtube/search` with bearer auth. Agent tokens need the youtube scope. This is an opt-in buy, not a 429 fallback.
|
|
248
262
|
|
|
249
|
-
`atris youtube` process first tries local transcript extraction with `yt-dlp`. It sends timestamped `transcript_text` to `/agent/process_youtube` with `cache_transcript=false`. If local transcript processing fails with a retryable error, it falls back to cloud video processing. Use `--json` to inspect `metadata.processing_method` and `metadata.transcript_source`.
|
|
263
|
+
`atris youtube` process first tries local transcript extraction with `yt-dlp`. It sends timestamped `transcript_text` to `/agent/process_youtube` with `cache_transcript=false`. A written VTT or clean.txt in the notes work dir is a local hit even when `-J` stdout is empty or the caption URL fetch fails, so process does not jump to paid cloud video for that case. If local transcript processing fails with a retryable error, it falls back to cloud video processing. Use `--json` to inspect `metadata.processing_method` and `metadata.transcript_source`.
|
|
250
264
|
|
|
251
265
|
---
|
|
252
266
|
|
|
253
267
|
## Billing
|
|
254
268
|
|
|
255
269
|
- **Search: 0 credits** (local discovery)
|
|
256
|
-
- **Paid search: 5 credits** (`--paid`; refund
|
|
270
|
+
- **Paid search: 5 credits** (`--paid`; print credits refunded only when the server marks a refund. A 401 that remints and retries prints those same credit lines from the first payload before the retry. `--json` stays quiet.)
|
|
257
271
|
- **Notes: 0 credits** (local captions + engine)
|
|
258
272
|
- **Process: 5 credits per video** (flat rate, any length)
|
|
259
273
|
- Credits deducted before processing
|
|
260
|
-
-
|
|
274
|
+
- Process 200/401/402/502 print Credits when present and say credits refunded only when `credits_refunded` is explicit. A local-transcript 502 that then retries cloud prints the same lines from the first payload. A 401 that remints and retries prints the same lines from the first payload. Unused `credits_used: 0` on a 200 is not a refund.
|
|
261
275
|
- Insufficient credits returns 402 with your current balance
|
|
262
276
|
|
|
263
277
|
---
|
|
@@ -269,9 +283,11 @@ Two layers, never mixed. The reply the person reads is flowing prose: ideas, spe
|
|
|
269
283
|
| `401` | Token expired/invalid | `atris login --agent` from a stored login |
|
|
270
284
|
| `402` | Not enough credits | Check balance, purchase at atris.ai |
|
|
271
285
|
| `400` | Invalid YouTube URL | Check URL format |
|
|
272
|
-
| `502` | Transcript or cloud processing failed | Retry; credits
|
|
286
|
+
| `502` | Transcript or cloud processing failed | Retry; print credits refunded only when the server marks a refund |
|
|
273
287
|
| search exit 2 | ytsearch/yt-dlp missing or no results | Install yt-dlp, or put ytsearch on PATH |
|
|
274
|
-
| search 429 | YouTube rate-limited local search | CLI already retried; use cached rows if printed; if the rate-limit sentence prints, STOP; do not use --paid |
|
|
288
|
+
| search 429 | YouTube rate-limited local search | use printed rows if any; else CLI already retried; use cached rows if printed (same rich/thin check as a live hit); if the rate-limit sentence prints, STOP; do not use --paid |
|
|
289
|
+
| teach 429 | YouTube rate-limited local metadata | use printed yt-dlp JSON if it parses; if the caption URL fetch fails or `-J` stdout is empty or broken, use a written VTT or clean.txt from the notes work dir; no written caption still fails; do not use process as a fallback |
|
|
290
|
+
| notes 429 | YouTube rate-limited local captions | use a written yt_<id>.md if it exists; ytnotes keeps a written manual or auto en / en-orig / en-US / en-GB VTT plus printed metadata, or a VTT written in the same run when print is empty, including /e/ and youtube-nocookie embed urls; a copied #t= timestamp still finds that leftover file; empty 429 with no captions still fails; do not use --paid |
|
|
275
291
|
|
|
276
292
|
---
|
|
277
293
|
|
package/commands/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { AGENT_TOKEN_EXPIRED_DETAIL, loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
|
|
1
|
+
const { AGENT_TOKEN_EXPIRED_DETAIL, decodeJwtClaims, 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
3
|
const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
|
|
4
4
|
const { hasFlag, readFlag } = require('../lib/arg-parser');
|
|
@@ -85,20 +85,45 @@ function extractAgentTokenMeta(data, requested, token) {
|
|
|
85
85
|
return { scopes, dailyCreditCap, expiresAt };
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function isAgentAccessToken(token) {
|
|
89
|
+
return decodeJwtClaims(token)?.type === 'agent_access';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function scopedTokenCandidate(credentials = {}) {
|
|
93
|
+
if (credentials.agent_token) return credentials.agent_token;
|
|
94
|
+
if (
|
|
95
|
+
credentials.source === 'env'
|
|
96
|
+
|| credentials.source === 'agent_token_file'
|
|
97
|
+
|| isAgentAccessToken(credentials.token)
|
|
98
|
+
) {
|
|
99
|
+
return firstNonEmptyString(credentials.token);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function canMintFromLogin(credentials = {}) {
|
|
105
|
+
const login = firstNonEmptyString(credentials.token);
|
|
106
|
+
const refresh = firstNonEmptyString(credentials.refresh_token);
|
|
107
|
+
if (isAgentAccessToken(login) && !refresh) return false;
|
|
108
|
+
return Boolean(login || refresh);
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
function persistMintedAgentToken(credentials, token, extras = {}) {
|
|
112
|
+
if (isAgentAccessToken(credentials.token)) {
|
|
113
|
+
throw new Error('Refusing to save a scoped agent token as the login token; keep it under agent_token');
|
|
114
|
+
}
|
|
89
115
|
const next = {
|
|
90
|
-
|
|
116
|
+
...credentials,
|
|
91
117
|
refresh_token: extras.refresh_token || credentials.refresh_token || null,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
saved_at: extras.saved_at || new Date().toISOString(),
|
|
118
|
+
agent_token: token,
|
|
119
|
+
agent_token_scopes: extras.scopes || [],
|
|
120
|
+
agent_token_expires_at: extras.expiresAt || null,
|
|
96
121
|
};
|
|
97
122
|
if (credentials.source_profile) {
|
|
98
123
|
saveProfile(credentials.source_profile, next);
|
|
99
124
|
return next;
|
|
100
125
|
}
|
|
101
|
-
saveCredentials(next.token, next.refresh_token, next.email, next.user_id, next.provider);
|
|
126
|
+
saveCredentials(next.token, next.refresh_token, next.email, next.user_id, next.provider, next);
|
|
102
127
|
return next;
|
|
103
128
|
}
|
|
104
129
|
|
|
@@ -139,10 +164,10 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
|
|
|
139
164
|
? requested.dailyCreditCap
|
|
140
165
|
: DEFAULT_DAILY_CREDIT_CAP;
|
|
141
166
|
|
|
142
|
-
const credentials = load() || {};
|
|
167
|
+
const credentials = await load(api) || {};
|
|
143
168
|
const accessToken = firstNonEmptyString(credentials.token);
|
|
144
169
|
const refreshToken = firstNonEmptyString(credentials.refresh_token);
|
|
145
|
-
if (!
|
|
170
|
+
if (!canMintFromLogin(credentials)) {
|
|
146
171
|
return { ok: false, code: 'not_logged_in', error: NO_STORED_JWT_MESSAGE };
|
|
147
172
|
}
|
|
148
173
|
|
|
@@ -150,7 +175,7 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
|
|
|
150
175
|
scopes,
|
|
151
176
|
daily_credit_cap: dailyCreditCap,
|
|
152
177
|
};
|
|
153
|
-
let authToken = accessToken || refreshToken;
|
|
178
|
+
let authToken = isAgentAccessToken(accessToken) ? refreshToken : (accessToken || refreshToken);
|
|
154
179
|
let result = await postAgentToken(api, authToken, body);
|
|
155
180
|
if (!result.ok && result.status === 401 && refreshToken && authToken !== refreshToken) {
|
|
156
181
|
authToken = refreshToken;
|
|
@@ -172,7 +197,9 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
|
|
|
172
197
|
return { ok: false, code: 'missing_token', error: 'backend did not return an agent token' };
|
|
173
198
|
}
|
|
174
199
|
|
|
200
|
+
const meta = extractAgentTokenMeta(result.data, { scopes, dailyCreditCap }, minted);
|
|
175
201
|
persist(credentials, minted, {
|
|
202
|
+
...meta,
|
|
176
203
|
refresh_token: firstNonEmptyString(result.data && result.data.refresh_token) || refreshToken,
|
|
177
204
|
saved_at: now(),
|
|
178
205
|
});
|
|
@@ -180,7 +207,8 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
|
|
|
180
207
|
return {
|
|
181
208
|
ok: true,
|
|
182
209
|
token: minted,
|
|
183
|
-
meta
|
|
210
|
+
meta,
|
|
211
|
+
storedIn: credentials.source_profile ? `profile ${credentials.source_profile}, agent_token` : '~/.atris/credentials.json, agent_token',
|
|
184
212
|
};
|
|
185
213
|
}
|
|
186
214
|
|
|
@@ -191,24 +219,21 @@ async function ensureBilledCommandAuth(scope, deps = {}) {
|
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
const api = deps.apiRequestJson || apiRequestJson;
|
|
194
|
-
const ensureFn = deps.ensureValidCredentials || ensureValidCredentials;
|
|
195
222
|
const load = deps.loadCredentials || loadCredentials;
|
|
196
223
|
const mint = deps.mintScopedAgentToken || mintScopedAgentToken;
|
|
197
224
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
225
|
+
const ensured = !deps.forceMint && deps.ensureValidCredentials
|
|
226
|
+
? await deps.ensureValidCredentials(api) : null;
|
|
227
|
+
const credentials = ensured?.credentials || await load(api) || {};
|
|
228
|
+
const candidate = scopedTokenCandidate(credentials);
|
|
229
|
+
const claims = decodeJwtClaims(candidate);
|
|
230
|
+
const scopes = claims?.scopes || credentials.agent_token_scopes || [];
|
|
231
|
+
const expiry = claims?.exp ? claims.exp * 1000 : Date.parse(credentials.agent_token_expires_at);
|
|
232
|
+
if (!deps.forceMint && candidate && Array.isArray(scopes) && scopes.includes(wanted) && Number.isFinite(expiry) && expiry > Date.now()) {
|
|
233
|
+
return { ok: true, token: candidate, minted: false, credentials };
|
|
208
234
|
}
|
|
209
235
|
|
|
210
|
-
|
|
211
|
-
if (!firstNonEmptyString(credentials.token, credentials.refresh_token)) {
|
|
236
|
+
if (!canMintFromLogin(credentials)) {
|
|
212
237
|
return { ok: false, error: NO_STORED_JWT_MESSAGE };
|
|
213
238
|
}
|
|
214
239
|
|
|
@@ -267,6 +292,7 @@ async function mintAgentToken(args = [], deps = {}) {
|
|
|
267
292
|
output(JSON.stringify({
|
|
268
293
|
ok: true,
|
|
269
294
|
minted: true,
|
|
295
|
+
stored_in: minted.storedIn,
|
|
270
296
|
scopes: minted.meta.scopes,
|
|
271
297
|
daily_credit_cap: minted.meta.dailyCreditCap,
|
|
272
298
|
expires_at: minted.meta.expiresAt,
|
|
@@ -275,6 +301,7 @@ async function mintAgentToken(args = [], deps = {}) {
|
|
|
275
301
|
}
|
|
276
302
|
|
|
277
303
|
printAgentTokenMint(minted.meta, output);
|
|
304
|
+
output(`stored in ${minted.storedIn}`);
|
|
278
305
|
return 0;
|
|
279
306
|
}
|
|
280
307
|
|
|
@@ -982,6 +1009,7 @@ module.exports = {
|
|
|
982
1009
|
shellInit,
|
|
983
1010
|
parseAgentTokenArgs,
|
|
984
1011
|
mintAgentToken,
|
|
1012
|
+
persistMintedAgentToken,
|
|
985
1013
|
ensureBilledCommandAuth,
|
|
986
1014
|
wantsAgentToken,
|
|
987
1015
|
};
|