killeros 2.0.21 → 2.0.22
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 +12 -0
- package/README.md +1 -1
- package/killeros/footer.ts +52 -18
- package/killeros/hooks.ts +40 -11
- package/killeros/personal-instructions.ts +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.22] - 2026-08-30
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Replaced the footer's changed-file total with a colored modified, added, and deleted breakdown.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Preserved the existing `createGitStatusRefresh` changed-file count callback contract.
|
|
16
|
+
- Rejected linked `AGENTS.local.md` files before reading personal instructions.
|
|
17
|
+
- Confirmed Windows hook process-tree cleanup before settling cancellations and timeouts, even when `taskkill` is absent from `PATH`.
|
|
18
|
+
|
|
7
19
|
## [2.0.21] - 2026-08-29
|
|
8
20
|
|
|
9
21
|
### Changed
|
package/README.md
CHANGED
|
@@ -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.
|
|
37
|
+
Pin a release by appending its tag, for example `@v2.0.22`. Add `-l` to install only for the current project. Restart Pi after installing.
|
|
38
38
|
|
|
39
39
|
## Commands
|
|
40
40
|
|
package/killeros/footer.ts
CHANGED
|
@@ -12,7 +12,13 @@ const GIT_STATUS_REFRESH_INTERVAL_MS = 30_000;
|
|
|
12
12
|
const CODEX_PROVIDER = "openai-codex";
|
|
13
13
|
const colorDirectory = (text: string): string => `\x1B[38;2;240;248;154m${text}\x1B[39m`;
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
interface GitFileChanges {
|
|
16
|
+
modified: number;
|
|
17
|
+
added: number;
|
|
18
|
+
deleted: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveGitFileChanges(cwd: string): Promise<GitFileChanges | undefined> {
|
|
16
22
|
return new Promise((resolve) => {
|
|
17
23
|
execFile(
|
|
18
24
|
"git",
|
|
@@ -29,25 +35,32 @@ function resolveUncommittedFileCount(cwd: string): Promise<number | undefined> {
|
|
|
29
35
|
return;
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
const changes: GitFileChanges = { modified: 0, added: 0, deleted: 0 };
|
|
32
39
|
const entries = stdout.split("\0");
|
|
33
|
-
let count = 0;
|
|
34
40
|
for (let index = 0; index < entries.length; index += 1) {
|
|
35
41
|
const entry = entries[index];
|
|
36
42
|
if (!entry) continue;
|
|
37
|
-
|
|
38
|
-
if (
|
|
43
|
+
const status = entry.slice(0, 2);
|
|
44
|
+
if (status.includes("D")) changes.deleted += 1;
|
|
45
|
+
else if (status === "??" || status.includes("A")) changes.added += 1;
|
|
46
|
+
else changes.modified += 1;
|
|
47
|
+
if (status.includes("R") || status.includes("C")) index += 1;
|
|
39
48
|
}
|
|
40
|
-
resolve(
|
|
49
|
+
resolve(changes);
|
|
41
50
|
},
|
|
42
51
|
);
|
|
43
52
|
});
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
async function resolveUncommittedFileCount(cwd: string): Promise<number | undefined> {
|
|
56
|
+
const changes = await resolveGitFileChanges(cwd);
|
|
57
|
+
return changes && changes.modified + changes.added + changes.deleted;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createGitRefresh<T>(
|
|
48
61
|
cwd: string,
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
onResult: (result: T | undefined) => void,
|
|
63
|
+
resolveResult: (cwd: string) => Promise<T | undefined>,
|
|
51
64
|
): { request: () => void; dispose: () => void } {
|
|
52
65
|
let disposed = false;
|
|
53
66
|
let pending = false;
|
|
@@ -59,8 +72,8 @@ export function createGitStatusRefresh(
|
|
|
59
72
|
return;
|
|
60
73
|
}
|
|
61
74
|
pending = true;
|
|
62
|
-
void
|
|
63
|
-
if (!disposed)
|
|
75
|
+
void resolveResult(cwd).then((result) => {
|
|
76
|
+
if (!disposed) onResult(result);
|
|
64
77
|
}).finally(() => {
|
|
65
78
|
pending = false;
|
|
66
79
|
if (!disposed && queued) {
|
|
@@ -78,6 +91,22 @@ export function createGitStatusRefresh(
|
|
|
78
91
|
};
|
|
79
92
|
}
|
|
80
93
|
|
|
94
|
+
/** Coalesces Git status requests to one active scan and one queued follow-up. */
|
|
95
|
+
export function createGitStatusRefresh(
|
|
96
|
+
cwd: string,
|
|
97
|
+
onCount: (count: number | undefined) => void,
|
|
98
|
+
resolveCount: (cwd: string) => Promise<number | undefined> = resolveUncommittedFileCount,
|
|
99
|
+
): { request: () => void; dispose: () => void } {
|
|
100
|
+
return createGitRefresh(cwd, onCount, resolveCount);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createGitFileChangesRefresh(
|
|
104
|
+
cwd: string,
|
|
105
|
+
onChanges: (changes: GitFileChanges | undefined) => void,
|
|
106
|
+
): { request: () => void; dispose: () => void } {
|
|
107
|
+
return createGitRefresh(cwd, onChanges, resolveGitFileChanges);
|
|
108
|
+
}
|
|
109
|
+
|
|
81
110
|
type ScheduleFallback = (refresh: () => void, intervalMs: number) => () => void;
|
|
82
111
|
|
|
83
112
|
const scheduleFallback: ScheduleFallback = (refresh, intervalMs) => {
|
|
@@ -236,6 +265,12 @@ function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
|
236
265
|
return "";
|
|
237
266
|
}
|
|
238
267
|
|
|
268
|
+
function formatGitFileChanges(changes: GitFileChanges, theme: Theme): string {
|
|
269
|
+
const total = changes.modified + changes.added + changes.deleted;
|
|
270
|
+
if (total === 0) return "";
|
|
271
|
+
return `${theme.fg("dim", `±${total} [`)}${theme.fg("warning", `~${changes.modified}`)} ${theme.fg("success", `+${changes.added}`)} ${theme.fg("error", `−${changes.deleted}`)}${theme.fg("dim", "]")}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
239
274
|
export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void {
|
|
240
275
|
let currentModel: ExtensionContext["model"];
|
|
241
276
|
let thinkingLevel: ThinkingLevel = "off";
|
|
@@ -273,10 +308,10 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
273
308
|
|
|
274
309
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
275
310
|
activeTui = tui;
|
|
276
|
-
let
|
|
277
|
-
const gitStatus =
|
|
278
|
-
if (
|
|
279
|
-
|
|
311
|
+
let gitFileChanges: GitFileChanges | undefined;
|
|
312
|
+
const gitStatus = createGitFileChangesRefresh(ctx.cwd, (changes) => {
|
|
313
|
+
if (JSON.stringify(changes) === JSON.stringify(gitFileChanges)) return;
|
|
314
|
+
gitFileChanges = changes;
|
|
280
315
|
tui.requestRender();
|
|
281
316
|
});
|
|
282
317
|
requestGitStatusRefresh = gitStatus.request;
|
|
@@ -332,10 +367,9 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
332
367
|
: footerRowFits(primaryFocused, "", width)
|
|
333
368
|
? renderFooterRow(primaryFocused, "", width)
|
|
334
369
|
: renderFooterRow(essentialModel, context, width);
|
|
370
|
+
const changes = gitFileChanges ? formatGitFileChanges(gitFileChanges, theme) : "";
|
|
335
371
|
const branchLabel = branch
|
|
336
|
-
?
|
|
337
|
-
? `${theme.fg("dim", `${branch} · `)}${theme.fg("warning", `${uncommittedFileCount} changed`)}`
|
|
338
|
-
: theme.fg("dim", branch)
|
|
372
|
+
? `${theme.fg("dim", branch)}${changes ? `${theme.fg("dim", " · ")}${changes}` : ""}`
|
|
339
373
|
: "";
|
|
340
374
|
const workspaceRight = goal || fullDirectory;
|
|
341
375
|
const secondaryRow = footerRowFits(branchLabel, workspaceRight, width)
|
package/killeros/hooks.ts
CHANGED
|
@@ -197,16 +197,27 @@ function appendBounded(output: HookOutputBuffer, chunk: Buffer | string): void {
|
|
|
197
197
|
output.text += output.decoder.write(captured);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/** Terminates a Windows hook tree without depending on the caller's PATH. */
|
|
201
|
+
function terminateWindowsHookTree(child: HookChildProcess): Promise<boolean> {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
try {
|
|
204
|
+
const taskkill = process.env.SystemRoot
|
|
205
|
+
? path.join(process.env.SystemRoot, "System32", "taskkill.exe")
|
|
206
|
+
: "taskkill";
|
|
207
|
+
const killer = spawn(taskkill, ["/pid", String(child.pid), "/T", "/F"], {
|
|
208
|
+
shell: false,
|
|
209
|
+
stdio: "ignore",
|
|
210
|
+
windowsHide: true,
|
|
211
|
+
});
|
|
212
|
+
killer.once("error", () => resolve(false));
|
|
213
|
+
killer.once("close", (code) => resolve(code === 0));
|
|
214
|
+
} catch {
|
|
215
|
+
resolve(false);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
200
220
|
function terminateHookProcess(child: HookChildProcess, force: boolean): void {
|
|
201
|
-
if (process.platform === "win32" && force && child.pid) {
|
|
202
|
-
const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
203
|
-
shell: false,
|
|
204
|
-
stdio: "ignore",
|
|
205
|
-
windowsHide: true,
|
|
206
|
-
});
|
|
207
|
-
killer.unref();
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
221
|
if (process.platform !== "win32" && child.pid) {
|
|
211
222
|
try {
|
|
212
223
|
process.kill(-child.pid, force ? "SIGKILL" : "SIGTERM");
|
|
@@ -282,6 +293,7 @@ export function executeHook(
|
|
|
282
293
|
let timer: NodeJS.Timeout | undefined;
|
|
283
294
|
let forceTimer: NodeJS.Timeout | undefined;
|
|
284
295
|
let settleTimer: NodeJS.Timeout | undefined;
|
|
296
|
+
let windowsCleanupPending = false;
|
|
285
297
|
const finish = (code: number, exitUnconfirmed = false): void => {
|
|
286
298
|
if (completed) return;
|
|
287
299
|
completed = true;
|
|
@@ -304,6 +316,21 @@ export function executeHook(
|
|
|
304
316
|
const beginTermination = (reason: "timeout" | "cancelled"): void => {
|
|
305
317
|
if (completed || termination) return;
|
|
306
318
|
termination = reason;
|
|
319
|
+
if (process.platform === "win32" && child.pid) {
|
|
320
|
+
windowsCleanupPending = true;
|
|
321
|
+
void terminateWindowsHookTree(child).then((confirmed) => {
|
|
322
|
+
if (completed) return;
|
|
323
|
+
if (!confirmed) terminateHookProcess(child, true);
|
|
324
|
+
finish(terminationCode(), !confirmed);
|
|
325
|
+
windowsCleanupPending = false;
|
|
326
|
+
});
|
|
327
|
+
settleTimer = setTimeout(() => {
|
|
328
|
+
terminateHookProcess(child, true);
|
|
329
|
+
finish(terminationCode(), true);
|
|
330
|
+
windowsCleanupPending = false;
|
|
331
|
+
}, 2_000);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
307
334
|
terminateHookProcess(child, false);
|
|
308
335
|
forceTimer = setTimeout(() => {
|
|
309
336
|
if (completed) return;
|
|
@@ -318,9 +345,11 @@ export function executeHook(
|
|
|
318
345
|
child.stderr.on("data", (chunk) => appendBounded(stderr, chunk));
|
|
319
346
|
child.on("error", (error) => {
|
|
320
347
|
appendBounded(stderr, error.message);
|
|
321
|
-
finish(termination ? terminationCode() : 1);
|
|
348
|
+
if (!windowsCleanupPending) finish(termination ? terminationCode() : 1);
|
|
349
|
+
});
|
|
350
|
+
child.once("close", (code) => {
|
|
351
|
+
if (!windowsCleanupPending) finish(termination ? terminationCode() : code ?? 1);
|
|
322
352
|
});
|
|
323
|
-
child.once("close", (code) => finish(termination ? terminationCode() : code ?? 1));
|
|
324
353
|
timer = setTimeout(() => beginTermination("timeout"), Math.max(1, Math.min(timeoutMs, HOOK_TIMEOUT_MAX_MS)));
|
|
325
354
|
});
|
|
326
355
|
}
|
|
@@ -11,7 +11,12 @@ const PERSONAL_INSTRUCTIONS_LIMIT = 32 * 1024;
|
|
|
11
11
|
function readBoundedText(filePath: string, limit = PERSONAL_INSTRUCTIONS_LIMIT): string | undefined {
|
|
12
12
|
let descriptor: number | undefined;
|
|
13
13
|
try {
|
|
14
|
+
const pathStat = lstatSync(filePath);
|
|
15
|
+
if (!pathStat.isFile() || pathStat.isSymbolicLink() || pathStat.nlink !== 1) return undefined;
|
|
14
16
|
descriptor = openSync(filePath, "r");
|
|
17
|
+
const openedStat = fstatSync(descriptor);
|
|
18
|
+
if (!openedStat.isFile() || openedStat.nlink !== 1
|
|
19
|
+
|| openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) return undefined;
|
|
15
20
|
const buffer = Buffer.alloc(limit + 1);
|
|
16
21
|
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
17
22
|
const decoder = new StringDecoder("utf8");
|