mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat(pi-hermes-memory): 导入上游 v0.9.10 源码快照
从官方 Git 仓库导入 chandra447/pi-hermes-memory 在 tag v0.9.10 (df624056a2e8c68bc89ba0a81e1c120856ec9665,2026-10-02)的源码快照。 本次仅导入源码,尚未接入组合包:package.json、加载顺序、依赖与权限策略 均未改动。 - 原样导入 src/、tests/、docs/、scripts/、README、CHANGELOG、PLAN、 AGENTS 与 LICENSE;该快照共 156 个上游文件。 - 剔除嵌套 .git、上游 .github 工作流、.ralph/、.claude/、node_modules、 覆盖率与构建产物。 - 新增中文 UPSTREAM.md,记录来源,以及启用前必须完成的待决事项: 在 pi-compaction 之前注册加载顺序、better-sqlite3 版本与根 allowScripts 对齐、strip-ansi 解析确认。 - 说明选择 tag 而非 main 的原因:导入时上游 main 已领先至 24fcd8ad26fc27badb8d7dbaf8034645de07b893(2026-10-11),选 tag 是为 对应一个已发布、可复现的版本。 尚未接线。启用前还需反转 tests/extension-load.test.ts 中"不得加载 Hermes"的断言,并同步 pi-tool-search 分组、权限策略与文档。 验证:快照 156 个上游文件全部导入,目录内仅多出 UPSTREAM.md 一份; 已确认与上游 main 内容存在差异,证明取的是 tag 而非 main。
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// Run from a development checkout: node --import tsx scripts/benchmark-memory-startup.mjs [--lazy] [entry.ts]
|
||||
// Measures the extension lifecycle, not Pi's TUI. No model calls or user data.
|
||||
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const lazy = process.argv.includes("--lazy");
|
||||
const entry = process.argv.slice(2).find((arg) => arg !== "--lazy")
|
||||
?? join(dirname(fileURLToPath(import.meta.url)), "../src/index.ts");
|
||||
const root = await mkdtemp(join(tmpdir(), "pi-memory-startup-bench-"));
|
||||
const globalDir = join(root, "pi-hermes-memory");
|
||||
const handlers = new Map();
|
||||
const tools = new Map();
|
||||
const ctx = {
|
||||
cwd: join(root, "workspace"), hasUI: false,
|
||||
sessionManager: { getBranch: () => [], getSessionFile: () => undefined },
|
||||
ui: { notify() {} },
|
||||
};
|
||||
const emit = async (event, data = {}) => {
|
||||
for (const handler of handlers.get(event) ?? []) await handler(data, ctx);
|
||||
};
|
||||
|
||||
try {
|
||||
process.env.PI_CODING_AGENT_DIR = root;
|
||||
await mkdir(globalDir);
|
||||
await mkdir(ctx.cwd);
|
||||
await writeFile(join(root, "hermes-memory-config.json"), JSON.stringify({
|
||||
lazyInitialization: lazy, memoryMode: "policy-only",
|
||||
reviewEnabled: false, correctionDetection: false,
|
||||
flushOnShutdown: false, flushOnCompact: false,
|
||||
}));
|
||||
await writeFile(join(globalDir, "MEMORY.md"), "durable global preference");
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const dir = join(root, "projects-memory", `project-${i}`);
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(join(dir, "MEMORY.md"), `durable project ${i} convention`);
|
||||
}
|
||||
|
||||
const started = performance.now();
|
||||
const { default: register } = await import(pathToFileURL(resolve(entry)).href);
|
||||
const imported = performance.now();
|
||||
register({
|
||||
on(event, handler) {
|
||||
if (!handlers.has(event)) handlers.set(event, []);
|
||||
handlers.get(event).push(handler);
|
||||
},
|
||||
registerTool(tool) { tools.set(tool.name, tool); },
|
||||
registerCommand() {},
|
||||
});
|
||||
const registered = performance.now();
|
||||
await emit("session_start");
|
||||
await emit("resources_discover", { cwd: ctx.cwd, reason: "startup" });
|
||||
const ready = performance.now();
|
||||
const databaseAtStartup = existsSync(join(globalDir, "sessions.db"));
|
||||
const search = await tools.get("memory_search").execute("bench", { query: "durable" }, undefined, undefined, ctx);
|
||||
const searched = performance.now();
|
||||
if (!search.details.success || search.details.count === 0) throw new Error("First-use search failed");
|
||||
console.log(JSON.stringify({
|
||||
lazy, entry: resolve(entry),
|
||||
importMs: Math.round(imported - started),
|
||||
registrationMs: Math.round(registered - imported),
|
||||
sessionStartMs: Math.round(ready - registered),
|
||||
totalStartupMs: Math.round(ready - started),
|
||||
firstSearchMs: Math.round(searched - ready),
|
||||
databaseAtStartup,
|
||||
}));
|
||||
} finally {
|
||||
await emit("session_shutdown", { reason: "quit" });
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Type-check the extension against the OLDEST Pi SDK we claim to support.
|
||||
*
|
||||
* Why this exists: `peerDependencies` is the only thing telling a user whether
|
||||
* this extension works on their Pi, and nothing verified it. The declared floor
|
||||
* had drifted to `>=0.74.0` while `src/handlers/review-memory-ops.ts` imports
|
||||
* `@earendil-works/pi-ai/compat`, a subpath that does not exist before 0.80.1 —
|
||||
* so anyone on 0.74-0.79.x got ERR_PACKAGE_PATH_NOT_EXPORTED and no extension
|
||||
* at all, with nothing in CI to catch it.
|
||||
*
|
||||
* The regular `check` job structurally cannot catch this: it installs whatever
|
||||
* the devDependency range resolves to, which is always new enough.
|
||||
*
|
||||
* Run: node scripts/check-min-sdk.mjs
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const SCOPE = "@earendil-works";
|
||||
// pi-tui is a direct dependency rather than a peer, but its types cross the
|
||||
// boundary (ExtensionCommandContext.ui.custom takes a pi-tui TUI). Leaving it
|
||||
// at a different version yields a duplicate-private-property error instead of
|
||||
// a real finding, so it moves with the floor.
|
||||
const FLOOR_PACKAGES = [`${SCOPE}/pi-coding-agent`, `${SCOPE}/pi-ai`, `${SCOPE}/pi-tui`];
|
||||
|
||||
const scopeDir = path.join(repoRoot, "node_modules", SCOPE);
|
||||
const stashDir = path.join(repoRoot, "node_modules", `${SCOPE}.real`);
|
||||
|
||||
function restore() {
|
||||
// Idempotent: safe to call from the finally block and from signal handlers.
|
||||
try {
|
||||
if (existsSync(stashDir)) {
|
||||
if (existsSync(scopeDir)) unlinkSync(scopeDir);
|
||||
renameSync(stashDir, scopeDir);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`\nFAILED TO RESTORE node_modules/${SCOPE}. Run: mv "${stashDir}" "${scopeDir}"\n`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.on(signal, () => { restore(); process.exit(130); });
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf-8"));
|
||||
const range = pkg.peerDependencies?.[`${SCOPE}/pi-coding-agent`];
|
||||
const floor = /(\d+\.\d+\.\d+)/.exec(range ?? "")?.[1];
|
||||
if (!floor) {
|
||||
console.error(`peerDependencies range "${range}" has no explicit floor — pin one like ">=0.80.1"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Minimum supported ${SCOPE}/pi-coding-agent: ${floor}`);
|
||||
|
||||
const scratch = mkdtempSync(path.join(tmpdir(), "pi-hermes-min-sdk-"));
|
||||
let failed = false;
|
||||
try {
|
||||
writeFileSync(path.join(scratch, "package.json"), `${JSON.stringify({ name: "min-sdk-probe", private: true })}\n`);
|
||||
const specs = FLOOR_PACKAGES.map((name) => `${name}@${floor}`);
|
||||
console.log(`Installing ${specs.join(" ")} ...`);
|
||||
execFileSync("npm", ["install", "--silent", "--no-audit", "--no-fund", "--no-package-lock", ...specs], {
|
||||
cwd: scratch,
|
||||
stdio: ["ignore", "ignore", "inherit"],
|
||||
});
|
||||
|
||||
// Swap the scope in place so the project's own tsconfig applies unchanged —
|
||||
// no divergent probe config that could drift from what `npm run check` uses.
|
||||
renameSync(scopeDir, stashDir);
|
||||
symlinkSync(path.join(scratch, "node_modules", SCOPE), scopeDir);
|
||||
|
||||
console.log("Type-checking src against the minimum SDK ...");
|
||||
execFileSync(path.join(repoRoot, "node_modules", ".bin", "tsc"), ["--noEmit"], {
|
||||
cwd: repoRoot,
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log(`OK — src type-checks against ${SCOPE}/pi-coding-agent@${floor}`);
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
if (!/Command failed/.test(String(error?.message))) console.error(error);
|
||||
console.error(
|
||||
`\nsrc does NOT type-check against the declared minimum (${floor}).\n`
|
||||
+ "Either raise the peerDependencies floor in package.json to a version that works,\n"
|
||||
+ "or stop using the SDK API that is missing at that version.\n",
|
||||
);
|
||||
} finally {
|
||||
restore();
|
||||
rmSync(scratch, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
// Exercise Pi's real jiti loader and completion SDK without package-local peers.
|
||||
// npm access installs production dependencies; model traffic is loopback-only.
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repo = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const root = await mkdtemp(join(tmpdir(), "pi-hermes-production-"));
|
||||
const previousAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
let requests = 0;
|
||||
const server = createServer((req, res) => {
|
||||
req.resume();
|
||||
req.on("end", () => {
|
||||
if (req.url !== "/v1/chat/completions") {
|
||||
res.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
requests++;
|
||||
res.writeHead(200, { "content-type": "text/event-stream" });
|
||||
const chunk = (delta, finish_reason) => ({
|
||||
id: "smoke", object: "chat.completion.chunk", created: 0, model: "smoke",
|
||||
choices: [{ index: 0, delta, finish_reason }],
|
||||
});
|
||||
res.end([
|
||||
`data: ${JSON.stringify(chunk({ role: "assistant", content: '{"operations":[]}' }, null))}`,
|
||||
`data: ${JSON.stringify(chunk({}, "stop"))}`,
|
||||
"data: [DONE]", "",
|
||||
].join("\n\n"));
|
||||
});
|
||||
});
|
||||
|
||||
function npm(args, cwd) {
|
||||
return execFileSync(process.platform === "win32" ? "npm.cmd" : "npm", args, {
|
||||
cwd, encoding: "utf8", timeout: 180_000, stdio: ["ignore", "pipe", "inherit"],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
process.env.PI_CODING_AGENT_DIR = join(root, "agent");
|
||||
await mkdir(process.env.PI_CODING_AGENT_DIR);
|
||||
const [packed] = JSON.parse(npm(["pack", "--ignore-scripts", "--json", "--pack-destination", root], repo));
|
||||
const install = join(root, "install");
|
||||
await mkdir(install);
|
||||
await writeFile(join(install, "package.json"), JSON.stringify({ name: "production-smoke", private: true }));
|
||||
npm(["install", "--omit=dev", "--omit=peer", "--ignore-scripts", "--no-audit", "--no-fund", join(root, packed.filename)], install);
|
||||
const installed = join(install, "node_modules", "pi-hermes-memory");
|
||||
const requireFromPackage = createRequire(join(installed, "package.json"));
|
||||
for (const specifier of ["@earendil-works/pi-ai/compat", "@earendil-works/pi-coding-agent"]) {
|
||||
assert.throws(() => requireFromPackage.resolve(specifier), { code: "MODULE_NOT_FOUND" });
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const baseUrl = `http://127.0.0.1:${server.address().port}/v1`;
|
||||
const probe = join(installed, "production-probe.ts");
|
||||
await writeFile(probe, `
|
||||
import { Type } from "typebox";
|
||||
import { runDirectMemoryCompletion } from "./src/handlers/review-memory-ops.js";
|
||||
export default function (pi) {
|
||||
pi.registerTool({
|
||||
name: "production_probe", label: "Probe", description: "Synthetic direct completion", parameters: Type.Object({}),
|
||||
async execute() {
|
||||
const model = {
|
||||
provider: "openai", api: "openai-completions", id: "smoke", name: "Smoke", baseUrl: ${JSON.stringify(baseUrl)},
|
||||
reasoning: false, input: ["text"], contextWindow: 8192, maxTokens: 1024,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
};
|
||||
const result = await runDirectMemoryCompletion({
|
||||
model, modelRegistry: { getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "synthetic-smoke-key" }) },
|
||||
}, {}, null, { userPrompt: "Return no memory operations.", systemPrompt: "Synthetic test.", config: {}, timeoutMs: 10000 });
|
||||
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
||||
},
|
||||
});
|
||||
}
|
||||
`);
|
||||
|
||||
// The host SDK is installed in the development checkout, not beside the
|
||||
// production package. Only Pi's loader can provide its aliases to the probe.
|
||||
const hostSdk = import.meta.resolve("@earendil-works/pi-coding-agent");
|
||||
const { loadExtensions } = await import(new URL("./core/extensions/loader.js", hostSdk).href);
|
||||
const loaded = await loadExtensions([join(installed, "src", "index.ts"), probe], install);
|
||||
assert.deepEqual(loaded.errors, []);
|
||||
const tool = loaded.extensions.find((extension) => extension.tools.has("production_probe"))
|
||||
?.tools.get("production_probe").definition;
|
||||
assert.ok(tool, "production probe registered through Pi");
|
||||
for (const operation of ["review", "flush", "correction", "consolidation"]) {
|
||||
const result = await tool.execute(operation, {});
|
||||
assert.equal(result.details.ok, true, JSON.stringify(result.details));
|
||||
assert.equal(result.details.appliedCount, 0);
|
||||
}
|
||||
assert.equal(requests, 4);
|
||||
console.log("Production install passed: no package-local SDK peers; Pi jiti load and four loopback direct completions succeeded.");
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previousAgentDir;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Guard for check/test npm scripts.
|
||||
*
|
||||
* The published tarball intentionally omits tests/ and TypeScript (devDependency).
|
||||
* When those scripts are run from an installed package (e.g. after `pi install`),
|
||||
* fail with a clear message instead of `tsc: not found` / `tests/run-all.sh: not found`.
|
||||
*
|
||||
* @see https://github.com/chandra447/pi-hermes-memory/issues/108
|
||||
*/
|
||||
import { existsSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const mode = process.argv[2];
|
||||
const REPO = "https://github.com/chandra447/pi-hermes-memory";
|
||||
|
||||
function fail(message) {
|
||||
console.error(`\n[pi-hermes-memory] ${message}`);
|
||||
console.error("");
|
||||
console.error("These scripts only work from a full source checkout:");
|
||||
console.error(` git clone ${REPO}.git`);
|
||||
console.error(" cd pi-hermes-memory && npm install");
|
||||
console.error(" npm run check # or: npm test");
|
||||
console.error("");
|
||||
console.error(
|
||||
"The published package is for runtime use via Pi; CI validates check/test before publish.",
|
||||
);
|
||||
console.error("");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (mode === "check") {
|
||||
const require = createRequire(join(root, "package.json"));
|
||||
try {
|
||||
require.resolve("typescript/package.json");
|
||||
} catch {
|
||||
fail(
|
||||
"`npm run check` requires TypeScript (a devDependency). The published npm package does not include it.",
|
||||
);
|
||||
}
|
||||
if (!existsSync(join(root, "tsconfig.json"))) {
|
||||
fail(
|
||||
"`npm run check` requires tsconfig.json. The published npm package does not include it.",
|
||||
);
|
||||
}
|
||||
} else if (mode === "test") {
|
||||
if (!existsSync(join(root, "tests", "run-all.sh"))) {
|
||||
fail(
|
||||
"`npm test` requires the tests/ directory. The published npm package intentionally omits it.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
fail(`Unknown mode "${mode ?? ""}". Expected "check" or "test".`);
|
||||
}
|
||||
Reference in New Issue
Block a user