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.
@@ -34,20 +34,21 @@ function readBoundedText(filePath: string, limit = PERSONAL_INSTRUCTIONS_LIMIT):
34
34
  }
35
35
  }
36
36
 
37
- export function resolvePersonalInstructions(cwd: string): { content: string; source: string } | undefined {
37
+ export function resolvePersonalInstructions(cwd: string): string | undefined {
38
38
  const localPath = path.join(cwd, PERSONAL_INSTRUCTIONS_FILE);
39
39
  const local = readBoundedText(localPath);
40
40
  if (!local) return undefined;
41
41
 
42
42
  const importMatch = local.trim().match(/^@(.+)$/u);
43
- if (!importMatch) return { content: local, source: localPath };
44
-
45
- const requestedPath = importMatch[1]!.trim();
46
- const importedPath = requestedPath.startsWith("~/") || requestedPath.startsWith("~\\")
47
- ? path.join(os.homedir(), requestedPath.slice(2))
48
- : path.resolve(cwd, requestedPath);
49
- const imported = readBoundedText(importedPath);
50
- return imported ? { content: imported, source: importedPath } : { content: local, source: localPath };
43
+ let content = local;
44
+ if (importMatch) {
45
+ const requestedPath = (importMatch[1] ?? "").trim();
46
+ const importedPath = requestedPath.startsWith("~/") || requestedPath.startsWith("~\\")
47
+ ? path.join(os.homedir(), requestedPath.slice(2))
48
+ : path.resolve(cwd, requestedPath);
49
+ content = readBoundedText(importedPath) ?? local;
50
+ }
51
+ return `<personal_instructions>\n${content}\n</personal_instructions>`;
51
52
  }
52
53
 
53
54
  export function registerPersonalInstructions(pi: ExtensionAPI, initState: InitRuntime): void {
@@ -59,9 +60,7 @@ export function registerPersonalInstructions(pi: ExtensionAPI, initState: InitRu
59
60
  systemPrompt: [
60
61
  event.systemPrompt,
61
62
  "",
62
- `<personal_instructions source="${personal.source}">`,
63
- personal.content,
64
- "</personal_instructions>",
63
+ personal,
65
64
  ].join("\n"),
66
65
  };
67
66
  });
@@ -165,7 +165,9 @@ function boundedQuestionLines(question: string, width: number, rowLimit: number)
165
165
  const wrapped = wrapTextWithAnsi(question.replace(/\s+/gu, " ").trim(), width);
166
166
  if (wrapped.length <= rowLimit) return wrapped;
167
167
  const visible = wrapped.slice(0, rowLimit);
168
- visible[rowLimit - 1] = truncateToWidth(visible[rowLimit - 1]!, width, "…");
168
+ const finalIndex = rowLimit - 1;
169
+ const finalLine = visible[finalIndex];
170
+ if (finalLine !== undefined) visible[finalIndex] = truncateToWidth(finalLine, width, "…");
169
171
  return visible;
170
172
  }
171
173
 
@@ -173,12 +175,12 @@ function compactMultipleAnswers(answers: readonly string[], width: number): stri
173
175
  const prefix = "✓ ";
174
176
  if (answers.length === 0) return truncateToWidth(prefix + "No answers", width, "…");
175
177
  const visible: string[] = [];
