@tiny-fish/cli 0.47.2 → 0.47.3-next.380

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.
@@ -1,8 +1,8 @@
1
1
  import { type ConnectAuthMode } from '../connect-runtime.js';
2
2
  import { cursorMcpPath, writeCursorMcpConfig } from './cursor-config.js';
3
3
  import { ompMcpPath, writeOmpMcpConfig } from './omp-config.js';
4
- import { piMcpPath, writePiMcpConfig } from './pi-config.js';
5
- declare function piPostConnectNotes(): string | undefined;
4
+ import { piMcpAdapterState, type PiAdapterState, piMcpPath, writePiMcpConfig } from './pi-config.js';
5
+ declare function piPostConnectNotes(adapterState?: PiAdapterState): string | undefined;
6
6
  declare const CONFIG_FILE_HARNESSES: {
7
7
  cursor: {
8
8
  skillTarget: {
@@ -44,6 +44,7 @@ declare const CONFIG_FILE_HARNESSES: {
44
44
  write: typeof writePiMcpConfig;
45
45
  configPath: typeof piMcpPath;
46
46
  tryLaunch: () => false;
47
+ adapterState: typeof piMcpAdapterState;
47
48
  postConnectNote: typeof piPostConnectNotes;
48
49
  copy: {
49
50
  verified: string;
@@ -7,7 +7,7 @@ import { ConnectStepError, createConnectTelemetry, runGuarded, settle, } from '.
7
7
  import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from './cursor-config.js';
8
8
  import { ompMcpPath, readOmpTinyfishEntry, writeOmpMcpConfig } from './omp-config.js';
9
9
  import { PI_ADAPTER_INSTALL_COMMAND, piMcpAdapterState, piSkillDirMismatch, piMcpPath, readPiTinyfishEntry, writePiMcpConfig, } from './pi-config.js';
10
- import { AuthMode, HARNESS_DISPLAY_NAMES } from '../harness/harness-detect.js';
10
+ import { AuthMode, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES, isStaleConfig, } from '../harness/harness-detect.js';
11
11
  import { detectHumanInitiated } from '../harness.js';
12
12
  import { errLine } from '../output.js';
13
13
  import { verifyMcpAuth } from '../verify.js';
@@ -21,15 +21,14 @@ const PI_ADAPTER_DORMANT_NOTE = 'Note: pi has no built-in MCP support, so the en
21
21
  // Only the dormant case was spoken for, leaving a working MCP install silent.
22
22
  const PI_ADAPTER_LIVE_NOTE = 'pi-mcp-adapter is installed, so pi loads the TinyFish MCP tools as well as the skill once ' +
23
23
  'you restart it.';
24
- function piAdapterNote() {
25
- const state = piMcpAdapterState();
24
+ function piAdapterNote(state) {
26
25
  if (state === 'installed')
27
26
  return PI_ADAPTER_LIVE_NOTE;
28
27
  return state === 'absent' ? PI_ADAPTER_DORMANT_NOTE : undefined;
29
28
  }
30
29
  // The `skills` CLI writes to the default dir even when pi reads another one.
31
- function piPostConnectNotes() {
32
- const notes = [piAdapterNote()];
30
+ function piPostConnectNotes(adapterState) {
31
+ const notes = [piAdapterNote(adapterState)];
33
32
  const skills = piSkillDirMismatch();
34
33
  if (skills) {
35
34
  notes.push(`Note: PI_CODING_AGENT_DIR points pi at ${skills.readFrom}, but the TinyFish skill ` +
@@ -79,6 +78,7 @@ const CONFIG_FILE_HARNESSES = {
79
78
  configPath: piMcpPath,
80
79
  // No install deeplink exists; the copy carries the restart instead.
81
80
  tryLaunch: () => false,
81
+ adapterState: piMcpAdapterState,
82
82
  postConnectNote: piPostConnectNotes,
83
83
  copy: {
84
84
  verified: "TinyFish is connected and verified (health+auth). Restart pi and it's live.",
@@ -111,6 +111,36 @@ function reportPostInstallFailure(harness, failed, onPostInstallFailed) {
111
111
  errLine(finishSetupHint(harness));
112
112
  onPostInstallFailed?.();
113
113
  }
114
+ // A dormant config written here reads as `completed` to every dashboard.
115
+ function requireHarnessInstalled(harness) {
116
+ const detection = detectInstalledHarnesses().find((d) => d.harness === harness);
117
+ if (detection?.detected && !isStaleConfig(detection))
118
+ return;
119
+ throw new ConnectStepError(`${HARNESS_DISPLAY_NAMES[harness]} is not installed (no \`${HARNESS_COMMANDS[harness]}\` on PATH). ` +
120
+ `Install it, then re-run: tinyfish connect ${harness}`, 'harness_not_installed');
121
+ }
122
+ /** The copy after `completed`: what the user does next depends on the auth path. */
123
+ async function printConnectedCopy(spec, displayName, opts) {
124
+ if (opts.keyless) {
125
+ errLine(`TinyFish keyless Search is connected in ${displayName}.`);
126
+ }
127
+ else if (opts.resolvedKey) {
128
+ // Deeplink would embed the key in a URL (process args, LaunchServices logs) — reload instead.
129
+ const verify = await verifyMcpAuth(opts.resolvedKey, apiBaseFromMcpUrl(opts.apiMcpUrl));
130
+ if (verify.ok) {
131
+ errLine(spec.copy.verified);
132
+ }
133
+ else {
134
+ errLine(spec.copy.authFailed(verify.reason ?? 'unknown'));
135
+ }
136
+ }
137
+ else if (detectHumanInitiated() && spec.tryLaunch(opts.mcpUrl.toString())) {
138
+ errLine(spec.copy.launched);
139
+ }
140
+ else {
141
+ errLine(spec.copy.reload);
142
+ }
143
+ }
114
144
  /** Writes the harness's MCP config file itself; no `mcp add` exists. */
115
145
  export async function connectConfigFileHarness(harness, options) {
116
146
  const spec = CONFIG_FILE_HARNESSES[harness];
@@ -154,6 +184,7 @@ export async function connectConfigFileHarness(harness, options) {
154
184
  };
155
185
  await runGuarded(state, telemetry, async () => {
156
186
  telemetry.track('started');
187
+ requireHarnessInstalled(harness);
157
188
  telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
158
189
  const resolveAuth = async () => {
159
190
  let resolvedKey = validatedApiKey(options.apiKey);
@@ -202,28 +233,16 @@ export async function connectConfigFileHarness(harness, options) {
202
233
  saveConnectContext(harness, telemetry.attemptId, AuthMode.Keyless);
203
234
  else
204
235
  saveConnectContext(harness, telemetry.attemptId);
205
- settle(state, telemetry, 'completed', { authMode });
236
+ const piAdapterState = spec.adapterState?.();
237
+ settle(state, telemetry, 'completed', { authMode, piAdapterState });
206
238
  const postInstallFailed = installSkillPostSettle(spec, displayName, state, telemetry, options);
207
- if (keyless) {
208
- errLine(`TinyFish keyless Search is connected in ${displayName}.`);
209
- }
210
- else if (resolvedKey) {
211
- // Deeplink would embed the key in a URL (process args, LaunchServices logs) — reload instead.
212
- const verify = await verifyMcpAuth(resolvedKey, apiBaseFromMcpUrl(options.mcpUrl));
213
- if (verify.ok) {
214
- errLine(spec.copy.verified);
215
- }
216
- else {
217
- errLine(spec.copy.authFailed(verify.reason ?? 'unknown'));
218
- }
219
- }
220
- else if (detectHumanInitiated() && spec.tryLaunch(mcpUrl.toString())) {
221
- errLine(spec.copy.launched);
222
- }
223
- else {
224
- errLine(spec.copy.reload);
225
- }
226
- const note = spec.postConnectNote?.();
239
+ await printConnectedCopy(spec, displayName, {
240
+ keyless,
241
+ resolvedKey,
242
+ mcpUrl,
243
+ apiMcpUrl: options.mcpUrl,
244
+ });
245
+ const note = spec.postConnectNote?.(piAdapterState);
227
246
  if (note)
228
247
  errLine(note);
229
248
  reportPostInstallFailure(harness, postInstallFailed, options.onPostInstallFailed);
@@ -12,7 +12,7 @@ import { KEYLESS_ONBOARDING_PROMPT } from './connect-clients.js';
12
12
  import { gateApiKey } from './connect-preflight.js';
13
13
  import { ConnectInterruptedError } from './connect-runtime.js';
14
14
  import { pickHarnesses } from './connect-picker.js';
15
- import { commandOnPath, detectInstalledHarnesses, AuthMode, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, } from './harness/harness-detect.js';
15
+ import { commandOnPath, detectInstalledHarnesses, isStaleConfig, AuthMode, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, } from './harness/harness-detect.js';
16
16
  import { detectHumanInitiated } from './harness.js';
17
17
  import { emitNotice } from './notice.js';
18
18
  import { errLine, outLine, setErrIndent } from './output.js';
@@ -138,13 +138,6 @@ function noWorkOutcome(d, uninstall) {
138
138
  export function needsFirstRun(detection) {
139
139
  return detection.detected && !detection.configDirExists;
140
140
  }
141
- /** A config-writing connect needs no binary; every other harness spawns one. */
142
- function isStaleConfig(detection) {
143
- const spec = harnessSpec(detection.harness);
144
- // Cursor's IDE reads mcp.json binary-less; omp and pi have no second reader.
145
- const binaryless = spec.cliWritesConfig && spec.readableWithoutBinary;
146
- return detection.detected && !detection.binaryOnPath && !binaryless;
147
- }
148
141
  async function resolvePick(detections, opts, prompt) {
149
142
  let deselected = new Set();
150
143
  let mode = 'all';
@@ -1,5 +1,6 @@
1
1
  import { AuthMode } from './harness/harness-detect.js';
2
2
  import type { Harness, HarnessSupportCheck } from './harness/harness-spec.js';
3
+ import type { PiAdapterState } from './config/pi-config.js';
3
4
  export declare const NON_INTERACTIVE_TIMEOUT_MS = 10000;
4
5
  export type AgentClient = Harness;
5
6
  type ConnectStage = 'started' | 'checkpoint' | 'completed' | 'failed' | 'aborted' | 'post_install_failed';
@@ -75,6 +76,8 @@ interface ConnectStageDetail {
75
76
  stageDurationMs?: number;
76
77
  /** Dropped from the wire unless `failedStage` is set (route refine). */
77
78
  failureDetail?: string;
79
+ /** pi only: without the adapter its MCP entry is dormant. */
80
+ piAdapterState?: PiAdapterState;
78
81
  }
79
82
  export interface ConnectTelemetry {
80
83
  attemptId: string;
@@ -325,6 +325,7 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
325
325
  plugin_version: detail?.pluginVersion,
326
326
  auth_mode: detail?.authMode,
327
327
  harness_degraded: detail?.harnessDegraded,
328
+ pi_adapter_state: detail?.piAdapterState,
328
329
  stage_duration_ms: stageDurationMs,
329
330
  runtime_platform: process.platform,
330
331
  node_version: process.version,
@@ -1,4 +1,4 @@
1
- import type { DoctorCheck } from './doctor-report.js';
1
+ import { type DoctorCheck } from './doctor-report.js';
2
2
  import type { RegistrationStatus } from './registration-detect.js';
3
3
  import { type VerifyResult } from './verify.js';
4
4
  export declare function provesHarnessReach(status: RegistrationStatus, keyAuth: VerifyResult | undefined, healthOk: boolean, mcpUrl: string): boolean;
@@ -1,5 +1,6 @@
1
1
  import { apiKeyStatus } from './auth.js';
2
2
  import { CLI_VERSION } from './constants.js';
3
+ import { NOT_INSTALLED_DETAIL } from './doctor-report.js';
3
4
  import { AuthMode, Registered } from './harness/harness-detect.js';
4
5
  import { resolveHermesHome } from './hermes-env.js';
5
6
  import { hermesWebBackendsOurs, readHermesPluginStatus, } from './hermes-plugin.js';
@@ -100,7 +101,7 @@ function keyedVerdict(status, keyAuth, cliKeyMissing) {
100
101
  }
101
102
  function registrationVerdict(status, mcpUrl, keyAuth, cliKeyMissing) {
102
103
  if (!status.detected)
103
- return { status: 'skip', detail: 'harness not installed' };
104
+ return { status: 'skip', detail: NOT_INSTALLED_DETAIL };
104
105
  if (status.registered === Registered.Unknown) {
105
106
  return { status: 'warn', detail: status.reason ?? 'could not determine registration' };
106
107
  }
@@ -260,7 +261,7 @@ export function checkPiAdapter(status) {
260
261
  scope: 'harness',
261
262
  };
262
263
  if (!status.detected)
263
- return { ...base, status: 'skip', detail: 'harness not installed' };
264
+ return { ...base, status: 'skip', detail: NOT_INSTALLED_DETAIL };
264
265
  if (status.registered !== Registered.Yes) {
265
266
  const why = status.registered === Registered.Unknown
266
267
  ? 'pi registration could not be determined'
@@ -293,7 +294,7 @@ export function checkHermesPlugin(status) {
293
294
  scope: 'harness',
294
295
  };
295
296
  if (!status.detected) {
296
- return { check: { ...base, status: 'skip', detail: 'harness not installed' } };
297
+ return { check: { ...base, status: 'skip', detail: NOT_INSTALLED_DETAIL } };
297
298
  }
298
299
  if (status.registered !== Registered.Yes) {
299
300
  return { check: { ...base, status: 'skip', detail: 'TinyFish is not registered in Hermes' } };
@@ -219,5 +219,6 @@ export declare function verdictFor(checks: DoctorCheck[]): {
219
219
  };
220
220
  /** Harness scope only: the caller asked about its agent, not us. */
221
221
  export declare function exitCodeFor(checks: DoctorCheck[]): 0 | 1;
222
+ export declare const NOT_INSTALLED_DETAIL = "harness not installed";
222
223
  export declare function renderPretty(report: DoctorReport): string;
223
224
  export {};
@@ -95,8 +95,17 @@ function footnotes(report) {
95
95
  }
96
96
  return notes;
97
97
  }
98
+ export const NOT_INSTALLED_DETAIL = 'harness not installed';
99
+ const isNotInstalled = (c) => c.status === 'skip' && c.detail === NOT_INSTALLED_DETAIL;
98
100
  export function renderPretty(report) {
99
- const lines = report.checks.map((c) => `${GLYPHS[c.status]} ${c.title}${c.detail ? ` — ${c.detail}` : ''}`);
101
+ const lines = report.checks
102
+ .filter((c) => !isNotInstalled(c))
103
+ .map((c) => `${GLYPHS[c.status]} ${c.title}${c.detail ? ` — ${c.detail}` : ''}`);
104
+ // Per-harness skip rows read as a to-do list; agents ran `connect` on each.
105
+ const absent = new Set(report.checks.filter(isNotInstalled).map((c) => c.harness));
106
+ if (absent.size > 0) {
107
+ lines.push(`${GLYPHS.skip} Not installed, nothing to do: ${[...absent].join(', ')}`);
108
+ }
100
109
  if (report.repairs.length > 0) {
101
110
  lines.push('', 'Repairs:');
102
111
  for (const repair of report.repairs)
@@ -28,3 +28,5 @@ export interface HarnessDetection {
28
28
  configDirExists: boolean;
29
29
  }
30
30
  export declare function detectInstalledHarnesses(): HarnessDetection[];
31
+ /** A config-writing connect needs no binary; every other harness spawns one. */
32
+ export declare function isStaleConfig(detection: HarnessDetection): boolean;
@@ -71,3 +71,10 @@ export function detectInstalledHarnesses() {
71
71
  };
72
72
  });
73
73
  }
74
+ /** A config-writing connect needs no binary; every other harness spawns one. */
75
+ export function isStaleConfig(detection) {
76
+ const spec = harnessSpec(detection.harness);
77
+ // Cursor's IDE reads mcp.json binary-less; omp and pi have no second reader.
78
+ const binaryless = spec.cliWritesConfig && spec.readableWithoutBinary;
79
+ return detection.detected && !detection.binaryOnPath && !binaryless;
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.47.2",
3
+ "version": "0.47.3-next.380",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {