@co0ontty/wand 4.83.1 → 4.84.0
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/README.md +4 -0
- package/dist/agent-dispatch.js +5 -1
- package/dist/ai-team-availability.d.ts +1 -1
- package/dist/ai-team-prompts.js +13 -4
- package/dist/ai-team-runner.d.ts +80 -5
- package/dist/ai-team-runner.js +332 -55
- package/dist/ai-team-types.d.ts +92 -5
- package/dist/ai-team-types.js +9 -2
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +1 -0
- package/dist/cli-api.js +3 -0
- package/dist/cli.js +18 -1
- package/dist/commit-task-archive.d.ts +3 -1
- package/dist/commit-task-archive.js +21 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +73 -0
- package/dist/conversation-mentions.d.ts +16 -0
- package/dist/conversation-mentions.js +32 -0
- package/dist/conversation-service.d.ts +90 -0
- package/dist/conversation-service.js +759 -0
- package/dist/conversation-session-preview.d.ts +9 -0
- package/dist/conversation-session-preview.js +47 -0
- package/dist/conversation-task-preview.d.ts +5 -0
- package/dist/conversation-task-preview.js +8 -0
- package/dist/conversation-types.d.ts +80 -0
- package/dist/conversation-types.js +7 -0
- package/dist/core-extension-host.d.ts +27 -0
- package/dist/core-extension-host.js +138 -0
- package/dist/core-runner.d.ts +86 -0
- package/dist/core-runner.js +757 -0
- package/dist/core-status-cli.d.ts +9 -0
- package/dist/core-status-cli.js +280 -0
- package/dist/core-turn-tracker.d.ts +12 -0
- package/dist/core-turn-tracker.js +36 -0
- package/dist/decision-runner.d.ts +6 -0
- package/dist/decision-runner.js +3 -1
- package/dist/decision-tool.d.ts +2 -0
- package/dist/decision-tool.js +49 -22
- package/dist/env-utils.d.ts +7 -3
- package/dist/env-utils.js +30 -5
- package/dist/git-quick-commit.d.ts +5 -0
- package/dist/git-quick-commit.js +9 -3
- package/dist/git-utils.js +2 -1
- package/dist/harness-engine.d.ts +110 -0
- package/dist/harness-engine.js +331 -0
- package/dist/mission-diff.js +2 -1
- package/dist/model-auto-assign.d.ts +50 -0
- package/dist/model-auto-assign.js +163 -0
- package/dist/model-group-runner.d.ts +4 -0
- package/dist/model-group-runner.js +72 -0
- package/dist/model-groups.d.ts +38 -0
- package/dist/model-groups.js +138 -0
- package/dist/models.d.ts +12 -0
- package/dist/models.js +102 -8
- package/dist/npm-update-utils.js +2 -1
- package/dist/openrouter-free-cli.d.ts +25 -0
- package/dist/openrouter-free-cli.js +109 -0
- package/dist/openrouter-free-models.d.ts +85 -0
- package/dist/openrouter-free-models.js +458 -0
- package/dist/openrouter-free-selection.d.ts +5 -0
- package/dist/openrouter-free-selection.js +7 -0
- package/dist/path-repair.d.ts +10 -0
- package/dist/path-repair.js +50 -5
- package/dist/pi-auto-resources.d.ts +12 -0
- package/dist/pi-auto-resources.js +151 -0
- package/dist/pi-execution-types.d.ts +48 -0
- package/dist/pi-execution-types.js +1 -0
- package/dist/pi-execution.d.ts +9 -0
- package/dist/pi-execution.js +187 -0
- package/dist/pi-extension-resources.d.ts +13 -0
- package/dist/pi-extension-resources.js +71 -0
- package/dist/pi-openrouter-free-bridge.d.ts +2 -0
- package/dist/pi-openrouter-free-bridge.js +80 -0
- package/dist/pi-resource-bridge.d.ts +2 -0
- package/dist/pi-resource-bridge.js +73 -0
- package/dist/pi-resource-catalog.d.ts +18 -0
- package/dist/pi-resource-catalog.js +82 -0
- package/dist/pi-resource-recommendation.d.ts +29 -0
- package/dist/pi-resource-recommendation.js +169 -0
- package/dist/pi-resource-run.d.ts +8 -0
- package/dist/pi-resource-run.js +28 -0
- package/dist/pi-session-settings.d.ts +157 -0
- package/dist/pi-session-settings.js +166 -0
- package/dist/platform-action-catalog.d.ts +24 -0
- package/dist/platform-action-catalog.js +141 -0
- package/dist/platform-action-service.d.ts +37 -0
- package/dist/platform-action-service.js +138 -0
- package/dist/platform-action-types.d.ts +125 -0
- package/dist/platform-action-types.js +7 -0
- package/dist/process-manager.d.ts +21 -1
- package/dist/process-manager.js +168 -15
- package/dist/provider-catalog.js +1 -1
- package/dist/provider-cli-updater.js +29 -8
- package/dist/provider-history-scanner.d.ts +9 -0
- package/dist/provider-history-scanner.js +107 -1
- package/dist/retention.d.ts +19 -7
- package/dist/retention.js +171 -28
- package/dist/server-ai-team-routes.js +6 -2
- package/dist/server-conversation-routes.d.ts +5 -0
- package/dist/server-conversation-routes.js +91 -0
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +85 -11
- package/dist/server-pi-recommendation-routes.d.ts +13 -0
- package/dist/server-pi-recommendation-routes.js +67 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.d.ts +4 -3
- package/dist/server-session-routes.js +182 -37
- package/dist/server-settings-routes.d.ts +5 -0
- package/dist/server-settings-routes.js +46 -0
- package/dist/server-task-routes.js +49 -4
- package/dist/server-team-dispatch-routes.d.ts +19 -0
- package/dist/server-team-dispatch-routes.js +71 -0
- package/dist/server-update-routes.d.ts +1 -1
- package/dist/server-update-routes.js +10 -5
- package/dist/server-workspace-routes.js +44 -24
- package/dist/server.d.ts +1 -0
- package/dist/server.js +148 -30
- package/dist/session-ai-context.d.ts +5 -1
- package/dist/session-ai-context.js +9 -4
- package/dist/session-registry.d.ts +4 -0
- package/dist/session-registry.js +47 -13
- package/dist/session-transport.js +8 -1
- package/dist/settings-web-access.d.ts +15 -0
- package/dist/settings-web-access.js +37 -0
- package/dist/silicon-employee-defaults.d.ts +29 -0
- package/dist/silicon-employee-defaults.js +41 -0
- package/dist/silicon-employee-dispatch.d.ts +5 -1
- package/dist/silicon-employee-dispatch.js +10 -4
- package/dist/silicon-employee-draft.d.ts +21 -4
- package/dist/silicon-employee-draft.js +103 -16
- package/dist/storage.d.ts +73 -2
- package/dist/storage.js +362 -26
- package/dist/structured-claude-protocol.js +1 -1
- package/dist/structured-client-protocol.js +11 -33
- package/dist/structured-content.d.ts +23 -0
- package/dist/structured-content.js +72 -0
- package/dist/structured-failure.d.ts +16 -0
- package/dist/structured-failure.js +36 -0
- package/dist/structured-pi-adapter.d.ts +8 -3
- package/dist/structured-pi-adapter.js +131 -30
- package/dist/structured-provider-common.d.ts +6 -0
- package/dist/structured-provider-common.js +23 -1
- package/dist/structured-runner.d.ts +45 -0
- package/dist/structured-session-manager.d.ts +78 -4
- package/dist/structured-session-manager.js +513 -129
- package/dist/structured-thinking.d.ts +15 -0
- package/dist/structured-thinking.js +46 -0
- package/dist/subagent-dispatch.d.ts +56 -0
- package/dist/subagent-dispatch.js +102 -0
- package/dist/system-employee.d.ts +4 -3
- package/dist/system-employee.js +7 -4
- package/dist/task-retention.d.ts +23 -0
- package/dist/task-retention.js +69 -0
- package/dist/task-types.d.ts +4 -0
- package/dist/task-types.js +3 -0
- package/dist/team-dispatch.d.ts +101 -0
- package/dist/team-dispatch.js +269 -0
- package/dist/tool-preview.js +7 -1
- package/dist/tui/commands.js +50 -1
- package/dist/tui/ipc-protocol.d.ts +1 -1
- package/dist/tui/ipc-server.d.ts +4 -0
- package/dist/tui/ipc-server.js +23 -0
- package/dist/turn-heartbeat.d.ts +35 -0
- package/dist/turn-heartbeat.js +64 -0
- package/dist/types.d.ts +147 -1
- package/dist/user-profile.d.ts +33 -0
- package/dist/user-profile.js +70 -0
- package/dist/web-ui/chat-message-meta.d.ts +11 -0
- package/dist/web-ui/chat-message-meta.js +47 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +795 -95
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -6808
- package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/index.d.ts +1 -1
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/markdown.d.ts +2 -0
- package/dist/web-ui/markdown.js +111 -0
- package/dist/web-ui/page.d.ts +4 -0
- package/dist/web-ui/page.js +8 -0
- package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
- package/dist/web-ui/react/local-preview/controller.js +159 -0
- package/dist/web-ui/running-activity.d.ts +63 -0
- package/dist/web-ui/running-activity.js +134 -0
- package/dist/web-ui/styles.js +2 -11
- package/package.json +7 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Pi CLI provider bridge. No Agent/SDK runner and no changes to the user's Pi auth/models files. */
|
|
2
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { createAssistantMessageEventStream, lazyApi } from "@earendil-works/pi-ai";
|
|
5
|
+
import { OPENROUTER_FREE_PROVIDER, OPENROUTER_FREE_ROUTING } from "./openrouter-free-models.js";
|
|
6
|
+
import { openOpenRouterFreeCliAccess, revokeOpenRouterFreeCliAccess } from "./openrouter-free-cli.js";
|
|
7
|
+
export default function wandOpenRouterFree(pi) {
|
|
8
|
+
const file = process.env.WAND_PI_FREE_POLICY;
|
|
9
|
+
const token = process.env.WAND_PI_FREE_TOKEN ?? "";
|
|
10
|
+
delete process.env.WAND_PI_FREE_POLICY;
|
|
11
|
+
delete process.env.WAND_PI_FREE_TOKEN;
|
|
12
|
+
if (!file)
|
|
13
|
+
throw new Error("免费模型调用缺少本轮策略。");
|
|
14
|
+
const policy = JSON.parse(readFileSync(file, "utf8"));
|
|
15
|
+
// Pi applies its HTTP/proxy configuration after factories load; use the current fetch per request.
|
|
16
|
+
const access = openOpenRouterFreeCliAccess(policy.databasePath, token, (url, init) => globalThis.fetch(url, init));
|
|
17
|
+
const api = lazyApi(() => import("@earendil-works/pi-ai/api/openai-completions"));
|
|
18
|
+
let activeModel = policy.model;
|
|
19
|
+
const streamSimple = (model, transcript, options) => {
|
|
20
|
+
const stream = createAssistantMessageEventStream();
|
|
21
|
+
void (async () => {
|
|
22
|
+
let key = "";
|
|
23
|
+
try {
|
|
24
|
+
const checked = await access.service.resolveForCall(`${OPENROUTER_FREE_PROVIDER}/${activeModel.id}`, options?.signal, access.requirements);
|
|
25
|
+
activeModel = checked.model;
|
|
26
|
+
key = checked.apiKey;
|
|
27
|
+
const inner = api.streamSimple(activeModel, transcript, {
|
|
28
|
+
...options, apiKey: key,
|
|
29
|
+
maxTokens: Math.min(options?.maxTokens ?? activeModel.maxTokens, activeModel.maxTokens),
|
|
30
|
+
reasoning: activeModel.reasoning ? options?.reasoning : undefined,
|
|
31
|
+
onPayload: async (payload, dispatchedModel) => {
|
|
32
|
+
const customized = await options?.onPayload?.(payload, dispatchedModel) ?? payload;
|
|
33
|
+
access.assertCurrent();
|
|
34
|
+
return { ...customized, model: activeModel.id, provider: OPENROUTER_FREE_ROUTING };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
for await (const event of inner) {
|
|
38
|
+
if (event.type === "error" && event.error.errorMessage) {
|
|
39
|
+
event.error.errorMessage = event.error.errorMessage.replaceAll(key, "[REDACTED]");
|
|
40
|
+
}
|
|
41
|
+
stream.push(event);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (cause) {
|
|
45
|
+
const reason = options?.signal?.aborted ? "aborted" : "error";
|
|
46
|
+
const message = cause instanceof Error ? cause.message : "免费模型调用失败。";
|
|
47
|
+
stream.push({ type: "error", reason, error: {
|
|
48
|
+
role: "assistant", content: [], api: model.api, provider: model.provider, model: activeModel.id,
|
|
49
|
+
timestamp: Date.now(), stopReason: reason, errorMessage: key ? message.replaceAll(key, "[REDACTED]") : message,
|
|
50
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
|
|
51
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
52
|
+
} });
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
stream.end();
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
return stream;
|
|
59
|
+
};
|
|
60
|
+
// Use a distinct API handler: never replace Pi's built-in openai-completions implementation.
|
|
61
|
+
pi.registerProvider(OPENROUTER_FREE_PROVIDER, {
|
|
62
|
+
name: "OpenRouter 免费分组", api: "wand-openrouter-free-completions", apiKey: "wand-scoped-free-inference",
|
|
63
|
+
baseUrl: activeModel.baseUrl,
|
|
64
|
+
models: [{ ...activeModel, api: "wand-openrouter-free-completions",
|
|
65
|
+
id: access.selector.slice(OPENROUTER_FREE_PROVIDER.length + 1) }],
|
|
66
|
+
streamSimple,
|
|
67
|
+
});
|
|
68
|
+
let closed = false;
|
|
69
|
+
pi.on("session_shutdown", () => {
|
|
70
|
+
if (closed)
|
|
71
|
+
return;
|
|
72
|
+
closed = true;
|
|
73
|
+
access.close();
|
|
74
|
+
try {
|
|
75
|
+
revokeOpenRouterFreeCliAccess(policy.databasePath, token);
|
|
76
|
+
}
|
|
77
|
+
catch { /* Expiry and the parent are additional cleanup paths. */ }
|
|
78
|
+
rmSync(dirname(file), { recursive: true, force: true });
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Explicit CLI extension: replaces built-in MCP discovery without changing the user's agent directory. */
|
|
2
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { createCodemodeExtension, createMcpExtension, createToolSearchExtension } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
export default async function wandResourceSelection(pi) {
|
|
6
|
+
const policyFile = process.env.WAND_PI_RESOURCE_POLICY;
|
|
7
|
+
if (!policyFile)
|
|
8
|
+
throw new Error("Wand 资源选择缺少本轮策略。");
|
|
9
|
+
const policy = JSON.parse(readFileSync(policyFile, "utf8"));
|
|
10
|
+
// Entries were validated by Pi's parser immediately before spawning. Read only this user's file;
|
|
11
|
+
// do not replace the agent directory or discover trusted-project/extension servers implicitly.
|
|
12
|
+
const allowed = new Set(policy.mcpServers ?? []);
|
|
13
|
+
const scoped = new Proxy(pi, { get(target, key) {
|
|
14
|
+
// Extensions may register servers too. They are not the file-configured entries the user selected.
|
|
15
|
+
if (key === "getMcpServers")
|
|
16
|
+
return () => [];
|
|
17
|
+
const value = Reflect.get(target, key);
|
|
18
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
19
|
+
} });
|
|
20
|
+
if (policy.mcpServers)
|
|
21
|
+
createMcpExtension({
|
|
22
|
+
loadConfig: () => {
|
|
23
|
+
if (!allowed.size)
|
|
24
|
+
return { servers: [], errors: [] };
|
|
25
|
+
try {
|
|
26
|
+
const source = join(policy.agentDir, "mcp.json");
|
|
27
|
+
const raw = JSON.parse(readFileSync(source, "utf8"));
|
|
28
|
+
const servers = policy.mcpServers.map((name) => {
|
|
29
|
+
const config = raw.mcpServers?.[name];
|
|
30
|
+
if (!config)
|
|
31
|
+
throw new Error("MCP 已移除");
|
|
32
|
+
return { name, source, scope: "global", config: { ...config, enabled: true } };
|
|
33
|
+
});
|
|
34
|
+
return { servers, errors: [], autoEnableCodemode: policy.codemodeOverride !== "off" && raw.autoEnableCodemode !== false };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { servers: [], errors: ["所选 MCP 配置无法读取,没有连接其他服务器。"] };
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
updateConfig: () => { throw new Error("请通过 Wand 输入框中的 Skills / MCP 设置修改本会话选择。"); },
|
|
41
|
+
})(scoped);
|
|
42
|
+
if (policy.codemodeOverride) {
|
|
43
|
+
// This explicit factory replaces the CLI builtin and can express `only` without modifying
|
|
44
|
+
// settings.json. Active-tool changes preserve every common tool and selected MCP connection.
|
|
45
|
+
const codemodeApi = policy.codemodeOverride === "off" ? new Proxy(pi, { get(target, key) {
|
|
46
|
+
if (key === "registerTool")
|
|
47
|
+
return (definition) => target.registerTool({ ...definition, exposure: "hidden" });
|
|
48
|
+
const value = Reflect.get(target, key);
|
|
49
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
50
|
+
} }) : pi;
|
|
51
|
+
createCodemodeExtension({ mode: policy.codemodeOverride === "only" ? "only" : "on" })(codemodeApi);
|
|
52
|
+
// MCP tools with indirect exposure still need an entrance when CodeMode is explicitly off.
|
|
53
|
+
const searchInstead = policy.codemodeOverride === "off" && !!policy.mcpServers?.length;
|
|
54
|
+
if (searchInstead)
|
|
55
|
+
createToolSearchExtension()(pi);
|
|
56
|
+
const apply = () => {
|
|
57
|
+
const other = pi.getActiveTools().filter((name) => name !== "codemode");
|
|
58
|
+
pi.setActiveTools(policy.codemodeOverride === "off"
|
|
59
|
+
? searchInstead ? [...other, "tool_search"] : other : [...other, "codemode"]);
|
|
60
|
+
};
|
|
61
|
+
pi.on("session_start", apply);
|
|
62
|
+
pi.on("before_agent_start", apply);
|
|
63
|
+
pi.on("tool_call", (event) => {
|
|
64
|
+
if (event.toolName === "codemode" && policy.codemodeOverride === "off") {
|
|
65
|
+
return { block: true, reason: "本会话已关闭 CodeMode,请在输入框设置中启用。" };
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
pi.on("session_shutdown", () => {
|
|
70
|
+
// The CLI outlives web restarts: cleanup belongs to the child, not only its original web owner.
|
|
71
|
+
rmSync(dirname(policyFile), { recursive: true, force: true });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PiResourceCatalog, PiResourceSelection } from "./pi-session-settings.js";
|
|
2
|
+
import type { WandConfig } from "./types.js";
|
|
3
|
+
import type { Skill, McpExtensionOptions } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
type McpConfig = ReturnType<NonNullable<McpExtensionOptions["loadConfig"]>>;
|
|
5
|
+
export interface PiResourceInventory {
|
|
6
|
+
catalog: PiResourceCatalog;
|
|
7
|
+
skills: Map<string, Skill>;
|
|
8
|
+
servers: Map<string, McpConfig["servers"][number]>;
|
|
9
|
+
}
|
|
10
|
+
/** Pinned, read-only parser boundary; never connects, runs credential commands or loads extension code. */
|
|
11
|
+
export declare function readUserPiMcpConfig(agentDir: string, cwd: string): Promise<McpConfig>;
|
|
12
|
+
/** Installed USER resources only. No package downloads and no project config mutation/trust elevation. */
|
|
13
|
+
export declare function discoverPiResources(config: Pick<WandConfig, "harness">, cwd: string): Promise<PiResourceInventory>;
|
|
14
|
+
export declare function resolvePiResourceSelection(inventory: PiResourceInventory, selection: PiResourceSelection): {
|
|
15
|
+
skills: Skill[];
|
|
16
|
+
servers: McpConfig["servers"];
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { coreHarnessAgentDir, loadPiCodingAgent } from "./harness-engine.js";
|
|
5
|
+
function resourceId(kind, identity) {
|
|
6
|
+
return `${kind}-${createHash("sha256").update(identity).digest("hex").slice(0, 24)}`;
|
|
7
|
+
}
|
|
8
|
+
/** Pinned, read-only parser boundary; never connects, runs credential commands or loads extension code. */
|
|
9
|
+
export async function readUserPiMcpConfig(agentDir, cwd) {
|
|
10
|
+
const url = new URL("./extensions/mcp/config.js", import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
11
|
+
const parser = await import(url.href);
|
|
12
|
+
return parser.loadMcpConfig({ agentDir, cwd, projectTrusted: false });
|
|
13
|
+
}
|
|
14
|
+
/** Installed USER resources only. No package downloads and no project config mutation/trust elevation. */
|
|
15
|
+
export async function discoverPiResources(config, cwd) {
|
|
16
|
+
const pi = await loadPiCodingAgent();
|
|
17
|
+
const agentDir = coreHarnessAgentDir(config.harness);
|
|
18
|
+
let raw = {};
|
|
19
|
+
try {
|
|
20
|
+
const content = await readFile(path.join(agentDir, "settings.json"), "utf8");
|
|
21
|
+
if (content.length > 1_048_576)
|
|
22
|
+
throw new Error("配置过大");
|
|
23
|
+
raw = JSON.parse(content);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== "ENOENT")
|
|
27
|
+
throw new Error("Pi 资源配置无法读取,请检查 settings.json。");
|
|
28
|
+
}
|
|
29
|
+
const settings = pi.SettingsManager.inMemory({
|
|
30
|
+
packages: Array.isArray(raw.packages) ? raw.packages : [],
|
|
31
|
+
skills: Array.isArray(raw.skills) ? raw.skills.filter((item) => typeof item === "string") : [],
|
|
32
|
+
extensions: Array.isArray(raw.extensions) ? raw.extensions.filter((item) => typeof item === "string") : [],
|
|
33
|
+
}, { projectTrusted: false });
|
|
34
|
+
const manager = new pi.DefaultPackageManager({ cwd, agentDir, settingsManager: settings });
|
|
35
|
+
const resolved = await manager.resolve(async () => "skip");
|
|
36
|
+
const paths = resolved.skills.filter((item) => item.enabled && item.metadata.scope === "user").map((item) => item.path);
|
|
37
|
+
const loaded = pi.loadSkills({ cwd, agentDir, skillPaths: paths, includeDefaults: false });
|
|
38
|
+
const skills = new Map();
|
|
39
|
+
for (const skill of loaded.skills) {
|
|
40
|
+
const canonical = await realpath(skill.filePath);
|
|
41
|
+
const info = await stat(canonical);
|
|
42
|
+
if (!info.isFile() || info.size > 128 * 1024)
|
|
43
|
+
continue;
|
|
44
|
+
const id = resourceId("skill", canonical);
|
|
45
|
+
if (!skills.has(id))
|
|
46
|
+
skills.set(id, { ...skill, filePath: canonical });
|
|
47
|
+
if (skills.size >= 200)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const mcp = await readUserPiMcpConfig(agentDir, cwd);
|
|
51
|
+
const servers = new Map();
|
|
52
|
+
for (const server of mcp.servers.slice(0, 64))
|
|
53
|
+
servers.set(resourceId("mcp", server.name), server);
|
|
54
|
+
const customMcp = resolved.extensions.some((item) => item.enabled && /pi-mcp-adapter/.test(item.metadata.source));
|
|
55
|
+
return { skills, servers, catalog: {
|
|
56
|
+
supported: !customMcp,
|
|
57
|
+
reason: customMcp ? "当前 Pi 使用第三方 MCP 适配器,无法保证只连接选定服务器,请先使用 Pi 内置 MCP。"
|
|
58
|
+
: mcp.errors.length ? "MCP 配置含无效条目,仅列出已验证的服务器;未选条目不会连接。" : "",
|
|
59
|
+
skills: [...skills].map(([id, skill]) => ({ id, name: skill.name,
|
|
60
|
+
description: skill.description.slice(0, 1024), source: "已安装" })),
|
|
61
|
+
mcpServers: [...servers].map(([id, server]) => ({ id, name: server.name,
|
|
62
|
+
description: "url" in server.config ? "已配置的 HTTP MCP 服务" : "已配置的本地 MCP 服务",
|
|
63
|
+
source: server.config.enabled === false ? "全局停用 · 本会话可选" : "用户配置" })),
|
|
64
|
+
} };
|
|
65
|
+
}
|
|
66
|
+
export function resolvePiResourceSelection(inventory, selection) {
|
|
67
|
+
if (!inventory.catalog.supported)
|
|
68
|
+
throw new Error(inventory.catalog.reason);
|
|
69
|
+
const skills = selection.skills.map((id) => {
|
|
70
|
+
const found = inventory.skills.get(id);
|
|
71
|
+
if (!found)
|
|
72
|
+
throw new Error("所选 Skill 已不存在或已被停用,请重新选择;没有改用其他 Skill。");
|
|
73
|
+
return found;
|
|
74
|
+
});
|
|
75
|
+
const servers = selection.mcpServers.map((id) => {
|
|
76
|
+
const found = inventory.servers.get(id);
|
|
77
|
+
if (!found)
|
|
78
|
+
throw new Error("所选 MCP 已不存在,请重新选择;没有连接其他 MCP。");
|
|
79
|
+
return found;
|
|
80
|
+
});
|
|
81
|
+
return { skills, servers };
|
|
82
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type DecisionResult } from "./decision-types.js";
|
|
2
|
+
import { type PiResourceCatalog, type PiResourceRecommendation, type PiResourceSelection } from "./pi-session-settings.js";
|
|
3
|
+
export declare const PI_RECOMMEND_MAX_CANDIDATES = 24;
|
|
4
|
+
export declare const PI_RECOMMEND_MAX_PROMPT = 1200;
|
|
5
|
+
export type PiResourceEvaluator = (value: unknown, caller: string, signal?: AbortSignal) => Promise<DecisionResult>;
|
|
6
|
+
export declare function parsePiRecommendationRequest(raw: unknown): {
|
|
7
|
+
prompt: string;
|
|
8
|
+
candidates: PiResourceSelection;
|
|
9
|
+
};
|
|
10
|
+
export interface PiAutomaticCodemodeChoice {
|
|
11
|
+
override?: "off" | "on";
|
|
12
|
+
source: "explicit" | "local" | "uncertain";
|
|
13
|
+
}
|
|
14
|
+
/** CodeMode is a per-round tool entry choice, never authority to execute its tools. */
|
|
15
|
+
export declare function decidePiCodemode(input: {
|
|
16
|
+
prompt: string;
|
|
17
|
+
evaluate: PiResourceEvaluator;
|
|
18
|
+
caller: string;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}): Promise<PiAutomaticCodemodeChoice>;
|
|
21
|
+
/** Read-only matching. Results never modify settings, connect MCP, or authorize an operation. */
|
|
22
|
+
export declare function recommendPiResources(input: {
|
|
23
|
+
prompt: string;
|
|
24
|
+
candidates: PiResourceSelection;
|
|
25
|
+
catalog: PiResourceCatalog;
|
|
26
|
+
evaluate: PiResourceEvaluator;
|
|
27
|
+
caller: string;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
}): Promise<PiResourceRecommendation>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { DecisionError } from "./decision-types.js";
|
|
2
|
+
import { isPiSettingsDraft, patchPiResourceSelection } from "./pi-session-settings.js";
|
|
3
|
+
export const PI_RECOMMEND_MAX_CANDIDATES = 24;
|
|
4
|
+
export const PI_RECOMMEND_MAX_PROMPT = 1200;
|
|
5
|
+
export function parsePiRecommendationRequest(raw) {
|
|
6
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
7
|
+
throw new DecisionError("INVALID_REQUEST", "请提供当前提示词和候选资源。");
|
|
8
|
+
const body = raw;
|
|
9
|
+
if (Object.keys(body).some((key) => key !== "prompt" && key !== "candidates")
|
|
10
|
+
|| typeof body.prompt !== "string" || !body.prompt.trim() || isPiSettingsDraft(body.prompt)
|
|
11
|
+
|| body.prompt.length > PI_RECOMMEND_MAX_PROMPT) {
|
|
12
|
+
throw new DecisionError("INVALID_REQUEST", "请先输入任务提示词(最多1200字符);设置命令不能用于推荐。");
|
|
13
|
+
}
|
|
14
|
+
const candidates = patchPiResourceSelection(body.candidates);
|
|
15
|
+
if (!candidates.skills.every((id) => id.startsWith("skill-")) || !candidates.mcpServers.every((id) => id.startsWith("mcp-"))
|
|
16
|
+
|| candidates.skills.length + candidates.mcpServers.length > PI_RECOMMEND_MAX_CANDIDATES) {
|
|
17
|
+
throw new DecisionError("CANDIDATE_LIMIT", "一次最多推荐24项资源,请用搜索缩小候选范围。");
|
|
18
|
+
}
|
|
19
|
+
return { prompt: body.prompt.trim(), candidates };
|
|
20
|
+
}
|
|
21
|
+
/** Literal resource names only; conservative exclusions take priority over inclusion. */
|
|
22
|
+
function explicitMention(prompt, name) {
|
|
23
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
+
const literal = new RegExp(`(^|[^a-zA-Z0-9_-])${escaped}(?=$|[^a-zA-Z0-9_-])`, "i");
|
|
25
|
+
let included = false;
|
|
26
|
+
for (const clause of prompt.split(/[。!?!?;;\n,,]/)) {
|
|
27
|
+
const match = literal.exec(clause);
|
|
28
|
+
if (!match)
|
|
29
|
+
continue;
|
|
30
|
+
const before = clause.slice(0, match.index + match[1].length);
|
|
31
|
+
const after = clause.slice(match.index + match[0].length);
|
|
32
|
+
if (/(?:不要|不用|不使用|不启用|不加载|不连接|排除|禁用|停用|无需|不需要|without|do not|don['’]t|exclude|disable)/i.test(before)
|
|
33
|
+
|| /^\s*(?:也|暂时|这次)?\s*(?:不要|不用|不使用|不启用|不加载|不连接|禁用|停用|不需要|disabled|excluded)/i.test(after))
|
|
34
|
+
return "excluded";
|
|
35
|
+
included = true;
|
|
36
|
+
}
|
|
37
|
+
return included ? "included" : null;
|
|
38
|
+
}
|
|
39
|
+
/** Strip only standalone resource switches, never business constraints or the submitted prompt itself. */
|
|
40
|
+
function resourceTask(prompt, catalog) {
|
|
41
|
+
const names = [...catalog.skills, ...catalog.mcpServers].map((item) => item.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
42
|
+
if (!names.length)
|
|
43
|
+
return prompt;
|
|
44
|
+
const name = `(?:${names.join("|")})`;
|
|
45
|
+
const verb = "(?:不要|不用|不使用|不启用|不加载|不连接|排除|禁用|停用|无需|不需要|使用|启用|加载|连接|without|do not use|don't use|exclude|disable|use|enable)";
|
|
46
|
+
const switchOnly = new RegExp(`^(?:(?:也|这次|暂时)\\s*)?${verb}\\s*${name}$|^${name}\\s*(?:(?:也|这次|暂时)\\s*)?${verb}$`, "i");
|
|
47
|
+
const clauses = prompt.split(/[。!?!?;;\n,,]/);
|
|
48
|
+
const task = clauses.filter((clause) => !switchOnly.test(clause.trim()));
|
|
49
|
+
return task.length === clauses.length ? prompt : task.filter((clause) => clause.trim()).join(",").trim();
|
|
50
|
+
}
|
|
51
|
+
const MATCH_INSTRUCTIONS = "Which skill fits the task?";
|
|
52
|
+
/** Keep the task separate from skill marketing text; the small model overweights long descriptions. */
|
|
53
|
+
function shortlistQuestions(name) {
|
|
54
|
+
return {
|
|
55
|
+
forward: { type: "choice", instructions: MATCH_INSTRUCTIONS, criteria: { match: name, none: "None" } },
|
|
56
|
+
reverse: { type: "choice", instructions: MATCH_INSTRUCTIONS, criteria: { none: "None", match: name } },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function matchProbability(result, id) {
|
|
60
|
+
const answer = result.answers[id];
|
|
61
|
+
const probabilities = answer?.probabilities;
|
|
62
|
+
const match = probabilities?.match;
|
|
63
|
+
const none = probabilities?.none;
|
|
64
|
+
if (answer?.type !== "choice" || !["match", "none"].includes(String(answer.choice))
|
|
65
|
+
|| typeof match !== "number" || typeof none !== "number" || !Number.isFinite(match) || !Number.isFinite(none)
|
|
66
|
+
|| match < 0 || match > 1 || none < 0 || none > 1 || Math.abs(match + none - 1) > 0.002
|
|
67
|
+
|| (answer.choice === "match" ? match < none : none < match)) {
|
|
68
|
+
throw new DecisionError("INVALID_RESULT", "本地模型返回无效判断,原选择未改变。", 502);
|
|
69
|
+
}
|
|
70
|
+
return match;
|
|
71
|
+
}
|
|
72
|
+
/** CodeMode is a per-round tool entry choice, never authority to execute its tools. */
|
|
73
|
+
export async function decidePiCodemode(input) {
|
|
74
|
+
if (input.signal?.aborted)
|
|
75
|
+
throw new DecisionError("CANCELLED", "CodeMode 自动判断已取消。", 499);
|
|
76
|
+
const mention = explicitMention(input.prompt, "codemode") ?? explicitMention(input.prompt, "code mode");
|
|
77
|
+
if (mention === "excluded")
|
|
78
|
+
return { override: "off", source: "explicit" };
|
|
79
|
+
if (mention === "included" && (/(?:使用|启用|开启|打开|通过|用|enable|use)\s*code\s?mode/i.test(input.prompt)
|
|
80
|
+
|| /code\s?mode\s*(?:开启|启用|打开|on\b)/i.test(input.prompt)))
|
|
81
|
+
return { override: "on", source: "explicit" };
|
|
82
|
+
let result;
|
|
83
|
+
try {
|
|
84
|
+
result = await input.evaluate({
|
|
85
|
+
state: input.prompt,
|
|
86
|
+
questions: {
|
|
87
|
+
forward: { type: "choice", instructions: "How to run the tools?",
|
|
88
|
+
criteria: { match: "Batch tool calls", none: "Ordinary tool calls" } },
|
|
89
|
+
reverse: { type: "choice", instructions: "How to run the tools?",
|
|
90
|
+
criteria: { none: "Ordinary tool calls", match: "Batch tool calls" } },
|
|
91
|
+
},
|
|
92
|
+
}, input.caller, input.signal);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof DecisionError && ["CONTEXT_LIMIT", "QUESTION_LIMIT", "OPTIONS_COLLAPSED"].includes(error.code)) {
|
|
96
|
+
return { source: "uncertain" };
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
const forward = matchProbability(result, "forward");
|
|
101
|
+
const reverse = matchProbability(result, "reverse");
|
|
102
|
+
if (forward >= 0.8 && reverse >= 0.8)
|
|
103
|
+
return { override: "on", source: "local" };
|
|
104
|
+
if (forward <= 0.2 && reverse <= 0.2)
|
|
105
|
+
return { override: "off", source: "local" };
|
|
106
|
+
return { source: "uncertain" };
|
|
107
|
+
}
|
|
108
|
+
/** Read-only matching. Results never modify settings, connect MCP, or authorize an operation. */
|
|
109
|
+
export async function recommendPiResources(input) {
|
|
110
|
+
const resources = [];
|
|
111
|
+
const task = resourceTask(input.prompt, input.catalog);
|
|
112
|
+
let calls = 0;
|
|
113
|
+
for (const kind of ["skills", "mcpServers"]) {
|
|
114
|
+
for (const id of input.candidates[kind]) {
|
|
115
|
+
if (input.signal?.aborted)
|
|
116
|
+
throw new DecisionError("CANCELLED", "推荐已取消。", 499);
|
|
117
|
+
const item = input.catalog[kind].find((resource) => resource.id === id);
|
|
118
|
+
if (!item)
|
|
119
|
+
throw new DecisionError("RESOURCE_MISSING", "候选资源已移除,请重新打开设置面板。");
|
|
120
|
+
const mention = explicitMention(input.prompt, item.name);
|
|
121
|
+
if (mention) {
|
|
122
|
+
resources.push({ id, kind, status: mention === "included" ? "recommended" : "unmatched", source: "explicit" });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// Transport type is not a description of a server's capabilities. Never connect for discovery.
|
|
126
|
+
if (!task || kind === "mcpServers" || item.description.length > 1000) {
|
|
127
|
+
resources.push({ id, kind, status: "unassessed", source: "local" });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
let result;
|
|
131
|
+
try {
|
|
132
|
+
calls++;
|
|
133
|
+
const shortlist = await input.evaluate({ state: task, questions: shortlistQuestions(item.name) }, input.caller, input.signal);
|
|
134
|
+
const forward = matchProbability(shortlist, "forward");
|
|
135
|
+
const reverse = matchProbability(shortlist, "reverse");
|
|
136
|
+
if (forward < 0.8 || reverse < 0.8) {
|
|
137
|
+
resources.push({ id, kind, status: forward <= 0.2 && reverse <= 0.2 ? "unmatched" : "uncertain", source: "local" });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (input.signal?.aborted)
|
|
141
|
+
throw new DecisionError("CANCELLED", "推荐已取消。", 499);
|
|
142
|
+
// A name match alone cannot load a skill. Confirm against its complete description.
|
|
143
|
+
calls++;
|
|
144
|
+
result = await input.evaluate({
|
|
145
|
+
state: { task, resource: { name: item.name, description: item.description } },
|
|
146
|
+
questions: { relevant: { type: "noul", instructions: "Does this skill match the task?" } },
|
|
147
|
+
}, input.caller, input.signal);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof DecisionError && ["CONTEXT_LIMIT", "QUESTION_LIMIT", "OPTIONS_COLLAPSED"].includes(error.code)) {
|
|
151
|
+
resources.push({ id, kind, status: "unassessed", source: "local" });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
const relevant = result.answers.relevant?.noul;
|
|
157
|
+
if (typeof relevant !== "number" || !Number.isFinite(relevant) || relevant < 0 || relevant > 1) {
|
|
158
|
+
throw new DecisionError("INVALID_RESULT", "本地模型返回无效判断,原选择未改变。", 502);
|
|
159
|
+
}
|
|
160
|
+
const status = relevant >= 0.8 ? "recommended" : relevant <= 0.2 ? "unmatched" : "uncertain";
|
|
161
|
+
resources.push({ id, kind, status, source: "local" });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const selection = { skills: [], mcpServers: [] };
|
|
165
|
+
for (const resource of resources)
|
|
166
|
+
if (resource.status === "recommended")
|
|
167
|
+
selection[resource.kind].push(resource.id);
|
|
168
|
+
return { selection, resources, calls, experimental: true };
|
|
169
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PiResourceSelection } from "./pi-session-settings.js";
|
|
2
|
+
export interface PreparedPiResources {
|
|
3
|
+
args: string[];
|
|
4
|
+
env: NodeJS.ProcessEnv;
|
|
5
|
+
close(): void;
|
|
6
|
+
}
|
|
7
|
+
/** Re-resolve at execution time: removed resources fail, never silently enable defaults. */
|
|
8
|
+
export declare function preparePiResources(agentDir: string, cwd: string, selection: PiResourceSelection | undefined, baseEnv: NodeJS.ProcessEnv, codemodeOverride?: "off" | "on" | "only"): Promise<PreparedPiResources>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, chmodSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { discoverPiResources, resolvePiResourceSelection } from "./pi-resource-catalog.js";
|
|
6
|
+
/** Re-resolve at execution time: removed resources fail, never silently enable defaults. */
|
|
7
|
+
export async function preparePiResources(agentDir, cwd, selection, baseEnv, codemodeOverride) {
|
|
8
|
+
const selected = selection ? resolvePiResourceSelection(await discoverPiResources({ harness: { engine: "cli", agentDir } }, cwd), selection) : undefined;
|
|
9
|
+
const privateDir = mkdtempSync(path.join(os.tmpdir(), "wand-pi-resources-"));
|
|
10
|
+
chmodSync(privateDir, 0o700);
|
|
11
|
+
try {
|
|
12
|
+
const file = path.join(privateDir, "selection.json");
|
|
13
|
+
writeFileSync(file, JSON.stringify({ agentDir, codemodeOverride,
|
|
14
|
+
...(selected ? { mcpServers: selected.servers.map((server) => server.name) } : {}) }), { mode: 0o600 });
|
|
15
|
+
const js = fileURLToPath(new URL("./pi-resource-bridge.js", import.meta.url));
|
|
16
|
+
const bridge = existsSync(js) ? js : js.replace(/\.js$/, ".ts");
|
|
17
|
+
return {
|
|
18
|
+
args: [...(selected ? ["--no-skills", ...selected.skills.flatMap((skill) => ["--skill", skill.filePath])] : []),
|
|
19
|
+
"--extension", bridge],
|
|
20
|
+
env: { ...baseEnv, WAND_PI_RESOURCE_POLICY: file },
|
|
21
|
+
close: () => rmSync(privateDir, { recursive: true, force: true }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
rmSync(privateDir, { recursive: true, force: true });
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/** Shared, data-only contract for per-session Pi settings. */
|
|
2
|
+
export declare const PI_BUILTIN_TOOLS: readonly ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
3
|
+
export type PiBuiltinTool = typeof PI_BUILTIN_TOOLS[number];
|
|
4
|
+
/**
|
|
5
|
+
* pi-goal 注册的工具名。Pi 的 `--tools` 是白名单(会替换默认集合),所以 CLI 会话要开启目标模式
|
|
6
|
+
* 必须逐个点名;名字与 `core-extension-host.ts` 的排除列表保持同一份。
|
|
7
|
+
*/
|
|
8
|
+
export declare const PI_GOAL_TOOLS: readonly ["create_goal", "get_goal", "update_goal"];
|
|
9
|
+
export interface PiSessionSettings {
|
|
10
|
+
codemode: "off" | "on" | "only";
|
|
11
|
+
/** Optional per-session CLI override; omitted means Pi's own CodeMode setting. */
|
|
12
|
+
codemodeOverride?: "off" | "on" | "only";
|
|
13
|
+
globalTools: boolean;
|
|
14
|
+
goalMode: boolean;
|
|
15
|
+
tools: PiBuiltinTool[];
|
|
16
|
+
localDecision: boolean;
|
|
17
|
+
autoCompaction: boolean;
|
|
18
|
+
/** Omitted on legacy sessions = existing Pi discovery; present = exact, opt-in resources. */
|
|
19
|
+
resources?: PiResourceSelection;
|
|
20
|
+
/** Skill IDs whose current on/off state automatic selection must preserve. Manual edits remain allowed. */
|
|
21
|
+
lockedSkills?: string[];
|
|
22
|
+
/** Explicit opt-in: select unlocked skills per prompt; preserve skill locks and manual MCP selections. */
|
|
23
|
+
autoResources?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export type PiSessionSettingsPatch = Omit<Partial<PiSessionSettings>, "codemodeOverride"> & {
|
|
26
|
+
codemodeOverride?: "off" | "on" | "only" | null;
|
|
27
|
+
};
|
|
28
|
+
export interface PiResourceSelection {
|
|
29
|
+
skills: string[];
|
|
30
|
+
mcpServers: string[];
|
|
31
|
+
}
|
|
32
|
+
export type PiSkillMode = "off" | "on" | "locked";
|
|
33
|
+
export declare function piSkillMode(settings: PiSessionSettings, id: string): PiSkillMode;
|
|
34
|
+
/** A single manual gesture changes selection and lock atomically, never mutating its snapshot. */
|
|
35
|
+
export declare function piSkillModePatch(settings: PiSessionSettings, id: string, mode: PiSkillMode): PiSessionSettingsPatch;
|
|
36
|
+
export interface PiResourceSelectionNotice {
|
|
37
|
+
status: "selecting" | "selected" | "fallback" | "cancelled";
|
|
38
|
+
label: string;
|
|
39
|
+
skills: string[];
|
|
40
|
+
mcpServers: string[];
|
|
41
|
+
codemode?: {
|
|
42
|
+
mode: "off" | "on" | "only" | "follow";
|
|
43
|
+
source: "manual" | "automatic" | "uncertain" | "fallback";
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export interface PiResourceItem {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
source: string;
|
|
51
|
+
}
|
|
52
|
+
export interface PiResourceRecommendation {
|
|
53
|
+
selection: PiResourceSelection;
|
|
54
|
+
resources: Array<{
|
|
55
|
+
id: string;
|
|
56
|
+
kind: "skills" | "mcpServers";
|
|
57
|
+
status: "recommended" | "unmatched" | "uncertain" | "unassessed";
|
|
58
|
+
source: "explicit" | "local";
|
|
59
|
+
}>;
|
|
60
|
+
calls: number;
|
|
61
|
+
experimental: true;
|
|
62
|
+
}
|
|
63
|
+
export interface PiResourceCatalog {
|
|
64
|
+
skills: PiResourceItem[];
|
|
65
|
+
mcpServers: PiResourceItem[];
|
|
66
|
+
/** Listing does not connect to any server or evaluate credentials. */
|
|
67
|
+
supported: boolean;
|
|
68
|
+
reason: string;
|
|
69
|
+
}
|
|
70
|
+
export declare function patchPiResourceSelection(raw: unknown): PiResourceSelection;
|
|
71
|
+
export declare function defaultPiSessionSettings(autoCompaction?: boolean): PiSessionSettings;
|
|
72
|
+
/**
|
|
73
|
+
* CLI 会话的出厂默认:完全跟随 Pi 自身配置(默认工具 + 已安装的全局扩展 + codemode),
|
|
74
|
+
* 与直接在终端里跑 `pi` 一致——用户已经装好的子代理、待办、CodeMode 不该因为从 Wand 启动就消失。
|
|
75
|
+
* 想逐项挑选工具时再关掉「全局扩展工具」,会话就切到 Wand 托管的 `--tools` 白名单。
|
|
76
|
+
* 进程内 SDK 会话仍用 `defaultPiSessionSettings`:员工会话不静默继承用户的全局扩展。
|
|
77
|
+
*/
|
|
78
|
+
export declare function defaultPiCliSessionSettings(autoCompaction?: boolean): PiSessionSettings;
|
|
79
|
+
/** Strict partial updates: an invalid/unknown field never silently enables a capability. */
|
|
80
|
+
export declare function patchPiSessionSettings(current: PiSessionSettings, raw: unknown): PiSessionSettings;
|
|
81
|
+
/**
|
|
82
|
+
* CLI 会话的工具选择策略。
|
|
83
|
+
*
|
|
84
|
+
* Pi CLI 的 `--tools` 是**白名单**:一旦给出,未列出的工具(含扩展工具、codemode、目标工具)
|
|
85
|
+
* 都不在注册表里;不给出时才用 Pi 自身的 `defaultTools` + 全部扩展工具。因此只有两条真实可表达的路径:
|
|
86
|
+
*
|
|
87
|
+
* - `allowlist`:Wand 逐项接管——只启用勾选的基础工具,CodeMode / 目标模式按开关把工具名追加进白名单。
|
|
88
|
+
* - `config`:`globalTools` 打开时完全跟随 Pi 自身配置(默认工具 + 已安装的全局扩展),
|
|
89
|
+
* 逐项开关在该模式下不参与启动参数,UI 会如实说明而不是伪造生效。
|
|
90
|
+
*/
|
|
91
|
+
export interface PiToolSelection {
|
|
92
|
+
mode: "allowlist" | "config";
|
|
93
|
+
names: string[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `codemode: "only"`(连声明都藏起来、只从脚本调用)由 Pi 的 `codemode.mode` 设置决定,
|
|
97
|
+
* 没有对应的 CLI 开关:CLI 会话一律按「启用」处理,读取时也归一到 `on`,不谎报模式。
|
|
98
|
+
*/
|
|
99
|
+
export declare function cliCodemode(codemode: PiSessionSettings["codemode"]): "off" | "on";
|
|
100
|
+
/** 会话设置在该引擎下真正生效的形态(目前只有 CLI 的 CodeMode 需要归一)。 */
|
|
101
|
+
export declare function effectivePiSessionSettings(settings: PiSessionSettings, engine: "core" | "cli"): PiSessionSettings;
|
|
102
|
+
export declare function piToolSelection(settings: PiSessionSettings): PiToolSelection;
|
|
103
|
+
export interface PiSettingsResponse {
|
|
104
|
+
settings: PiSessionSettings;
|
|
105
|
+
available: boolean;
|
|
106
|
+
reason: string;
|
|
107
|
+
engine: "core" | "cli";
|
|
108
|
+
/** CLI Pi 支持的基础工具 allowlist;core/SDK 专属开关仍由 available 表示。 */
|
|
109
|
+
toolsAvailable: boolean;
|
|
110
|
+
goalAvailable: boolean;
|
|
111
|
+
globalExtensions: string[];
|
|
112
|
+
localDecisionAvailable: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* 逐项可用性由服务端裁决;客户端只按这里禁用,不再自己推断引擎。
|
|
115
|
+
* 早期服务端不带这个字段,客户端必须用 `piSettingsControls()` 退回它当时的真实能力,
|
|
116
|
+
* 不能假装开关可用(写了也不会生效)。
|
|
117
|
+
*/
|
|
118
|
+
controls?: PiSettingsControls;
|
|
119
|
+
/** Absent on older servers. Never contains file paths, URLs, commands, env or credentials. */
|
|
120
|
+
resourceCatalog?: PiResourceCatalog;
|
|
121
|
+
/** Read-only recommendation; applying remains a separate, explicit settings update. */
|
|
122
|
+
recommendationAvailable?: boolean;
|
|
123
|
+
recommendationReason?: string;
|
|
124
|
+
autoResourcesAvailable?: boolean;
|
|
125
|
+
autoResourcesReason?: string;
|
|
126
|
+
/** Older automatic-resource servers do not infer CodeMode. */
|
|
127
|
+
autoCodemodeAvailable?: boolean;
|
|
128
|
+
/** Absent on older servers; clients must not pretend skill locks are supported. */
|
|
129
|
+
skillLocksAvailable?: boolean;
|
|
130
|
+
}
|
|
131
|
+
export interface PiSettingsControls {
|
|
132
|
+
tools: boolean;
|
|
133
|
+
codemode: boolean;
|
|
134
|
+
/** 是否提供「仅 CodeMode」(只有进程内 SDK 会话能表达)。 */
|
|
135
|
+
codemodeOnly: boolean;
|
|
136
|
+
/** Per-turn bridge supports exact off/on/only, independent of globalTools. */
|
|
137
|
+
codemodeOverride?: boolean;
|
|
138
|
+
globalTools: boolean;
|
|
139
|
+
goalMode: boolean;
|
|
140
|
+
localDecision: boolean;
|
|
141
|
+
autoCompaction: boolean;
|
|
142
|
+
}
|
|
143
|
+
export declare const NO_PI_SETTINGS_CONTROLS: PiSettingsControls;
|
|
144
|
+
/**
|
|
145
|
+
* 面板逐项可用性。新服务端直接给 `controls`;没有这个字段的旧服务端按它当时的语义退回:
|
|
146
|
+
* SDK 原生开关看 `available`,CLI 只开放基础工具列表,其余一律不可改。
|
|
147
|
+
*/
|
|
148
|
+
export declare function piSettingsControls(response: Partial<PiSettingsResponse> | null | undefined): PiSettingsControls;
|
|
149
|
+
/** CLI 会话能表达的设置字段;其余字段只对进程内 SDK 会话有意义。 */
|
|
150
|
+
export declare const PI_CLI_SETTING_KEYS: readonly ["tools", "codemode", "globalTools", "goalMode", "localDecision", "resources", "lockedSkills", "codemodeOverride", "autoResources"];
|
|
151
|
+
/**
|
|
152
|
+
* CLI 会话的写入裁决:无法表达的字段/取值直接拒绝,不能存下一条永远不会生效的设置。
|
|
153
|
+
* 返回拒绝原因,允许时返回 null。
|
|
154
|
+
*/
|
|
155
|
+
export declare function cliPiSettingsRejection(raw: unknown, next: PiSessionSettings): string | null;
|
|
156
|
+
/** Only settings commands, never paths, extension commands, or ordinary slash-prefixed text. */
|
|
157
|
+
export declare function isPiSettingsDraft(text: string): boolean;
|