killeros 1.0.0 → 1.0.1

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 (3) hide show
  1. package/Killeros.ts +81 -129
  2. package/README.md +7 -11
  3. package/package.json +3 -2
package/Killeros.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  } from "@earendil-works/pi-coding-agent";
13
13
  import {
14
14
  Container,
15
+ decodeKittyPrintable,
15
16
  Editor,
16
17
  Key,
17
18
  matchesKey,
@@ -35,30 +36,17 @@ const FOOTER_REFRESH_INTERVAL_MS = 1_000;
35
36
 
36
37
  const brand = (text: string): string => `\x1B[38;2;${BRAND_RGB}m${text}\x1B[39m`;
37
38
 
38
- interface LogoFrame {
39
- phase: number;
40
- active: "left" | "top" | "right" | "none";
41
- ax: number;
42
- ay: number;
43
- flash: boolean;
44
- white: boolean;
45
- }
46
-
47
- const LOGO_FRAMES: LogoFrame[] = [
48
- ...Array.from({ length: 4 }, (_, ay): LogoFrame => ({ phase: 0, active: "left", ax: 2, ay, flash: false, white: false })),
49
- ...Array.from({ length: 3 }, (_, ay): LogoFrame => ({ phase: 1, active: "top", ax: 2, ay, flash: false, white: false })),
50
- ...Array.from({ length: 5 }, (_, ay): LogoFrame => ({ phase: 2, active: "right", ax: 5, ay, flash: false, white: false })),
51
- { phase: 3, active: "none", ax: 0, ay: 0, flash: false, white: false },
52
- { phase: 3, active: "none", ax: 0, ay: 0, flash: true, white: false },
53
- { phase: 3, active: "none", ax: 0, ay: 0, flash: false, white: false },
54
- { phase: 3, active: "none", ax: 0, ay: 0, flash: true, white: false },
55
- { phase: 4, active: "none", ax: 0, ay: 0, flash: false, white: false },
56
- { phase: 5, active: "none", ax: 0, ay: 0, flash: false, white: false },
57
- { phase: 5, active: "none", ax: 0, ay: 0, flash: false, white: true },
58
- { phase: 5, active: "none", ax: 0, ay: 0, flash: false, white: false },
59
- { phase: 5, active: "none", ax: 0, ay: 0, flash: false, white: true },
60
- { phase: 6, active: "none", ax: 0, ay: 0, flash: false, white: false },
61
- ];
39
+ const ORCA_ART = [
40
+ ".....g......",
41
+ "....ggg.....",
42
+ ".gggggggg..g",
43
+ "gbggwwgggggg",
44
+ ".ggwwgggg..g",
45
+ "...gggg.....",
46
+ ".....gg.....",
47
+ ] as const;
48
+ const ORCA_WIDTH = ORCA_ART[0].length;
49
+ const LOGO_FRAME_COUNT = ORCA_WIDTH + 3;
62
50
 
63
51
  function extractProvider(model: ExtensionContext["model"]): string {
64
52
  return model?.provider ?? "";
@@ -89,71 +77,27 @@ function padRight(text: string, width: number): string {
89
77
  return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
90
78
  }
91
79
 
92
- function hasCell(y: number, x: number, cells: string): boolean {
93
- return cells.split(" ").includes(`${y},${x}`);
94
- }
95
-
96
- function hasPiece(y: number, x: number, py: number, px: number, cells: string): boolean {
97
- return cells.split(" ").some((item) => {
98
- const [dy, dx] = item.split(",").map(Number);
99
- return y === py + dy && x === px + dx;
100
- });
101
- }
102
-
103
- type LogoColor = "cyan" | "red" | "green" | "orange" | "flash" | "white" | "brand" | "panel";
80
+ type LogoColor = "body" | "white" | "brand" | "panel";
104
81
 
105
82
  function colorCell(color: LogoColor): string {
106
83
  switch (color) {
107
- case "cyan": return `\x1B[36m${LOGO_CELL}\x1B[39m`;
108
- case "red": return `\x1B[31m${LOGO_CELL}\x1B[39m`;
109
- case "green": return `\x1B[32m${LOGO_CELL}\x1B[39m`;
110
- case "orange":
111
- case "flash": return `\x1B[33m${LOGO_CELL}\x1B[39m`;
84
+ case "body": return `\x1B[90m${LOGO_CELL}\x1B[39m`;
112
85
  case "white": return `\x1B[97m${LOGO_CELL}\x1B[39m`;
113
86
  case "brand": return brand(LOGO_CELL);
114
87
  default: return " ".repeat(LOGO_CELL.length);
115
88
  }
116
89
  }
117
90
 
118
- function logoCellColor(frame: LogoFrame, y: number, x: number): LogoColor {
119
- if (frame.white) {
120
- return hasCell(y, x, "3,2 3,3 3,4 4,2 4,4 5,2 5,3 5,5 6,2 6,5") ? "white" : "panel";
121
- }
122
- if (frame.flash && y === 6 && x >= 1 && x <= 6) return "flash";
123
- if (frame.active === "left" && hasPiece(y, x, frame.ay, frame.ax, "0,0 1,0 1,1 2,0")) return "red";
124
- if (frame.active === "top" && hasPiece(y, x, frame.ay, frame.ax, "0,0 0,1 0,2 1,2")) return "cyan";
125
- if (frame.active === "right" && hasPiece(y, x, frame.ay, frame.ax, "0,0 1,0 2,0 2,1")) return "green";
126
- if (frame.phase === 6) {
127
- return hasCell(y, x, "3,2 3,3 3,4 4,4 4,2 5,2 5,3 5,5 6,2 6,5") ? "brand" : "panel";
128
- }
129
- if (frame.phase === 4) {
130
- if (hasCell(y, x, "2,2 2,3 2,4 3,4")) return "cyan";
131
- if (hasCell(y, x, "3,2 4,2 4,3 5,2")) return "red";
132
- if (hasCell(y, x, "4,5 5,5")) return "green";
133
- return "panel";
134
- }
135
- if (frame.phase >= 5) {
136
- if (hasCell(y, x, "3,2 3,3 3,4 4,4")) return "cyan";
137
- if (hasCell(y, x, "4,2 5,2 5,3 6,2")) return "red";
138
- if (hasCell(y, x, "5,5 6,5")) return "green";
139
- return "panel";
140
- }
141
- if (frame.phase <= 3 && hasCell(y, x, "6,1 6,2 6,3 6,4")) return "orange";
142
- if (frame.phase >= 2 && hasCell(y, x, "2,2 2,3 2,4 3,4")) return "cyan";
143
- if (frame.phase >= 1 && hasCell(y, x, "3,2 4,2 4,3 5,2")) return "red";
144
- if (frame.phase >= 3 && hasCell(y, x, "4,5 5,5 6,5 6,6")) return "green";
145
- return "panel";
146
- }
147
-
148
91
  function piLogoFrame(frameIndex: number): string[] {
149
- const frame = LOGO_FRAMES[frameIndex % LOGO_FRAMES.length]!;
150
- const lines: string[] = [];
151
- for (let y = 1; y <= 7; y += 1) {
152
- let line = "";
153
- for (let x = 1; x <= 8; x += 1) line += colorCell(logoCellColor(frame, y, x));
154
- lines.push(line);
155
- }
156
- return lines;
92
+ const visibleColumns = Math.min(ORCA_WIDTH, frameIndex + 1);
93
+ const eyeColor: LogoColor = frameIndex === ORCA_WIDTH ? "white" : "brand";
94
+ return ORCA_ART.map((row) => [...row].map((cell, index) => {
95
+ if (index >= visibleColumns) return colorCell("panel");
96
+ if (cell === "g") return colorCell("body");
97
+ if (cell === "w") return colorCell("white");
98
+ if (cell === "b") return colorCell(eyeColor);
99
+ return colorCell("panel");
100
+ }).join(""));
157
101
  }
158
102
 
159
103
  function borderLine(left: string, label: string, right: string, width: number): string {
@@ -248,6 +192,9 @@ function getTipLines(index: number, theme: Theme): string[] {
248
192
  }
249
193
 
250
194
  class PiStartupHeader {
195
+ private readonly pi: ExtensionAPI;
196
+ private readonly ctx: ExtensionContext;
197
+ private readonly tui: TUI;
251
198
  private frame = 0;
252
199
  private tipIndex = 0;
253
200
  private animationTimer?: ReturnType<typeof setInterval>;
@@ -255,19 +202,22 @@ class PiStartupHeader {
255
202
  private disposed = false;
256
203
 
257
204
  constructor(
258
- private readonly pi: ExtensionAPI,
259
- private readonly ctx: ExtensionContext,
260
- private readonly tui: TUI,
205
+ pi: ExtensionAPI,
206
+ ctx: ExtensionContext,
207
+ tui: TUI,
261
208
  ) {
209
+ this.pi = pi;
210
+ this.ctx = ctx;
211
+ this.tui = tui;
262
212
  this.animationTimer = setInterval(() => {
263
213
  if (this.disposed) return;
264
- if (this.frame >= LOGO_FRAMES.length - 1) {
214
+ if (this.frame >= LOGO_FRAME_COUNT - 1) {
265
215
  this.stopAnimation();
266
216
  return;
267
217
  }
268
218
  this.frame += 1;
269
219
  this.tui.requestRender();
270
- if (this.frame >= LOGO_FRAMES.length - 1) this.stopAnimation();
220
+ if (this.frame >= LOGO_FRAME_COUNT - 1) this.stopAnimation();
271
221
  }, LOGO_ANIMATION_INTERVAL_MS);
272
222
  this.animationTimer.unref?.();
273
223
 
@@ -301,7 +251,7 @@ class PiStartupHeader {
301
251
  const cwd = formatCwd(this.ctx.cwd);
302
252
  const leftWidth = isTwoColumn ? Math.min(LEFT_PANEL_WIDTH, Math.floor(innerWidth * 0.55)) : innerWidth;
303
253
  const rightWidth = isTwoColumn ? innerWidth - leftWidth - 3 : 0;
304
- const logoLines = leftWidth >= 24
254
+ const logoLines = leftWidth >= ORCA_WIDTH * visibleWidth(LOGO_CELL)
305
255
  ? piLogoFrame(this.frame).map((line) => center(line, leftWidth))
306
256
  : ["", "", center(brand(theme.bold("Pi Coding Agent")), leftWidth), "", "", "", ""];
307
257
  const modelText = leftWidth < 34 ? `${model} (${effort})` : `${model} with ${effort} effort`;
@@ -348,8 +298,11 @@ function isBorderLine(line: string): boolean {
348
298
  }
349
299
 
350
300
  class PiCodeEditor extends CustomEditor {
351
- constructor(tui: TUI, theme: EditorTheme, private readonly appKeybindings: KeybindingsManager) {
301
+ private readonly appKeybindings: KeybindingsManager;
302
+
303
+ constructor(tui: TUI, theme: EditorTheme, appKeybindings: KeybindingsManager) {
352
304
  super(tui, theme, appKeybindings);
305
+ this.appKeybindings = appKeybindings;
353
306
  }
354
307
 
355
308
  override handleInput(data: string): void {
@@ -514,7 +467,34 @@ function rememberCustomInput(value: string): void {
514
467
  }
515
468
 
516
469
  function isPrintableInput(data: string): boolean {
517
- return data.length > 0 && !/[\u0000-\u001F\u007F]/u.test(data);
470
+ return data.length > 0 && !/[\u0000-\u001F\u007F-\u009F]/u.test(data);
471
+ }
472
+
473
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
474
+
475
+ function decodeQuestionFilterInput(data: string): string | undefined {
476
+ const kittyPrintable = decodeKittyPrintable(data);
477
+ if (kittyPrintable !== undefined) return isPrintableInput(kittyPrintable) ? kittyPrintable : undefined;
478
+
479
+ const pasteStart = "\x1B[200~";
480
+ const pasteEnd = "\x1B[201~";
481
+ const startIndex = data.indexOf(pasteStart);
482
+ const endIndex = data.indexOf(pasteEnd, startIndex + pasteStart.length);
483
+ if (startIndex >= 0 && endIndex >= 0) {
484
+ return data
485
+ .slice(startIndex + pasteStart.length, endIndex)
486
+ .replace(/\r\n|\r|\n/gu, "")
487
+ .replace(/\t/gu, " ")
488
+ .replace(/[\u0000-\u001F\u007F-\u009F]/gu, "");
489
+ }
490
+
491
+ return isPrintableInput(data) ? data : undefined;
492
+ }
493
+
494
+ function removeLastGrapheme(value: string): string {
495
+ const segments = [...graphemeSegmenter.segment(value)];
496
+ const last = segments.at(-1);
497
+ return last ? value.slice(0, last.index) : "";
518
498
  }
519
499
 
520
500
  function registerQuestionTool(pi: ExtensionAPI): void {
@@ -552,8 +532,6 @@ function registerQuestionTool(pi: ExtensionAPI): void {
552
532
  let optionIndex = 0;
553
533
  let editMode = false;
554
534
  let filterQuery = "";
555
- let historyIndex = -1;
556
- let savedDraft = "";
557
535
  let cachedWidth: number | undefined;
558
536
  let cachedLines: string[] | undefined;
559
537
  let completed = false;
@@ -576,6 +554,7 @@ function registerQuestionTool(pi: ExtensionAPI): void {
576
554
  },
577
555
  };
578
556
  const editor = new Editor(tui, editorTheme);
557
+ customInputHistory.forEach((value) => editor.addToHistory(value));
579
558
 
580
559
  const filteredOptions = (): DisplayOption[] => {
581
560
  const query = filterQuery.trim().toLocaleLowerCase();
@@ -605,15 +584,11 @@ function registerQuestionTool(pi: ExtensionAPI): void {
605
584
  }
606
585
  editMode = false;
607
586
  editor.setText("");
608
- historyIndex = -1;
609
- savedDraft = "";
610
587
  refresh();
611
588
  };
612
589
 
613
590
  const enterCustomMode = (): void => {
614
591
  editMode = true;
615
- historyIndex = -1;
616
- savedDraft = "";
617
592
  refresh();
618
593
  };
619
594
 
@@ -622,30 +597,6 @@ function registerQuestionTool(pi: ExtensionAPI): void {
622
597
  if (matchesKey(data, Key.escape)) {
623
598
  editMode = false;
624
599
  editor.setText("");
625
- historyIndex = -1;
626
- savedDraft = "";
627
- refresh();
628
- return;
629
- }
630
- if (matchesKey(data, Key.up) && customInputHistory.length > 0) {
631
- if (historyIndex < 0) {
632
- savedDraft = editor.getText();
633
- historyIndex = customInputHistory.length - 1;
634
- } else if (historyIndex > 0) {
635
- historyIndex -= 1;
636
- }
637
- editor.setText(customInputHistory[historyIndex] ?? "");
638
- refresh();
639
- return;
640
- }
641
- if (matchesKey(data, Key.down) && historyIndex >= 0) {
642
- if (historyIndex < customInputHistory.length - 1) {
643
- historyIndex += 1;
644
- editor.setText(customInputHistory[historyIndex] ?? "");
645
- } else {
646
- historyIndex = -1;
647
- editor.setText(savedDraft);
648
- }
649
600
  refresh();
650
601
  return;
651
602
  }
@@ -685,21 +636,23 @@ function registerQuestionTool(pi: ExtensionAPI): void {
685
636
  }
686
637
  if (matchesKey(data, Key.backspace)) {
687
638
  if (filterQuery) {
688
- filterQuery = Array.from(filterQuery).slice(0, -1).join("");
639
+ filterQuery = removeLastGrapheme(filterQuery);
689
640
  optionIndex = 0;
690
641
  refresh();
691
642
  }
692
643
  return;
693
644
  }
694
- if (/^[1-9]$/.test(data)) {
695
- const selected = visibleOptions[Number(data) - 1];
645
+ const printableInput = decodeQuestionFilterInput(data);
646
+ const isPasteInput = data.includes("\x1B[200~");
647
+ if (!isPasteInput && printableInput && /^[1-9]$/.test(printableInput)) {
648
+ const selected = visibleOptions[Number(printableInput) - 1];
696
649
  if (!selected) return;
697
650
  if (selected.isOther) enterCustomMode();
698
651
  else finish({ kind: "selected", answer: selected.label, originalIndex: selected.originalIndex });
699
652
  return;
700
653
  }
701
- if (isPrintableInput(data)) {
702
- filterQuery += data;
654
+ if (printableInput) {
655
+ filterQuery += printableInput;
703
656
  optionIndex = 0;
704
657
  refresh();
705
658
  }
@@ -848,7 +801,7 @@ function registerAliases(pi: ExtensionAPI): void {
848
801
  await ctx.newSession();
849
802
  };
850
803
  pi.registerCommand("clear", { description: "Start a new session after confirmation", handler: startNewSession });
851
- pi.registerCommand("quit", {
804
+ pi.registerCommand("exit", {
852
805
  description: "Quit Pi gracefully",
853
806
  handler: async (_args, ctx) => ctx.shutdown(),
854
807
  });
@@ -891,13 +844,9 @@ const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
891
844
  model: "/model [provider/model]",
892
845
  "scoped-models": "/scoped-models",
893
846
  login: "/login [provider]",
894
- logout: "/logout [provider]",
895
847
  export: "/export [filename]",
896
848
  import: "/import [path]",
897
849
  name: "/name [session-name]",
898
- fork: "/fork [name]",
899
- clone: "/clone [name]",
900
- resume: "/resume [session-id]",
901
850
  };
902
851
 
903
852
  interface TaggedAutocompleteItem extends AutocompleteItem {
@@ -1173,9 +1122,12 @@ export function formatContextProgress(tokensUsed: number | null, contextWindow:
1173
1122
 
1174
1123
  function sumSessionCost(ctx: ExtensionContext): number {
1175
1124
  let total = 0;
1176
- for (const entry of ctx.sessionManager.getBranch()) {
1177
- if (entry.type === "message" && entry.message.role === "assistant") {
1125
+ for (const entry of ctx.sessionManager.getEntries()) {
1126
+ if (entry.type === "message" && entry.message.role === "assistant") total += entry.message.usage.cost.total;
1127
+ else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.usage) {
1178
1128
  total += entry.message.usage.cost.total;
1129
+ } else if ((entry.type === "compaction" || entry.type === "branch_summary") && entry.usage) {
1130
+ total += entry.usage.cost.total;
1179
1131
  }
1180
1132
  }
1181
1133
  return total;
package/README.md CHANGED
@@ -14,8 +14,6 @@ The extension is strict TypeScript and uses only packages provided by Pi.
14
14
 
15
15
  ### npm
16
16
 
17
- After the first npm release:
18
-
19
17
  ```bash
20
18
  pi install npm:killeros
21
19
  ```
@@ -44,7 +42,7 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
44
42
  - `/variants` selector and direct reasoning-level arguments
45
43
  - `question` tool with filtering, keyboard selection, custom answers, history, cancellation, and resize-safe rendering
46
44
  - Mid-prompt slash completion with current Pi `0.82.1` commands, extensions, prompts, and skills
47
- - `/clear` for a confirmed new session, plus `/quit` for graceful shutdown
45
+ - `/clear` for a confirmed new session, plus `/exit` for graceful shutdown
48
46
  - Concise system-prompt guidance without modifying completed assistant messages
49
47
 
50
48
  ## Commands
@@ -53,7 +51,7 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
53
51
  /variants Open the reasoning-level selector
54
52
  /variants high Set a reasoning level directly
55
53
  /clear Start a new session after confirmation
56
- /quit Quit Pi gracefully
54
+ /exit Quit Pi gracefully
57
55
  ```
58
56
 
59
57
  Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. KillerOS limits choices to levels supported by the current model.
@@ -83,6 +81,7 @@ Before release, run:
83
81
  ```bash
84
82
  npm ci
85
83
  npm run check
84
+ npm test
86
85
  npm pack --dry-run
87
86
  pi -e . --mode rpc
88
87
  ```
@@ -93,16 +92,13 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
93
92
 
94
93
  The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog.
95
94
 
95
+ Choose `patch`, `minor`, or `major` for the release, then publish and push the version commit and tag:
96
+
96
97
  ```bash
97
98
  npm login
99
+ npm version patch
98
100
  npm publish
99
- ```
100
-
101
- Create and push a matching Git tag after publication:
102
-
103
- ```bash
104
- git tag v1.0.0
105
- git push origin main v1.0.0
101
+ git push origin main --follow-tags
106
102
  ```
107
103
 
108
104
  ## Security
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A production-hardened TUI and workflow extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -27,7 +27,8 @@
27
27
  "node": ">=22.19.0"
28
28
  },
29
29
  "scripts": {
30
- "check": "tsc --noEmit"
30
+ "check": "tsc --noEmit",
31
+ "test": "node --test --experimental-strip-types test/*.test.js"
31
32
  },
32
33
  "pi": {
33
34
  "extensions": [