@tabbio-technologies/cli 1.2.8 → 1.3.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.
- package/README.md +85 -18
- package/dist/chunks/{chunk-Y6GWIFHI.js → chunk-FO6AU5UZ.js} +3 -53
- package/dist/chunks/chunk-FO6AU5UZ.js.map +7 -0
- package/dist/chunks/{chunk-NAWA6NCZ.js → chunk-NRFVKK3S.js} +37 -96
- package/dist/chunks/chunk-NRFVKK3S.js.map +7 -0
- package/dist/chunks/chunk-TP3CRQJD.js +106 -0
- package/dist/chunks/chunk-TP3CRQJD.js.map +7 -0
- package/dist/chunks/{chunk-QVR5KIEQ.js → chunk-WRDHOZD7.js} +1796 -244
- package/dist/chunks/chunk-WRDHOZD7.js.map +7 -0
- package/dist/chunks/{chunk-7LNC3FIV.js → chunk-XR6APSWD.js} +95 -14
- package/dist/chunks/{chunk-7LNC3FIV.js.map → chunk-XR6APSWD.js.map} +4 -4
- package/dist/chunks/{entry-WENOOT6W.js → entry-E22QDWDP.js} +2023 -770
- package/dist/chunks/entry-E22QDWDP.js.map +7 -0
- package/dist/chunks/{mount-NGUARMCL.js → mount-36JUVX3R.js} +3 -2
- package/dist/chunks/{mount-NGUARMCL.js.map → mount-36JUVX3R.js.map} +1 -1
- package/dist/chunks/{program-O2BA5L6Z.js → program-HOHDF6FM.js} +314 -375
- package/dist/chunks/program-HOHDF6FM.js.map +7 -0
- package/dist/chunks/{status-I5UAQPBY.js → status-P5E6N54K.js} +3 -3
- package/dist/cli.js +1 -1
- package/package.json +2 -1
- package/dist/chunks/chunk-NAWA6NCZ.js.map +0 -7
- package/dist/chunks/chunk-QVR5KIEQ.js.map +0 -7
- package/dist/chunks/chunk-Y6GWIFHI.js.map +0 -7
- package/dist/chunks/entry-WENOOT6W.js.map +0 -7
- package/dist/chunks/program-O2BA5L6Z.js.map +0 -7
- /package/dist/chunks/{status-I5UAQPBY.js.map → status-P5E6N54K.js.map} +0 -0
package/README.md
CHANGED
|
@@ -191,6 +191,26 @@ personal token, it only deletes the local copy: the token itself stays valid,
|
|
|
191
191
|
because other clients may use it. Revoke it in Settings › MCP Access if you
|
|
192
192
|
want it gone.
|
|
193
193
|
|
|
194
|
+
### Inside the app: `/login` and `/logout`
|
|
195
|
+
|
|
196
|
+
`tabbio` opens the home screen even when you are not signed in: the account
|
|
197
|
+
line says **Not signed in**, the palette offers `/login`, `/help` and `/exit`,
|
|
198
|
+
and `/chat`, `/tools`, `/approvals` and `/status` answer "Sign in first:
|
|
199
|
+
/login" instead of failing. `/login` works on the home screen and in a chat,
|
|
200
|
+
without leaving the app:
|
|
201
|
+
|
|
202
|
+
| Command | Method |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `/login` | A chooser: open the browser (recommended), email code, or personal token |
|
|
205
|
+
| `/login browser` | Opens the approval page and waits; the link is shown too, and Esc cancels |
|
|
206
|
+
| `/login email you@example.com` | Sends a 6-digit code and asks for it in place (three tries) |
|
|
207
|
+
| `/login token` | Asks for a personal MCP token (masked) and checks it against the tool list |
|
|
208
|
+
| `/logout` | Ends the session on the server, deletes this profile's credentials and returns to the signed-out home screen |
|
|
209
|
+
|
|
210
|
+
They use the same flows and the same credential store as `tabbio login` and
|
|
211
|
+
`tabbio logout`, and the home screen, the plan, the tool count and your skills
|
|
212
|
+
refresh for the new account right away.
|
|
213
|
+
|
|
194
214
|
## Commands
|
|
195
215
|
|
|
196
216
|
```
|
|
@@ -209,7 +229,7 @@ tabbio workflows says that workflows are not exposed ove
|
|
|
209
229
|
tabbio approvals [list] [--json] ; approvals show <id> ; approvals approve <id> [--reason r] [--wait] ; approvals reject <id> [--reason r] ; approvals wait <id> [--timeout 10m]
|
|
210
230
|
tabbio artifacts [list] [--kind k] [--status s] ; artifacts get|open|versions|delete|wait <id> ; artifacts download <id> [--format f] [--out p] [--force] [--wait] ; artifacts restore <id> <versionId>
|
|
211
231
|
tabbio ask "<question>" [--mode m] [--via-mcp] [--json] one question through the chat stream (app session)
|
|
212
|
-
tabbio chat [message] [--mode seeker|employer] [--company <id>] [--thread <id>] [--model <id>] [--research auto|on|off] [--image auto|on|off] [--result auto|summary|document|slides|page|image] [--save] [--json]
|
|
232
|
+
tabbio chat [message] [--mode seeker|employer] [--company <id>] [--thread <id>] [--model <id>] [--research auto|on|off] [--image auto|on|off] [--result auto|summary|document|slides|page|image] [--remember] [--no-save] [--json]
|
|
213
233
|
tabbio mcp serve [--profile p] stdio ↔ remote MCP bridge for stdio-only clients
|
|
214
234
|
tabbio mcp url prints the MCP URL for `claude mcp add`
|
|
215
235
|
tabbio completion bash|zsh|fish
|
|
@@ -347,9 +367,14 @@ tabbio ask "Summarise my application pipeline" --json
|
|
|
347
367
|
|
|
348
368
|
One question, one answer. With an app session (browser or email sign-in) the
|
|
349
369
|
question goes through the chat stream (`POST /api/agent/chat`), exactly like a
|
|
350
|
-
one-message `tabbio chat`: the answer streams,
|
|
351
|
-
|
|
352
|
-
|
|
370
|
+
one-message `tabbio chat`: the answer streams, tool results print as the app's
|
|
371
|
+
cards (numbered jobs with their apply links, CVs with `/download <id>`,
|
|
372
|
+
skills as `/<skill-id>`), sources are listed, and it is metered like any chat
|
|
373
|
+
reply. It sends the profile's saved research, image and result choice.
|
|
374
|
+
|
|
375
|
+
Files the answer makes (pages, documents, slide decks, images) and CV exports
|
|
376
|
+
are saved to `./tabbio-artifacts` (see [Saved files](#saved-files)); `--no-save`
|
|
377
|
+
skips that.
|
|
353
378
|
|
|
354
379
|
With a personal token, or with `--via-mcp`, it looks for the MCP ask tool
|
|
355
380
|
(`ask_tabbio`) instead. Today's server does not offer that tool (the MCP
|
|
@@ -365,16 +390,43 @@ tabbio chat --mode employer --company cmp_456
|
|
|
365
390
|
```
|
|
366
391
|
|
|
367
392
|
Streaming chat with the Tabbio agent, rendered as Markdown, with one-line
|
|
368
|
-
tool-call rows (`ctrl+o` expands
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
393
|
+
tool-call rows (`ctrl+o` expands the raw result) and inline approval prompts.
|
|
394
|
+
A result the app shows as a card prints as one under its row: numbered rows,
|
|
395
|
+
dim details, links (clickable where the terminal supports it, and always
|
|
396
|
+
printed as plain URLs so they can be copied) and the next step, such as
|
|
397
|
+
`/open 2` or `/apply 2` for a job or `/download <id>` for a CV. Files Tabbio
|
|
398
|
+
makes and CV exports are saved to `./tabbio-artifacts` as soon as they are
|
|
399
|
+
ready, with the path, the size and the first lines of a page or document
|
|
400
|
+
(`--no-save` or `TABBIO_NO_ARTIFACT_SAVE=1` turn that off). Inside
|
|
401
|
+
chat, the slash commands below are available. With `--json`, chat prints
|
|
402
|
+
NDJSON: one server event per line (including unknown event types), followed
|
|
403
|
+
by a local `files-saved` event for each generated file or CV export. A
|
|
404
|
+
`files-saved` event includes the saved paths or an error; save errors make
|
|
405
|
+
the command exit nonzero. Needs a full sign-in.
|
|
406
|
+
|
|
407
|
+
| Slash command | What it does |
|
|
408
|
+
|---|---|
|
|
409
|
+
| `/` | Opens the palette: the commands, then your Tabbio skills under **Skills** (loaded once per session). A bare `/` is never sent to the agent; Esc clears it. |
|
|
410
|
+
| `/<skill> [details]` | Runs one of your skills exactly like the app does (`/cv-health-check`). A `/name` that is neither a command nor a skill shows a notice and is not sent. |
|
|
411
|
+
| `/new`, `/threads` | Start a new chat, or resume a recent one |
|
|
412
|
+
| `/mode seeker\|employer`, `/model <id\|auto>` | Switch workspace or model |
|
|
413
|
+
| `/research`, `/image`, `/result` | Research, image and result modes (see below) |
|
|
414
|
+
| `/open <n>` | Opens link `n` of the last result card in the browser; the URL is printed as well |
|
|
415
|
+
| `/open <id>` | Opens a file Tabbio made (a 15-minute preview link) |
|
|
416
|
+
| `/apply <n>` | Asks Tabbio to prepare your application for job `n` of the last job result |
|
|
417
|
+
| `/save <id> [folder]` | Saves a file Tabbio made into `./tabbio-artifacts` (or the folder) |
|
|
418
|
+
| `/show <id>` | Prints a saved page or document in the terminal (saves it first when needed) |
|
|
419
|
+
| `/download [cvId] [pdf\|docx\|txt\|md]` | Saves a CV export; without an id, your main CV |
|
|
420
|
+
| `/login [email you@x.com\|token]`, `/logout` | Sign in or out without leaving the app (see [Signing in](#signing-in)) |
|
|
421
|
+
| `/help`, `/home`, `/exit` | Keys and commands, back to the home screen, quit |
|
|
422
|
+
|
|
423
|
+
Esc interrupts a reply, clears the composer, or goes back to the home screen;
|
|
424
|
+
Ctrl+C twice quits from any screen.
|
|
373
425
|
|
|
374
426
|
```sh
|
|
375
427
|
tabbio chat --research on "What changed in UAE labour law this year? Two sources."
|
|
376
428
|
tabbio chat --result document "A one-page brief on my job search"
|
|
377
|
-
tabbio chat --result page --
|
|
429
|
+
tabbio chat --result page --remember # every chat on this profile asks for pages until changed
|
|
378
430
|
```
|
|
379
431
|
|
|
380
432
|
See [Research, documents, pages and images](#research-documents-pages-and-images).
|
|
@@ -387,7 +439,7 @@ tabbio artifacts list --kind document --json
|
|
|
387
439
|
tabbio artifacts get art_123
|
|
388
440
|
tabbio artifacts open art_123 # prints a 15-minute link and opens it (on a terminal)
|
|
389
441
|
tabbio artifacts open art_123 --variant pdf --print
|
|
390
|
-
tabbio artifacts download art_123 #
|
|
442
|
+
tabbio artifacts download art_123 # ./tabbio-artifacts/<title>-<id>.pdf (documents), .pptx (slides), .html (pages), the image
|
|
391
443
|
tabbio artifacts download art_123 --format html --out ~/Downloads --force
|
|
392
444
|
tabbio artifacts wait art_123 --timeout 2m # exit 0 ready, 1 failed, 7 still rendering
|
|
393
445
|
tabbio artifacts versions art_123
|
|
@@ -396,8 +448,10 @@ tabbio artifacts delete art_123 --yes
|
|
|
396
448
|
```
|
|
397
449
|
|
|
398
450
|
The documents, slide decks, pages and images Tabbio made for you. `download`
|
|
399
|
-
|
|
400
|
-
|
|
451
|
+
saves into `./tabbio-artifacts` (or `TABBIO_ARTIFACTS_DIR`) unless `--out`
|
|
452
|
+
says otherwise, prints the path, never overwrites a file (a taken name gets
|
|
453
|
+
`-2`; with `--out`, it stops unless you pass `--force`), writes through a
|
|
454
|
+
temporary file, and names the file after the title. It fetches a signed link (15
|
|
401
455
|
minutes) without sending your session to the storage host. `delete` asks
|
|
402
456
|
first unless `--yes` (and a published site of that file goes offline). Needs a
|
|
403
457
|
full sign-in; with a personal token, `tabbio artifact list|get|read` reach the
|
|
@@ -438,8 +492,8 @@ The output format is chosen in this order:
|
|
|
438
492
|
| `--plain` | Tab-separated values, no colour. |
|
|
439
493
|
| `--fields a,b` | Only these fields (tables, plain and JSON). |
|
|
440
494
|
|
|
441
|
-
`tabbio chat --json` is the exception: it writes NDJSON, one
|
|
442
|
-
|
|
495
|
+
`tabbio chat --json` is the exception: it writes NDJSON, one event per line,
|
|
496
|
+
including local `files-saved` events after the server stream.
|
|
443
497
|
|
|
444
498
|
Colour is decided in this order: `--color` / `--no-color`, then `FORCE_COLOR`,
|
|
445
499
|
then `NO_COLOR`, then `CLICOLOR_FORCE` / `CLICOLOR`, then `TERM=dumb` (no
|
|
@@ -491,6 +545,16 @@ Files live in `$TABBIO_CONFIG_DIR`, else `$XDG_CONFIG_HOME/tabbio`, else
|
|
|
491
545
|
The tool catalog cache, update-check stamp and recent-runs history live in
|
|
492
546
|
`$TABBIO_CACHE_DIR`, else `$XDG_CACHE_HOME/tabbio`, else `~/.cache/tabbio`.
|
|
493
547
|
|
|
548
|
+
### Saved files
|
|
549
|
+
|
|
550
|
+
`chat` and `ask` save what Tabbio makes as soon as it is ready: a page as
|
|
551
|
+
`.html`, a document as `.html` and `.pdf`, slides as `.pptx`, an image as its
|
|
552
|
+
own format, and a CV export as the PDF (or the format you asked for). Files go
|
|
553
|
+
to `TABBIO_ARTIFACTS_DIR`, else `./tabbio-artifacts` in the current directory,
|
|
554
|
+
named `<title>-<last 6 characters of the id>.<ext>`, and are never
|
|
555
|
+
overwritten (`-2`, `-3`). `--no-save` or `TABBIO_NO_ARTIFACT_SAVE=1` turn it
|
|
556
|
+
off.
|
|
557
|
+
|
|
494
558
|
Other environment variables:
|
|
495
559
|
|
|
496
560
|
| Variable | Effect |
|
|
@@ -498,6 +562,8 @@ Other environment variables:
|
|
|
498
562
|
| `TABBIO_OUTPUT` | Default output format: `json`, `table` or `plain`. |
|
|
499
563
|
| `TABBIO_THEME` | `light` or `dark`, when the accent colour guesses the terminal background wrong. |
|
|
500
564
|
| `TABBIO_DEBUG=1` | Same as `--debug`. |
|
|
565
|
+
| `TABBIO_ARTIFACTS_DIR` | Where `chat`, `ask` and `artifacts download` save files. Default: `./tabbio-artifacts`. |
|
|
566
|
+
| `TABBIO_NO_ARTIFACT_SAVE=1` | Do not save files from `chat` and `ask` (same as `--no-save`). |
|
|
501
567
|
| `TABBIO_DEVICE_LABEL` | The name this computer shows on the sign-in page and in your sessions. Defaults to the hostname. |
|
|
502
568
|
| `TABBIO_NO_UPDATE_CHECK=1` | Skip the once-a-day check for a newer version on npm (printed on stderr). Also skipped under `CI`. |
|
|
503
569
|
| `TABBIO_ALLOW_INSECURE_HTTP=1` | Allow plain `http://` API URLs other than localhost. Development only. |
|
|
@@ -595,7 +661,7 @@ controls under the app's message box:
|
|
|
595
661
|
They are sent with every message in seeker chats, together with your
|
|
596
662
|
timezone (`Intl` timezone, so "today" and the daily limits follow your day).
|
|
597
663
|
Employer chats do not send them; the server ignores them there. Flags apply to
|
|
598
|
-
one run; `/research on` in the chat and `tabbio chat … --
|
|
664
|
+
one run; `/research on` in the chat and `tabbio chat … --remember` remember the
|
|
599
665
|
choice for the profile (`profiles.<name>.capabilities` in `config.json`). The
|
|
600
666
|
status bar shows every control you changed from `auto`. If your account
|
|
601
667
|
cannot use a control you switched on (the server has it off, you are outside
|
|
@@ -617,13 +683,14 @@ What you see in the chat:
|
|
|
617
683
|
- **Limits** (daily quota, plan tier, a switched-off feature) as a dim line
|
|
618
684
|
under the tool row with the reason.
|
|
619
685
|
|
|
620
|
-
`tabbio chat --json` passes every event through, including `artifact-draft`
|
|
686
|
+
`tabbio chat --json` passes every server event through, including `artifact-draft`
|
|
621
687
|
(the streaming writer's progress). Research sources are in the
|
|
622
688
|
`tool-result` payloads of `webSearch`, `readUrl`, `readPdf`,
|
|
623
689
|
`readGithubRepo` and `readSkill` (`result.sources`); files are in the
|
|
624
690
|
`tool-result` payloads of `createDocument`, `createPage`, `generateImage`,
|
|
625
691
|
`updatePage`, `updateDocument` and `editImage` (`result.artifactId`,
|
|
626
|
-
`result.status`: `pending`, `ready` or `failed`).
|
|
692
|
+
`result.status`: `pending`, `ready` or `failed`). Generated files also produce
|
|
693
|
+
local `files-saved` events after the turn unless `--no-save` is set.
|
|
627
694
|
|
|
628
695
|
Over MCP (`tabbio research …`, `tabbio artifact …`, `tabbio site …`), the
|
|
629
696
|
file tools and `site publish` always need your approval first, because they
|
|
@@ -1,73 +1,25 @@
|
|
|
1
1
|
import { createRequire as __tabbioCreateRequire } from 'node:module';
|
|
2
2
|
const require = __tabbioCreateRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
|
-
ApiClient,
|
|
5
4
|
CliError,
|
|
6
5
|
ExitCode,
|
|
7
|
-
McpSession,
|
|
8
6
|
configPaths,
|
|
7
|
+
createCommandContext,
|
|
9
8
|
describeMcpBearer,
|
|
10
9
|
fingerprint,
|
|
11
|
-
getGlobalOptions,
|
|
12
10
|
getMcpAccessState,
|
|
13
11
|
heading,
|
|
14
|
-
isInteractive,
|
|
15
|
-
loadCredentials,
|
|
16
12
|
notSignedInError,
|
|
17
13
|
printJson,
|
|
18
14
|
printKeyValues,
|
|
19
15
|
readCachedCatalog,
|
|
20
16
|
relativeTime,
|
|
21
|
-
resolveProfileWithSources,
|
|
22
|
-
setGlobalOptions,
|
|
23
17
|
writeOut
|
|
24
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-XR6APSWD.js";
|
|
25
19
|
import {
|
|
26
20
|
theme
|
|
27
21
|
} from "./chunk-VHHZFMIF.js";
|
|
28
22
|
|
|
29
|
-
// src/core/context.ts
|
|
30
|
-
function readGlobalOptions(cmd) {
|
|
31
|
-
const raw = cmd ? cmd.optsWithGlobals() : {};
|
|
32
|
-
const current = getGlobalOptions();
|
|
33
|
-
return setGlobalOptions({
|
|
34
|
-
profile: raw.profile ?? current.profile,
|
|
35
|
-
apiUrl: raw.apiUrl ?? current.apiUrl,
|
|
36
|
-
appUrl: raw.appUrl ?? current.appUrl,
|
|
37
|
-
json: Boolean(raw.json ?? current.json),
|
|
38
|
-
color: raw.color ?? current.color,
|
|
39
|
-
debug: Boolean(raw.debug ?? current.debug),
|
|
40
|
-
yes: Boolean(raw.yes ?? current.yes),
|
|
41
|
-
quiet: Boolean(raw.quiet ?? current.quiet)
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
function createCommandContext(cmd) {
|
|
45
|
-
const globals = readGlobalOptions(cmd);
|
|
46
|
-
const { profile, sources } = resolveProfileWithSources({
|
|
47
|
-
profile: globals.profile,
|
|
48
|
-
apiUrl: globals.apiUrl,
|
|
49
|
-
appUrl: globals.appUrl
|
|
50
|
-
});
|
|
51
|
-
const credentials = loadCredentials(profile.name);
|
|
52
|
-
const api = new ApiClient(profile, credentials);
|
|
53
|
-
let session = null;
|
|
54
|
-
return {
|
|
55
|
-
globals,
|
|
56
|
-
profile,
|
|
57
|
-
sources,
|
|
58
|
-
credentials,
|
|
59
|
-
api,
|
|
60
|
-
interactive: isInteractive() && !globals.json,
|
|
61
|
-
mcp() {
|
|
62
|
-
session ??= McpSession.connect(profile, api.credentials ?? credentials, { api });
|
|
63
|
-
return session;
|
|
64
|
-
},
|
|
65
|
-
async close() {
|
|
66
|
-
if (session) await (await session.catch(() => null))?.close();
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
23
|
// src/commands/whoami.ts
|
|
72
24
|
function pickMe(raw) {
|
|
73
25
|
if (!raw || typeof raw.id !== "string") return null;
|
|
@@ -217,11 +169,9 @@ function registerStatusCommand(program) {
|
|
|
217
169
|
}
|
|
218
170
|
|
|
219
171
|
export {
|
|
220
|
-
readGlobalOptions,
|
|
221
|
-
createCommandContext,
|
|
222
172
|
registerWhoamiCommand,
|
|
223
173
|
collectStatus,
|
|
224
174
|
mcpSummary,
|
|
225
175
|
registerStatusCommand
|
|
226
176
|
};
|
|
227
|
-
//# sourceMappingURL=chunk-
|
|
177
|
+
//# sourceMappingURL=chunk-FO6AU5UZ.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/commands/whoami.ts", "../../src/commands/status.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Command } from 'commander';\nimport { createCommandContext } from '../core/context';\nimport { fingerprint } from '../core/credentials';\nimport { notSignedInError } from '../core/errors';\nimport { printJson, writeOut } from '../core/io';\nimport { theme } from '../ui/theme';\n\n/** Fields of `GET /api/users/me` that are safe and useful to print. */\nexport type Me = {\n id: string;\n email: string;\n name: string | null;\n username: string | null;\n role?: string | null;\n status?: string | null;\n isOnboardingCompleted?: boolean;\n};\n\nexport function pickMe(raw: Record<string, unknown> | null | undefined): Me | null {\n if (!raw || typeof raw.id !== 'string') return null;\n const str = (value: unknown) => (typeof value === 'string' ? value : null);\n return {\n id: raw.id,\n email: str(raw.email) ?? '',\n name: str(raw.name),\n username: str(raw.username),\n role: str(raw.role),\n status: str(raw.status),\n ...(typeof raw.isOnboardingCompleted === 'boolean' ? { isOnboardingCompleted: raw.isOnboardingCompleted } : {}),\n };\n}\n\nexport function registerWhoamiCommand(program: Command): void {\n program\n .command('whoami')\n .description('Show the signed-in account')\n .action(async (_opts: unknown, cmd: Command) => {\n const ctx = createCommandContext(cmd);\n const creds = ctx.credentials;\n if (!creds?.accessToken && !creds?.mcpToken) throw notSignedInError(ctx.profile.name);\n\n if (!creds.accessToken) {\n // MCP-only: identity is not queryable without the app session.\n if (ctx.globals.json) {\n printJson({ profile: ctx.profile.name, mode: 'mcp-token', user: creds.user ?? null, mcpToken: fingerprint(creds.mcpToken ?? '') });\n return;\n }\n writeOut(creds.user?.email ?? `Personal MCP token ${fingerprint(creds.mcpToken ?? '')}`);\n writeOut(theme.dim(' MCP-only sign-in; run `tabbio login` for full account details.'));\n return;\n }\n\n const me = pickMe(await ctx.api.json<Record<string, unknown>>('/api/users/me'));\n if (!me) throw notSignedInError(ctx.profile.name);\n if (ctx.globals.json) {\n printJson({ profile: ctx.profile.name, mode: 'session', user: me });\n return;\n }\n writeOut(me.name ? `${me.name} <${me.email}>` : me.email);\n if (me.username) writeOut(theme.dim(` @${me.username} \u00B7 profile ${ctx.profile.name}`));\n });\n}\n", "import type { Command } from 'commander';\nimport type { SubscriptionStateResult } from '@tabbio/types';\nimport { type McpAccessState, getMcpAccessState } from '../core/auth';\nimport { configPaths } from '../core/config';\nimport { type CommandContext, createCommandContext } from '../core/context';\nimport { type Credentials, fingerprint } from '../core/credentials';\nimport { CliError, ExitCode } from '../core/errors';\nimport { type KvRow, heading, printJson, printKeyValues, relativeTime, writeOut } from '../core/io';\nimport { type McpCredentialKind, describeMcpBearer, readCachedCatalog } from '../core/mcp';\nimport { theme } from '../ui/theme';\nimport { type Me, pickMe } from './whoami';\n\nexport type StatusReport = {\n profile: { name: string; source: string; apiUrl: string; appUrl: string; mcpUrl: string; configDir: string };\n signedIn: boolean;\n mode: 'session' | 'mcp-token' | 'none';\n user: Me | { id: string; email: string; name?: string | null } | null;\n plan: string | null;\n session: { token: string; expiresAt: string | null; source: string; refreshable: boolean; valid: boolean | null } | null;\n /** Credential used for MCP tool calls (never the secret itself). */\n mcp: { kind: McpCredentialKind; source: 'env' | 'store'; token: string; label: string } | null;\n pendingApprovals: number | null;\n catalog: { tools: number; fetchedAt: string; fresh: boolean } | null;\n problems: string[];\n};\n\nfunction settledError(result: PromiseSettledResult<unknown>): CliError | null {\n if (result.status === 'fulfilled') return null;\n return result.reason instanceof CliError ? result.reason : null;\n}\n\nexport async function collectStatus(ctx: CommandContext): Promise<StatusReport> {\n const { profile, credentials: creds } = ctx;\n const report: StatusReport = {\n profile: { ...profile, source: ctx.sources.name, configDir: configPaths().dir },\n signedIn: Boolean(creds?.accessToken || creds?.mcpToken),\n mode: creds?.accessToken ? 'session' : creds?.mcpToken ? 'mcp-token' : 'none',\n user: creds?.user ?? null,\n plan: null,\n session: creds?.accessToken\n ? {\n token: fingerprint(creds.accessToken),\n expiresAt: creds.accessTokenExpiresAt ?? null,\n source: creds.origin?.accessToken ?? 'store',\n refreshable: Boolean(creds.refreshToken),\n valid: null,\n }\n : null,\n mcp: mcpSummary(creds),\n pendingApprovals: null,\n catalog: null,\n problems: [],\n };\n\n const cached = readCachedCatalog(profile, creds);\n if (cached) {\n report.catalog = { tools: cached.tools.length, fetchedAt: cached.fetchedAt.toISOString(), fresh: cached.fresh };\n }\n if (!creds?.accessToken) return report;\n\n const [me, access, subscription] = await Promise.allSettled([\n ctx.api.json<Record<string, unknown>>('/api/users/me'),\n getMcpAccessState(ctx.api),\n ctx.api.json<SubscriptionStateResult>('/api/subscription/state'),\n ]);\n\n const authFailure = [me, access, subscription]\n .map(settledError)\n .find((error) => error?.exitCode === ExitCode.Auth);\n if (report.session) report.session.valid = !authFailure;\n if (authFailure) {\n report.signedIn = Boolean(creds.mcpToken);\n report.problems.push(`Session: ${authFailure.message}. Run \\`tabbio login\\`.`);\n }\n\n if (me.status === 'fulfilled') report.user = pickMe(me.value) ?? report.user;\n if (subscription.status === 'fulfilled') report.plan = subscription.value?.snapshot?.tier ?? null;\n\n if (access.status === 'fulfilled') {\n const state: McpAccessState = access.value;\n report.pendingApprovals = state.pendingApprovals?.length ?? 0;\n } else if (!authFailure) {\n const error = settledError(access);\n report.problems.push(`Approvals: ${error?.message ?? 'unavailable'}`);\n }\n return report;\n}\n\nexport function mcpSummary(creds: Credentials | null): StatusReport['mcp'] {\n const bearer = describeMcpBearer(creds);\n if (!bearer) return null;\n const envName = bearer.kind === 'personal-token' ? 'TABBIO_TOKEN' : 'TABBIO_ACCESS_TOKEN';\n const base = bearer.kind === 'app-session' ? 'app session' : `personal token ${bearer.fingerprint}`;\n return {\n kind: bearer.kind,\n source: bearer.source,\n token: bearer.fingerprint,\n label: bearer.source === 'env' ? `${base} (from ${envName})` : base,\n };\n}\n\nfunction printStatus(report: StatusReport): void {\n const user = report.user;\n const who = user ? (user.name ? `${user.name} <${user.email}>` : user.email) : undefined;\n const session = report.session;\n const rows: KvRow[] = [\n ['Profile', `${report.profile.name} ${theme.dim(`(${report.profile.source})`)}`],\n ['API', report.profile.apiUrl],\n ['MCP', report.profile.mcpUrl],\n ['Account', who ?? (report.signedIn ? theme.dim('personal MCP token (no session)') : theme.warn('not signed in'))],\n ['Plan', report.plan],\n [\n 'Session',\n session\n ? [\n session.token,\n session.source === 'env' ? 'from TABBIO_ACCESS_TOKEN' : null,\n session.expiresAt ? `expires ${relativeTime(new Date(session.expiresAt))}` : null,\n session.refreshable ? 'auto-refresh' : null,\n session.valid === false ? theme.error('expired') : null,\n ]\n .filter(Boolean)\n .join(' \u00B7 ')\n : undefined,\n ],\n ['Tools via', report.mcp?.label],\n [\n 'Approvals',\n report.pendingApprovals === null\n ? undefined\n : report.pendingApprovals > 0\n ? `${theme.accent(`${report.pendingApprovals} pending`)} ${theme.dim('\u2192 tabbio approvals')}`\n : 'none pending',\n ],\n [\n 'Catalog',\n report.catalog\n ? `${report.catalog.tools} tools \u00B7 cached ${relativeTime(new Date(report.catalog.fetchedAt))}${report.catalog.fresh ? '' : theme.dim(' (stale)')}`\n : undefined,\n ],\n ];\n writeOut(heading('Tabbio CLI status'));\n printKeyValues(rows);\n for (const problem of report.problems) writeOut(`${theme.warn(theme.symbols.warning)} ${problem}`);\n if (!report.signedIn) writeOut(theme.dim('Run `tabbio login` to sign in.'));\n}\n\nexport function registerStatusCommand(program: Command): void {\n program\n .command('status')\n .description('Show profile, account, plan, tokens (fingerprints only), approvals and catalog age')\n .action(async (_opts: unknown, cmd: Command) => {\n const ctx = createCommandContext(cmd);\n const report = await collectStatus(ctx);\n if (ctx.globals.json) printJson(report);\n else printStatus(report);\n if (!report.signedIn) process.exitCode = ExitCode.Auth;\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createRequire as __tabbioCreateRequire } from 'node:module';
|
|
2
2
|
const require = __tabbioCreateRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
|
-
SPINNER_INTERVAL_MS,
|
|
5
4
|
theme
|
|
6
5
|
} from "./chunk-VHHZFMIF.js";
|
|
7
6
|
|
|
@@ -10,6 +9,39 @@ var ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*
|
|
|
10
9
|
function stripAnsi(text) {
|
|
11
10
|
return text.replace(ANSI_PATTERN, "");
|
|
12
11
|
}
|
|
12
|
+
function safeTerminalText(text) {
|
|
13
|
+
return stripAnsi(text).replace(/\s+/g, " ").replace(/[\u0000-\u001f\u007f-\u009f]/g, "").trim();
|
|
14
|
+
}
|
|
15
|
+
var TerminalTextSanitizer = class {
|
|
16
|
+
state = "plain";
|
|
17
|
+
reset() {
|
|
18
|
+
this.state = "plain";
|
|
19
|
+
}
|
|
20
|
+
write(text) {
|
|
21
|
+
let clean = "";
|
|
22
|
+
for (const char of text) {
|
|
23
|
+
const code = char.charCodeAt(0);
|
|
24
|
+
if (this.state === "plain") {
|
|
25
|
+
if (code === 27) this.state = "escape";
|
|
26
|
+
else if (code === 155) this.state = "csi";
|
|
27
|
+
else if (code === 144 || code === 157 || code === 158 || code === 159) this.state = "string";
|
|
28
|
+
else if (char === "\n" || char === " " || code >= 32 && !(code >= 127 && code <= 159)) clean += char;
|
|
29
|
+
} else if (this.state === "escape") {
|
|
30
|
+
if (char === "[") this.state = "csi";
|
|
31
|
+
else if (char === "]" || char === "P" || char === "^" || char === "_") this.state = "string";
|
|
32
|
+
else if (code !== 27) this.state = "plain";
|
|
33
|
+
} else if (this.state === "csi") {
|
|
34
|
+
if (code >= 64 && code <= 126) this.state = "plain";
|
|
35
|
+
else if (code === 27) this.state = "escape";
|
|
36
|
+
} else if (this.state === "string") {
|
|
37
|
+
if (code === 7 || code === 156) this.state = "plain";
|
|
38
|
+
else if (code === 27) this.state = "string-escape";
|
|
39
|
+
} else if (char === "\\" || code === 7 || code === 156) this.state = "plain";
|
|
40
|
+
else if (code !== 27) this.state = "string";
|
|
41
|
+
}
|
|
42
|
+
return clean;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
13
45
|
function charWidth(code) {
|
|
14
46
|
if (code < 32 || code >= 127 && code < 160) return 0;
|
|
15
47
|
if (code >= 768 && code <= 879 || code >= 8203 && code <= 8207 || code === 65039) return 0;
|
|
@@ -146,92 +178,10 @@ function estimateHeight(text, width) {
|
|
|
146
178
|
return text.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil(displayWidth(line) / cols)), 0);
|
|
147
179
|
}
|
|
148
180
|
|
|
149
|
-
// src/ui/components/Spinner.tsx
|
|
150
|
-
import { Text } from "ink";
|
|
151
|
-
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
152
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
153
|
-
var TickContext = createContext(0);
|
|
154
|
-
var SubscribeContext = createContext(null);
|
|
155
|
-
function SpinnerProvider({ children }) {
|
|
156
|
-
const [tick, setTick] = useState(0);
|
|
157
|
-
const [subscribers, setSubscribers] = useState(0);
|
|
158
|
-
useEffect(() => {
|
|
159
|
-
if (subscribers === 0) return void 0;
|
|
160
|
-
const id = setInterval(() => setTick((t) => (t + 1) % 1e6), SPINNER_INTERVAL_MS);
|
|
161
|
-
return () => clearInterval(id);
|
|
162
|
-
}, [subscribers]);
|
|
163
|
-
const subscribe = useCallback(() => {
|
|
164
|
-
setSubscribers((n) => n + 1);
|
|
165
|
-
return () => setSubscribers((n) => Math.max(0, n - 1));
|
|
166
|
-
}, []);
|
|
167
|
-
return /* @__PURE__ */ jsx(SubscribeContext.Provider, { value: subscribe, children: /* @__PURE__ */ jsx(TickContext.Provider, { value: tick, children }) });
|
|
168
|
-
}
|
|
169
|
-
function useClockTick(active = true) {
|
|
170
|
-
const subscribe = useContext(SubscribeContext);
|
|
171
|
-
const tick = useContext(TickContext);
|
|
172
|
-
useEffect(() => active && subscribe ? subscribe() : void 0, [active, subscribe]);
|
|
173
|
-
return tick;
|
|
174
|
-
}
|
|
175
|
-
var Spinner = memo(function Spinner2({ label }) {
|
|
176
|
-
const tick = useClockTick(theme.animations);
|
|
177
|
-
const frames = theme.spinnerFrames;
|
|
178
|
-
const glyph = theme.animations ? frames[tick % frames.length] ?? theme.symbols.pending : theme.symbols.pending;
|
|
179
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
180
|
-
theme.accent(glyph),
|
|
181
|
-
label ? ` ${label}` : ""
|
|
182
|
-
] });
|
|
183
|
-
});
|
|
184
|
-
var Elapsed = memo(function Elapsed2({ since, prefix = "" }) {
|
|
185
|
-
useClockTick(true);
|
|
186
|
-
return /* @__PURE__ */ jsx(Text, { children: theme.dim(`${prefix}${formatElapsed(Date.now() - since)}`) });
|
|
187
|
-
});
|
|
188
|
-
function Working({ label, since }) {
|
|
189
|
-
const text = useMemo(() => theme.dim(label), [label]);
|
|
190
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
191
|
-
/* @__PURE__ */ jsx(Spinner, {}),
|
|
192
|
-
" ",
|
|
193
|
-
text,
|
|
194
|
-
" ",
|
|
195
|
-
/* @__PURE__ */ jsx(Elapsed, { since })
|
|
196
|
-
] });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// src/ui/layout.tsx
|
|
200
|
-
import { Box, useStdout } from "ink";
|
|
201
|
-
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
202
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
203
|
-
var PAGE_MARGIN = 2;
|
|
204
|
-
function readSize(stdout) {
|
|
205
|
-
return { columns: stdout.columns || 80, rows: stdout.rows || 24 };
|
|
206
|
-
}
|
|
207
|
-
function useTerminalSize() {
|
|
208
|
-
const { stdout } = useStdout();
|
|
209
|
-
const [size, setSize] = useState2(() => readSize(stdout));
|
|
210
|
-
useEffect2(() => {
|
|
211
|
-
const onResize = () => setSize(readSize(stdout));
|
|
212
|
-
stdout.on("resize", onResize);
|
|
213
|
-
return () => {
|
|
214
|
-
stdout.off("resize", onResize);
|
|
215
|
-
};
|
|
216
|
-
}, [stdout]);
|
|
217
|
-
return size;
|
|
218
|
-
}
|
|
219
|
-
function fromTerminal(columns) {
|
|
220
|
-
return columns - pageMargin(columns) * 2;
|
|
221
|
-
}
|
|
222
|
-
function pageMargin(columns) {
|
|
223
|
-
return columns < 50 ? 1 : PAGE_MARGIN;
|
|
224
|
-
}
|
|
225
|
-
function contentWidth(columns) {
|
|
226
|
-
return Math.max(20, columns - pageMargin(columns) * 2);
|
|
227
|
-
}
|
|
228
|
-
function Page({ columns, children }) {
|
|
229
|
-
const margin = pageMargin(columns);
|
|
230
|
-
return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", paddingLeft: margin, paddingRight: margin, width: columns, children });
|
|
231
|
-
}
|
|
232
|
-
|
|
233
181
|
export {
|
|
234
182
|
stripAnsi,
|
|
183
|
+
safeTerminalText,
|
|
184
|
+
TerminalTextSanitizer,
|
|
235
185
|
displayWidth,
|
|
236
186
|
truncate,
|
|
237
187
|
truncateStart,
|
|
@@ -245,15 +195,6 @@ export {
|
|
|
245
195
|
wrapWords,
|
|
246
196
|
safeJson,
|
|
247
197
|
clipLines,
|
|
248
|
-
estimateHeight
|
|
249
|
-
SpinnerProvider,
|
|
250
|
-
useClockTick,
|
|
251
|
-
Spinner,
|
|
252
|
-
Working,
|
|
253
|
-
useTerminalSize,
|
|
254
|
-
fromTerminal,
|
|
255
|
-
pageMargin,
|
|
256
|
-
contentWidth,
|
|
257
|
-
Page
|
|
198
|
+
estimateHeight
|
|
258
199
|
};
|
|
259
|
-
//# sourceMappingURL=chunk-
|
|
200
|
+
//# sourceMappingURL=chunk-NRFVKK3S.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ui/text.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Pure text helpers for the Ink screens: ANSI-aware width, truncation,\n * padding and the small formatters every screen shares. No React here.\n */\nimport { theme } from './theme';\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_PATTERN = /[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:;[-a-zA-Z\\d/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d/#&.:=?%@~_]*)*)?\\u0007|(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~])/g;\n\nexport function stripAnsi(text: string): string {\n return text.replace(ANSI_PATTERN, '');\n}\n\n/** Server-supplied text must never carry terminal control bytes into Ink or stdout. */\nexport function safeTerminalText(text: string): string {\n // eslint-disable-next-line no-control-regex\n return stripAnsi(text).replace(/\\s+/g, ' ').replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, '').trim();\n}\n\n/** Streamed answer text may split an ANSI escape across SSE chunks. */\nexport class TerminalTextSanitizer {\n private state: 'plain' | 'escape' | 'csi' | 'string' | 'string-escape' = 'plain';\n\n reset(): void {\n this.state = 'plain';\n }\n\n write(text: string): string {\n let clean = '';\n for (const char of text) {\n const code = char.charCodeAt(0);\n if (this.state === 'plain') {\n if (code === 0x1b) this.state = 'escape';\n else if (code === 0x9b) this.state = 'csi';\n else if (code === 0x90 || code === 0x9d || code === 0x9e || code === 0x9f) this.state = 'string';\n else if (char === '\\n' || char === '\\t' || (code >= 0x20 && !(code >= 0x7f && code <= 0x9f))) clean += char;\n } else if (this.state === 'escape') {\n if (char === '[') this.state = 'csi';\n else if (char === ']' || char === 'P' || char === '^' || char === '_') this.state = 'string';\n else if (code !== 0x1b) this.state = 'plain';\n } else if (this.state === 'csi') {\n if (code >= 0x40 && code <= 0x7e) this.state = 'plain';\n else if (code === 0x1b) this.state = 'escape';\n } else if (this.state === 'string') {\n if (code === 0x07 || code === 0x9c) this.state = 'plain';\n else if (code === 0x1b) this.state = 'string-escape';\n } else if (char === '\\\\' || code === 0x07 || code === 0x9c) this.state = 'plain';\n else if (code !== 0x1b) this.state = 'string';\n }\n return clean;\n }\n}\n\nfunction charWidth(code: number): number {\n if (code < 0x20 || (code >= 0x7f && code < 0xa0)) return 0;\n if ((code >= 0x300 && code <= 0x36f) || (code >= 0x200b && code <= 0x200f) || code === 0xfe0f) return 0;\n const wide =\n (code >= 0x1100 && code <= 0x115f) ||\n (code >= 0x2e80 && code <= 0xa4cf) ||\n (code >= 0xac00 && code <= 0xd7a3) ||\n (code >= 0xf900 && code <= 0xfaff) ||\n (code >= 0xfe30 && code <= 0xfe4f) ||\n (code >= 0xff00 && code <= 0xff60) ||\n (code >= 0xffe0 && code <= 0xffe6) ||\n (code >= 0x1f300 && code <= 0x1faff) ||\n (code >= 0x20000 && code <= 0x3fffd);\n return wide ? 2 : 1;\n}\n\n/** Terminal cells a string occupies (ANSI stripped; wide glyphs count 2). */\nexport function displayWidth(text: string): number {\n let width = 0;\n for (const char of stripAnsi(text)) width += charWidth(char.codePointAt(0) ?? 0);\n return width;\n}\n\n/** Cuts a plain string to `max` cells, ending with the theme ellipsis when shortened. */\nexport function truncate(text: string, max: number, ellipsis: string = theme.symbols.ellipsis): string {\n if (max <= 0) return '';\n if (displayWidth(text) <= max) return text;\n const budget = Math.max(0, max - displayWidth(ellipsis));\n let out = '';\n let width = 0;\n for (const char of text) {\n const w = charWidth(char.codePointAt(0) ?? 0);\n if (width + w > budget) break;\n out += char;\n width += w;\n }\n return displayWidth(ellipsis) > max ? out : `${out}${ellipsis}`;\n}\n\n/** Keeps the end of a string (for inputs scrolled to the cursor). */\nexport function truncateStart(text: string, max: number, ellipsis: string = theme.symbols.ellipsis): string {\n if (max <= 0) return '';\n if (displayWidth(text) <= max) return text;\n const chars = [...text];\n const budget = Math.max(0, max - displayWidth(ellipsis));\n let out = '';\n let width = 0;\n for (let i = chars.length - 1; i >= 0; i -= 1) {\n const char = chars[i] as string;\n const w = charWidth(char.codePointAt(0) ?? 0);\n if (width + w > budget) break;\n out = char + out;\n width += w;\n }\n return `${ellipsis}${out}`;\n}\n\nexport function padEnd(text: string, width: number): string {\n return text + ' '.repeat(Math.max(0, width - displayWidth(text)));\n}\n\nexport function padStart(text: string, width: number): string {\n return ' '.repeat(Math.max(0, width - displayWidth(text))) + text;\n}\n\n/** One line: collapses whitespace (newlines included). */\nexport function oneLine(text: string): string {\n return text.replace(/\\s+/g, ' ').trim();\n}\n\n/** \"Job Search\" / \"job_search\" \u2192 \"Job search\". */\nexport function sentenceCase(text: string): string {\n const clean = oneLine(text.replace(/[_-]+/g, ' ').replace(/([a-z0-9])([A-Z])/g, '$1 $2'));\n if (!clean) return clean;\n const lower = clean\n .split(' ')\n .map((word, i) => (i > 0 && /^[A-Z][a-z]/.test(word) ? word.toLowerCase() : word))\n .join(' ');\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\n/** `0.8s`, `12s`, `1m 04s`; under a second shows tenths. */\nexport function formatElapsed(ms: number): string {\n const safe = Math.max(0, ms);\n if (safe < 10_000) return `${(safe / 1000).toFixed(1)}s`;\n if (safe < 60_000) return `${Math.floor(safe / 1000)}s`;\n const minutes = Math.floor(safe / 60_000);\n const seconds = Math.floor((safe % 60_000) / 1000);\n return `${minutes}m ${String(seconds).padStart(2, '0')}s`;\n}\n\n/** `apr_2Hq9\u2026k1`: enough of an id to recognise it, never the whole thing. */\nexport function shortId(id: string, head = 8, tail = 2): string {\n return id.length > head + tail + 1 ? `${id.slice(0, head)}${theme.symbols.ellipsis}${id.slice(-tail)}` : id;\n}\n\nfunction scalarText(value: unknown): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n if (Array.isArray(value) && value.every((v) => typeof v !== 'object' || v === null)) {\n return value.map(String).join(', ');\n }\n return null;\n}\n\n/**\n * Dim one-line argument summary for tool rows: scalar values only, strings\n * quoted, ids bare (`\"product designer\" \u00B7 Dubai \u00B7 cv_8f2`).\n */\nexport function summarizeArgs(args: Record<string, unknown> | undefined, max = 60): string {\n if (!args) return '';\n const sep = ` ${theme.symbols.middot} `;\n const parts: string[] = [];\n for (const [key, value] of Object.entries(args)) {\n if (key === 'userId' || key === 'companyId') continue;\n if (value === true) {\n parts.push(key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase());\n continue;\n }\n const text = scalarText(value);\n if (text === null || text === '' || typeof value === 'boolean') continue;\n const clean = oneLine(text);\n const part = /id$/i.test(key) || /^[\\w-]+$/.test(clean) ? clean : `\"${truncate(clean, 32)}\"`;\n const next = [...parts, part].join(sep);\n if (displayWidth(next) > max) {\n if (parts.length === 0) parts.push(truncate(part, max));\n break;\n }\n parts.push(part);\n }\n return parts.join(sep);\n}\n\n/** Word-wraps plain text into at most `maxLines` lines (the last one ends with `\u2026` when cut). */\nexport function wrapWords(text: string, width: number, maxLines = Number.POSITIVE_INFINITY): string[] {\n const words = oneLine(text).split(' ').filter(Boolean);\n const lines: string[] = [];\n let line = '';\n for (const word of words) {\n const candidate = line ? `${line} ${word}` : word;\n if (displayWidth(candidate) <= width) {\n line = candidate;\n continue;\n }\n if (line) lines.push(line);\n line = displayWidth(word) > width ? truncate(word, width) : word;\n if (lines.length === maxLines) break;\n }\n if (line && lines.length < maxLines) lines.push(line);\n if (lines.length === maxLines && words.join(' ') !== lines.join(' ')) {\n lines[maxLines - 1] = truncate(`${lines[maxLines - 1]} \u2026`, width);\n }\n return lines.slice(0, maxLines);\n}\n\n/** Pretty JSON, never throwing (cycles and BigInt fall back to String()). */\nexport function safeJson(value: unknown, indent = 2): string {\n try {\n return JSON.stringify(value, null, indent) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/** Keeps the first `max` lines and reports how many were cut. */\nexport function clipLines(text: string, max: number): { lines: string[]; hidden: number } {\n const lines = text.split('\\n');\n if (lines.length <= max) return { lines, hidden: 0 };\n return { lines: lines.slice(0, Math.max(0, max)), hidden: lines.length - Math.max(0, max) };\n}\n\n/** Rough visual height of text wrapped at `width` (used to budget live regions). */\nexport function estimateHeight(text: string, width: number): number {\n const cols = Math.max(1, width);\n return text.split('\\n').reduce((sum, line) => sum + Math.max(1, Math.ceil(displayWidth(line) / cols)), 0);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createRequire as __tabbioCreateRequire } from 'node:module';
|
|
2
|
+
const require = __tabbioCreateRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
formatElapsed
|
|
5
|
+
} from "./chunk-NRFVKK3S.js";
|
|
6
|
+
import {
|
|
7
|
+
SPINNER_INTERVAL_MS,
|
|
8
|
+
theme
|
|
9
|
+
} from "./chunk-VHHZFMIF.js";
|
|
10
|
+
|
|
11
|
+
// src/ui/components/Spinner.tsx
|
|
12
|
+
import { Text } from "ink";
|
|
13
|
+
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
var TickContext = createContext(0);
|
|
16
|
+
var SubscribeContext = createContext(null);
|
|
17
|
+
function SpinnerProvider({ children }) {
|
|
18
|
+
const [tick, setTick] = useState(0);
|
|
19
|
+
const [subscribers, setSubscribers] = useState(0);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (subscribers === 0) return void 0;
|
|
22
|
+
const id = setInterval(() => setTick((t) => (t + 1) % 1e6), SPINNER_INTERVAL_MS);
|
|
23
|
+
return () => clearInterval(id);
|
|
24
|
+
}, [subscribers]);
|
|
25
|
+
const subscribe = useCallback(() => {
|
|
26
|
+
setSubscribers((n) => n + 1);
|
|
27
|
+
return () => setSubscribers((n) => Math.max(0, n - 1));
|
|
28
|
+
}, []);
|
|
29
|
+
return /* @__PURE__ */ jsx(SubscribeContext.Provider, { value: subscribe, children: /* @__PURE__ */ jsx(TickContext.Provider, { value: tick, children }) });
|
|
30
|
+
}
|
|
31
|
+
function useClockTick(active = true) {
|
|
32
|
+
const subscribe = useContext(SubscribeContext);
|
|
33
|
+
const tick = useContext(TickContext);
|
|
34
|
+
useEffect(() => active && subscribe ? subscribe() : void 0, [active, subscribe]);
|
|
35
|
+
return tick;
|
|
36
|
+
}
|
|
37
|
+
var Spinner = memo(function Spinner2({ label }) {
|
|
38
|
+
const tick = useClockTick(theme.animations);
|
|
39
|
+
const frames = theme.spinnerFrames;
|
|
40
|
+
const glyph = theme.animations ? frames[tick % frames.length] ?? theme.symbols.pending : theme.symbols.pending;
|
|
41
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
42
|
+
theme.accent(glyph),
|
|
43
|
+
label ? ` ${label}` : ""
|
|
44
|
+
] });
|
|
45
|
+
});
|
|
46
|
+
var Elapsed = memo(function Elapsed2({ since, prefix = "" }) {
|
|
47
|
+
useClockTick(true);
|
|
48
|
+
return /* @__PURE__ */ jsx(Text, { children: theme.dim(`${prefix}${formatElapsed(Date.now() - since)}`) });
|
|
49
|
+
});
|
|
50
|
+
function Working({ label, since }) {
|
|
51
|
+
const text = useMemo(() => theme.dim(label), [label]);
|
|
52
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
53
|
+
/* @__PURE__ */ jsx(Spinner, {}),
|
|
54
|
+
" ",
|
|
55
|
+
text,
|
|
56
|
+
" ",
|
|
57
|
+
/* @__PURE__ */ jsx(Elapsed, { since })
|
|
58
|
+
] });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/ui/layout.tsx
|
|
62
|
+
import { Box, useStdout } from "ink";
|
|
63
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
64
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
65
|
+
var PAGE_MARGIN = 2;
|
|
66
|
+
function readSize(stdout) {
|
|
67
|
+
return { columns: stdout.columns || 80, rows: stdout.rows || 24 };
|
|
68
|
+
}
|
|
69
|
+
function useTerminalSize() {
|
|
70
|
+
const { stdout } = useStdout();
|
|
71
|
+
const [size, setSize] = useState2(() => readSize(stdout));
|
|
72
|
+
useEffect2(() => {
|
|
73
|
+
const onResize = () => setSize(readSize(stdout));
|
|
74
|
+
stdout.on("resize", onResize);
|
|
75
|
+
return () => {
|
|
76
|
+
stdout.off("resize", onResize);
|
|
77
|
+
};
|
|
78
|
+
}, [stdout]);
|
|
79
|
+
return size;
|
|
80
|
+
}
|
|
81
|
+
function fromTerminal(columns) {
|
|
82
|
+
return columns - pageMargin(columns) * 2;
|
|
83
|
+
}
|
|
84
|
+
function pageMargin(columns) {
|
|
85
|
+
return columns < 50 ? 1 : PAGE_MARGIN;
|
|
86
|
+
}
|
|
87
|
+
function contentWidth(columns) {
|
|
88
|
+
return Math.max(20, columns - pageMargin(columns) * 2);
|
|
89
|
+
}
|
|
90
|
+
function Page({ columns, children }) {
|
|
91
|
+
const margin = pageMargin(columns);
|
|
92
|
+
return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", paddingLeft: margin, paddingRight: margin, width: columns, children });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
SpinnerProvider,
|
|
97
|
+
useClockTick,
|
|
98
|
+
Spinner,
|
|
99
|
+
Working,
|
|
100
|
+
useTerminalSize,
|
|
101
|
+
fromTerminal,
|
|
102
|
+
pageMargin,
|
|
103
|
+
contentWidth,
|
|
104
|
+
Page
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=chunk-TP3CRQJD.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ui/components/Spinner.tsx", "../../src/ui/layout.tsx"],
|
|
4
|
+
"sourcesContent": ["import { Text } from 'ink';\nimport { type ReactNode, createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { SPINNER_INTERVAL_MS, theme } from '../theme';\nimport { formatElapsed } from '../text';\n\n/**\n * One shared 80 ms clock for every spinner and elapsed counter (research\n * \u00A75.10). The interval only runs while at least one consumer is mounted and\n * animations are on, and it is cleared on unmount.\n */\nconst TickContext = createContext(0);\nconst SubscribeContext = createContext<(() => () => void) | null>(null);\n\nexport function SpinnerProvider({ children }: { children: ReactNode }) {\n const [tick, setTick] = useState(0);\n const [subscribers, setSubscribers] = useState(0);\n\n useEffect(() => {\n if (subscribers === 0) return undefined;\n const id = setInterval(() => setTick((t) => (t + 1) % 1_000_000), SPINNER_INTERVAL_MS);\n return () => clearInterval(id);\n }, [subscribers]);\n\n const subscribe = useCallback(() => {\n setSubscribers((n) => n + 1);\n return () => setSubscribers((n) => Math.max(0, n - 1));\n }, []);\n\n return (\n <SubscribeContext.Provider value={subscribe}>\n <TickContext.Provider value={tick}>{children}</TickContext.Provider>\n </SubscribeContext.Provider>\n );\n}\n\n/** Current clock tick; subscribes the caller to the shared clock while `active`. */\nexport function useClockTick(active = true): number {\n const subscribe = useContext(SubscribeContext);\n const tick = useContext(TickContext);\n useEffect(() => (active && subscribe ? subscribe() : undefined), [active, subscribe]);\n return tick;\n}\n\n/** Braille spinner in the accent colour; a static `\u25D0` without animation. */\nexport const Spinner = memo(function Spinner({ label }: { label?: string }) {\n const tick = useClockTick(theme.animations);\n const frames = theme.spinnerFrames;\n const glyph = theme.animations ? (frames[tick % frames.length] ?? theme.symbols.pending) : theme.symbols.pending;\n return (\n <Text>\n {theme.accent(glyph)}\n {label ? ` ${label}` : ''}\n </Text>\n );\n});\n\n/** Live elapsed time since `since` (dim), driven by the shared clock. */\nexport const Elapsed = memo(function Elapsed({ since, prefix = '' }: { since: number; prefix?: string }) {\n useClockTick(true);\n return <Text>{theme.dim(`${prefix}${formatElapsed(Date.now() - since)}`)}</Text>;\n});\n\n/** Spinner + label + elapsed, the standard \"working\" line. */\nexport function Working({ label, since }: { label: string; since: number }) {\n const text = useMemo(() => theme.dim(label), [label]);\n return (\n <Text>\n <Spinner /> {text} <Elapsed since={since} />\n </Text>\n );\n}\n", "import { Box, useStdout } from 'ink';\nimport { type ReactNode, useEffect, useState } from 'react';\n\n/** Page gutter (research mockups indent everything by two cells). */\nexport const PAGE_MARGIN = 2;\n\nexport type TerminalSize = { columns: number; rows: number };\n\nfunction readSize(stdout: NodeJS.WriteStream): TerminalSize {\n return { columns: stdout.columns || 80, rows: stdout.rows || 24 };\n}\n\n/** Terminal size that follows `resize` events (listener removed on unmount). */\nexport function useTerminalSize(): TerminalSize {\n const { stdout } = useStdout();\n const [size, setSize] = useState<TerminalSize>(() => readSize(stdout));\n useEffect(() => {\n const onResize = () => setSize(readSize(stdout));\n stdout.on('resize', onResize);\n return () => {\n stdout.off('resize', onResize);\n };\n }, [stdout]);\n return size;\n}\n\n/**\n * Breakpoints are in terminal columns (research \u00A75.5/\u00A75.6); screens get the\n * content width, so compare with `fromTerminal(n)`.\n */\nexport function fromTerminal(columns: number): number {\n return columns - pageMargin(columns) * 2;\n}\n\n/** Gutter on each side: two cells, one on very narrow terminals. */\nexport function pageMargin(columns: number): number {\n return columns < 50 ? 1 : PAGE_MARGIN;\n}\n\n/** Usable content width inside the page gutter. */\nexport function contentWidth(columns: number): number {\n return Math.max(20, columns - pageMargin(columns) * 2);\n}\n\n/**\n * Page frame: a gutter on both sides and no background. Ink clears the whole\n * terminal when the live region reaches the terminal height, so screens size\n * themselves from `rows` and stay at least one line shorter.\n */\nexport function Page({ columns, children }: { columns: number; children: ReactNode }) {\n const margin = pageMargin(columns);\n return (\n <Box flexDirection=\"column\" paddingLeft={margin} paddingRight={margin} width={columns}>\n {children}\n </Box>\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["Spinner", "Elapsed", "useEffect", "useState", "jsx"]
|
|
7
|
+
}
|