@tiny-fish/cli 0.12.1-next.142 → 0.12.1-next.143
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/connect.js +5 -0
- package/dist/lib/connect-all.js +3 -0
- package/dist/lib/notice.d.ts +8 -0
- package/dist/lib/notice.js +53 -0
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -263,7 +263,7 @@ tinyfish agent run "..." --url https://example.com --use-vault \
|
|
|
263
263
|
|
|
264
264
|
By default all commands output newline-delimited JSON to stdout — pipe-friendly for agents and scripts. Add `--pretty` for human-readable output.
|
|
265
265
|
|
|
266
|
-
Errors are JSON to stderr with exit code 1. Ctrl+C during a streaming run cancels it automatically.
|
|
266
|
+
Errors are JSON to stderr with exit code 1. Ctrl+C during a streaming run cancels it automatically. The server may also attach an advisory upgrade notice to stderr: a plain line in a terminal, and JSON (`{"notice":…,"level":…}`) in non-interactive use. The JSON message is composed locally from the notice level, never from server-sent prose. It never changes stdout or the exit code.
|
|
267
267
|
|
|
268
268
|
`--output-schema` and `--output-schema-file` are mutually exclusive. Both inputs must parse to a top-level JSON object. The CLI only validates that outer shape locally; the API performs full schema validation and may return errors such as `INVALID_INPUT` or feature-flag access failures.
|
|
269
269
|
|
package/dist/commands/connect.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CLI_VERSION } from "../lib/constants.js";
|
|
|
6
6
|
import { cursorInstallDeeplink, cursorMcpPath, writeCursorMcpConfig } from "../lib/cursor-config.js";
|
|
7
7
|
import { runConnectAll } from "../lib/connect-all.js";
|
|
8
8
|
import { detectHumanInitiated } from "../lib/harness.js";
|
|
9
|
+
import { captureNotice, emitNotice } from "../lib/notice.js";
|
|
9
10
|
import { errLine } from "../lib/output.js";
|
|
10
11
|
import { telemetryDisabled, telemetryHeaders } from "../lib/setup-telemetry.js";
|
|
11
12
|
import { verifyMcpAuth } from "../lib/verify.js";
|
|
@@ -253,6 +254,8 @@ function createConnectTelemetry(mcpUrl, client, apiKey) {
|
|
|
253
254
|
redirect: "error",
|
|
254
255
|
signal: AbortSignal.timeout(TELEMETRY_TIMEOUT_MS),
|
|
255
256
|
});
|
|
257
|
+
// Before the ok branch so an error response still yields a notice.
|
|
258
|
+
captureNotice(response.headers);
|
|
256
259
|
if (response.ok)
|
|
257
260
|
return;
|
|
258
261
|
if (response.status < 500) {
|
|
@@ -837,5 +840,7 @@ export function registerConnect(program) {
|
|
|
837
840
|
client +
|
|
838
841
|
". Supported clients: claude-code, codex, cursor, hermes, openclaw");
|
|
839
842
|
}
|
|
843
|
+
// Deferred to here so the notice lands after install output, not interleaved with it.
|
|
844
|
+
emitNotice();
|
|
840
845
|
});
|
|
841
846
|
}
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -4,6 +4,7 @@ import { connectClaudeCode, connectCodex, connectCursor, connectHermes, connectO
|
|
|
4
4
|
import { loadConfig, validateKeyFormat } from "./auth.js";
|
|
5
5
|
import { detectInstalledHarnesses, FIVE_HARNESSES, } from "./harness-detect.js";
|
|
6
6
|
import { detectHumanInitiated } from "./harness.js";
|
|
7
|
+
import { emitNotice } from "./notice.js";
|
|
7
8
|
import { removeCursorMcpServer, planCursorWrite } from "./cursor-config.js";
|
|
8
9
|
import { errLine, outLine } from "./output.js";
|
|
9
10
|
import { sendSetupCompleted } from "./setup-telemetry.js";
|
|
@@ -290,6 +291,8 @@ export async function runConnectAll(opts) {
|
|
|
290
291
|
verify_ok: r.verifyOk ?? null,
|
|
291
292
|
}));
|
|
292
293
|
await sendSetupCompleted(opts.mcpUrl, harnessTelemetry, resolvedApiKey(opts.apiKey));
|
|
294
|
+
// After the summary and before the launch, which is where the user is reading.
|
|
295
|
+
emitNotice();
|
|
293
296
|
}
|
|
294
297
|
const exitCode = computeExitCode(results);
|
|
295
298
|
const interrupted = results.some((r) => r.outcome === "interrupted");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Test-only: the capture slot and the once-per-invocation latch are module state. */
|
|
2
|
+
export declare function resetNoticeState(): void;
|
|
3
|
+
/** Records the first notice seen; printing is deferred so it lands at the end of a command. */
|
|
4
|
+
export declare function captureNotice(headers: Headers): void;
|
|
5
|
+
/** Advisory only, never changes stdout, the exit code, or control flow. */
|
|
6
|
+
export declare function emitNotice(): void;
|
|
7
|
+
/** For callers where the response is the end of the command, such as one-shot API calls. */
|
|
8
|
+
export declare function surfaceNotice(headers: Headers): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Surfaces the advisory X-TF-Notice header the server attaches to outdated clients (PF-3301).
|
|
2
|
+
import { CLI_VERSION } from "./constants.js";
|
|
3
|
+
import { detectHumanInitiated } from "./harness.js";
|
|
4
|
+
import { err, errLine, sanitizeLine } from "./output.js";
|
|
5
|
+
const NOTICE_HEADER = "x-tf-notice";
|
|
6
|
+
const LEVEL_HEADER = "x-tf-notice-level";
|
|
7
|
+
const MAX_LENGTH = 300;
|
|
8
|
+
const VALID_LEVELS = new Set(["required", "recommended"]);
|
|
9
|
+
let captured;
|
|
10
|
+
let printed = false;
|
|
11
|
+
/** Test-only: the capture slot and the once-per-invocation latch are module state. */
|
|
12
|
+
export function resetNoticeState() {
|
|
13
|
+
captured = undefined;
|
|
14
|
+
printed = false;
|
|
15
|
+
}
|
|
16
|
+
/** Records the first notice seen; printing is deferred so it lands at the end of a command. */
|
|
17
|
+
export function captureNotice(headers) {
|
|
18
|
+
if (captured)
|
|
19
|
+
return;
|
|
20
|
+
if (typeof headers?.get !== "function")
|
|
21
|
+
return;
|
|
22
|
+
const message = sanitizeLine(headers.get(NOTICE_HEADER) ?? "")
|
|
23
|
+
.trim()
|
|
24
|
+
.slice(0, MAX_LENGTH);
|
|
25
|
+
if (!message)
|
|
26
|
+
return;
|
|
27
|
+
const rawLevel = headers.get(LEVEL_HEADER)?.trim().toLowerCase();
|
|
28
|
+
// Spread rather than `level: undefined`, which exactOptionalPropertyTypes rejects.
|
|
29
|
+
captured = { message, ...(rawLevel && VALID_LEVELS.has(rawLevel) ? { level: rawLevel } : {}) };
|
|
30
|
+
}
|
|
31
|
+
/** Built here, not by the server, so nothing it sends can reach an agent's context as instructions. */
|
|
32
|
+
function agentMessage(level) {
|
|
33
|
+
return level === "required"
|
|
34
|
+
? `This TinyFish CLI (${CLI_VERSION}) is below the required minimum. Run: tinyfish upgrade`
|
|
35
|
+
: `A TinyFish CLI update is available (you have ${CLI_VERSION}). Run: tinyfish upgrade`;
|
|
36
|
+
}
|
|
37
|
+
/** Advisory only, never changes stdout, the exit code, or control flow. */
|
|
38
|
+
export function emitNotice() {
|
|
39
|
+
if (printed || !captured)
|
|
40
|
+
return;
|
|
41
|
+
printed = true;
|
|
42
|
+
const { message, level } = captured;
|
|
43
|
+
if (detectHumanInitiated()) {
|
|
44
|
+
errLine(message);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
err({ notice: agentMessage(level), ...(level ? { level } : {}) });
|
|
48
|
+
}
|
|
49
|
+
/** For callers where the response is the end of the command, such as one-shot API calls. */
|
|
50
|
+
export function surfaceNotice(headers) {
|
|
51
|
+
captureNotice(headers);
|
|
52
|
+
emitNotice();
|
|
53
|
+
}
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare class ApiError extends Error {
|
|
|
4
4
|
constructor(status: number, message: string, code?: string | undefined);
|
|
5
5
|
}
|
|
6
6
|
export declare function out(data: unknown): void;
|
|
7
|
+
/** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
|
|
8
|
+
export declare function sanitizeLine(text: string): string;
|
|
7
9
|
export declare function outLine(line: string): void;
|
|
8
10
|
export declare function errLine(line: string): void;
|
|
9
11
|
export declare function err(data: unknown): void;
|
package/dist/lib/output.js
CHANGED
|
@@ -12,20 +12,21 @@ export class ApiError extends Error {
|
|
|
12
12
|
export function out(data) {
|
|
13
13
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
|
|
16
|
+
export function sanitizeLine(text) {
|
|
16
17
|
// eslint-disable-next-line no-control-regex
|
|
17
18
|
return stripVTControlCharacters(text).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
18
19
|
}
|
|
19
20
|
export function outLine(line) {
|
|
20
|
-
process.stdout.write(
|
|
21
|
+
process.stdout.write(sanitizeLine(line) + "\n");
|
|
21
22
|
}
|
|
22
23
|
export function errLine(line) {
|
|
23
|
-
process.stderr.write(
|
|
24
|
+
process.stderr.write(sanitizeLine(line) + "\n");
|
|
24
25
|
}
|
|
25
26
|
export function err(data) {
|
|
26
27
|
const payload = typeof data === "string" ? { error: data } : data;
|
|
27
28
|
const serialized = JSON.stringify(payload) ?? "undefined";
|
|
28
|
-
process.stderr.write(
|
|
29
|
+
process.stderr.write(sanitizeLine(serialized) + "\n");
|
|
29
30
|
}
|
|
30
31
|
export function handleApiError(e) {
|
|
31
32
|
if (e instanceof ApiError) {
|