killeros 2.0.20 → 2.0.21
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/README.md +2 -2
- package/killeros/display.ts +8 -3
- package/killeros/footer.ts +118 -25
- package/killeros/goal-state.ts +407 -0
- package/killeros/goals.ts +21 -375
- package/killeros/handoff.ts +39 -16
- package/killeros/init-evidence.ts +15 -3
- package/killeros/personal-instructions.ts +57 -5
- package/killeros/question-ui.ts +590 -0
- package/killeros/question.ts +12 -558
- package/killeros/secret-detector.ts +19 -0
- package/killeros/shell-ui.ts +1 -1
- package/package.json +5 -5
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
|
+
import { containsLikelySecret } from "./secret-detector.ts";
|
|
5
6
|
|
|
6
7
|
export const INIT_READ_TOOL = "killeros_init_read";
|
|
7
8
|
export const INIT_LIST_TOOL = "killeros_init_list";
|
|
@@ -19,7 +20,7 @@ const EXCLUDED_GUIDANCE = new Set([
|
|
|
19
20
|
".cursorrules", "agents.md", "agents.local.md", "claude.md", "claude.local.md", "copilot-instructions.md", "gemini.md", "memory.md", "skill.md",
|
|
20
21
|
]);
|
|
21
22
|
const ROOT_EVIDENCE = [
|
|
22
|
-
"README.md", "README.rst", "README.txt", "CONTRIBUTING.md", "package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod", "Makefile", "Dockerfile", "compose.yaml", "compose.yml", "
|
|
23
|
+
"README.md", "README.rst", "README.txt", "CONTRIBUTING.md", "package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod", "Makefile", "Dockerfile", "compose.yaml", "compose.yml", "tsconfig.json", "vite.config.ts", "vite.config.js", "eslint.config.js", "eslint.config.mjs",
|
|
23
24
|
] as const;
|
|
24
25
|
const NESTED_EVIDENCE = new Set(["package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod"]);
|
|
25
26
|
|
|
@@ -43,6 +44,10 @@ function sensitiveEvidencePath(relativePath: string): boolean {
|
|
|
43
44
|
const name = path.posix.basename(normalized);
|
|
44
45
|
return /^\.env(?:\.|$)/u.test(name)
|
|
45
46
|
|| [".npmrc", ".pypirc", ".netrc", "id_rsa", "id_ed25519", "credentials.json"].includes(name)
|
|
47
|
+
|| /(?:^|\/)\.aws\/credentials$/u.test(normalized)
|
|
48
|
+
|| /(?:^|\/)\.docker\/config\.json$/u.test(normalized)
|
|
49
|
+
|| /(?:^|\/)\.kube\/config$/u.test(normalized)
|
|
50
|
+
|| /(?:credential|secret|token)/u.test(name)
|
|
46
51
|
|| /^service-account.*\.json$/u.test(name)
|
|
47
52
|
|| /\.(?:pem|key|p12|pfx|jks|keystore)$/u.test(name);
|
|
48
53
|
}
|
|
@@ -200,6 +205,13 @@ async function validateAndRead(projectRoot: string, absolutePath: string, limit:
|
|
|
200
205
|
}
|
|
201
206
|
}
|
|
202
207
|
|
|
208
|
+
/** Reads evidence only when its bounded content does not resemble a credential. */
|
|
209
|
+
async function readEvidenceFile(projectRoot: string, absolutePath: string, limit: number): Promise<{ content: string; truncated: boolean }> {
|
|
210
|
+
const result = await validateAndRead(projectRoot, absolutePath, limit);
|
|
211
|
+
if (containsLikelySecret(result.content)) throw new Error("file is not available to /init");
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
203
215
|
function normalizeRequestedPath(requestedPath: string): string {
|
|
204
216
|
if (!requestedPath || requestedPath.trim() !== requestedPath || requestedPath.startsWith("~")
|
|
205
217
|
|| /^file:/iu.test(requestedPath) || path.isAbsolute(requestedPath)) {
|
|
@@ -237,7 +249,7 @@ export async function buildInitEvidence(projectRoot: string): Promise<InitEviden
|
|
|
237
249
|
const relativePath = canonicalPaths.get(evidenceKey(requested));
|
|
238
250
|
if (!relativePath || Buffer.byteLength(snapshot, "utf8") >= SNAPSHOT_LIMIT) continue;
|
|
239
251
|
try {
|
|
240
|
-
const result = await
|
|
252
|
+
const result = await readEvidenceFile(projectRoot, path.join(projectRoot, relativePath), AUTOMATIC_FILE_LIMIT);
|
|
241
253
|
const suffix = result.truncated ? "\n[truncated by /init]" : "";
|
|
242
254
|
snapshot = appendWithinLimit(snapshot, `\n\n## ${relativePath}\n${result.content}${suffix}`, SNAPSHOT_LIMIT);
|
|
243
255
|
} catch {
|
|
@@ -251,7 +263,7 @@ export async function readInitEvidence(index: InitEvidenceIndex, requestedPath:
|
|
|
251
263
|
const normalized = normalizeRequestedPath(requestedPath);
|
|
252
264
|
const relativePath = index.canonicalPaths.get(evidenceKey(normalized));
|
|
253
265
|
if (!relativePath) throw new Error(`${requestedPath} is not available to /init`);
|
|
254
|
-
const result = await
|
|
266
|
+
const result = await readEvidenceFile(index.projectRoot, path.join(index.projectRoot, relativePath), READ_LIMIT);
|
|
255
267
|
return result.truncated ? `${result.content}\n[truncated by /init at ${READ_LIMIT} bytes]` : result.content;
|
|
256
268
|
}
|
|
257
269
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { closeSync, openSync, readSync } from "node:fs";
|
|
1
|
+
import { closeSync, fstatSync, lstatSync, openSync, readSync, realpathSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
|
-
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { InitRuntime } from "./runtime.ts";
|
|
7
7
|
|
|
8
8
|
const PERSONAL_INSTRUCTIONS_FILE = "AGENTS.local.md";
|
|
@@ -33,9 +33,59 @@ function readBoundedText(filePath: string, limit = PERSONAL_INSTRUCTIONS_LIMIT):
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Checks path containment using host filesystem casing rules. */
|
|
37
|
+
function pathInside(root: string, candidate: string): boolean {
|
|
38
|
+
const normalizedRoot = process.platform === "win32" ? root.toLowerCase() : root;
|
|
39
|
+
const normalizedCandidate = process.platform === "win32" ? candidate.toLowerCase() : candidate;
|
|
40
|
+
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
41
|
+
return Boolean(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Reads one stable, unlinked import from inside Pi's agent directory. */
|
|
45
|
+
function readImportedText(filePath: string): string | undefined {
|
|
46
|
+
let descriptor: number | undefined;
|
|
47
|
+
try {
|
|
48
|
+
const allowedPath = path.resolve(getAgentDir());
|
|
49
|
+
const requestedPath = path.resolve(filePath);
|
|
50
|
+
if (!pathInside(allowedPath, requestedPath)) return undefined;
|
|
51
|
+
|
|
52
|
+
const allowedRealPath = realpathSync(allowedPath);
|
|
53
|
+
const requestedRealPath = realpathSync(requestedPath);
|
|
54
|
+
if (!pathInside(allowedRealPath, requestedRealPath)) return undefined;
|
|
55
|
+
|
|
56
|
+
let current = allowedPath;
|
|
57
|
+
for (const segment of path.relative(allowedPath, requestedPath).split(path.sep)) {
|
|
58
|
+
current = path.join(current, segment);
|
|
59
|
+
if (lstatSync(current).isSymbolicLink()) return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pathStat = lstatSync(requestedPath);
|
|
63
|
+
if (!pathStat.isFile() || pathStat.isSymbolicLink() || pathStat.nlink !== 1) return undefined;
|
|
64
|
+
descriptor = openSync(requestedPath, "r");
|
|
65
|
+
const openedStat = fstatSync(descriptor);
|
|
66
|
+
if (!openedStat.isFile() || openedStat.nlink !== 1
|
|
67
|
+
|| openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) return undefined;
|
|
68
|
+
|
|
69
|
+
const buffer = Buffer.alloc(PERSONAL_INSTRUCTIONS_LIMIT + 1);
|
|
70
|
+
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
71
|
+
if (bytesRead > PERSONAL_INSTRUCTIONS_LIMIT) return undefined;
|
|
72
|
+
const content = new StringDecoder("utf8").write(buffer.subarray(0, bytesRead));
|
|
73
|
+
return content.trim() ? content : undefined;
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
} finally {
|
|
77
|
+
if (descriptor !== undefined) {
|
|
78
|
+
try {
|
|
79
|
+
closeSync(descriptor);
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore cleanup failures after rejecting or reading an import.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
36
87
|
export function resolvePersonalInstructions(cwd: string): string | undefined {
|
|
37
|
-
const
|
|
38
|
-
const local = readBoundedText(localPath);
|
|
88
|
+
const local = readBoundedText(path.join(cwd, PERSONAL_INSTRUCTIONS_FILE));
|
|
39
89
|
if (!local) return undefined;
|
|
40
90
|
|
|
41
91
|
const importMatch = local.trim().match(/^@(.+)$/u);
|
|
@@ -45,7 +95,9 @@ export function resolvePersonalInstructions(cwd: string): string | undefined {
|
|
|
45
95
|
const importedPath = requestedPath.startsWith("~/") || requestedPath.startsWith("~\\")
|
|
46
96
|
? path.join(os.homedir(), requestedPath.slice(2))
|
|
47
97
|
: path.resolve(cwd, requestedPath);
|
|
48
|
-
|
|
98
|
+
const imported = readImportedText(importedPath);
|
|
99
|
+
if (!imported) return undefined;
|
|
100
|
+
content = imported;
|
|
49
101
|
}
|
|
50
102
|
return `<personal_instructions>\n${content}\n</personal_instructions>`;
|
|
51
103
|
}
|