killeros 2.0.1 → 2.0.3
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 +24 -0
- package/Killeros.ts +13 -8
- package/README.md +14 -8
- package/killeros/footer.ts +46 -1
- package/killeros/goals.ts +118 -55
- package/killeros/notifications.ts +167 -0
- package/killeros/question.ts +16 -1
- package/killeros/runtime.ts +1 -25
- package/killeros/shell-ui.ts +15 -2
- package/package.json +2 -2
- package/killeros/context-compaction.ts +0 -614
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import type { StopReason } from "@earendil-works/pi-ai";
|
|
5
|
+
import {
|
|
6
|
+
getAgentDir,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
export interface NotificationPreferenceStore {
|
|
12
|
+
load(): boolean;
|
|
13
|
+
save(enabled: boolean): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CompletionNotificationDependencies {
|
|
17
|
+
store: NotificationPreferenceStore;
|
|
18
|
+
ring(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const COMPLETION_BELL_GLYPH = "";
|
|
22
|
+
|
|
23
|
+
const defaultSettingsPath = (): string => join(getAgentDir(), "killeros.json");
|
|
24
|
+
|
|
25
|
+
type StoredSettings = Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
function readStoredSettings(settingsPath: string): StoredSettings {
|
|
28
|
+
try {
|
|
29
|
+
const parsed: unknown = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
30
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
31
|
+
throw new Error("KillerOS settings must contain a JSON object");
|
|
32
|
+
}
|
|
33
|
+
return parsed as StoredSettings;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createNotificationPreferenceStore(
|
|
41
|
+
settingsPath = defaultSettingsPath(),
|
|
42
|
+
): NotificationPreferenceStore {
|
|
43
|
+
return {
|
|
44
|
+
load: () => readStoredSettings(settingsPath).completionSound === true,
|
|
45
|
+
save: (enabled) => {
|
|
46
|
+
const current = readStoredSettings(settingsPath);
|
|
47
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
48
|
+
const temporaryPath = `${settingsPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(
|
|
51
|
+
temporaryPath,
|
|
52
|
+
`${JSON.stringify({ ...current, completionSound: enabled }, null, 2)}\n`,
|
|
53
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
54
|
+
);
|
|
55
|
+
renameSync(temporaryPath, settingsPath);
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(temporaryPath, { force: true });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatNotificationTitle(
|
|
64
|
+
cwd: string,
|
|
65
|
+
sessionName: string | undefined,
|
|
66
|
+
enabled: boolean,
|
|
67
|
+
): string {
|
|
68
|
+
const directory = basename(cwd);
|
|
69
|
+
const base = sessionName ? `π - ${sessionName} - ${directory}` : `π - ${directory}`;
|
|
70
|
+
return enabled ? `${base} ${COMPLETION_BELL_GLYPH}` : base;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function errorMessage(error: unknown): string {
|
|
74
|
+
return error instanceof Error ? error.message : String(error);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerCompletionNotifications(
|
|
78
|
+
pi: ExtensionAPI,
|
|
79
|
+
dependencies?: CompletionNotificationDependencies,
|
|
80
|
+
): void {
|
|
81
|
+
const runtime = dependencies ?? {
|
|
82
|
+
store: createNotificationPreferenceStore(),
|
|
83
|
+
ring: () => { process.stdout.write("\x07"); },
|
|
84
|
+
};
|
|
85
|
+
let enabled = false;
|
|
86
|
+
let requestPending = false;
|
|
87
|
+
let lastStopReason: StopReason | undefined;
|
|
88
|
+
|
|
89
|
+
const applyTitle = (ctx: ExtensionContext): void => {
|
|
90
|
+
if (ctx.mode !== "tui") return;
|
|
91
|
+
ctx.ui.setTitle(formatNotificationTitle(ctx.cwd, pi.getSessionName(), enabled));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
pi.registerCommand("notification", {
|
|
95
|
+
description: "Configure the completion sound",
|
|
96
|
+
handler: async (_args, ctx) => {
|
|
97
|
+
if (ctx.mode !== "tui") {
|
|
98
|
+
ctx.ui.notify("/notification requires TUI mode", "error");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const selected = await ctx.ui.select("Completion sound", [
|
|
102
|
+
enabled ? "On ← current" : "On",
|
|
103
|
+
enabled ? "Off" : "Off ← current",
|
|
104
|
+
]);
|
|
105
|
+
if (!selected) return;
|
|
106
|
+
const nextEnabled = selected.startsWith("On");
|
|
107
|
+
try {
|
|
108
|
+
runtime.store.save(nextEnabled);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
ctx.ui.notify(`Completion sound setting could not be saved: ${errorMessage(error)}`, "error");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
enabled = nextEnabled;
|
|
114
|
+
applyTitle(ctx);
|
|
115
|
+
ctx.ui.notify(`Completion sound: ${enabled ? "On" : "Off"}`, "info");
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.on("session_start", (_event, ctx) => {
|
|
120
|
+
requestPending = false;
|
|
121
|
+
lastStopReason = undefined;
|
|
122
|
+
if (ctx.mode !== "tui") return;
|
|
123
|
+
try {
|
|
124
|
+
enabled = runtime.store.load();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
enabled = false;
|
|
127
|
+
ctx.ui.notify(`Completion sound settings could not be read: ${errorMessage(error)}`, "error");
|
|
128
|
+
}
|
|
129
|
+
applyTitle(ctx);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
pi.on("session_info_changed", (_event, ctx) => applyTitle(ctx));
|
|
133
|
+
|
|
134
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
135
|
+
if (ctx.mode !== "tui") return;
|
|
136
|
+
requestPending = true;
|
|
137
|
+
lastStopReason = undefined;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
pi.on("agent_end", (event, ctx) => {
|
|
141
|
+
if (ctx.mode !== "tui" || !requestPending) return;
|
|
142
|
+
lastStopReason = undefined;
|
|
143
|
+
for (let index = event.messages.length - 1; index >= 0; index -= 1) {
|
|
144
|
+
const message = event.messages[index];
|
|
145
|
+
if (message?.role !== "assistant") continue;
|
|
146
|
+
lastStopReason = message.stopReason;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
152
|
+
if (ctx.mode !== "tui" || !requestPending) return;
|
|
153
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
154
|
+
requestPending = false;
|
|
155
|
+
if (!enabled || lastStopReason === "aborted") return;
|
|
156
|
+
try {
|
|
157
|
+
runtime.ring();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
ctx.ui.notify(`Completion sound failed: ${errorMessage(error)}`, "error");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
pi.on("session_shutdown", () => {
|
|
164
|
+
requestPending = false;
|
|
165
|
+
lastStopReason = undefined;
|
|
166
|
+
});
|
|
167
|
+
}
|
package/killeros/question.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ExtensionAPI, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
decodeKittyPrintable,
|
|
4
4
|
Editor,
|
|
@@ -174,6 +174,21 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
174
174
|
};
|
|
175
175
|
finishFromAbort = () => finish({ kind: "aborted" });
|
|
176
176
|
|
|
177
|
+
const keyHint = (
|
|
178
|
+
keybinding: Parameters<typeof keybindings.getKeys>[0],
|
|
179
|
+
description: string,
|
|
180
|
+
): string => {
|
|
181
|
+
const keyText = keybindings.getKeys(keybinding)
|
|
182
|
+
.join("/")
|
|
183
|
+
.split("/")
|
|
184
|
+
.map((key) => key
|
|
185
|
+
.split("+")
|
|
186
|
+
.map((part) => process.platform === "darwin" && part.toLocaleLowerCase() === "alt" ? "option" : part)
|
|
187
|
+
.join("+"))
|
|
188
|
+
.join("/");
|
|
189
|
+
return theme.fg("dim", keyText) + theme.fg("muted", ` ${description}`);
|
|
190
|
+
};
|
|
191
|
+
|
|
177
192
|
const editorTheme: EditorTheme = {
|
|
178
193
|
borderColor: (text) => theme.fg("accent", text),
|
|
179
194
|
selectList: {
|
package/killeros/runtime.ts
CHANGED
|
@@ -23,13 +23,13 @@ export interface GoalState {
|
|
|
23
23
|
blockedAuditStartTurn: number;
|
|
24
24
|
baselineTokens: number;
|
|
25
25
|
result?: string;
|
|
26
|
+
resumeAfterManualCompaction?: true;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface GoalRuntime {
|
|
29
30
|
state?: GoalState;
|
|
30
31
|
continuationScheduled: boolean;
|
|
31
32
|
continuationHeld: boolean;
|
|
32
|
-
continuationHeldForCompaction: boolean;
|
|
33
33
|
goalTurnInFlight: boolean;
|
|
34
34
|
agentEndObserved: boolean;
|
|
35
35
|
persistenceRetryNeeded: boolean;
|
|
@@ -46,7 +46,6 @@ export function createGoalRuntime(): GoalRuntime {
|
|
|
46
46
|
return {
|
|
47
47
|
continuationScheduled: false,
|
|
48
48
|
continuationHeld: false,
|
|
49
|
-
continuationHeldForCompaction: false,
|
|
50
49
|
goalTurnInFlight: false,
|
|
51
50
|
agentEndObserved: false,
|
|
52
51
|
persistenceRetryNeeded: false,
|
|
@@ -61,26 +60,3 @@ export function resetInitRuntime(state: InitRuntime): void {
|
|
|
61
60
|
state.projectRoot = undefined;
|
|
62
61
|
state.activeTools = undefined;
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
export interface CompactionRuntime {
|
|
66
|
-
compactionInFlight: boolean;
|
|
67
|
-
automaticCompactionArmed: boolean;
|
|
68
|
-
automaticCompactionAwaitingHook: boolean;
|
|
69
|
-
automaticCompactionPending: boolean;
|
|
70
|
-
compactionOperationId: number;
|
|
71
|
-
sessionGeneration: number;
|
|
72
|
-
lastCompactionAt?: number;
|
|
73
|
-
thresholdPercent: number;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function createCompactionRuntime(): CompactionRuntime {
|
|
77
|
-
return {
|
|
78
|
-
compactionInFlight: false,
|
|
79
|
-
automaticCompactionArmed: true,
|
|
80
|
-
automaticCompactionAwaitingHook: false,
|
|
81
|
-
automaticCompactionPending: false,
|
|
82
|
-
compactionOperationId: 0,
|
|
83
|
-
sessionGeneration: 0,
|
|
84
|
-
thresholdPercent: 40,
|
|
85
|
-
};
|
|
86
|
-
}
|
package/killeros/shell-ui.ts
CHANGED
|
@@ -42,6 +42,7 @@ const STARTUP_TIPS = [
|
|
|
42
42
|
"Press Shift+Enter to insert a line break without sending.",
|
|
43
43
|
"Run /variants to tune the model's reasoning depth.",
|
|
44
44
|
"Type / to browse every command available in this session.",
|
|
45
|
+
"Run /notification to enable a terminal bell when work settles.",
|
|
45
46
|
] as const;
|
|
46
47
|
|
|
47
48
|
export function resolveGitBranch(cwd: string): Promise<string | undefined> {
|
|
@@ -369,8 +370,17 @@ class PiCodeEditor extends CustomEditor {
|
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
372
|
|
|
373
|
+
const ACTIVITY_FRAMES = [
|
|
374
|
+
"·", "✢", "✱", "✶", "✻", "✽",
|
|
375
|
+
"✽", "✻", "✶", "✱", "✢", "·",
|
|
376
|
+
] as const;
|
|
377
|
+
const ACTIVITY_FRAME_INTERVAL_MS = 120;
|
|
372
378
|
const ACTIVITY_WORDS = ["Brewing", "Pondering", "Tinkering", "Wrangling", "Noodling", "Cooking"] as const;
|
|
373
379
|
|
|
380
|
+
function formatActivityMessage(word: string, theme: Theme): string {
|
|
381
|
+
return `${theme.fg("accent", `${word}…`)} ${theme.fg("dim", `(${theme.bold("esc")} to interrupt · thinking)`)}`;
|
|
382
|
+
}
|
|
383
|
+
|
|
374
384
|
export function registerShellUi(pi: ExtensionAPI): void {
|
|
375
385
|
let activeHeader: PiStartupHeader | undefined;
|
|
376
386
|
let activityDeck: string[] = [];
|
|
@@ -413,7 +423,10 @@ export function registerShellUi(pi: ExtensionAPI): void {
|
|
|
413
423
|
return activeHeader;
|
|
414
424
|
});
|
|
415
425
|
clearActivityTimer();
|
|
416
|
-
ctx.ui.setWorkingIndicator({
|
|
426
|
+
ctx.ui.setWorkingIndicator({
|
|
427
|
+
frames: ACTIVITY_FRAMES.map((frame) => ctx.ui.theme.fg("accent", frame)),
|
|
428
|
+
intervalMs: ACTIVITY_FRAME_INTERVAL_MS,
|
|
429
|
+
});
|
|
417
430
|
ctx.ui.setHiddenThinkingLabel("└ Thinking…");
|
|
418
431
|
ctx.ui.setEditorComponent((tui, editorTheme, keybindings) =>
|
|
419
432
|
new PiCodeEditor(tui, editorTheme, keybindings, ctx.ui.theme, () => availableCommandNames(pi)));
|
|
@@ -425,7 +438,7 @@ export function registerShellUi(pi: ExtensionAPI): void {
|
|
|
425
438
|
pi.on("agent_start", (_event, ctx) => {
|
|
426
439
|
if (ctx.mode !== "tui") return;
|
|
427
440
|
clearActivityTimer();
|
|
428
|
-
const updateWorkingWord = (): void => ctx.ui.setWorkingMessage(
|
|
441
|
+
const updateWorkingWord = (): void => ctx.ui.setWorkingMessage(formatActivityMessage(nextActivityWord(), ctx.ui.theme));
|
|
429
442
|
updateWorkingWord();
|
|
430
443
|
activityTimer = setInterval(updateWorkingWord, 2_500);
|
|
431
444
|
activityTimer.unref?.();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.3",
|
|
4
|
+
"description": "TUI, goals, and workflow automation for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|