@mafdet/cli 0.3.0 → 0.4.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +22 -3
  3. package/README.zh-CN.md +17 -1
  4. package/dist/agent/loop.d.ts +17 -0
  5. package/dist/agent/loop.d.ts.map +1 -1
  6. package/dist/agent/loop.js +17 -7
  7. package/dist/agent/session.d.ts +8 -1
  8. package/dist/agent/session.d.ts.map +1 -1
  9. package/dist/agent/session.js +1 -1
  10. package/dist/commands/agent.d.ts +9 -0
  11. package/dist/commands/agent.d.ts.map +1 -1
  12. package/dist/commands/agent.js +3 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +45 -6
  15. package/dist/tui/adapters/auth.d.ts +26 -0
  16. package/dist/tui/adapters/auth.d.ts.map +1 -0
  17. package/dist/tui/adapters/auth.js +76 -0
  18. package/dist/tui/adapters/models.d.ts +47 -0
  19. package/dist/tui/adapters/models.d.ts.map +1 -0
  20. package/dist/tui/adapters/models.js +146 -0
  21. package/dist/tui/agent-surface.d.ts +25 -0
  22. package/dist/tui/agent-surface.d.ts.map +1 -0
  23. package/dist/tui/agent-surface.js +95 -0
  24. package/dist/tui/app.d.ts +25 -0
  25. package/dist/tui/app.d.ts.map +1 -0
  26. package/dist/tui/app.js +159 -0
  27. package/dist/tui/approval.d.ts +27 -0
  28. package/dist/tui/approval.d.ts.map +1 -0
  29. package/dist/tui/approval.js +79 -0
  30. package/dist/tui/banner.d.ts +13 -0
  31. package/dist/tui/banner.d.ts.map +1 -0
  32. package/dist/tui/banner.js +97 -0
  33. package/dist/tui/input.d.ts +17 -0
  34. package/dist/tui/input.d.ts.map +1 -0
  35. package/dist/tui/input.js +57 -0
  36. package/dist/tui/interrupt.d.ts +32 -0
  37. package/dist/tui/interrupt.d.ts.map +1 -0
  38. package/dist/tui/interrupt.js +47 -0
  39. package/dist/tui/keys.d.ts +30 -0
  40. package/dist/tui/keys.d.ts.map +1 -0
  41. package/dist/tui/keys.js +86 -0
  42. package/dist/tui/onboarding.d.ts +28 -0
  43. package/dist/tui/onboarding.d.ts.map +1 -0
  44. package/dist/tui/onboarding.js +117 -0
  45. package/dist/tui/picker.d.ts +24 -0
  46. package/dist/tui/picker.d.ts.map +1 -0
  47. package/dist/tui/picker.js +146 -0
  48. package/dist/tui/resize.d.ts +22 -0
  49. package/dist/tui/resize.d.ts.map +1 -0
  50. package/dist/tui/resize.js +37 -0
  51. package/dist/tui/route.d.ts +26 -0
  52. package/dist/tui/route.d.ts.map +1 -0
  53. package/dist/tui/route.js +68 -0
  54. package/dist/tui/start-agent.d.ts +31 -0
  55. package/dist/tui/start-agent.d.ts.map +1 -0
  56. package/dist/tui/start-agent.js +82 -0
  57. package/dist/tui/terminal.d.ts +41 -0
  58. package/dist/tui/terminal.d.ts.map +1 -0
  59. package/dist/tui/terminal.js +78 -0
  60. package/dist/tui/theme.d.ts +22 -0
  61. package/dist/tui/theme.d.ts.map +1 -0
  62. package/dist/tui/theme.js +59 -0
  63. package/dist/tui/tool-card.d.ts +41 -0
  64. package/dist/tui/tool-card.d.ts.map +1 -0
  65. package/dist/tui/tool-card.js +132 -0
  66. package/dist/tui/widgets.d.ts +44 -0
  67. package/dist/tui/widgets.d.ts.map +1 -0
  68. package/dist/tui/widgets.js +137 -0
  69. package/dist/tui/workspace-summary.d.ts +10 -0
  70. package/dist/tui/workspace-summary.d.ts.map +1 -0
  71. package/dist/tui/workspace-summary.js +49 -0
  72. package/package.json +1 -1
