@teammates/cli 0.1.0 → 0.2.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 (76) hide show
  1. package/README.md +31 -22
  2. package/dist/adapter.d.ts +1 -1
  3. package/dist/adapter.js +68 -56
  4. package/dist/adapter.test.js +34 -21
  5. package/dist/adapters/cli-proxy.d.ts +11 -4
  6. package/dist/adapters/cli-proxy.js +176 -162
  7. package/dist/adapters/copilot.d.ts +50 -0
  8. package/dist/adapters/copilot.js +210 -0
  9. package/dist/adapters/echo.d.ts +2 -2
  10. package/dist/adapters/echo.js +2 -1
  11. package/dist/adapters/echo.test.js +4 -2
  12. package/dist/cli-utils.d.ts +21 -0
  13. package/dist/cli-utils.js +74 -0
  14. package/dist/cli-utils.test.d.ts +1 -0
  15. package/dist/cli-utils.test.js +179 -0
  16. package/dist/cli.js +3160 -961
  17. package/dist/compact.d.ts +39 -0
  18. package/dist/compact.js +269 -0
  19. package/dist/compact.test.d.ts +1 -0
  20. package/dist/compact.test.js +198 -0
  21. package/dist/console/ansi.d.ts +18 -0
  22. package/dist/console/ansi.js +20 -0
  23. package/dist/console/ansi.test.d.ts +1 -0
  24. package/dist/console/ansi.test.js +50 -0
  25. package/dist/console/dropdown.d.ts +23 -0
  26. package/dist/console/dropdown.js +63 -0
  27. package/dist/console/file-drop.d.ts +59 -0
  28. package/dist/console/file-drop.js +186 -0
  29. package/dist/console/file-drop.test.d.ts +1 -0
  30. package/dist/console/file-drop.test.js +145 -0
  31. package/dist/console/index.d.ts +22 -0
  32. package/dist/console/index.js +23 -0
  33. package/dist/console/interactive-readline.d.ts +65 -0
  34. package/dist/console/interactive-readline.js +132 -0
  35. package/dist/console/markdown-table.d.ts +17 -0
  36. package/dist/console/markdown-table.js +270 -0
  37. package/dist/console/markdown-table.test.d.ts +1 -0
  38. package/dist/console/markdown-table.test.js +130 -0
  39. package/dist/console/mutable-output.d.ts +21 -0
  40. package/dist/console/mutable-output.js +51 -0
  41. package/dist/console/paste-handler.d.ts +63 -0
  42. package/dist/console/paste-handler.js +177 -0
  43. package/dist/console/prompt-box.d.ts +55 -0
  44. package/dist/console/prompt-box.js +120 -0
  45. package/dist/console/prompt-input.d.ts +136 -0
  46. package/dist/console/prompt-input.js +618 -0
  47. package/dist/console/startup.d.ts +20 -0
  48. package/dist/console/startup.js +138 -0
  49. package/dist/console/startup.test.d.ts +1 -0
  50. package/dist/console/startup.test.js +41 -0
  51. package/dist/console/wordwheel.d.ts +75 -0
  52. package/dist/console/wordwheel.js +123 -0
  53. package/dist/dropdown.js +4 -21
  54. package/dist/index.d.ts +5 -5
  55. package/dist/index.js +3 -3
  56. package/dist/onboard.d.ts +24 -0
  57. package/dist/onboard.js +174 -11
  58. package/dist/orchestrator.d.ts +8 -11
  59. package/dist/orchestrator.js +33 -81
  60. package/dist/orchestrator.test.js +59 -79
  61. package/dist/registry.d.ts +1 -1
  62. package/dist/registry.js +56 -12
  63. package/dist/registry.test.js +57 -13
  64. package/dist/theme.d.ts +56 -0
  65. package/dist/theme.js +54 -0
  66. package/dist/types.d.ts +18 -13
  67. package/package.json +8 -3
  68. package/template/CROSS-TEAM.md +2 -2
  69. package/template/PROTOCOL.md +72 -15
  70. package/template/README.md +2 -2
  71. package/template/TEMPLATE.md +118 -15
  72. package/template/example/SOUL.md +2 -1
  73. package/template/example/WISDOM.md +9 -0
  74. package/dist/adapters/codex.d.ts +0 -50
  75. package/dist/adapters/codex.js +0 -213
  76. package/template/example/MEMORIES.md +0 -26
