@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.
- package/CHANGELOG.md +43 -0
- package/README.md +22 -3
- package/README.zh-CN.md +17 -1
- package/dist/agent/loop.d.ts +17 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +17 -7
- package/dist/agent/session.d.ts +8 -1
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +1 -1
- package/dist/commands/agent.d.ts +9 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -6
- package/dist/tui/adapters/auth.d.ts +26 -0
- package/dist/tui/adapters/auth.d.ts.map +1 -0
- package/dist/tui/adapters/auth.js +76 -0
- package/dist/tui/adapters/models.d.ts +47 -0
- package/dist/tui/adapters/models.d.ts.map +1 -0
- package/dist/tui/adapters/models.js +146 -0
- package/dist/tui/agent-surface.d.ts +25 -0
- package/dist/tui/agent-surface.d.ts.map +1 -0
- package/dist/tui/agent-surface.js +95 -0
- package/dist/tui/app.d.ts +25 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +159 -0
- package/dist/tui/approval.d.ts +27 -0
- package/dist/tui/approval.d.ts.map +1 -0
- package/dist/tui/approval.js +79 -0
- package/dist/tui/banner.d.ts +13 -0
- package/dist/tui/banner.d.ts.map +1 -0
- package/dist/tui/banner.js +97 -0
- package/dist/tui/input.d.ts +17 -0
- package/dist/tui/input.d.ts.map +1 -0
- package/dist/tui/input.js +57 -0
- package/dist/tui/interrupt.d.ts +32 -0
- package/dist/tui/interrupt.d.ts.map +1 -0
- package/dist/tui/interrupt.js +47 -0
- package/dist/tui/keys.d.ts +30 -0
- package/dist/tui/keys.d.ts.map +1 -0
- package/dist/tui/keys.js +86 -0
- package/dist/tui/onboarding.d.ts +28 -0
- package/dist/tui/onboarding.d.ts.map +1 -0
- package/dist/tui/onboarding.js +117 -0
- package/dist/tui/picker.d.ts +24 -0
- package/dist/tui/picker.d.ts.map +1 -0
- package/dist/tui/picker.js +146 -0
- package/dist/tui/resize.d.ts +22 -0
- package/dist/tui/resize.d.ts.map +1 -0
- package/dist/tui/resize.js +37 -0
- package/dist/tui/route.d.ts +26 -0
- package/dist/tui/route.d.ts.map +1 -0
- package/dist/tui/route.js +68 -0
- package/dist/tui/start-agent.d.ts +31 -0
- package/dist/tui/start-agent.d.ts.map +1 -0
- package/dist/tui/start-agent.js +82 -0
- package/dist/tui/terminal.d.ts +41 -0
- package/dist/tui/terminal.d.ts.map +1 -0
- package/dist/tui/terminal.js +78 -0
- package/dist/tui/theme.d.ts +22 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +59 -0
- package/dist/tui/tool-card.d.ts +41 -0
- package/dist/tui/tool-card.d.ts.map +1 -0
- package/dist/tui/tool-card.js +132 -0
- package/dist/tui/widgets.d.ts +44 -0
- package/dist/tui/widgets.d.ts.map +1 -0
- package/dist/tui/widgets.js +137 -0
- package/dist/tui/workspace-summary.d.ts +10 -0
- package/dist/tui/workspace-summary.d.ts.map +1 -0
- package/dist/tui/workspace-summary.js +49 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type TerminalCapabilities } from "./theme.js";
|
|
2
|
+
export type ToolState = "queued" | "awaiting_approval" | "running" | "succeeded" | "refused" | "failed";
|
|
3
|
+
export interface ToolCard {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly state: ToolState;
|
|
6
|
+
/** Trimmed, control-stripped rendering of the model's arguments. */
|
|
7
|
+
readonly arguments: string;
|
|
8
|
+
/** One line of outcome, once there is one. */
|
|
9
|
+
readonly detail?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Render the model's arguments for display.
|
|
13
|
+
*
|
|
14
|
+
* Not `JSON.stringify` of the raw string: that would keep any control bytes
|
|
15
|
+
* the model put in a path, and a card is drawn straight to the terminal.
|
|
16
|
+
*/
|
|
17
|
+
export declare function summariseArguments(raw: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Fold the agent's events into cards.
|
|
20
|
+
*
|
|
21
|
+
* A single tool call is one card that changes state, rather than several log
|
|
22
|
+
* lines: the point of the card is that "what is it doing right now" has one
|
|
23
|
+
* place to look.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ToolCardLog {
|
|
26
|
+
private readonly cards;
|
|
27
|
+
/** Returns the card that changed, so a caller can redraw just that one. */
|
|
28
|
+
apply(event: {
|
|
29
|
+
type: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
ok?: boolean;
|
|
32
|
+
detail?: string;
|
|
33
|
+
arguments?: string;
|
|
34
|
+
summary?: string;
|
|
35
|
+
}): ToolCard | undefined;
|
|
36
|
+
list(): readonly ToolCard[];
|
|
37
|
+
private lastOpen;
|
|
38
|
+
private replace;
|
|
39
|
+
}
|
|
40
|
+
export declare function renderCard(card: ToolCard, capabilities: TerminalCapabilities): string;
|
|
41
|
+
//# sourceMappingURL=tool-card.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-card.d.ts","sourceRoot":"","sources":["../../src/tui/tool-card.ts"],"names":[],"mappings":"AAcA,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,mBAAmB,GACnB,SAAS,GACT,WAAW,GACX,SAAS,GACT,QAAQ,CAAC;AAEb,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAcD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAsBtD;AAQD;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IAExC,2EAA2E;IAC3E,KAAK,CAAC,KAAK,EAAE;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,EAAE,CAAC,EAAE,OAAO,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,QAAQ,GAAG,SAAS;IAkCxB,IAAI,IAAI,SAAS,QAAQ,EAAE;IAI3B,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,OAAO;CAKhB;AAOD,wBAAgB,UAAU,CACxB,IAAI,EAAE,QAAQ,EACd,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAUR"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// CLI-021 · Tool cards.
|
|
2
|
+
//
|
|
3
|
+
// One card per tool call, moving through the states the plan names:
|
|
4
|
+
//
|
|
5
|
+
// queued -> awaiting_approval -> running -> succeeded
|
|
6
|
+
// \-> refused
|
|
7
|
+
// \-> failed
|
|
8
|
+
//
|
|
9
|
+
// The card shows what the tool was *asked* to do, trimmed. The arguments come
|
|
10
|
+
// from the model, so they are untrusted text on their way to a terminal: every
|
|
11
|
+
// value is stripped of control characters and cut to a line before it is
|
|
12
|
+
// drawn. A file name is a perfectly good place to hide an escape sequence.
|
|
13
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
14
|
+
import { palette } from "./theme.js";
|
|
15
|
+
/** Longest argument rendering kept on a card. */
|
|
16
|
+
const ARGUMENT_LIMIT = 96;
|
|
17
|
+
const GLYPHS = {
|
|
18
|
+
queued: " ",
|
|
19
|
+
awaiting_approval: "? ",
|
|
20
|
+
running: "> ",
|
|
21
|
+
succeeded: "ok",
|
|
22
|
+
refused: "no",
|
|
23
|
+
failed: "!!",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Render the model's arguments for display.
|
|
27
|
+
*
|
|
28
|
+
* Not `JSON.stringify` of the raw string: that would keep any control bytes
|
|
29
|
+
* the model put in a path, and a card is drawn straight to the terminal.
|
|
30
|
+
*/
|
|
31
|
+
export function summariseArguments(raw) {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw || "{}");
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return clip(stripAnsi(raw).replace(/\s+/g, " ").trim());
|
|
38
|
+
}
|
|
39
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
40
|
+
return clip(stripAnsi(String(parsed)));
|
|
41
|
+
}
|
|
42
|
+
const parts = [];
|
|
43
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
44
|
+
const text = typeof value === "string"
|
|
45
|
+
? value
|
|
46
|
+
: (JSON.stringify(value) ?? String(value));
|
|
47
|
+
parts.push(`${key}=${stripAnsi(text).replace(/\s+/g, " ").trim()}`);
|
|
48
|
+
}
|
|
49
|
+
return clip(parts.join(" "));
|
|
50
|
+
}
|
|
51
|
+
function clip(text) {
|
|
52
|
+
const chars = [...text];
|
|
53
|
+
if (chars.length <= ARGUMENT_LIMIT)
|
|
54
|
+
return text;
|
|
55
|
+
return chars.slice(0, ARGUMENT_LIMIT - 3).join("") + "...";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Fold the agent's events into cards.
|
|
59
|
+
*
|
|
60
|
+
* A single tool call is one card that changes state, rather than several log
|
|
61
|
+
* lines: the point of the card is that "what is it doing right now" has one
|
|
62
|
+
* place to look.
|
|
63
|
+
*/
|
|
64
|
+
export class ToolCardLog {
|
|
65
|
+
cards = [];
|
|
66
|
+
/** Returns the card that changed, so a caller can redraw just that one. */
|
|
67
|
+
apply(event) {
|
|
68
|
+
if (event.name === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (event.type === "tool_started") {
|
|
71
|
+
const card = {
|
|
72
|
+
name: event.name,
|
|
73
|
+
state: "running",
|
|
74
|
+
arguments: summariseArguments(event.arguments ?? "{}"),
|
|
75
|
+
};
|
|
76
|
+
this.cards.push(card);
|
|
77
|
+
return card;
|
|
78
|
+
}
|
|
79
|
+
const open = this.lastOpen(event.name);
|
|
80
|
+
if (open === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (event.type === "tool_awaiting_approval") {
|
|
83
|
+
return this.replace(open, {
|
|
84
|
+
state: "awaiting_approval",
|
|
85
|
+
detail: event.summary,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (event.type === "declined") {
|
|
89
|
+
return this.replace(open, { state: "refused", detail: event.summary });
|
|
90
|
+
}
|
|
91
|
+
if (event.type === "tool") {
|
|
92
|
+
return this.replace(open, {
|
|
93
|
+
state: event.ok === true ? "succeeded" : "failed",
|
|
94
|
+
detail: firstLine(event.detail ?? ""),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
list() {
|
|
100
|
+
return this.cards;
|
|
101
|
+
}
|
|
102
|
+
lastOpen(name) {
|
|
103
|
+
for (let i = this.cards.length - 1; i >= 0; i -= 1) {
|
|
104
|
+
const card = this.cards[i];
|
|
105
|
+
if (card.name === name &&
|
|
106
|
+
(card.state === "running" || card.state === "awaiting_approval")) {
|
|
107
|
+
return i;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
replace(index, patch) {
|
|
113
|
+
const next = { ...this.cards[index], ...patch };
|
|
114
|
+
this.cards[index] = next;
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function firstLine(text) {
|
|
119
|
+
const line = stripAnsi(text).split("\n")[0] ?? "";
|
|
120
|
+
return clip(line.trim());
|
|
121
|
+
}
|
|
122
|
+
export function renderCard(card, capabilities) {
|
|
123
|
+
const { dim, bold, accent } = palette(capabilities);
|
|
124
|
+
const glyph = GLYPHS[card.state];
|
|
125
|
+
const head = ` ${glyph} ${bold(card.name)} ${dim(card.arguments)}`;
|
|
126
|
+
if (card.detail === undefined || card.detail.length === 0)
|
|
127
|
+
return `${head}\n`;
|
|
128
|
+
const detail = card.state === "failed" || card.state === "refused"
|
|
129
|
+
? accent(card.detail)
|
|
130
|
+
: dim(card.detail);
|
|
131
|
+
return `${head}\n ${detail}\n`;
|
|
132
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Key } from "./keys.js";
|
|
2
|
+
import { type TerminalCapabilities } from "./theme.js";
|
|
3
|
+
/** Where a widget draws and how it hears about keys. */
|
|
4
|
+
export interface WidgetIo {
|
|
5
|
+
write(text: string): void;
|
|
6
|
+
/** Resolves with the next chunk of keys, or undefined once input ends. */
|
|
7
|
+
nextKeys(): Promise<readonly Key[] | undefined>;
|
|
8
|
+
readonly capabilities: TerminalCapabilities;
|
|
9
|
+
}
|
|
10
|
+
export interface SelectItem<T> {
|
|
11
|
+
readonly value: T;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
/** A second, dimmer line under the label. */
|
|
14
|
+
readonly detail?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SelectResult<T> {
|
|
17
|
+
readonly kind: "picked" | "cancelled";
|
|
18
|
+
readonly value?: T;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A vertical list. Arrow keys or j/k move, Enter picks, Ctrl+C and Escape
|
|
22
|
+
* cancel. Cancelling is a first-class outcome, not an exception: the caller
|
|
23
|
+
* decides whether it means "go back" or "quit".
|
|
24
|
+
*/
|
|
25
|
+
export declare function select<T>(io: WidgetIo, options: {
|
|
26
|
+
readonly title: string;
|
|
27
|
+
readonly items: readonly SelectItem<T>[];
|
|
28
|
+
}): Promise<SelectResult<T>>;
|
|
29
|
+
export interface SecretResult {
|
|
30
|
+
readonly kind: "entered" | "cancelled";
|
|
31
|
+
readonly value?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A single-line input that never echoes what was typed.
|
|
35
|
+
*
|
|
36
|
+
* The value is returned to the caller and deliberately not retained here. The
|
|
37
|
+
* mask shows a count so a paste visibly registers, which matters because a
|
|
38
|
+
* silent field gives no way to tell a failed paste from a wrong key.
|
|
39
|
+
*/
|
|
40
|
+
export declare function secret(io: WidgetIo, options: {
|
|
41
|
+
readonly prompt: string;
|
|
42
|
+
readonly hint?: string;
|
|
43
|
+
}): Promise<SecretResult>;
|
|
44
|
+
//# sourceMappingURL=widgets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widgets.d.ts","sourceRoot":"","sources":["../../src/tui/widgets.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAQhE,wDAAwD;AACxD,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,IAAI,OAAO,CAAC,SAAS,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC;IAChD,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;CAC7C;AAyBD,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,CAAC,EAC5B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,CAAC,CAAC,CAAC,EAAE,CAAA;CAAE,GAC5E,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAgD1B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,WAAW,CAAC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAsB,MAAM,CAC1B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3D,OAAO,CAAC,YAAY,CAAC,CAgDvB"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// CLI-019 · The two interactive widgets, over an append-only log.
|
|
2
|
+
//
|
|
3
|
+
// Per the ADR: no alternate screen, no full-screen repaint. A widget draws
|
|
4
|
+
// itself, redraws in place while it has focus, and when it is done it leaves
|
|
5
|
+
// one settled line behind so the transcript above it stays readable and
|
|
6
|
+
// scrollback keeps working.
|
|
7
|
+
import { palette } from "./theme.js";
|
|
8
|
+
const ESC = String.fromCharCode(27);
|
|
9
|
+
const CSI = `${ESC}[`;
|
|
10
|
+
const CLEAR_LINE = `${CSI}2K`;
|
|
11
|
+
const TO_COL_0 = `${CSI}G`;
|
|
12
|
+
const up = (n) => (n > 0 ? `${CSI}${n}A` : "");
|
|
13
|
+
/** Painter for a block of lines that is rewritten in place. */
|
|
14
|
+
function inPlace(io) {
|
|
15
|
+
let painted = 0;
|
|
16
|
+
return {
|
|
17
|
+
paint(lines) {
|
|
18
|
+
const body = lines.map((line) => CLEAR_LINE + line).join("\n");
|
|
19
|
+
io.write(up(painted) + TO_COL_0 + body + "\n");
|
|
20
|
+
painted = lines.length;
|
|
21
|
+
},
|
|
22
|
+
/** Replace the block with a single settled line. */
|
|
23
|
+
settle(line) {
|
|
24
|
+
io.write(up(painted) + TO_COL_0);
|
|
25
|
+
// Erase the rest of what was drawn so nothing is left half-visible.
|
|
26
|
+
const blanks = Array.from({ length: painted }, () => CLEAR_LINE).join("\n");
|
|
27
|
+
if (painted > 0)
|
|
28
|
+
io.write(blanks + up(painted - 1) + TO_COL_0);
|
|
29
|
+
io.write(CLEAR_LINE + line + "\n");
|
|
30
|
+
painted = 0;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A vertical list. Arrow keys or j/k move, Enter picks, Ctrl+C and Escape
|
|
36
|
+
* cancel. Cancelling is a first-class outcome, not an exception: the caller
|
|
37
|
+
* decides whether it means "go back" or "quit".
|
|
38
|
+
*/
|
|
39
|
+
export async function select(io, options) {
|
|
40
|
+
const { bold, dim, accent } = palette(io.capabilities);
|
|
41
|
+
const painter = inPlace(io);
|
|
42
|
+
let index = 0;
|
|
43
|
+
const draw = () => {
|
|
44
|
+
const lines = [bold(options.title), ""];
|
|
45
|
+
options.items.forEach((item, i) => {
|
|
46
|
+
const selected = i === index;
|
|
47
|
+
lines.push(selected ? accent(` > ${item.label}`) : ` ${item.label}`);
|
|
48
|
+
if (item.detail !== undefined)
|
|
49
|
+
lines.push(dim(` ${item.detail}`));
|
|
50
|
+
});
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push(dim(" up/down to move, Enter to select, Ctrl+C to cancel"));
|
|
53
|
+
painter.paint(lines);
|
|
54
|
+
};
|
|
55
|
+
draw();
|
|
56
|
+
for (;;) {
|
|
57
|
+
const keys = await io.nextKeys();
|
|
58
|
+
if (keys === undefined) {
|
|
59
|
+
painter.settle(dim(`${options.title} cancelled`));
|
|
60
|
+
return { kind: "cancelled" };
|
|
61
|
+
}
|
|
62
|
+
for (const key of keys) {
|
|
63
|
+
if (key.kind === "up" || (key.kind === "char" && key.value === "k")) {
|
|
64
|
+
index = index === 0 ? options.items.length - 1 : index - 1;
|
|
65
|
+
}
|
|
66
|
+
else if (key.kind === "down" ||
|
|
67
|
+
(key.kind === "char" && key.value === "j")) {
|
|
68
|
+
index = index === options.items.length - 1 ? 0 : index + 1;
|
|
69
|
+
}
|
|
70
|
+
else if (key.kind === "enter") {
|
|
71
|
+
const chosen = options.items[index];
|
|
72
|
+
painter.settle(`${dim(options.title)} ${bold(chosen.label)}`);
|
|
73
|
+
return { kind: "picked", value: chosen.value };
|
|
74
|
+
}
|
|
75
|
+
else if (key.kind === "interrupt" ||
|
|
76
|
+
key.kind === "escape" ||
|
|
77
|
+
key.kind === "eof") {
|
|
78
|
+
painter.settle(dim(`${options.title} cancelled`));
|
|
79
|
+
return { kind: "cancelled" };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
draw();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A single-line input that never echoes what was typed.
|
|
87
|
+
*
|
|
88
|
+
* The value is returned to the caller and deliberately not retained here. The
|
|
89
|
+
* mask shows a count so a paste visibly registers, which matters because a
|
|
90
|
+
* silent field gives no way to tell a failed paste from a wrong key.
|
|
91
|
+
*/
|
|
92
|
+
export async function secret(io, options) {
|
|
93
|
+
const { bold, dim } = palette(io.capabilities);
|
|
94
|
+
const painter = inPlace(io);
|
|
95
|
+
let buffer = "";
|
|
96
|
+
const draw = () => {
|
|
97
|
+
const lines = [bold(options.prompt)];
|
|
98
|
+
if (options.hint !== undefined)
|
|
99
|
+
lines.push(dim(options.hint));
|
|
100
|
+
lines.push("");
|
|
101
|
+
// A count, not the characters: enough to see a paste land, nothing more.
|
|
102
|
+
lines.push(buffer.length === 0
|
|
103
|
+
? dim(" (hidden - paste or type, then press Enter)")
|
|
104
|
+
: ` ${"*".repeat(Math.min(buffer.length, 32))} ${dim(`${buffer.length} characters`)}`);
|
|
105
|
+
painter.paint(lines);
|
|
106
|
+
};
|
|
107
|
+
draw();
|
|
108
|
+
for (;;) {
|
|
109
|
+
const keys = await io.nextKeys();
|
|
110
|
+
if (keys === undefined) {
|
|
111
|
+
painter.settle(dim(`${options.prompt} cancelled`));
|
|
112
|
+
return { kind: "cancelled" };
|
|
113
|
+
}
|
|
114
|
+
for (const key of keys) {
|
|
115
|
+
if (key.kind === "char") {
|
|
116
|
+
buffer += key.value;
|
|
117
|
+
}
|
|
118
|
+
else if (key.kind === "backspace") {
|
|
119
|
+
buffer = buffer.slice(0, -1);
|
|
120
|
+
}
|
|
121
|
+
else if (key.kind === "enter") {
|
|
122
|
+
const value = buffer;
|
|
123
|
+
buffer = "";
|
|
124
|
+
painter.settle(dim(`${options.prompt} received`));
|
|
125
|
+
return { kind: "entered", value };
|
|
126
|
+
}
|
|
127
|
+
else if (key.kind === "interrupt" ||
|
|
128
|
+
key.kind === "escape" ||
|
|
129
|
+
key.kind === "eof") {
|
|
130
|
+
buffer = "";
|
|
131
|
+
painter.settle(dim(`${options.prompt} cancelled`));
|
|
132
|
+
return { kind: "cancelled" };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
draw();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TerminalCapabilities } from "./theme.js";
|
|
2
|
+
export interface WorkspaceFacts {
|
|
3
|
+
/** Resolved through symlinks; undefined when the directory is unusable. */
|
|
4
|
+
readonly root?: string;
|
|
5
|
+
readonly problem?: string;
|
|
6
|
+
readonly isGitRepository: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function inspectWorkspace(cwd: string, exists: (path: string) => boolean): WorkspaceFacts;
|
|
9
|
+
export declare function renderWorkspaceSummary(facts: WorkspaceFacts, capabilities: TerminalCapabilities): string;
|
|
10
|
+
//# sourceMappingURL=workspace-summary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-summary.d.ts","sourceRoot":"","sources":["../../src/tui/workspace-summary.ts"],"names":[],"mappings":"AAUA,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAChC,cAAc,CAahB;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,cAAc,EACrB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAgBR"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// CLI-020 · What the session may touch, stated before anything is done.
|
|
2
|
+
//
|
|
3
|
+
// The path shown is the canonical one. A symlinked or relative cwd that
|
|
4
|
+
// resolves elsewhere must be displayed as where it really is - telling
|
|
5
|
+
// someone the agent is working in ~/project when it is really in /tmp is the
|
|
6
|
+
// kind of small lie that makes a permission model useless.
|
|
7
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
8
|
+
import { Workspace } from "../filesystem/workspace.js";
|
|
9
|
+
import { redact } from "../security/redact.js";
|
|
10
|
+
import { palette } from "./theme.js";
|
|
11
|
+
export function inspectWorkspace(cwd, exists) {
|
|
12
|
+
try {
|
|
13
|
+
const workspace = Workspace.open(cwd);
|
|
14
|
+
return {
|
|
15
|
+
root: workspace.root,
|
|
16
|
+
isGitRepository: exists(`${workspace.root}/.git`),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return {
|
|
21
|
+
problem: error instanceof Error ? error.message : String(error),
|
|
22
|
+
isGitRepository: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function renderWorkspaceSummary(facts, capabilities) {
|
|
27
|
+
const { bold, dim } = palette(capabilities);
|
|
28
|
+
if (facts.root === undefined) {
|
|
29
|
+
return ` ${bold("workspace")} unavailable: ${safe(facts.problem ?? "")}\n`;
|
|
30
|
+
}
|
|
31
|
+
const lines = [
|
|
32
|
+
` ${bold("workspace")} ${safe(facts.root)}`,
|
|
33
|
+
dim(` ${facts.isGitRepository ? "a git repository" : "not a git repository"}`),
|
|
34
|
+
// Said up front, every session: the defaults are the permission model, and
|
|
35
|
+
// a user should not have to find them in a manual.
|
|
36
|
+
dim(" the agent starts read-only; writing and running commands are"),
|
|
37
|
+
dim(" opt-in per run and every change is shown before it is applied"),
|
|
38
|
+
];
|
|
39
|
+
return lines.join("\n") + "\n";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A path is not trusted text. It comes from the filesystem, which a cloned
|
|
43
|
+
* repository can influence, and it is on a direct path to the terminal - so it
|
|
44
|
+
* is stripped of control sequences and passed through the redactor in case a
|
|
45
|
+
* directory name carries something that looks like a key.
|
|
46
|
+
*/
|
|
47
|
+
function safe(text) {
|
|
48
|
+
return redact(stripAnsi(text));
|
|
49
|
+
}
|