killeros 2.0.17 → 2.0.19

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
@@ -4,6 +4,56 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.0.19] - 2026-08-24
8
+
9
+ ### Changed
10
+
11
+ - Hardened JSON and caught-error boundaries, made goal and `/init` target states constructive, added exhaustive outcome handling, and replaced unsafe test doubles with checked adapters.
12
+ - Deprecated positional `executeHook` arguments in favor of object options; the compatibility adapter will be removed in the next major release.
13
+
14
+ ### Fixed
15
+
16
+ - Paused active goals during automatic compaction and resumed the exact paused revision once after compaction and turn settlement succeed.
17
+ - Restored active goals saved by v2.0.18 shutdown checkpoints that omitted their stopped clock timestamp.
18
+
19
+ ## [2.0.18] - 2026-08-24
20
+
21
+ ### Added
22
+
23
+ - Added a real Pi SDK lifecycle test for package activation, shutdown-before-reload, fresh registration, and stale-context rejection.
24
+
25
+ ### Changed
26
+
27
+ - Raised the locked Pi development packages and minimum supported Pi peer version to 0.84.3.
28
+ - Made Pi lifecycle compatibility coverage install the generated npm tarball offline before activation and reload.
29
+
30
+ ### Fixed
31
+
32
+ - Preserved provider-managed OAuth routing during `/handoff`, fixing GitHub Copilot `421 Misdirected Request` failures.
33
+ - Repaired incompatible process-global `/codex-fast` state during extension updates while preserving a valid saved enabled flag.
34
+ - Stopped lifecycle-hook failures from echoing configured shell commands, which could expose inline credentials in Pi diagnostics.
35
+ - Kept lifecycle-hook configuration paths terminal-safe and single-line in warnings.
36
+ - Limited trusted lifecycle-hook configuration to a 64 KiB regular, non-linked file in the project's real `.pi` directory and verified the opened file identity before parsing it.
37
+ - Sanitized dynamic slash-command descriptions and omitted unsafe command names from KillerOS autocomplete.
38
+ - Rejected explicit file goals when their starting filesystem baseline cannot be inspected instead of treating every error as a missing file.
39
+ - Kept custom model and provider labels single-line and terminal-safe in the shell header and footer.
40
+ - Sanitized model-reported `/init` policy conflicts before returning, storing, or notifying with them.
41
+ - Kept session-storage failure details terminal-safe when an explicit goal pause falls back to in-memory state.
42
+ - Stripped terminal commands and unsafe controls from saved goal objectives before clear confirmations.
43
+ - Kept rejected `/variants` arguments terminal-safe and single-line in error notifications.
44
+ - Kept provider and model identifiers terminal-safe and single-line throughout `/variants`.
45
+ - Flushed partial UTF-8 lifecycle-hook output on process close instead of silently dropping final diagnostic bytes.
46
+ - Contained synchronous lifecycle-hook process-start failures inside normal hook failure handling.
47
+ - Kept header and footer paths single-line and terminal-safe for unusual cwd or home-directory names.
48
+ - Sanitized manual-compaction abort diagnostics before saving recovery-eligible goal state.
49
+ - Stripped terminal commands and unsafe controls from saved goal text before showing status panels or notifications.
50
+ - Sanitized provider and storage diagnostics before persisting or announcing an automatically paused goal.
51
+ - Routed handoff generation and session-replacement failures through the shared terminal-safe error reporter.
52
+ - Stripped terminal commands and unsafe control bytes from caught errors before showing KillerOS failure notifications.
53
+ - Stripped terminal commands and control bytes from cwd and session names before emitting the KillerOS terminal title.
54
+ - Removed filesystem source paths from personal-instruction prompt wrappers so unusual project paths cannot alter prompt structure.
55
+ - Stripped terminal commands and unsafe control bytes from lifecycle hook failure notifications and tool block reasons.
56
+
7
57
  ## [2.0.17] - 2026-08-23
8
58
 
9
59
  ### Added
package/README.md CHANGED
@@ -19,7 +19,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
19
19
  ## Requirements
20
20
 
21
21
  - Node.js 22.19.0+
