@herbertgao/pi-extensions 2026.9.13 → 2026.9.15

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.
Files changed (70) hide show
  1. package/README.md +3 -3
  2. package/THIRD_PARTY_NOTICES.md +2 -2
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
  5. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  6. package/node_modules/@herbertgao/pi-subagents/package.json +4 -4
  7. package/node_modules/@herbertgao/resume-from/package.json +2 -2
  8. package/node_modules/@herbertgao/sol-pi/package.json +5 -5
  9. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +2 -0
  10. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +8 -0
  11. package/node_modules/pi-jev-auto-mode/README.md +7 -0
  12. package/node_modules/pi-jev-auto-mode/package.json +1 -1
  13. package/node_modules/pi-jev-auto-mode/src/extension.ts +21 -2
  14. package/node_modules/pi-jev-auto-mode/src/records.ts +68 -2
  15. package/node_modules/pi-jev-auto-mode/src/settings.ts +22 -0
  16. package/node_modules/pi-jev-auto-mode/src/ui.ts +10 -0
  17. package/node_modules/pi-mcp-adapter/CHANGELOG.md +26 -0
  18. package/node_modules/pi-mcp-adapter/README.md +39 -9
  19. package/node_modules/pi-mcp-adapter/cli.js +33 -22
  20. package/node_modules/pi-mcp-adapter/commands.ts +3 -3
  21. package/node_modules/pi-mcp-adapter/config.ts +9 -1
  22. package/node_modules/pi-mcp-adapter/dist/config.js +9 -1
  23. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  24. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +1 -2
  25. package/node_modules/pi-mcp-adapter/dist/jev-client.js +59 -32
  26. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -1
  27. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +1 -1
  28. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +33 -4
  29. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +148 -28
  30. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -1
  31. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1 -2
  32. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -1
  33. package/node_modules/pi-mcp-adapter/dist/types.d.ts +7 -1
  34. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  35. package/node_modules/pi-mcp-adapter/index.ts +51 -38
  36. package/node_modules/pi-mcp-adapter/jev-client.ts +58 -31
  37. package/node_modules/pi-mcp-adapter/jev-contracts.ts +2 -2
  38. package/node_modules/pi-mcp-adapter/jev-key-store.ts +144 -31
  39. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  40. package/node_modules/pi-mcp-adapter/mcp-code.ts +6 -6
  41. package/node_modules/pi-mcp-adapter/package.json +5 -5
  42. package/node_modules/pi-mcp-adapter/semantic-search.ts +3 -3
  43. package/node_modules/pi-mcp-adapter/types.ts +7 -1
  44. package/node_modules/pi-multi-account/CHANGELOG.md +12 -0
  45. package/node_modules/pi-multi-account/README.md +21 -6
  46. package/node_modules/pi-multi-account/index.ts +295 -41
  47. package/node_modules/pi-multi-account/package.json +3 -3
  48. package/node_modules/pi-typesafe/dist/backends.d.ts +10 -0
  49. package/node_modules/pi-typesafe/dist/backends.js +26 -0
  50. package/node_modules/pi-typesafe/dist/client.d.ts +1 -1
  51. package/node_modules/pi-typesafe/dist/client.js +10 -6
  52. package/node_modules/pi-typesafe/dist/errors.d.ts +10 -2
  53. package/node_modules/pi-typesafe/dist/errors.js +28 -7
  54. package/node_modules/pi-typesafe/dist/extension.js +2 -1
  55. package/node_modules/pi-typesafe/package.json +4 -4
  56. package/node_modules/pi-web-access/CHANGELOG.md +22 -0
  57. package/node_modules/pi-web-access/README.md +21 -8
  58. package/node_modules/pi-web-access/baizhi.ts +124 -0
  59. package/node_modules/pi-web-access/crawl4ai.ts +2 -10
  60. package/node_modules/pi-web-access/curator-page.ts +3 -1
  61. package/node_modules/pi-web-access/curator-server.ts +1 -0
  62. package/node_modules/pi-web-access/dist/index.js +540 -220
  63. package/node_modules/pi-web-access/firecrawl.ts +2 -10
  64. package/node_modules/pi-web-access/gemini-search.ts +5 -10
  65. package/node_modules/pi-web-access/index.ts +134 -23
  66. package/node_modules/pi-web-access/package.json +5 -1
  67. package/node_modules/pi-web-access/storage.ts +1 -0
  68. package/node_modules/pi-web-access/tool-activation.ts +134 -0
  69. package/node_modules/pi-web-access/utils.ts +9 -1
  70. package/package.json +14 -14
