@tiny-fish/cli 0.39.1-next.309 → 0.40.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.
Files changed (51) hide show
  1. package/dist/commands/connect.d.ts +2 -14
  2. package/dist/commands/connect.js +109 -75
  3. package/dist/commands/doctor.js +3 -1
  4. package/dist/commands/profile.d.ts +0 -3
  5. package/dist/commands/profile.js +7 -22
  6. package/dist/commands/run.js +20 -40
  7. package/dist/commands/runs.js +24 -58
  8. package/dist/lib/auth.d.ts +1 -3
  9. package/dist/lib/auth.js +1 -1
  10. package/dist/lib/claude-config.d.ts +0 -4
  11. package/dist/lib/claude-config.js +4 -4
  12. package/dist/lib/cli-install.d.ts +0 -1
  13. package/dist/lib/cli-install.js +0 -1
  14. package/dist/lib/client.js +82 -147
  15. package/dist/lib/connect-all-auth.js +1 -1
  16. package/dist/lib/connect-all-summary.js +4 -1
  17. package/dist/lib/connect-all-uninstall.js +12 -1
  18. package/dist/lib/connect-all.d.ts +4 -2
  19. package/dist/lib/connect-all.js +21 -32
  20. package/dist/lib/connect-clients.d.ts +11 -8
  21. package/dist/lib/connect-clients.js +53 -13
  22. package/dist/lib/connect-fallback.d.ts +2 -1
  23. package/dist/lib/connect-picker.d.ts +3 -2
  24. package/dist/lib/connect-runtime.js +4 -6
  25. package/dist/lib/doctor-checks.d.ts +1 -0
  26. package/dist/lib/doctor-checks.js +38 -3
  27. package/dist/lib/doctor-report.d.ts +6 -0
  28. package/dist/lib/harness-detect.d.ts +2 -0
  29. package/dist/lib/harness-detect.js +11 -3
  30. package/dist/lib/harness-spec.d.ts +20 -39
  31. package/dist/lib/harness-spec.js +12 -31
  32. package/dist/lib/harness.js +2 -0
  33. package/dist/lib/hermes-config.d.ts +3 -0
  34. package/dist/lib/hermes-config.js +8 -4
  35. package/dist/lib/hermes-env.d.ts +7 -2
  36. package/dist/lib/hermes-env.js +7 -6
  37. package/dist/lib/hermes-plugin.d.ts +4 -0
  38. package/dist/lib/hermes-plugin.js +17 -9
  39. package/dist/lib/output.d.ts +2 -0
  40. package/dist/lib/output.js +9 -0
  41. package/dist/lib/pi-config.d.ts +26 -0
  42. package/dist/lib/pi-config.js +111 -0
  43. package/dist/lib/registration-detect.d.ts +0 -1
  44. package/dist/lib/registration-detect.js +60 -81
  45. package/dist/lib/setup-telemetry.d.ts +13 -7
  46. package/dist/lib/setup-telemetry.js +5 -5
  47. package/dist/lib/skill-install.d.ts +1 -0
  48. package/dist/lib/skill-install.js +43 -44
  49. package/dist/lib/types.d.ts +4 -3
  50. package/dist/program.js +2 -3
  51. package/package.json +1 -2
@@ -1,3 +1,4 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
1
2
  import { RunStatus } from '@tiny-fish/sdk';
2
3
  import { getApiKey } from '../lib/auth.js';
3
4
  import { cancelRun, getRun, getRunSteps, listRuns } from '../lib/client.js';
@@ -16,6 +17,7 @@ function formatStep(step, index) {
16
17
  const meta = [step.status, step.duration].filter(Boolean).join(', ');
17
18
  return `${String(index + 1).padStart(2, ' ')}. ${action}${meta ? ` (${meta})` : ''}`;
18
19
  }
