killeros 2.0.2 → 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 +17 -0
- package/Killeros.ts +13 -8
- package/README.md +13 -7
- 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 +1 -0
- 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> {
|
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",
|