@@ -1,20 +1,44 @@
1
1
  import { createSecureKeyringStore, type SecureKeyringStore } from "./secure-keyring.ts";
2
2
 
3
+ /** The Jev decisions endpoint used when `SYSTEMONE_ENDPOINT` is not set. */
4
+ export const JEV_DEFAULT_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
5
+ const SYSTEMONE_ENDPOINT_ENV = "SYSTEMONE_ENDPOINT";
6
+ const SYSTEMONE_API_KEY_ENV = "SYSTEMONE_API_KEY";
7
+ /** TypeSafe-issued key: honored only for the default endpoint and never sent anywhere else. */
8
+ const LEGACY_TYPESAFE_API_KEY_ENV = "TYPESAFE_API_KEY";
9
+ /** Origin recorded in version 1 credential records. */
3
10
  export const TYPESAFE_API_ORIGIN = "https://api.typesafe.ai";
4
- export const JEV_KEYRING_SERVICE = "pi-mcp-adapter.service-key";
11
+ /** Version 1 credential account, still read for the default endpoint. */
5
12
  export const JEV_KEYRING_ACCOUNT = "typesafe@sha256(https://api.typesafe.ai)";
13
+ export const JEV_KEYRING_SERVICE = "pi-mcp-adapter.service-key";
14
+
15
+ const MAX_ENDPOINT_LENGTH = 512;
16
+ /** The path `@typesafe-ai/sdk` appends to whatever base URL it is given. */
17
+ export const JEV_SDK_PATH = "/v1/systemone";
18
+
19
+ export interface ResolvedJevEndpoint {
20
+ /** Canonical absolute URL sent to the provider, for example `https://opencode.ai/zen/v1/systemone`. */
21
+ readonly href: string;
22
+ /** URL origin, used as the SDK base URL. */
23
+ readonly origin: string;
24
+ /** URL path, rewritten onto the SDK's fixed path by the pinned fetch. */
25
+ readonly path: string;
26
+ }
27
+
28
+ export type JevEndpointResolution =
29
+ | { status: "resolved"; source: "environment" | "default"; endpoint: ResolvedJevEndpoint }
30
+ | { status: "unavailable"; message: string };
6
31
 
7
32
  interface StoredJevKey {
8
- version: 1;
9
- provider: "typesafe";
10
- origin: typeof TYPESAFE_API_ORIGIN;
33
+ version: 2;
34
+ endpoint: string;
11
35
  apiKey: string;
12
36
  }
13
37
 
14
38
  export class JevCredentialStoreError extends Error {
15
39
  readonly code = "JEV_CREDENTIAL_STORE_UNAVAILABLE";
16
40
  constructor(readonly operation: "read" | "write" | "remove", cause: unknown) {
17
- super(`TypeSafe API key secure credential store unavailable during ${operation}. Configure or unlock the OS credential store and retry.`, { cause });
41
+ super(`Jev API key secure credential store unavailable during ${operation}. Configure or unlock the OS credential store and retry.`, { cause });
18
42
  this.name = "JevCredentialStoreError";
19
43
  }
20
44
  }
@@ -26,54 +50,143 @@ export type JevCredentialResolution =
26
50
 
27
51
  function validateApiKey(value: unknown): string {
28
52
  if (typeof value !== "string" || value.trim().length === 0 || /[\u0000-\u001f\u007f]/.test(value)) {
29
- throw new Error("TypeSafe API key must be a non-empty string without control characters");
53
+ throw new Error("Jev API key must be a non-empty string without control characters");
30
54
  }
31
55
  return value;
32
56
  }
33
57
 
