drexler 0.2.12 → 0.2.14

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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.14
4
+
5
+ - Added a startup Mood panel with a stable boot gauge, percentage-only loading row, and rotating mood-specific posture/detail copy.
6
+ - Anchored the wide startup dashboard so wrapped greeting copy no longer pushes the Mood and Deal Desk boxes down or adds stray rows.
7
+ - Reworked the embedded Deal Desk into satirical mood-shaped product chrome instead of model/context telemetry.
8
+ - Improved Drexler transcript rendering with complete bordered cards, a diamond response marker, cleaned markdown/code fence display, and Dracula-inspired code syntax colors.
9
+ - Updated documentation to match current startup chrome, command palette behavior, keyboard controls, source setup, and layout invariants.
10
+
11
+ ## 0.2.13
12
+
13
+ - Hardened startup panel layout across narrow, standard, and wide terminals.
14
+ - Clamped the embedded Deal Desk to its actual startup-panel column.
15
+ - Improved display-width clipping for Deal Desk, command palette, spinner, status bar, and transcript row budgeting.
16
+ - Added regression coverage for duplicate startup chrome, wide glyphs, long command rows, and short-terminal startup suppression.
17
+
3
18
  ## 0.2.12
4
19
 
5
20
  - Removed the duplicate startup card render so normal launches show one startup panel.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![license](https://img.shields.io/npm/l/drexler.svg)](./LICENSE)
5
5
  [![bun](https://img.shields.io/badge/runtime-bun%20%E2%89%A5%201.1-black)](https://bun.sh)
6
6
 
7
- CLI chat with **Drexler**, a corporate-executive AI persona who speaks in broken third-person and treats every conversation like a hostile takeover. Built with Bun + TypeScript. Talks to OpenRouter's Gemma 4 31B model (paid).
7
+ CLI chat with **Drexler**, a corporate-executive AI persona who speaks in broken third-person and treats every conversation like a hostile takeover. Built with Bun + TypeScript, Ink, and OpenRouter-compatible Gemma models.
8
8
 
9
9
  > "Drexler usually charge consulting fee for this. Today, pro bono. You welcome."
10
10
 
@@ -13,7 +13,7 @@ CLI chat with **Drexler**, a corporate-executive AI persona who speaks in broken
13
13
  ## Quickstart
14
14
 
15
15
  ```bash
16
- bun add -g drexler
16
+ bun install -g drexler@latest
17
17
  drexler
18
18
  ```
19
19
 
@@ -39,7 +39,7 @@ Verify: `bun --version` → should print `1.1.0` or higher.
39
39
  ### 2. Install Drexler globally
40
40
 
41
41
  ```bash
42
- bun add -g drexler
42
+ bun install -g drexler@latest
43
43
  ```
44
44
 
45
45
  This installs the `drexler` command into `~/.bun/bin/drexler`. Make sure `~/.bun/bin` is on your `$PATH` (Bun's installer does this automatically; if not, add `export PATH="$HOME/.bun/bin:$PATH"` to your shell rc).
@@ -65,9 +65,11 @@ Paste the key, hit return. Drexler saves it to `~/.config/drexler/config.json` (
65
65
  ## Update
66
66
 
67
67
  ```bash
68
- bun update -g drexler
68
+ bun install -g drexler@latest
69
69
  ```
70
70
 
71
+ Global installs replace the existing `drexler` package in Bun's global install location; they do not keep stacking duplicate app copies.
72
+
71
73
  ## Uninstall
72
74
 
73
75
  ```bash
@@ -79,6 +81,33 @@ rm -rf ~/.config/drexler # optional: wipe stored key + settings
79
81
 
80
82
  ## Usage
81
83
 
84
+ ### Interactive UI
85
+
86
+ Drexler runs as an Ink terminal UI when both stdin and stdout are TTYs. The normal launch shows one integrated startup panel with the mascot, tips, a **Mood** readout, and the **Drexler Deal Desk**. Short terminals automatically suppress oversized startup chrome so the chat stays usable.
87
+
88
+ The startup panel is designed to stay stable while it boots: the mascot loading bar and Mood gauge animate without changing width, greeting copy is held in a fixed slot, and the Mood and Deal Desk boxes stay aligned when the greeting wraps. After boot, Mood resolves into a rotating Drexler-flavored posture with a short satirical subtext line.
89
+
90
+ The Deal Desk is intentionally not a frontier-model telemetry panel. It shows mood-shaped corporate nonsense like boardroom status, memo count, mandate, risk, fees, and counsel posture. Values rotate by mood and session so repeated moods still feel alive.
91
+
92
+ Conversation turns render as bordered cards aligned to the chat input width. User and Drexler responses use separate accents, wrapped text, and fixed-width borders so long responses stay inside the terminal instead of clipping at the right edge. Drexler responses use a diamond body marker. Markdown/code fence labels are cleaned up for display, and code blocks render with Dracula-inspired terminal syntax colors.
93
+
94
+ Typing `/` opens the directive palette. Use `Tab`, `Enter`, or `↑`/`↓` to select. Commands with fixed arguments open smoother option choosers:
95
+
96
+ - `/theme` previews all themes with descriptions.
97
+ - `/startup` offers `fast`, `no-intro`, and `normal`.
98
+ - `/retry` offers `terse` and `brutal`.
99
+ - `/export` offers `md`, `txt`, `json`, and `html`.
100
+ - `/model` offers `31b` and `26b`.
101
+
102
+ `/synergy` runs a rotating animated corporate event in the live UI, then returns control to the chat when the animation completes.
103
+
104
+ Keyboard notes:
105
+
106
+ - `Tab`, `Enter`, and `↑`/`↓` operate the directive palette.
107
+ - `PageUp`/`PageDown` scroll transcript history when it exceeds the visible viewport.
108
+ - `Esc` cancels an in-flight model response without quitting.
109
+ - `Ctrl+C` exits gracefully with an in-character farewell.
110
+
82
111
  ### Flags
83
112
 
84
113
  | flag | what |
@@ -93,12 +122,12 @@ rm -rf ~/.config/drexler # optional: wipe stored key + settings
93
122
 
94
123
  ### Slash commands (inside the REPL)
95
124
 
96
- | cmd | what it do |
125
+ | cmd | what it does |
97
126
  | -------------- | ------------------------------------------------ |
98
127
  | `/help` | list directives |
99
128
  | `/clear` | shred conversation history (system prompt pinned) |
100
129
  | `/exit` | meeting adjourned |
101
- | `/synergy` | SYNERGY! |
130
+ | `/synergy` | run a rotating animated morale event |
102
131
  | `/model` | show current model, or `/model 26b` to switch |
103
132
  | `/theme` | show/switch theme; append `save` to persist, e.g. `/theme midnight save` |
104
133
  | `/startup fast\|no-intro\|normal` | persist startup behavior for future launches |
@@ -114,8 +143,6 @@ rm -rf ~/.config/drexler # optional: wipe stored key + settings
114
143
  | `/save-last [path]` | save Drexler's last response only |
115
144
  | `/copy-last` | copy Drexler's latest response to the clipboard |
116
145
 
117
- `Ctrl+C` exits gracefully with an in-character farewell.
118
-
119
146
  ---
120
147
 
121
148
  ## Configuration
@@ -126,6 +153,8 @@ Drexler reads config in this priority (later wins):
126
153
  2. Environment variables
127
154
  3. CLI flags
128
155
 
156
+ If the current config file does not exist, Drexler also checks the legacy `~/.drexlerrc` path.
157
+
129
158
  ### Environment variables
130
159
 
131
160
  | var | purpose |
@@ -159,13 +188,26 @@ Default `maxHistory`: 50 messages.
159
188
  Available launch/config themes: `apollo`, `amber`, `mono`, `terminal`, `dealroom`, `midnight`, `paper`, and `plasma`.
160
189
  `NO_COLOR` always forces `mono`.
161
190
 
191
+ Theme notes:
192
+
193
+ | theme | character |
194
+ | ---------- | ------------------------------------------ |
195
+ | `apollo` | signature Drexler green, the default |
196
+ | `amber` | warm amber deal glow |
197
+ | `mono` | plain high-contrast ANSI colors |
198
+ | `terminal` | classic green/cyan terminal |
199
+ | `dealroom` | restrained teal boardroom palette |
200
+ | `midnight` | cool blue late-session desk |
201
+ | `paper` | clean document-style contrast |
202
+ | `plasma` | high-energy magenta trading-floor accent |
203
+
162
204
  ---
163
205
 
164
206
  ## Models
165
207
 
166
208
  | alias | id | notes |
167
209
  | ------ | ----------------------------------- | ------------------------------ |
168
- | `31b` | `google/gemma-4-31b-it` | primary (paid) |
210
+ | `31b` | `google/gemma-4-31b-it` | primary default |
169
211
  | `26b` | `google/gemma-4-26b-a4b-it` | fallback, auto-retry on 429 |
170
212
 
171
213
  Pass `--model vendor/name:tag` for any other OpenRouter-compatible model.
@@ -178,7 +220,7 @@ Pass `--model vendor/name:tag` for any other OpenRouter-compatible model.
178
220
  git clone https://github.com/showOS/Drexler.git
179
221
  cd Drexler
180
222
  bun install
181
- cp .env.example .env # then edit, paste key into OPENROUTER_API_KEY=
223
+ export OPENROUTER_API_KEY=sk-or-v1-your-key
182
224
  bun run start
183
225
  ```
184
226
 
@@ -192,11 +234,12 @@ bun run typecheck
192
234
  ### Releasing a new version
193
235
 
194
236
  ```bash
195
- npm version <patch|minor> # bumps package.json, commits, tags
196
- git push --follow-tags # CI publishes to npm automatically
237
+ bun run prepublishOnly
238
+ npm version <patch|minor> # bumps package.json, commits, and tags
239
+ git push origin main --follow-tags
197
240
  ```
198
241
 
199
- The `.github/workflows/publish.yml` workflow runs typecheck + tests + `npm publish --provenance` on every `v*` tag push.
242
+ The `.github/workflows/publish.yml` workflow runs install, tag/package version verification, typecheck, tests, and `npm publish --provenance` on every `v*` tag push.
200
243
 
201
244
  ---
202
245
 
@@ -210,6 +253,8 @@ The `.github/workflows/publish.yml` workflow runs typecheck + tests + `npm publi
210
253
  | Garbled box-drawing characters | Use a UTF-8 terminal with a Nerd Font (e.g. iTerm2, Alacritty, WezTerm) |
211
254
  | Want to switch themes mid-session | Use `/theme midnight`, `/theme dealroom`, `/theme amber`, or any listed theme inside the REPL |
212
255
  | Want a faster launch | Use `drexler --fast` or set `"fast": true` in config |
256
+ | Startup panel looks cramped | Enlarge the terminal, or use `/startup no-intro` or `/startup fast` |
257
+ | Slash command options are not visible | Type `/`, `/theme`, `/startup`, `/retry`, `/export`, or `/model`; exact fixed-argument commands open their chooser automatically |
213
258
 
214
259
  ---
215
260
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drexler",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "CLI chat with Drexler, a corporate-executive AI persona built on OpenRouter Gemma 4 31B.",
5
5
  "license": "MIT",
6
6
  "author": "showOS",
package/src/index.ts CHANGED
@@ -133,7 +133,7 @@ async function main(): Promise<void> {
133
133
  }
134
134
 
135
135
  console.log("");
136
- console.log(" " + infoLine() + " · mood: " + mood);
136
+ console.log(" " + infoLine());
137
137
  console.log("");
138
138
 
139
139
  const { waitUntilExit } = render(
package/src/ui/App.tsx CHANGED
@@ -34,7 +34,11 @@ import {
34
34
  insertAtCursor,
35
35
  } from "./graphemes.ts";
36
36
  import { InputBox } from "./InputBox.tsx";
37
- import { MascotDashboard } from "./MascotIntro.tsx";
37
+ import {
38
+ introPhaseColor,
39
+ MascotDashboard,
40
+ useIntroAnimation,
41
+ } from "./MascotIntro.tsx";
38
42
  import { StreamingMessage } from "./Message.tsx";
39
43
  import { Spinner } from "./Spinner.tsx";
40
44
  import { StatusBar } from "./StatusBar.tsx";
@@ -136,14 +140,17 @@ export function App({
136
140
  const chromeWidth = useMemo(() => Math.max(1, cols), [cols]);
137
141
  const statusBarWidth = inputWidth;
138
142
  const isCompact = mode === "very-narrow";
139
- const integratedIntro = showIntroChrome && typeof greeting === "string";
143
+ const integratedIntro =
144
+ showIntroChrome && typeof greeting === "string" && rows >= 32;
145
+ const introRowBudget =
146
+ integratedIntro ? (chromeWidth >= 112 ? 14 : chromeWidth >= 72 ? 26 : 6) : 0;
140
147
  const maxTranscriptRows = useMemo(
141
148
  () =>
142
149
  Math.max(
143
150
  1,
144
- transcriptRowsForTerminalRows(rows) - (integratedIntro ? 10 : 0),
151
+ transcriptRowsForTerminalRows(rows) - introRowBudget,
145
152
  ),
146
- [integratedIntro, rows],
153
+ [introRowBudget, rows],
147
154
  );
148
155
 
149
156
  const [items, setItems] = useState<ChatItem[]>([]);
@@ -201,6 +208,7 @@ export function App({
201
208
  const [historyIdx, setHistoryIdx] = useState<number | null>(null);
202
209
  const [paletteIdx, setPaletteIdx] = useState(0);
203
210
  const [scrollOffset, setScrollOffset] = useState(0);
211
+ const intro = useIntroAnimation(chromeWidth, integratedIntro);
204
212
 
205
213
  const paletteItems = useMemo(() => filterPaletteByPrefix(input), [input]);
206
214
  const paletteOpen = paletteItems.length > 0;
@@ -716,11 +724,7 @@ export function App({
716
724
  const isBusy =
717
725
  requestInFlight || streaming !== null || thinking !== null || synergyEvent !== null;
718
726
  const headerStatus = isBusy ? "streaming" : deskStatus;
719
- const embeddedDealDeskWidth =
720
- chromeWidth >= 112
721
- ? Math.min(72, Math.max(42, Math.floor(chromeWidth * 0.34)))
722
- : Math.max(32, chromeWidth - 8);
723
- const dealDeskHeader = (
727
+ const renderDealDeskHeader = (width: number) => (
724
728
  <DealDeskHeader
725
729
  model={model}
726
730
  mood={mood}
@@ -732,13 +736,15 @@ export function App({
732
736
  status={headerStatus}
733
737
  compact={isCompact}
734
738
  notice={!integratedIntro ? deskNotice ?? undefined : undefined}
735
- maxWidth={integratedIntro ? embeddedDealDeskWidth : chromeWidth}
739
+ maxWidth={Math.max(1, width)}
736
740
  marginBottom={integratedIntro ? 0 : 1}
737
741
  />
738
742
  );
743
+ const dealDeskHeader = renderDealDeskHeader(chromeWidth);
739
744
  const visibleTranscriptRows = synergyEvent
740
745
  ? Math.max(1, maxTranscriptRows - synergyEventRows(chromeWidth, isCompact))
741
746
  : maxTranscriptRows;
747
+ const introBarColor = introPhaseColor(intro.colorPhase, t);
742
748
 
743
749
  return (
744
750
  <ThemeProvider value={activeTheme}>
@@ -748,7 +754,13 @@ export function App({
748
754
  <MascotDashboard
749
755
  greeting={greeting}
750
756
  width={chromeWidth}
751
- dealDesk={dealDeskHeader}
757
+ mood={mood}
758
+ bootProgress={intro.progress}
759
+ state={intro.state}
760
+ bar={intro.bar}
761
+ barColor={introBarColor}
762
+ mascotStatus={intro.status}
763
+ dealDesk={renderDealDeskHeader}
752
764
  />
753
765
  </Box>
754
766
  ) : (
@@ -769,7 +781,7 @@ export function App({
769
781
  </Box>
770
782
  )}
771
783
  {thinking !== null && streaming === null && (
772
- <Box paddingX={1} marginBottom={1}>
784
+ <Box marginBottom={1}>
773
785
  <Spinner label={thinking} width={chromeWidth} />
774
786
  </Box>
775
787
  )}
@@ -809,11 +821,11 @@ export function App({
809
821
  width={inputWidth}
810
822
  />
811
823
  </Box>
812
- <Box>
824
+ <Box paddingLeft={2}>
813
825
  <StatusBar
814
826
  messageCount={msgCount}
815
827
  witticism={witticism}
816
- maxWidth={statusBarWidth}
828
+ maxWidth={Math.max(1, statusBarWidth - 2)}
817
829
  status={isBusy ? "streaming" : deskStatus}
818
830
  compact={isCompact}
819
831
  scrollHint={scrollHint}
@@ -54,13 +54,18 @@ function paletteHeading(items: ReadonlyArray<SlashCommand>): {
54
54
  };
55
55
  }
56
56
 
57
+ function padDisplayText(input: string, width: number): string {
58
+ const clipped = fitDisplayText(input, width);
59
+ return `${clipped}${" ".repeat(Math.max(0, width - displayWidth(clipped)))}`;
60
+ }
61
+
57
62
  function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
58
63
  const t = useTheme();
59
64
  const safeWidth = Math.max(1, Math.floor(width));
60
65
  const tiny = safeWidth < 26;
61
66
  const heading = useMemo(() => paletteHeading(items), [items]);
62
67
  const maxNameW = useMemo(
63
- () => items.reduce((m, i) => Math.max(m, i.name.length), 0),
68
+ () => items.reduce((m, i) => Math.max(m, displayWidth(i.name)), 0),
64
69
  [items],
65
70
  );
66
71
  if (items.length === 0) return null;
@@ -87,10 +92,21 @@ function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
87
92
  }
88
93
 
89
94
  const innerWidth = Math.max(1, safeWidth - 4);
90
- const descBudget = Math.max(8, Math.floor(innerWidth * 0.36));
95
+ const markerWidth = 2;
96
+ const nameBudget = Math.max(
97
+ 6,
98
+ Math.min(maxNameW + 1, Math.floor(innerWidth * 0.44)),
99
+ );
100
+ const descBudget = Math.max(
101
+ 6,
102
+ Math.min(
103
+ Math.floor(innerWidth * 0.34),
104
+ innerWidth - markerWidth - nameBudget - 1,
105
+ ),
106
+ );
91
107
  const hintBudget = Math.max(
92
108
  0,
93
- innerWidth - 4 - maxNameW - descBudget - 4,
109
+ innerWidth - markerWidth - nameBudget - 1 - descBudget - 2,
94
110
  );
95
111
 
96
112
  return (
@@ -121,18 +137,12 @@ function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
121
137
  const hint =
122
138
  item.hint ??
123
139
  (isArgumentSuggestion ? "" : COMMAND_HINTS[item.name] ?? item.description);
124
- const name = item.name.padEnd(maxNameW + 1);
125
- const desc = fitDisplayText(item.description, descBudget);
140
+ const name = padDisplayText(item.name, nameBudget);
141
+ const desc = padDisplayText(item.description, descBudget);
126
142
  const clippedHint =
127
143
  hintBudget > 0 ? fitDisplayText(hint, hintBudget) : "";
128
- const rowWidth =
129
- 2 +
130
- displayWidth(name) +
131
- 1 +
132
- displayWidth(desc) +
133
- (clippedHint ? 2 + displayWidth(clippedHint) : 0);
134
144
  return (
135
- <Box key={item.name} width={Math.min(innerWidth, rowWidth)} flexShrink={1}>
145
+ <Box key={item.name} width={innerWidth} flexShrink={1}>
136
146
  <Text color={sel ? t.primaryLight : t.primaryDim} bold={sel}>
137
147
  {sel ? "› " : " "}
138
148
  </Text>
@@ -140,7 +150,7 @@ function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
140
150
  {name}
141
151
  </Text>
142
152
  <Text color={t.primaryDim}> </Text>
143
- <Text color={sel ? t.text : t.dim} wrap="truncate">
153
+ <Text color={sel ? t.text : t.dim}>
144
154
  {desc}
145
155
  </Text>
146
156
  {clippedHint ? (