22
- - Pi 0.84.2+
22
+ - Pi 0.84.3+
23
23
  - An interactive TUI session for the custom header, editor, footer, `question`, and `/init`
24
24
 
25
25
  ## Install
@@ -34,7 +34,7 @@ Or from GitHub:
34
34
  pi install git:github.com/KyrosHendrix/pi-KillerOS
35
35
  ```
36
36
 
37
- Pin a release by appending its tag, for example `@v2.0.17`. Add `-l` to install only for the current project. Restart Pi after installing.
37
+ Pin a release by appending its tag, for example `@v2.0.19`. Add `-l` to install only for the current project. Restart Pi after installing.
38
38
 
39
39
  ## Commands
40
40
 
@@ -85,7 +85,7 @@ Releases go through CI on `main`; do not push version tags manually.
85
85
 
86
86
  ## Security
87
87
 
88
- Pi extensions run with your user permissions. Review the source before installing globally. Hook commands run only for projects Pi marks as trusted; check `.pi/killeros-hooks.json` before enabling project trust.
88
+ Pi extensions run with your user permissions. Review the source before installing globally. Hook commands run only for projects Pi marks as trusted; check `.pi/killeros-hooks.json` before enabling project trust. KillerOS accepts that configuration only as a regular, non-linked file no larger than 64 KiB in the project's real `.pi` directory.
89
89
 
90
90
  ## License
91
91
 
@@ -52,6 +52,10 @@ export function formatActivityMessage(message: ActivityMessage, theme: Theme): s
52
52
  detail = `using ${safeToolName(message.toolName)}`;
53
53
  }
54
54
  break;
55
+ default: {
56
+ const exhaustive: never = message;
57
+ return exhaustive;
58
+ }
55
59
  }
56
60
 
57
61
  return `${theme.fg("accent", verb)} ${theme.fg("dim", `(${theme.bold("esc")} to interrupt · ${detail})`)}`;
@@ -5,16 +5,25 @@ interface CodexFastState {
5
5
  listeners: Set<CodexFastStateListener>;
6
6
  }
7
7
 
8
- const GLOBAL_STATE_KEY = "__killerosCodexFastState";
9
- type GlobalWithCodexFastState = typeof globalThis & {
10
- [GLOBAL_STATE_KEY]?: CodexFastState;
11
- };
8
+ declare global {
9
+ var __killerosCodexFastState: unknown;
10
+ }
11
+
12
+ // Accepts process-global state only when it is safe to reuse across extension versions.
13
+ function isCodexFastState(value: unknown): value is CodexFastState {
14
+ return typeof value === "object" && value !== null
15
+ && "enabled" in value && typeof value.enabled === "boolean"
16
+ && "listeners" in value && value.listeners instanceof Set
17
+ && [...value.listeners].every((listener) => typeof listener === "function");
18
+ }
12
19
 
13
- const globalState = globalThis as GlobalWithCodexFastState;
14
- const state = globalState[GLOBAL_STATE_KEY] ??= {
15
- enabled: false,
20
+ const savedState = globalThis.__killerosCodexFastState;
21
+ const state: CodexFastState = isCodexFastState(savedState) ? savedState : {
22
+ enabled: typeof savedState === "object" && savedState !== null
23
+ && "enabled" in savedState && savedState.enabled === true,
16
24
  listeners: new Set<CodexFastStateListener>(),
17
25
  };
26
+ globalThis.__killerosCodexFastState = state;
18
27
 
19
28
  export function isCodexFastEnabled(): boolean {
20
29
  return state.enabled;
@@ -1,5 +1,6 @@
1
1
  import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AutocompleteItem } from "@earendil-works/pi-tui";
3
+ import { safeTerminalText } from "./safe-terminal-text.ts";
3
4
 
4
5
  async function confirmNewSession(ctx: ExtensionCommandContext): Promise<boolean> {
5
6
  if (!ctx.hasUI) return true;
@@ -87,6 +88,11 @@ interface TaggedAutocompleteItem extends AutocompleteItem {
87
88
  const SLASH_COMMAND_PREFIX_PATTERN = /(?:^|[ \t])\/([^\s/]*)$/u;
88
89
  const SLASH_COMMAND_TOKEN_PATTERN = /(?:^|[ \t])\/([^\s/]+)(?=$|[ \t])/gu;
89
90
 
91
+ function safeCommandName(name: string): string | undefined {
92
+ const safe = safeTerminalText(name).replaceAll("\n", "").trim();
93
+ return safe === name && safe && !/[\s/]/u.test(safe) ? safe : undefined;
94
+ }
95
+
90
96
  export function getSlashCommandPrefix(line: string): { prefix: string; slashIndex: number } | undefined {
91
97
  const match = SLASH_COMMAND_PREFIX_PATTERN.exec(line);
92
98
  if (!match || match.index === undefined) return undefined;
@@ -107,7 +113,8 @@ export function findSlashCommandTokens(line: string): SlashCommandToken[] {
107
113
  }
108
114
 
109
115
  function commandNameFromAutocompleteItem(item: AutocompleteItem): string {
110
- return (item.value || item.label).replace(/^\//u, "").trim().split(/\s+/u)[0] ?? "";
116
+ const name = (item.value || item.label).replace(/^\//u, "").trim().split(/\s+/u)[0] ?? "";
117
+ return safeCommandName(name) ?? "";
111
118
  }
112
119
 
113
120
  export function createSlashCommandResolver(
@@ -124,16 +131,20 @@ export function createSlashCommandResolver(
124
131
  }));
125
132
 
126
133
  for (const command of pi.getCommands()) {
134
+ const name = safeCommandName(command.name);
135
+ if (!name) continue;
127
136
  const category: CommandInfo["category"] = command.source === "skill"
128
137
  ? "Skill"
129
138
  : command.source === "prompt"
130
139
  ? "Prompt"
131
140
  : "Extension";
132
- commands.set(command.name, {
133
- name: command.name,
134
- description: command.description,
141
+ commands.set(name, {
142
+ name,
143
+ description: command.description === undefined
144
+ ? undefined
145
+ : safeTerminalText(command.description).replaceAll("\n", " "),
135
146
  category,
136
- syntaxHint: COMMAND_SYNTAX_HINTS[command.name],
147
+ syntaxHint: COMMAND_SYNTAX_HINTS[name],
137
148
  });
138
149
  }
139
150
 
@@ -142,7 +153,11 @@ export function createSlashCommandResolver(
142
153
  : fallbackCommands;
143
154
  for (const [name, description] of baseCommands) {
144
155
  if (name && !commands.has(name)) {
145
- commands.set(name, { name, description, category: "Built-in" });
156
+ commands.set(name, {
157
+ name,
158
+ description: description === undefined ? undefined : safeTerminalText(description).replaceAll("\n", " "),
159
+ category: "Built-in",
160
+ });
146
161
  }
147
162
  }
148
163
  return commands;
@@ -219,9 +234,11 @@ export function registerSlashAutocomplete(
219
234
  };
220
235
  },
221
236
  applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
222
- const tagged = item as TaggedAutocompleteItem;
223
- if (!tagged.killerosCommand) return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
224
- usage.set(tagged.killerosCommand, (usage.get(tagged.killerosCommand) ?? 0) + 1);
237
+ const commandName = "killerosCommand" in item && typeof item.killerosCommand === "string"
238
+ ? item.killerosCommand
239
+ : undefined;
240
+ if (!commandName) return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
241
+ usage.set(commandName, (usage.get(commandName) ?? 0) + 1);
225
242
  const line = lines[cursorLine] ?? "";
226
243
  const beforeCursor = line.slice(0, cursorCol);
227
244
  const afterCursor = line.slice(cursorCol);
@@ -1,18 +1,21 @@
1
1
  import os from "node:os";
2
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { safeTerminalText } from "./safe-terminal-text.ts";
3
4
 
5
+ /** Formats a terminal-safe, single-line project path with home abbreviation. */
4
6
  export function formatCwd(cwd: string): string {
5
- const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
6
- if (!home) return cwd;
7
+ const safeCwd = safeTerminalText(cwd).replaceAll("\n", "");
8
+ const home = safeTerminalText(process.env.HOME || process.env.USERPROFILE || os.homedir()).replaceAll("\n", "");
9
+ if (!home) return safeCwd;
7
10
  const normalizedHome = home.replace(/[\\/]+$/, "");
8
- const normalizedCwd = cwd.replace(/[\\/]+$/, "");
11
+ const normalizedCwd = safeCwd.replace(/[\\/]+$/, "");
9
12
  const comparedHome = process.platform === "win32" ? normalizedHome.toLowerCase() : normalizedHome;
10
13
  const comparedCwd = process.platform === "win32" ? normalizedCwd.toLowerCase() : normalizedCwd;
11
14
  if (comparedCwd === comparedHome) return "~";
12
15
  const separator = normalizedCwd.slice(normalizedHome.length, normalizedHome.length + 1);
13
16
  return comparedCwd.startsWith(comparedHome) && (separator === "/" || separator === "\\")
14
17
  ? `~${normalizedCwd.slice(normalizedHome.length)}`
15
- : cwd;
18
+ : safeCwd;
16
19
  }
17
20
 
18
21
  export function padRight(text: string, width: number): string {
@@ -1,8 +1,17 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { safeTerminalText } from "./safe-terminal-text.ts";
3
+
4
+ /** Checks a caught value for one exact string error code. */
5
+ export function hasErrorCode(error: unknown, code: string): boolean {
6
+ return typeof error === "object"
7
+ && error !== null
8
+ && "code" in error
9
+ && error.code === code;
10
+ }
2
11
 
3
12
  /** Converts unknown caught values into text suitable for user-facing errors. */
4
13
  export function errorMessage(error: unknown): string {
5
- return error instanceof Error ? error.message : String(error);
14
+ return safeTerminalText(error instanceof Error ? error.message : String(error));
6
15
  }
7
16
 
8
17
  export function reportError(ctx: ExtensionContext, area: string, error: unknown): void {
@@ -4,6 +4,7 @@ import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
4
4
  import { formatCwd, formatTime, formatTokens, padRight } from "./display.ts";
5
5
  import { goalElapsedMilliseconds } from "./goals.ts";
6
6
  import type { GoalRuntime, GoalState } from "./runtime.ts";
7
+ import { safeTerminalText } from "./safe-terminal-text.ts";
7
8
  import { LEVEL_COLORS, type ThinkingLevel } from "./variants.ts";
8
9
 
9
10
  const FOOTER_REFRESH_INTERVAL_MS = 1_000;
@@ -76,7 +77,7 @@ const PROVIDER_WORDS: Readonly<Record<string, string>> = {
76
77
  };
77
78
 
78
79
  function formatProviderName(provider: string): string {
79
- const normalized = provider.trim();
80
+ const normalized = safeTerminalText(provider).replaceAll("\n", "").trim();
80
81
  const known = PROVIDER_LABELS[normalized.toLowerCase()];
81
82
  if (known) return known;
82
83
  return normalized
@@ -87,7 +88,8 @@ function formatProviderName(provider: string): string {
87
88
  }
88
89
 
89
90
  function modelDisplayName(model: NonNullable<ExtensionContext["model"]>): string {
90
- return model.name?.trim() || model.id;
91
+ const name = safeTerminalText(model.name ?? "").replaceAll("\n", "").trim();
92
+ return name || safeTerminalText(model.id).replaceAll("\n", "").trim() || "Unknown model";
91
93
  }
92
94
 
93
95
  export function formatModel(
@@ -194,7 +196,7 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
194
196
  unsubscribeCodexFast = subscribeCodexFast(() => activeTui?.requestRender());
195
197
  const sessionStart = Date.now();
196
198
  currentModel = ctx.model;
197
- thinkingLevel = pi.getThinkingLevel() as ThinkingLevel;
199
+ thinkingLevel = pi.getThinkingLevel();
198
200
  const cwd = formatCwd(ctx.cwd);
199
201
 
200
202
  ctx.ui.setFooter((tui, theme, footerData) => {