58
+ /**
59
+ * Constrains the operator-supplied endpoint to an absolute HTTPS URL without credentials, query, or fragment, so
60
+ * the pinned fetch can match it exactly and a stray value cannot redirect requests or smuggle a query string.
61
+ */
62
+ function parseEndpoint(raw: string, label: string): ResolvedJevEndpoint {
63
+ if (typeof raw !== "string" || raw.trim().length === 0) throw new Error(`${label} must be a non-empty URL`);
64
+ if (raw.length > MAX_ENDPOINT_LENGTH) throw new Error(`${label} must be at most ${MAX_ENDPOINT_LENGTH} characters`);
65
+ if (/[\u0000-\u001f\u007f\s]/.test(raw)) throw new Error(`${label} must not contain whitespace or control characters`);
66
+ let url: URL;
67
+ try { url = new URL(raw); }
68
+ catch { throw new Error(`${label} must be an absolute URL`); }
69
+ if (url.protocol !== "https:") throw new Error(`${label} must use https`);
70
+ if (url.username !== "" || url.password !== "") throw new Error(`${label} must not embed credentials`);
71
+ if (url.search !== "") throw new Error(`${label} must not include a query string`);
72
+ if (url.hash !== "") throw new Error(`${label} must not include a fragment`);
73
+ if (url.pathname === "" || url.pathname === "/") throw new Error(`${label} must include a path such as ${JEV_SDK_PATH}`);
74
+ return { href: `${url.origin}${url.pathname}`, origin: url.origin, path: url.pathname };
75
+ }
76
+
77
+ /**
78
+ * Resolves the Jev endpoint. An invalid `SYSTEMONE_ENDPOINT` resolves to `unavailable` instead of silently falling
79
+ * back to the default: sending an operator's judgment payload to a provider they did not ask for would be a data leak.
80
+ */
81
+ export function resolveJevEndpoint(env: NodeJS.ProcessEnv = process.env): JevEndpointResolution {
82
+ if (Object.hasOwn(env, SYSTEMONE_ENDPOINT_ENV)) {
83
+ try { return { status: "resolved", source: "environment", endpoint: parseEndpoint(env[SYSTEMONE_ENDPOINT_ENV] as string, SYSTEMONE_ENDPOINT_ENV) }; }
84
+ catch (error) { return { status: "unavailable", message: error instanceof Error ? error.message : `${SYSTEMONE_ENDPOINT_ENV} is invalid.` }; }
85
+ }
86
+ return { status: "resolved", source: "default", endpoint: parseEndpoint(JEV_DEFAULT_ENDPOINT, "the default Jev endpoint") };
87
+ }
88
+
89
+ /** Keyring account for an endpoint, so keys for different providers coexist instead of overwriting each other. */
90
+ export function jevKeyringAccount(endpoint: ResolvedJevEndpoint): string {
91
+ return `systemone@sha256(${endpoint.href})`;
92
+ }
93
+
34
94
  function store(): SecureKeyringStore {
35
95
  return createSecureKeyringStore(JEV_KEYRING_SERVICE);
36
96
  }
37
97
 
