privateer-agent 0.12.19 → 0.12.21
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/package.json +1 -1
- package/patches/@earendil-works+pi-coding-agent+0.84.1.patch +321 -9
- package/src/config/moat.ts +115 -22
- package/src/engine/errors.ts +7 -0
- package/src/mcp/catalog.ts +41 -0
- package/src/providers/phala/aci-verifier/VENDORED.md +54 -30
- package/src/providers/phala/aci-verifier/crypto.ts +35 -27
- package/src/providers/phala/aci-verifier/digest.ts +31 -95
- package/src/providers/phala/aci-verifier/e2ee-channel.ts +3 -1
- package/src/providers/phala/aci-verifier/errors.ts +2 -21
- package/src/providers/phala/aci-verifier/index.ts +29 -35
- package/src/providers/phala/aci-verifier/jcs.ts +8 -3
- package/src/providers/phala/aci-verifier/receipt.ts +96 -88
- package/src/providers/phala/aci-verifier/report.ts +62 -85
- package/src/providers/phala/aci-verifier/session.ts +40 -0
- package/src/providers/phala/aci-verifier/types.ts +90 -80
- package/src/providers/phalaSeal.ts +8 -9
- package/src/providers/phala/reportBinding.ts +0 -193
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.21",
|
|
4
4
|
"description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,7 +68,7 @@ index 4600b23..075ecae 100644
|
|
|
68
68
|
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
|
69
69
|
export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
|
|
70
70
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
|
|
71
|
-
index ce8a9a2..
|
|
71
|
+
index ce8a9a2..b7b339e 100644
|
|
72
72
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
|
|
73
73
|
+++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
|
|
74
74
|
@@ -38,6 +38,70 @@ import { createLocalBashOperations } from "./tools/bash.js";
|
|
@@ -174,7 +174,7 @@ index ce8a9a2..c52ea80 100644
|
|
|
174
174
|
// Emit to extensions first
|
|
175
175
|
await this._emitExtensionEvent(event);
|
|
176
176
|
// Notify all listeners
|
|
177
|
-
@@ -772,6 +854,
|
|
177
|
+
@@ -772,6 +854,25 @@ export class AgentSession {
|
|
178
178
|
finalError: msg.errorMessage,
|
|
179
179
|
});
|
|
180
180
|
this._retryAttempt = 0;
|
|
@@ -186,11 +186,21 @@ index ce8a9a2..c52ea80 100644
|
|
|
186
186
|
+ // That re-entry is what produced the endless "retrying 1/3…2/3…3/3" loop
|
|
187
187
|
+ // on a persistently-failing provider/tool. Context-overflow errors never
|
|
188
188
|
+ // reach this branch (_isRetryableError → false), so compaction is untouched.
|
|
189
|
+
+ return false;
|
|
190
|
+
+ }
|
|
191
|
+
+ // Privateer patch: a first-attempt hard 4xx (a WAF 403, a 401, a 404) is also
|
|
192
|
+
+ // terminal. Stock Pi still falls through to compaction here because _retryAttempt
|
|
193
|
+
+ // is 0, and _checkCompaction will summarise a session that already has usage.
|
|
194
|
+
+ // That summary is another LLM call to the SAME blocked endpoint; retryAssistantCall
|
|
195
|
+
+ // classifies the raw HTML body as transient (it contains "500"/"502"), so the
|
|
196
|
+
+ // agent looks hung — retrying a compaction that can never succeed — until the
|
|
197
|
+
+ // summarizer budget burns. Context overflow never matches isHardHttpFailure.
|
|
198
|
+
+ if (msg.stopReason === "error" && isHardHttpFailure(msg.errorMessage)) {
|
|
189
199
|
+ return false;
|
|
190
200
|
}
|
|
191
201
|
if (await this._checkCompaction(msg)) {
|
|
192
202
|
return true;
|
|
193
|
-
@@ -852,6 +
|
|
203
|
+
@@ -852,6 +953,14 @@ export class AgentSession {
|
|
194
204
|
if (!hasConfiguredAuth) {
|
|
195
205
|
const isOAuth = this._modelRuntime.isUsingOAuth(this.model.provider);
|
|
196
206
|
if (isOAuth) {
|
|
@@ -205,7 +215,7 @@ index ce8a9a2..c52ea80 100644
|
|
|
205
215
|
throw new Error(`Authentication failed for "${this.model.provider}". ` +
|
|
206
216
|
`Credentials may have expired or network is unavailable. ` +
|
|
207
217
|
`Run '/login ${this.model.provider}' to re-authenticate.`);
|
|
208
|
-
@@ -2084,6 +
|
|
218
|
+
@@ -2084,6 +2193,27 @@ export class AgentSession {
|
|
209
219
|
// Context overflow is handled by compaction, not retry.
|
|
210
220
|
if (isContextOverflow(message, this.model?.contextWindow ?? 0))
|
|
211
221
|
return false;
|
|
@@ -1052,17 +1062,19 @@ index 6f7ed6a..b0ed0ae 100644
|
|
|
1052
1062
|
getResourcePattern(item) {
|
|
1053
1063
|
const scope = item.metadata.scope;
|
|
1054
1064
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
|
|
1055
|
-
index 1d9f046..
|
|
1065
|
+
index 1d9f046..c0326e3 100644
|
|
1056
1066
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
|
|
1057
1067
|
+++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
|
|
1058
|
-
@@ -1,
|
|
1068
|
+
@@ -1,7 +1,6 @@
|
|
1059
1069
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
1060
1070
|
-import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
1061
1071
|
+import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
1062
1072
|
import { areExperimentalFeaturesEnabled } from "../../../core/experimental.js";
|
|
1063
|
-
|
|
1073
|
+
-import { addUsageToTotals, createUsageTotals } from "../../../core/usage-totals.js";
|
|
1064
1074
|
import { theme } from "../theme/theme.js";
|
|
1065
|
-
|
|
1075
|
+
/**
|
|
1076
|
+
* Sanitize text for display in a single-line status.
|
|
1077
|
+
@@ -14,6 +13,43 @@ function sanitizeStatusText(text) {
|
|
1066
1078
|
.replace(/ +/g, " ")
|
|
1067
1079
|
.trim();
|
|
1068
1080
|
}
|
|
@@ -1106,7 +1118,182 @@ index 1d9f046..2a1fba8 100644
|
|
|
1106
1118
|
/**
|
|
1107
1119
|
* Format token counts for compact footer display.
|
|
1108
1120
|
*/
|
|
1109
|
-
@@ -
|
|
1121
|
+
@@ -41,8 +77,11 @@ export function formatCwdForFooter(cwd, home) {
|
|
1122
|
+
return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
- * Footer component that shows pwd, token stats, and context usage.
|
|
1126
|
+
- * Computes token/context stats from session, gets git branch and extension statuses from provider.
|
|
1127
|
+
+ * Footer component that shows pwd (with git branch) and the active model.
|
|
1128
|
+
+ *
|
|
1129
|
+
+ * Privateer trimmed this: upstream also drew a second row of cumulative token
|
|
1130
|
+
+ * counters, cache-hit rate, cost and a context gauge. Gets git branch and
|
|
1131
|
+
+ * extension statuses from provider.
|
|
1132
|
+
*/
|
|
1133
|
+
export class FooterComponent {
|
|
1134
|
+
autoCompactEnabled = true;
|
|
1135
|
+
@@ -74,29 +113,9 @@ export class FooterComponent {
|
|
1136
|
+
}
|
|
1137
|
+
render(width) {
|
|
1138
|
+
const state = this.session.state;
|
|
1139
|
+
- // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
|
|
1140
|
+
- const usageTotals = createUsageTotals();
|
|
1141
|
+
- let latestCacheHitRate;
|
|
1142
|
+
- for (const entry of this.session.sessionManager.getEntries()) {
|
|
1143
|
+
- if (entry.type === "message" && entry.message.role === "assistant") {
|
|
1144
|
+
- addUsageToTotals(usageTotals, entry.message.usage);
|
|
1145
|
+
- const latestPromptTokens = entry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;
|
|
1146
|
+
- latestCacheHitRate =
|
|
1147
|
+
- latestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;
|
|
1148
|
+
- }
|
|
1149
|
+
- else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.usage) {
|
|
1150
|
+
- addUsageToTotals(usageTotals, entry.message.usage);
|
|
1151
|
+
- }
|
|
1152
|
+
- else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
|
|
1153
|
+
- addUsageToTotals(usageTotals, entry.usage);
|
|
1154
|
+
- }
|
|
1155
|
+
- }
|
|
1156
|
+
- // Calculate context usage from session (handles compaction correctly).
|
|
1157
|
+
- // After compaction, tokens are unknown until the next LLM response.
|
|
1158
|
+
- const contextUsage = this.session.getContextUsage();
|
|
1159
|
+
- const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
1160
|
+
- const contextPercentValue = contextUsage?.percent ?? 0;
|
|
1161
|
+
- const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
|
1162
|
+
+ // Privateer: no token counters, cache-hit rate, cost or context gauge in the
|
|
1163
|
+
+ // footer — so the accumulation that fed them is gone too. Upstream walked EVERY
|
|
1164
|
+
+ // session entry on EVERY render to build numbers we no longer draw.
|
|
1165
|
+
// Replace home directory with ~
|
|
1166
|
+
let pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
|
|
1167
|
+
// Add git branch if available
|
|
1168
|
+
@@ -109,56 +128,24 @@ export class FooterComponent {
|
|
1169
|
+
if (sessionName) {
|
|
1170
|
+
pwd = `${pwd} • ${sessionName}`;
|
|
1171
|
+
}
|
|
1172
|
+
- // Build stats line
|
|
1173
|
+
- const statsParts = [];
|
|
1174
|
+
- if (usageTotals.input)
|
|
1175
|
+
- statsParts.push(`↑${formatTokens(usageTotals.input)}`);
|
|
1176
|
+
- if (usageTotals.output)
|
|
1177
|
+
- statsParts.push(`↓${formatTokens(usageTotals.output)}`);
|
|
1178
|
+
- if (usageTotals.cacheRead)
|
|
1179
|
+
- statsParts.push(`R${formatTokens(usageTotals.cacheRead)}`);
|
|
1180
|
+
- if (usageTotals.cacheWrite)
|
|
1181
|
+
- statsParts.push(`W${formatTokens(usageTotals.cacheWrite)}`);
|
|
1182
|
+
- if ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {
|
|
1183
|
+
- statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
|
|
1184
|
+
- }
|
|
1185
|
+
- // Kimi Coding is subscription-backed despite using API-key authentication.
|
|
1186
|
+
- const usingSubscription = state.model
|
|
1187
|
+
- ? state.model.provider === "kimi-coding" || this.session.modelRuntime.isUsingSubscription(state.model.provider)
|
|
1188
|
+
- : false;
|
|
1189
|
+
- if (usageTotals.cost || usingSubscription) {
|
|
1190
|
+
- const costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
|
|
1191
|
+
- statsParts.push(costStr);
|
|
1192
|
+
- }
|
|
1193
|
+
- // Colorize context percentage based on usage
|
|
1194
|
+
- let contextPercentStr;
|
|
1195
|
+
- const autoIndicator = this.autoCompactEnabled ? " (auto)" : "";
|
|
1196
|
+
- const contextPercentDisplay = contextPercent === "?"
|
|
1197
|
+
- ? `?/${formatTokens(contextWindow)}${autoIndicator}`
|
|
1198
|
+
- : `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
1199
|
+
- if (contextPercentValue > 90) {
|
|
1200
|
+
- contextPercentStr = theme.fg("error", contextPercentDisplay);
|
|
1201
|
+
- }
|
|
1202
|
+
- else if (contextPercentValue > 70) {
|
|
1203
|
+
- contextPercentStr = theme.fg("warning", contextPercentDisplay);
|
|
1204
|
+
- }
|
|
1205
|
+
- else {
|
|
1206
|
+
- contextPercentStr = contextPercentDisplay;
|
|
1207
|
+
- }
|
|
1208
|
+
- statsParts.push(contextPercentStr);
|
|
1209
|
+
- if (areExperimentalFeaturesEnabled()) {
|
|
1210
|
+
- statsParts.push(`${theme.fg("dim", "•")} ${theme.bold(theme.fg("warning", "xp"))}`);
|
|
1211
|
+
- }
|
|
1212
|
+
- let statsLeft = statsParts.join(" ");
|
|
1213
|
+
+ // Privateer: ONE footer row — the working directory on the left, the model on
|
|
1214
|
+
+ // the right. Upstream spent a second row on cumulative token counters, cache-hit
|
|
1215
|
+
+ // rate and cost; those are session trivia that never change what you do next, and
|
|
1216
|
+
+ // they made the busiest part of the screen the least useful. The `xp` flag stays:
|
|
1217
|
+
+ // it warns that experimental features are live, which is not trivia.
|
|
1218
|
+
+ const experimentalFlag = areExperimentalFeaturesEnabled()
|
|
1219
|
+
+ ? ` ${theme.fg("dim", "•")} ${theme.bold(theme.fg("warning", "xp"))}`
|
|
1220
|
+
+ : "";
|
|
1221
|
+
+ let left = pwd + experimentalFlag;
|
|
1222
|
+
// Add model name on the right side, plus thinking level if model supports it
|
|
1223
|
+
const modelName = state.model?.id || "no-model";
|
|
1224
|
+
- let statsLeftWidth = visibleWidth(statsLeft);
|
|
1225
|
+
- // If statsLeft is too wide, truncate it
|
|
1226
|
+
- if (statsLeftWidth > width) {
|
|
1227
|
+
- statsLeft = truncateToWidth(statsLeft, width, "...");
|
|
1228
|
+
- statsLeftWidth = visibleWidth(statsLeft);
|
|
1229
|
+
+ let leftWidth = visibleWidth(left);
|
|
1230
|
+
+ // If the left side is too wide, truncate it
|
|
1231
|
+
+ if (leftWidth > width) {
|
|
1232
|
+
+ left = truncateToWidth(left, width, "...");
|
|
1233
|
+
+ leftWidth = visibleWidth(left);
|
|
1234
|
+
}
|
|
1235
|
+
- // Calculate available space for padding (minimum 2 spaces between stats and model)
|
|
1236
|
+
+ // Calculate available space for padding (minimum 2 spaces between cwd and model)
|
|
1237
|
+
const minPadding = 2;
|
|
1238
|
+
// Add thinking level indicator if model supports reasoning
|
|
1239
|
+
let rightSideWithoutProvider = modelName;
|
|
1240
|
+
@@ -171,50 +158,48 @@ export class FooterComponent {
|
|
1241
|
+
let rightSide = rightSideWithoutProvider;
|
|
1242
|
+
if (this.footerData.getAvailableProviderCount() > 1 && state.model) {
|
|
1243
|
+
rightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;
|
|
1244
|
+
- if (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {
|
|
1245
|
+
+ if (leftWidth + minPadding + visibleWidth(rightSide) > width) {
|
|
1246
|
+
// Too wide, fall back
|
|
1247
|
+
rightSide = rightSideWithoutProvider;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const rightSideWidth = visibleWidth(rightSide);
|
|
1251
|
+
- const totalNeeded = statsLeftWidth + minPadding + rightSideWidth;
|
|
1252
|
+
- let statsLine;
|
|
1253
|
+
+ const totalNeeded = leftWidth + minPadding + rightSideWidth;
|
|
1254
|
+
+ let footerLine;
|
|
1255
|
+
if (totalNeeded <= width) {
|
|
1256
|
+
// Both fit - add padding to right-align model
|
|
1257
|
+
- const padding = " ".repeat(width - statsLeftWidth - rightSideWidth);
|
|
1258
|
+
- statsLine = statsLeft + padding + rightSide;
|
|
1259
|
+
+ const padding = " ".repeat(width - leftWidth - rightSideWidth);
|
|
1260
|
+
+ footerLine = left + padding + rightSide;
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
// Need to truncate right side
|
|
1264
|
+
- const availableForRight = width - statsLeftWidth - minPadding;
|
|
1265
|
+
+ const availableForRight = width - leftWidth - minPadding;
|
|
1266
|
+
if (availableForRight > 0) {
|
|
1267
|
+
const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
|
|
1268
|
+
const truncatedRightWidth = visibleWidth(truncatedRight);
|
|
1269
|
+
- const padding = " ".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));
|
|
1270
|
+
- statsLine = statsLeft + padding + truncatedRight;
|
|
1271
|
+
+ const padding = " ".repeat(Math.max(0, width - leftWidth - truncatedRightWidth));
|
|
1272
|
+
+ footerLine = left + padding + truncatedRight;
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
// Not enough space for right side at all
|
|
1276
|
+
- statsLine = statsLeft;
|
|
1277
|
+
+ footerLine = left;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
- // Apply dim to each part separately. statsLeft may contain color codes (for context %)
|
|
1281
|
+
- // that end with a reset, which would clear an outer dim wrapper. So we dim the parts
|
|
1282
|
+
- // before and after the colored section independently.
|
|
1283
|
+
- const dimStatsLeft = theme.fg("dim", statsLeft);
|
|
1284
|
+
- const remainder = statsLine.slice(statsLeft.length); // padding + rightSide
|
|
1285
|
+
- const dimRemainder = theme.fg("dim", remainder);
|
|
1286
|
+
- const pwdLine = truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "..."));
|
|
1287
|
+
- const lines = [pwdLine, dimStatsLeft + dimRemainder];
|
|
1288
|
+
+ // Apply dim to each part separately. `left` may carry the bold/warning-coloured
|
|
1289
|
+
+ // xp flag, whose reset would clear an outer dim wrapper. So dim the parts before
|
|
1290
|
+
+ // and after the coloured section independently.
|
|
1291
|
+
+ const dimLeft = theme.fg("dim", left);
|
|
1292
|
+
+ const remainder = footerLine.slice(left.length); // padding + rightSide
|
|
1293
|
+
+ const lines = [dimLeft + theme.fg("dim", remainder)];
|
|
1294
|
+
// Add extension statuses on a single line, sorted by key alphabetically
|
|
1295
|
+
const extensionStatuses = this.footerData.getExtensionStatuses();
|
|
1296
|
+
if (extensionStatuses.size > 0) {
|
|
1110
1297
|
const sortedStatuses = Array.from(extensionStatuses.entries())
|
|
1111
1298
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
1112
1299
|
.map(([, text]) => sanitizeStatusText(text));
|
|
@@ -1119,6 +1306,97 @@ index 1d9f046..2a1fba8 100644
|
|
|
1119
1306
|
}
|
|
1120
1307
|
return lines;
|
|
1121
1308
|
}
|
|
1309
|
+
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
|
|
1310
|
+
index 3f93cc6..bbe2c10 100644
|
|
1311
|
+
--- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
|
|
1312
|
+
+++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
|
|
1313
|
+
@@ -1,8 +1,35 @@
|
|
1314
|
+
-import { Box, Container, getCapabilities, Image, Spacer, Text } from "@earendil-works/pi-tui";
|
|
1315
|
+
+import { Box, Container, getCapabilities, Image, Spacer, visibleWidth, Text } from "@earendil-works/pi-tui";
|
|
1316
|
+
import { createAllToolDefinitions } from "../../../core/tools/index.js";
|
|
1317
|
+
import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.js";
|
|
1318
|
+
import { convertToPng } from "../../../utils/image-convert.js";
|
|
1319
|
+
import { theme } from "../theme/theme.js";
|
|
1320
|
+
+/**
|
|
1321
|
+
+ * Privateer: mark a failed tool call inline on its title line.
|
|
1322
|
+
+ *
|
|
1323
|
+
+ * Upstream's ONLY failure signal is the full-width red `toolErrorBg` wash behind
|
|
1324
|
+
+ * the whole block. We flatten those washes (see dark.json / light.json) because
|
|
1325
|
+
+ * large blocks of colour read as noise — but flattening the error one alone would
|
|
1326
|
+
+ * make a failure indistinguishable from a success, which is a real regression.
|
|
1327
|
+
+ *
|
|
1328
|
+
+ * So we put the signal back where it costs nothing: a red glyph prefixed to the
|
|
1329
|
+
+ * first line of whatever the tool's own renderCall produced. Wrapping the CALL
|
|
1330
|
+
+ * component (not the Box) means Box still owns padding and width, and the marker
|
|
1331
|
+
+ * works for every tool without touching seven per-tool renderers. Subsequent
|
|
1332
|
+
+ * lines get a blank hanging indent so the title stays visually aligned.
|
|
1333
|
+
+ */
|
|
1334
|
+
+function markCallErrored(component, marker) {
|
|
1335
|
+
+ const markerWidth = visibleWidth(marker);
|
|
1336
|
+
+ const indent = " ".repeat(markerWidth);
|
|
1337
|
+
+ return {
|
|
1338
|
+
+ render(width) {
|
|
1339
|
+
+ const lines = component.render(Math.max(1, width - markerWidth));
|
|
1340
|
+
+ return lines.map((line, i) => (i === 0 ? marker + line : indent + line));
|
|
1341
|
+
+ },
|
|
1342
|
+
+ invalidate() {
|
|
1343
|
+
+ component.invalidate?.();
|
|
1344
|
+
+ },
|
|
1345
|
+
+ };
|
|
1346
|
+
+}
|
|
1347
|
+
export class ToolExecutionComponent extends Container {
|
|
1348
|
+
contentBox;
|
|
1349
|
+
contentText;
|
|
1350
|
+
@@ -106,6 +133,13 @@ export class ToolExecutionComponent extends Container {
|
|
1351
|
+
createCallFallback() {
|
|
1352
|
+
return new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0);
|
|
1353
|
+
}
|
|
1354
|
+
+ /** Privateer: red-flag a failed call, pass a pending/succeeded one straight through. */
|
|
1355
|
+
+ decorateCall(component) {
|
|
1356
|
+
+ if (!this.result?.isError || this.isPartial) {
|
|
1357
|
+
+ return component;
|
|
1358
|
+
+ }
|
|
1359
|
+
+ return markCallErrored(component, theme.fg("error", "\u2717 "));
|
|
1360
|
+
+ }
|
|
1361
|
+
createResultFallback() {
|
|
1362
|
+
const output = this.getTextOutput();
|
|
1363
|
+
if (!output) {
|
|
1364
|
+
@@ -218,19 +252,22 @@ export class ToolExecutionComponent extends Container {
|
|
1365
|
+
renderContainer.clear();
|
|
1366
|
+
const callRenderer = this.getCallRenderer();
|
|
1367
|
+
if (!callRenderer) {
|
|
1368
|
+
- renderContainer.addChild(this.createCallFallback());
|
|
1369
|
+
+ renderContainer.addChild(this.decorateCall(this.createCallFallback()));
|
|
1370
|
+
hasContent = true;
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
try {
|
|
1374
|
+
const component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));
|
|
1375
|
+
+ // Store the RAW component: it comes back as `lastComponent` on the
|
|
1376
|
+
+ // next render and the tool's renderer calls its own methods on it,
|
|
1377
|
+
+ // so handing it a wrapper would break every stateful renderer.
|
|
1378
|
+
this.callRendererComponent = component;
|
|
1379
|
+
- renderContainer.addChild(component);
|
|
1380
|
+
+ renderContainer.addChild(this.decorateCall(component));
|
|
1381
|
+
hasContent = true;
|
|
1382
|
+
}
|
|
1383
|
+
catch {
|
|
1384
|
+
this.callRendererComponent = undefined;
|
|
1385
|
+
- renderContainer.addChild(this.createCallFallback());
|
|
1386
|
+
+ renderContainer.addChild(this.decorateCall(this.createCallFallback()));
|
|
1387
|
+
hasContent = true;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
@@ -302,7 +339,8 @@ export class ToolExecutionComponent extends Container {
|
|
1391
|
+
return getRenderedTextOutput(this.result, this.showImages);
|
|
1392
|
+
}
|
|
1393
|
+
formatToolExecution() {
|
|
1394
|
+
- let text = theme.fg("toolTitle", theme.bold(this.toolName));
|
|
1395
|
+
+ const errorMark = this.result?.isError && !this.isPartial ? theme.fg("error", "\u2717 ") : "";
|
|
1396
|
+
+ let text = errorMark + theme.fg("toolTitle", theme.bold(this.toolName));
|
|
1397
|
+
const content = JSON.stringify(this.args, null, 2);
|
|
1398
|
+
if (content) {
|
|
1399
|
+
text += `\n\n${content}`;
|
|
1122
1400
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
|
|
1123
1401
|
index 42e655d..d00bec9 100644
|
|
1124
1402
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
|
|
@@ -1269,6 +1547,40 @@ index 42e655d..d00bec9 100644
|
|
|
1269
1547
|
}
|
|
1270
1548
|
async getUserInput() {
|
|
1271
1549
|
const queuedInput = this.pendingUserInputs.shift();
|
|
1550
|
+
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
|
|
1551
|
+
index 9db9cbd..b370180 100644
|
|
1552
|
+
--- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
|
|
1553
|
+
+++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
|
|
1554
|
+
@@ -39,9 +39,9 @@
|
|
1555
|
+
"customMessageBg": "customMsgBg",
|
|
1556
|
+
"customMessageText": "text",
|
|
1557
|
+
"customMessageLabel": "#9575cd",
|
|
1558
|
+
- "toolPendingBg": "toolPendingBg",
|
|
1559
|
+
- "toolSuccessBg": "toolSuccessBg",
|
|
1560
|
+
- "toolErrorBg": "toolErrorBg",
|
|
1561
|
+
+ "toolPendingBg": "",
|
|
1562
|
+
+ "toolSuccessBg": "",
|
|
1563
|
+
+ "toolErrorBg": "",
|
|
1564
|
+
"toolTitle": "text",
|
|
1565
|
+
"toolOutput": "gray",
|
|
1566
|
+
|
|
1567
|
+
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
|
|
1568
|
+
index 74ef3d1..709a21d 100644
|
|
1569
|
+
--- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
|
|
1570
|
+
+++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
|
|
1571
|
+
@@ -38,9 +38,9 @@
|
|
1572
|
+
"customMessageBg": "customMsgBg",
|
|
1573
|
+
"customMessageText": "text",
|
|
1574
|
+
"customMessageLabel": "#7e57c2",
|
|
1575
|
+
- "toolPendingBg": "toolPendingBg",
|
|
1576
|
+
- "toolSuccessBg": "toolSuccessBg",
|
|
1577
|
+
- "toolErrorBg": "toolErrorBg",
|
|
1578
|
+
+ "toolPendingBg": "",
|
|
1579
|
+
+ "toolSuccessBg": "",
|
|
1580
|
+
+ "toolErrorBg": "",
|
|
1581
|
+
"toolTitle": "text",
|
|
1582
|
+
"toolOutput": "mediumGray",
|
|
1583
|
+
|
|
1272
1584
|
diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js b/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js
|
|
1273
1585
|
index 4230b18..f47e80f 100644
|
|
1274
1586
|
--- a/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js
|
package/src/config/moat.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* buildMoat() — the one place that decides which extensions a session gets, in what order.
|
|
3
3
|
*
|
|
4
|
-
* WHAT THIS REPLACES.
|
|
4
|
+
* WHAT THIS REPLACES. Six entry points each hand-assembled their own `extensionFactories`
|
|
5
5
|
* array: the harbor's sessions (routines, workflows, tasks), its live task spawns, the
|
|
6
|
-
* channels runner, ACP,
|
|
7
|
-
* same privacy extension, same account provider, the same
|
|
8
|
-
* conditionals — copied with small deliberate differences
|
|
9
|
-
* comment explaining them. Nothing held them together, so
|
|
10
|
-
* silently didn't exist on the
|
|
11
|
-
* the ordering rule below lived as prose repeated in each copy.
|
|
6
|
+
* channels runner, ACP, the dev REPL, and the desktop app's per-window session. The lists
|
|
7
|
+
* were the same list — same gate, same privacy extension, same account provider, the same
|
|
8
|
+
* `webEnabled()`/`mediaEnabled()` conditionals — copied with small deliberate differences
|
|
9
|
+
* and a large amount of duplicated comment explaining them. Nothing held them together, so
|
|
10
|
+
* an extension added to one path silently didn't exist on the others (privateer-media had
|
|
11
|
+
* reached three of five), and the ordering rule below lived as prose repeated in each copy.
|
|
12
|
+
*
|
|
13
|
+
* THE DESKTOP IS WHY THIS NOTE IS NOW ABOUT SIX. It was left out of the first pass and
|
|
14
|
+
* proved the point within the month: the app's Super Computer had NO generation tools at
|
|
15
|
+
* all — no generate_image / _video / _model / _speech / _music / _sfx, no
|
|
16
|
+
* media_capabilities, and not even video_compose, which every other kind gets
|
|
17
|
+
* unconditionally because it is local ffmpeg work that costs nothing. Its MCP connectors
|
|
18
|
+
* worked, so Godot and Unreal drove the editor fine while the same window could not make
|
|
19
|
+
* a texture to put in it. The lesson is the one this module was written for: the fix is
|
|
20
|
+
* not "add the media factory to that array", it is "there are no arrays".
|
|
12
21
|
*
|
|
13
22
|
* ORDER MATTERS, ONCE. pi-privacy's own catalog registers a `privateer` provider (its
|
|
14
23
|
* PUBLIC developer-key channel, one seed model), and Pi's registerProvider REPLACES a
|
|
@@ -51,7 +60,8 @@ export type MoatKind =
|
|
|
51
60
|
| "live-task" // a drivable session the harbor spawns for the app
|
|
52
61
|
| "channels" // Telegram / Slack / Discord / WhatsApp bridge sessions
|
|
53
62
|
| "acp" // `privateer acp` — an ACP host (Zed, Buzz) drives
|
|
54
|
-
| "repl"
|
|
63
|
+
| "repl" // the lean dev REPL (npm run chat)
|
|
64
|
+
| "desktop"; // the desktop app's Super Computer — one session per window
|
|
55
65
|
|
|
56
66
|
export interface MoatOptions {
|
|
57
67
|
kind: MoatKind;
|
|
@@ -72,24 +82,83 @@ export interface MoatOptions {
|
|
|
72
82
|
* the point: a run whose result goes to a webhook or a file has nothing to attach to.
|
|
73
83
|
*/
|
|
74
84
|
resultMedia?: import("../routines/resultMedia.ts").ResultMedia;
|
|
85
|
+
/**
|
|
86
|
+
* What a SIGNED-OUT session hears when it calls web_search / web_fetch. Required by
|
|
87
|
+
* `web: "guarded"` and ignored otherwise. It lives on the caller because only the caller
|
|
88
|
+
* knows what the user can do about it — a desktop window can point at its account menu,
|
|
89
|
+
* a headless host has no menu to point at (src/tools/web.ts).
|
|
90
|
+
*/
|
|
91
|
+
webHint?: string;
|
|
92
|
+
/**
|
|
93
|
+
* How to import a THIRD-PARTY dependency (today: pi-mcp-adapter). Defaults to a plain
|
|
94
|
+
* dynamic import, which is right for every process that runs from a normal Node
|
|
95
|
+
* resolution root.
|
|
96
|
+
*
|
|
97
|
+
* The desktop is not one. It runs the agent's copy of every shared package — one Pi
|
|
98
|
+
* instance, one captured-cert map — so it resolves from privateer-agent's own
|
|
99
|
+
* package.json rather than the app's, and it needs a fallback for adapters whose entry
|
|
100
|
+
* is an `index.ts` (under Electron, tsx's `register()` patches only the ESM loader, so
|
|
101
|
+
* a vanilla CJS resolve looks for index.js and reports MODULE_NOT_FOUND). Taking the
|
|
102
|
+
* resolver from the host keeps that knowledge in the host, where it belongs, instead of
|
|
103
|
+
* teaching this module about Electron.
|
|
104
|
+
*/
|
|
105
|
+
hostImport?: (spec: string) => Promise<any>;
|
|
75
106
|
}
|
|
76
107
|
|
|
77
108
|
/**
|
|
78
109
|
* Per-kind capabilities. `web` and `media` are the CEILING — each is still ANDed with its
|
|
79
110
|
* runtime switch (webEnabled/mediaEnabled), so a false here means "never", not "by default".
|
|
80
111
|
*
|
|
81
|
-
* web is deliberately off for live-task and repl:
|
|
82
|
-
* API precisely because an unattended run must not hold a search provider key, and
|
|
83
|
-
* those paths have a human at the other end who can use their own provider
|
|
84
|
-
* compose is unconditional everywhere: local ffmpeg work, no account,
|
|
85
|
-
* so a run with generation off can still assemble media that
|
|
112
|
+
* web is deliberately off for live-task and repl: the account form routes through the
|
|
113
|
+
* account API precisely because an unattended run must not hold a search provider key, and
|
|
114
|
+
* both of those paths have a human at the other end who can use their own provider
|
|
115
|
+
* (src/tools/web.ts). compose is unconditional everywhere: local ffmpeg work, no account,
|
|
116
|
+
* no network, no spend — so a run with generation off can still assemble media that
|
|
117
|
+
* already exists on disk.
|
|
118
|
+
*
|
|
119
|
+
* The three optional rows below are all FALSE for the unattended kinds, which is exactly
|
|
120
|
+
* what those five paths did before the desktop joined the table. They exist because the
|
|
121
|
+
* desktop is the first ATTENDED session built from here, and an attended session differs
|
|
122
|
+
* from a headless one in ways that are real rather than cosmetic — a project context file
|
|
123
|
+
* to load, a folder whose skills follow it, and a model PICKER whose registry has to be
|
|
124
|
+
* repaired. Leaving them off keeps every pre-existing kind byte-identical.
|
|
86
125
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
126
|
+
interface MoatCaps {
|
|
127
|
+
/**
|
|
128
|
+
* false — never; the tools do not exist for this kind.
|
|
129
|
+
* "account" — registered only once webEnabled() says there are credentials. The
|
|
130
|
+
* UNATTENDED shape: decide at build, because nothing will change mid-run.
|
|
131
|
+
* "guarded" — always registered, each call re-checks sign-in and answers with
|
|
132
|
+
* `webHint` when there is none. The ATTENDED shape: the session outlives
|
|
133
|
+
* `/signin`, so the question has to be asked when the tool RUNS
|
|
134
|
+
* (src/tools/web.ts spells out why these are two functions, not one).
|
|
135
|
+
*/
|
|
136
|
+
web: false | "account" | "guarded";
|
|
137
|
+
media: boolean;
|
|
138
|
+
mcp: boolean;
|
|
139
|
+
/** PRIVATEER.md project context + /init (extensions/privateer-context.ts). */
|
|
140
|
+
context?: boolean;
|
|
141
|
+
/** The folder's own skills, contributed from ~/.privateer rather than the user's tree. */
|
|
142
|
+
spawnSkills?: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Load the privacy SHIM (extensions/privateer-privacy.ts) rather than the bare
|
|
145
|
+
* privacyExtension() below — the same configuration plus two provider REPAIRS.
|
|
146
|
+
* pi-privacy re-registers `tinfoil` with a one-model seed catalog and `privateer` with
|
|
147
|
+
* its public developer-key channel, and registerProvider REPLACES a provider's models.
|
|
148
|
+
* A kind that resolves ONE configured model never notices; a kind with a model picker
|
|
149
|
+
* does, loudly — every build throwing "Model tinfoil/… not found" on a machine with a
|
|
150
|
+
* working key. See the shim's header for the full account.
|
|
151
|
+
*/
|
|
152
|
+
privacyRepairs?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const CAPABILITIES: Record<MoatKind, MoatCaps> = {
|
|
156
|
+
"harbor-session": { web: "account", media: true, mcp: true },
|
|
89
157
|
"live-task": { web: false, media: true, mcp: false },
|
|
90
|
-
channels: { web:
|
|
91
|
-
acp: { web:
|
|
158
|
+
channels: { web: "account", media: true, mcp: false },
|
|
159
|
+
acp: { web: "account", media: true, mcp: false },
|
|
92
160
|
repl: { web: false, media: true, mcp: false },
|
|
161
|
+
desktop: { web: "guarded", media: true, mcp: true, context: true, spawnSkills: true, privacyRepairs: true },
|
|
93
162
|
};
|
|
94
163
|
|
|
95
164
|
/**
|
|
@@ -180,16 +249,32 @@ export async function buildMoat(opts: MoatOptions): Promise<ExtensionFactory[]>
|
|
|
180
249
|
const { makePermissionGate } = await import("../ext/permissionGate.ts");
|
|
181
250
|
const { makeAccountProvider } = await import("../providers/account.ts");
|
|
182
251
|
const { webEnabled, mediaEnabled } = await import("./hosted.ts");
|
|
183
|
-
const { privacyExtension } = await import("./privacyPolicy.ts");
|
|
184
252
|
|
|
185
253
|
const factories: ExtensionFactory[] = [makePermissionGate(opts.gate)];
|
|
186
254
|
|
|
187
255
|
// pi-privacy is configured in exactly ONE place, shared with the DISCOVERED copy of this
|
|
188
256
|
// extension (the TUI's, and every subagent child's) — see ./privacyPolicy.ts for the two
|
|
189
|
-
// bugs that came of configuring it in two.
|
|
190
|
-
|
|
257
|
+
// bugs that came of configuring it in two. `privacyRepairs` picks which of the two
|
|
258
|
+
// ROUTES into that one configuration a kind takes, never which options it gets.
|
|
259
|
+
if (caps.privacyRepairs) {
|
|
260
|
+
const { default: privateerPrivacy } = await import("../../extensions/privateer-privacy.ts");
|
|
261
|
+
factories.push(privateerPrivacy);
|
|
262
|
+
} else {
|
|
263
|
+
const { privacyExtension } = await import("./privacyPolicy.ts");
|
|
264
|
+
factories.push(privacyExtension());
|
|
265
|
+
}
|
|
191
266
|
factories.push(makeAccountProvider()); // must follow pi-privacy — see header
|
|
192
267
|
|
|
268
|
+
if (caps.context) {
|
|
269
|
+
const { default: privateerContext } = await import("../../extensions/privateer-context.ts");
|
|
270
|
+
factories.push(privateerContext);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (caps.spawnSkills) {
|
|
274
|
+
const { default: privateerSpawnSkills } = await import("../../extensions/privateer-spawn-skills.ts");
|
|
275
|
+
factories.push(privateerSpawnSkills);
|
|
276
|
+
}
|
|
277
|
+
|
|
193
278
|
if (opts.relayFiles) {
|
|
194
279
|
const { makeRelayFileTools } = await import("../tools/relayFileTools.ts");
|
|
195
280
|
factories.push(makeRelayFileTools(opts.relayFiles.bridge, opts.relayFiles.attachments));
|
|
@@ -200,9 +285,16 @@ export async function buildMoat(opts: MoatOptions): Promise<ExtensionFactory[]>
|
|
|
200
285
|
factories.push(makeAttachResultTools(opts.resultMedia));
|
|
201
286
|
}
|
|
202
287
|
|
|
203
|
-
if (caps.web && webEnabled()) {
|
|
288
|
+
if (caps.web === "account" && webEnabled()) {
|
|
204
289
|
const { makeWebTools } = await import("../tools/web.ts");
|
|
205
290
|
factories.push(makeWebTools());
|
|
291
|
+
} else if (caps.web === "guarded") {
|
|
292
|
+
if (!opts.webHint) throw new Error(`buildMoat: kind "${opts.kind}" needs a webHint for guarded web tools`);
|
|
293
|
+
const hint = opts.webHint;
|
|
294
|
+
const { guardedWebToolDefinitions } = await import("../tools/web.ts");
|
|
295
|
+
factories.push((pi: any) => {
|
|
296
|
+
for (const def of guardedWebToolDefinitions(hint)) pi.registerTool?.(def);
|
|
297
|
+
});
|
|
206
298
|
}
|
|
207
299
|
|
|
208
300
|
if (caps.media && mediaEnabled()) {
|
|
@@ -222,7 +314,8 @@ export async function buildMoat(opts: MoatOptions): Promise<ExtensionFactory[]>
|
|
|
222
314
|
// (inside createAgentSessionServices), not when it is imported here, so the env has to
|
|
223
315
|
// be set around session creation rather than around this call.
|
|
224
316
|
const mcpAdapterSpec = "pi-mcp-adapter";
|
|
225
|
-
const
|
|
317
|
+
const hostImport = opts.hostImport ?? ((spec: string) => import(spec));
|
|
318
|
+
const { default: mcpAdapter } = await hostImport(mcpAdapterSpec);
|
|
226
319
|
factories.push(mcpAdapter);
|
|
227
320
|
}
|
|
228
321
|
|
package/src/engine/errors.ts
CHANGED
|
@@ -134,6 +134,13 @@ export function isAccountCapCode(code: string | null | undefined): boolean {
|
|
|
134
134
|
// patches/@earendil-works+pi-coding-agent+*.patch, which mirrors these two helpers):
|
|
135
135
|
// squeeze the page down to the line a person can act on, and let the STATUS decide
|
|
136
136
|
// retryability rather than a substring of the body.
|
|
137
|
+
//
|
|
138
|
+
// A first-attempt hard 4xx is also terminal for the post-run loop. Stock Pi still
|
|
139
|
+
// falls through to compaction when _retryAttempt is 0, and a session that already
|
|
140
|
+
// has usage will summarise — another LLM call to the same blocked endpoint. The
|
|
141
|
+
// summarizer retries on the raw HTML (it contains "500"/"502"), so the agent looks
|
|
142
|
+
// hung until that budget burns. The patch returns false from _handlePostAgentRun
|
|
143
|
+
// before _checkCompaction when isHardHttpFailure matches.
|
|
137
144
|
|
|
138
145
|
/** Hard cap on an error message we display, persist, or classify. */
|
|
139
146
|
export const MAX_ERROR_CHARS = 2_000;
|