@@ -0,0 +1,97 @@
1
+ // CLI-018 · The welcome page.
2
+ //
3
+ // Pure: returns the text, writes nothing. That is what lets the tests assert
4
+ // the ASCII rule per byte instead of trusting a review.
5
+ import { stripAnsi } from "../agent/command-policy.js";
6
+ import { redact } from "../security/redact.js";
7
+ import { NARROW_COLUMNS, palette } from "./theme.js";
8
+ // Default form. Every byte is ASCII, so it survives a cp936 console.
9
+ const WORDMARK_ASCII = [
10
+ " __ __ _ _____ ____ _____ _____",
11
+ "| \\/ | / \\ | ___| _ \\| ____|_ _|",
12
+ "| |\\/| | / _ \\ | |_ | | | | _| | |",
13
+ "| | | |/ ___ \\| _| | |_| | |___ | |",
14
+ "|_| |_/_/ \\_\\_| |____/|_____| |_|",
15
+ ];
16
+ // Used only where the encoding is known to be UTF-8 (see theme.ts).
17
+ const WORDMARK_UNICODE = [
18
+ "███╗ ███╗ █████╗ ███████╗██████╗ ███████╗████████╗",
19
+ "████╗ ████║██╔══██╗██╔════╝██╔══██╗██╔════╝╚══██╔══╝",
20
+ "██╔████╔██║███████║█████╗ ██║ ██║█████╗ ██║",
21
+ "██║╚██╔╝██║██╔══██║██╔══╝ ██║ ██║██╔══╝ ██║",
22
+ "██║ ╚═╝ ██║██║ ██║██║ ██████╔╝███████╗ ██║",
23
+ "╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚══════╝ ╚═╝",
24
+ ];
25
+ const TIPS = [
26
+ "Add MAFDET.md to describe project-specific instructions",
27
+ "Use /model to switch verified models",
28
+ "Use /permissions before allowing local changes",
29
+ "Use /help for commands",
30
+ ];
31
+ /**
32
+ * The wordmark, or a single line when the terminal is too narrow to hold it
33
+ * without wrapping — a wrapped wordmark looks like a rendering bug.
34
+ */
35
+ export function wordmark(capabilities) {
36
+ const art = capabilities.unicode ? WORDMARK_UNICODE : WORDMARK_ASCII;
37
+ const widest = Math.max(...art.map((line) => [...line].length));
38
+ if (capabilities.columns < widest || capabilities.columns < NARROW_COLUMNS) {
39
+ return ["Mafdet CLI"];
40
+ }
41
+ return art;
42
+ }
43
+ /**
44
+ * Cut a line to the terminal width.
45
+ *
46
+ * Applied to plain text before any colour is added, because clamping a string
47
+ * that already contains escape sequences can slice one in half and leave the
48
+ * terminal wearing the style. Nothing here may overflow: a wrapped banner
49
+ * reads as a rendering bug.
50
+ */
51
+ function clamp(text, columns) {
52
+ const chars = [...text];
53
+ if (chars.length <= columns)
54
+ return text;
55
+ if (columns <= 3)
56
+ return chars.slice(0, columns).join("");
57
+ return chars.slice(0, columns - 3).join("") + "...";
58
+ }
59
+ export function renderBanner(input) {
60
+ const styles = palette(input.capabilities);
61
+ const { columns } = input.capabilities;
62
+ const art = wordmark(input.capabilities);
63
+ const compact = art.length === 1;
64
+ const rows = art.map((text) => ({ text, style: "accent" }));
65
+ rows.push({ text: "", style: null });
66
+ rows.push({
67
+ text: compact ? `v${input.version}` : `Mafdet CLI v${input.version}`,
68
+ style: "bold",
69
+ });
70
+ // The path is filesystem-supplied text going straight to a terminal.
71
+ rows.push({
72
+ text: `Workspace: ${redact(stripAnsi(input.workspace))}`,
73
+ style: "dim",
74
+ });
75
+ // Tips are guidance, not decoration; they go once, on the welcome page. When
76
+ // they do not fit they collapse to one line, the same way the wordmark does
77
+ // - a truncated sentence is worse than a short one that is whole.
78
+ const tipRows = TIPS.map((tip, index) => ({
79
+ text: ` ${index + 1}. ${tip}`,
80
+ style: "dim",
81
+ }));
82
+ const tipsFit = tipRows.every((row) => [...row.text].length <= columns);
83
+ rows.push({ text: "", style: null });
84
+ if (tipsFit) {
85
+ rows.push({ text: "Tips:", style: "dim" });
86
+ rows.push(...tipRows);
87
+ }
88
+ else {
89
+ rows.push({ text: "Run /help for commands", style: "dim" });
90
+ }
91
+ rows.push({ text: "", style: null });
92
+ const rendered = rows.map(({ text, style }) => {
93
+ const fitted = clamp(text, columns);
94
+ return style === null || fitted === "" ? fitted : styles[style](fitted);
95
+ });
96
+ return rendered.join("\n") + "\n";
97
+ }
@@ -0,0 +1,17 @@
1
+ import { type Key } from "./keys.js";
2
+ export interface KeySource {
3
+ nextKeys(): Promise<readonly Key[] | undefined>;
4
+ /** Stop reading and put stdin back the way it was found. */
5
+ close(): void;
6
+ }
7
+ export interface RawStdin {
8
+ setRawMode?(mode: boolean): unknown;
9
+ resume?(): unknown;
10
+ pause?(): unknown;
11
+ setEncoding?(encoding: string): unknown;
12
+ on(event: string, listener: (chunk: string) => void): unknown;
13
+ off?(event: string, listener: (chunk: string) => void): unknown;
14
+ readonly isTTY?: boolean;
15
+ }
16
+ export declare function keyboard(stdin: RawStdin): KeySource;
17
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/tui/input.ts"],"names":[],"mappings":"AAQA,OAAO,EAAc,KAAK,GAAG,EAAE,MAAM,WAAW,CAAC;AAEjD,MAAM,WAAW,SAAS;IACxB,QAAQ,IAAI,OAAO,CAAC,SAAS,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC;IAChD,4DAA4D;IAC5D,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACpC,MAAM,CAAC,IAAI,OAAO,CAAC;IACnB,KAAK,CAAC,IAAI,OAAO,CAAC;IAClB,WAAW,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;IAC9D,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CA8CnD"}
@@ -0,0 +1,57 @@
1
+ // CLI-019 · Reading the keyboard.
2
+ //
3
+ // A queue, not a per-prompt listener. CLI-014 lost pasted lines because
4
+ // readline discarded what it had already buffered between questions; the same
5
+ // shape of bug here would silently truncate a pasted API key. So every chunk
6
+ // that arrives is decoded and kept, and a widget asking for input drains the
7
+ // queue before it waits.
8
+ import { decodeKeys } from "./keys.js";
9
+ export function keyboard(stdin) {
10
+ const pending = [];
11
+ let waiting;
12
+ let ended = false;
13
+ const deliver = (keys) => {
14
+ if (waiting !== undefined) {
15
+ const resolve = waiting;
16
+ waiting = undefined;
17
+ resolve(keys);
18
+ return;
19
+ }
20
+ if (keys !== undefined)
21
+ pending.push([...keys]);
22
+ };
23
+ const onData = (chunk) => {
24
+ const keys = decodeKeys(chunk);
25
+ if (keys.length > 0)
26
+ deliver(keys);
27
+ };
28
+ const onEnd = () => {
29
+ ended = true;
30
+ deliver(undefined);
31
+ };
32
+ if (stdin.isTTY === true)
33
+ stdin.setRawMode?.(true);
34
+ stdin.setEncoding?.("utf8");
35
+ stdin.resume?.();
36
+ stdin.on("data", onData);
37
+ stdin.on("end", onEnd);
38
+ return {
39
+ async nextKeys() {
40
+ const queued = pending.shift();
41
+ if (queued !== undefined)
42
+ return queued;
43
+ if (ended)
44
+ return undefined;
45
+ return new Promise((resolve) => {
46
+ waiting = resolve;
47
+ });
48
+ },
49
+ close() {
50
+ stdin.off?.("data", onData);
51
+ stdin.off?.("end", onEnd);
52
+ if (stdin.isTTY === true)
53
+ stdin.setRawMode?.(false);
54
+ stdin.pause?.();
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,32 @@
1
+ export type InterruptOutcome = {
2
+ readonly action: "cancel-operation";
3
+ } | {
4
+ readonly action: "exit";
5
+ } | {
6
+ readonly action: "force-exit";
7
+ readonly warnUnknownBilling: boolean;
8
+ };
9
+ export interface InterruptState {
10
+ /** A model request or a local command is running. */
11
+ readonly busy: boolean;
12
+ /** A cancel was already requested and has not finished unwinding. */
13
+ readonly cleaningUp: boolean;
14
+ }
15
+ /** Pure: what a Ctrl+C means right now. */
16
+ export declare function interpretInterrupt(state: InterruptState): InterruptOutcome;
17
+ export declare const FORCED_EXIT_NOTICE: string;
18
+ /**
19
+ * Tracks whether an operation is in flight, so the interpretation above has
20
+ * something truthful to read.
21
+ */
22
+ export declare class InterruptTracker {
23
+ private busy;
24
+ private cleaningUp;
25
+ begin(): void;
26
+ finish(): void;
27
+ /** Called when a cancel has been requested but has not settled yet. */
28
+ startCleanup(): void;
29
+ state(): InterruptState;
30
+ press(): InterruptOutcome;
31
+ }
32
+ //# sourceMappingURL=interrupt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interrupt.d.ts","sourceRoot":"","sources":["../../src/tui/interrupt.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GACvC;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAA;CAAE,CAAC;AAE5E,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED,2CAA2C;AAC3C,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,gBAAgB,CAM1E;AAED,eAAO,MAAM,kBAAkB,QAE0B,CAAC;AAE1D;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,UAAU,CAAS;IAE3B,KAAK,IAAI,IAAI;IAIb,MAAM,IAAI,IAAI;IAKd,uEAAuE;IACvE,YAAY,IAAI,IAAI;IAIpB,KAAK,IAAI,cAAc;IAIvB,KAAK,IAAI,gBAAgB;CAK1B"}
@@ -0,0 +1,47 @@
1
+ // CLI-021 · Two-stage Ctrl+C.
2
+ //
3
+ // Plan §19.7. One press while something is in flight cancels that operation
4
+ // and waits for it to unwind; a press while idle leaves. The second press
5
+ // during cleanup forces the local process down, and it has to say plainly that
6
+ // the upstream may still be generating - CLI-006 established that a cancelled
7
+ // stream produced no charge in testing but that this is an observation, not a
8
+ // guarantee. Exiting quietly there would upgrade an observation into a promise.
9
+ /** Pure: what a Ctrl+C means right now. */
10
+ export function interpretInterrupt(state) {
11
+ if (state.cleaningUp) {
12
+ return { action: "force-exit", warnUnknownBilling: true };
13
+ }
14
+ if (state.busy)
15
+ return { action: "cancel-operation" };
16
+ return { action: "exit" };
17
+ }
18
+ export const FORCED_EXIT_NOTICE = "forced exit during cleanup - the upstream request may have completed, " +
19
+ "so a charge is possible; check 'mafdet account usage'";
20
+ /**
21
+ * Tracks whether an operation is in flight, so the interpretation above has
22
+ * something truthful to read.
23
+ */
24
+ export class InterruptTracker {
25
+ busy = false;
26
+ cleaningUp = false;
27
+ begin() {
28
+ this.busy = true;
29
+ }
30
+ finish() {
31
+ this.busy = false;
32
+ this.cleaningUp = false;
33
+ }
34
+ /** Called when a cancel has been requested but has not settled yet. */
35
+ startCleanup() {
36
+ this.cleaningUp = true;
37
+ }
38
+ state() {
39
+ return { busy: this.busy, cleaningUp: this.cleaningUp };
40
+ }
41
+ press() {
42
+ const outcome = interpretInterrupt(this.state());
43
+ if (outcome.action === "cancel-operation")
44
+ this.startCleanup();
45
+ return outcome;
46
+ }
47
+ }
@@ -0,0 +1,30 @@
1
+ export type Key = {
2
+ readonly kind: "up";
3
+ } | {
4
+ readonly kind: "down";
5
+ } | {
6
+ readonly kind: "left";
7
+ } | {
8
+ readonly kind: "right";
9
+ } | {
10
+ readonly kind: "enter";
11
+ } | {
12
+ readonly kind: "escape";
13
+ } | {
14
+ readonly kind: "backspace";
15
+ } | {
16
+ readonly kind: "interrupt";
17
+ } | {
18
+ readonly kind: "eof";
19
+ } | {
20
+ readonly kind: "char";
21
+ readonly value: string;
22
+ };
23
+ /**
24
+ * Decode one chunk from a raw-mode stdin.
25
+ *
26
+ * Unrecognised control bytes are dropped rather than surfaced as characters:
27
+ * a stray byte must never end up inside a pasted API key.
28
+ */
29
+ export declare function decodeKeys(chunk: string): Key[];
30
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/tui/keys.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,GAAG,GACX;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAStD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,CAyE/C"}
@@ -0,0 +1,86 @@
1
+ // CLI-019 · Turning raw terminal bytes into key events.
2
+ //
3
+ // Pure, and it returns an array rather than a single key, because a paste
4
+ // arrives as one chunk containing every character at once. CLI-014 lost four
5
+ // lines of pasted input to exactly this shape of mistake - readline dropped
6
+ // what it had already buffered - so here the decoder is written to expect a
7
+ // chunk and tested on one.
8
+ const ESC = String.fromCharCode(27);
9
+ const ARROWS = {
10
+ A: "up",
11
+ B: "down",
12
+ C: "right",
13
+ D: "left",
14
+ };
15
+ /**
16
+ * Decode one chunk from a raw-mode stdin.
17
+ *
18
+ * Unrecognised control bytes are dropped rather than surfaced as characters:
19
+ * a stray byte must never end up inside a pasted API key.
20
+ */
21
+ export function decodeKeys(chunk) {
22
+ const keys = [];
23
+ const chars = [...chunk];
24
+ let i = 0;
25
+ while (i < chars.length) {
26
+ const ch = chars[i];
27
+ const code = ch.codePointAt(0) ?? 0;
28
+ // CSI sequences: ESC [ <final>. Anything else starting with ESC is
29
+ // treated as a bare Escape and the rest is decoded normally.
30
+ if (ch === ESC && chars[i + 1] === "[") {
31
+ const final = chars[i + 2];
32
+ if (final !== undefined && ARROWS[final] !== undefined) {
33
+ keys.push({ kind: ARROWS[final] });
34
+ i += 3;
35
+ continue;
36
+ }
37
+ // An unknown CSI: skip to its final byte (0x40-0x7e) so its parameters
38
+ // are not mistaken for typed characters.
39
+ let j = i + 2;
40
+ while (j < chars.length) {
41
+ const c = chars[j].codePointAt(0) ?? 0;
42
+ if (c >= 0x40 && c <= 0x7e)
43
+ break;
44
+ j += 1;
45
+ }
46
+ i = j + 1;
47
+ continue;
48
+ }
49
+ if (ch === ESC) {
50
+ keys.push({ kind: "escape" });
51
+ i += 1;
52
+ continue;
53
+ }
54
+ if (ch === "\r" || ch === "\n") {
55
+ keys.push({ kind: "enter" });
56
+ i += 1;
57
+ // A CRLF pair is one Enter, not two.
58
+ if (ch === "\r" && chars[i] === "\n")
59
+ i += 1;
60
+ continue;
61
+ }
62
+ if (code === 0x7f || code === 0x08) {
63
+ keys.push({ kind: "backspace" });
64
+ i += 1;
65
+ continue;
66
+ }
67
+ if (code === 0x03) {
68
+ keys.push({ kind: "interrupt" });
69
+ i += 1;
70
+ continue;
71
+ }
72
+ if (code === 0x04) {
73
+ keys.push({ kind: "eof" });
74
+ i += 1;
75
+ continue;
76
+ }
77
+ // Remaining C0 controls carry no meaning here and are dropped.
78
+ if (code < 0x20) {
79
+ i += 1;
80
+ continue;
81
+ }
82
+ keys.push({ kind: "char", value: ch });
83
+ i += 1;
84
+ }
85
+ return keys;
86
+ }
@@ -0,0 +1,28 @@
1
+ import type { CommandIo } from "../commands/runtime.js";
2
+ import { diagnoseKey, type KeyDiagnosis } from "./adapters/auth.js";
3
+ import { type TerminalCapabilities } from "./theme.js";
4
+ import { type WidgetIo } from "./widgets.js";
5
+ export declare const KEYS_URL = "https://www.mafdet.ai/api-keys";
6
+ export interface OnboardingResult {
7
+ readonly kind: "authenticated" | "cancelled";
8
+ /** Held by the caller for the life of the session. Never persisted. */
9
+ readonly apiKey?: string;
10
+ readonly diagnosis?: KeyDiagnosis;
11
+ }
12
+ export interface OnboardingOptions {
13
+ readonly io: WidgetIo;
14
+ readonly commandIo: CommandIo;
15
+ readonly capabilities: TerminalCapabilities;
16
+ /** Injected so the flow can be driven without touching the network. */
17
+ readonly diagnose?: typeof diagnoseKey;
18
+ }
19
+ type Method = "environment" | "paste" | "how-to";
20
+ /** Which routes make sense right now, and which one to land on. */
21
+ export declare function methodsFor(hasEnvKey: boolean): {
22
+ value: Method;
23
+ label: string;
24
+ detail?: string;
25
+ }[];
26
+ export declare function runOnboarding(options: OnboardingOptions): Promise<OnboardingResult>;
27
+ export {};
28
+ //# sourceMappingURL=onboarding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../../src/tui/onboarding.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7D,eAAO,MAAM,QAAQ,mCAAmC,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAAC;IAC7C,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,YAAY,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,WAAW,CAAC;CACxC;AAED,KAAK,MAAM,GAAG,aAAa,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEjD,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,SAAS,EAAE,OAAO;WACrB,MAAM;WAAS,MAAM;aAAW,MAAM;IAkB7D;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA0D3B"}
@@ -0,0 +1,117 @@
1
+ // CLI-019 · Getting a working key, without ever writing one down.
2
+ //
3
+ // Only the three routes the current auth system genuinely supports are
4
+ // offered. The plan (§19.4) is explicit that Google sign-in, browser login and
5
+ // "save securely" must not appear as entries that do nothing - an option that
6
+ // cannot work is worse than an option that is absent.
7
+ import { loadConfig } from "../config/load.js";
8
+ import { diagnoseKey } from "./adapters/auth.js";
9
+ import { palette } from "./theme.js";
10
+ import { select, secret } from "./widgets.js";
11
+ export const KEYS_URL = "https://www.mafdet.ai/api-keys";
12
+ /** Which routes make sense right now, and which one to land on. */
13
+ export function methodsFor(hasEnvKey) {
14
+ const items = [];
15
+ if (hasEnvKey) {
16
+ items.push({
17
+ value: "environment",
18
+ label: "Use MAFDET_API_KEY from the environment",
19
+ detail: "already set in this shell",
20
+ });
21
+ }
22
+ items.push({
23
+ value: "paste",
24
+ label: "Paste a Mafdet API key for this session",
25
+ detail: "kept in memory only - not written to any file",
26
+ });
27
+ items.push({
28
+ value: "how-to",
29
+ label: "Show how to create an API key",
30
+ });
31
+ return items;
32
+ }
33
+ export async function runOnboarding(options) {
34
+ const { io, commandIo, capabilities } = options;
35
+ const diagnose = options.diagnose ?? diagnoseKey;
36
+ const { bold, dim } = palette(capabilities);
37
+ // Resolved the same way every other command resolves it, rather than by
38
+ // reading env directly: `commandIo.env` is optional, so poking at it here
39
+ // meant the environment entry silently vanished in production while every
40
+ // scripted test - which always passes an env - stayed green.
41
+ const envKey = loadConfig({
42
+ env: commandIo.env,
43
+ cwd: commandIo.cwd,
44
+ userConfigFile: commandIo.userConfigFile,
45
+ }).apiKey?.trim();
46
+ for (;;) {
47
+ const choice = await select(io, {
48
+ title: "How would you like to authenticate?",
49
+ items: methodsFor(envKey !== undefined && envKey.length > 0),
50
+ });
51
+ if (choice.kind === "cancelled")
52
+ return { kind: "cancelled" };
53
+ if (choice.value === "how-to") {
54
+ io.write(instructions(capabilities));
55
+ continue;
56
+ }
57
+ let candidate;
58
+ if (choice.value === "environment") {
59
+ candidate = envKey;
60
+ }
61
+ else {
62
+ const entered = await secret(io, {
63
+ prompt: "Paste your API key",
64
+ hint: "input is hidden; it is not saved and not added to the environment",
65
+ });
66
+ if (entered.kind === "cancelled")
67
+ continue;
68
+ candidate = entered.value?.trim();
69
+ }
70
+ if (candidate === undefined || candidate.length === 0) {
71
+ io.write(dim(" no key entered\n\n"));
72
+ continue;
73
+ }
74
+ io.write(dim(" checking the key...\n"));
75
+ const diagnosis = await diagnose({ apiKey: candidate, io: commandIo });
76
+ if (!diagnosis.ok) {
77
+ // Drop the reference before looping: a rejected key has no reason to
78
+ // stay reachable for the rest of the process.
79
+ candidate = undefined;
80
+ io.write(` ${bold("could not use that key")}: ${diagnosis.problem}\n\n`);
81
+ continue;
82
+ }
83
+ io.write(summary(diagnosis, capabilities));
84
+ return { kind: "authenticated", apiKey: candidate, diagnosis };
85
+ }
86
+ }
87
+ function summary(diagnosis, capabilities) {
88
+ const { dim, bold } = palette(capabilities);
89
+ const lines = [
90
+ ` ${bold("key")} ${diagnosis.fingerprint}`,
91
+ ` ${bold("models")} ${diagnosis.modelCount} callable`,
92
+ ];
93
+ if (diagnosis.balance !== undefined) {
94
+ lines.push(` ${bold("balance")} ${diagnosis.balance}`);
95
+ }
96
+ if (diagnosis.missingAccountRead) {
97
+ // A missing optional scope is worth one sentence, not a failure.
98
+ lines.push(dim(" balance and plan need the account:read scope; model calls work without it"));
99
+ }
100
+ return lines.join("\n") + "\n\n";
101
+ }
102
+ function instructions(capabilities) {
103
+ const { bold, dim } = palette(capabilities);
104
+ return ([
105
+ "",
106
+ bold(" Creating an API key"),
107
+ "",
108
+ ` 1. Open ${KEYS_URL}`,
109
+ " 2. Create a key. Tick account:read if you want balance and plan",
110
+ " to show here; model calls do not need it.",
111
+ " 3. Copy it once - it is not shown again.",
112
+ "",
113
+ dim(" The key is sent only to Mafdet AI, over HTTPS. This CLI never"),
114
+ dim(" writes it to a file and never prints it in full."),
115
+ "",
116
+ ].join("\n") + "\n");
117
+ }
@@ -0,0 +1,24 @@
1
+ import type { ResolvedModel } from "../api/models.js";
2
+ import type { Key } from "./keys.js";
3
+ import { type TerminalCapabilities } from "./theme.js";
4
+ import type { WidgetIo } from "./widgets.js";
5
+ export interface PickResult {
6
+ readonly kind: "picked" | "cancelled";
7
+ readonly model?: ResolvedModel;
8
+ }
9
+ export interface PickerOptions {
10
+ readonly models: readonly ResolvedModel[];
11
+ /** Restrict to models the agent may drive. */
12
+ readonly agentOnly: boolean;
13
+ /** Pre-selected if still present in the list. */
14
+ readonly current?: string;
15
+ }
16
+ export declare function pickModel(io: WidgetIo, options: PickerOptions): Promise<PickResult>;
17
+ /**
18
+ * What to say when this key can call chat models but the agent may drive none
19
+ * of them. Not an error: chat still works, and the reason is checkable.
20
+ */
21
+ export declare function noAgentModelsNotice(chatCount: number, capabilities: TerminalCapabilities): string;
22
+ /** Used by the picker's key handling; exported for the tests. */
23
+ export type { Key };
24
+ //# sourceMappingURL=picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/tui/picker.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAW7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAC7B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,UAAU,CAAC,CAiHrB;AA6BD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAYR;AAED,iEAAiE;AACjE,YAAY,EAAE,GAAG,EAAE,CAAC"}