176
- for (let index = 0; index < answers.length; index += 1) {
178
+ for (const [index, answer] of answers.entries()) {
177
179
  const remaining = answers.length - index - 1;
178
- const candidate = [...visible, oneLine(answers[index]!)].join(", ");
180
+ const candidate = [...visible, oneLine(answer)].join(", ");
179
181
  const suffix = remaining > 0 ? `, +${remaining} more` : "";
180
182
  if (visibleWidth(prefix + candidate + suffix) > width) break;
181
- visible.push(oneLine(answers[index]!));
183
+ visible.push(oneLine(answer));
182
184
  }
183
185
  if (visible.length === answers.length) return prefix + visible.join(", ");
184
186
  const hidden = answers.length - visible.length;
@@ -228,7 +230,8 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
228
230
  if (bytes > CUSTOM_INPUT_HISTORY_BYTES) return false;
229
231
  const existingIndex = customInputHistory.indexOf(value);
230
232
  if (existingIndex >= 0) {
231
- customInputHistoryBytes -= Buffer.byteLength(customInputHistory[existingIndex]!, "utf8");
233
+ const existing = customInputHistory[existingIndex];
234
+ if (existing !== undefined) customInputHistoryBytes -= Buffer.byteLength(existing, "utf8");
232
235
  customInputHistory.splice(existingIndex, 1);
233
236
  }
234
237
  while (customInputHistory.length >= CUSTOM_INPUT_HISTORY_LIMIT || customInputHistoryBytes + bytes > CUSTOM_INPUT_HISTORY_BYTES) {
@@ -329,7 +332,11 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
329
332
  const selectedCount = (): number => selectedOriginalIndices.size + (customAnswer === undefined ? 0 : 1);
330
333
  const orderedMultipleSelection = () => {
331
334
  const selectedIndices = [...selectedOriginalIndices].sort((left, right) => left - right);
332
- const predefined = selectedIndices.map((index) => params.options[index - 1]!.label);
335
+ const predefined = selectedIndices.map((index) => {
336
+ const option = params.options[index - 1];
337
+ if (!option) throw new Error("Question selection no longer matches an available option");
338
+ return option.label;
339
+ });
333
340
  return {
334
341
  answers: customAnswer === undefined ? predefined : [...predefined, customAnswer],
335
342
  selectedIndices,
@@ -640,7 +647,8 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
640
647
  lines.push(...(draftLines.length > 0 ? draftLines : [editMode === "filter" ? "Type a filter" : "Type an answer"]).slice(-contentRows));
641
648
  } else {
642
649
  for (let index = start; index < end; index += 1) {
643
- const option = visibleOptions[index]!;
650
+ const option = visibleOptions[index];
651
+ if (!option) continue;
644
652
  const color: ThemeColor = index === optionIndex ? "accent" : "text";
645
653
  lines.push(theme.fg(color, truncateToWidth(optionLabel(option, index), width, "…")));
646
654
  }
@@ -38,31 +38,63 @@ export interface GoalFileVerification {
38
38
  baseline: GoalFileBaseline;
39
39
  }
40
40
 
41
- export interface GoalState {
41
+ export interface GoalStateCommon {
42
42
  version: 1;
43
43
  revision: number;
44
44
  objective: string;
45
- status: GoalStatus;
46
45
  createdAt: number;
47
46
  updatedAt: number;
48
47
  activeMilliseconds: number;
49
- activeStartedAt?: number;
50
48
  turns: number;
51
49
  blockedAuditStartTurn: number;
52
50
  baselineTokens: number;
53
- result?: string;
54
- resumeAfterManualCompaction?: true;
55
- blockerAudit?: GoalBlockerAudit;
56
51
  verification?: GoalFileVerification;
57
52
  }
58
53
 
54
+ export type GoalState = GoalStateCommon & (
55
+ | {
56
+ status: "active";
57
+ activeStartedAt: number;
58
+ result?: string;
59
+ blockerAudit?: GoalBlockerAudit;
60
+ resumeAfterManualCompaction?: never;
61
+ }
62
+ | {
63
+ status: "paused";
64
+ activeStartedAt?: never;
65
+ result?: string;
66
+ blockerAudit?: GoalBlockerAudit;
67
+ resumeAfterManualCompaction?: true;
68
+ }
69
+ | {
70
+ status: "blocked";
71
+ activeStartedAt?: never;
72
+ result: string;
73
+ blockerAudit?: GoalBlockerAudit;
74
+ resumeAfterManualCompaction?: never;
75
+ }
76
+ | {
77
+ status: "complete";
78
+ activeStartedAt?: never;
79
+ result: string;
80
+ blockerAudit?: never;
81
+ resumeAfterManualCompaction?: never;
82
+ }
83
+ );
84
+
85
+ export interface AutomaticGoalCompaction {
86
+ pausedRevision: number;
87
+ compactionSucceeded: boolean;
88
+ turnSettled: boolean;
89
+ }
90
+
59
91
  export interface GoalRuntime {
60
92
  state?: GoalState;
61
93
  continuationScheduled: boolean;
62
94
  continuationHeld: boolean;
63
95
  goalTurnInFlight: boolean;
64
96
  agentEndObserved: boolean;
65
- automaticCompaction?: "pending";
97
+ automaticCompaction?: AutomaticGoalCompaction;
66
98
  persistenceRetryNeeded: boolean;
67
99
  lastStopReason?: string;
68
100
  lastError?: string;
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import { hasErrorCode } from "./errors.ts";
5
6
 
6
7
  export type KillerosSettings = Record<string, unknown>;
7
8
 
@@ -10,15 +11,17 @@ export interface KillerosSettingsStore {
10
11
  update(patch: Readonly<Record<string, unknown>>): void;
11
12
  }
12
13
 
14
+ function isSettings(value: unknown): value is KillerosSettings {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+
13
18
  function readStoredSettings(settingsPath: string): KillerosSettings {
14
19
  try {
15
20
  const parsed: unknown = JSON.parse(readFileSync(settingsPath, "utf8"));
16
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
17
- throw new Error("KillerOS settings must contain a JSON object");
18
- }
19
- return parsed as KillerosSettings;
21
+ if (!isSettings(parsed)) throw new Error("KillerOS settings must contain a JSON object");
22
+ return parsed;
20
23
  } catch (error) {
21
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
24
+ if (hasErrorCode(error, "ENOENT")) return {};
22
25
  throw error;
23
26
  }
24
27
  }
@@ -25,14 +25,19 @@ import {
25
25
  } from "./commands.ts";
26
26
  import { reportError } from "./errors.ts";
27
27
  import { formatModel } from "./footer.ts";
28
- import { LEVEL_COLORS, type ThinkingLevel } from "./variants.ts";
28
+ import { LEVEL_COLORS } from "./variants.ts";
29
29
 
30
30
  const COMPACT_HEADER_MAX_WIDTH = 52;
31
31
 
32
32
  function readPackageVersion(path: string | URL): string | undefined {
33
33
  try {
34
- const value = JSON.parse(readFileSync(path, "utf8")) as { version?: unknown };
35
- return typeof value.version === "string" ? value.version : undefined;
34
+ const value: unknown = JSON.parse(readFileSync(path, "utf8"));
35
+ return typeof value === "object"
36
+ && value !== null
37
+ && "version" in value
38
+ && typeof value.version === "string"
39
+ ? value.version
40
+ : undefined;
36
41
  } catch {
37
42
  return undefined;
38
43
  }
@@ -93,7 +98,11 @@ function shuffledDeck(values: readonly string[]): string[] {
93
98
  const deck = [...values];
94
99
  for (let index = deck.length - 1; index > 0; index -= 1) {
95
100
  const swapIndex = Math.floor(Math.random() * (index + 1));
96
- [deck[index], deck[swapIndex]] = [deck[swapIndex]!, deck[index]!];
101
+ const current = deck[index];
102
+ const swap = deck[swapIndex];
103
+ if (current === undefined || swap === undefined) continue;
104
+ deck[index] = swap;
105
+ deck[swapIndex] = current;
97
106
  }
98
107
  return deck;
99
108
  }
@@ -152,7 +161,7 @@ class PiStartupHeader {
152
161
  const innerWidth = panelWidth - 4;
153
162
  const version = KILLEROS_VERSION ? theme.fg("dim", ` (v${KILLEROS_VERSION})`) : "";
154
163
  const identity = `${theme.fg("dim", "›")} ${theme.fg("text", theme.bold("KillerOS"))}${version}`;
155
- const thinkingLevel = this.pi.getThinkingLevel() as ThinkingLevel;
164
+ const thinkingLevel = this.pi.getThinkingLevel();
156
165
  const reasoning = this.ctx.model?.reasoning === false
157
166
  ? theme.fg("thinkingOff", "no reasoning")
158
167
  : theme.fg(LEVEL_COLORS[thinkingLevel], thinkingLevel);
@@ -1,5 +1,6 @@
1
1
  import { DynamicBorder, type ExtensionAPI, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
2
2
  import { SelectList, truncateToWidth } from "@earendil-works/pi-tui";
3
+ import { safeTerminalText } from "./safe-terminal-text.ts";
3
4
 
4
5
  export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
5
6
 
@@ -60,7 +61,7 @@ function supportedLevels(model: ExtensionContext["model"]): ThinkingLevel[] {
60
61
  }
61
62
 
62
63
  function modelLabel(model: ExtensionContext["model"]): string {
63
- return model ? `${model.provider}/${model.id}` : "unknown model";
64
+ return model ? safeTerminalText(`${model.provider}/${model.id}`).replaceAll("\n", "") : "unknown model";
64
65
  }
65
66
 
66
67
  export function registerVariants(pi: ExtensionAPI): void {
@@ -80,7 +81,7 @@ export function registerVariants(pi: ExtensionAPI): void {
80
81
  if (args.trim()) {
81
82
  const level = resolveThinkingLevel(args);
82
83
  if (!level) {
83
- ctx.ui.notify(`Unknown reasoning level "${args.trim()}". Use: ${ALL_LEVELS.join(", ")}`, "error");
84
+ ctx.ui.notify(`Unknown reasoning level "${safeTerminalText(args.trim()).replaceAll("\n", "")}". Use: ${ALL_LEVELS.join(", ")}`, "error");
84
85
  return;
85
86
  }
86
87
  setLevel(ctx, level);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
4
4
  "description": "TUI, goals, and workflow automation for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -42,15 +42,15 @@
42
42
  ]
43
43
  },
44
44
  "peerDependencies": {
45
- "@earendil-works/pi-ai": ">=0.84.2",
46
- "@earendil-works/pi-coding-agent": ">=0.84.2",
47
- "@earendil-works/pi-tui": ">=0.84.2",
45
+ "@earendil-works/pi-ai": ">=0.84.3",
46
+ "@earendil-works/pi-coding-agent": ">=0.84.3",
47
+ "@earendil-works/pi-tui": ">=0.84.3",
48
48
  "typebox": ">=1.1.38 <2"
49
49
  },
50
50
  "devDependencies": {
51
- "@earendil-works/pi-ai": "0.84.2",
52
- "@earendil-works/pi-coding-agent": "0.84.2",
53
- "@earendil-works/pi-tui": "0.84.2",
51
+ "@earendil-works/pi-ai": "0.84.3",
52
+ "@earendil-works/pi-coding-agent": "0.84.3",
53
+ "@earendil-works/pi-tui": "0.84.3",
54
54
  "@types/node": "24.12.4",
55
55
  "typebox": "1.1.38",
56
56
  "typescript": "5.9.3"