@tiny-fish/cli 0.30.2-next.258 → 0.30.2-next.264
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/dist/commands/batch.js +140 -141
- package/dist/commands/connect.d.ts +0 -1
- package/dist/commands/connect.js +71 -30
- package/dist/commands/doctor.js +5 -15
- package/dist/commands/profile.d.ts +2 -1
- package/dist/commands/profile.js +49 -45
- package/dist/commands/runs.js +194 -192
- package/dist/lib/auth.js +15 -13
- package/dist/lib/cli-install.d.ts +2 -3
- package/dist/lib/cli-install.js +1 -3
- package/dist/lib/connect-all.js +61 -6
- package/dist/lib/connect-clients.d.ts +23 -0
- package/dist/lib/connect-clients.js +55 -4
- package/dist/lib/connect-fallback.js +57 -29
- package/dist/lib/connect-picker.js +25 -8
- package/dist/lib/connect-runtime.d.ts +6 -0
- package/dist/lib/connect-runtime.js +28 -9
- package/dist/lib/hermes-config.d.ts +18 -0
- package/dist/lib/hermes-config.js +70 -0
- package/dist/lib/hermes-env.d.ts +6 -0
- package/dist/lib/hermes-env.js +50 -5
- package/dist/lib/mcp-endpoint.d.ts +4 -0
- package/dist/lib/mcp-endpoint.js +15 -0
- package/dist/lib/registration-detect.js +33 -7
- package/dist/lib/verify.d.ts +1 -1
- package/dist/lib/verify.js +1 -1
- package/package.json +2 -1
package/dist/commands/runs.js
CHANGED
|
@@ -78,227 +78,229 @@ function sleep(ms, signal) {
|
|
|
78
78
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
81
|
+
async function runsListAction(opts) {
|
|
82
|
+
// Validate --status
|
|
83
|
+
if (opts.status != null &&
|
|
84
|
+
!VALID_STATUSES.includes(opts.status)) {
|
|
85
|
+
err({
|
|
86
|
+
error: `Invalid --status "${opts.status}". Must be one of: ${VALID_STATUSES.join(', ')}`,
|
|
87
|
+
});
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// Validate --limit
|
|
91
|
+
const MAX_LIMIT = 100;
|
|
92
|
+
let limit;
|
|
93
|
+
if (opts.limit != null) {
|
|
94
|
+
limit = parseInt(opts.limit, 10);
|
|
95
|
+
if (!isFinite(limit) || limit <= 0) {
|
|
96
|
+
err({ error: `Invalid --limit "${opts.limit}". Must be a positive integer.` });
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
if (limit > MAX_LIMIT) {
|
|
100
|
+
err({ error: `--limit cannot exceed ${MAX_LIMIT}.` });
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const apiKey = getApiKey();
|
|
105
|
+
const filter = {
|
|
106
|
+
status: opts.status,
|
|
107
|
+
limit,
|
|
108
|
+
cursor: opts.cursor,
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
const body = await listRuns(filter, apiKey);
|
|
112
|
+
if (opts.pretty) {
|
|
113
|
+
if (body.data.length === 0) {
|
|
114
|
+
outLine('No runs found.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
outLine(`${'RUN ID'.padEnd(36)} ${'STATUS'.padEnd(10)} ${'CREATED AT'.padEnd(24)} GOAL`);
|
|
118
|
+
outLine(`${'-'.repeat(36)} ${'-'.repeat(10)} ${'-'.repeat(24)} ${'-'.repeat(30)}`);
|
|
119
|
+
for (const run of body.data) {
|
|
120
|
+
outLine(`${run.run_id.padEnd(36)} ${run.status.padEnd(10)} ${run.created_at.padEnd(24)} ${run.goal}`);
|
|
121
|
+
}
|
|
122
|
+
if (body.pagination.has_more) {
|
|
123
|
+
outLine(`\nNext cursor: ${body.pagination.next_cursor}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
out(body);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
handleApiError(e);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function runsGetAction(runId, opts) {
|
|
135
|
+
const apiKey = getApiKey();
|
|
136
|
+
try {
|
|
137
|
+
const run = await getRun(runId, apiKey);
|
|
138
|
+
if (opts.pretty) {
|
|
139
|
+
outLine(`Run ID: ${run.run_id}`);
|
|
140
|
+
outLine(`Status: ${run.status}`);
|
|
141
|
+
outLine(`Goal: ${run.goal}`);
|
|
142
|
+
outLine(`Created: ${run.created_at}`);
|
|
143
|
+
if (run.started_at)
|
|
144
|
+
outLine(`Started: ${run.started_at}`);
|
|
145
|
+
if (run.finished_at)
|
|
146
|
+
outLine(`Finished: ${run.finished_at}`);
|
|
147
|
+
outLine(`Steps: ${run.num_of_steps}`);
|
|
148
|
+
if (run.result !== null && run.result !== undefined) {
|
|
149
|
+
outLine(`\nResult:\n${JSON.stringify(run.result, null, 2)}`);
|
|
150
|
+
}
|
|
151
|
+
if (run.error)
|
|
152
|
+
outLine(`\nError: ${run.error.message}`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
out(run);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
handleApiError(e);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function runsStepsAction(runId, opts) {
|
|
163
|
+
const apiKey = getApiKey();
|
|
164
|
+
try {
|
|
165
|
+
const result = await getRunSteps(runId, apiKey);
|
|
166
|
+
if (opts.pretty) {
|
|
167
|
+
if (result.steps.length === 0) {
|
|
168
|
+
outLine('No steps found.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
outLine(`Run ID: ${result.run_id}`);
|
|
172
|
+
for (const [index, step] of result.steps.entries()) {
|
|
173
|
+
outLine(formatStep(step, index));
|
|
174
|
+
outLine(` ${step.timestamp}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
out(result);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
handleApiError(e);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function runsWatchAction(runId, opts) {
|
|
186
|
+
const apiKey = getApiKey();
|
|
187
|
+
const timeoutMs = parsePositiveInt(opts.timeout, '--timeout', DEFAULT_WATCH_TIMEOUT_MS);
|
|
188
|
+
const intervalMs = parsePositiveInt(opts.interval, '--interval', DEFAULT_WATCH_INTERVAL_MS, MIN_WATCH_INTERVAL_MS);
|
|
189
|
+
const controller = new AbortController();
|
|
190
|
+
const onSigint = () => {
|
|
191
|
+
controller.abort();
|
|
192
|
+
process.exit(130);
|
|
193
|
+
};
|
|
194
|
+
process.once('SIGINT', onSigint);
|
|
195
|
+
const deadline = Date.now() + timeoutMs;
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
try {
|
|
198
|
+
const initial = await withDeadline(getRun(runId, apiKey), deadline, controller.signal);
|
|
199
|
+
if (TERMINAL_STATUSES.includes(initial.status)) {
|
|
94
200
|
err({
|
|
95
|
-
error: `
|
|
201
|
+
error: `Run ${runId} is already ${initial.status}; nothing to watch. Use 'tinyfish agent run steps ${runId}' for the trace.`,
|
|
96
202
|
});
|
|
97
203
|
process.exit(1);
|
|
98
204
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
205
|
+
while (!controller.signal.aborted) {
|
|
206
|
+
// One API call per tick — the steps endpoint also returns the run's current status.
|
|
207
|
+
const tick = await withDeadline(getRunSteps(runId, apiKey), deadline, controller.signal);
|
|
208
|
+
let nextIndex = seen.size;
|
|
209
|
+
for (const step of tick.steps) {
|
|
210
|
+
if (seen.has(step.id))
|
|
211
|
+
continue;
|
|
212
|
+
seen.add(step.id);
|
|
213
|
+
if (opts.pretty) {
|
|
214
|
+
outLine(formatStep(step, nextIndex));
|
|
215
|
+
outLine(` ${step.timestamp}`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
out(step);
|
|
219
|
+
}
|
|
220
|
+
nextIndex += 1;
|
|
221
|
+
}
|
|
222
|
+
if (TERMINAL_STATUSES.includes(tick.status)) {
|
|
223
|
+
if (opts.pretty)
|
|
224
|
+
outLine(`Run ${tick.status.toLowerCase()}.`);
|
|
225
|
+
return;
|
|
107
226
|
}
|
|
108
|
-
if (
|
|
109
|
-
err({ error:
|
|
227
|
+
if (Date.now() + intervalMs > deadline) {
|
|
228
|
+
err({ error: `Watch timed out after ${timeoutMs}ms (run still ${tick.status}).` });
|
|
110
229
|
process.exit(1);
|
|
111
230
|
}
|
|
231
|
+
try {
|
|
232
|
+
await sleep(intervalMs, controller.signal);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return; // aborted
|
|
236
|
+
}
|
|
112
237
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
if (e instanceof Error && e.message === 'watch deadline exceeded') {
|
|
241
|
+
err({ error: `Watch timed out after ${timeoutMs}ms.` });
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
if (e instanceof Error && e.message === 'watch aborted') {
|
|
245
|
+
return; // SIGINT handler already triggered process.exit(130)
|
|
246
|
+
}
|
|
247
|
+
handleApiError(e);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
process.off('SIGINT', onSigint);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function runsCancelAction(runId, opts) {
|
|
254
|
+
const apiKey = getApiKey();
|
|
255
|
+
try {
|
|
256
|
+
const result = await cancelRun(runId, apiKey);
|
|
257
|
+
if (opts.pretty) {
|
|
258
|
+
if (result.status === RunStatus.CANCELLED) {
|
|
259
|
+
outLine(result.message
|
|
260
|
+
? `Run ${result.run_id} cancelled (${result.message}).`
|
|
261
|
+
: `Run ${result.run_id} cancelled.`);
|
|
134
262
|
}
|
|
135
263
|
else {
|
|
136
|
-
|
|
264
|
+
outLine(`Run ${result.run_id} already finished (${result.status}).`);
|
|
137
265
|
}
|
|
138
266
|
}
|
|
139
|
-
|
|
140
|
-
|
|
267
|
+
else {
|
|
268
|
+
out(result);
|
|
141
269
|
}
|
|
142
|
-
}
|
|
143
|
-
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
handleApiError(e);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export function registerRuns(runCmd) {
|
|
276
|
+
runCmd
|
|
277
|
+
.command('list')
|
|
278
|
+
.description('List your automation runs')
|
|
279
|
+
.option('--status <status>', 'Filter by status (PENDING|RUNNING|COMPLETED|FAILED|CANCELLED)')
|
|
280
|
+
.option('--limit <n>', 'Max number of runs to return (default: 20)')
|
|
281
|
+
.option('--cursor <cursor>', 'Pagination cursor from a previous response')
|
|
282
|
+
.option('--pretty', 'Human-readable output')
|
|
283
|
+
.action(runsListAction);
|
|
144
284
|
runCmd
|
|
145
285
|
.command('get <run_id>')
|
|
146
286
|
.description('Get a run by ID')
|
|
147
287
|
.option('--pretty', 'Human-readable output')
|
|
148
|
-
.action(
|
|
149
|
-
const apiKey = getApiKey();
|
|
150
|
-
try {
|
|
151
|
-
const run = await getRun(runId, apiKey);
|
|
152
|
-
if (opts.pretty) {
|
|
153
|
-
outLine(`Run ID: ${run.run_id}`);
|
|
154
|
-
outLine(`Status: ${run.status}`);
|
|
155
|
-
outLine(`Goal: ${run.goal}`);
|
|
156
|
-
outLine(`Created: ${run.created_at}`);
|
|
157
|
-
if (run.started_at)
|
|
158
|
-
outLine(`Started: ${run.started_at}`);
|
|
159
|
-
if (run.finished_at)
|
|
160
|
-
outLine(`Finished: ${run.finished_at}`);
|
|
161
|
-
outLine(`Steps: ${run.num_of_steps}`);
|
|
162
|
-
if (run.result !== null && run.result !== undefined) {
|
|
163
|
-
outLine(`\nResult:\n${JSON.stringify(run.result, null, 2)}`);
|
|
164
|
-
}
|
|
165
|
-
if (run.error)
|
|
166
|
-
outLine(`\nError: ${run.error.message}`);
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
out(run);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch (e) {
|
|
173
|
-
handleApiError(e);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
288
|
+
.action(runsGetAction);
|
|
176
289
|
runCmd
|
|
177
290
|
.command('steps <run_id>')
|
|
178
291
|
.description('Print the step-by-step trace for a run')
|
|
179
292
|
.option('--pretty', 'Human-readable output')
|
|
180
|
-
.action(
|
|
181
|
-
const apiKey = getApiKey();
|
|
182
|
-
try {
|
|
183
|
-
const result = await getRunSteps(runId, apiKey);
|
|
184
|
-
if (opts.pretty) {
|
|
185
|
-
if (result.steps.length === 0) {
|
|
186
|
-
outLine('No steps found.');
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
outLine(`Run ID: ${result.run_id}`);
|
|
190
|
-
for (const [index, step] of result.steps.entries()) {
|
|
191
|
-
outLine(formatStep(step, index));
|
|
192
|
-
outLine(` ${step.timestamp}`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
out(result);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
catch (e) {
|
|
200
|
-
handleApiError(e);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
293
|
+
.action(runsStepsAction);
|
|
203
294
|
runCmd
|
|
204
295
|
.command('watch <run_id>')
|
|
205
296
|
.description('Poll a run and print new steps as they arrive (exits when run reaches a terminal status)')
|
|
206
297
|
.option('--pretty', 'Human-readable output')
|
|
207
298
|
.option('--timeout <ms>', `Stop polling after this many milliseconds (default: ${DEFAULT_WATCH_TIMEOUT_MS})`)
|
|
208
299
|
.option('--interval <ms>', `Poll interval (default: ${DEFAULT_WATCH_INTERVAL_MS}, min: ${MIN_WATCH_INTERVAL_MS})`)
|
|
209
|
-
.action(
|
|
210
|
-
const apiKey = getApiKey();
|
|
211
|
-
const timeoutMs = parsePositiveInt(opts.timeout, '--timeout', DEFAULT_WATCH_TIMEOUT_MS);
|
|
212
|
-
const intervalMs = parsePositiveInt(opts.interval, '--interval', DEFAULT_WATCH_INTERVAL_MS, MIN_WATCH_INTERVAL_MS);
|
|
213
|
-
const controller = new AbortController();
|
|
214
|
-
const onSigint = () => {
|
|
215
|
-
controller.abort();
|
|
216
|
-
process.exit(130);
|
|
217
|
-
};
|
|
218
|
-
process.once('SIGINT', onSigint);
|
|
219
|
-
const deadline = Date.now() + timeoutMs;
|
|
220
|
-
const seen = new Set();
|
|
221
|
-
try {
|
|
222
|
-
const initial = await withDeadline(getRun(runId, apiKey), deadline, controller.signal);
|
|
223
|
-
if (TERMINAL_STATUSES.includes(initial.status)) {
|
|
224
|
-
err({
|
|
225
|
-
error: `Run ${runId} is already ${initial.status}; nothing to watch. Use 'tinyfish agent run steps ${runId}' for the trace.`,
|
|
226
|
-
});
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
229
|
-
while (!controller.signal.aborted) {
|
|
230
|
-
// One API call per tick — the steps endpoint also returns the run's current status.
|
|
231
|
-
const tick = await withDeadline(getRunSteps(runId, apiKey), deadline, controller.signal);
|
|
232
|
-
let nextIndex = seen.size;
|
|
233
|
-
for (const step of tick.steps) {
|
|
234
|
-
if (seen.has(step.id))
|
|
235
|
-
continue;
|
|
236
|
-
seen.add(step.id);
|
|
237
|
-
if (opts.pretty) {
|
|
238
|
-
outLine(formatStep(step, nextIndex));
|
|
239
|
-
outLine(` ${step.timestamp}`);
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
out(step);
|
|
243
|
-
}
|
|
244
|
-
nextIndex += 1;
|
|
245
|
-
}
|
|
246
|
-
if (TERMINAL_STATUSES.includes(tick.status)) {
|
|
247
|
-
if (opts.pretty)
|
|
248
|
-
outLine(`Run ${tick.status.toLowerCase()}.`);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
if (Date.now() + intervalMs > deadline) {
|
|
252
|
-
err({ error: `Watch timed out after ${timeoutMs}ms (run still ${tick.status}).` });
|
|
253
|
-
process.exit(1);
|
|
254
|
-
}
|
|
255
|
-
try {
|
|
256
|
-
await sleep(intervalMs, controller.signal);
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
return; // aborted
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
catch (e) {
|
|
264
|
-
if (e instanceof Error && e.message === 'watch deadline exceeded') {
|
|
265
|
-
err({ error: `Watch timed out after ${timeoutMs}ms.` });
|
|
266
|
-
process.exit(1);
|
|
267
|
-
}
|
|
268
|
-
if (e instanceof Error && e.message === 'watch aborted') {
|
|
269
|
-
return; // SIGINT handler already triggered process.exit(130)
|
|
270
|
-
}
|
|
271
|
-
handleApiError(e);
|
|
272
|
-
}
|
|
273
|
-
finally {
|
|
274
|
-
process.off('SIGINT', onSigint);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
// ── run cancel ────────────────────────────────────────────────────────────
|
|
300
|
+
.action(runsWatchAction);
|
|
278
301
|
runCmd
|
|
279
302
|
.command('cancel <run_id>')
|
|
280
303
|
.description('Cancel a run by ID')
|
|
281
304
|
.option('--pretty', 'Human-readable output')
|
|
282
|
-
.action(
|
|
283
|
-
const apiKey = getApiKey();
|
|
284
|
-
try {
|
|
285
|
-
const result = await cancelRun(runId, apiKey);
|
|
286
|
-
if (opts.pretty) {
|
|
287
|
-
if (result.status === RunStatus.CANCELLED) {
|
|
288
|
-
outLine(result.message
|
|
289
|
-
? `Run ${result.run_id} cancelled (${result.message}).`
|
|
290
|
-
: `Run ${result.run_id} cancelled.`);
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
outLine(`Run ${result.run_id} already finished (${result.status}).`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
out(result);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch (e) {
|
|
301
|
-
handleApiError(e);
|
|
302
|
-
}
|
|
303
|
-
});
|
|
305
|
+
.action(runsCancelAction);
|
|
304
306
|
}
|
package/dist/lib/auth.js
CHANGED
|
@@ -179,6 +179,20 @@ export function clearConnectContext(client) {
|
|
|
179
179
|
errLine(`Warning: could not clear connect context: ${e instanceof Error ? e.message : String(e)}`);
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
+
function resolveProfilePath(shell, platform) {
|
|
183
|
+
const home = os.homedir();
|
|
184
|
+
const macBashProfile = ['.bash_profile', '.bash_login', '.profile']
|
|
185
|
+
.map((name) => path.join(home, name)) // nosemgrep: path-join-resolve-traversal -- fixed filenames under os.homedir()
|
|
186
|
+
.find((candidate) => fs.existsSync(candidate)) ?? path.join(home, '.bash_profile');
|
|
187
|
+
if (shell === 'zsh')
|
|
188
|
+
return path.join(home, '.zshrc');
|
|
189
|
+
if (shell === 'bash') {
|
|
190
|
+
return platform === 'darwin' ? macBashProfile : path.join(home, '.bashrc');
|
|
191
|
+
}
|
|
192
|
+
if (shell === 'fish')
|
|
193
|
+
return path.join(home, '.config', 'fish', 'config.fish');
|
|
194
|
+
return path.join(home, '.profile');
|
|
195
|
+
}
|
|
182
196
|
export function persistApiKeyToEnvironment(apiKey, options = {}) {
|
|
183
197
|
const platform = options.platform ?? process.platform;
|
|
184
198
|
if (platform === 'win32') {
|
|
@@ -190,21 +204,9 @@ export function persistApiKeyToEnvironment(apiKey, options = {}) {
|
|
|
190
204
|
}
|
|
191
205
|
return;
|
|
192
206
|
}
|
|
193
|
-
const home = os.homedir();
|
|
194
207
|
const shell = path.basename(options.shell ?? process.env['SHELL'] ?? '');
|
|
195
208
|
const fish = shell === 'fish';
|
|
196
|
-
const
|
|
197
|
-
.map((name) => path.join(home, name)) // nosemgrep: path-join-resolve-traversal -- fixed filenames under os.homedir()
|
|
198
|
-
.find((candidate) => fs.existsSync(candidate)) ?? path.join(home, '.bash_profile');
|
|
199
|
-
const profile = shell === 'zsh'
|
|
200
|
-
? path.join(home, '.zshrc')
|
|
201
|
-
: shell === 'bash'
|
|
202
|
-
? platform === 'darwin'
|
|
203
|
-
? macBashProfile
|
|
204
|
-
: path.join(home, '.bashrc')
|
|
205
|
-
: fish
|
|
206
|
-
? path.join(home, '.config', 'fish', 'config.fish')
|
|
207
|
-
: path.join(home, '.profile');
|
|
209
|
+
const profile = resolveProfilePath(shell, platform);
|
|
208
210
|
const quotedKey = `'${apiKey.replaceAll("'", "'\\''")}'`;
|
|
209
211
|
const prefix = fish ? `set -gx ${TINYFISH_API_KEY_VAR} ` : `export ${TINYFISH_API_KEY_VAR}=`;
|
|
210
212
|
const assignment = `${prefix}${quotedKey}`;
|
|
@@ -12,6 +12,7 @@ export type HoistedCliInstall = {
|
|
|
12
12
|
durationMs: number;
|
|
13
13
|
} | {
|
|
14
14
|
ok: false;
|
|
15
|
+
durationMs: number;
|
|
15
16
|
error: Error;
|
|
16
17
|
};
|
|
17
18
|
/** Every harness reads the outcome; only the first take reports it. */
|
|
@@ -19,9 +20,7 @@ export type TakeHoistedCliInstall = () => {
|
|
|
19
20
|
outcome: HoistedCliInstall;
|
|
20
21
|
firstTake: boolean;
|
|
21
22
|
};
|
|
22
|
-
|
|
23
|
-
export declare class HoistedFailureReportedError extends Error {
|
|
24
|
-
}
|
|
23
|
+
export { HoistedFailureReportedError } from './connect-runtime.js';
|
|
25
24
|
export declare function captureStdio(verbose: boolean): {
|
|
26
25
|
stdio: "inherit";
|
|
27
26
|
timeout: number;
|
package/dist/lib/cli-install.js
CHANGED
|
@@ -7,9 +7,7 @@ export const SKILL_INSTALL_TIMEOUT_MS = 120_000;
|
|
|
7
7
|
const TINYFISH_CLI_INSTALL_SPEC = `${TINYFISH_CLI_PACKAGE}@latest`;
|
|
8
8
|
// spawnSync caps piped output at 1 MiB by default.
|
|
9
9
|
export const STEP_MAX_BUFFER = 10 * 1024 * 1024;
|
|
10
|
-
|
|
11
|
-
export class HoistedFailureReportedError extends Error {
|
|
12
|
-
}
|
|
10
|
+
export { HoistedFailureReportedError } from './connect-runtime.js';
|
|
13
11
|
export function captureStdio(verbose) {
|
|
14
12
|
return verbose
|
|
15
13
|
? { stdio: 'inherit', timeout: SKILL_INSTALL_TIMEOUT_MS }
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -7,6 +7,7 @@ import { HARNESS_CAPABILITIES, NATIVE_BY_HARNESS, openclawSkillUninstall, } from
|
|
|
7
7
|
import { createStdinPrompt, runCliFallback } from './connect-fallback.js';
|
|
8
8
|
import { ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, spawnRemoval, } from './connect-runtime.js';
|
|
9
9
|
import { pickHarnesses } from './connect-picker.js';
|
|
10
|
+
import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
|
|
10
11
|
import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, RELOAD_ACTION, } from './harness-detect.js';
|
|
11
12
|
import { detectHumanInitiated } from './harness.js';
|
|
12
13
|
import { emitNotice } from './notice.js';
|
|
@@ -84,7 +85,11 @@ function runHoistedCliInstall(verbose) {
|
|
|
84
85
|
catch (error) {
|
|
85
86
|
if (error instanceof ConnectInterruptedError)
|
|
86
87
|
return undefined;
|
|
87
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
91
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
92
|
+
};
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
async function verifyHarness(harness, mcpUrl, keyed, apiKey) {
|
|
@@ -100,6 +105,10 @@ async function verifyHarness(harness, mcpUrl, keyed, apiKey) {
|
|
|
100
105
|
function planText(harness, mcpUrl, apiKey) {
|
|
101
106
|
if (harness === 'cursor')
|
|
102
107
|
return planCursorWrite(mcpUrl, apiKey);
|
|
108
|
+
// Only the keyed path seeds the .env; a keyless Hermes install writes nothing.
|
|
109
|
+
if (harness === 'hermes' && apiKey) {
|
|
110
|
+
return `would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ${HERMES_KEY_VAR} to Hermes' .env)`;
|
|
111
|
+
}
|
|
103
112
|
return `would run \`tinyfish connect ${harness}\` (harness-owned MCP write; no local file touched by the CLI)`;
|
|
104
113
|
}
|
|
105
114
|
function uninstallPlanText(harness) {
|
|
@@ -109,7 +118,9 @@ function uninstallPlanText(harness) {
|
|
|
109
118
|
if (!uninstallRuns(harness)) {
|
|
110
119
|
return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
|
|
111
120
|
}
|
|
112
|
-
|
|
121
|
+
// Hermes alone leaves a seeded key behind; `mcp remove` never clears it.
|
|
122
|
+
const envNote = harness === 'hermes' ? ` and clear ${HERMES_KEY_VAR} from Hermes' .env` : '';
|
|
123
|
+
return `would run \`${uninstallPointer(harness)}\`${envNote}`;
|
|
113
124
|
}
|
|
114
125
|
/** Undefined: no harness-CLI removal exists (opencode). */
|
|
115
126
|
function uninstallRuns(harness) {
|
|
@@ -132,7 +143,46 @@ function uninstallPointer(harness) {
|
|
|
132
143
|
const joiner = process.platform === 'win32' ? ' && ' : ' ; ';
|
|
133
144
|
return removal.runs.map((args) => `${removal.command} ${args.join(' ')}`).join(joiner);
|
|
134
145
|
}
|
|
146
|
+
/** `mcp remove` leaves this behind; a stale value gets silently reused. */
|
|
147
|
+
function clearHermesEnvKey() {
|
|
148
|
+
const home = resolveHermesHome();
|
|
149
|
+
const retry = 're-run `tinyfish connect --all --uninstall`';
|
|
150
|
+
if (!home) {
|
|
151
|
+
return (`could not locate Hermes' home, so ${HERMES_KEY_VAR} may still be set in its .env. ` +
|
|
152
|
+
`A stale key is reused by the next install: delete that line, or ${retry}.`);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
removeHermesKey(home);
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
160
|
+
return (`could not clear ${HERMES_KEY_VAR} from ${hermesEnvPath(home)} (${detail}). ` +
|
|
161
|
+
`A stale key is reused by the next install: delete that line, or ${retry}.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Kept on purpose: the entry survives and still interpolates this variable. */
|
|
165
|
+
function hermesKeyKeptAdvisory() {
|
|
166
|
+
return (`left ${HERMES_KEY_VAR} in Hermes' .env because the entry was not removed. ` +
|
|
167
|
+
'Run the command above, then re-run `tinyfish connect --all --uninstall` to clear it.');
|
|
168
|
+
}
|
|
135
169
|
function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
|
|
170
|
+
const result = removeHarnessRegistration(harness, configPath, verbose, binaryOnPath);
|
|
171
|
+
if (harness !== 'hermes')
|
|
172
|
+
return result;
|
|
173
|
+
// Clearing a key the entry still references would break a working install.
|
|
174
|
+
const removed = !result.removalFailed &&
|
|
175
|
+
(result.outcome === 'uninstalled' || result.outcome === 'uninstall_noop');
|
|
176
|
+
const advisory = removed ? clearHermesEnvKey() : hermesKeyKeptAdvisory();
|
|
177
|
+
if (!advisory)
|
|
178
|
+
return result;
|
|
179
|
+
// Appended, never replacing: an advisory must not hide the removal's message.
|
|
180
|
+
return {
|
|
181
|
+
...result,
|
|
182
|
+
errorMessage: result.errorMessage ? `${result.errorMessage} ${advisory}` : advisory,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function removeHarnessRegistration(harness, configPath, verbose, binaryOnPath) {
|
|
136
186
|
const detection = { harness, detected: true, configPath, installed: false };
|
|
137
187
|
if (harness !== 'cursor') {
|
|
138
188
|
const removal = uninstallRuns(harness);
|
|
@@ -165,6 +215,7 @@ function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
|
|
|
165
215
|
return {
|
|
166
216
|
...detection,
|
|
167
217
|
outcome: 'uninstalled',
|
|
218
|
+
removalFailed: true,
|
|
168
219
|
errorMessage: `could not run \`${removal.command} ${args.join(' ')}\` (${result.error.message}) — nothing removed.`,
|
|
169
220
|
fixCommand: uninstallPointer(harness),
|
|
170
221
|
};
|
|
@@ -209,6 +260,7 @@ function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
|
|
|
209
260
|
return {
|
|
210
261
|
...detection,
|
|
211
262
|
outcome: 'uninstalled',
|
|
263
|
+
removalFailed: true,
|
|
212
264
|
errorMessage: `could not update Cursor's mcp.json (${result.error}) — nothing removed.`,
|
|
213
265
|
fixCommand: 'Fix ~/.cursor/mcp.json by hand, then re-run: tinyfish connect --all --uninstall',
|
|
214
266
|
};
|
|
@@ -377,9 +429,12 @@ function summaryLine(result, uninstall) {
|
|
|
377
429
|
return `… ${name} — no tinyfish entry found, nothing removed`;
|
|
378
430
|
}
|
|
379
431
|
if (result.outcome === 'uninstalled') {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
432
|
+
if (!result.errorMessage)
|
|
433
|
+
return `✓ ${name} — removed`;
|
|
434
|
+
// The removal itself worked; the note is advice, not a failed removal.
|
|
435
|
+
if (!result.removalFailed)
|
|
436
|
+
return `⚠ ${name} — removed, but ${result.errorMessage}`;
|
|
437
|
+
return `✗ ${name} — ${result.errorMessage} Fix: ${result.fixCommand}`;
|
|
383
438
|
}
|
|
384
439
|
if (result.outcome === 'interrupted') {
|
|
385
440
|
return `✗ ${name} — interrupted before finishing. Fix: ${result.fixCommand}`;
|
|
@@ -457,7 +512,7 @@ function computeExitCode(results) {
|
|
|
457
512
|
if (results.some((r) => r.outcome === 'interrupted'))
|
|
458
513
|
return 130;
|
|
459
514
|
// A removal that could not run is a failure, not a silent success.
|
|
460
|
-
if (results.some((r) => r.
|
|
515
|
+
if (results.some((r) => r.removalFailed))
|
|
461
516
|
return 1;
|
|
462
517
|
const detected = results.filter(actionable);
|
|
463
518
|
if (detected.length === 0)
|
|
@@ -18,6 +18,22 @@ export declare const CURSOR_SKILL_TARGET: {
|
|
|
18
18
|
readonly skillAgent: "cursor";
|
|
19
19
|
readonly displayName: "Cursor";
|
|
20
20
|
};
|
|
21
|
+
/** The harness reads the key from its own store. */
|
|
22
|
+
export interface SeededInstall {
|
|
23
|
+
/** Pins the add child at the home the key landed in. */
|
|
24
|
+
env: typeof process.env;
|
|
25
|
+
/** Answers prompts `mcp add` still asks once the key is seeded. */
|
|
26
|
+
stdinInput: string;
|
|
27
|
+
/** Names the file it wrote, never the value. */
|
|
28
|
+
note: string;
|
|
29
|
+
/** Undoes the seed, carrying the value it replaced. */
|
|
30
|
+
rollback: () => void;
|
|
31
|
+
/** `mcp add` exits 0 even having saved nothing; the code lies. */
|
|
32
|
+
confirmRegistered: () => {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
detail?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
21
37
|
export interface NativeMcpClient extends SupportedCommand {
|
|
22
38
|
connectClient: Exclude<AgentClient, 'openclaw'>;
|
|
23
39
|
/** Omitted where the `skills` CLI has no agent for this harness (Grok ships its own plugin). */
|
|
@@ -29,6 +45,8 @@ export interface NativeMcpClient extends SupportedCommand {
|
|
|
29
45
|
/** Which variable the client reads the key from. */
|
|
30
46
|
envVar?: string;
|
|
31
47
|
addArgs: (mcpUrl: string, apiKey: string) => string[];
|
|
48
|
+
/** Writes into the harness's store, so connect verifies the key first. */
|
|
49
|
+
seedKey?: (apiKey: string) => SeededInstall;
|
|
32
50
|
};
|
|
33
51
|
/** Shown when a keyless install leaves sign-in to the harness. */
|
|
34
52
|
signInHint?: string;
|
|
@@ -61,6 +79,11 @@ export declare const CLAUDE_CODE: NativeMcpClient;
|
|
|
61
79
|
export declare function openExternalUrl(url: string): ReturnType<typeof spawn.sync>;
|
|
62
80
|
export declare const CODEX: NativeMcpClient;
|
|
63
81
|
export declare const GROK: NativeMcpClient;
|
|
82
|
+
/** `mcp add` exits 0 after saving nothing, or a disabled entry. */
|
|
83
|
+
export declare function hermesRegistrationEnabled(home: string): {
|
|
84
|
+
ok: boolean;
|
|
85
|
+
detail?: string;
|
|
86
|
+
};
|
|
64
87
|
export declare const HERMES: NativeMcpClient;
|
|
65
88
|
export declare const OPENCODE: NativeMcpClient;
|
|
66
89
|
/** Every native MCP client. Derive from this, so a new harness cannot be missed. */
|