@trawlme/cli 3.7.5 → 3.8.1
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/README.md +16 -6
- package/dist/commands/login.js +19 -1
- package/dist/commands/scraps.js +90 -7
- package/dist/commands/spec.d.ts +92 -0
- package/dist/commands/spec.js +83 -0
- package/dist/commands/token.js +34 -7
- package/dist/commands/upgrade.js +27 -3
- package/dist/commands/whoami.js +11 -1
- package/dist/index.d.ts +52 -0
- package/dist/index.js +85 -4
- package/dist/lib/api.d.ts +36 -2
- package/dist/lib/api.js +184 -13
- package/dist/lib/config.d.ts +40 -8
- package/dist/lib/config.js +51 -8
- package/dist/lib/errors.d.ts +113 -2
- package/dist/lib/errors.js +178 -12
- package/docs/agent-quickstart.md +88 -11
- package/package.json +2 -1
package/dist/lib/errors.d.ts
CHANGED
|
@@ -37,11 +37,122 @@ export interface ErrorEnvelope {
|
|
|
37
37
|
message: string;
|
|
38
38
|
status?: number;
|
|
39
39
|
kind: string;
|
|
40
|
+
/** Is retrying the SAME command, unchanged, worth it? */
|
|
41
|
+
retryable: boolean;
|
|
42
|
+
/** Commands worth running next, most useful first. Empty when there is
|
|
43
|
+
* nothing honest to suggest — never filled to look helpful. */
|
|
44
|
+
next?: string[];
|
|
40
45
|
}
|
|
41
46
|
export interface ClassifiedError {
|
|
42
47
|
exitCode: number;
|
|
43
48
|
envelope: ErrorEnvelope;
|
|
44
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The exit-code taxonomy (#71 findings 13/14/60), named once so
|
|
52
|
+
* `classifyError` below and `spec.ts`'s `buildSpec()` (#170) read the SAME
|
|
53
|
+
* numbers instead of `spec.ts` hand-listing its own copy — exactly the kind
|
|
54
|
+
* of second source of truth `tests/contracts/` exists to catch drifting.
|
|
55
|
+
*/
|
|
56
|
+
export declare const EXIT_CODES: Readonly<{
|
|
57
|
+
SUCCESS: 0;
|
|
58
|
+
UNKNOWN: 1;
|
|
59
|
+
USAGE: 2;
|
|
60
|
+
AUTH: 3;
|
|
61
|
+
NOT_FOUND: 4;
|
|
62
|
+
NETWORK: 5;
|
|
63
|
+
}>;
|
|
64
|
+
/** `trawl spec --json`'s `exitCodes` field — a short label per code. Code
|
|
65
|
+
* `1` is shared by several `kind`s (`api`, `refused`, `unknown`); the label
|
|
66
|
+
* names the generic/unmapped bucket it represents, not an exhaustive list. */
|
|
67
|
+
export declare const EXIT_CODE_LABELS: Readonly<Record<string, string>>;
|
|
68
|
+
/**
|
|
69
|
+
* #170 — ONE frozen map, keyed by the existing `kind`, driving the
|
|
70
|
+
* envelope's new `retryable`/`next` fields. This is also the exhaustive set
|
|
71
|
+
* of `kind`s `classifyError` can produce — `ERROR_KINDS` below derives its
|
|
72
|
+
* list from these keys rather than hand-listing them a second time, and
|
|
73
|
+
* `spec.ts`'s `buildSpec()` reads `ERROR_KINDS`, never its own copy.
|
|
74
|
+
*
|
|
75
|
+
* `network` is the one kind worth retrying unchanged. `auth` isn't
|
|
76
|
+
* retryable but has an honest next step (`trawl login --token <jwt>`).
|
|
77
|
+
* Everything else (`usage`/`not_found`/`api`/`refused`/`unknown`) is neither
|
|
78
|
+
* — retrying a bad flag, a missing resource, or an unmapped bug with no new
|
|
79
|
+
* information just repeats the same failure.
|
|
80
|
+
*
|
|
81
|
+
* #170 review F8 — `next` must be an EXECUTABLE next command, not just a verb
|
|
82
|
+
* name: bare `trawl login` still blocks on an interactive prompt (email then
|
|
83
|
+
* password) — for a non-interactive caller (stdin closed, the exact
|
|
84
|
+
* situation an auth failure implies) it fails immediately with its OWN usage
|
|
85
|
+
* error instead of the login the agent was told to run. `trawl login --token
|
|
86
|
+
* <jwt>` is the one form of `login` that never prompts.
|
|
87
|
+
*/
|
|
88
|
+
export declare const RETRY_POLICY: Readonly<Record<string, {
|
|
89
|
+
retryable: boolean;
|
|
90
|
+
next?: readonly string[];
|
|
91
|
+
}>>;
|
|
92
|
+
/** Every `kind` string `classifyError` can produce — derived from
|
|
93
|
+
* `RETRY_POLICY`'s keys (see its doc comment), never a second hand list. */
|
|
94
|
+
export declare const ERROR_KINDS: readonly string[];
|
|
95
|
+
/**
|
|
96
|
+
* Every `kind` a real `--json` error envelope can carry — NOT the same set
|
|
97
|
+
* as `ERROR_KINDS`. `ERROR_KINDS` is deliberately narrow (exactly what
|
|
98
|
+
* `classifyError` produces, see its own doc comment — `RETRY_POLICY` must
|
|
99
|
+
* keep meaning that, `retryFieldsFor` relies on it), but three more kinds
|
|
100
|
+
* reach a real envelope from hand-built emitters that never go through
|
|
101
|
+
* `classifyError` at all:
|
|
102
|
+
* - `in_progress` — `src/commands/scraps.ts`, `data <id>`'s `reportDataState`
|
|
103
|
+
* call for a run still in flight
|
|
104
|
+
* - `run_failed` — same file, same helper, for a last run that failed
|
|
105
|
+
* - `upgrade_failed` — `src/commands/upgrade.ts`, when `npm install -g` itself fails
|
|
106
|
+
*
|
|
107
|
+
* `spec.ts`'s `errorKinds` field publishes THIS constant, never
|
|
108
|
+
* `ERROR_KINDS` — an agent that builds its allow-list from the published
|
|
109
|
+
* spec must not reject a perfectly valid `{"error":{"kind":"in_progress",…}}`
|
|
110
|
+
* envelope just because it was hand-built instead of classified. Built as a
|
|
111
|
+
* union (spread `ERROR_KINDS` + list the hand-built kinds) rather than a
|
|
112
|
+
* second hand-copy of the first group, so the classifyError kinds are
|
|
113
|
+
* PROVABLY a subset — see errors.test.ts's exhaustiveness check.
|
|
114
|
+
*
|
|
115
|
+
* Adding a new hand-built `kind:` literal anywhere in `src/` means
|
|
116
|
+
* registering it here too, or the published spec will lie about it exactly
|
|
117
|
+
* like this constant exists to prevent.
|
|
118
|
+
*/
|
|
119
|
+
export declare const ENVELOPE_KINDS: readonly string[];
|
|
120
|
+
/**
|
|
121
|
+
* #170 review F7 — `spec --json`'s flat `exitCodes` map (`EXIT_CODE_LABELS`)
|
|
122
|
+
* publishes ONE label per code, which is honest about code `1` being a
|
|
123
|
+
* generic/unmapped bucket but says nothing about which `kind`s actually land
|
|
124
|
+
* there. An agent building an `exitCode -> kind` table off `exitCodes` alone
|
|
125
|
+
* reads `"1":"unknown"` and treats every other kind sharing that code
|
|
126
|
+
* (`api`/`refused`/`in_progress`/`run_failed`/`upgrade_failed`) as an
|
|
127
|
+
* unmapped bug it should give up on — instead of honouring the very
|
|
128
|
+
* `retryable`/`next` fields those envelopes correctly carry.
|
|
129
|
+
*
|
|
130
|
+
* This is the inverse direction, `kind -> exitCode`, one entry per
|
|
131
|
+
* `ENVELOPE_KINDS` member — additive (published as a SIBLING field,
|
|
132
|
+
* `kindExitCodes`, never replacing `exitCodes`) and keyed off the same
|
|
133
|
+
* `EXIT_CODES` constants every real call site already uses, so a future exit
|
|
134
|
+
* code change here can't silently drift from `classifyError`/
|
|
135
|
+
* `reportDataState`/`upgrade.ts`'s actual behaviour without also changing the
|
|
136
|
+
* single source those all draw from. errors.test.ts asserts every entry here
|
|
137
|
+
* against classifyError's REAL returned exitCode for that kind — the closest
|
|
138
|
+
* an exhaustive hand-map can get to "derived", short of `classifyError`
|
|
139
|
+
* itself being rewritten to loop over a shared table (out of scope here).
|
|
140
|
+
*/
|
|
141
|
+
export declare const KIND_EXIT_CODES: Readonly<Record<string, number>>;
|
|
142
|
+
/**
|
|
143
|
+
* Look up the frozen default `retryable`/`next` for a `kind`. Total (never
|
|
144
|
+
* throws) — a `kind` outside `RETRY_POLICY` (e.g. `scraps data`'s own
|
|
145
|
+
* `run_failed`/`in_progress` states, which aren't part of `classifyError`'s
|
|
146
|
+
* taxonomy) falls back to the conservative "not retryable, nothing to
|
|
147
|
+
* suggest" default. Callers with a genuine kind-is-wrong override (an
|
|
148
|
+
* `ApiError` 429, `scraps data`'s `in_progress` refusal) pass their own
|
|
149
|
+
* `retryable`/`next` instead of trusting this lookup — see classifyError and
|
|
150
|
+
* scraps.ts's `reportDataState`.
|
|
151
|
+
*/
|
|
152
|
+
export declare function retryFieldsFor(kind: string): {
|
|
153
|
+
retryable: boolean;
|
|
154
|
+
next?: string[];
|
|
155
|
+
};
|
|
45
156
|
/**
|
|
46
157
|
* Central status → exit-code map (#71 findings 13/14/60). Agents driving this
|
|
47
158
|
* CLI unattended need to tell "you're not logged in" (3) from "that id
|
|
@@ -53,8 +164,8 @@ export declare function classifyError(err: unknown): ClassifiedError;
|
|
|
53
164
|
/**
|
|
54
165
|
* Print a classified error to the correct stream and return its exit code.
|
|
55
166
|
* stdout is reserved for payload — under --json the error itself IS the
|
|
56
|
-
* payload (`{"error":{message,status,kind}}`); otherwise the
|
|
57
|
-
* line goes to stderr, never stdout. (#71 findings 13/14/60)
|
|
167
|
+
* payload (`{"error":{message,status,kind,retryable,next?}}`); otherwise the
|
|
168
|
+
* human-readable line goes to stderr, never stdout. (#71 findings 13/14/60)
|
|
58
169
|
*
|
|
59
170
|
* `quiet` skips the human-readable stderr line (used when the caller already
|
|
60
171
|
* printed a fuller diagnostic, e.g. a raw stack trace under --debug) while
|
package/dist/lib/errors.js
CHANGED
|
@@ -43,6 +43,145 @@ export class RefusalError extends Error {
|
|
|
43
43
|
export function stripCommanderErrorPrefix(message) {
|
|
44
44
|
return message.startsWith('error: ') ? message.slice('error: '.length) : message;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The exit-code taxonomy (#71 findings 13/14/60), named once so
|
|
48
|
+
* `classifyError` below and `spec.ts`'s `buildSpec()` (#170) read the SAME
|
|
49
|
+
* numbers instead of `spec.ts` hand-listing its own copy — exactly the kind
|
|
50
|
+
* of second source of truth `tests/contracts/` exists to catch drifting.
|
|
51
|
+
*/
|
|
52
|
+
export const EXIT_CODES = Object.freeze({
|
|
53
|
+
SUCCESS: 0,
|
|
54
|
+
UNKNOWN: 1,
|
|
55
|
+
USAGE: 2,
|
|
56
|
+
AUTH: 3,
|
|
57
|
+
NOT_FOUND: 4,
|
|
58
|
+
NETWORK: 5,
|
|
59
|
+
});
|
|
60
|
+
/** `trawl spec --json`'s `exitCodes` field — a short label per code. Code
|
|
61
|
+
* `1` is shared by several `kind`s (`api`, `refused`, `unknown`); the label
|
|
62
|
+
* names the generic/unmapped bucket it represents, not an exhaustive list. */
|
|
63
|
+
export const EXIT_CODE_LABELS = Object.freeze({
|
|
64
|
+
[EXIT_CODES.SUCCESS]: 'success',
|
|
65
|
+
[EXIT_CODES.UNKNOWN]: 'unknown',
|
|
66
|
+
[EXIT_CODES.USAGE]: 'usage',
|
|
67
|
+
[EXIT_CODES.AUTH]: 'auth',
|
|
68
|
+
[EXIT_CODES.NOT_FOUND]: 'not_found',
|
|
69
|
+
[EXIT_CODES.NETWORK]: 'network',
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* #170 — ONE frozen map, keyed by the existing `kind`, driving the
|
|
73
|
+
* envelope's new `retryable`/`next` fields. This is also the exhaustive set
|
|
74
|
+
* of `kind`s `classifyError` can produce — `ERROR_KINDS` below derives its
|
|
75
|
+
* list from these keys rather than hand-listing them a second time, and
|
|
76
|
+
* `spec.ts`'s `buildSpec()` reads `ERROR_KINDS`, never its own copy.
|
|
77
|
+
*
|
|
78
|
+
* `network` is the one kind worth retrying unchanged. `auth` isn't
|
|
79
|
+
* retryable but has an honest next step (`trawl login --token <jwt>`).
|
|
80
|
+
* Everything else (`usage`/`not_found`/`api`/`refused`/`unknown`) is neither
|
|
81
|
+
* — retrying a bad flag, a missing resource, or an unmapped bug with no new
|
|
82
|
+
* information just repeats the same failure.
|
|
83
|
+
*
|
|
84
|
+
* #170 review F8 — `next` must be an EXECUTABLE next command, not just a verb
|
|
85
|
+
* name: bare `trawl login` still blocks on an interactive prompt (email then
|
|
86
|
+
* password) — for a non-interactive caller (stdin closed, the exact
|
|
87
|
+
* situation an auth failure implies) it fails immediately with its OWN usage
|
|
88
|
+
* error instead of the login the agent was told to run. `trawl login --token
|
|
89
|
+
* <jwt>` is the one form of `login` that never prompts.
|
|
90
|
+
*/
|
|
91
|
+
export const RETRY_POLICY = Object.freeze({
|
|
92
|
+
auth: { retryable: false, next: ['trawl login --token <jwt>'] },
|
|
93
|
+
network: { retryable: true },
|
|
94
|
+
usage: { retryable: false },
|
|
95
|
+
not_found: { retryable: false },
|
|
96
|
+
api: { retryable: false },
|
|
97
|
+
refused: { retryable: false },
|
|
98
|
+
unknown: { retryable: false },
|
|
99
|
+
});
|
|
100
|
+
/** Every `kind` string `classifyError` can produce — derived from
|
|
101
|
+
* `RETRY_POLICY`'s keys (see its doc comment), never a second hand list. */
|
|
102
|
+
export const ERROR_KINDS = Object.freeze(Object.keys(RETRY_POLICY));
|
|
103
|
+
/**
|
|
104
|
+
* Every `kind` a real `--json` error envelope can carry — NOT the same set
|
|
105
|
+
* as `ERROR_KINDS`. `ERROR_KINDS` is deliberately narrow (exactly what
|
|
106
|
+
* `classifyError` produces, see its own doc comment — `RETRY_POLICY` must
|
|
107
|
+
* keep meaning that, `retryFieldsFor` relies on it), but three more kinds
|
|
108
|
+
* reach a real envelope from hand-built emitters that never go through
|
|
109
|
+
* `classifyError` at all:
|
|
110
|
+
* - `in_progress` — `src/commands/scraps.ts`, `data <id>`'s `reportDataState`
|
|
111
|
+
* call for a run still in flight
|
|
112
|
+
* - `run_failed` — same file, same helper, for a last run that failed
|
|
113
|
+
* - `upgrade_failed` — `src/commands/upgrade.ts`, when `npm install -g` itself fails
|
|
114
|
+
*
|
|
115
|
+
* `spec.ts`'s `errorKinds` field publishes THIS constant, never
|
|
116
|
+
* `ERROR_KINDS` — an agent that builds its allow-list from the published
|
|
117
|
+
* spec must not reject a perfectly valid `{"error":{"kind":"in_progress",…}}`
|
|
118
|
+
* envelope just because it was hand-built instead of classified. Built as a
|
|
119
|
+
* union (spread `ERROR_KINDS` + list the hand-built kinds) rather than a
|
|
120
|
+
* second hand-copy of the first group, so the classifyError kinds are
|
|
121
|
+
* PROVABLY a subset — see errors.test.ts's exhaustiveness check.
|
|
122
|
+
*
|
|
123
|
+
* Adding a new hand-built `kind:` literal anywhere in `src/` means
|
|
124
|
+
* registering it here too, or the published spec will lie about it exactly
|
|
125
|
+
* like this constant exists to prevent.
|
|
126
|
+
*/
|
|
127
|
+
export const ENVELOPE_KINDS = Object.freeze([
|
|
128
|
+
...ERROR_KINDS,
|
|
129
|
+
'in_progress',
|
|
130
|
+
'run_failed',
|
|
131
|
+
'upgrade_failed',
|
|
132
|
+
]);
|
|
133
|
+
/**
|
|
134
|
+
* #170 review F7 — `spec --json`'s flat `exitCodes` map (`EXIT_CODE_LABELS`)
|
|
135
|
+
* publishes ONE label per code, which is honest about code `1` being a
|
|
136
|
+
* generic/unmapped bucket but says nothing about which `kind`s actually land
|
|
137
|
+
* there. An agent building an `exitCode -> kind` table off `exitCodes` alone
|
|
138
|
+
* reads `"1":"unknown"` and treats every other kind sharing that code
|
|
139
|
+
* (`api`/`refused`/`in_progress`/`run_failed`/`upgrade_failed`) as an
|
|
140
|
+
* unmapped bug it should give up on — instead of honouring the very
|
|
141
|
+
* `retryable`/`next` fields those envelopes correctly carry.
|
|
142
|
+
*
|
|
143
|
+
* This is the inverse direction, `kind -> exitCode`, one entry per
|
|
144
|
+
* `ENVELOPE_KINDS` member — additive (published as a SIBLING field,
|
|
145
|
+
* `kindExitCodes`, never replacing `exitCodes`) and keyed off the same
|
|
146
|
+
* `EXIT_CODES` constants every real call site already uses, so a future exit
|
|
147
|
+
* code change here can't silently drift from `classifyError`/
|
|
148
|
+
* `reportDataState`/`upgrade.ts`'s actual behaviour without also changing the
|
|
149
|
+
* single source those all draw from. errors.test.ts asserts every entry here
|
|
150
|
+
* against classifyError's REAL returned exitCode for that kind — the closest
|
|
151
|
+
* an exhaustive hand-map can get to "derived", short of `classifyError`
|
|
152
|
+
* itself being rewritten to loop over a shared table (out of scope here).
|
|
153
|
+
*/
|
|
154
|
+
export const KIND_EXIT_CODES = Object.freeze({
|
|
155
|
+
auth: EXIT_CODES.AUTH,
|
|
156
|
+
network: EXIT_CODES.NETWORK,
|
|
157
|
+
usage: EXIT_CODES.USAGE,
|
|
158
|
+
not_found: EXIT_CODES.NOT_FOUND,
|
|
159
|
+
api: EXIT_CODES.UNKNOWN,
|
|
160
|
+
refused: EXIT_CODES.UNKNOWN,
|
|
161
|
+
unknown: EXIT_CODES.UNKNOWN,
|
|
162
|
+
// Hand-built kinds (never routed through classifyError) — in_progress/
|
|
163
|
+
// run_failed both set via reportDataState's literal `1` (scraps.ts),
|
|
164
|
+
// upgrade_failed via its own literal `process.exitCode = 1` (upgrade.ts).
|
|
165
|
+
in_progress: EXIT_CODES.UNKNOWN,
|
|
166
|
+
run_failed: EXIT_CODES.UNKNOWN,
|
|
167
|
+
upgrade_failed: EXIT_CODES.UNKNOWN,
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* Look up the frozen default `retryable`/`next` for a `kind`. Total (never
|
|
171
|
+
* throws) — a `kind` outside `RETRY_POLICY` (e.g. `scraps data`'s own
|
|
172
|
+
* `run_failed`/`in_progress` states, which aren't part of `classifyError`'s
|
|
173
|
+
* taxonomy) falls back to the conservative "not retryable, nothing to
|
|
174
|
+
* suggest" default. Callers with a genuine kind-is-wrong override (an
|
|
175
|
+
* `ApiError` 429, `scraps data`'s `in_progress` refusal) pass their own
|
|
176
|
+
* `retryable`/`next` instead of trusting this lookup — see classifyError and
|
|
177
|
+
* scraps.ts's `reportDataState`.
|
|
178
|
+
*/
|
|
179
|
+
export function retryFieldsFor(kind) {
|
|
180
|
+
const policy = RETRY_POLICY[kind];
|
|
181
|
+
if (!policy)
|
|
182
|
+
return { retryable: false };
|
|
183
|
+
return policy.next ? { retryable: policy.retryable, next: [...policy.next] } : { retryable: policy.retryable };
|
|
184
|
+
}
|
|
46
185
|
/**
|
|
47
186
|
* Central status → exit-code map (#71 findings 13/14/60). Agents driving this
|
|
48
187
|
* CLI unattended need to tell "you're not logged in" (3) from "that id
|
|
@@ -57,32 +196,59 @@ export function classifyError(err) {
|
|
|
57
196
|
// `status:401` — that would claim a server response that never happened.
|
|
58
197
|
// Same exit code / kind as a real server 401 (ApiError below); only the
|
|
59
198
|
// envelope shape differs.
|
|
199
|
+
//
|
|
200
|
+
// #169 review round 2, finding 2 — `err.next` is only ever set (in api.ts,
|
|
201
|
+
// via authNextSteps()) for an apiKey-mode failure, where the frozen
|
|
202
|
+
// `next` default ("trawl login --token <jwt>") is inert until a live
|
|
203
|
+
// TRAWL_API_KEY/TRAWL_TOKEN is unset first. Spread AFTER retryFieldsFor so
|
|
204
|
+
// it overrides that default's `next` key; every other AuthError/ApiError
|
|
205
|
+
// 401 (jwt mode, notLoggedInError — no credential at all) leaves `err.next`
|
|
206
|
+
// undefined and keeps the unmodified frozen default, unchanged from before.
|
|
60
207
|
if (err instanceof AuthError) {
|
|
61
|
-
return {
|
|
208
|
+
return {
|
|
209
|
+
exitCode: EXIT_CODES.AUTH,
|
|
210
|
+
envelope: { message, kind: 'auth', ...retryFieldsFor('auth'), ...(err.next ? { next: err.next } : {}) },
|
|
211
|
+
};
|
|
62
212
|
}
|
|
63
213
|
if (err instanceof ApiError) {
|
|
64
|
-
if (err.status === 401)
|
|
65
|
-
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
214
|
+
if (err.status === 401) {
|
|
215
|
+
return {
|
|
216
|
+
exitCode: EXIT_CODES.AUTH,
|
|
217
|
+
envelope: {
|
|
218
|
+
message,
|
|
219
|
+
status: 401,
|
|
220
|
+
kind: 'auth',
|
|
221
|
+
...retryFieldsFor('auth'),
|
|
222
|
+
...(err.next ? { next: err.next } : {}),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (err.status === 404) {
|
|
227
|
+
return { exitCode: EXIT_CODES.NOT_FOUND, envelope: { message, status: 404, kind: 'not_found', ...retryFieldsFor('not_found') } };
|
|
228
|
+
}
|
|
229
|
+
// #170 — a 429 (rate-limited / quota-exhausted) is the one `api`-kind
|
|
230
|
+
// response worth retrying, even though the frozen map's default for
|
|
231
|
+
// `api` is not retryable. Genuinely kind-is-wrong override, not a new
|
|
232
|
+
// kind — `kind` stays `"api"`.
|
|
233
|
+
const retry = err.status === 429 ? { retryable: true } : retryFieldsFor('api');
|
|
234
|
+
return { exitCode: EXIT_CODES.UNKNOWN, envelope: { message, status: err.status, kind: 'api', ...retry } };
|
|
69
235
|
}
|
|
70
236
|
if (err instanceof NetworkError) {
|
|
71
|
-
return { exitCode:
|
|
237
|
+
return { exitCode: EXIT_CODES.NETWORK, envelope: { message, kind: 'network', ...retryFieldsFor('network') } };
|
|
72
238
|
}
|
|
73
239
|
if (err instanceof UsageError) {
|
|
74
|
-
return { exitCode:
|
|
240
|
+
return { exitCode: EXIT_CODES.USAGE, envelope: { message, kind: 'usage', ...retryFieldsFor('usage') } };
|
|
75
241
|
}
|
|
76
242
|
if (err instanceof RefusalError) {
|
|
77
|
-
return { exitCode:
|
|
243
|
+
return { exitCode: EXIT_CODES.UNKNOWN, envelope: { message, kind: 'refused', ...retryFieldsFor('refused') } };
|
|
78
244
|
}
|
|
79
|
-
return { exitCode:
|
|
245
|
+
return { exitCode: EXIT_CODES.UNKNOWN, envelope: { message, kind: 'unknown', ...retryFieldsFor('unknown') } };
|
|
80
246
|
}
|
|
81
247
|
/**
|
|
82
248
|
* Print a classified error to the correct stream and return its exit code.
|
|
83
249
|
* stdout is reserved for payload — under --json the error itself IS the
|
|
84
|
-
* payload (`{"error":{message,status,kind}}`); otherwise the
|
|
85
|
-
* line goes to stderr, never stdout. (#71 findings 13/14/60)
|
|
250
|
+
* payload (`{"error":{message,status,kind,retryable,next?}}`); otherwise the
|
|
251
|
+
* human-readable line goes to stderr, never stdout. (#71 findings 13/14/60)
|
|
86
252
|
*
|
|
87
253
|
* `quiet` skips the human-readable stderr line (used when the caller already
|
|
88
254
|
* printed a fuller diagnostic, e.g. a raw stack trace under --debug) while
|
package/docs/agent-quickstart.md
CHANGED
|
@@ -6,21 +6,75 @@ telemetry, etc.) see the [main README](../README.md) — the human/CI guide.
|
|
|
6
6
|
|
|
7
7
|
## Auth — zero prompts
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
**Use `TRAWL_API_KEY`, not `TRAWL_TOKEN`, if you can.** A `trawl_*` API key
|
|
10
|
+
is revocable on its own and can be restricted to a set of scraps; a JWT
|
|
11
|
+
(`TRAWL_TOKEN`) is the human's own session — expiring, full-privilege, and
|
|
12
|
+
impossible to revoke without logging that human out everywhere.
|
|
13
|
+
|
|
14
|
+
Be precise about what "scoped" buys you, because the two axes are not equal
|
|
15
|
+
in practice. A key can carry a **scrap allow-list** (set in the dashboard at
|
|
16
|
+
creation) and an **action scope list** (`trawl:scraps:read` and friends).
|
|
17
|
+
Only the first is reachable from the web app; the second must be passed as an
|
|
18
|
+
explicit `scopes` array when the key is created over the API, and a key
|
|
19
|
+
created without one has **every action granted**. So unless someone minted it
|
|
20
|
+
deliberately through the API, the key you are holding is restricted by scrap,
|
|
21
|
+
not by action. `trawl` picks the right
|
|
22
|
+
`Authorization`/`Cookie` shape automatically based on which one you set —
|
|
23
|
+
never a flag:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
export TRAWL_API_KEY=trawl_xxx
|
|
27
|
+
trawl list --json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`TRAWL_TOKEN` (a human session JWT) still works — set it only if you don't
|
|
31
|
+
have a key yet — and `TRAWL_API_KEY` always wins when both happen to be set:
|
|
11
32
|
|
|
12
33
|
```bash
|
|
13
34
|
export TRAWL_TOKEN=<jwt>
|
|
14
|
-
trawl
|
|
35
|
+
trawl list --json
|
|
15
36
|
```
|
|
16
37
|
|
|
38
|
+
**What a key cannot do.** Of the twelve core commands below, a scoped key
|
|
39
|
+
covers all of them except `whoami`. Outside the core tier, a key also can't
|
|
40
|
+
reach `scraps update`/`delete`, `scraps account set`/`delete`/`clear-session`,
|
|
41
|
+
`scraps account session set`, `scraps banner`, or `scraps snapshot` (its
|
|
42
|
+
underlying `html-snapshot` route is JWT-only even though the scrap lookup it
|
|
43
|
+
starts from isn't) — nor the standalone SSE **command** `scraps watch`.
|
|
44
|
+
Those, plus `whoami`, stay JWT-only. `scraps account status`, `scraps
|
|
45
|
+
doctor`, and `scraps autofix` are the exceptions inside that same
|
|
46
|
+
management-tier group: all three only ever read routes trawl_node opened to
|
|
47
|
+
keys (`GET /api/scraps/:id`, `GET /api/historys/:hid`, `GET
|
|
48
|
+
/api/scraps/:id/activities`), so all three work fine under a key. (`scraps
|
|
49
|
+
watch` is a separate command, not the same thing as the `--watch` polling
|
|
50
|
+
flag on `run`/`trigger` below, which works fine under a key — do not
|
|
51
|
+
conflate the two.)
|
|
52
|
+
|
|
53
|
+
This list mirrors trawl_node's route wiring as of this writing, not a
|
|
54
|
+
permanent contract — a route's auth mode can change on either side without
|
|
55
|
+
this doc catching up same-day. Don't trust the list blind for a command not
|
|
56
|
+
named above: the reliable check is the real `--json` envelope, every time —
|
|
57
|
+
a JWT-only command run under a key exits `3` with a `kind:"auth"` envelope
|
|
58
|
+
pointing at `trawl login`, never a silent wrong answer, whether or not this
|
|
59
|
+
paragraph is current. `whoami` catches this client-side and names itself in
|
|
60
|
+
the message; every other JWT-only route above is caught by the real server
|
|
61
|
+
401 instead, so the message says a session is required without naming which
|
|
62
|
+
command hit it.
|
|
63
|
+
|
|
64
|
+
`trawl token` also needs a word here: `getToken()` (the same resolver every
|
|
65
|
+
command above uses) can hand back `TRAWL_API_KEY` just as easily as a
|
|
66
|
+
session token, so `trawl token`/`$(trawl token)` may print a scoped,
|
|
67
|
+
**non-expiring** API key instead of an expiring JWT — check `--json`'s
|
|
68
|
+
`mode` field (`"apiKey"` or `"jwt"`) if your script branches on which one it
|
|
69
|
+
got.
|
|
70
|
+
|
|
17
71
|
(Interactive `trawl login` and the `--url`/config-file flow are documented in
|
|
18
72
|
the README's [Authentication](../README.md#authentication) section — an
|
|
19
73
|
agent should never need them.)
|
|
20
74
|
|
|
21
75
|
## Core commands (agent + human)
|
|
22
76
|
|
|
23
|
-
These
|
|
77
|
+
These twelve commands are the CLI's agent+human surface — `--json` is
|
|
24
78
|
first-class on every one, and none of them ever blocks on a prompt (see
|
|
25
79
|
[Non-interactive contract](#non-interactive-contract) below):
|
|
26
80
|
|
|
@@ -35,8 +89,9 @@ trawl data <id> [--json] [--fresh] [--errors] Get scrap data (last persisted ru
|
|
|
35
89
|
trawl history <id> [--json] [-n <limit>] List past runs for a scrap
|
|
36
90
|
trawl run-info <hid> [--json] Show details of a single run
|
|
37
91
|
trawl whoami [--json] Show the authenticated user's identity
|
|
38
|
-
trawl token [--json] Print the stored session JWT (for MCP Bearer auth)
|
|
92
|
+
trawl token [--json] Print the stored credential — API key or session JWT (for MCP Bearer auth)
|
|
39
93
|
trawl ping [--json] Health/version handshake against the Trawl API
|
|
94
|
+
trawl spec [--json] Print a versioned, machine-readable description of the command tree
|
|
40
95
|
```
|
|
41
96
|
|
|
42
97
|
`trawl create` is the closest primitive to "turn this URL + goal into a
|
|
@@ -85,11 +140,33 @@ reaches a terminal state (not the whole progress stream), and there is no
|
|
|
85
140
|
JSON form of an HTML page (irrelevant to the core verbs above — that only
|
|
86
141
|
applies to the management-only `scraps snapshot`).
|
|
87
142
|
|
|
143
|
+
Capture **stdout alone** — never `2>&1`. Stderr legitimately carries
|
|
144
|
+
human-facing lines even in `--json` mode (the one-time telemetry disclosure
|
|
145
|
+
on first run, the skills re-sync notice, warnings); merging the two streams
|
|
146
|
+
corrupts the payload you're about to parse.
|
|
147
|
+
|
|
88
148
|
On failure, `--json` emits a single error envelope on stdout instead of
|
|
89
|
-
prose — `{"error":{"message","status?","kind"}}` — and
|
|
90
|
-
line goes to stderr, never stdout. `kind` is the
|
|
91
|
-
discriminant (`"usage"`/`"auth"`/`"not_found"`/`"network"
|
|
92
|
-
`"refused"`/`"unknown"
|
|
149
|
+
prose — `{"error":{"message","status?","kind","retryable","next?"}}` — and
|
|
150
|
+
the human-readable line goes to stderr, never stdout. `kind` is the
|
|
151
|
+
machine-readable discriminant (`"usage"`/`"auth"`/`"not_found"`/`"network"`/
|
|
152
|
+
`"api"`/`"refused"`/`"unknown"`/`"in_progress"`/`"run_failed"`/
|
|
153
|
+
`"upgrade_failed"`) a script should switch on — run `trawl spec --json` for
|
|
154
|
+
the exhaustive, versioned list.
|
|
155
|
+
|
|
156
|
+
`retryable` is the field a retry loop actually branches on — not `kind`, and
|
|
157
|
+
never a regex over `message` (e.g. "retry shortly" is prose, not a
|
|
158
|
+
contract): `true` means retrying the exact same command, unchanged, is
|
|
159
|
+
worth it (a network blip, a `429`, a run still in progress); `false` means
|
|
160
|
+
it isn't (a bad flag, an expired token, a missing resource — nothing about
|
|
161
|
+
retrying without changes would help). `next` is advisory only — commands
|
|
162
|
+
worth running next, most useful first (e.g. `["trawl login --token <jwt>"]`
|
|
163
|
+
on an auth failure — the one form of `login` that never blocks on an
|
|
164
|
+
interactive prompt) — and **may be empty or absent**: it is never filled in
|
|
165
|
+
just to look helpful, so don't treat its absence as an error.
|
|
166
|
+
|
|
167
|
+
For the full, versioned machine description of every command (arguments,
|
|
168
|
+
options, aliases, exit codes, error kinds) — instead of parsing this doc —
|
|
169
|
+
run `trawl spec --json`.
|
|
93
170
|
|
|
94
171
|
## Non-interactive contract
|
|
95
172
|
|
|
@@ -107,7 +184,7 @@ hanging. Full rule + rationale: README's
|
|
|
107
184
|
| `0` | Success |
|
|
108
185
|
| `1` | Unknown/generic error, or a business-logic outcome (e.g. `create`'s honest `success:false` first-run outcome, `data`'s `run_failed`/`in_progress`) |
|
|
109
186
|
| `2` | Usage error (bad flag/value, invalid ID, missing required argument, or the non-interactive guard refusing to prompt) |
|
|
110
|
-
| `3` | Auth error (not logged in,
|
|
187
|
+
| `3` | Auth error (not logged in, the session token is expired/invalid, or a JWT-only command — see [above](#auth--zero-prompts) — was run under an API key) |
|
|
111
188
|
| `4` | Not found (no such resource, or no persisted payload to read) |
|
|
112
189
|
| `5` | Network error (API host unreachable, DNS/connection/TLS failure, or timeout) |
|
|
113
190
|
|
|
@@ -119,5 +196,5 @@ section — treat that as canonical if the two ever seem to disagree.
|
|
|
119
196
|
## Minimal example
|
|
120
197
|
|
|
121
198
|
```bash
|
|
122
|
-
|
|
199
|
+
TRAWL_API_KEY=trawl_xxx trawl create https://example.com --prompt "Extract the article title and body text" --json
|
|
123
200
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trawlme/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "Trawl CLI — manage scraps from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"strip-ansi": "^7.2.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
+
"@semantic-release/commit-analyzer": "13.0.1",
|
|
53
54
|
"@types/node": "^25.5.2",
|
|
54
55
|
"@vitest/coverage-v8": "^4.1.2",
|
|
55
56
|
"typescript": "^6.0.2",
|