20
+ const MAX_TIMER_MS = 2 ** 31 - 1;
19
21
  function parsePositiveInt(value, flag, defaultMs, minMs = 1) {
20
22
  if (value === undefined)
21
23
  return defaultMs;
@@ -27,56 +29,24 @@ function parsePositiveInt(value, flag, defaultMs, minMs = 1) {
27
29
  process.exit(1);
28
30
  }
29
31
  const parsed = Number(value);
30
- if (parsed < minMs) {
31
- err({ error: `Invalid ${flag} "${value}". Must be an integer >= ${minMs}ms.` });
32
+ // Node timers take a signed 32-bit delay; larger overflows to 1ms or throws.
33
+ if (parsed < minMs || parsed > MAX_TIMER_MS) {
34
+ err({
35
+ error: `Invalid ${flag} "${value}". Must be an integer between ${minMs} and ${MAX_TIMER_MS}ms.`,
36
+ });
32
37
  process.exit(1);
33
38
  }
34
39
  return parsed;
35
40
  }
36
- function withDeadline(promise, deadline, signal) {
37
- return new Promise((resolve, reject) => {
38
- const remaining = deadline - Date.now();
39
- if (remaining <= 0) {
40
- reject(new Error('watch deadline exceeded'));
41
- return;
42
- }
43
- if (signal.aborted) {
44
- reject(new Error('watch aborted'));
45
- return;
46
- }
47
- const timer = setTimeout(() => reject(new Error('watch deadline exceeded')), remaining);
48
- const onAbort = () => {
49
- clearTimeout(timer);
50
- reject(new Error('watch aborted'));
51
- };
52
- signal.addEventListener('abort', onAbort, { once: true });
53
- promise.then((v) => {
54
- clearTimeout(timer);
55
- signal.removeEventListener('abort', onAbort);
56
- resolve(v);
57
- }, (e) => {
58
- clearTimeout(timer);
59
- signal.removeEventListener('abort', onAbort);
60
- reject(e);
61
- });
62
- });
63
- }
64
- function sleep(ms, signal) {
65
- return new Promise((resolve, reject) => {
66
- if (signal.aborted) {
67
- reject(new Error('aborted'));
68
- return;
69
- }
70
- const timer = setTimeout(() => {
71
- signal.removeEventListener('abort', onAbort);
72
- resolve();
73
- }, ms);
74
- const onAbort = () => {
75
- clearTimeout(timer);
76
- reject(new Error('aborted'));
77
- };
78
- signal.addEventListener('abort', onAbort, { once: true });
41
+ /** The SDK calls take no signal, so the race is the only cancel. */
42
+ function withSignal(promise, signal) {
43
+ if (signal.aborted)
44
+ return Promise.reject(signal.reason);
45
+ const done = new AbortController();
46
+ const aborted = new Promise((_, reject) => {
47
+ signal.addEventListener('abort', () => reject(signal.reason), { signal: done.signal });
79
48
  });
49
+ return Promise.race([promise, aborted]).finally(() => done.abort());
80
50
  }
81
51
  async function runsListAction(opts) {
82
52
  // Validate --status
@@ -193,18 +163,20 @@ async function runsWatchAction(runId, opts) {
193
163
  };
194
164
  process.once('SIGINT', onSigint);
195
165
  const deadline = Date.now() + timeoutMs;
166
+ const timedOut = AbortSignal.timeout(timeoutMs);
167
+ const signal = AbortSignal.any([controller.signal, timedOut]);
196
168
  const seen = new Set();
197
169
  try {
198
- const initial = await withDeadline(getRun(runId, apiKey), deadline, controller.signal);
170
+ const initial = await withSignal(getRun(runId, apiKey), signal);
199
171
  if (TERMINAL_STATUSES.includes(initial.status)) {
200
172
  err({
201
173
  error: `Run ${runId} is already ${initial.status}; nothing to watch. Use 'tinyfish agent run steps ${runId}' for the trace.`,
202
174
  });
203
175
  process.exit(1);
204
176
  }
205
- while (!controller.signal.aborted) {
177
+ while (!signal.aborted) {
206
178
  // 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);
179
+ const tick = await withSignal(getRunSteps(runId, apiKey), signal);
208
180
  let nextIndex = seen.size;
209
181
  for (const step of tick.steps) {
210
182
  if (seen.has(step.id))
@@ -228,22 +200,16 @@ async function runsWatchAction(runId, opts) {
228
200
  err({ error: `Watch timed out after ${timeoutMs}ms (run still ${tick.status}).` });
229
201
  process.exit(1);
230
202
  }
231
- try {
232
- await sleep(intervalMs, controller.signal);
233
- }
234
- catch {
235
- return; // aborted
236
- }
203
+ await sleep(intervalMs, undefined, { signal });
237
204
  }
238
205
  }
239
206
  catch (e) {
240
- if (e instanceof Error && e.message === 'watch deadline exceeded') {
207
+ if (timedOut.aborted) {
241
208
  err({ error: `Watch timed out after ${timeoutMs}ms.` });
242
209
  process.exit(1);
243
210
  }
244
- if (e instanceof Error && e.message === 'watch aborted') {
245
- return; // SIGINT handler already triggered process.exit(130)
246
- }
211
+ if (controller.signal.aborted)
212
+ return; // SIGINT handler already exited 130
247
213
  handleApiError(e);
248
214
  }
249
215
  finally {
@@ -6,7 +6,7 @@ export declare const CONNECT_SOURCE = "tinyfish_cli";
6
6
  export declare const CONNECT_ATTEMPT_ENV = "TINYFISH_CONNECT_ATTEMPT_ID";
7
7
  /** Mirrors ConnectAuthMode; duplicated to keep auth.ts free of connect-runtime imports. */
8
8
  export type RecordedAuthMode = 'api-key' | 'oauth' | 'deferred';
9
- export interface ConnectEntry {
9
+ interface ConnectEntry {
10
10
  attempt_id: string;
11
11
  auth_mode?: RecordedAuthMode;
12
12
  }
@@ -43,8 +43,6 @@ interface EnvironmentOptions {
43
43
  }
44
44
  export declare function persistApiKeyToEnvironment(apiKey: string, options?: EnvironmentOptions): void;
45
45
  export declare function clearConfig(): boolean;
46
- /** The one rule that is ours to enforce: the key becomes an HTTP header, so CR/LF cannot pass. */
47
- export declare function isHeaderSafe(key: string): boolean;
48
46
  export declare function validateKeyFormat(key: string): boolean;
49
47
  export declare function maskKey(key: string): string;
50
48
  export type KeySource = 'explicit' | 'env' | 'config' | 'none';
package/dist/lib/auth.js CHANGED
@@ -236,7 +236,7 @@ export function clearConfig() {
236
236
  }
237
237
  }
238
238
  /** The one rule that is ours to enforce: the key becomes an HTTP header, so CR/LF cannot pass. */
239
- export function isHeaderSafe(key) {
239
+ function isHeaderSafe(key) {
240
240
  for (let i = 0; i < key.length; i += 1) {
241
241
  if (key.charCodeAt(i) < 0x20)
242
242
  return false;
@@ -1,17 +1,13 @@
1
- export declare function claudeDir(): string;
2
1
  export declare function claudeSettingsPath(): string;
3
2
  export declare function claudeMdPath(): string;
4
3
  export declare const TINYFISH_PERMISSION = "Bash(tinyfish:*)";
5
4
  export declare const CLAUDE_MD_MARKER = "<!-- tinyfish-configure-claude -->";
6
- export declare const CLAUDE_MD_BLOCK = "<!-- tinyfish-configure-claude -->\n## WebSearch or Search\nWhen looking to use Search or WebSearch, always use the TinyFish Web Search tool, run `tinyfish search query \"<query>\"`\nOnly fall back to native Search when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)\n\n## WebFetch or Fetch\nWhen looking to use Fetch or WebFetch, always use the TinyFish Fetch tool, run `tinyfish fetch content get \"<url>\"`\nOnly fall back to native Fetch when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)\n<!-- tinyfish-configure-claude -->";
7
5
  export declare function readSettingsJson(): Record<string, unknown>;
8
6
  export declare function writeSettingsJson(settings: Record<string, unknown>): void;
9
7
  export declare function mergeSettings(existing: Record<string, unknown>): Record<string, unknown>;
10
8
  export declare function removeFromSettings(existing: Record<string, unknown>): Record<string, unknown>;
11
9
  export declare function readClaudeMd(): string;
12
10
  export declare function writeClaudeMd(content: string): void;
13
- export declare function hasMarker(content: string): boolean;
14
- export declare function hasLegacyBlock(content: string): boolean;
15
11
  export declare function mergeClaudeMd(existing: string): {
16
12
  content: string;
17
13
  replaced: boolean;
@@ -2,7 +2,7 @@ import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  // ─── Path helpers ─────────────────────────────────────────────────────────────
5
- export function claudeDir() {
5
+ function claudeDir() {
6
6
  return path.join(os.homedir(), '.claude');
7
7
  }
8
8
  export function claudeSettingsPath() {
@@ -40,7 +40,7 @@ const TINYFISH_SUBAGENT_HOOK = {
40
40
  ],
41
41
  };
42
42
  export const CLAUDE_MD_MARKER = '<!-- tinyfish-configure-claude -->';
43
- export const CLAUDE_MD_BLOCK = `${CLAUDE_MD_MARKER}
43
+ const CLAUDE_MD_BLOCK = `${CLAUDE_MD_MARKER}
44
44
  ## WebSearch or Search
45
45
  When looking to use Search or WebSearch, always use the TinyFish Web Search tool, run \`tinyfish search query "<query>"\`
46
46
  Only fall back to native Search when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)
@@ -159,10 +159,10 @@ function escapeRegExp(s) {
159
159
  }
160
160
  const MARKER_REGEX = new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}`, 'm');
161
161
  const LEGACY_REGEX = /## Web(?:Search|Seach) or Search[\s\S]*?(?:tinyfish fetch[^\n]*\n(?:[^\n]*tinyfish[^\n]*\n)*[^\n]*)\n*/m;
162
- export function hasMarker(content) {
162
+ function hasMarker(content) {
163
163
  return content.includes(CLAUDE_MD_MARKER);
164
164
  }
165
- export function hasLegacyBlock(content) {
165
+ function hasLegacyBlock(content) {
166
166
  return !content.includes(CLAUDE_MD_MARKER) && LEGACY_REGEX.test(content);
167
167
  }
168
168
  export function mergeClaudeMd(existing) {
@@ -20,7 +20,6 @@ export type TakeHoistedCliInstall = () => {
20
20
  outcome: HoistedCliInstall;
21
21
  firstTake: boolean;
22
22
  };
23
- export { HoistedFailureReportedError } from './connect-runtime.js';
24
23
  export declare function captureStdio(verbose: boolean): {
25
24
  stdio: "inherit";
26
25
  timeout: number;
@@ -7,7 +7,6 @@ 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
- export { HoistedFailureReportedError } from './connect-runtime.js';
11
10
  export function captureStdio(verbose) {
12
11
  return verbose
13
12
  ? { stdio: 'inherit', timeout: SKILL_INSTALL_TIMEOUT_MS }
@@ -108,6 +108,9 @@ function createSdkClient(apiKey, timeout, baseUrl) {
108
108
  ...(timeout !== undefined ? { timeout } : {}),
109
109
  });
110
110
  }
111
+ function sdk(apiKey, call) {
112
+ return call(createSdkClient(apiKey)).catch(rethrowSdkError);
113
+ }
111
114
  function rethrowSdkError(error) {
112
115
  if (error instanceof APIStatusError) {
113
116
  throw new ApiError(error.statusCode, error.message);
@@ -136,14 +139,8 @@ const batchGetResponseSchema = z.object({
136
139
  data: z.array(runSchema),
137
140
  not_found: z.array(z.string()).nullable(),
138
141
  });
139
- const batchCancelResultSchema = z.object({
140
- run_id: z.string(),
141
- status: z.string(),
142
- cancelled_at: z.string().nullable(),
143
- message: z.string().nullable(),
144
- });
145
142
  const batchCancelResponseSchema = z.object({
146
- results: z.array(batchCancelResultSchema),
143
+ results: z.array(cancelRunResponseSchema),
147
144
  not_found: z.array(z.string()).nullable(),
148
145
  });
149
146
  const runStepSchema = z.object({
@@ -219,27 +216,21 @@ async function* parseSseStream(stream) {
219
216
  reader.releaseLock();
220
217
  }
221
218
  }
222
- export async function runSync(req, apiKey) {
223
- try {
224
- const response = await createSdkClient(apiKey).post('/v1/automation/run', {
219
+ export function runSync(req, apiKey) {
220
+ return sdk(apiKey, async (client) => {
221
+ const response = await client.post('/v1/automation/run', {
225
222
  json: req,
226
223
  });
227
224
  return parseWithSchema(agentRunResponseSchema, response, 'Invalid agent run response');
228
- }
229
- catch (error) {
230
- rethrowSdkError(error);
231
- }
225
+ });
232
226
  }
233
- export async function runAsync(req, apiKey) {
234
- try {
235
- const response = await createSdkClient(apiKey).post('/v1/automation/run-async', {
227
+ export function runAsync(req, apiKey) {
228
+ return sdk(apiKey, async (client) => {
229
+ const response = await client.post('/v1/automation/run-async', {
236
230
  json: req,
237
231
  });
238
232
  return parseWithSchema(agentRunAsyncResponseSchema, response, 'Invalid async run response');
239
- }
240
- catch (error) {
241
- rethrowSdkError(error);
242
- }
233
+ });
243
234
  }
244
235
  export async function* runStream(req, apiKey, signal) {
245
236
  let stream = null;
@@ -268,110 +259,75 @@ export async function* runStream(req, apiKey, signal) {
268
259
  }
269
260
  }
270
261
  }
271
- export async function listRuns(opts, apiKey, timeout, baseUrl) {
272
- try {
273
- return await createSdkClient(apiKey, timeout, baseUrl).runs.list(opts);
274
- }
275
- catch (error) {
276
- rethrowSdkError(error);
277
- }
262
+ export function listRuns(opts, apiKey, timeout, baseUrl) {
263
+ return createSdkClient(apiKey, timeout, baseUrl).runs.list(opts).catch(rethrowSdkError);
278
264
  }
279
- export async function getRun(runId, apiKey) {
280
- try {
281
- return await createSdkClient(apiKey).runs.get(runId);
282
- }
283
- catch (error) {
284
- rethrowSdkError(error);
285
- }
265
+ export function getRun(runId, apiKey) {
266
+ return sdk(apiKey, (client) => client.runs.get(runId));
286
267
  }
287
- export async function getRunSteps(runId, apiKey) {
288
- try {
268
+ export function getRunSteps(runId, apiKey) {
269
+ return sdk(apiKey, async (client) => {
289
270
  // The public GET /v1/runs/:id route returns the base run payload plus `steps`;
290
271
  // the installed SDK runSchema only models the base run, so steps need this local schema.
291
- const response = await createSdkClient(apiKey).get(`/v1/runs/${encodeURIComponent(runId)}`, { params: { screenshots: 'none' } });
272
+ const response = await client.get(`/v1/runs/${encodeURIComponent(runId)}`, {
273
+ params: { screenshots: 'none' },
274
+ });
292
275
  return parseWithSchema(runStepsResponseSchema, response, 'Invalid run steps response');
293
- }
294
- catch (error) {
295
- rethrowSdkError(error);
296
- }
276
+ });
297
277
  }
298
- export async function searchQuery(params, apiKey) {
299
- try {
300
- const response = await createSdkClient(apiKey).search.query(params);
278
+ export function searchQuery(params, apiKey) {
279
+ return sdk(apiKey, async (client) => {
280
+ const response = await client.search.query(params);
301
281
  return searchQueryResponseSchema.parse(response);
302
- }
303
- catch (error) {
304
- rethrowSdkError(error);
305
- }
282
+ });
306
283
  }
307
- export async function fetchContentGet(params, apiKey) {
308
- try {
309
- const response = await createSdkClient(apiKey).fetch.getContents(params);
284
+ export function fetchContentGet(params, apiKey) {
285
+ return sdk(apiKey, async (client) => {
286
+ const response = await client.fetch.getContents(params);
310
287
  return fetchResponseSchema.parse(response);
311
- }
312
- catch (error) {
313
- rethrowSdkError(error);
314
- }
288
+ });
315
289
  }
316
- export async function browserSessionCreate(params, apiKey) {
317
- try {
318
- const response = await createSdkClient(apiKey).post('/v1/browser', { json: params });
290
+ export function browserSessionCreate(params, apiKey) {
291
+ return sdk(apiKey, async (client) => {
292
+ const response = await client.post('/v1/browser', { json: params });
319
293
  return browserSessionSchema.parse(response);
320
- }
321
- catch (error) {
322
- rethrowSdkError(error);
323
- }
294
+ });
324
295
  }
325
- export async function cancelRun(runId, apiKey) {
326
- try {
327
- const response = await createSdkClient(apiKey).post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, { json: {} });
296
+ export function cancelRun(runId, apiKey) {
297
+ return sdk(apiKey, async (client) => {
298
+ const response = await client.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {
299
+ json: {},
300
+ });
328
301
  return parseWithSchema(cancelRunResponseSchema, response, 'Invalid cancel run response');
329
- }
330
- catch (error) {
331
- rethrowSdkError(error);
332
- }
302
+ });
333
303
  }
334
304
  // ── Wallet ──────────────────────────────────────────────────────────────────
335
- export async function getWallet(apiKey) {
336
- try {
337
- return await createSdkClient(apiKey).wallet.get();
338
- }
339
- catch (error) {
340
- rethrowSdkError(error);
341
- }
305
+ export function getWallet(apiKey) {
306
+ return sdk(apiKey, (client) => client.wallet.get());
342
307
  }
343
- export async function submitBatch(req, apiKey) {
344
- try {
345
- const response = await createSdkClient(apiKey).post('/v1/automation/run-batch', {
308
+ export function submitBatch(req, apiKey) {
309
+ return sdk(apiKey, async (client) => {
310
+ const response = await client.post('/v1/automation/run-batch', {
346
311
  json: req,
347
312
  });
348
313
  return parseWithSchema(batchRunResponseSchema, response, 'Invalid batch run response');
349
- }
350
- catch (error) {
351
- rethrowSdkError(error);
352
- }
314
+ });
353
315
  }
354
- export async function getBatchRuns(runIds, apiKey) {
355
- try {
356
- const response = await createSdkClient(apiKey).post('/v1/runs/batch', {
316
+ export function getBatchRuns(runIds, apiKey) {
317
+ return sdk(apiKey, async (client) => {
318
+ const response = await client.post('/v1/runs/batch', {
357
319
  json: { run_ids: runIds },
358
320
  });
359
321
  return parseWithSchema(batchGetResponseSchema, response, 'Invalid batch get response');
360
- }
361
- catch (error) {
362
- rethrowSdkError(error);
363
- }
322
+ });
364
323
  }
365
- export async function cancelBatchRuns(runIds, apiKey) {
366
- try {
367
- const response = await createSdkClient(apiKey).post('/v1/runs/batch/cancel', {
324
+ export function cancelBatchRuns(runIds, apiKey) {
325
+ return sdk(apiKey, async (client) => {
326
+ const response = await client.post('/v1/runs/batch/cancel', {
368
327
  json: { run_ids: runIds },
369
328
  });
370
329
  return parseWithSchema(batchCancelResponseSchema, response, 'Invalid batch cancel response');
371
- }
372
- catch (error) {
373
- rethrowSdkError(error);
374
- }
330
+ });
375
331
  }
376
332
  const profileCreateResponseSchema = z.object({
377
333
  id: z.string(),
@@ -384,25 +340,19 @@ const profileUploadResponseSchema = z.object({
384
340
  domains_updated: z.array(z.string()),
385
341
  domains_failed: z.array(z.string()).optional(),
386
342
  });
387
- export async function profileCreate(name, apiKey) {
388
- try {
389
- const response = await createSdkClient(apiKey).post('/v1/profiles', {
343
+ export function profileCreate(name, apiKey) {
344
+ return sdk(apiKey, async (client) => {
345
+ const response = await client.post('/v1/profiles', {
390
346
  json: { name },
391
347
  });
392
348
  return parseWithSchema(profileCreateResponseSchema, response, 'Invalid profile create response');
393
- }
394
- catch (error) {
395
- rethrowSdkError(error);
396
- }
349
+ });
397
350
  }
398
- export async function profileUpload(profileId, cookies, apiKey) {
399
- try {
400
- const response = await createSdkClient(apiKey).post(`/v1/profiles/${encodeURIComponent(profileId)}/upload`, { json: { cookies } });
351
+ export function profileUpload(profileId, cookies, apiKey) {
352
+ return sdk(apiKey, async (client) => {
353
+ const response = await client.post(`/v1/profiles/${encodeURIComponent(profileId)}/upload`, { json: { cookies } });
401
354
  return parseWithSchema(profileUploadResponseSchema, response, 'Invalid profile upload response');
402
- }
403
- catch (error) {
404
- rethrowSdkError(error);
405
- }
355
+ });
406
356
  }
407
357
  // ── Vault ───────────────────────────────────────────────────────────────────
408
358
  // /v1/vault/* returns camelCase fields (unlike the snake_case rest of the API).
@@ -451,52 +401,37 @@ const vaultItemsSyncResponseSchema = z.object({
451
401
  removed: z.number(),
452
402
  }),
453
403
  });
454
- export async function vaultConnections(apiKey) {
455
- try {
456
- const response = await createSdkClient(apiKey).get('/v1/vault/connections');
404
+ export function vaultConnections(apiKey) {
405
+ return sdk(apiKey, async (client) => {
406
+ const response = await client.get('/v1/vault/connections');
457
407
  return parseWithSchema(vaultConnectionsListResponseSchema, response, 'Invalid vault connections response');
458
- }
459
- catch (error) {
460
- rethrowSdkError(error);
461
- }
408
+ });
462
409
  }
463
- export async function vaultConnect(req, apiKey) {
464
- try {
465
- const response = await createSdkClient(apiKey).post('/v1/vault/connections', {
410
+ export function vaultConnect(req, apiKey) {
411
+ return sdk(apiKey, async (client) => {
412
+ const response = await client.post('/v1/vault/connections', {
466
413
  json: req,
467
414
  });
468
415
  return parseWithSchema(vaultConnectResponseSchema, response, 'Invalid vault connect response');
469
- }
470
- catch (error) {
471
- rethrowSdkError(error);
472
- }
416
+ });
473
417
  }
474
- export async function vaultDisconnect(connectionId, apiKey) {
475
- try {
476
- const response = await createSdkClient(apiKey).del(`/v1/vault/connections/${encodeURIComponent(connectionId)}`);
418
+ export function vaultDisconnect(connectionId, apiKey) {
419
+ return sdk(apiKey, async (client) => {
420
+ const response = await client.del(`/v1/vault/connections/${encodeURIComponent(connectionId)}`);
477
421
  return parseWithSchema(vaultDisconnectResponseSchema, response, 'Invalid vault disconnect response');
478
- }
479
- catch (error) {
480
- rethrowSdkError(error);
481
- }
422
+ });
482
423
  }
483
- export async function vaultItems(apiKey) {
484
- try {
485
- const response = await createSdkClient(apiKey).get('/v1/vault/items');
424
+ export function vaultItems(apiKey) {
425
+ return sdk(apiKey, async (client) => {
426
+ const response = await client.get('/v1/vault/items');
486
427
  return parseWithSchema(vaultItemsListResponseSchema, response, 'Invalid vault items response');
487
- }
488
- catch (error) {
489
- rethrowSdkError(error);
490
- }
428
+ });
491
429
  }
492
- export async function vaultSync(apiKey) {
493
- try {
494
- const response = await createSdkClient(apiKey).post('/v1/vault/items/sync', {
430
+ export function vaultSync(apiKey) {
431
+ return sdk(apiKey, async (client) => {
432
+ const response = await client.post('/v1/vault/items/sync', {
495
433
  json: {},
496
434
  });
497
435
  return parseWithSchema(vaultItemsSyncResponseSchema, response, 'Invalid vault sync response');
498
- }
499
- catch (error) {
500
- rethrowSdkError(error);
501
- }
436
+ });
502
437
  }
@@ -19,7 +19,7 @@ const NO_BROWSER_REASONS = new Set([
19
19
  // Derived: these block `connect` on a browser sign-in when keyless.
20
20
  const BROWSER_SIGN_IN = new Set(ALL_HARNESSES.filter((harness) => {
21
21
  const native = NATIVE_BY_HARNESS.get(harness);
22
- return !!native?.loginArgs || !!native?.oauthInAdd;
22
+ return !!native?.loginArgs;
23
23
  }));
24
24
  // Derived: these refuse a keyless install, so no sign-in helps.
25
25
  const KEY_REQUIRED = new Set(ALL_HARNESSES.filter((harness) => !!NATIVE_BY_HARNESS.get(harness)?.keyRequired));
@@ -9,7 +9,7 @@ const HIDDEN_ONCE_SOMETHING_WORKED = new Set([
9
9
  ]);
10
10
  /** A stale config dir is a detection signal with nothing behind it. */
11
11
  export function actionable(result) {
12
- return result.detected && result.outcome !== 'stale_config';
12
+ return (result.detected && result.outcome !== 'stale_config' && result.outcome !== 'needs_first_run');
13
13
  }
14
14
  function absentSummaryLine(result, uninstall) {
15
15
  const name = DISPLAY_NAMES[result.harness];
@@ -29,6 +29,9 @@ function skippedSummaryLine(result) {
29
29
  if (result.outcome === 'stale_config') {
30
30
  return `… ${name} — config at ${result.configPath} but no \`${HARNESS_COMMANDS[result.harness]}\` on PATH, skipped. Install ${name}, then run: ${result.fixCommand}`;
31
31
  }
32
+ if (result.outcome === 'needs_first_run') {
33
+ return `… ${name} — installed but never run, so ${result.configPath} does not exist yet, skipped. Run \`${HARNESS_COMMANDS[result.harness]}\` once, then: ${result.fixCommand}`;
34
+ }
32
35
  if (result.outcome === 'not_reached') {
33
36
  return `… ${name} — not reached, the run stopped early. Fix: ${result.fixCommand}`;
34
37
  }
@@ -3,6 +3,7 @@ import { NATIVE_BY_HARNESS, openclawSkillUninstall } from './connect-clients.js'
3
3
  import { ConnectInterruptedError, spawnRemoval } from './connect-runtime.js';
4
4
  import { removeCursorMcpServer, planCursorWrite } from './cursor-config.js';
5
5
  import { planOmpWrite, removeOmpMcpServer } from './omp-config.js';
6
+ import { piMcpPath, planPiWrite, removePiMcpServer } from './pi-config.js';
6
7
  import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
7
8
  import { clearHermesWebBackends, HERMES_PLUGIN_SHA } from './hermes-plugin.js';
8
9
  import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES } from './harness-detect.js';
@@ -14,6 +15,8 @@ export function planText(harness, mcpUrl, apiKey) {
14
15
  return planCursorWrite(mcpUrl, apiKey);
15
16
  if (harness === 'omp')
16
17
  return planOmpWrite(mcpUrl, apiKey);
18
+ if (harness === 'pi')
19
+ return planPiWrite(mcpUrl, apiKey);
17
20
  // Only the keyed path seeds the .env; a keyless Hermes install writes nothing.
18
21
  if (harness === 'hermes' && apiKey) {
19
22
  return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
@@ -29,6 +32,9 @@ export function uninstallPlanText(harness) {
29
32
  if (harness === 'omp') {
30
33
  return "would remove the tinyfish entry from omp's mcp.json (located via `omp config path`)";
31
34
  }
35
+ if (harness === 'pi') {
36
+ return `would remove the tinyfish entry from ${piMcpPath()}`;
37
+ }
32
38
  if (!uninstallRuns(harness)) {
33
39
  return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
34
40
  }
@@ -62,7 +68,8 @@ function uninstallPointer(harness) {
62
68
  }
63
69
  /** `mcp remove` clears neither the key nor the backends we wrote. */
64
70
  function clearHermesLocalState() {
65
- const home = resolveHermesHome();
71
+ const resolved = resolveHermesHome();
72
+ const home = typeof resolved === 'string' ? resolved : undefined;
66
73
  const advisories = [clearHermesEnvKey(home), clearHermesBackends(home)].filter((advisory) => advisory !== undefined);
67
74
  return advisories.length > 0 ? advisories.join(' ') : undefined;
68
75
  }
@@ -186,6 +193,10 @@ const MCP_JSON_UNINSTALLS = {
186
193
  remove: removeOmpMcpServer,
187
194
  fix: "Fix omp's mcp.json (at `omp config path`) by hand, then re-run: tinyfish connect --all --uninstall",
188
195
  },
196
+ pi: {
197
+ remove: removePiMcpServer,
198
+ fix: `Fix ${piMcpPath()} by hand, then re-run: tinyfish connect --all --uninstall`,
199
+ },
189
200
  };
190
201
  function uninstallMcpJson(detection, spec) {
191
202
  let result;
@@ -1,6 +1,6 @@
1
- import { type Harness } from './harness-detect.js';
1
+ import { type Harness, type HarnessDetection } from './harness-detect.js';
2
2
  import { type VerifyDepth } from './verify.js';
3
- export type Outcome = 'not_detected' | 'not_installed' | 'stale_config' | 'installed' | 'failed' | 'harness_too_old' | 'interrupted' | 'auth_pending' | 'no_tty_auth_skip' | 'uninstalled' | 'uninstall_noop' | 'uninstall_pointer' | 'dry_run' | 'not_reached' | 'deselected';
3
+ export type Outcome = 'not_detected' | 'not_installed' | 'stale_config' | 'needs_first_run' | 'installed' | 'failed' | 'harness_too_old' | 'interrupted' | 'auth_pending' | 'no_tty_auth_skip' | 'uninstalled' | 'uninstall_noop' | 'uninstall_pointer' | 'dry_run' | 'not_reached' | 'deselected';
4
4
  export type HarnessResultBase = {
5
5
  harness: Harness;
6
6
  detected: boolean;
@@ -34,5 +34,7 @@ export interface ConnectAllOptions {
34
34
  /** Setup-page id riding in on --url; joins "copied the command" to this run. */
35
35
  attemptId?: string;
36
36
  }
37
+ /** The harness has never created its dir, so connecting would build one it does not own. */
38
+ export declare function needsFirstRun(detection: HarnessDetection): boolean;
37
39
  export declare function runConnectAll(opts: ConnectAllOptions): Promise<number>;
38
40
  export declare const FIRST_TASK_PROMPT: string;