mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
feat: prepare self-contained open-source release
This commit is contained in:
+1
-1
@@ -53,7 +53,7 @@ app/api/
|
||||
auth/logout/[provider]/route.ts POST OAuth logout
|
||||
auth/providers/route.ts GET OAuth provider list
|
||||
cwd/validate/route.ts POST validate/select a cwd
|
||||
default-cwd/route.ts POST create ~/pi-cwd-YYYYMMDD
|
||||
default-cwd/route.ts POST create managed data/workspaces/default (standalone: ~/pi-cwd-YYYYMMDD)
|
||||
files/[...path]/route.ts GET file contents for viewer
|
||||
home/route.ts GET user home directory
|
||||
models/route.ts GET { models, modelList, defaultModel }
|
||||
|
||||
@@ -2,14 +2,20 @@ import { NextResponse } from "next/server";
|
||||
import { mkdirSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
import { getManagedRuntimePaths, isManagedRuntime } from "@/lib/app-runtime";
|
||||
import { allowFileRoot } from "@/lib/file-access";
|
||||
|
||||
// POST /api/default-cwd
|
||||
// Creates ~/pi-cwd-<YYYYMMDD> if it doesn't exist and returns the path.
|
||||
// Managed app: creates <dataDir>/workspaces/default.
|
||||
// Standalone pi-web: preserves the upstream ~/pi-cwd-<YYYYMMDD> behavior.
|
||||
export async function POST() {
|
||||
try {
|
||||
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
||||
const dir = join(homedir(), `pi-cwd-${date}`);
|
||||
const dir = isManagedRuntime()
|
||||
? join(getManagedRuntimePaths().dataDir, "workspaces", "default")
|
||||
: join(
|
||||
homedir(),
|
||||
`pi-cwd-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}`,
|
||||
);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
allowFileRoot(dir);
|
||||
return NextResponse.json({ cwd: dir });
|
||||
|
||||
@@ -1849,7 +1849,7 @@ export function ModelsConfig({ onClose }: { onClose: () => void }) {
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 18px", borderBottom: "1px solid var(--border)", flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
|
||||
<span style={{ fontSize: 15, fontWeight: 700, color: "var(--text)" }}>{t("common.models")}</span>
|
||||
<code style={{ fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>~/.pi/agent/models.json</code>
|
||||
<code style={{ fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>data/agent/models.json</code>
|
||||
</div>
|
||||
<button onClick={onClose} style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", fontSize: 20, lineHeight: 1, padding: "2px 6px" }}>×</button>
|
||||
</div>
|
||||
|
||||
@@ -466,6 +466,12 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
||||
|
||||
try {
|
||||
const stateRes = await fetch(`/api/sessions/${encodeURIComponent(sid)}/state`);
|
||||
if (stateRes.status === 404) {
|
||||
if (sessionIdRef.current === sid) {
|
||||
setQueuedMessages({ steering: [], followUp: [] });
|
||||
}
|
||||
return { running: false };
|
||||
}
|
||||
if (!stateRes.ok) throw new Error(`HTTP ${stateRes.status}`);
|
||||
const agentState = await stateRes.json() as { running: boolean; state?: AgentStateResponse };
|
||||
if (sessionIdRef.current !== sid) return null;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const extensionPath = resolve(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"../../resources/extensions/searxng-search.ts",
|
||||
);
|
||||
const extensionDir = dirname(extensionPath);
|
||||
|
||||
test("managed SearXNG extension loads and maps the complete search contract", async () => {
|
||||
const fixtureRoot = mkdtempSync(join(tmpdir(), "pi-searxng-extension-"));
|
||||
const agentDir = resolve(fixtureRoot, "agent");
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalToken = process.env.SEARXNG_TOKEN;
|
||||
const originalUrl = process.env.SEARXNG_URL;
|
||||
let capturedRequest;
|
||||
|
||||
process.env.SEARXNG_TOKEN = "test-token";
|
||||
process.env.SEARXNG_URL = "https://search.example.test/search";
|
||||
globalThis.fetch = async (input, init) => {
|
||||
capturedRequest = { input: String(input), init };
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
answers: ["A current answer"],
|
||||
results: [
|
||||
{
|
||||
title: "Example result",
|
||||
url: "https://example.test/result",
|
||||
content: "Example snippet",
|
||||
engines: ["google", "github"],
|
||||
publishedDate: "2026-07-30",
|
||||
},
|
||||
],
|
||||
suggestions: ["refined query"],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const loader = new DefaultResourceLoader({
|
||||
cwd: fixtureRoot,
|
||||
agentDir,
|
||||
additionalExtensionPaths: [extensionDir],
|
||||
});
|
||||
await loader.reload();
|
||||
|
||||
const loaded = loader.getExtensions();
|
||||
assert.deepEqual(loaded.errors, []);
|
||||
const extension = loaded.extensions.find((candidate) => resolve(candidate.path) === extensionPath);
|
||||
assert.ok(extension, "SearXNG extension should load through the managed extension directory manifest");
|
||||
|
||||
const registeredTool = extension.tools.get("web_search");
|
||||
assert.ok(registeredTool, "web_search should be registered");
|
||||
const tool = registeredTool.definition;
|
||||
|
||||
const result = await tool.execute(
|
||||
"test-call",
|
||||
{
|
||||
query: "pi coding agent",
|
||||
num_results: 5,
|
||||
engines: ["google", "github"],
|
||||
language: "en",
|
||||
page: 2,
|
||||
time_range: "week",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
assert.ok(capturedRequest, "web_search should make an HTTP request");
|
||||
const requestUrl = new URL(capturedRequest.input);
|
||||
assert.equal(requestUrl.searchParams.get("q"), "pi coding agent");
|
||||
assert.equal(requestUrl.searchParams.get("format"), "json");
|
||||
assert.equal(requestUrl.searchParams.get("engines"), "google,github");
|
||||
assert.equal(requestUrl.searchParams.get("language"), "en");
|
||||
assert.equal(requestUrl.searchParams.get("pageno"), "2");
|
||||
assert.equal(requestUrl.searchParams.get("time_range"), "week");
|
||||
assert.equal(new Headers(capturedRequest.init.headers).get("X-Search-Token"), "test-token");
|
||||
assert.match(result.content[0].text, /Example result/);
|
||||
assert.match(result.content[0].text, /https:\/\/example\.test\/result/);
|
||||
assert.equal(result.details.resultCount, 1);
|
||||
assert.equal(JSON.stringify(result).includes("test-token"), false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalToken === undefined) delete process.env.SEARXNG_TOKEN;
|
||||
else process.env.SEARXNG_TOKEN = originalToken;
|
||||
if (originalUrl === undefined) delete process.env.SEARXNG_URL;
|
||||
else process.env.SEARXNG_URL = originalUrl;
|
||||
rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -10,6 +10,7 @@ try {
|
||||
} catch { /* package not found, use default */ }
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
devIndicators: false,
|
||||
serverExternalPackages: [
|
||||
"undici",
|
||||
"@earendil-works/pi-coding-agent",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev -H 127.0.0.1 -p 30141",
|
||||
"dev:lan": "next dev -H 0.0.0.0 -p 30141",
|
||||
"restart": "node ../scripts/restart.mjs",
|
||||
"build": "next build --webpack",
|
||||
"start": "next start -H 127.0.0.1 -p 30141",
|
||||
"start:lan": "next start -H 0.0.0.0 -p 30141",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const appRoot = resolve(scriptDir, "../..");
|
||||
const extensionDir = resolve(appRoot, "resources/extensions");
|
||||
const extensionPath = resolve(appRoot, "resources/extensions/searxng-search.ts");
|
||||
const fixtureRoot = mkdtempSync(join(tmpdir(), "pi-searxng-smoke-"));
|
||||
const agentDir = resolve(fixtureRoot, "agent");
|
||||
const query = process.argv.slice(2).join(" ").trim() || "Pi coding agent";
|
||||
|
||||
if (!process.env.SEARXNG_TOKEN?.trim()) {
|
||||
console.error("SEARXNG_TOKEN is not available to this process.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
try {
|
||||
const loader = new DefaultResourceLoader({
|
||||
cwd: appRoot,
|
||||
agentDir,
|
||||
additionalExtensionPaths: [extensionDir],
|
||||
});
|
||||
await loader.reload();
|
||||
|
||||
const loaded = loader.getExtensions();
|
||||
if (loaded.errors.length > 0) {
|
||||
throw new Error(`Extension load failed: ${loaded.errors.map((error) => error.error).join("; ")}`);
|
||||
}
|
||||
|
||||
const extension = loaded.extensions.find((candidate) => resolve(candidate.path) === extensionPath);
|
||||
const tool = extension?.tools.get("web_search")?.definition;
|
||||
if (!tool) throw new Error("web_search was not registered");
|
||||
|
||||
const result = await tool.execute(
|
||||
"searxng-smoke",
|
||||
{ query, num_results: 3 },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
extensionLoaded: true,
|
||||
tool: tool.name,
|
||||
query,
|
||||
resultCount: result.details?.resultCount ?? null,
|
||||
responseHasText: Boolean(result.content[0]?.text),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user