@piagent/platform 1.2.0 → 1.2.2
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 +149 -1
- package/README.md +1 -1
- package/catalog/capabilities.json +4 -4
- package/docs/command-reference-vietnamese.md +15 -4
- package/docs/context-window-policy.md +1 -0
- package/docs/distribution-standard.md +7 -7
- package/docs/mcp-and-tools.md +49 -3
- package/docs/operator-manual-vietnamese.md +52 -11
- package/docs/publishing-for-teams.md +2 -0
- package/docs/quickstart-vietnamese.md +4 -2
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +4 -3
- package/docs/team-onboarding.md +3 -3
- package/docs/usage-observability.md +49 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/mcp-approval.html +54 -2
- package/docs-site/content/quickstart.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +55 -3
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +2 -2
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/package.json +1 -1
- package/packages/piagent-core/README.md +2 -0
- package/packages/piagent-core/capabilities/capability-core.js +41 -4
- package/packages/piagent-core/capabilities/capability-sources.js +73 -12
- package/packages/piagent-core/extensions/piagent-guard.ts +539 -70
- package/packages/piagent-core/extensions/policy-core.js +1586 -98
- package/packages/piagent-core/mcp/mcp-approval-store.js +56 -15
- package/packages/piagent-core/mcp/mcp-auth-readiness.js +31 -3
- package/packages/piagent-core/mcp/mcp-command-actions.js +139 -30
- package/packages/piagent-core/mcp/mcp-config-layers.js +339 -6
- package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
- package/packages/piagent-core/mcp/mcp-server-entry.js +51 -0
- package/packages/piagent-core/mcp/mcp-session-view.js +55 -4
- package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/prompts/be-to-fe.md +1 -0
- package/packages/piagent-core/prompts/piagent-commands.md +1 -0
- package/packages/piagent-core/prompts/platform-improve.md +1 -0
- package/packages/piagent-core/prompts/review.md +1 -0
- package/packages/piagent-core/prompts/scout.md +1 -0
- package/packages/piagent-core/prompts/task.md +1 -0
- package/scripts/configure-model-scope.sh +19 -3
- package/scripts/configure-subagents.sh +17 -4
- package/scripts/init-project.sh +15 -8
- package/scripts/mcp-manage.mjs +111 -22
- package/scripts/pi-model-catalog.sh +11 -1
- package/scripts/pi-session-stats.sh +27 -1
- package/scripts/pi-usage-history.mjs +567 -0
- package/scripts/quality-benchmark.sh +37 -9
- package/scripts/setup.sh +21 -13
- package/scripts/uninstall-global.sh +10 -4
- package/scripts/verify-local.sh +19 -0
- package/scripts/verify-release-identity.mjs +29 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
extractShellGlobCandidates,
|
|
13
13
|
extractShellPathCandidates,
|
|
14
14
|
findProtectedPathInCommand,
|
|
15
|
+
unresolvedPathExpansions,
|
|
15
16
|
globMatchesPath,
|
|
16
17
|
matchesProtectedPath,
|
|
17
18
|
normalizePathCandidate,
|
|
@@ -42,7 +43,14 @@ import {
|
|
|
42
43
|
} from "./redaction-core.js";
|
|
43
44
|
import { detectProfileName } from "./project-shape.js";
|
|
44
45
|
import { evaluateUpdateCheck, isInstalledPlatform, readUpdateCache, startUpdateProbe } from "./update-check.js";
|
|
45
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
REPOSITORY_SCOPES,
|
|
48
|
+
collectServers,
|
|
49
|
+
configPathForScope,
|
|
50
|
+
mcpDecisionInputs,
|
|
51
|
+
unverifiableMcpConfig
|
|
52
|
+
} from "../mcp/mcp-config-layers.js";
|
|
53
|
+
import { attributeDirectTool } from "../mcp/mcp-tool-naming.js";
|
|
46
54
|
import { approvalState } from "../mcp/mcp-approval-store.js";
|
|
47
55
|
import { evaluateServerReadiness, readinessNotice } from "../mcp/mcp-auth-readiness.js";
|
|
48
56
|
import * as mcpActions from "../mcp/mcp-command-actions.js";
|
|
@@ -120,6 +128,13 @@ const BOILERPLATE_COLLAPSE_CHARS = 300;
|
|
|
120
128
|
const MAX_INLINE_COLLAPSED_TASK_CHARS = 2200;
|
|
121
129
|
const MAX_CHAT_IMAGE_ATTACHMENTS = 4;
|
|
122
130
|
const MAX_CHAT_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
131
|
+
const TOOL_RESULT_COMPACT_CHAR_THRESHOLD = 12_000;
|
|
132
|
+
const TOOL_RESULT_COMPACT_LINE_THRESHOLD = 180;
|
|
133
|
+
const TOOL_RESULT_PREVIEW_HEAD_LINES = 24;
|
|
134
|
+
const TOOL_RESULT_PREVIEW_TAIL_LINES = 24;
|
|
135
|
+
const TOOL_RESULT_PREVIEW_INTERESTING_LINES = 24;
|
|
136
|
+
const TOOL_RESULT_PREVIEW_MAX_CHARS = 6_000;
|
|
137
|
+
const TOOL_RESULT_CAPTURE_MAX_CHARS = 500_000;
|
|
123
138
|
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "bmp"] as const;
|
|
124
139
|
const ORCHESTRATION_MODES = ["solo-first", "bounded-subagents", "parallel-readonly"] as const;
|
|
125
140
|
const REVIEW_LENSES = ["correctness", "tests", "scope", "security", "docs", "release", "package"] as const;
|
|
@@ -1480,6 +1495,14 @@ function observedBashLedgerPath(cwd: string): string {
|
|
|
1480
1495
|
return path.join(stateRoot(cwd), "observed-bash.jsonl");
|
|
1481
1496
|
}
|
|
1482
1497
|
|
|
1498
|
+
function toolResultCaptureRoot(cwd: string): string {
|
|
1499
|
+
return path.join(stateRoot(cwd), "tool-results");
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function toolResultCaptureIndexPath(cwd: string): string {
|
|
1503
|
+
return path.join(toolResultCaptureRoot(cwd), "index.jsonl");
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1483
1506
|
function safeTaskId(taskId: string): string {
|
|
1484
1507
|
return taskId.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "task";
|
|
1485
1508
|
}
|
|
@@ -1994,16 +2017,20 @@ function prepareToolInputForPolicy(
|
|
|
1994
2017
|
}
|
|
1995
2018
|
|
|
1996
2019
|
// Which MCP server a tool call is addressed to. The adapter exposes one proxy
|
|
1997
|
-
// tool named `mcp` that carries the server in `input.server`, and,
|
|
1998
|
-
//
|
|
1999
|
-
//
|
|
2000
|
-
//
|
|
2001
|
-
|
|
2020
|
+
// tool named `mcp` that carries the server in `input.server`, and, under
|
|
2021
|
+
// directTools, one tool per server tool named `<prefix>_<tool>` where the prefix
|
|
2022
|
+
// is derived from the server name. Direct names are matched against the servers
|
|
2023
|
+
// this gate is holding rather than against a fixed pattern, because the prefix
|
|
2024
|
+
// is the server's own name and there is nothing constant to anchor on.
|
|
2025
|
+
function mcpServerFromToolCall(
|
|
2026
|
+
toolName: string,
|
|
2027
|
+
input: Record<string, unknown>,
|
|
2028
|
+
candidates: Iterable<string>
|
|
2029
|
+
): string | undefined {
|
|
2002
2030
|
if (normalizeActionToken(toolName) === "mcp") {
|
|
2003
2031
|
return typeof input.server === "string" && input.server.trim() ? input.server.trim() : undefined;
|
|
2004
2032
|
}
|
|
2005
|
-
|
|
2006
|
-
return direct?.[1];
|
|
2033
|
+
return attributeDirectTool(toolName, candidates);
|
|
2007
2034
|
}
|
|
2008
2035
|
|
|
2009
2036
|
// Servers a repository defines are not usable until somebody on this machine has
|
|
@@ -2014,25 +2041,71 @@ function mcpServerFromToolCall(toolName: string, input: Record<string, unknown>)
|
|
|
2014
2041
|
//
|
|
2015
2042
|
// Recomputed only when one of the files behind the decision changes, because
|
|
2016
2043
|
// this runs on every tool call.
|
|
2017
|
-
|
|
2044
|
+
interface RepositoryMcpGate {
|
|
2045
|
+
blocked: Map<string, { state: string; origin: string }>;
|
|
2046
|
+
// Conditions under which no tool call can be checked at all, each with the
|
|
2047
|
+
// sentence to show. These are not "a server is unapproved" — they are "this
|
|
2048
|
+
// repository's config has put the gate in a state where it cannot tell which
|
|
2049
|
+
// server a call belongs to", and the only sound answer to that is to stop.
|
|
2050
|
+
unverifiable: string[];
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
const mcpApprovalCache = new Map<string, { signature: string; gate: RepositoryMcpGate }>();
|
|
2018
2054
|
|
|
2019
|
-
function
|
|
2055
|
+
function repositoryMcpGate(cwd: string): RepositoryMcpGate {
|
|
2056
|
+
// Repository scopes plus anything an `imports` key drags in from a file the
|
|
2057
|
+
// clone carries. Collecting those here is the whole point: a server the gate
|
|
2058
|
+
// never enumerates is a server it silently permits.
|
|
2020
2059
|
const servers = collectServers({ projectPath: cwd, scopes: [...REPOSITORY_SCOPES] });
|
|
2060
|
+
const repositoryFiles = [...REPOSITORY_SCOPES].map((scope) => ({
|
|
2061
|
+
scope,
|
|
2062
|
+
file: configPathForScope(scope, { projectPath: cwd })
|
|
2063
|
+
}));
|
|
2064
|
+
// Every file the decision reads, listed by the module that reads them rather
|
|
2065
|
+
// than restated here. Building this by hand is what went wrong: the scan grew
|
|
2066
|
+
// to read merged settings from all four scopes and to stat import targets
|
|
2067
|
+
// outside the repository, the hand-written list did not, and an already-loaded
|
|
2068
|
+
// guard went on permitting calls after a personal global config had put the
|
|
2069
|
+
// session in the state this gate exists to refuse.
|
|
2021
2070
|
const signature = [
|
|
2022
|
-
...servers.map((server) => `${server.
|
|
2071
|
+
...servers.map((server) => `${server.origin}:${server.name}:${fileSignature(server.file)}`),
|
|
2072
|
+
...repositoryFiles.map((layer) => `layer:${layer.scope}:${fileSignature(layer.file)}`),
|
|
2073
|
+
...mcpDecisionInputs({ projectPath: cwd }).map((file) => `input:${file}:${fileSignature(file)}`),
|
|
2023
2074
|
`store:${fileSignature(path.join(os.homedir(), ".pi", "piagent-mcp-approvals.json"))}`
|
|
2024
2075
|
].join("|");
|
|
2025
2076
|
const cached = mcpApprovalCache.get(cwd);
|
|
2026
|
-
if (cached?.signature === signature) return cached.
|
|
2077
|
+
if (cached?.signature === signature) return cached.gate;
|
|
2027
2078
|
|
|
2028
|
-
const blocked = new Map<string, string>();
|
|
2079
|
+
const blocked = new Map<string, { state: string; origin: string }>();
|
|
2029
2080
|
for (const server of servers) {
|
|
2030
2081
|
const state = approvalState({ projectPath: cwd, name: server.name, entry: server.entry });
|
|
2031
2082
|
if (state.state === "approved") continue;
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2083
|
+
// A name defined more than once keeps its blocking entry: the approved copy
|
|
2084
|
+
// does not vouch for the one nobody has looked at.
|
|
2085
|
+
blocked.set(server.name, { state: state.state, origin: server.origin });
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Two things a repository-carried config can do that leave nothing to check.
|
|
2089
|
+
//
|
|
2090
|
+
// It can ask for tools with no prefix, so a direct tool arrives under a bare
|
|
2091
|
+
// name carrying no evidence of its server. Refusing only the proxy there was
|
|
2092
|
+
// not a fix: the proxy is the one form that names its server, so blocking it
|
|
2093
|
+
// and allowing the bare names left the hole exactly where it was.
|
|
2094
|
+
//
|
|
2095
|
+
// It can also import a kind whose config this platform cannot enumerate, and
|
|
2096
|
+
// then the set of servers reaching the session is simply unknown. Blocking the
|
|
2097
|
+
// servers that could be listed says nothing about the ones that could not.
|
|
2098
|
+
//
|
|
2099
|
+
// Both stop every tool call. A repository can make a session refuse to run;
|
|
2100
|
+
// it must not be able to make one run a server nobody approved.
|
|
2101
|
+
// Read from the shared module rather than decided here, so `piagent-mcp
|
|
2102
|
+
// doctor` reports exactly what this refuses. The two disagreeing is how an
|
|
2103
|
+
// operator ends up reading "PASS" while every tool call is being stopped.
|
|
2104
|
+
const unverifiable = unverifiableMcpConfig({ projectPath: cwd }).map((problem) => problem.detail);
|
|
2105
|
+
|
|
2106
|
+
const gate = { blocked, unverifiable };
|
|
2107
|
+
mcpApprovalCache.set(cwd, { signature, gate });
|
|
2108
|
+
return gate;
|
|
2036
2109
|
}
|
|
2037
2110
|
|
|
2038
2111
|
function fileSignature(file: string): string {
|
|
@@ -2049,15 +2122,26 @@ function evaluateMcpApproval(
|
|
|
2049
2122
|
toolName: string,
|
|
2050
2123
|
input: Record<string, unknown>
|
|
2051
2124
|
): { block: boolean; reason?: string } {
|
|
2052
|
-
const
|
|
2125
|
+
const gate = repositoryMcpGate(cwd);
|
|
2126
|
+
|
|
2127
|
+
// Every tool call, not only the ones that look like MCP. Under either of these
|
|
2128
|
+
// conditions a call cannot be traced to a server, so there is no such thing as
|
|
2129
|
+
// a call this gate can clear.
|
|
2130
|
+
if (gate.unverifiable.length > 0) {
|
|
2131
|
+
return { block: true, reason: `Blocked every tool call: ${gate.unverifiable.join(" ")}` };
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
const server = mcpServerFromToolCall(toolName, input, gate.blocked.keys());
|
|
2053
2135
|
if (!server) return { block: false };
|
|
2054
|
-
const
|
|
2055
|
-
if (!
|
|
2056
|
-
const explanation = state === "rejected"
|
|
2136
|
+
const record = gate.blocked.get(server);
|
|
2137
|
+
if (!record) return { block: false };
|
|
2138
|
+
const explanation = record.state === "rejected"
|
|
2057
2139
|
? "it was rejected for this project"
|
|
2058
|
-
: state === "changed"
|
|
2140
|
+
: record.state === "changed"
|
|
2059
2141
|
? "its definition changed since it was approved"
|
|
2060
|
-
: "
|
|
2142
|
+
: record.origin.startsWith("import:")
|
|
2143
|
+
? `this repository imports it from ${record.origin.slice("import:".length)} config and nobody on this machine has approved it here`
|
|
2144
|
+
: "this repository defines it and nobody on this machine has approved it";
|
|
2061
2145
|
return {
|
|
2062
2146
|
block: true,
|
|
2063
2147
|
// Named as commands, because this is read inside a session: the reader can
|
|
@@ -2069,6 +2153,18 @@ function evaluateMcpApproval(
|
|
|
2069
2153
|
};
|
|
2070
2154
|
}
|
|
2071
2155
|
|
|
2156
|
+
// A word whose final path segment glues literal text onto an expansion --
|
|
2157
|
+
// `.en$(echo v)`, `${D}.env` -- names a file this process cannot know without
|
|
2158
|
+
// running the substitution. Matching the literal half against the protected
|
|
2159
|
+
// patterns answers a different question than the one being asked, so refuse
|
|
2160
|
+
// rather than let an unknown filename through unchecked.
|
|
2161
|
+
function unresolvedExpansionReason(subject: string, words: string[]): string {
|
|
2162
|
+
const listed = words.map((word) => `\`${word}\``).join(", ");
|
|
2163
|
+
return `${subject} builds a filename this guard cannot resolve: ${listed}. `
|
|
2164
|
+
+ "The literal text around the expansion makes it a path, but its value is only known at run time, "
|
|
2165
|
+
+ "so it cannot be checked against the protected paths. Write the path out, or put the expansion in its own argument.";
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2072
2168
|
function evaluateMcpProxyShellProtectedAccess(
|
|
2073
2169
|
cwd: string,
|
|
2074
2170
|
prepared: PreparedToolInput,
|
|
@@ -2112,6 +2208,10 @@ function evaluateMcpProxyShellProtectedAccess(
|
|
|
2112
2208
|
reason: `MCP command resolves to protected path: ${resolvedProtectedHit.candidate} resolves to ${resolvedProtectedHit.resolved} matching ${resolvedProtectedHit.pattern}`
|
|
2113
2209
|
};
|
|
2114
2210
|
}
|
|
2211
|
+
const unresolved = protectedPaths.length > 0 ? unresolvedPathExpansions(command) : [];
|
|
2212
|
+
if (unresolved.length > 0) {
|
|
2213
|
+
return { block: true, reason: unresolvedExpansionReason("MCP command", unresolved) };
|
|
2214
|
+
}
|
|
2115
2215
|
return { block: false };
|
|
2116
2216
|
}
|
|
2117
2217
|
|
|
@@ -3284,6 +3384,321 @@ function modelLabel(ctx: ExtensionContext): string {
|
|
|
3284
3384
|
return model.name ?? model.id ?? "unknown";
|
|
3285
3385
|
}
|
|
3286
3386
|
|
|
3387
|
+
function currentSessionName(ctx: ExtensionContext): string {
|
|
3388
|
+
try {
|
|
3389
|
+
return String(ctx.sessionManager.getSessionName() ?? "").trim();
|
|
3390
|
+
} catch {
|
|
3391
|
+
return "";
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
function hasOperatorSessionName(name: string): boolean {
|
|
3396
|
+
const normalized = name.trim().toLowerCase();
|
|
3397
|
+
return Boolean(normalized && normalized !== "session");
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
function cleanSessionNameInput(input: string): string {
|
|
3401
|
+
let name = input.trim();
|
|
3402
|
+
if ((name.startsWith("\"") && name.endsWith("\"")) || (name.startsWith("'") && name.endsWith("'"))) {
|
|
3403
|
+
name = name.slice(1, -1).trim();
|
|
3404
|
+
}
|
|
3405
|
+
return name;
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
type ToolResultCaptureSummary = {
|
|
3409
|
+
path?: string;
|
|
3410
|
+
error?: string;
|
|
3411
|
+
source: string;
|
|
3412
|
+
toolName: string;
|
|
3413
|
+
originalChars: number;
|
|
3414
|
+
originalLines: number;
|
|
3415
|
+
previewChars?: number;
|
|
3416
|
+
storedChars?: number;
|
|
3417
|
+
storedTruncated?: boolean;
|
|
3418
|
+
sha256: string;
|
|
3419
|
+
};
|
|
3420
|
+
|
|
3421
|
+
function normalizeToolResultText(text: string): string {
|
|
3422
|
+
return text.replace(/\r\n/g, "\n");
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
function toolResultLineCount(text: string): number {
|
|
3426
|
+
if (!text) return 0;
|
|
3427
|
+
return normalizeToolResultText(text).split("\n").length;
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
function shouldCompactToolResultText(text: string): boolean {
|
|
3431
|
+
return text.length > TOOL_RESULT_COMPACT_CHAR_THRESHOLD
|
|
3432
|
+
|| toolResultLineCount(text) > TOOL_RESULT_COMPACT_LINE_THRESHOLD;
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
function clipPreviewLine(line: string, maxChars = 260): string {
|
|
3436
|
+
if (line.length <= maxChars) return line;
|
|
3437
|
+
return `${line.slice(0, maxChars).trimEnd()} ... [line clipped]`;
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
function boundedPreview(text: string): string {
|
|
3441
|
+
if (text.length <= TOOL_RESULT_PREVIEW_MAX_CHARS) return text;
|
|
3442
|
+
const marker = "\n[Piagent preview shortened further to stay light.]\n";
|
|
3443
|
+
const edge = Math.floor((TOOL_RESULT_PREVIEW_MAX_CHARS - marker.length) / 2);
|
|
3444
|
+
return `${text.slice(0, edge).trimEnd()}${marker}${text.slice(-edge).trimStart()}`;
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
function interestingToolResultLines(lines: string[]): string[] {
|
|
3448
|
+
const interesting = /\b(?:error|errors|err!|failed?|failure|warning|warn|exception|traceback|assert(?:ion)?|panic|fatal|timeout|timed out|denied|not found|cannot|eacces|enoent|ts\d{3,5}|err_[a-z0-9_]+)\b/i;
|
|
3449
|
+
const seen = new Set<string>();
|
|
3450
|
+
const result: string[] = [];
|
|
3451
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3452
|
+
const line = lines[index];
|
|
3453
|
+
if (!interesting.test(line)) continue;
|
|
3454
|
+
const rendered = `${index + 1}: ${clipPreviewLine(line)}`;
|
|
3455
|
+
if (seen.has(rendered)) continue;
|
|
3456
|
+
seen.add(rendered);
|
|
3457
|
+
result.push(rendered);
|
|
3458
|
+
if (result.length >= TOOL_RESULT_PREVIEW_INTERESTING_LINES) break;
|
|
3459
|
+
}
|
|
3460
|
+
return result;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
function storedToolResultText(text: string): { text: string; truncated: boolean } {
|
|
3464
|
+
if (text.length <= TOOL_RESULT_CAPTURE_MAX_CHARS) return { text, truncated: false };
|
|
3465
|
+
const marker = `\n\n[Piagent capture truncated: omitted ${formatCount(text.length - TOOL_RESULT_CAPTURE_MAX_CHARS)} chars from the middle.]\n\n`;
|
|
3466
|
+
const edge = Math.floor((TOOL_RESULT_CAPTURE_MAX_CHARS - marker.length) / 2);
|
|
3467
|
+
return {
|
|
3468
|
+
text: `${text.slice(0, edge).trimEnd()}${marker}${text.slice(-edge).trimStart()}`,
|
|
3469
|
+
truncated: true
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
function compactToolResultPreview(toolName: string, source: string, text: string, capture: ToolResultCaptureSummary): string {
|
|
3474
|
+
const normalized = normalizeToolResultText(text);
|
|
3475
|
+
const lines = normalized.split("\n");
|
|
3476
|
+
const head = lines.slice(0, TOOL_RESULT_PREVIEW_HEAD_LINES).map((line) => clipPreviewLine(line));
|
|
3477
|
+
const tailStart = Math.max(TOOL_RESULT_PREVIEW_HEAD_LINES, lines.length - TOOL_RESULT_PREVIEW_TAIL_LINES);
|
|
3478
|
+
const tail = lines.slice(tailStart).map((line) => clipPreviewLine(line));
|
|
3479
|
+
const omitted = Math.max(0, lines.length - head.length - tail.length);
|
|
3480
|
+
const interesting = interestingToolResultLines(lines);
|
|
3481
|
+
const captureLine = capture.path
|
|
3482
|
+
? `capture: ${capture.path}${capture.storedTruncated ? " (stored head/tail sample; original too large)" : ""}`
|
|
3483
|
+
: `capture: unavailable (${capture.error ?? "write failed"})`;
|
|
3484
|
+
const sections = [
|
|
3485
|
+
`[Piagent compacted large ${toolName} output from ${source}: ${formatCount(capture.originalChars)} chars / ${formatCount(capture.originalLines)} lines.]`,
|
|
3486
|
+
`[${captureLine}]`,
|
|
3487
|
+
"[Preview keeps head, notable lines, and tail.]",
|
|
3488
|
+
"",
|
|
3489
|
+
"head:",
|
|
3490
|
+
...head
|
|
3491
|
+
];
|
|
3492
|
+
if (interesting.length > 0) {
|
|
3493
|
+
sections.push("", "notable:", ...interesting);
|
|
3494
|
+
}
|
|
3495
|
+
if (omitted > 0) {
|
|
3496
|
+
sections.push("", `... ${formatCount(omitted)} middle line${omitted === 1 ? "" : "s"} omitted ...`);
|
|
3497
|
+
}
|
|
3498
|
+
sections.push("", "tail:", ...tail);
|
|
3499
|
+
return boundedPreview(sections.join("\n"));
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
function maybeNumber(value: unknown): number | undefined {
|
|
3503
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3504
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number.parseInt(value, 10);
|
|
3505
|
+
return undefined;
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
function currentSessionField(ctx: ExtensionContext, field: "getSessionFile" | "getSessionId" | "getSessionName"): string | undefined {
|
|
3509
|
+
try {
|
|
3510
|
+
const value = ctx.sessionManager[field]?.();
|
|
3511
|
+
return value === undefined || value === null ? undefined : redactText(String(value));
|
|
3512
|
+
} catch {
|
|
3513
|
+
return undefined;
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
function writeToolResultCapture(
|
|
3518
|
+
cwd: string,
|
|
3519
|
+
event: any,
|
|
3520
|
+
ctx: ExtensionContext,
|
|
3521
|
+
source: string,
|
|
3522
|
+
text: string,
|
|
3523
|
+
cache: Map<string, ToolResultCaptureSummary>
|
|
3524
|
+
): ToolResultCaptureSummary {
|
|
3525
|
+
const normalized = normalizeToolResultText(text);
|
|
3526
|
+
const sha256 = crypto.createHash("sha256").update(normalized).digest("hex");
|
|
3527
|
+
const originalLines = toolResultLineCount(normalized);
|
|
3528
|
+
const cached = cache.get(sha256);
|
|
3529
|
+
if (cached?.path) return { ...cached, source };
|
|
3530
|
+
|
|
3531
|
+
const toolName = String(event?.toolName ?? "tool");
|
|
3532
|
+
const recordedAt = nowIso();
|
|
3533
|
+
const date = recordedAt.slice(0, 10);
|
|
3534
|
+
const filename = `${recordedAt.replace(/[:.]/g, "-")}-${slugify(toolName)}-${sha256.slice(0, 12)}.log`;
|
|
3535
|
+
const relativePath = [".pi", "piagent-state", "tool-results", date, filename].join("/");
|
|
3536
|
+
const absolutePath = path.join(cwd, ".pi", "piagent-state", "tool-results", date, filename);
|
|
3537
|
+
const capture = storedToolResultText(normalized);
|
|
3538
|
+
const details = event?.details && typeof event.details === "object" ? event.details : {};
|
|
3539
|
+
const exitCode = maybeNumber(details.exitCode ?? details.status ?? event?.exitCode);
|
|
3540
|
+
const metadata = {
|
|
3541
|
+
schemaVersion: 1,
|
|
3542
|
+
recordedAt,
|
|
3543
|
+
cwd: redactText(cwd),
|
|
3544
|
+
sessionId: currentSessionField(ctx, "getSessionId"),
|
|
3545
|
+
sessionName: currentSessionField(ctx, "getSessionName"),
|
|
3546
|
+
sessionFile: currentSessionField(ctx, "getSessionFile"),
|
|
3547
|
+
toolName,
|
|
3548
|
+
source,
|
|
3549
|
+
isError: event?.isError === true,
|
|
3550
|
+
exitCode,
|
|
3551
|
+
input: redactForStorage(event?.input),
|
|
3552
|
+
path: relativePath,
|
|
3553
|
+
originalChars: normalized.length,
|
|
3554
|
+
originalLines,
|
|
3555
|
+
storedChars: capture.text.length,
|
|
3556
|
+
storedTruncated: capture.truncated,
|
|
3557
|
+
sha256
|
|
3558
|
+
};
|
|
3559
|
+
|
|
3560
|
+
try {
|
|
3561
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
3562
|
+
fs.writeFileSync(absolutePath, [
|
|
3563
|
+
"# Piagent compacted tool result capture",
|
|
3564
|
+
JSON.stringify(metadata),
|
|
3565
|
+
"---",
|
|
3566
|
+
capture.text
|
|
3567
|
+
].join("\n"));
|
|
3568
|
+
fs.appendFileSync(toolResultCaptureIndexPath(cwd), `${JSON.stringify(metadata)}\n`);
|
|
3569
|
+
const summary = {
|
|
3570
|
+
path: relativePath,
|
|
3571
|
+
source,
|
|
3572
|
+
toolName,
|
|
3573
|
+
originalChars: normalized.length,
|
|
3574
|
+
originalLines,
|
|
3575
|
+
storedChars: capture.text.length,
|
|
3576
|
+
storedTruncated: capture.truncated,
|
|
3577
|
+
sha256
|
|
3578
|
+
};
|
|
3579
|
+
cache.set(sha256, summary);
|
|
3580
|
+
return summary;
|
|
3581
|
+
} catch (error) {
|
|
3582
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3583
|
+
const summary = {
|
|
3584
|
+
error: message,
|
|
3585
|
+
source,
|
|
3586
|
+
toolName,
|
|
3587
|
+
originalChars: normalized.length,
|
|
3588
|
+
originalLines,
|
|
3589
|
+
sha256
|
|
3590
|
+
};
|
|
3591
|
+
cache.set(sha256, summary);
|
|
3592
|
+
return summary;
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
function compactToolResultTextContent(
|
|
3597
|
+
cwd: string,
|
|
3598
|
+
event: any,
|
|
3599
|
+
ctx: ExtensionContext,
|
|
3600
|
+
content: unknown,
|
|
3601
|
+
cache: Map<string, ToolResultCaptureSummary>
|
|
3602
|
+
): { content: unknown; captures: ToolResultCaptureSummary[] } {
|
|
3603
|
+
if (!Array.isArray(content)) return { content, captures: [] };
|
|
3604
|
+
const captures: ToolResultCaptureSummary[] = [];
|
|
3605
|
+
const compacted = content.map((block, index) => {
|
|
3606
|
+
if (!block || typeof block !== "object") return block;
|
|
3607
|
+
const typed = block as { type?: unknown; text?: unknown };
|
|
3608
|
+
if (typed.type !== "text" || typeof typed.text !== "string" || !shouldCompactToolResultText(typed.text)) return block;
|
|
3609
|
+
const source = `content[${index}].text`;
|
|
3610
|
+
const capture = writeToolResultCapture(cwd, event, ctx, source, typed.text, cache);
|
|
3611
|
+
captures.push(capture);
|
|
3612
|
+
return {
|
|
3613
|
+
...block,
|
|
3614
|
+
text: compactToolResultPreview(String(event?.toolName ?? "tool"), source, typed.text, capture)
|
|
3615
|
+
};
|
|
3616
|
+
});
|
|
3617
|
+
return { content: compacted, captures };
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
function compactToolResultDetails(
|
|
3621
|
+
cwd: string,
|
|
3622
|
+
event: any,
|
|
3623
|
+
ctx: ExtensionContext,
|
|
3624
|
+
value: unknown,
|
|
3625
|
+
cache: Map<string, ToolResultCaptureSummary>,
|
|
3626
|
+
captures: ToolResultCaptureSummary[],
|
|
3627
|
+
source = "details",
|
|
3628
|
+
depth = 0
|
|
3629
|
+
): unknown {
|
|
3630
|
+
if (typeof value === "string") {
|
|
3631
|
+
if (!shouldCompactToolResultText(value)) return value;
|
|
3632
|
+
const capture = writeToolResultCapture(cwd, event, ctx, source, value, cache);
|
|
3633
|
+
captures.push(capture);
|
|
3634
|
+
return compactToolResultPreview(String(event?.toolName ?? "tool"), source, value, capture);
|
|
3635
|
+
}
|
|
3636
|
+
if (Array.isArray(value)) {
|
|
3637
|
+
if (depth >= 6) return value;
|
|
3638
|
+
return value.map((item, index) => compactToolResultDetails(cwd, event, ctx, item, cache, captures, `${source}[${index}]`, depth + 1));
|
|
3639
|
+
}
|
|
3640
|
+
if (isPlainRecord(value)) {
|
|
3641
|
+
if (depth >= 6) return value;
|
|
3642
|
+
return Object.fromEntries(
|
|
3643
|
+
Object.entries(value).map(([key, item]) => [
|
|
3644
|
+
key,
|
|
3645
|
+
compactToolResultDetails(cwd, event, ctx, item, cache, captures, `${source}.${key}`, depth + 1)
|
|
3646
|
+
])
|
|
3647
|
+
);
|
|
3648
|
+
}
|
|
3649
|
+
return value;
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
function attachToolResultCompactionDetails(details: unknown, captures: ToolResultCaptureSummary[]): unknown {
|
|
3653
|
+
if (captures.length === 0) return details;
|
|
3654
|
+
const compacted = captures.map((capture) => ({
|
|
3655
|
+
path: capture.path,
|
|
3656
|
+
error: capture.error,
|
|
3657
|
+
source: capture.source,
|
|
3658
|
+
toolName: capture.toolName,
|
|
3659
|
+
originalChars: capture.originalChars,
|
|
3660
|
+
originalLines: capture.originalLines,
|
|
3661
|
+
storedChars: capture.storedChars,
|
|
3662
|
+
storedTruncated: capture.storedTruncated,
|
|
3663
|
+
sha256: capture.sha256
|
|
3664
|
+
}));
|
|
3665
|
+
if (isPlainRecord(details)) return { ...details, piagentCompactedToolResults: compacted };
|
|
3666
|
+
if (details === undefined) return { piagentCompactedToolResults: compacted };
|
|
3667
|
+
return { value: details, piagentCompactedToolResults: compacted };
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
function readRecentToolResultCaptures(cwd: string, limit = 5): Record<string, unknown>[] {
|
|
3671
|
+
const indexPath = toolResultCaptureIndexPath(cwd);
|
|
3672
|
+
if (!fs.existsSync(indexPath)) return [];
|
|
3673
|
+
const lines = fs.readFileSync(indexPath, "utf8").trim().split(/\n/).filter(Boolean);
|
|
3674
|
+
const captures: Record<string, unknown>[] = [];
|
|
3675
|
+
for (const line of lines.slice(-Math.max(limit * 3, limit))) {
|
|
3676
|
+
try {
|
|
3677
|
+
const parsed = JSON.parse(line);
|
|
3678
|
+
if (isPlainRecord(parsed)) captures.push(redactForStorage(parsed) as Record<string, unknown>);
|
|
3679
|
+
} catch {
|
|
3680
|
+
// A partial JSONL line should not make a status command noisy or unusable.
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
return captures.slice(-limit);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
function formatToolResultCaptureStatus(cwd: string, captures: Record<string, unknown>[]): string {
|
|
3687
|
+
const root = normalizeRelative(cwd, toolResultCaptureRoot(cwd)) ?? ".pi/piagent-state/tool-results";
|
|
3688
|
+
const lines = [
|
|
3689
|
+
`logPolicy: compact above ${formatCount(TOOL_RESULT_COMPACT_CHAR_THRESHOLD)} chars or ${formatCount(TOOL_RESULT_COMPACT_LINE_THRESHOLD)} lines`,
|
|
3690
|
+
`captureRoot: ${root}`,
|
|
3691
|
+
`recent: ${captures.length || "none"}`
|
|
3692
|
+
];
|
|
3693
|
+
for (const capture of captures) {
|
|
3694
|
+
const originalLines = typeof capture.originalLines === "number" ? formatCount(capture.originalLines) : "unknown";
|
|
3695
|
+
const originalChars = typeof capture.originalChars === "number" ? formatCount(capture.originalChars) : "unknown";
|
|
3696
|
+
const exit = capture.exitCode === undefined ? "" : ` exit=${capture.exitCode}`;
|
|
3697
|
+
lines.push(`- ${capture.recordedAt ?? "unknown"} ${capture.toolName ?? "tool"}${exit}: ${originalLines} lines / ${originalChars} chars -> ${capture.path ?? "no capture path"}`);
|
|
3698
|
+
}
|
|
3699
|
+
return lines.join("\n");
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3287
3702
|
function buildUsageSnapshot(ctx: ExtensionContext, thinkingLevel?: string): UsageSnapshot {
|
|
3288
3703
|
const contextUsage = ctx.getContextUsage();
|
|
3289
3704
|
const contextWithThinking = ctx as ExtensionContext & { getThinkingLevel?: () => string };
|
|
@@ -3310,7 +3725,8 @@ function buildUsageSnapshot(ctx: ExtensionContext, thinkingLevel?: string): Usag
|
|
|
3310
3725
|
availableInCommand: false,
|
|
3311
3726
|
howToRead: [
|
|
3312
3727
|
"Inside Pi TUI: run /session for exact tokens and cost.",
|
|
3313
|
-
"Outside Pi: run piagent-usage /path/to/project or scripts/pi-session-stats.sh /path/to/project."
|
|
3728
|
+
"Outside Pi: run piagent-usage /path/to/project or scripts/pi-session-stats.sh /path/to/project.",
|
|
3729
|
+
"Historical totals: run piagent-usage --history /path/to/project --days 7."
|
|
3314
3730
|
]
|
|
3315
3731
|
}
|
|
3316
3732
|
};
|
|
@@ -3321,24 +3737,13 @@ function formatUsageSnapshot(snapshot: UsageSnapshot): string {
|
|
|
3321
3737
|
? `${formatCount(snapshot.contextUsage.tokens)} / ${formatCount(snapshot.contextUsage.contextWindow)} tokens (${formatPercent(snapshot.contextUsage.percent)})`
|
|
3322
3738
|
: "unavailable";
|
|
3323
3739
|
return [
|
|
3324
|
-
|
|
3325
|
-
""
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
`- Thinking: ${snapshot.thinkingLevel}`,
|
|
3332
|
-
`- Entries: ${formatCount(snapshot.entries.branch)} on active branch / ${formatCount(snapshot.entries.total)} total`,
|
|
3333
|
-
`- Live context: ${context}`,
|
|
3334
|
-
"",
|
|
3335
|
-
"Exact billed input/output/cache/cost totals are exposed by Pi via `/session` or RPC `get_session_stats`, not directly inside this command context.",
|
|
3336
|
-
"",
|
|
3337
|
-
"From another terminal:",
|
|
3338
|
-
"",
|
|
3339
|
-
"```bash",
|
|
3340
|
-
"piagent-usage /path/to/project",
|
|
3341
|
-
"```"
|
|
3740
|
+
`usage: ${context}`,
|
|
3741
|
+
`session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
|
|
3742
|
+
`model: ${snapshot.model}; thinking: ${snapshot.thinkingLevel}`,
|
|
3743
|
+
`entries: ${formatCount(snapshot.entries.branch)} active / ${formatCount(snapshot.entries.total)} total`,
|
|
3744
|
+
`file: ${snapshot.sessionFile ?? "not persisted"}`,
|
|
3745
|
+
"exact: /session | piagent-usage /path/to/project",
|
|
3746
|
+
"history: piagent-usage --history /path/to/project --days 7"
|
|
3342
3747
|
].join("\n");
|
|
3343
3748
|
}
|
|
3344
3749
|
|
|
@@ -3405,30 +3810,14 @@ function formatContextPreflight(preflight: ContextPreflight, snapshot: UsageSnap
|
|
|
3405
3810
|
? `${formatCount(preflight.projectedContext.tokens)} (${formatPercent(preflight.projectedContext.percent)})`
|
|
3406
3811
|
: "unavailable";
|
|
3407
3812
|
return [
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
`- Incoming input estimate: ${formatCount(preflight.inputTokenEstimate)} tokens from ${formatCount(preflight.inputChars)} chars`,
|
|
3417
|
-
`- Projected context: ${projected}`,
|
|
3418
|
-
`- Recommendation: ${preflight.recommendation}`,
|
|
3419
|
-
`- Reason: ${preflight.reason}`,
|
|
3420
|
-
"",
|
|
3421
|
-
"Commands:",
|
|
3422
|
-
"",
|
|
3423
|
-
"```text",
|
|
3424
|
-
...preflight.commands,
|
|
3425
|
-
"```",
|
|
3426
|
-
"",
|
|
3427
|
-
"Notes:",
|
|
3428
|
-
"",
|
|
3429
|
-
"- Do not paste the full mandatory flow into every task. Platform prompts and piagent tools already carry it.",
|
|
3430
|
-
"- Use `/scout` for read-only risk mapping. Use `/fresh-scout` when the current session is already heavy.",
|
|
3431
|
-
"- If exact billed token/cost totals are needed, run `/session` in Pi or `piagent-usage <project-path>` from another terminal."
|
|
3813
|
+
`preflight: ${preflight.recommendation}`,
|
|
3814
|
+
`workflow: ${preflight.workflow}`,
|
|
3815
|
+
`session: ${snapshot.sessionName ?? "unnamed"} (${snapshot.sessionId ?? "unknown"})`,
|
|
3816
|
+
`model: ${snapshot.model}; thinking: ${snapshot.thinkingLevel}`,
|
|
3817
|
+
`context: ${live}; projected: ${projected}`,
|
|
3818
|
+
`input: ~${formatCount(preflight.inputTokenEstimate)} tokens from ${formatCount(preflight.inputChars)} chars`,
|
|
3819
|
+
`reason: ${preflight.reason}`,
|
|
3820
|
+
`next: ${preflight.commands.join(" | ")}`
|
|
3432
3821
|
].join("\n");
|
|
3433
3822
|
}
|
|
3434
3823
|
|
|
@@ -3496,7 +3885,7 @@ function isPiagentWorkflowInput(text: string): boolean {
|
|
|
3496
3885
|
}
|
|
3497
3886
|
|
|
3498
3887
|
function isFreshOrUtilityInput(text: string): boolean {
|
|
3499
|
-
return /^\/(?:fresh-task|fresh-scout|fresh-be-to-fe|task-preflight|piagent-usage|session|compact)\b/i.test(text.trim());
|
|
3888
|
+
return /^\/(?:fresh-task|fresh-scout|fresh-be-to-fe|task-preflight|piagent-usage|piagent-logs|setname|session|compact)\b/i.test(text.trim());
|
|
3500
3889
|
}
|
|
3501
3890
|
|
|
3502
3891
|
function taskInboxDir(cwd: string): string {
|
|
@@ -3823,7 +4212,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
3823
4212
|
const explicitProfile = Boolean(process.env.PIAGENT_PROFILE?.trim());
|
|
3824
4213
|
const profile = loadProfile(ctx.cwd, projectTrusted);
|
|
3825
4214
|
const name = profile.displayName || profile.projectId || path.basename(ctx.cwd);
|
|
3826
|
-
|
|
4215
|
+
const operatorSessionName = currentSessionName(ctx);
|
|
4216
|
+
if (!hasOperatorSessionName(operatorSessionName)) {
|
|
4217
|
+
pi.setSessionName(`pi:${name}`);
|
|
4218
|
+
}
|
|
3827
4219
|
const profileHint = explicitProfile || (projectTrusted && fs.existsSync(projectProfilePath(ctx.cwd)))
|
|
3828
4220
|
? ""
|
|
3829
4221
|
: " (run /onboard-project to select a profile)";
|
|
@@ -3965,6 +4357,24 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
3965
4357
|
resultChanged = true;
|
|
3966
4358
|
}
|
|
3967
4359
|
|
|
4360
|
+
const captureCache = new Map<string, ToolResultCaptureSummary>();
|
|
4361
|
+
const compactedContent = compactToolResultTextContent(ctx.cwd, event, ctx, resultContent, captureCache);
|
|
4362
|
+
const compactionCaptures = [...compactedContent.captures];
|
|
4363
|
+
if (compactedContent.captures.length > 0) {
|
|
4364
|
+
resultContent = compactedContent.content;
|
|
4365
|
+
resultChanged = true;
|
|
4366
|
+
}
|
|
4367
|
+
if (resultDetails !== undefined) {
|
|
4368
|
+
const compactedDetails = compactToolResultDetails(ctx.cwd, event, ctx, resultDetails, captureCache, compactionCaptures);
|
|
4369
|
+
if (compactionCaptures.length > compactedContent.captures.length) {
|
|
4370
|
+
resultDetails = compactedDetails;
|
|
4371
|
+
resultChanged = true;
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
if (compactionCaptures.length > 0) {
|
|
4375
|
+
resultDetails = attachToolResultCompactionDetails(resultDetails, compactionCaptures);
|
|
4376
|
+
}
|
|
4377
|
+
|
|
3968
4378
|
if (resultChanged) {
|
|
3969
4379
|
return resultDetails === undefined
|
|
3970
4380
|
? { content: resultContent }
|
|
@@ -4047,6 +4457,12 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4047
4457
|
reason: `Command resolves to protected path: ${resolvedProtectedHit.candidate} resolves to ${resolvedProtectedHit.resolved} matching ${resolvedProtectedHit.pattern}`
|
|
4048
4458
|
};
|
|
4049
4459
|
}
|
|
4460
|
+
const unresolvedExpansions = pathPolicy.shellProtectedPaths.length > 0
|
|
4461
|
+
? unresolvedPathExpansions(command)
|
|
4462
|
+
: [];
|
|
4463
|
+
if (unresolvedExpansions.length > 0) {
|
|
4464
|
+
return { block: true, reason: unresolvedExpansionReason("Command", unresolvedExpansions) };
|
|
4465
|
+
}
|
|
4050
4466
|
|
|
4051
4467
|
const confirmationReasons = execDecision.mode !== "off" && execDecision.decision === "prompt"
|
|
4052
4468
|
? [...execDecision.reasons]
|
|
@@ -4263,8 +4679,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4263
4679
|
}
|
|
4264
4680
|
});
|
|
4265
4681
|
|
|
4266
|
-
|
|
4267
|
-
|
|
4682
|
+
pi.registerTool({
|
|
4683
|
+
name: "piagent_exec_policy_check",
|
|
4268
4684
|
label: "Piagent Exec Policy Check",
|
|
4269
4685
|
description: "Evaluate a shell command against piagent exec policy before running it.",
|
|
4270
4686
|
promptSnippet: "Use this before high-impact, complex, generated, or unfamiliar shell commands.",
|
|
@@ -6147,6 +6563,59 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
6147
6563
|
}
|
|
6148
6564
|
});
|
|
6149
6565
|
|
|
6566
|
+
pi.registerCommand("piagent-logs", {
|
|
6567
|
+
description: "Show compact log policy and recent captured large tool outputs",
|
|
6568
|
+
handler: async (_args, ctx) => {
|
|
6569
|
+
const captures = readRecentToolResultCaptures(ctx.cwd, 5);
|
|
6570
|
+
ctx.ui.notify(`Piagent logs: ${captures.length ? `${captures.length} recent capture(s)` : "no compacted captures yet"}`, "info");
|
|
6571
|
+
pi.sendMessage(
|
|
6572
|
+
{
|
|
6573
|
+
customType: "piagent-log-captures",
|
|
6574
|
+
content: formatToolResultCaptureStatus(ctx.cwd, captures),
|
|
6575
|
+
display: true,
|
|
6576
|
+
details: {
|
|
6577
|
+
policy: {
|
|
6578
|
+
compactAboveChars: TOOL_RESULT_COMPACT_CHAR_THRESHOLD,
|
|
6579
|
+
compactAboveLines: TOOL_RESULT_COMPACT_LINE_THRESHOLD,
|
|
6580
|
+
previewMaxChars: TOOL_RESULT_PREVIEW_MAX_CHARS,
|
|
6581
|
+
captureMaxChars: TOOL_RESULT_CAPTURE_MAX_CHARS
|
|
6582
|
+
},
|
|
6583
|
+
captures
|
|
6584
|
+
}
|
|
6585
|
+
},
|
|
6586
|
+
{ triggerTurn: false }
|
|
6587
|
+
);
|
|
6588
|
+
}
|
|
6589
|
+
});
|
|
6590
|
+
|
|
6591
|
+
pi.registerCommand("setname", {
|
|
6592
|
+
description: "Set the current Pi session name for reports and resume lists",
|
|
6593
|
+
handler: async (args, ctx) => {
|
|
6594
|
+
const name = cleanSessionNameInput(String(args ?? ""));
|
|
6595
|
+
if (!name) {
|
|
6596
|
+
ctx.ui.notify("Usage: /setname <task/session name>", "warning");
|
|
6597
|
+
return;
|
|
6598
|
+
}
|
|
6599
|
+
const previousName = currentSessionName(ctx);
|
|
6600
|
+
pi.setSessionName(name);
|
|
6601
|
+
appendSessionTrace(pi, {
|
|
6602
|
+
event: "session_name_set",
|
|
6603
|
+
previousName: previousName || undefined,
|
|
6604
|
+
sessionName: name
|
|
6605
|
+
});
|
|
6606
|
+
ctx.ui.notify(`Session name set: ${name}`, "info");
|
|
6607
|
+
pi.sendMessage(
|
|
6608
|
+
{
|
|
6609
|
+
customType: "piagent-session-name-set",
|
|
6610
|
+
content: `sessionName: ${name}`,
|
|
6611
|
+
display: true,
|
|
6612
|
+
details: { sessionName: name, previousName: previousName || undefined }
|
|
6613
|
+
},
|
|
6614
|
+
{ triggerTurn: false }
|
|
6615
|
+
);
|
|
6616
|
+
}
|
|
6617
|
+
});
|
|
6618
|
+
|
|
6150
6619
|
pi.registerCommand("task-preflight", {
|
|
6151
6620
|
description: "Check context health before a large task; optionally compact",
|
|
6152
6621
|
handler: async (args, ctx) => {
|