38
- function readStoredKey(secretStore: SecureKeyringStore = store()): string | undefined {
39
- let payload: string | undefined;
40
- try { payload = secretStore.read(JEV_KEYRING_ACCOUNT); }
41
- catch (error) { throw new JevCredentialStoreError("read", error); }
42
- if (payload === undefined) return undefined;
43
- try {
44
- const value: unknown = JSON.parse(payload);
45
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid record");
46
- const record = value as Partial<StoredJevKey>;
47
- const keys = Object.keys(record);
98
+ function requireEndpoint(endpoint: ResolvedJevEndpoint | undefined): ResolvedJevEndpoint {
99
+ if (endpoint) return endpoint;
100
+ const resolution = resolveJevEndpoint();
101
+ if (resolution.status === "unavailable") throw new Error(resolution.message);
102
+ return resolution.endpoint;
103
+ }
104
+
105
+ /**
106
+ * Version 1 records were written before the endpoint was configurable, so they are accepted only for the default
107
+ * endpoint.
108
+ */
109
+ function parseStoredKey(payload: string, endpoint: ResolvedJevEndpoint): string {
110
+ let value: unknown;
111
+ try { value = JSON.parse(payload); }
112
+ catch { throw new Error("invalid record"); }
113
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid record");
114
+ const record = value as Record<string, unknown>;
115
+ const keys = Object.keys(record);
116
+ if (record.version === 2) {
117
+ if (keys.length !== 3 || !["version", "endpoint", "apiKey"].every(key => Object.hasOwn(record, key))) throw new Error("invalid record fields");
118
+ if (record.endpoint !== endpoint.href) throw new Error("invalid or mismatched record");
119
+ return validateApiKey(record.apiKey);
120
+ }
121
+ if (record.version === 1) {
48
122
  if (keys.length !== 4 || !["version", "provider", "origin", "apiKey"].every(key => Object.hasOwn(record, key))) throw new Error("invalid record fields");
49
- if (record.version !== 1 || record.provider !== "typesafe" || record.origin !== TYPESAFE_API_ORIGIN) throw new Error("invalid or mismatched record");
123
+ if (record.provider !== "typesafe" || record.origin !== TYPESAFE_API_ORIGIN) throw new Error("invalid or mismatched record");
124
+ if (endpoint.href !== JEV_DEFAULT_ENDPOINT) throw new Error("invalid or mismatched record");
50
125
  return validateApiKey(record.apiKey);
126
+ }
127
+ throw new Error("invalid record version");
128
+ }
129
+
130
+ function readStoredKey(secretStore: SecureKeyringStore, endpoint: ResolvedJevEndpoint): string | undefined {
131
+ try {
132
+ let payload = secretStore.read(jevKeyringAccount(endpoint));
133
+ if (payload === undefined && endpoint.href === JEV_DEFAULT_ENDPOINT) payload = secretStore.read(JEV_KEYRING_ACCOUNT);
134
+ return payload === undefined ? undefined : parseStoredKey(payload, endpoint);
51
135
  } catch (error) {
52
136
  throw new JevCredentialStoreError("read", error);
53
137
  }
54
138
  }
55
139
 
56
- export function resolveJevCredential(env: NodeJS.ProcessEnv = process.env, secretStore?: SecureKeyringStore): JevCredentialResolution {
57
- if (Object.hasOwn(env, "TYPESAFE_API_KEY")) {
58
- try { return { status: "present", source: "environment", apiKey: validateApiKey(env.TYPESAFE_API_KEY) }; }
59
- catch { return { status: "unavailable", message: "TYPESAFE_API_KEY is present but invalid." }; }
140
+ export function resolveJevCredential(
141
+ env: NodeJS.ProcessEnv = process.env,
142
+ endpoint?: ResolvedJevEndpoint,
143
+ secretStore: SecureKeyringStore = store(),
144
+ ): JevCredentialResolution {
145
+ let target: ResolvedJevEndpoint;
146
+ if (endpoint) target = endpoint;
147
+ else {
148
+ const resolution = resolveJevEndpoint(env);
149
+ if (resolution.status === "unavailable") return resolution;
150
+ target = resolution.endpoint;
60
151
  }
61
- try {
62
- const apiKey = readStoredKey(secretStore ?? store());
63
- return apiKey === undefined ? { status: "missing" } : { status: "present", source: "keyring", apiKey };
64
- } catch (error) {
152
+ if (Object.hasOwn(env, SYSTEMONE_API_KEY_ENV)) {
153
+ try { return { status: "present", source: "environment", apiKey: validateApiKey(env[SYSTEMONE_API_KEY_ENV]) }; }
154
+ catch { return { status: "unavailable", message: `${SYSTEMONE_API_KEY_ENV} is present but invalid.` }; }
155
+ }
156
+ const legacySet = Object.hasOwn(env, LEGACY_TYPESAFE_API_KEY_ENV);
157
+ if (legacySet && target.href === JEV_DEFAULT_ENDPOINT) {
158
+ try { return { status: "present", source: "environment", apiKey: validateApiKey(env[LEGACY_TYPESAFE_API_KEY_ENV]) }; }
159
+ catch { return { status: "unavailable", message: `${LEGACY_TYPESAFE_API_KEY_ENV} is present but invalid.` }; }
160
+ }
161
+ let stored: string | undefined;
162
+ try { stored = readStoredKey(secretStore, target); }
163
+ catch (error) {
65
164
  if (!(error instanceof JevCredentialStoreError)) throw error;
66
165
  return { status: "unavailable", message: error.message };
67
166
  }
167
+ if (stored !== undefined) return { status: "present", source: "keyring", apiKey: stored };
168
+ // A TypeSafe-issued credential is never sent to another endpoint, but it must not mask a credential stored for
169
+ // this endpoint either, so it is only reported once nothing for this endpoint resolves.
170
+ if (legacySet) {
171
+ return {
172
+ status: "unavailable",
173
+ message: `${LEGACY_TYPESAFE_API_KEY_ENV} is a TypeSafe credential and is not sent to ${target.href}; set ${SYSTEMONE_API_KEY_ENV} for that endpoint.`,
174
+ };
175
+ }
176
+ return { status: "missing" };
68
177
  }
69
178
 
70
- export function saveJevApiKey(apiKey: string, secretStore: SecureKeyringStore = store()): void {
71
- const record: StoredJevKey = { version: 1, provider: "typesafe", origin: TYPESAFE_API_ORIGIN, apiKey: validateApiKey(apiKey) };
72
- try { secretStore.write(JEV_KEYRING_ACCOUNT, JSON.stringify(record)); }
179
+ export function saveJevApiKey(apiKey: string, endpoint?: ResolvedJevEndpoint, secretStore: SecureKeyringStore = store()): void {
180
+ const target = requireEndpoint(endpoint);
181
+ const record: StoredJevKey = { version: 2, endpoint: target.href, apiKey: validateApiKey(apiKey) };
182
+ try { secretStore.write(jevKeyringAccount(target), JSON.stringify(record)); }
73
183
  catch (error) { throw new JevCredentialStoreError("write", error); }
74
184
  }
75
185
 
76
- export function removeJevApiKey(secretStore: SecureKeyringStore = store()): void {
77
- try { secretStore.remove(JEV_KEYRING_ACCOUNT); }
78
- catch (error) { throw new JevCredentialStoreError("remove", error); }
186
+ export function removeJevApiKey(endpoint?: ResolvedJevEndpoint, secretStore: SecureKeyringStore = store()): void {
187
+ const target = requireEndpoint(endpoint);
188
+ try {
189
+ secretStore.remove(jevKeyringAccount(target));
190
+ if (target.href === JEV_DEFAULT_ENDPOINT) secretStore.remove(JEV_KEYRING_ACCOUNT);
191
+ } catch (error) { throw new JevCredentialStoreError("remove", error); }
79
192
  }
@@ -1191,9 +1191,8 @@ export async function getValidToken(
1191
1191
  }
1192
1192
  }
1193
1193
 
1194
- // No expiration info or no refresh token, assume valid
1195
1194
  authority()
1196
- return entry.tokens
1195
+ return null
1197
1196
  }
1198
1197
 
1199
1198
  /**
@@ -213,7 +213,7 @@ export async function runMcpScript(
213
213
  let evaluationAttempts = 0;
214
214
  let evaluationBytes = 0;
215
215
  let evaluationTokensRemaining = jevSettings.maxEvaluationTokensPerScript;
216
- const tokenBudgetExhausted = (): JevEvaluationEnvelope => ({ ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation token budget exhausted." } });
216
+ const tokenBudgetExhausted = (): JevEvaluationEnvelope => ({ ok: false, error: { code: "budget_exhausted", message: "Jev evaluation token budget exhausted." } });
217
217
  const chargeEvaluationTokens = (envelope: JevEvaluationEnvelope): JevEvaluationEnvelope => {
218
218
  if (!envelope.ok) return envelope;
219
219
  const used = envelope.data.usage.inputTokens + envelope.data.usage.outputTokens;
@@ -226,16 +226,16 @@ export async function runMcpScript(
226
226
  };
227
227
  const admitEvaluation = (input: unknown): JevEvaluationEnvelope | undefined => {
228
228
  if (++evaluationAttempts > jevSettings.maxEvaluationsPerScript) {
229
- return { ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation count budget exhausted." } };
229
+ return { ok: false, error: { code: "budget_exhausted", message: "Jev evaluation count budget exhausted." } };
230
230
  }
231
231
  if (evaluationTokensRemaining === 0) return tokenBudgetExhausted();
232
232
  let serialized: string | undefined;
233
233
  try { serialized = JSON.stringify(input); }
234
- catch { return { ok: false, error: { code: "invalid_request", message: "Invalid TypeSafe evaluation request." } }; }
235
- if (serialized === undefined) return { ok: false, error: { code: "invalid_request", message: "Invalid TypeSafe evaluation request." } };
234
+ catch { return { ok: false, error: { code: "invalid_request", message: "Invalid Jev evaluation request." } }; }
235
+ if (serialized === undefined) return { ok: false, error: { code: "invalid_request", message: "Invalid Jev evaluation request." } };
236
236
  const bytes = Buffer.byteLength(serialized, "utf8");
237
237
  if (bytes > jevSettings.maxEvaluationBytesPerScript - evaluationBytes) {
238
- return { ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation byte budget exhausted." } };
238
+ return { ok: false, error: { code: "budget_exhausted", message: "Jev evaluation byte budget exhausted." } };
239
239
  }
240
240
  evaluationBytes += bytes;
241
241
  return undefined;
@@ -257,7 +257,7 @@ export async function runMcpScript(
257
257
  envelope = chargeEvaluationTokens(envelope);
258
258
  throwIfAborted(callSignal);
259
259
  if (!reserveIntermediateBytes(JSON.stringify(envelope))) {
260
- envelope = { ok: false, error: { code: "budget_exhausted", message: "TypeSafe evaluation exceeds the remaining mcpScript intermediate transfer budget (16 MiB per script)." } };
260
+ envelope = { ok: false, error: { code: "budget_exhausted", message: "Jev evaluation exceeds the remaining mcpScript intermediate transfer budget (16 MiB per script)." } };
261
261
  }
262
262
  calls[index] = envelope.ok
263
263
  ? {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.36.0",
3
+ "version": "2.37.0",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "types": "./index.ts",
@@ -185,7 +185,7 @@
185
185
  "zod": "^3.25.0 || ^4.0.0"
186
186
  },
187
187
  "peerDependencies": {
188
- "@earendil-works/pi-ai": "^0.84.1 || ^0.85.0 || ^0.86.0",
188
+ "@earendil-works/pi-ai": "^0.84.1 || ^0.85.0 || ^0.86.0 || ^0.87.0",
189
189
  "@earendil-works/pi-tui": "*",
190
190
  "typebox": "*",
191
191
  "zod": "^3.25.0 || ^4.0.0"
@@ -202,9 +202,9 @@
202
202
  }
203
203
  },
204
204
  "devDependencies": {
205
- "@earendil-works/pi-ai": "0.86.0",
206
- "@earendil-works/pi-coding-agent": "0.86.0",
207
- "@earendil-works/pi-tui": "0.86.0",
205
+ "@earendil-works/pi-ai": "0.87.0",
206
+ "@earendil-works/pi-coding-agent": "0.87.0",
207
+ "@earendil-works/pi-tui": "0.87.0",
208
208
  "@modelcontextprotocol/conformance": "0.1.16",
209
209
  "@types/bun": "^1.0.0",
210
210
  "@types/cross-spawn": "6.0.6",
@@ -114,10 +114,10 @@ export async function semanticSearch(
114
114
  try {
115
115
  settings = resolveSemanticJevSettings(state);
116
116
  } catch {
117
- return { ok: false, error: { code: "invalid_request", message: "Invalid TypeSafe semantic search settings." } };
117
+ return { ok: false, error: { code: "invalid_request", message: "Invalid Jev semantic search settings." } };
118
118
  }
119
119
  if (!settings.semanticSearch) {
120
- return { ok: false, error: { code: "disabled", message: "TypeSafe semantic search is disabled." } };
120
+ return { ok: false, error: { code: "disabled", message: "Jev semantic search is disabled." } };
121
121
  }
122
122
  if (settings.allowedServers.length === 0) {
123
123
  return { ok: false, error: { code: "data_policy_denied", message: "Semantic search is enabled, but settings.jev.allowedServers is empty. Run /mcp jev setup or allow specific MCP servers." } };
@@ -166,7 +166,7 @@ export async function semanticSearch(
166
166
  }
167
167
  const answer = envelope.data.answers.match;
168
168
  if (!answer || answer.type !== "choice") {
169
- return { ok: false, error: { code: "invalid_response", message: "TypeSafe returned an invalid semantic search response." } };
169
+ return { ok: false, error: { code: "invalid_response", message: "Jev returned an invalid semantic search response." } };
170
170
  }
171
171
  const ranked = candidates
172
172
  .map(candidate => ({ candidate, probability: answer.probabilities[candidate.id] ?? 0 }))
@@ -589,6 +589,8 @@ export type { JevAnswer, JevErrorCode, JevEvaluateInput, JevEvaluationData, JevE
589
589
 
590
590
  export interface McpSettings {
591
591
  toolPrefix?: ToolPrefix;
592
+ /** Allow agents to persist remote MCP endpoints with the install action. Defaults to true. */
593
+ allowInstall?: boolean;
592
594
  /** Show the plug prefix in MCP status and connection text (default: true). Set to false to disable it. */
593
595
  showStatusIcon?: boolean;
594
596
  /** Footer status verbosity: full details, compact connected/enabled count, or no footer status. Defaults to full. */
@@ -603,6 +605,8 @@ export interface McpSettings {
603
605
  agentPluginPaths?: string[];
604
606
  idleTimeout?: number; // minutes, default 10, 0 to disable
605
607
  requestTimeoutMs?: number; // milliseconds, overrides the SDK request timeout when > 0
608
+ /** Defer lazy runtime startup even when persisted metadata is missing or invalid. Defaults to false. */
609
+ deferWithMissingMetadata?: boolean;
606
610
  directTools?: boolean | "search";
607
611
  /** Register per-server mcp__<server> namespace proxies. Defaults to true. */
608
612
  namespaceProxyTools?: boolean;
@@ -620,7 +624,9 @@ export interface McpSettings {
620
624
  warnOnLargeDirectTools?: boolean;
621
625
  /** Register the trusted MCP-only JavaScript scripting tool. Defaults to true; set false to hide it. */
622
626
  scriptMode?: boolean;
623
- /** Optional TypeSafe Jev integrations. A valid key enables semantic search; script evaluation remains disabled by default. */
627
+ /** Expose MCP resources as tools (default: true). Set to false to disable globally across all servers. */
628
+ exposeResources?: boolean;
629
+ /** Optional Jev (System One) integrations. A valid key enables semantic search; script evaluation remains disabled by default. */
624
630
  jev?: false | {
625
631
  semanticSearch?: boolean;
626
632
  scriptEvaluation?: boolean;
@@ -4,6 +4,18 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ ## [1.23.2] — 2026-09-24
8
+
9
+ ### Fixed
10
+
11
+ - Sessions hosted by pi-web's session daemon no longer deactivate after the first one. The in-process root-activation lease (designed for terminal Pi's `/reload` and in-process children) misclassified every later pi-web session as a subagent child, leaving it in `subagent-child-passive` mode with failover, preflight and auto-continue silently disabled. When `PI_WEB_SESSION=1` is present, each session now activates as an independent root; any other multi-session SDK host can opt in with the host-neutral `PI_MULTI_ACCOUNT_INDEPENDENT_ROOTS=1`. Genuine pi-subagents children still run in a runner process marked `PI_SUBAGENT_CHILD=1` and remain passive.
12
+ - In-process independent roots and same-session rehydrates now share a process-scoped canonical child proxy and publication lifetime. A sibling can exit in either order without restoring real OAuth into child-facing `auth.json` or orphaning `models.json` loopback routes; only the final root closes the listener and restores auth. A root whose proxy is disabled still retires public numbered aliases before another root publishes placeholders, and keeps that listener alive through handoff. The surviving root handles newly discovered slots, while explicit `PI_SUBAGENT_CHILD` remains passive.
13
+
14
+ ## [1.23.1] — 2026-09-23
15
+
16
+ ### Fixed
17
+
18
+ - Restore `pi-ai` as an installed runtime dependency for the OAuth/catalog bridge (#75). Pi-managed npm, bun and pnpm extension installs deliberately omit peer dependencies; v1.23.0 made pi-ai peer-only, so a fresh install could not log into a numbered subscription account. Native provider transports remain bound to the running Pi host, not this bridge copy. The package check now rejects future peer-only regressions.
7
19
 
8
20
  ## [1.23.0] — 2026-09-23
9
21
 
@@ -27,11 +27,12 @@ pi install npm:pi-multi-account
27
27
  Restart Pi or run `/reload` after installation.
28
28
 
29
29
  Requires Node 22+ and Agent Pi / pi-ai **>=0.85.1, <0.88.0** (CI covers 0.85.1, 0.86.1 and 0.87.1).
30
- Pi core packages are peers, not a private pinned transport dependency. Provider wrappers use Pi's
31
- host-bound imports, so the request adapter and host context format stay together; a stale nested
32
- pi-ai cannot silently remove tools or system instructions. Standalone SDK callers must pair their
33
- context with the installed pi-ai version. The OAuth/catalog bridge remains a separate, best-effort
34
- compatibility layer: unavailable subscription login does not prevent API-key account discovery.
30
+ Pi's host package is a peer. `pi-ai` is a runtime dependency for the OAuth/catalog bridge:
31
+ Pi-managed installs intentionally do not install extension peer dependencies, but subscription
32
+ login must be able to locate pi-ai on disk. Provider transports still use Pi's **host-bound**
33
+ imports, not that filesystem-resolved bridge copy, so a stale nested pi-ai cannot remove tools or
34
+ system instructions. Standalone SDK callers must pair their context with the installed pi-ai
35
+ version. If the OAuth bridge is unavailable, API-key account discovery still works.
35
36
 
36
37
  Kimi OAuth spares remain available in `/login`, but are not written to `models.json`: without a
37
38
  Kimi child OAuth proxy those aliases would be resolvable but unauthenticated. Only real API-key
@@ -178,7 +179,21 @@ Background attempts share the same cooldown and invalidation state as foreground
178
179
 
179
180
  ### pi-subagents and delegation-broker compatibility
180
181
 
181
- `pi-subagents` marks native child processes with `PI_SUBAGENT_CHILD=1` and owns their explicit model plus `fallbackModels` chain. Concurrent in-process SDK sessions also stay passive while a root session activation is live. The root lease is released at session shutdown, so `/reload`, `/new` and replacement root sessions do not permanently lose routing ownership. This is process-local ownership, not detection based on model names or a permanent first-factory flag. In those children this extension stays loaded only for provider/account registration, OAuth request shaping, and catalog support. It deliberately does **not** restore the interactive process's remembered model, persist the child's model as a user preference, switch models, queue work, or auto-continue after errors. The original provider error is returned unchanged so the parent runner can advance its verified fallback chain without a second router competing for model identity.
182
+ `pi-subagents` marks native child processes with `PI_SUBAGENT_CHILD=1` and owns their explicit model plus `fallbackModels` chain. Under terminal Pi, a distinct concurrent in-process child also stays passive while a root activation is live. A replacement with the **same nonempty session ID** reacquires that root's failover ownership without closing its child proxy. The root lease is released at final session shutdown, so `/reload` and `/new` do not permanently lose routing ownership. In passive children this extension stays loaded only for provider/account registration, OAuth request shaping, and catalog support. It deliberately does **not** restore the interactive process's remembered model, persist the child's model as a user preference, switch models, queue work, or auto-continue after errors. The original provider error is returned unchanged so the parent runner can advance its verified fallback chain without a second router competing for model identity.
183
+
184
+ ### Multi-session hosts (pi-web and other in-process SDK hosts)
185
+
186
+ Some hosts run many independent sessions in a single long-lived process — pi-web's session daemon, or in-process SDK hosts such as Enso (one session per chat thread). Terminal Pi's in-process root-activation lease would incorrectly demote every session but the first to passive in those hosts.
187
+
188
+ - Under pi-web (`PI_WEB_SESSION=1`, set by the daemon itself) every session activates as an independent root automatically.
189
+ - Any other multi-session host opts in by exporting `PI_MULTI_ACCOUNT_INDEPENDENT_ROOTS=1` in the host process. Set this only when each session truly owns its model; sessions that share one model identity must keep the default single-root behavior.
190
+ - Genuine `pi-subagents` children are unaffected either way: they run in a runner process marked `PI_SUBAGENT_CHILD=1`, which always takes precedence and stays passive.
191
+
192
+ Hosts should not set `PI_WEB_SESSION` themselves — other extensions read it to detect pi-web specifically.
193
+
194
+ In one process, independent roots share one canonical loopback proxy and its child-facing auth/models publication while any root has the child proxy enabled. Closing one root cannot expose its sibling's real OAuth or remove the route its bare children use; the final live root closes the listener and restores the original auth. A rehydrated session with the same ID replaces the old instance's failover ownership rather than counting as a second root. Another process holding the canonical port remains the publisher; a foreign listener is never allowed to overwrite its shared files.
195
+
196
+ Roots with `childProxy: false` still participate in the shared lifetime if another root publishes placeholders. Any public numbered alias they registered earlier is moved to loopback **before** the placeholder appears; they keep the canonical listener alive if the publisher exits first, and only the final root restores auth. With no other publishing root, disabling `childProxy` retains the usual direct route. Hosts should still keep one consistent setting across concurrent roots.
182
197
 
183
198
  [`pi-delegation-broker`](https://github.com/Sarrius/pi-delegation-broker) is an optional companion for splitting work among isolated child agents, with task budgets, reports and verification. Each extension works independently and depends on Agent Pi, not on the other extension. Together, Multi Account manages your interactive account rotation while the broker delegates through Pi's registered providers and models. Neither installs or initializes the other.
184
199