@tiny-fish/cli 0.37.1-next.300 → 0.37.1-next.302
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 +1 -1
- package/dist/commands/auth.js +4 -1
- package/dist/commands/connect.js +49 -2
- package/dist/commands/doctor.d.ts +1 -14
- package/dist/commands/doctor.js +3 -173
- package/dist/lib/auth.d.ts +2 -1
- package/dist/lib/auth.js +3 -3
- package/dist/lib/connect-all-uninstall.js +25 -7
- package/dist/lib/connect-all.js +34 -3
- package/dist/lib/connect-clients.d.ts +2 -1
- package/dist/lib/connect-clients.js +4 -0
- package/dist/lib/connect-preflight.d.ts +15 -0
- package/dist/lib/connect-preflight.js +67 -0
- package/dist/lib/doctor-repairs.d.ts +9 -0
- package/dist/lib/doctor-repairs.js +119 -0
- package/dist/lib/doctor-report.d.ts +13 -1
- package/dist/lib/doctor-telemetry.d.ts +9 -0
- package/dist/lib/doctor-telemetry.js +61 -0
- package/dist/lib/harness-spec.d.ts +15 -1
- package/dist/lib/harness-spec.js +12 -0
- package/dist/lib/mcp-json-config.d.ts +2 -0
- package/dist/lib/mcp-json-config.js +4 -0
- package/dist/lib/omp-config.d.ts +16 -0
- package/dist/lib/omp-config.js +71 -0
- package/dist/lib/registration-detect.js +34 -0
- package/dist/lib/setup-telemetry.d.ts +23 -1
- package/dist/lib/setup-telemetry.js +24 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npx -y @tiny-fish/cli@latest connect
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
It detects the agent configs on your machine (Claude Code, Codex, Cursor, Grok, Hermes,
|
|
22
|
-
OpenClaw, OpenCode) and opens a checklist with everything pre-selected. Press enter to connect
|
|
22
|
+
omp, OpenClaw, OpenCode) and opens a checklist with everything pre-selected. Press enter to connect
|
|
23
23
|
them all, or use the arrow keys and space to narrow the set first; `q` cancels without touching
|
|
24
24
|
anything. Terminals without raw-key support get a numbered prompt instead.
|
|
25
25
|
|
package/dist/commands/auth.js
CHANGED
|
@@ -128,7 +128,10 @@ export function registerAuth(program) {
|
|
|
128
128
|
// Read from stdin pipe (CI/CD) or hidden TTY prompt (interactive)
|
|
129
129
|
const key = await readKeyFromStdinOrPrompt();
|
|
130
130
|
if (!key) {
|
|
131
|
-
|
|
131
|
+
// `echo $KEY | tinyfish auth set` with KEY unset pipes a blank line, not a key.
|
|
132
|
+
err(process.stdin.isTTY
|
|
133
|
+
? 'API key cannot be empty'
|
|
134
|
+
: "No key received on stdin. Example: echo 'sk-tinyfish-...' | tinyfish auth set");
|
|
132
135
|
process.exit(1);
|
|
133
136
|
}
|
|
134
137
|
if (!validateKeyFormat(key)) {
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
2
|
import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from '../lib/auth.js';
|
|
3
|
-
import { CURSOR_SKILL_TARGET, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
|
|
3
|
+
import { CURSOR_SKILL_TARGET, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
|
|
4
4
|
import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from '../lib/cli-install.js';
|
|
5
5
|
import { ensureCliAuthenticated } from '../lib/connect-auth.js';
|
|
6
6
|
import { createStdinPrompt, runCliFallback } from '../lib/connect-fallback.js';
|
|
@@ -8,6 +8,7 @@ import { installWebSkill } from '../lib/skill-install.js';
|
|
|
8
8
|
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, stageDurationOf, withStageDuration, spawnRemoval, } from '../lib/connect-runtime.js';
|
|
9
9
|
import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from '../lib/cursor-config.js';
|
|
10
10
|
import { runConnectAll } from '../lib/connect-all.js';
|
|
11
|
+
import { ompMcpPath, readOmpTinyfishEntry, writeOmpMcpConfig } from '../lib/omp-config.js';
|
|
11
12
|
import { ALL_HARNESSES, AuthMode, HARNESS_DISPLAY_NAMES, RELOAD_ACTION, } from '../lib/harness-detect.js';
|
|
12
13
|
import { detectHumanInitiated } from '../lib/harness.js';
|
|
13
14
|
import { TINYFISH_API_KEY_VAR } from '../lib/constants.js';
|
|
@@ -16,6 +17,7 @@ import { SIGN_IN_PROBES } from '../lib/registration-detect.js';
|
|
|
16
17
|
import { errLine, warnLine } from '../lib/output.js';
|
|
17
18
|
import { z } from 'zod';
|
|
18
19
|
import { verifyMcpAuth } from '../lib/verify.js';
|
|
20
|
+
import { gateApiKey } from '../lib/connect-preflight.js';
|
|
19
21
|
import { apiBaseFromMcpUrl, DEFAULT_MCP_URL } from '../lib/mcp-endpoint.js';
|
|
20
22
|
function removeExistingRegistration(client, removal, verbose) {
|
|
21
23
|
const result = spawnRemoval(client.command, removal.args);
|
|
@@ -506,6 +508,9 @@ export async function connectOpenClaw(options) {
|
|
|
506
508
|
}
|
|
507
509
|
});
|
|
508
510
|
}
|
|
511
|
+
// Only interactive omp wires the OAuth handler; headless cannot sign in.
|
|
512
|
+
const OMP_KEYLESS_COPY = 'TinyFish is configured in omp. Open omp and sign in to TinyFish when prompted, or run ' +
|
|
513
|
+
'`/mcp reauth tinyfish` there.';
|
|
509
514
|
const CONFIG_FILE_HARNESSES = {
|
|
510
515
|
cursor: {
|
|
511
516
|
skillTarget: CURSOR_SKILL_TARGET,
|
|
@@ -521,6 +526,22 @@ const CONFIG_FILE_HARNESSES = {
|
|
|
521
526
|
reload: 'TinyFish is connected. Reload the Cursor window, then approve/sign in under Settings → MCP.',
|
|
522
527
|
},
|
|
523
528
|
},
|
|
529
|
+
omp: {
|
|
530
|
+
// No skill exists for omp; installWebSkill returns without one.
|
|
531
|
+
skillTarget: { displayName: HARNESS_DISPLAY_NAMES.omp },
|
|
532
|
+
registeredUrl: () => readOmpTinyfishEntry()?.url,
|
|
533
|
+
write: writeOmpMcpConfig,
|
|
534
|
+
configPath: ompMcpPath,
|
|
535
|
+
// No install deeplink exists; the keyless copy carries the sign-in.
|
|
536
|
+
tryLaunch: () => false,
|
|
537
|
+
copy: {
|
|
538
|
+
verified: "TinyFish is connected and verified (health+auth). Restart omp and it's live.",
|
|
539
|
+
authFailed: (reason) => `TinyFish is configured, but the authenticated check failed (${reason}). ` +
|
|
540
|
+
'Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect omp',
|
|
541
|
+
launched: OMP_KEYLESS_COPY,
|
|
542
|
+
reload: OMP_KEYLESS_COPY,
|
|
543
|
+
},
|
|
544
|
+
},
|
|
524
545
|
};
|
|
525
546
|
/** Writes the harness's MCP config file itself; no `mcp add` exists. */
|
|
526
547
|
async function connectConfigFileHarness(harness, options) {
|
|
@@ -628,6 +649,7 @@ function launchCursorDeeplink(mcpUrl) {
|
|
|
628
649
|
}
|
|
629
650
|
/** Total over the non-native harnesses: a new one fails to compile until wired here. */
|
|
630
651
|
const LAUNCH_OVERRIDES = {
|
|
652
|
+
omp: launchOmpWalkthrough,
|
|
631
653
|
openclaw: launchOpenClawWalkthrough,
|
|
632
654
|
// Cursor has no chat CLI to launch.
|
|
633
655
|
cursor: () => {
|
|
@@ -654,6 +676,20 @@ const CONNECTOR_OVERRIDES = {
|
|
|
654
676
|
}
|
|
655
677
|
return undefined;
|
|
656
678
|
},
|
|
679
|
+
omp: async (options) => {
|
|
680
|
+
await connectConfigFileHarness('omp', options);
|
|
681
|
+
if (options.launch) {
|
|
682
|
+
try {
|
|
683
|
+
await launchOmpWalkthrough();
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
// Post-settle, like the native walkthrough step: never fail the connect.
|
|
687
|
+
errLine(error instanceof Error ? error.message : String(error));
|
|
688
|
+
errLine(finishSetupHint('omp'));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return undefined;
|
|
692
|
+
},
|
|
657
693
|
};
|
|
658
694
|
function requireNative(harness) {
|
|
659
695
|
const client = NATIVE_BY_HARNESS.get(harness);
|
|
@@ -746,13 +782,24 @@ async function connectSingleClient(client, options, mcpUrl, attemptId) {
|
|
|
746
782
|
if (!ALL_HARNESSES.includes(client)) {
|
|
747
783
|
throw new Error('Unsupported client: ' + client + '. Supported clients: ' + ALL_HARNESSES.join(', '));
|
|
748
784
|
}
|
|
785
|
+
const gate = await gateApiKey({
|
|
786
|
+
apiKey: options.apiKey,
|
|
787
|
+
mcpUrl,
|
|
788
|
+
retryCommand: `tinyfish connect ${client}`,
|
|
789
|
+
mode: 'single',
|
|
790
|
+
attemptId,
|
|
791
|
+
});
|
|
792
|
+
if (gate === 'blocked') {
|
|
793
|
+
process.exitCode = 1;
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
749
796
|
await connectHarness(client, connectOptions);
|
|
750
797
|
}
|
|
751
798
|
export function registerConnect(program) {
|
|
752
799
|
program
|
|
753
800
|
.command('connect')
|
|
754
801
|
.description('Connect TinyFish to an AI agent')
|
|
755
|
-
.argument('[client]', 'Agent client to connect (claude-code, codex, cursor, grok, hermes, openclaw, or opencode); omit to pick from the agents detected on this machine')
|
|
802
|
+
.argument('[client]', 'Agent client to connect (claude-code, codex, cursor, grok, hermes, omp, openclaw, or opencode); omit to pick from the agents detected on this machine')
|
|
756
803
|
.option('--all', 'Detect and connect every supported harness found on this machine')
|
|
757
804
|
.option('--dry-run', 'Print planned writes per harness without touching anything (--all only)')
|
|
758
805
|
.option('--uninstall', 'Remove TinyFish entries written by connect --all')
|
|
@@ -1,17 +1,4 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import { type
|
|
3
|
-
import { type Harness } from '../lib/harness-detect.js';
|
|
4
|
-
export interface DoctorOptions {
|
|
5
|
-
mcpUrl: string;
|
|
6
|
-
harness?: Harness;
|
|
7
|
-
fix?: boolean;
|
|
8
|
-
yes?: boolean;
|
|
9
|
-
}
|
|
2
|
+
import { type DoctorOptions, type DoctorReport } from '../lib/doctor-report.js';
|
|
10
3
|
export declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
|
|
11
|
-
export interface RepairOutcome {
|
|
12
|
-
repair: DoctorRepair;
|
|
13
|
-
status: 'repaired' | 'skipped' | 'failed';
|
|
14
|
-
reason?: string;
|
|
15
|
-
}
|
|
16
|
-
export declare function applyRepairs(repairs: DoctorRepair[], options: DoctorOptions): Promise<RepairOutcome[]>;
|
|
17
4
|
export declare function registerDoctor(program: Command): void;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import spawn from 'cross-spawn';
|
|
2
1
|
import { apiKeyStatus } from '../lib/auth.js';
|
|
3
2
|
import { CLI_VERSION } from '../lib/constants.js';
|
|
4
3
|
import { DOCTOR_COULD_NOT_RUN, DOCTOR_INVALID_INPUT, DOCTOR_SCHEMA_VERSION, doctorReportSchema, exitCodeFor, renderPretty, verdictFor, } from '../lib/doctor-report.js';
|
|
4
|
+
import { applyRepairs, repairsFor } from '../lib/doctor-repairs.js';
|
|
5
|
+
import { couldNotRunPayload, telemetryKey, telemetryPayload } from '../lib/doctor-telemetry.js';
|
|
5
6
|
import { ALL_HARNESSES, AuthMode, Registered } from '../lib/harness-detect.js';
|
|
6
|
-
import { detectHumanInitiated } from '../lib/harness.js';
|
|
7
7
|
import { err, errLine, out, outLine } from '../lib/output.js';
|
|
8
8
|
import { detectRegistrations } from '../lib/registration-detect.js';
|
|
9
|
-
import {
|
|
9
|
+
import { sendDoctorCompleted } from '../lib/setup-telemetry.js';
|
|
10
10
|
import { verifyMcpAuth, verifyMcpHealth } from '../lib/verify.js';
|
|
11
11
|
import { resolveHermesHome } from '../lib/hermes-env.js';
|
|
12
12
|
import { hermesWebBackendsOurs, readHermesPluginStatus, } from '../lib/hermes-plugin.js';
|
|
13
13
|
import { DEFAULT_MCP_URL, endpointOf, isDefaultEndpoint } from '../lib/mcp-endpoint.js';
|
|
14
|
-
import { connectHarness } from './connect.js';
|
|
15
14
|
// A green auth mode with a red auth call proves nothing, so the call's verdict gates the claim.
|
|
16
15
|
function provesHarnessReach(status, keyAuth, healthOk, mcpUrl) {
|
|
17
16
|
// An endpoint doctor could not reach proves nothing about a harness pointed at it.
|
|
@@ -270,57 +269,6 @@ function checkHermesPlugin(status) {
|
|
|
270
269
|
}
|
|
271
270
|
return { ...check, ...hermesPluginVerdict(home) };
|
|
272
271
|
}
|
|
273
|
-
// Only Cursor's repair is a pure local file write; every other one needs a browser sign-in.
|
|
274
|
-
const UNATTENDED_SAFE = new Set(['cursor']);
|
|
275
|
-
function repairsFor(checks, statuses) {
|
|
276
|
-
const repairs = [];
|
|
277
|
-
// A revoked-but-well-formed key passes the credential check and fails the call; both need login.
|
|
278
|
-
const credentialBroken = checks.some((c) => (c.id === 'cli-credential' || c.id === 'cli-auth-call') && c.status === 'fail');
|
|
279
|
-
// connect writes the stored key, so replace a dead one first.
|
|
280
|
-
if (credentialBroken) {
|
|
281
|
-
repairs.push({
|
|
282
|
-
for: 'cli-credential-invalid',
|
|
283
|
-
action: 'auth-login',
|
|
284
|
-
harness: null,
|
|
285
|
-
command: 'tinyfish auth login',
|
|
286
|
-
unattended_safe: false,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
const authCallPassed = checks.some((c) => c.id === 'cli-auth-call' && c.status === 'pass');
|
|
290
|
-
for (const status of statuses) {
|
|
291
|
-
// `unknown` earns nothing: connect would contradict the check's detail.
|
|
292
|
-
// A registered entry that still fails earns one too.
|
|
293
|
-
// `connected: false` is the harness's own no; it only warns, so it earns a repair here.
|
|
294
|
-
const brokenRegistration = status.connected === false ||
|
|
295
|
-
checks.some((c) => c.harness === status.harness && c.id !== 'hermes-plugin' && c.status === 'fail');
|
|
296
|
-
if (!status.detected || (status.registered !== Registered.No && !brokenRegistration))
|
|
297
|
-
continue;
|
|
298
|
-
repairs.push({
|
|
299
|
-
for: status.registered === Registered.Yes
|
|
300
|
-
? `${status.harness}-registration-broken`
|
|
301
|
-
: status.connectedBefore
|
|
302
|
-
? `${status.harness}-registration-lost`
|
|
303
|
-
: `${status.harness}-not-connected`,
|
|
304
|
-
action: 'connect',
|
|
305
|
-
harness: status.harness,
|
|
306
|
-
command: `tinyfish connect ${status.harness}`,
|
|
307
|
-
// A dead well-formed key passes the format check.
|
|
308
|
-
unattended_safe: UNATTENDED_SAFE.has(status.harness) && authCallPassed,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
// connect hermes reinstalls the plugin; a second row would double it.
|
|
312
|
-
const pluginBroken = checks.some((c) => c.id === 'hermes-plugin' && c.status === 'fail');
|
|
313
|
-
if (pluginBroken && !repairs.some((repair) => repair.harness === 'hermes')) {
|
|
314
|
-
repairs.push({
|
|
315
|
-
for: 'hermes-plugin-broken',
|
|
316
|
-
action: 'connect',
|
|
317
|
-
harness: 'hermes',
|
|
318
|
-
command: 'tinyfish connect hermes',
|
|
319
|
-
unattended_safe: false,
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
return repairs;
|
|
323
|
-
}
|
|
324
272
|
export async function runDoctor(options) {
|
|
325
273
|
const statuses = detectRegistrations(options.harness ? [options.harness] : undefined);
|
|
326
274
|
const credential = checkCredential();
|
|
@@ -359,124 +307,6 @@ export async function runDoctor(options) {
|
|
|
359
307
|
// Nothing to scrub: every string above is authored here or enumerated at its source.
|
|
360
308
|
return doctorReportSchema.parse(body);
|
|
361
309
|
}
|
|
362
|
-
// Shell out rather than import the login flow: `auth login` owns its own browser and prompts.
|
|
363
|
-
// Re-invoke this same entry script, never a global `tinyfish` — under `npx` there is no such
|
|
364
|
-
// binary on PATH, and npx is the install-nothing path doctor exists to serve.
|
|
365
|
-
function runAuthLogin() {
|
|
366
|
-
const entry = process.argv[1];
|
|
367
|
-
if (!entry)
|
|
368
|
-
throw new Error('Could not locate the TinyFish CLI entry point to re-invoke');
|
|
369
|
-
const result = spawn.sync(process.execPath, [entry, 'auth', 'login', '--source', 'doctor'], {
|
|
370
|
-
stdio: 'inherit',
|
|
371
|
-
});
|
|
372
|
-
if (result.error)
|
|
373
|
-
throw new Error('Could not run `tinyfish auth login`', { cause: result.error });
|
|
374
|
-
if (result.status !== 0) {
|
|
375
|
-
throw new Error(`\`tinyfish auth login\` exited ${result.status ?? 'unknown'}`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
/** Dispatches on `action`, never on a null harness: that conflation made the credential fix a no-op. */
|
|
379
|
-
async function runRepair(repair, mcpUrl) {
|
|
380
|
-
if (repair.action === 'auth-login')
|
|
381
|
-
return runAuthLogin();
|
|
382
|
-
if (repair.harness)
|
|
383
|
-
await connectHarness(repair.harness, { mcpUrl, launch: false });
|
|
384
|
-
}
|
|
385
|
-
export async function applyRepairs(repairs, options) {
|
|
386
|
-
const interactive = detectHumanInitiated();
|
|
387
|
-
const outcomes = [];
|
|
388
|
-
for (const repair of repairs) {
|
|
389
|
-
if (!interactive && !options.yes) {
|
|
390
|
-
outcomes.push({
|
|
391
|
-
repair,
|
|
392
|
-
status: 'skipped',
|
|
393
|
-
reason: 'no terminal; re-run with --yes to repair unattended',
|
|
394
|
-
});
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
if (!interactive && !repair.unattended_safe) {
|
|
398
|
-
outcomes.push({
|
|
399
|
-
repair,
|
|
400
|
-
status: 'skipped',
|
|
401
|
-
reason: 'needs a browser sign-in, which has no unattended path',
|
|
402
|
-
});
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
try {
|
|
406
|
-
await runRepair(repair, options.mcpUrl);
|
|
407
|
-
outcomes.push({ repair, status: 'repaired' });
|
|
408
|
-
}
|
|
409
|
-
catch (e) {
|
|
410
|
-
// Connect quotes absolute config paths in its errors, so the raw text stays behind
|
|
411
|
-
// --debug and the user is told where to find it rather than handed a generic dead end.
|
|
412
|
-
if (process.env['TINYFISH_DEBUG']) {
|
|
413
|
-
errLine(e instanceof Error ? (e.stack ?? e.message) : String(e));
|
|
414
|
-
}
|
|
415
|
-
outcomes.push({
|
|
416
|
-
repair,
|
|
417
|
-
status: 'failed',
|
|
418
|
-
reason: `\`${repair.command}\` failed; re-run it directly, or --debug for the raw error`,
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
return outcomes;
|
|
423
|
-
}
|
|
424
|
-
/** Mirrors the route's cap; over it the payload would 400 and never be retried. */
|
|
425
|
-
const MAX_DURATION_MS = 3_600_000;
|
|
426
|
-
function fixModeOf(options) {
|
|
427
|
-
return !options.fix ? 'none' : options.yes ? 'fix_yes' : 'fix';
|
|
428
|
-
}
|
|
429
|
-
/** Enumerated fields only; check `detail` text never leaves the machine. */
|
|
430
|
-
function telemetryPayload(report, offered, outcomes, options, durationMs) {
|
|
431
|
-
const count = (status) => report.checks.filter((check) => check.status === status).length;
|
|
432
|
-
const failed = report.checks.filter((check) => check.status === 'fail');
|
|
433
|
-
return {
|
|
434
|
-
outcome: 'completed',
|
|
435
|
-
schema_version: report.schema_version,
|
|
436
|
-
cli_version: report.cli_version,
|
|
437
|
-
ok_harnesses: report.ok_harnesses,
|
|
438
|
-
ok_cli: report.ok_cli,
|
|
439
|
-
checks_pass: count('pass'),
|
|
440
|
-
checks_fail: count('fail'),
|
|
441
|
-
checks_warn: count('warn'),
|
|
442
|
-
checks_skip: count('skip'),
|
|
443
|
-
// From the pre-repair report; the verdicts above describe the machine after it.
|
|
444
|
-
repairs_offered: offered.length,
|
|
445
|
-
repair_actions: [...new Set(offered.map((repair) => repair.action))],
|
|
446
|
-
repairs_attempted: outcomes.filter((outcome) => outcome.status !== 'skipped').length,
|
|
447
|
-
repairs_succeeded: outcomes.filter((outcome) => outcome.status === 'repaired').length,
|
|
448
|
-
fix_mode: fixModeOf(options),
|
|
449
|
-
// --harness scopes detection, so the counts above cover that harness alone.
|
|
450
|
-
harness_scope: options.harness ?? 'all',
|
|
451
|
-
// Same signal cli_connect_stage carries, so agent and human doctor runs split.
|
|
452
|
-
is_human_initiated: detectHumanInitiated(),
|
|
453
|
-
// Two flat arrays rather than one of pairs: PostHog groups by element, not by field.
|
|
454
|
-
failed_check_ids: [...new Set(failed.map((check) => check.id))],
|
|
455
|
-
failed_harnesses: [...new Set(failed.map((check) => check.harness).filter((h) => h !== null))],
|
|
456
|
-
harnesses: report.harnesses,
|
|
457
|
-
// Clamped, not validated: a --fix parked at a prompt must not fail the parse and drop the run.
|
|
458
|
-
duration_ms: Math.min(durationMs, MAX_DURATION_MS),
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
/** Both paths send keyless when there is no key; the route records those anonymously. */
|
|
462
|
-
function telemetryKey() {
|
|
463
|
-
const resolved = apiKeyStatus();
|
|
464
|
-
return 'key' in resolved ? resolved.key : undefined;
|
|
465
|
-
}
|
|
466
|
-
/** Enumerated outcome and timing only; the raw error stays behind --debug. */
|
|
467
|
-
function couldNotRunPayload(error, options, durationMs) {
|
|
468
|
-
return {
|
|
469
|
-
outcome: 'could_not_run',
|
|
470
|
-
error_class: doctorErrorClass(error),
|
|
471
|
-
schema_version: DOCTOR_SCHEMA_VERSION,
|
|
472
|
-
cli_version: CLI_VERSION,
|
|
473
|
-
fix_mode: fixModeOf(options),
|
|
474
|
-
harness_scope: options.harness ?? 'all',
|
|
475
|
-
is_human_initiated: detectHumanInitiated(),
|
|
476
|
-
// Clamped like the completed row: over the cap the payload would 400 un-retried.
|
|
477
|
-
duration_ms: Math.min(durationMs, MAX_DURATION_MS),
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
310
|
export function registerDoctor(program) {
|
|
481
311
|
program
|
|
482
312
|
.command('doctor')
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { DASHBOARD_URL, getDashboardUrl } from './constants.js';
|
|
2
2
|
export declare function configDir(): string;
|
|
3
|
+
export declare function configFile(): string;
|
|
3
4
|
/** Install channel baked onto connect context; the connect flow is always CLI-driven. */
|
|
4
5
|
export declare const CONNECT_SOURCE = "tinyfish_cli";
|
|
5
6
|
export declare const CONNECT_ATTEMPT_ENV = "TINYFISH_CONNECT_ATTEMPT_ID";
|
|
@@ -65,5 +66,5 @@ export declare function validatedApiKey(explicitApiKey?: string): string | undef
|
|
|
65
66
|
* The diagnosis, not the key: doctor must report a bad credential and say where it came from
|
|
66
67
|
* rather than exit like getApiKey does.
|
|
67
68
|
*/
|
|
68
|
-
export declare function apiKeyStatus(): ResolvedKey | UnresolvedKey;
|
|
69
|
+
export declare function apiKeyStatus(explicitApiKey?: string): ResolvedKey | UnresolvedKey;
|
|
69
70
|
export declare function getApiKey(): string;
|
package/dist/lib/auth.js
CHANGED
|
@@ -11,7 +11,7 @@ export function configDir() {
|
|
|
11
11
|
// os.homedir() is cross-platform; process.env.HOME is undefined on Windows
|
|
12
12
|
return path.join(os.homedir(), '.tinyfish');
|
|
13
13
|
}
|
|
14
|
-
function configFile() {
|
|
14
|
+
export function configFile() {
|
|
15
15
|
return path.join(configDir(), 'config.json');
|
|
16
16
|
}
|
|
17
17
|
/** Install channel baked onto connect context; the connect flow is always CLI-driven. */
|
|
@@ -283,8 +283,8 @@ export function validatedApiKey(explicitApiKey) {
|
|
|
283
283
|
* The diagnosis, not the key: doctor must report a bad credential and say where it came from
|
|
284
284
|
* rather than exit like getApiKey does.
|
|
285
285
|
*/
|
|
286
|
-
export function apiKeyStatus() {
|
|
287
|
-
const { value, source } = lookupApiKey();
|
|
286
|
+
export function apiKeyStatus(explicitApiKey) {
|
|
287
|
+
const { value, source } = lookupApiKey(explicitApiKey);
|
|
288
288
|
if (!value || source === 'none') {
|
|
289
289
|
return { error: 'No API key found. Run: tinyfish auth login', source: 'none' };
|
|
290
290
|
}
|
|
@@ -2,6 +2,7 @@ import { clearConnectContext, connectContextState } from './auth.js';
|
|
|
2
2
|
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
|
+
import { planOmpWrite, removeOmpMcpServer } from './omp-config.js';
|
|
5
6
|
import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
|
|
6
7
|
import { clearHermesWebBackends, HERMES_PLUGIN_SHA } from './hermes-plugin.js';
|
|
7
8
|
import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES } from './harness-detect.js';
|
|
@@ -11,6 +12,8 @@ const OPENCODE_UNINSTALL_POINTER = 'remove the tinyfish entry from ~/.config/ope
|
|
|
11
12
|
export function planText(harness, mcpUrl, apiKey) {
|
|
12
13
|
if (harness === 'cursor')
|
|
13
14
|
return planCursorWrite(mcpUrl, apiKey);
|
|
15
|
+
if (harness === 'omp')
|
|
16
|
+
return planOmpWrite(mcpUrl, apiKey);
|
|
14
17
|
// Only the keyed path seeds the .env; a keyless Hermes install writes nothing.
|
|
15
18
|
if (harness === 'hermes' && apiKey) {
|
|
16
19
|
return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
|
|
@@ -23,6 +26,9 @@ export function uninstallPlanText(harness) {
|
|
|
23
26
|
if (harness === 'cursor') {
|
|
24
27
|
return 'would remove the tinyfish entry from ~/.cursor/mcp.json';
|
|
25
28
|
}
|
|
29
|
+
if (harness === 'omp') {
|
|
30
|
+
return "would remove the tinyfish entry from omp's mcp.json (located via `omp config path`)";
|
|
31
|
+
}
|
|
26
32
|
if (!uninstallRuns(harness)) {
|
|
27
33
|
return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
|
|
28
34
|
}
|
|
@@ -170,10 +176,21 @@ function uninstallViaCommands(detection, verbose, binaryOnPath) {
|
|
|
170
176
|
fixCommand: `Retry by hand: ${uninstallPointer(harness)}`,
|
|
171
177
|
};
|
|
172
178
|
}
|
|
173
|
-
|
|
179
|
+
/** The CLI wrote these mcp.json entries, so it removes them. */
|
|
180
|
+
const MCP_JSON_UNINSTALLS = {
|
|
181
|
+
cursor: {
|
|
182
|
+
remove: removeCursorMcpServer,
|
|
183
|
+
fix: 'Fix ~/.cursor/mcp.json by hand, then re-run: tinyfish connect --all --uninstall',
|
|
184
|
+
},
|
|
185
|
+
omp: {
|
|
186
|
+
remove: removeOmpMcpServer,
|
|
187
|
+
fix: "Fix omp's mcp.json (at `omp config path`) by hand, then re-run: tinyfish connect --all --uninstall",
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
function uninstallMcpJson(detection, spec) {
|
|
174
191
|
let result;
|
|
175
192
|
try {
|
|
176
|
-
result =
|
|
193
|
+
result = spec.remove();
|
|
177
194
|
}
|
|
178
195
|
catch (e) {
|
|
179
196
|
result = { status: 'corrupt_skip', error: e instanceof Error ? e.message : String(e) };
|
|
@@ -183,20 +200,21 @@ function uninstallCursor(detection) {
|
|
|
183
200
|
...detection,
|
|
184
201
|
outcome: 'uninstalled',
|
|
185
202
|
removalFailed: true,
|
|
186
|
-
errorMessage: `could not update
|
|
187
|
-
fixCommand:
|
|
203
|
+
errorMessage: `could not update ${DISPLAY_NAMES[detection.harness]}'s mcp.json (${result.error}) — nothing removed.`,
|
|
204
|
+
fixCommand: spec.fix,
|
|
188
205
|
};
|
|
189
206
|
}
|
|
190
207
|
if (result.status === 'unchanged') {
|
|
191
208
|
return { ...detection, outcome: 'uninstall_noop' };
|
|
192
209
|
}
|
|
193
|
-
clearConnectContext(
|
|
210
|
+
clearConnectContext(detection.harness);
|
|
194
211
|
return { ...detection, outcome: 'uninstalled' };
|
|
195
212
|
}
|
|
196
213
|
export function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
|
|
197
214
|
const detection = { harness, detected: true, configPath, installed: false };
|
|
198
|
-
const
|
|
199
|
-
|
|
215
|
+
const mcpJson = MCP_JSON_UNINSTALLS[harness];
|
|
216
|
+
const result = mcpJson
|
|
217
|
+
? uninstallMcpJson(detection, mcpJson)
|
|
200
218
|
: uninstallViaCommands(detection, verbose, binaryOnPath);
|
|
201
219
|
if (harness !== 'hermes')
|
|
202
220
|
return result;
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -9,6 +9,7 @@ import { actionable, computeExitCode, reloadHint, renderSummary } from './connec
|
|
|
9
9
|
import { planText, uninstallHarness, uninstallPlanText } from './connect-all-uninstall.js';
|
|
10
10
|
import { NATIVE_BY_HARNESS } from './connect-clients.js';
|
|
11
11
|
import { createStdinPrompt, runCliFallback } from './connect-fallback.js';
|
|
12
|
+
import { gateApiKey } from './connect-preflight.js';
|
|
12
13
|
import { ConnectInterruptedError } from './connect-runtime.js';
|
|
13
14
|
import { pickHarnesses } from './connect-picker.js';
|
|
14
15
|
import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, } from './harness-detect.js';
|
|
@@ -142,7 +143,10 @@ function noWorkOutcome(d, uninstall) {
|
|
|
142
143
|
}
|
|
143
144
|
/** A config-writing connect needs no binary; every other harness spawns one. */
|
|
144
145
|
function isStaleConfig(detection) {
|
|
145
|
-
|
|
146
|
+
const spec = harnessSpec(detection.harness);
|
|
147
|
+
// Cursor's IDE reads mcp.json binary-less; omp's path needs the binary.
|
|
148
|
+
const binaryless = spec.cliWritesConfig && !spec.configPathFromBinary;
|
|
149
|
+
return detection.detected && !detection.binaryOnPath && !binaryless;
|
|
146
150
|
}
|
|
147
151
|
async function resolvePick(detections, opts, prompt) {
|
|
148
152
|
let deselected = new Set();
|
|
@@ -212,8 +216,13 @@ async function connectLoop(detections, opts, deselected, io) {
|
|
|
212
216
|
try {
|
|
213
217
|
setErrIndent(STEP_INDENT);
|
|
214
218
|
result = await processHarness(detection, opts, io.prompt, oauthBudget, io.takeHoistedCli, (line) => io.outro.add(line));
|
|
219
|
+
// A failed harness prints nothing of its own, so its step reads as skipped.
|
|
220
|
+
if (result.outcome === 'failed' && result.errorMessage)
|
|
221
|
+
errLine(result.errorMessage);
|
|
215
222
|
}
|
|
216
223
|
catch (e) {
|
|
224
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
225
|
+
errLine(errorMessage);
|
|
217
226
|
// Isolation: one harness throwing never stops the others.
|
|
218
227
|
result = {
|
|
219
228
|
harness,
|
|
@@ -221,7 +230,7 @@ async function connectLoop(detections, opts, deselected, io) {
|
|
|
221
230
|
configPath: detection.configPath,
|
|
222
231
|
installed: false,
|
|
223
232
|
outcome: 'failed',
|
|
224
|
-
errorMessage
|
|
233
|
+
errorMessage,
|
|
225
234
|
fixCommand: `tinyfish connect ${harness}`,
|
|
226
235
|
};
|
|
227
236
|
}
|
|
@@ -295,11 +304,32 @@ async function finishRun(results, opts, isTTY) {
|
|
|
295
304
|
}
|
|
296
305
|
return exitCode;
|
|
297
306
|
}
|
|
307
|
+
/** Neither path needs a live key, and neither writes a registration. */
|
|
308
|
+
function keyGateApplies(opts) {
|
|
309
|
+
return !opts.uninstall && !opts.dryRun;
|
|
310
|
+
}
|
|
311
|
+
async function blockedByKeyGate(opts) {
|
|
312
|
+
if (!keyGateApplies(opts))
|
|
313
|
+
return false;
|
|
314
|
+
const pick = opts.pick === true;
|
|
315
|
+
const gate = await gateApiKey({
|
|
316
|
+
apiKey: opts.apiKey,
|
|
317
|
+
mcpUrl: opts.mcpUrl,
|
|
318
|
+
// The picker path is reached by a bare `tinyfish connect`.
|
|
319
|
+
retryCommand: pick ? 'tinyfish connect' : 'tinyfish connect --all',
|
|
320
|
+
mode: pick ? 'pick' : 'all',
|
|
321
|
+
attemptId: opts.attemptId,
|
|
322
|
+
});
|
|
323
|
+
return gate === 'blocked';
|
|
324
|
+
}
|
|
298
325
|
export async function runConnectAll(opts) {
|
|
299
326
|
const isTTY = detectHumanInitiated();
|
|
327
|
+
// Above the setup event and every write: a blocked run is not a setup attempt.
|
|
328
|
+
if (await blockedByKeyGate(opts))
|
|
329
|
+
return 1;
|
|
300
330
|
// Ahead of detection and the hoisted install; either can end the process silently.
|
|
301
331
|
// Not on the picker path: an explicit cancel is not a setup.
|
|
302
|
-
if (
|
|
332
|
+
if (keyGateApplies(opts) && !opts.pick) {
|
|
303
333
|
// validatedApiKey falls back to env/config; opts.apiKey alone splits the run's two ends.
|
|
304
334
|
await sendSetupStarted(opts.mcpUrl, opts.attemptId, validatedApiKey(opts.apiKey));
|
|
305
335
|
}
|
|
@@ -377,6 +407,7 @@ export const FIRST_TASK_PROMPT = 'My TinyFish setup just finished — verify it
|
|
|
377
407
|
const LAUNCHERS = [
|
|
378
408
|
{ harness: 'cursor', args: [FIRST_TASK_PROMPT] },
|
|
379
409
|
{ harness: 'claude-code', args: [FIRST_TASK_PROMPT] },
|
|
410
|
+
{ harness: 'omp', args: [FIRST_TASK_PROMPT] },
|
|
380
411
|
];
|
|
381
412
|
async function launchFirstTask(results) {
|
|
382
413
|
const candidates = LAUNCHERS.filter((l) => results.some((r) => r.harness === l.harness && r.installed) &&
|
|
@@ -95,11 +95,12 @@ export declare function hermesRegistrationEnabled(home: string): {
|
|
|
95
95
|
};
|
|
96
96
|
export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
|
|
97
97
|
/** Native descriptor by harness id; Cursor and OpenClaw have none. */
|
|
98
|
-
export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code", NativeMcpClient>;
|
|
98
|
+
export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "omp" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code", NativeMcpClient>;
|
|
99
99
|
export declare const OPENCLAW: SupportedCommand;
|
|
100
100
|
/** "printed" = handed to the user to paste; the walkthrough was never started for them. */
|
|
101
101
|
export type WalkthroughOutcome = 'launched' | 'printed';
|
|
102
102
|
/** Awaited before the handover; track+flush must land pre-block. */
|
|
103
103
|
type OnWalkthroughDecided = (outcome: WalkthroughOutcome) => void | Promise<void>;
|
|
104
104
|
export declare function launchNativeMcpClient(client: NativeMcpClient, onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
105
|
+
export declare function launchOmpWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
105
106
|
export declare function launchOpenClawWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
|
|
@@ -293,6 +293,10 @@ export async function launchNativeMcpClient(client, onDecided) {
|
|
|
293
293
|
? client.launchWalkthrough()
|
|
294
294
|
: handOverTerminal(client.command, [DEFAULT_ONBOARDING_PROMPT], client.displayName), onDecided);
|
|
295
295
|
}
|
|
296
|
+
// Via deliverWalkthrough: handOverTerminal alone would wedge a headless --launch.
|
|
297
|
+
export function launchOmpWalkthrough(onDecided) {
|
|
298
|
+
return deliverWalkthrough('omp', 'omp', DEFAULT_ONBOARDING_PROMPT, () => handOverTerminal('omp', [DEFAULT_ONBOARDING_PROMPT], 'omp'), onDecided);
|
|
299
|
+
}
|
|
296
300
|
export function launchOpenClawWalkthrough(onDecided) {
|
|
297
301
|
return deliverWalkthrough('OpenClaw', 'openclaw', OPENCLAW_ONBOARDING_PROMPT, () => handOverTerminal('openclaw', ['chat', '--message', OPENCLAW_ONBOARDING_PROMPT], 'OpenClaw'), onDecided);
|
|
298
302
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type SetupBlockedMode } from './setup-telemetry.js';
|
|
2
|
+
export type PreflightOutcome = 'ok' | 'blocked';
|
|
3
|
+
interface PreflightOptions {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
mcpUrl: string;
|
|
6
|
+
retryCommand: string;
|
|
7
|
+
mode: SetupBlockedMode;
|
|
8
|
+
attemptId?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks the key every harness would install with, once, before anything is written.
|
|
12
|
+
* A bad key otherwise fails all seven installs the slow way, and Hermes refuses outright.
|
|
13
|
+
*/
|
|
14
|
+
export declare function gateApiKey(options: PreflightOptions): Promise<PreflightOutcome>;
|
|
15
|
+
export {};
|