@synqa/cli 0.0.0-stage → 1.1.1
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 +278 -2
- package/bin/synqa.js +8 -0
- package/dist/account.js +93 -0
- package/dist/agent.js +122 -0
- package/dist/api.js +200 -0
- package/dist/commands.js +183 -0
- package/dist/compact.js +85 -0
- package/dist/config.js +68 -0
- package/dist/diff.js +108 -0
- package/dist/init.js +177 -0
- package/dist/journal.js +61 -0
- package/dist/main.js +360 -0
- package/dist/paths.js +243 -0
- package/dist/prompt.js +330 -0
- package/dist/sanitize.js +53 -0
- package/dist/statusbar.js +121 -0
- package/dist/theme.js +146 -0
- package/dist/tiers.js +53 -0
- package/dist/tools.js +413 -0
- package/dist/ui.js +574 -0
- package/package.json +35 -4
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The status bar: one line with the tier, routing mode, TTFT, tok/s and session tokens. On a
|
|
2
|
+
// terminal it is pinned to the bottom row (the scroll region stops one row short of it); anywhere
|
|
3
|
+
// else it is printed as an ordinary line by the caller, so nothing here needs a terminal to be tested.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// 1234567 -> "1,234,567" without relying on the locale.
|
|
16
|
+
export function groupDigits(n ) {
|
|
17
|
+
return String(Math.max(0, Math.round(n))).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatTtft(ms ) {
|
|
21
|
+
return ms === undefined || !Number.isFinite(ms) || ms < 0 ? "-" : `${(ms / 1000).toFixed(2)}s`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatRate(tps ) {
|
|
25
|
+
return tps === undefined || !Number.isFinite(tps) || tps <= 0 ? "-" : `${tps.toFixed(1)} tok/s`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fits `width` columns by dropping segments from the least important end; the tier always stays.
|
|
29
|
+
// Unknown values (routing "?", no TTFT or rate yet) are left out, never shown as "?" or "-".
|
|
30
|
+
export function formatStatus(p , width ) {
|
|
31
|
+
const tier = p.tier.display;
|
|
32
|
+
const effort = p.effort === "default" ? "" : `effort ${p.effort}`;
|
|
33
|
+
const route = p.routing && p.routing !== "?" ? p.routing : "";
|
|
34
|
+
const ttft = formatTtft(p.ttftMs);
|
|
35
|
+
const t = ttft === "-" ? "" : `TTFT ${ttft}`;
|
|
36
|
+
const r = formatRate(p.tokPerSec);
|
|
37
|
+
const rate = r === "-" ? "" : r;
|
|
38
|
+
const toks = `${groupDigits(p.tokens)} tokens`;
|
|
39
|
+
const short = `${groupDigits(p.tokens)} tok`;
|
|
40
|
+
const variants = [
|
|
41
|
+
[tier, effort, route, t, rate, toks],
|
|
42
|
+
[tier, route, t, rate, toks],
|
|
43
|
+
[tier, route, rate, short],
|
|
44
|
+
[tier, route, short],
|
|
45
|
+
[tier, route],
|
|
46
|
+
];
|
|
47
|
+
const join = (parts ) => parts.filter((x) => x !== "").join(" | ");
|
|
48
|
+
if (width === undefined) return join(variants[0] );
|
|
49
|
+
for (const v of variants) {
|
|
50
|
+
const s = join(v);
|
|
51
|
+
if (s.length <= width) return s;
|
|
52
|
+
}
|
|
53
|
+
const last = join(variants[variants.length - 1] );
|
|
54
|
+
return width > 1 ? last.slice(0, width - 1) + "…" : last.slice(0, width);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
const ESC = "\x1b";
|
|
60
|
+
|
|
61
|
+
// Pins `text` to the last terminal row. All escape sequences are fixed strings made here; the text
|
|
62
|
+
// comes from formatStatus (tier names and numbers), never from the model.
|
|
63
|
+
export class StatusBar {
|
|
64
|
+
out ;
|
|
65
|
+
on ;
|
|
66
|
+
text = "";
|
|
67
|
+
installed = false;
|
|
68
|
+
paint ;
|
|
69
|
+
|
|
70
|
+
constructor(out , enabled , paint = (s) => s) {
|
|
71
|
+
this.out = out;
|
|
72
|
+
this.paint = paint;
|
|
73
|
+
// Needs room for at least the bar plus a few lines of scrollback.
|
|
74
|
+
this.on = enabled && (out.rows ?? 0) >= 6 && (out.columns ?? 0) >= 20;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get enabled() {
|
|
78
|
+
return this.on;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
rows() {
|
|
82
|
+
return this.out.rows ?? 24;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cols() {
|
|
86
|
+
return this.out.columns ?? 80;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
install() {
|
|
90
|
+
if (!this.on || this.installed) return;
|
|
91
|
+
this.installed = true;
|
|
92
|
+
// A newline guarantees a spare row below the cursor, then the cursor goes back up.
|
|
93
|
+
this.out.write(`\n${ESC}[1A${ESC}7${ESC}[1;${this.rows() - 1}r${ESC}8`);
|
|
94
|
+
this.draw();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
update(text ) {
|
|
98
|
+
this.text = text;
|
|
99
|
+
this.draw();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Terminal size changed: move the scroll region and repaint.
|
|
103
|
+
resize() {
|
|
104
|
+
if (!this.on || !this.installed) return;
|
|
105
|
+
this.out.write(`${ESC}7${ESC}[1;${this.rows() - 1}r${ESC}8`);
|
|
106
|
+
this.draw();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
draw() {
|
|
110
|
+
if (!this.on || !this.installed) return;
|
|
111
|
+
const line = this.text.length > this.cols() - 1 ? this.text.slice(0, this.cols() - 2) + "…" : this.text;
|
|
112
|
+
this.out.write(`${ESC}7${ESC}[${this.rows()};1H${ESC}[2K${this.paint(line)}${ESC}8`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Gives the whole screen back and clears the bar row.
|
|
116
|
+
uninstall() {
|
|
117
|
+
if (!this.on || !this.installed) return;
|
|
118
|
+
this.installed = false;
|
|
119
|
+
this.out.write(`${ESC}7${ESC}[r${ESC}[${this.rows()};1H${ESC}[2K${ESC}8`);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// The Synqa palette and every colour escape the CLI emits. This file is the only place that builds
|
|
2
|
+
// an escape sequence for colour; text from the model or from tools never reaches it as a code.
|
|
3
|
+
// 24-bit colour only when the terminal is known to draw it (COLORTERM=truecolor|24bit, or a
|
|
4
|
+
// TERM_PROGRAM known for it); macOS Terminal.app and anything unknown get xterm-256 colours, which
|
|
5
|
+
// every modern terminal draws. NO_COLOR, a non-TTY or TERM=dumb turn all of it off.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export const TERRACOTTA = [0xc8, 0x5a, 0x32]; // #c85a32 accent: the period, spinner, prompt, monogram
|
|
10
|
+
export const BONE = [0xf0, 0xea, 0xd8]; // #f0ead8 warm bone: name and active values
|
|
11
|
+
export const STONE = [0x9e, 0x95, 0x85]; // #9e9585 warm stone: borders, labels, footer
|
|
12
|
+
export const SAGE = [0x4a, 0x7c, 0x59]; // #4a7c59 sage: balance highlights and added diff lines
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// depth is optional; absent means 256 colours, the safe choice (Terminal.app has no 24-bit colour).
|
|
17
|
+
// bg is the terminal background when the environment says so (COLORFGBG); bone text is used only
|
|
18
|
+
// on a known dark background, plain bold otherwise, so names read on light and dark themes.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
export function colorEnabled(env , isTTY ) {
|
|
23
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") return false;
|
|
24
|
+
if (env.TERM === "dumb") return false;
|
|
25
|
+
return isTTY;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Terminals that draw 24-bit colour, by TERM_PROGRAM (compared lower case).
|
|
29
|
+
const TRUECOLOR_PROGRAMS = new Set(["iterm.app", "wezterm", "vscode", "ghostty", "hyper", "tabby", "warpterminal", "rio", "kitty", "alacritty", "contour", "zed"]);
|
|
30
|
+
|
|
31
|
+
export function colorDepth(env ) {
|
|
32
|
+
const program = (env.TERM_PROGRAM ?? "").toLowerCase();
|
|
33
|
+
// Terminal.app before macOS 26 misdraws 24-bit escapes, and a COLORTERM copied from another
|
|
34
|
+
// terminal's profile (or passed through ssh or tmux) must not switch them on there.
|
|
35
|
+
if (program === "apple_terminal") return 8;
|
|
36
|
+
const c = (env.COLORTERM ?? "").toLowerCase();
|
|
37
|
+
if (c === "truecolor" || c === "24bit") return 24;
|
|
38
|
+
if (TRUECOLOR_PROGRAMS.has(program)) return 24;
|
|
39
|
+
const term = (env.TERM ?? "").toLowerCase();
|
|
40
|
+
if (/(^|-)(kitty|alacritty|direct|truecolor|ghostty|wezterm)($|-)/.test(term) || term.endsWith("-direct")) return 24;
|
|
41
|
+
return 8;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// COLORFGBG ("15;0", or "15;default;0") names the background colour index when a terminal sets
|
|
45
|
+
// it (rxvt, Konsole, iTerm2 with the option on). 0-6 and 8 are dark; 7 and 9-15 are light.
|
|
46
|
+
export function detectBackground(env ) {
|
|
47
|
+
const raw = env.COLORFGBG;
|
|
48
|
+
if (!raw) return undefined;
|
|
49
|
+
const bg = Number(raw.split(";").pop());
|
|
50
|
+
if (!Number.isInteger(bg) || bg < 0 || bg > 15) return undefined;
|
|
51
|
+
return bg === 7 || bg >= 9 ? "light" : "dark";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const CUBE = [0, 95, 135, 175, 215, 255] ;
|
|
55
|
+
|
|
56
|
+
// Nearest xterm-256 colour: the best of the 6x6x6 cube (16..231) and the grey ramp (232..255).
|
|
57
|
+
export function rgbTo256(rgb ) {
|
|
58
|
+
const [r, g, b] = rgb;
|
|
59
|
+
const nearest = (v ) => {
|
|
60
|
+
let best = 0;
|
|
61
|
+
for (let i = 1; i < CUBE.length; i++) {
|
|
62
|
+
if (Math.abs((CUBE[i] ) - v) < Math.abs((CUBE[best] ) - v)) best = i;
|
|
63
|
+
}
|
|
64
|
+
return best;
|
|
65
|
+
};
|
|
66
|
+
const ri = nearest(r);
|
|
67
|
+
const gi = nearest(g);
|
|
68
|
+
const bi = nearest(b);
|
|
69
|
+
const cubeIdx = 16 + 36 * ri + 6 * gi + bi;
|
|
70
|
+
const dist = (x ) => (x[0] - r) ** 2 + (x[1] - g) ** 2 + (x[2] - b) ** 2;
|
|
71
|
+
const cubeDist = dist([CUBE[ri] , CUBE[gi] , CUBE[bi] ]);
|
|
72
|
+
const grey = Math.min(23, Math.max(0, Math.round(((r + g + b) / 3 - 8) / 10)));
|
|
73
|
+
const level = 8 + 10 * grey;
|
|
74
|
+
const greyDist = dist([level, level, level]);
|
|
75
|
+
return greyDist < cubeDist ? 232 + grey : cubeIdx;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const RESET = "\x1b[0m";
|
|
79
|
+
|
|
80
|
+
// The SGR parameters that select `rgb` as the foreground.
|
|
81
|
+
// Hand-picked 256-colour stand-ins where the nearest match loses the brand: the nearest index for
|
|
82
|
+
// terracotta (167) is a pink salmon; 166 (#d75f00) keeps the burnt orange and reads on light and
|
|
83
|
+
// dark backgrounds.
|
|
84
|
+
const TUNED_256 = new Map ([[TERRACOTTA.join(","), 166]]);
|
|
85
|
+
|
|
86
|
+
export function to256(rgb ) {
|
|
87
|
+
return TUNED_256.get(rgb.join(",")) ?? rgbTo256(rgb);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function fgParams(depth , rgb ) {
|
|
91
|
+
return depth === 24 ? `38;2;${rgb[0]};${rgb[1]};${rgb[2]}` : `38;5;${to256(rgb)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function paint(s , params , text ) {
|
|
95
|
+
return s.color ? `\x1b[${params}m${text}${RESET}` : text;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function terracotta(s , text ) {
|
|
99
|
+
return paint(s, fgParams(s.depth, TERRACOTTA), text);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Bone on a known dark background; plain bold (the terminal's own text colour) otherwise, because
|
|
103
|
+
// bone is nearly invisible on a light background.
|
|
104
|
+
export function boneBold(s , text ) {
|
|
105
|
+
return paint(s, s.bg === "dark" ? `1;${fgParams(s.depth, BONE)}` : "1", text);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function terracottaBold(s , text ) {
|
|
109
|
+
return paint(s, `1;${fgParams(s.depth, TERRACOTTA)}`, text);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function stone(s , text ) {
|
|
113
|
+
return paint(s, fgParams(s.depth, STONE), text);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Stone and bold together: labels stay readable on a light terminal background.
|
|
117
|
+
export function stoneBold(s , text ) {
|
|
118
|
+
return paint(s, `1;${fgParams(s.depth, STONE)}`, text);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function sage(s , text ) {
|
|
122
|
+
return paint(s, fgParams(s.depth, SAGE), text);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Kept names: the accent is terracotta.
|
|
126
|
+
export const accent = terracotta;
|
|
127
|
+
|
|
128
|
+
export function dim(s , text ) {
|
|
129
|
+
return paint(s, "2", text);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function bold(s , text ) {
|
|
133
|
+
return paint(s, "1", text);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function green(s , text ) {
|
|
137
|
+
return paint(s, "32", text);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function red(s , text ) {
|
|
141
|
+
return paint(s, "31", text);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function cyan(s , text ) {
|
|
145
|
+
return paint(s, "36", text);
|
|
146
|
+
}
|
package/dist/tiers.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export const TIERS = [
|
|
4
|
+
{ key: "blink", id: "synqa-blink-5.5", display: "Blink 5.5" },
|
|
5
|
+
{ key: "sonata", id: "synqa-sonata-6.0", display: "Sonata 6.0" },
|
|
6
|
+
{ key: "aeon", id: "synqa-aeon-6.5", display: "Aeon 6.5" },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_TIER = "blink";
|
|
10
|
+
|
|
11
|
+
// Reasoning effort the user can pick. "default" sends no reasoning_effort at all.
|
|
12
|
+
export const EFFORTS = ["default", "low", "medium"] ;
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export function findEffort(name ) {
|
|
16
|
+
const n = name.trim().toLowerCase();
|
|
17
|
+
return EFFORTS.find((e) => e === n);
|
|
18
|
+
}
|
|
19
|
+
export const CONTEXT_TOKENS = 262144;
|
|
20
|
+
export const MAX_OUTPUT_TOKENS = 16384;
|
|
21
|
+
export const REQUEST_TIMEOUT_MS = 600_000;
|
|
22
|
+
|
|
23
|
+
export function findTier(name ) {
|
|
24
|
+
const n = name.trim().toLowerCase();
|
|
25
|
+
return TIERS.find((t) => t.key === n || t.id === n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Map the x-synqa-routing header to what the status bar shows.
|
|
29
|
+
// local and local-gated -> local, hybrid -> hybrid, fallback-* -> fallback, absent -> "?".
|
|
30
|
+
export function routingLabel(header ) {
|
|
31
|
+
if (!header) return "?";
|
|
32
|
+
const v = header.trim().toLowerCase();
|
|
33
|
+
if (v === "local" || v === "local-gated") return "local";
|
|
34
|
+
if (v === "hybrid") return "hybrid";
|
|
35
|
+
if (v.startsWith("fallback")) return "fallback";
|
|
36
|
+
return "?";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export function usageFrom(raw ) {
|
|
42
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
43
|
+
const r = raw ;
|
|
44
|
+
const p = r.prompt_tokens;
|
|
45
|
+
const c = r.completion_tokens;
|
|
46
|
+
if (typeof p !== "number" && typeof c !== "number") return undefined;
|
|
47
|
+
return { prompt: typeof p === "number" ? p : 0, completion: typeof c === "number" ? c : 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function statusLine(tier , routing , usage , total ) {
|
|
51
|
+
const ctx = usage.prompt > 0 ? ` | ctx ${usage.prompt}/${CONTEXT_TOKENS}` : "";
|
|
52
|
+
return `${tier.display} | ${routing} | tokens ${total.prompt} in ${total.completion} out${ctx}`;
|
|
53
|
+
}
|