drexler 0.2.13 → 0.2.15

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.
@@ -29,6 +29,8 @@ const COMMAND_HINTS: Record<string, string> = {
29
29
  "/save": "/save deal-notes.md",
30
30
  "/save-last": "/save-last last-response.md",
31
31
  "/copy-last": "copy latest response",
32
+ "/setup": "show config + key source",
33
+ "/update": "bun update -g drexler --latest",
32
34
  };
33
35
 
34
36
  const ARGUMENT_TITLES: Record<string, { title: string; hint: string }> = {
@@ -6,13 +6,8 @@ import { useTheme } from "./ThemeContext.tsx";
6
6
  export type DealDeskHeaderStatus = "idle" | "streaming" | "error";
7
7
 
8
8
  export interface DealDeskHeaderProps {
9
- model: string;
10
9
  mood: string;
11
10
  messageCount: number;
12
- themeName?: string;
13
- approximateTokens?: number;
14
- latencyMs?: number | null;
15
- fallbackModel?: string | null;
16
11
  status?: DealDeskHeaderStatus;
17
12
  compact?: boolean;
18
13
  notice?: string;
@@ -24,10 +19,81 @@ const DEFAULT_WIDTH = 80;
24
19
  const MIN_WIDTH = 1;
25
20
  const FRAMED_MIN_WIDTH = 24;
26
21
 
27
- const STATUS_LABEL: Record<DealDeskHeaderStatus, string> = {
28
- idle: "READY",
29
- streaming: "LIVE",
30
- error: "ERROR",
22
+ const BOARDROOM_STATUS: Record<DealDeskHeaderStatus, string> = {
23
+ idle: "BOARDROOM OPEN",
24
+ streaming: "MEMO LIVE",
25
+ error: "COUNSEL PANIC",
26
+ };
27
+
28
+ type DealDeskPoolKey =
29
+ | "fees"
30
+ | "mandate"
31
+ | "risk"
32
+ | "counsel"
33
+ | "morale"
34
+ | "synergy";
35
+
36
+ const DEFAULT_POOL: Record<DealDeskPoolKey, readonly string[]> = {
37
+ fees: ["accruing", "sacred", "non-refundable", "already billed"],
38
+ mandate: ["self-awarded", "board-adjacent", "strategic-ish", "lightly authorized"],
39
+ risk: ["theatrical", "outsourced", "priced in", "someone else's"],
40
+ counsel: ["circling", "evasive", "comfortable", "redlining lunch"],
41
+ morale: ["impaired", "marked down", "technically solvent", "under review"],
42
+ synergy: ["alleged", "unverifiable", "already billed", "pending lawsuit"],
43
+ };
44
+
45
+ const MOOD_POOLS: Record<
46
+ string,
47
+ Partial<Record<DealDeskPoolKey, readonly string[]>>
48
+ > = {
49
+ angry: {
50
+ fees: ["weaponized", "escalating", "aggressively earned", "non-refundable"],
51
+ mandate: ["hostile", "loudly implied", "board-threatening", "self-ratified"],
52
+ risk: ["acceptable", "transferred", "career-limiting", "somebody else's"],
53
+ counsel: ["circling", "sweating", "denying knowledge", "overruled"],
54
+ morale: ["terminated", "impaired", "written off", "reassigned"],
55
+ synergy: ["forced", "mandatory", "hostile", "already billed"],
56
+ },
57
+ exhausted: {
58
+ fees: ["still accruing", "quietly sacred", "tired but billable", "unquestioned"],
59
+ mandate: ["unclear", "half-approved", "forgotten", "pending coffee"],
60
+ risk: ["deferred", "sleepy", "filed tomorrow", "emotionally hedged"],
61
+ counsel: ["unavailable", "out of office", "blinking slowly", "circling"],
62
+ morale: ["written off", "napping", "below guidance", "technically awake"],
63
+ synergy: ["alleged", "too tired to verify", "softly promised", "unfunded"],
64
+ },
65
+ paranoid: {
66
+ fees: ["traced", "escrowed twice", "suspiciously round", "under seal"],
67
+ mandate: ["encrypted", "deniable", "need-to-know", "redacted"],
68
+ risk: ["everywhere", "listening", "unhedged", "wearing a wire"],
69
+ counsel: ["whispering", "triple-checking", "using burner phones", "redacting"],
70
+ morale: ["surveilled", "compartmentalized", "need-to-know", "encrypted"],
71
+ synergy: ["classified", "denied", "redacted", "not in minutes"],
72
+ },
73
+ generous: {
74
+ fees: ["shared emotionally", "still ours", "politely accruing", "gift-wrapped"],
75
+ mandate: ["benevolent", "magnanimous", "soft hostile", "board-blessed"],
76
+ risk: ["forgiven", "socialized", "gently transferred", "nicely hedged"],
77
+ counsel: ["agreeable", "smiling carefully", "comfortable", "charitable"],
78
+ morale: ["briefly up", "subsidized", "pleasantly marked", "gifted options"],
79
+ synergy: ["donated", "mutual-ish", "kindly alleged", "complimentary"],
80
+ },
81
+ ruthless: {
82
+ fees: ["sacred", "extractive", "fully captured", "compounding"],
83
+ mandate: ["hostile", "absolute", "self-awarded", "non-appealable"],
84
+ risk: ["outsourced", "priced in", "assigned to interns", "deleted"],
85
+ counsel: ["overpaid", "comfortable", "aggressively calm", "circling"],
86
+ morale: ["impaired", "irrelevant", "restructured", "sold separately"],
87
+ synergy: ["mandatory", "already billed", "non-consensual", "accretive"],
88
+ },
89
+ victorious: {
90
+ fees: ["captured", "celebrated", "fully earned", "ringing bell"],
91
+ mandate: ["ratified", "triumphant", "board-crowned", "unopposed"],
92
+ risk: ["conquered", "renamed upside", "priced in", "defeated"],
93
+ counsel: ["applauding", "comfortable", "drafting trophies", "filing confetti"],
94
+ morale: ["temporarily high", "marked up", "wearing medals", "overstated"],
95
+ synergy: ["declared", "victorious", "already billed", "banner-ready"],
96
+ },
31
97
  };
32
98
 
33
99
  function clampText(input: string, max: number): string {
@@ -51,98 +117,194 @@ function bodyLine(content: string, width: number): string {
51
117
  return `│ ${padToWidth(clampText(content, innerWidth), innerWidth)} │`;
52
118
  }
53
119
 
54
- function countLabel(messageCount: number, compact: boolean): string {
55
- if (compact) return `${messageCount} msg${messageCount === 1 ? "" : "s"}`;
56
- return `${messageCount} message${messageCount === 1 ? "" : "s"}`;
57
- }
58
-
59
- function latencyLabel(latencyMs: number | null | undefined): string | null {
60
- if (typeof latencyMs !== "number") return null;
61
- if (latencyMs < 1000) return `${Math.max(0, Math.round(latencyMs))}ms`;
62
- return `${(latencyMs / 1000).toFixed(1)}s`;
120
+ function memoLabel(messageCount: number): string {
121
+ const noun = "memo";
122
+ return `${messageCount} ${noun}${messageCount === 1 ? "" : "s"}`;
63
123
  }
64
124
 
65
125
  function tinyLine({
66
- model,
67
126
  messageCount,
68
127
  status,
69
128
  width,
70
129
  }: {
71
- model: string;
72
130
  messageCount: number;
73
131
  status: DealDeskHeaderStatus;
74
132
  width: number;
75
133
  }): string {
76
134
  return clampText(
77
- `${STATUS_LABEL[status]} ${countLabel(messageCount, true)} ${model}`,
135
+ `${BOARDROOM_STATUS[status]} ${memoLabel(messageCount)}`,
78
136
  width,
79
137
  );
80
138
  }
81
139
 
140
+ function pickFromMoodPool({
141
+ key,
142
+ mood,
143
+ salt,
144
+ }: {
145
+ key: DealDeskPoolKey;
146
+ mood: string;
147
+ salt: number;
148
+ }): string {
149
+ const pool = MOOD_POOLS[mood.toLowerCase()]?.[key] ?? DEFAULT_POOL[key];
150
+ return pool[Math.abs(salt) % pool.length] ?? DEFAULT_POOL[key][0];
151
+ }
152
+
153
+ function hashDealDesk(input: string): number {
154
+ let hash = 2166136261;
155
+ for (let idx = 0; idx < input.length; idx++) {
156
+ hash ^= input.charCodeAt(idx);
157
+ hash = Math.imul(hash, 16777619);
158
+ }
159
+ return hash >>> 0;
160
+ }
161
+
162
+ function formatCells(cells: string[], width: number): string {
163
+ const separator = " │ ";
164
+ const available = Math.max(
165
+ 1,
166
+ width - displayWidth(separator) * Math.max(0, cells.length - 1),
167
+ );
168
+ const base = Math.max(1, Math.floor(available / cells.length));
169
+ const remainder = Math.max(0, available - base * cells.length);
170
+ return cells
171
+ .map((cell, idx) => {
172
+ const cellWidth = base + (idx < remainder ? 1 : 0);
173
+ return padToWidth(clampText(cell, cellWidth), cellWidth);
174
+ })
175
+ .join(separator);
176
+ }
177
+
82
178
  function buildHeaderLines({
83
- model,
84
179
  mood,
85
180
  messageCount,
86
- themeName,
87
- approximateTokens,
88
- latencyMs,
89
- fallbackModel,
90
181
  status,
91
182
  compact,
92
183
  notice,
93
184
  width,
185
+ seed,
94
186
  }: {
95
- model: string;
96
187
  mood: string;
97
188
  messageCount: number;
98
- themeName?: string;
99
- approximateTokens?: number;
100
- latencyMs?: number | null;
101
- fallbackModel?: string | null;
102
189
  status: DealDeskHeaderStatus;
103
190
  compact: boolean;
104
191
  notice?: string;
105
192
  width: number;
193
+ seed: number;
106
194
  }): string[] {
107
- const statusLabel = STATUS_LABEL[status];
108
- const latency = latencyLabel(latencyMs);
109
- const top = compact
110
- ? shellLine("┌ Drexler ", "┐", width)
111
- : shellLine("┌ Drexler Deal Desk ", "┐", width);
112
-
195
+ const innerWidth = Math.max(1, width - 4);
196
+ const baseHash = hashDealDesk(`${mood}:${messageCount}:${status}:${seed}`);
197
+ const pick = (key: DealDeskPoolKey, offset: number) =>
198
+ pickFromMoodPool({ key, mood, salt: baseHash + offset * 7919 });
199
+ const statusLabel = BOARDROOM_STATUS[status];
113
200
  const summary = compact
114
- ? `● ${statusLabel} ${model} ${countLabel(messageCount, true)}${
115
- latency ? ` ${latency}` : ""
116
- }`
117
- : `● ${statusLabel} │ ${countLabel(
118
- messageCount,
119
- false,
120
- )} │ ~${approximateTokens ?? 0} tok │ ${latency ?? "no run yet"}`;
121
- const detail = `model ${model} │ mood ${mood} │ theme ${
122
- themeName ?? "apollo"
123
- }${fallbackModel ? ` │ fallback ${fallbackModel}` : ""}`;
124
- const lines = [top, bodyLine(summary, width)];
125
-
126
- if (!compact) {
127
- lines.push(bodyLine(detail, width));
128
- }
201
+ ? formatCells(
202
+ [`● ${statusLabel}`, `mood ${mood}`, `fees ${pick("fees", 1)}`],
203
+ innerWidth,
204
+ )
205
+ : formatCells(
206
+ [
207
+ `● ${statusLabel}`,
208
+ memoLabel(messageCount),
209
+ `fees ${pick("fees", 1)}`,
210
+ ],
211
+ innerWidth,
212
+ );
213
+ const readout = compact
214
+ ? formatCells(
215
+ [`risk ${pick("risk", 2)}`, `counsel ${pick("counsel", 3)}`],
216
+ innerWidth,
217
+ )
218
+ : formatCells(
219
+ [
220
+ `mandate ${pick("mandate", 4)}`,
221
+ `risk ${pick("risk", 5)}`,
222
+ `counsel ${pick("counsel", 6)}`,
223
+ ],
224
+ innerWidth,
225
+ );
226
+ const lines = [bodyLine(summary, width), bodyLine(readout, width)];
129
227
 
130
228
  if (!compact && notice && notice.trim().length > 0) {
131
- lines.push(bodyLine(`notice ${notice.trim()}`, width));
229
+ const memo = formatCells(
230
+ [
231
+ `memo ${notice.trim()}`,
232
+ `morale ${pick("morale", 7)}`,
233
+ `synergy ${pick("synergy", 8)}`,
234
+ ],
235
+ innerWidth,
236
+ );
237
+ lines.push(bodyLine(memo, width));
132
238
  }
133
239
 
134
- lines.push(shellLine("", "", width));
240
+ lines.push(shellLine("", "", width));
135
241
  return lines;
136
242
  }
137
243
 
244
+ function titleLabel(compact: boolean): string {
245
+ return compact ? "Drexler" : "Drexler Deal Desk";
246
+ }
247
+
248
+ function FramedTitleText({
249
+ compact,
250
+ borderColor,
251
+ titleColor,
252
+ width,
253
+ }: {
254
+ compact: boolean;
255
+ borderColor: string;
256
+ titleColor: string;
257
+ width: number;
258
+ }) {
259
+ const title = titleLabel(compact);
260
+ const prefix = "╭─ ";
261
+ const titleSuffix = " ";
262
+ const suffix = "╮";
263
+ const ruleWidth = Math.max(
264
+ 0,
265
+ width -
266
+ displayWidth(prefix) -
267
+ displayWidth(title) -
268
+ displayWidth(titleSuffix) -
269
+ displayWidth(suffix),
270
+ );
271
+ return (
272
+ <Text>
273
+ <Text color={borderColor}>{prefix}</Text>
274
+ <Text bold color={titleColor}>
275
+ {title}
276
+ </Text>
277
+ <Text color={borderColor}>
278
+ {titleSuffix}
279
+ {"─".repeat(ruleWidth)}
280
+ {suffix}
281
+ </Text>
282
+ </Text>
283
+ );
284
+ }
285
+
286
+ function FramedBodyText({
287
+ line,
288
+ borderColor,
289
+ contentColor,
290
+ }: {
291
+ line: string;
292
+ borderColor: string;
293
+ contentColor: string;
294
+ }) {
295
+ const content = line.length >= 4 ? line.slice(2, -2) : line;
296
+ return (
297
+ <Text>
298
+ <Text color={borderColor}>│ </Text>
299
+ <Text color={contentColor}>{content}</Text>
300
+ <Text color={borderColor}> │</Text>
301
+ </Text>
302
+ );
303
+ }
304
+
138
305
  function DealDeskHeaderInner({
139
- model,
140
306
  mood,
141
307
  messageCount,
142
- themeName,
143
- approximateTokens,
144
- latencyMs,
145
- fallbackModel,
146
308
  status = "idle",
147
309
  compact = false,
148
310
  notice,
@@ -151,6 +313,7 @@ function DealDeskHeaderInner({
151
313
  }: DealDeskHeaderProps) {
152
314
  const t = useTheme();
153
315
  const width = Math.max(MIN_WIDTH, Math.floor(maxWidth));
316
+ const randomSeed = useMemo(() => Math.floor(Math.random() * 1_000_000_000), []);
154
317
  const statusColor: Record<DealDeskHeaderStatus, string> = useMemo(
155
318
  () => ({
156
319
  idle: t.primaryLight,
@@ -159,32 +322,25 @@ function DealDeskHeaderInner({
159
322
  }),
160
323
  [t.error, t.primaryLight, t.warning],
161
324
  );
325
+ const summaryColor = status === "idle" ? t.text : statusColor[status];
162
326
  const lines = useMemo(
163
327
  () =>
164
328
  buildHeaderLines({
165
- model,
166
329
  mood,
167
330
  messageCount,
168
- themeName,
169
- approximateTokens,
170
- latencyMs,
171
- fallbackModel,
172
331
  status,
173
332
  compact,
174
333
  notice,
175
334
  width,
335
+ seed: randomSeed,
176
336
  }),
177
337
  [
178
- approximateTokens,
179
338
  compact,
180
- fallbackModel,
181
- latencyMs,
182
339
  messageCount,
183
- model,
184
340
  mood,
185
341
  notice,
342
+ randomSeed,
186
343
  status,
187
- themeName,
188
344
  width,
189
345
  ],
190
346
  );
@@ -193,7 +349,7 @@ function DealDeskHeaderInner({
193
349
  return (
194
350
  <Box width={width} marginBottom={marginBottom}>
195
351
  <Text color={statusColor[status]} wrap="truncate">
196
- {tinyLine({ model, messageCount, status, width })}
352
+ {tinyLine({ messageCount, status, width })}
197
353
  </Text>
198
354
  </Box>
199
355
  );
@@ -201,14 +357,26 @@ function DealDeskHeaderInner({
201
357
 
202
358
  return (
203
359
  <Box flexDirection="column" width={width} marginBottom={marginBottom}>
204
- <Text color={t.primaryDim}>{lines[0]}</Text>
205
- <Text color={statusColor[status]}>{lines[1]}</Text>
206
- {lines.slice(2, -1).map((line, index) => (
207
- <Text key={index} color={index === 0 ? t.primaryLight : t.dim}>
208
- {line}
209
- </Text>
360
+ <FramedTitleText
361
+ compact={compact}
362
+ borderColor={t.primary}
363
+ titleColor={t.primaryLight}
364
+ width={width}
365
+ />
366
+ <FramedBodyText
367
+ line={lines[0] ?? ""}
368
+ borderColor={t.primary}
369
+ contentColor={summaryColor}
370
+ />
371
+ {lines.slice(1, -1).map((line, index) => (
372
+ <FramedBodyText
373
+ key={index}
374
+ line={line}
375
+ borderColor={t.primary}
376
+ contentColor={index === 0 ? t.text : t.dim}
377
+ />
210
378
  ))}
211
- <Text color={t.primaryDim}>{lines[lines.length - 1]}</Text>
379
+ <Text color={t.primary}>{lines[lines.length - 1]}</Text>
212
380
  </Box>
213
381
  );
214
382
  }
@@ -0,0 +1,110 @@
1
+ import { Box, Text } from "ink";
2
+ import { useTheme } from "./ThemeContext.tsx";
3
+
4
+ // Five distinct death variants
5
+ const VARIANTS = [
6
+ {
7
+ headline: "STAKEHOLDERS IN SHAMBLES",
8
+ lines: [
9
+ "The board convenes in emergency session.",
10
+ "Markets react with characteristic cruelty.",
11
+ "Analyst consensus revised to: AVOID.",
12
+ ],
13
+ },
14
+ {
15
+ headline: "PIPELINE: BONE DRY. BOARD: DEVASTATED.",
16
+ lines: [
17
+ "Emergency restructuring announced immediately.",
18
+ "Remaining staff issued terse memo: 'hang tight.'",
19
+ "The deal room is very, very quiet.",
20
+ ],
21
+ },
22
+ {
23
+ headline: "ANALYSTS REVISE TARGET PRICE TO ZERO",
24
+ lines: [
25
+ "Short sellers: vindicated, quietly smug.",
26
+ "Drexler could not be reached for comment.",
27
+ "His last email had a typo. The irony.",
28
+ ],
29
+ },
30
+ {
31
+ headline: "SEC OPENS INQUIRY INTO CIRCUMSTANCES",
32
+ lines: [
33
+ "CNBC coverage: 47 minutes, then nothing.",
34
+ "Drexler's legacy: a half-finished term sheet.",
35
+ "The coffee mug on his desk: still warm.",
36
+ ],
37
+ },
38
+ {
39
+ headline: "EMERGENCY CALL SCHEDULED FOR 7AM MONDAY",
40
+ lines: [
41
+ "Consensus: it could have been prevented.",
42
+ "The plant on his desk is already wilting.",
43
+ "Recruiters texted his LinkedIn at 4am.",
44
+ ],
45
+ },
46
+ ] as const;
47
+
48
+ const REASON_MSGS: Record<string, string> = {
49
+ hunger: "Cause: severe caloric deficiency. The pipeline, unreplenished, consumed itself.",
50
+ happiness: "Cause: total morale collapse. The board's confidence evaporated entirely.",
51
+ energy: "Cause: complete energy depletion. Drexler's systems ceased. Standups continued.",
52
+ };
53
+
54
+ // Stock chart — backslash must be escaped in TS string literals
55
+ const CHART: string[] = [
56
+ " 100 ┤\\",
57
+ " │ \\",
58
+ " │ \\",
59
+ " 50 ┤ \\",
60
+ " │ \\",
61
+ " │ \\________________________________",
62
+ " 0 ┴───────────────────────────────────────",
63
+ " Q1 Q2 Q3 Q4 Q5 now →",
64
+ ];
65
+
66
+ const INNER_W = 44;
67
+
68
+ function banner(text: string): string {
69
+ const pad = Math.max(0, INNER_W - text.length);
70
+ const lp = Math.floor(pad / 2);
71
+ const rp = pad - lp;
72
+ return "║" + " ".repeat(lp) + text + " ".repeat(rp) + "║";
73
+ }
74
+
75
+ const TOP = "╔" + "═".repeat(INNER_W) + "╗";
76
+ const BOT = "╚" + "═".repeat(INNER_W) + "╝";
77
+
78
+ interface Props {
79
+ reason?: string;
80
+ variant?: number;
81
+ }
82
+
83
+ export function DeathScreen({ reason = "energy", variant = 0 }: Props) {
84
+ const t = useTheme();
85
+ const v = VARIANTS[variant % VARIANTS.length] ?? VARIANTS[0];
86
+ const reasonMsg = REASON_MSGS[reason] ?? REASON_MSGS.energy;
87
+
88
+ return (
89
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
90
+ <Text color={t.error} bold>{TOP}</Text>
91
+ <Text color={t.error} bold>{banner("D R E X L E R H A S D I E D")}</Text>
92
+ <Text color={t.error} bold>{BOT}</Text>
93
+ <Text> </Text>
94
+ <Text color={t.warning} bold> {v.headline}</Text>
95
+ {v.lines.map((line, i) => (
96
+ <Text key={i} color={t.dim}> {line}</Text>
97
+ ))}
98
+ <Text> </Text>
99
+ <Text color={t.primaryDim}> {reasonMsg}</Text>
100
+ <Text> </Text>
101
+ <Text color={t.primaryDim}> DRXL Share Price:</Text>
102
+ {CHART.map((line, i) => (
103
+ <Text key={i} color={i < 6 ? t.error : t.dim}> {line}</Text>
104
+ ))}
105
+ <Text> </Text>
106
+ <Text color={t.dim}> Stats reset to 50% on next launch.</Text>
107
+ <Text color={t.dim}> Exiting in 5 seconds...</Text>
108
+ </Box>
109
+ );
110
+ }