@@ -0,0 +1,177 @@
1
+ /**
2
+ * PasteHandler — detects and manages pasted text in a readline REPL.
3
+ *
4
+ * Handles:
5
+ * - Multi-line pastes: collapses into a numbered placeholder, expands on Enter
6
+ * - Long single-line pastes: dispatches directly with truncated preview
7
+ * - File drag & drop: detects pasted file paths, converts to [Image #N] / [File #N] tags
8
+ *
9
+ * Works on both Windows and macOS terminals.
10
+ */
11
+ import { esc } from "@teammates/consolonia";
12
+ import { FileDropHandler } from "./file-drop.js";
13
+ export class PasteHandler {
14
+ buffer = [];
15
+ timer = null;
16
+ count = 0;
17
+ storedTexts = new Map();
18
+ prePastePrefix = "";
19
+ lastKeystrokeTime = 0;
20
+ fileDrop;
21
+ rl;
22
+ output;
23
+ debounceMs;
24
+ longPasteThreshold;
25
+ onLine;
26
+ formatPrompt;
27
+ formatFileTag;
28
+ formatFileHint;
29
+ constructor(options) {
30
+ this.rl = options.rl;
31
+ this.output = options.output;
32
+ this.debounceMs = options.debounceMs ?? 30;
33
+ this.longPasteThreshold = options.longPasteThreshold ?? 100;
34
+ this.onLine = options.onLine;
35
+ this.formatPrompt = options.formatPrompt ?? (() => "> ");
36
+ this.fileDrop = new FileDropHandler();
37
+ this.formatFileTag =
38
+ options.formatFileTag ??
39
+ ((a) => {
40
+ const type = a.isImage ? "Image" : "File";
41
+ return `[${type} #${a.id}]`;
42
+ });
43
+ this.formatFileHint =
44
+ options.formatFileHint ??
45
+ ((a) => {
46
+ const type = a.isImage ? "Image" : "File";
47
+ const sizeKB = (a.size / 1024).toFixed(1);
48
+ return ` ${type}: ${a.name} (${sizeKB}KB)`;
49
+ });
50
+ this.installHooks();
51
+ }
52
+ /** Call from _ttyWrite override to track keystroke timing. */
53
+ onKeystroke() {
54
+ const now = Date.now();
55
+ if (now - this.lastKeystrokeTime > 50) {
56
+ this.prePastePrefix = this.rl.line ?? "";
57
+ }
58
+ this.lastKeystrokeTime = now;
59
+ }
60
+ /** Clear all stored paste data (e.g. on session reset). */
61
+ reset() {
62
+ this.storedTexts.clear();
63
+ this.fileDrop.clear();
64
+ this.buffer = [];
65
+ this.count = 0;
66
+ if (this.timer) {
67
+ clearTimeout(this.timer);
68
+ this.timer = null;
69
+ }
70
+ }
71
+ installHooks() {
72
+ // Pre-mute: detect paste from stdin chunk size/shape BEFORE readline echoes
73
+ process.stdin.prependListener("data", (chunk) => {
74
+ const str = chunk.toString();
75
+ const hasMultipleNewlines = str.includes("\n") && str.indexOf("\n") < str.length - 1;
76
+ const isLongChunk = str.length > this.longPasteThreshold;
77
+ if (hasMultipleNewlines || isLongChunk) {
78
+ this.output.mute();
79
+ }
80
+ });
81
+ // Buffer lines from readline, debounce to detect paste vs typing
82
+ this.rl.on("line", (line) => {
83
+ this.buffer.push(line);
84
+ if (this.buffer.length === 1) {
85
+ this.output.mute();
86
+ }
87
+ if (this.timer)
88
+ clearTimeout(this.timer);
89
+ this.timer = setTimeout(() => this.processPaste(), this.debounceMs);
90
+ });
91
+ }
92
+ processPaste() {
93
+ this.timer = null;
94
+ this.output.unmute();
95
+ const lines = this.buffer;
96
+ this.buffer = [];
97
+ if (lines.length === 0)
98
+ return;
99
+ if (lines.length > 1) {
100
+ // Multi-line paste — erase the first echoed line, show placeholder
101
+ process.stdout.write(esc.moveUp(1) + esc.eraseLine);
102
+ this.count++;
103
+ const combined = lines.join("\n");
104
+ const sizeKB = Buffer.byteLength(combined, "utf-8") / 1024;
105
+ const tag = `[Pasted text #${this.count} +${lines.length} lines, ${sizeKB.toFixed(1)}KB] `;
106
+ this.storedTexts.set(this.count, combined);
107
+ const newLine = this.prePastePrefix + tag;
108
+ this.prePastePrefix = "";
109
+ this.rl.line = newLine;
110
+ this.rl.cursor = newLine.length;
111
+ this.rl.prompt(true);
112
+ return;
113
+ }
114
+ // Single line
115
+ const rawLine = lines[0];
116
+ // ── File drop detection ──────────────────────────────────────────
117
+ // Check if the pasted text is a file path (drag & drop).
118
+ // The raw line may include the pre-paste prefix (e.g. "@beacon ").
119
+ const trimmedLine = rawLine.trim();
120
+ const detectedPath = this.fileDrop.detectFilePath(trimmedLine);
121
+ if (detectedPath) {
122
+ // It's a file drop — convert to an inline tag
123
+ const { text: taggedText } = this.fileDrop.processInput(trimmedLine);
124
+ const attachment = this.fileDrop.getAll().at(-1);
125
+ // Clear the echoed path and replace with tag in the prompt line
126
+ process.stdout.write(`\r${esc.eraseLine}`);
127
+ const newLine = this.prePastePrefix + taggedText;
128
+ this.prePastePrefix = "";
129
+ this.rl.line = newLine;
130
+ this.rl.cursor = newLine.length;
131
+ // Show the file hint below
132
+ console.log(this.formatPrompt() + newLine);
133
+ console.log(this.formatFileHint(attachment));
134
+ this.rl.prompt(true);
135
+ return;
136
+ }
137
+ // Also check if a file path is embedded in the line (with other text)
138
+ const { text: processedText, filesDetected } = this.fileDrop.processInput(rawLine);
139
+ const effectiveLine = filesDetected ? processedText : rawLine;
140
+ // If it was a long muted paste, show a truncated preview
141
+ if (effectiveLine.length > this.longPasteThreshold && !filesDetected) {
142
+ const preview = `${effectiveLine.slice(0, 80)}...`;
143
+ process.stdout.write(`\r${esc.eraseLine}`);
144
+ const prompt = this.formatPrompt();
145
+ process.stdout.write(`${prompt + preview}\n`);
146
+ }
147
+ // If files were detected but the line has other text too, update the display
148
+ if (filesDetected) {
149
+ process.stdout.write(`\r${esc.eraseLine}`);
150
+ const prompt = this.formatPrompt();
151
+ process.stdout.write(`${prompt + processedText}\n`);
152
+ for (const a of this.fileDrop.getAll()) {
153
+ console.log(this.formatFileHint(a));
154
+ }
155
+ }
156
+ // Expand paste placeholders from prior multi-line pastes
157
+ const hasPaste = /\[Pasted text #\d+/.test(effectiveLine);
158
+ const input = effectiveLine
159
+ .replace(/\[Pasted text #(\d+) \+\d+ lines, [\d.]+KB\]\s*/g, (_match, num) => {
160
+ const n = parseInt(num, 10);
161
+ const text = this.storedTexts.get(n);
162
+ if (text) {
163
+ this.storedTexts.delete(n);
164
+ return `${text}\n`;
165
+ }
166
+ return "";
167
+ })
168
+ .trim();
169
+ // Expand file tags to paths and collect attachments
170
+ const { text: expandedInput, attachments } = this.fileDrop.expandTags(input);
171
+ this.onLine({
172
+ input: filesDetected ? expandedInput : input,
173
+ hadPaste: hasPaste || filesDetected,
174
+ attachments,
175
+ });
176
+ }
177
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * PromptBox — renders a fenced input box around the readline prompt.
3
+ *
4
+ * Draws horizontal borders above and below the input line, similar to
5
+ * Claude Code's UI. Automatically recalculates on terminal resize.
6
+ *
7
+ * Layout:
8
+ * ────────────────────────────────────────
9
+ * ❯ user input here
10
+ * ────────────────────────────────────────
11
+ * (optional status / dropdown lines)
12
+ *
13
+ * Approach (inspired by Consolonia's dirty-region rendering):
14
+ * - _refreshLine hook uses esc.eraseDown to nuke everything below the
15
+ * prompt, then redraws the bottom border + dropdown fresh each time.
16
+ * - On resize, the entire prompt area (top border + prompt + bottom)
17
+ * is cleared and redrawn. We don't try to navigate to old content
18
+ * since terminal reflow makes cursor-relative positioning unreliable.
19
+ */
20
+ import type { Interface as ReadlineInterface } from "node:readline";
21
+ export interface PromptBoxOptions {
22
+ /** Readline interface. */
23
+ rl: ReadlineInterface;
24
+ /** Border character (default: "─"). */
25
+ borderChar?: string;
26
+ /** ANSI color wrapper for the border (default: dim/gray). */
27
+ borderStyle?: (s: string) => string;
28
+ /** Optional status line rendered below the bottom border. */
29
+ getStatusLine?: () => string | null;
30
+ }
31
+ export declare class PromptBox {
32
+ private rl;
33
+ private out;
34
+ private borderChar;
35
+ private borderStyle;
36
+ private getStatusLine;
37
+ private refreshing;
38
+ private dropdownLines;
39
+ private active;
40
+ constructor(options: PromptBoxOptions);
41
+ /** Set dropdown content to render below the bottom border. */
42
+ setDropdown(lines: string[]): void;
43
+ /** Clear dropdown content. */
44
+ clearDropdown(): void;
45
+ /**
46
+ * Draw the top border and activate the prompt box.
47
+ * Call before rl.prompt().
48
+ */
49
+ drawTopBorder(): void;
50
+ /** Deactivate during dispatch so streaming output isn't corrupted. */
51
+ deactivate(): void;
52
+ private buildBorder;
53
+ private installHook;
54
+ private listenResize;
55
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * PromptBox — renders a fenced input box around the readline prompt.
3
+ *
4
+ * Draws horizontal borders above and below the input line, similar to
5
+ * Claude Code's UI. Automatically recalculates on terminal resize.
6
+ *
7
+ * Layout:
8
+ * ────────────────────────────────────────
9
+ * ❯ user input here
10
+ * ────────────────────────────────────────
11
+ * (optional status / dropdown lines)
12
+ *
13
+ * Approach (inspired by Consolonia's dirty-region rendering):
14
+ * - _refreshLine hook uses esc.eraseDown to nuke everything below the
15
+ * prompt, then redraws the bottom border + dropdown fresh each time.
16
+ * - On resize, the entire prompt area (top border + prompt + bottom)
17
+ * is cleared and redrawn. We don't try to navigate to old content
18
+ * since terminal reflow makes cursor-relative positioning unreliable.
19
+ */
20
+ import { esc, stripAnsi, truncateAnsi } from "@teammates/consolonia";
21
+ import { cursorToCol } from "./ansi.js";
22
+ export class PromptBox {
23
+ rl;
24
+ out = process.stdout;
25
+ borderChar;
26
+ borderStyle;
27
+ getStatusLine;
28
+ refreshing = false;
29
+ dropdownLines = [];
30
+ active = false;
31
+ constructor(options) {
32
+ this.rl = options.rl;
33
+ this.borderChar = options.borderChar ?? "─";
34
+ this.borderStyle = options.borderStyle ?? ((s) => `\x1b[2m${s}\x1b[0m`);
35
+ this.getStatusLine = options.getStatusLine ?? (() => null);
36
+ this.installHook();
37
+ this.listenResize();
38
+ }
39
+ /** Set dropdown content to render below the bottom border. */
40
+ setDropdown(lines) {
41
+ this.dropdownLines = lines;
42
+ this.rl._refreshLine();
43
+ }
44
+ /** Clear dropdown content. */
45
+ clearDropdown() {
46
+ this.dropdownLines = [];
47
+ }
48
+ /**
49
+ * Draw the top border and activate the prompt box.
50
+ * Call before rl.prompt().
51
+ */
52
+ drawTopBorder() {
53
+ this.out.write(`${this.buildBorder()}\n`);
54
+ this.active = true;
55
+ }
56
+ /** Deactivate during dispatch so streaming output isn't corrupted. */
57
+ deactivate() {
58
+ this.active = false;
59
+ }
60
+ buildBorder() {
61
+ const width = this.out.columns || 80;
62
+ return this.borderStyle(this.borderChar.repeat(width));
63
+ }
64
+ installHook() {
65
+ const origRefresh = this.rl._refreshLine.bind(this.rl);
66
+ this.rl._refreshLine = () => {
67
+ if (this.refreshing || !this.active) {
68
+ origRefresh();
69
+ return;
70
+ }
71
+ this.refreshing = true;
72
+ // Let readline clear the prompt line and rewrite it
73
+ origRefresh();
74
+ // Nuke everything below the prompt line, then draw fresh.
75
+ // This avoids stale content from previous renders at different widths.
76
+ const cols = this.out.columns || 80;
77
+ let buf = esc.eraseDown;
78
+ let linesBelow = 0;
79
+ // Bottom border
80
+ buf += `\n${this.buildBorder()}`;
81
+ linesBelow++;
82
+ // Status line
83
+ const status = this.getStatusLine();
84
+ if (status) {
85
+ buf += `\n${truncateAnsi(status, cols - 1)}`;
86
+ linesBelow++;
87
+ }
88
+ // Dropdown lines
89
+ for (const line of this.dropdownLines) {
90
+ buf += `\n${truncateAnsi(line, cols - 1)}`;
91
+ linesBelow++;
92
+ }
93
+ this.out.write(buf);
94
+ // Move cursor back up to the prompt line
95
+ this.out.write(esc.moveUp(linesBelow));
96
+ // Restore cursor column
97
+ const promptText = this.rl._prompt ?? "";
98
+ const promptLen = stripAnsi(promptText).length;
99
+ const cursor = this.rl.cursor ?? 0;
100
+ this.out.write(cursorToCol(promptLen + cursor + 1));
101
+ this.refreshing = false;
102
+ };
103
+ }
104
+ listenResize() {
105
+ this.out.on("resize", () => {
106
+ if (!this.active)
107
+ return;
108
+ // After resize, terminal has reflowed all content. Cursor-relative
109
+ // navigation to old content is unreliable (a 150-char border now
110
+ // wraps to 2 lines on an 80-col terminal, etc.).
111
+ //
112
+ // Strategy: scroll past any reflowed junk by writing blank lines,
113
+ // then draw a fresh prompt area. The old top border in scrollback
114
+ // may look wrong — that's acceptable, same as any terminal app.
115
+ this.out.write(`\n\n${esc.eraseLine}`);
116
+ this.out.write(`${this.buildBorder()}\n`);
117
+ this.rl.prompt();
118
+ });
119
+ }
120
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * PromptInput — consolonia-based replacement for node:readline.
3
+ *
4
+ * Uses consolonia's InputProcessor for raw terminal input parsing
5
+ * (escape sequences, bracketed paste, mouse events) and renders
6
+ * the prompt line + fenced borders directly via ANSI escape codes.
7
+ *
8
+ * This is a scrolling REPL input component, NOT a full-screen TUI.
9
+ * Agent output scrolls normally above; only the input area is managed.
10
+ *
11
+ * Layout:
12
+ * ────────────────────────────────────────
13
+ * ❯ user input here|
14
+ * ────────────────────────────────────────
15
+ * (optional dropdown lines)
16
+ */
17
+ import { EventEmitter } from "node:events";
18
+ export interface PromptInputOptions {
19
+ /** Prompt string (may include ANSI color codes). */
20
+ prompt?: string;
21
+ /** Border character (default: "─"). */
22
+ borderChar?: string;
23
+ /** ANSI style wrapper for the border. */
24
+ borderStyle?: (s: string) => string;
25
+ /** Command history entries (most recent last). */
26
+ history?: string[];
27
+ /**
28
+ * Intercept up/down arrow keys before history navigation.
29
+ * Return true to consume the key (e.g. for wordwheel navigation).
30
+ */
31
+ onUpDown?: (direction: "up" | "down") => boolean;
32
+ /**
33
+ * Called just before Enter submits the line. Can modify the value
34
+ * (e.g. to accept a wordwheel selection). Return the final line text,
35
+ * or undefined to use the current value as-is.
36
+ */
37
+ beforeSubmit?: (currentValue: string) => string | undefined;
38
+ /**
39
+ * Colorize the input value for display. Must preserve visible length
40
+ * (only add ANSI codes, don't change the text). Used for syntax
41
+ * highlighting @mentions and /commands.
42
+ */
43
+ colorize?: (value: string) => string;
44
+ /**
45
+ * Return dim placeholder text to show after the current input value.
46
+ * Called on every refresh. Return null or empty string for no hint.
47
+ * The returned text is displayed in dim style and is not part of the value.
48
+ */
49
+ hint?: (value: string) => string | null;
50
+ }
51
+ export declare class PromptInput extends EventEmitter {
52
+ private _prompt;
53
+ private _promptLen;
54
+ private _borderChar;
55
+ private _borderStyle;
56
+ private _value;
57
+ private _cursor;
58
+ private _history;
59
+ private _historyIndex;
60
+ private _savedInput;
61
+ private _active;
62
+ private _dropdownLines;
63
+ private _linesBelow;
64
+ private _processor;
65
+ private _events;
66
+ private _dataHandler;
67
+ private _resizeHandler;
68
+ private _wasRawMode;
69
+ private _onUpDown;
70
+ private _beforeSubmit;
71
+ private _colorize;
72
+ private _hint;
73
+ private _resizeTimer;
74
+ private _promptRows;
75
+ private _cursorRow;
76
+ private _drawnCols;
77
+ private _statusLine;
78
+ constructor(options?: PromptInputOptions);
79
+ /** Current line text. */
80
+ get line(): string;
81
+ /** Current cursor position. */
82
+ get cursor(): number;
83
+ /** Whether the input is active (accepting keystrokes). */
84
+ get active(): boolean;
85
+ /** Command history. */
86
+ get history(): string[];
87
+ /** Set prompt text. */
88
+ set prompt(text: string);
89
+ get prompt(): string;
90
+ /** Set the line text and move cursor to end. */
91
+ setLine(text: string): void;
92
+ /** Show dropdown content below the bottom border. */
93
+ setDropdown(lines: string[]): void;
94
+ /** Clear dropdown content. */
95
+ clearDropdown(): void;
96
+ /**
97
+ * Activate the prompt: enable raw mode, bracketed paste, draw UI.
98
+ * Call this to show the prompt and start accepting input.
99
+ */
100
+ activate(): void;
101
+ /**
102
+ * Deactivate the prompt: stop drawing but keep raw mode and input
103
+ * handling active so Ctrl+C still works during dispatch.
104
+ */
105
+ deactivate(): void;
106
+ /**
107
+ * Erase the entire prompt area (top border + prompt + bottom border + dropdown)
108
+ * and deactivate. Cursor ends at the position where the top border was.
109
+ * Used when the prompt area should be replaced with other content (e.g. user message block).
110
+ */
111
+ deactivateAndErase(): void;
112
+ /**
113
+ * Set a status line that renders above the top border.
114
+ * Pass null to clear it. The status line is re-rendered in place
115
+ * without redrawing the entire prompt — ideal for animation.
116
+ */
117
+ setStatus(text: string | null): void;
118
+ /** Fully close the input, destroy processor, restore terminal. */
119
+ close(): void;
120
+ private _handleInput;
121
+ private _handlePaste;
122
+ private _handleKey;
123
+ private _historyBack;
124
+ private _historyForward;
125
+ private _wordBoundaryLeft;
126
+ private _wordBoundaryRight;
127
+ private _cols;
128
+ private _buildBorder;
129
+ private _drawStatusLine;
130
+ private _drawTopBorder;
131
+ private _drawPromptLine;
132
+ private _drawBelow;
133
+ /** Full refresh of the prompt area. */
134
+ private _refresh;
135
+ private _onResize;
136
+ }