mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
配置统一:权限策略、硬防御、Tool Search、提示词封面以及新迁入的 RTK、pi-compaction、pi-worktree、pi-hermes-memory 配置全部由扩展 直接读取已安装组合包内的 config/,pi install / pi update 即完成部署; 删除 rebuild-bundle-config.mjs 与 bundle-manifest.json,my-pi/ 只保留 日志与缓存。update.sh 成功升级后将默认 git 来源的已安装副本 reset 回发布版本。AGENTS.md 新增「组合包配置原则」。 脚本整合:search_config.sh / ssh_config.sh 合并为 configure.sh search|ssh;install.sh / update.sh 共享函数抽到 scripts/lib.sh,远程 安装器改为先下载到临时文件再执行,下载失败可被检测。 权限:hard-guards 新增 protectedWrites,写入 Pi 凭据/设置、已安装 组合包与扩展目录、pi-ssh 凭据库时一律人工确认(跳过 AutoReview、 yolo 与 Session grant,无 UI 时拒绝);放行 ls、LSP 查询、ctx 只读、 web_search、Chrome 只读与 git 只读命令,skill_manage 改为 ask。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
161 lines
5.4 KiB
TypeScript
161 lines
5.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
readCoverFile,
|
|
resolveCoverPath,
|
|
spliceCover,
|
|
} from "../extensions/system-prompt-cover-config.ts";
|
|
|
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const bundledCoverPath = join(repositoryRoot, "config", "system-prompt-cover.md");
|
|
|
|
const DEFAULT_PREAMBLE =
|
|
"You are an expert coding assistant operating inside pi, a coding agent harness. " +
|
|
"You help users by reading files, executing commands, editing code, and writing new files.";
|
|
|
|
/** A rendered prompt shaped like Pi's: preamble, then tag-wrapped top-level sections. */
|
|
function renderedPrompt(preamble = DEFAULT_PREAMBLE) {
|
|
return [
|
|
preamble,
|
|
"<tools>",
|
|
"- read: reads files",
|
|
"</tools>",
|
|
"<rules>",
|
|
"- Be concise",
|
|
"</rules>",
|
|
"<skills>",
|
|
"<available_skills>",
|
|
" <skill>",
|
|
" <name>s</name>",
|
|
" </skill>",
|
|
"</available_skills>",
|
|
"</skills>",
|
|
"<project_context>",
|
|
'<project_instructions path="/work/AGENTS.md">',
|
|
"Nested content with a </project_context>-looking line is not a tag.",
|
|
"</project_instructions>",
|
|
"</project_context>",
|
|
"<cwd>",
|
|
"/work",
|
|
"</cwd>",
|
|
].join("\n");
|
|
}
|
|
|
|
function tagsOf(text: string): string[] {
|
|
return [...text.matchAll(/^<([a-z][a-z0-9_-]*)>$/gm)].map((match) => match[1]);
|
|
}
|
|
|
|
async function makeTempDir() {
|
|
return mkdtemp(join(tmpdir(), "my-pi-cover-"));
|
|
}
|
|
|
|
test("bundled cover holds Pi's default preamble text", async () => {
|
|
const content = await readFile(bundledCoverPath, "utf8");
|
|
assert.equal(content.replace(/\s+$/, ""), DEFAULT_PREAMBLE);
|
|
});
|
|
|
|
test("spliceCover replaces only the preamble and preserves the remaining bytes", () => {
|
|
const rendered = renderedPrompt();
|
|
const spliced = spliceCover(rendered, "MY CUSTOM COVER");
|
|
|
|
assert.ok(spliced.startsWith("MY CUSTOM COVER\n"));
|
|
|
|
// Everything after the preamble's terminating newline is untouched.
|
|
const originalTail = rendered.slice(rendered.indexOf("\n<tools>"));
|
|
assert.ok(spliced.endsWith(originalTail));
|
|
|
|
// Section order — including nested tags — is identical to the unmodified prompt.
|
|
assert.deepEqual(tagsOf(spliced), tagsOf(rendered));
|
|
assert.deepEqual(tagsOf(spliced), [
|
|
"tools",
|
|
"rules",
|
|
"skills",
|
|
"available_skills",
|
|
"project_context",
|
|
"cwd",
|
|
]);
|
|
});
|
|
|
|
test("spliceCover keeps section order rather than appending recovered sections", () => {
|
|
const rendered = renderedPrompt("ORIGINAL COVER");
|
|
const spliced = spliceCover(rendered, "NEW COVER");
|
|
const order = tagsOf(spliced).join(",");
|
|
|
|
assert.equal(order, "tools,rules,skills,available_skills,project_context,cwd");
|
|
assert.ok(
|
|
order.indexOf("tools") < order.indexOf("cwd"),
|
|
"tools must still precede cwd; a re-appended section list would invert this",
|
|
);
|
|
assert.ok(
|
|
order.indexOf("project_context") < order.indexOf("cwd"),
|
|
"project_context must keep its original position",
|
|
);
|
|
});
|
|
|
|
test("spliceCover tolerates a trailing newline from the editor and a blank cover", () => {
|
|
const rendered = renderedPrompt();
|
|
|
|
assert.equal(spliceCover(rendered, "COVER\n\n"), spliceCover(rendered, "COVER"));
|
|
// A whitespace-only cover leaves an empty preamble but still keeps every section.
|
|
const blank = spliceCover(rendered, " \n");
|
|
assert.equal(blank, rendered.slice(rendered.indexOf("\n<tools>")));
|
|
assert.deepEqual(tagsOf(blank), tagsOf(rendered));
|
|
});
|
|
|
|
test("spliceCover degenerates to the cover when the prompt has no sections", () => {
|
|
assert.equal(spliceCover("just a preamble", "NEW"), "NEW");
|
|
assert.equal(spliceCover("", "NEW"), "NEW");
|
|
});
|
|
|
|
test("spliceCover keeps an attribute-bearing nested tag inside its parent section", () => {
|
|
// Pi nests <project_instructions path="..."> inside the bare <project_context> tag, so the
|
|
// boundary must be the first *bare* tag line and the nested markup must survive verbatim.
|
|
const rendered = [
|
|
"ORIGINAL COVER",
|
|
"<tools>",
|
|
"- read: reads files",
|
|
"</tools>",
|
|
"<project_context>",
|
|
'<project_instructions path="/work/AGENTS.md">',
|
|
"nested instructions",
|
|
"</project_instructions>",
|
|
"</project_context>",
|
|
"<cwd>",
|
|
"/work",
|
|
"</cwd>",
|
|
].join("\n");
|
|
|
|
const spliced = spliceCover(rendered, "NEW");
|
|
assert.equal(tagsOf(spliced).join(","), "tools,project_context,cwd");
|
|
assert.ok(spliced.startsWith("NEW\n<tools>"));
|
|
assert.ok(
|
|
spliced.includes('<project_instructions path="/work/AGENTS.md">\nnested instructions\n</project_instructions>'),
|
|
"nested project instructions must be preserved byte-for-byte",
|
|
);
|
|
});
|
|
|
|
test("resolveCoverPath points at the bundle config file", () => {
|
|
assert.equal(resolveCoverPath(), bundledCoverPath);
|
|
});
|
|
|
|
test("readCoverFile returns undefined when the file is absent or blank", async () => {
|
|
const dir = await makeTempDir();
|
|
try {
|
|
const coverPath = join(dir, "system-prompt-cover.md");
|
|
assert.equal(readCoverFile(coverPath), undefined);
|
|
|
|
await writeFile(coverPath, " \n\n", "utf8");
|
|
assert.equal(readCoverFile(coverPath), undefined);
|
|
|
|
await writeFile(coverPath, " REAL COVER \n", "utf8");
|
|
assert.equal(readCoverFile(coverPath), " REAL COVER");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|