@psnext/slingcli 2.4.20260601-1 → 2.5.20260602-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.
Files changed (96) hide show
  1. package/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler/package.json +4 -4
  2. package/node_modules/@aws-sdk/credential-provider-node/dist-cjs/index.js +12 -1
  3. package/node_modules/@aws-sdk/credential-provider-node/dist-es/runtime/memoize-chain.js +12 -1
  4. package/node_modules/@aws-sdk/credential-provider-node/package.json +1 -1
  5. package/node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler/package.json +4 -4
  6. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +81 -18
  7. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js +1 -0
  8. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +1 -0
  9. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +14 -1
  10. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  11. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +197 -190
  12. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +84 -21
  13. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +24 -14
  14. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js +5 -9
  15. package/node_modules/@earendil-works/pi-ai/dist/providers/google-vertex.js +1 -1
  16. package/node_modules/@earendil-works/pi-ai/dist/providers/google.js +5 -3
  17. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.js +2 -3
  18. package/node_modules/@earendil-works/pi-ai/dist/providers/mistral.js +2 -3
  19. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.js +130 -62
  20. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js +18 -12
  21. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses-shared.js +4 -1
  22. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js +5 -9
  23. package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.js +1 -0
  24. package/node_modules/@earendil-works/pi-ai/dist/stream.js +14 -2
  25. package/node_modules/@earendil-works/pi-ai/dist/utils/abort-signals.js +34 -0
  26. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js +8 -7
  27. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js +1 -1
  28. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/index.js +1 -1
  29. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.js +179 -79
  30. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  31. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js +27 -0
  32. package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +9 -1
  33. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.js +1 -0
  34. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +29 -5
  35. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js +21 -3
  36. package/node_modules/@earendil-works/pi-coding-agent/dist/core/footer-data-provider.js +29 -1
  37. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js +65 -13
  38. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +1 -1
  39. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resolve-config-value.js +134 -11
  40. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +12 -6
  41. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +167 -96
  42. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js +14 -9
  43. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js +0 -3
  44. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.js +7 -10
  45. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.js +5 -7
  46. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +6 -7
  47. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/render-utils.js +17 -1
  48. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.js +5 -6
  49. package/node_modules/@earendil-works/pi-coding-agent/dist/index.js +2 -0
  50. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +69 -16
  51. package/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js +118 -1
  52. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js +1 -1
  53. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +2 -4
  54. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +61 -6
  55. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
  56. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/print-mode.js +1 -0
  57. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +4 -1
  58. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/deprecation.js +13 -0
  59. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/json.js +7 -0
  60. package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +71 -56
  61. package/node_modules/@earendil-works/pi-coding-agent/package.json +7 -7
  62. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +9 -53
  63. package/node_modules/@earendil-works/pi-tui/dist/components/input.js +6 -54
  64. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +34 -7
  65. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +172 -37
  66. package/node_modules/@earendil-works/pi-tui/dist/tui.js +166 -22
  67. package/node_modules/@earendil-works/pi-tui/dist/utils.js +8 -3
  68. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js +96 -0
  69. package/node_modules/@earendil-works/pi-tui/package.json +2 -2
  70. package/node_modules/@mariozechner/clipboard/Cargo.toml +3 -3
  71. package/node_modules/@mariozechner/clipboard/index.d.ts +34 -20
  72. package/node_modules/@mariozechner/clipboard/index.js +546 -257
  73. package/node_modules/@mariozechner/clipboard/package.json +14 -14
  74. package/node_modules/@mariozechner/clipboard/src/lib.rs +4 -9
  75. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/clipboard.linux-x64-gnu.node +0 -0
  76. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/package.json +2 -2
  77. package/node_modules/@smithy/core/dist-cjs/submodules/retry/index.browser.js +39 -22
  78. package/node_modules/@smithy/core/dist-cjs/submodules/retry/index.js +39 -22
  79. package/node_modules/@smithy/core/dist-es/submodules/retry/middleware-retry/configurations.js +19 -6
  80. package/node_modules/@smithy/core/dist-es/submodules/retry/middleware-retry/retryMiddleware.js +4 -5
  81. package/node_modules/@smithy/core/dist-es/submodules/retry/service-error-classification/constants.js +1 -1
  82. package/node_modules/@smithy/core/dist-es/submodules/retry/util-retry/ConfiguredRetryStrategy.js +4 -5
  83. package/node_modules/@smithy/core/dist-es/submodules/retry/util-retry/DefaultRetryToken.js +3 -0
  84. package/node_modules/@smithy/core/dist-es/submodules/retry/util-retry/StandardRetryStrategy.js +9 -5
  85. package/node_modules/@smithy/core/dist-types/submodules/retry/middleware-retry/configurations.d.ts +4 -1
  86. package/node_modules/@smithy/core/dist-types/submodules/retry/util-retry/ConfiguredRetryStrategy.d.ts +1 -2
  87. package/node_modules/@smithy/core/dist-types/submodules/retry/util-retry/DefaultRetryToken.d.ts +3 -0
  88. package/node_modules/@smithy/core/dist-types/submodules/retry/util-retry/StandardRetryStrategy.d.ts +1 -1
  89. package/node_modules/@smithy/core/package.json +2 -2
  90. package/node_modules/@smithy/credential-provider-imds/package.json +3 -3
  91. package/node_modules/@smithy/fetch-http-handler/package.json +4 -4
  92. package/node_modules/@smithy/signature-v4/package.json +3 -3
  93. package/node_modules/@smithy/types/dist-types/retry.d.ts +25 -0
  94. package/node_modules/@smithy/types/package.json +1 -1
  95. package/package.json +6 -6
  96. package/slingshot/index.js +231 -230
@@ -122,7 +122,7 @@ async function pollForGitHubAccessToken(domain, device, signal) {
122
122
  }),
123
123
  });
124
124
  if (raw && typeof raw === "object" && typeof raw.access_token === "string") {
125
- return { status: "complete", accessToken: raw.access_token };
125
+ return { status: "complete", value: raw.access_token };
126
126
  }
127
127
  if (raw && typeof raw === "object" && typeof raw.error === "string") {
128
128
  const { error, error_description: description } = raw;
@@ -12,7 +12,7 @@ export * from "./device-code.js";
12
12
  // GitHub Copilot
13
13
  export { getGitHubCopilotBaseUrl, githubCopilotOAuthProvider, loginGitHubCopilot, normalizeDomain, refreshGitHubCopilotToken, } from "./github-copilot.js";
14
14
  // OpenAI Codex (ChatGPT OAuth)
15
- export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken } from "./openai-codex.js";
15
+ export { loginOpenAICodex, loginOpenAICodexDeviceCode, OPENAI_CODEX_BROWSER_LOGIN_METHOD, OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD, openaiCodexOAuthProvider, refreshOpenAICodexToken, } from "./openai-codex.js";
16
16
  export * from "./types.js";
17
17
  // ============================================================================
18
18
  // Provider Registry
@@ -15,13 +15,22 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
15
15
  _http = m;
16
16
  });
17
17
  }
18
+ import { pollOAuthDeviceCodeFlow } from "./device-code.js";
18
19
  import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js";
19
20
  import { generatePKCE } from "./pkce.js";
20
21
  const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1";
21
22
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
22
- const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
23
- const TOKEN_URL = "https://auth.openai.com/oauth/token";
23
+ const AUTH_BASE_URL = "https://auth.openai.com";
24
+ const AUTHORIZE_URL = `${AUTH_BASE_URL}/oauth/authorize`;
25
+ const TOKEN_URL = `${AUTH_BASE_URL}/oauth/token`;
24
26
  const REDIRECT_URI = "http://localhost:1455/auth/callback";
27
+ const DEVICE_USER_CODE_URL = `${AUTH_BASE_URL}/api/accounts/deviceauth/usercode`;
28
+ const DEVICE_TOKEN_URL = `${AUTH_BASE_URL}/api/accounts/deviceauth/token`;
29
+ const DEVICE_VERIFICATION_URI = `${AUTH_BASE_URL}/codex/device`;
30
+ const DEVICE_REDIRECT_URI = `${AUTH_BASE_URL}/deviceauth/callback`;
31
+ const DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;
32
+ export const OPENAI_CODEX_BROWSER_LOGIN_METHOD = "browser";
33
+ export const OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD = "device_code";
25
34
  const SCOPE = "openid profile email offline_access";
26
35
  const JWT_CLAIM_PATH = "https://api.openai.com/auth";
27
36
  function createState() {
@@ -70,8 +79,35 @@ function decodeJwt(token) {
70
79
  return null;
71
80
  }
72
81
  }
73
- async function exchangeAuthorizationCode(code, verifier, redirectUri = REDIRECT_URI) {
74
- const response = await fetch(TOKEN_URL, {
82
+ async function fetchWithLoginCancellation(input, init) {
83
+ try {
84
+ return await fetch(input, init);
85
+ }
86
+ catch (error) {
87
+ if (init.signal?.aborted) {
88
+ throw new Error("Login cancelled");
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+ async function readTokenResponse(response, operation) {
94
+ if (!response.ok) {
95
+ const text = await response.text().catch(() => "");
96
+ throw new Error(`OpenAI Codex token ${operation} failed (${response.status}): ${text || response.statusText}`);
97
+ }
98
+ const rawJson = await response.json();
99
+ const json = rawJson;
100
+ if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
101
+ throw new Error(`OpenAI Codex token ${operation} response missing fields: ${JSON.stringify(json)}`);
102
+ }
103
+ return {
104
+ access: json.access_token,
105
+ refresh: json.refresh_token,
106
+ expires: Date.now() + json.expires_in * 1000,
107
+ };
108
+ }
109
+ async function exchangeAuthorizationCode(code, verifier, redirectUri = REDIRECT_URI, signal) {
110
+ const response = await fetchWithLoginCancellation(TOKEN_URL, {
75
111
  method: "POST",
76
112
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
77
113
  body: new URLSearchParams({
@@ -81,32 +117,14 @@ async function exchangeAuthorizationCode(code, verifier, redirectUri = REDIRECT_
81
117
  code_verifier: verifier,
82
118
  redirect_uri: redirectUri,
83
119
  }),
120
+ signal,
84
121
  });
85
- if (!response.ok) {
86
- const text = await response.text().catch(() => "");
87
- return {
88
- type: "failed",
89
- status: response.status,
90
- message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`,
91
- };
92
- }
93
- const json = (await response.json());
94
- if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
95
- return {
96
- type: "failed",
97
- message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`,
98
- };
99
- }
100
- return {
101
- type: "success",
102
- access: json.access_token,
103
- refresh: json.refresh_token,
104
- expires: Date.now() + json.expires_in * 1000,
105
- };
122
+ return readTokenResponse(response, "exchange");
106
123
  }
107
124
  async function refreshAccessToken(refreshToken) {
125
+ let response;
108
126
  try {
109
- const response = await fetch(TOKEN_URL, {
127
+ response = await fetch(TOKEN_URL, {
110
128
  method: "POST",
111
129
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
112
130
  body: new URLSearchParams({
@@ -115,34 +133,94 @@ async function refreshAccessToken(refreshToken) {
115
133
  client_id: CLIENT_ID,
116
134
  }),
117
135
  });
118
- if (!response.ok) {
119
- const text = await response.text().catch(() => "");
120
- return {
121
- type: "failed",
122
- status: response.status,
123
- message: `OpenAI Codex token refresh failed (${response.status}): ${text || response.statusText}`,
124
- };
125
- }
126
- const json = (await response.json());
127
- if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
128
- return {
129
- type: "failed",
130
- message: `OpenAI Codex token refresh response missing fields: ${JSON.stringify(json)}`,
131
- };
132
- }
133
- return {
134
- type: "success",
135
- access: json.access_token,
136
- refresh: json.refresh_token,
137
- expires: Date.now() + json.expires_in * 1000,
138
- };
139
136
  }
140
137
  catch (error) {
141
- return {
142
- type: "failed",
143
- message: `OpenAI Codex token refresh error: ${error instanceof Error ? error.message : String(error)}`,
144
- };
138
+ throw new Error(`OpenAI Codex token refresh error: ${error instanceof Error ? error.message : String(error)}`);
145
139
  }
140
+ return readTokenResponse(response, "refresh");
141
+ }
142
+ async function startOpenAICodexDeviceAuth(signal) {
143
+ const response = await fetchWithLoginCancellation(DEVICE_USER_CODE_URL, {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({ client_id: CLIENT_ID }),
147
+ signal,
148
+ });
149
+ if (!response.ok) {
150
+ if (response.status === 404) {
151
+ throw new Error("OpenAI Codex device code login is not enabled for this server. Use browser login or verify the server URL.");
152
+ }
153
+ const responseBody = await response.text().catch(() => "");
154
+ throw new Error(`OpenAI Codex device code request failed with status ${response.status}${responseBody ? `: ${responseBody}` : ""}`);
155
+ }
156
+ const rawJson = await response.json();
157
+ const json = rawJson;
158
+ const intervalSeconds = typeof json?.interval === "string" ? Number(json.interval.trim()) : json?.interval;
159
+ if (!json?.device_auth_id ||
160
+ !json.user_code ||
161
+ typeof intervalSeconds !== "number" ||
162
+ !Number.isFinite(intervalSeconds) ||
163
+ intervalSeconds < 0) {
164
+ throw new Error(`Invalid OpenAI Codex device code response: ${JSON.stringify(json)}`);
165
+ }
166
+ return {
167
+ deviceAuthId: json.device_auth_id,
168
+ userCode: json.user_code,
169
+ intervalSeconds,
170
+ };
171
+ }
172
+ async function pollOpenAICodexDeviceAuth(device, signal) {
173
+ return pollOAuthDeviceCodeFlow({
174
+ intervalSeconds: device.intervalSeconds,
175
+ expiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,
176
+ signal,
177
+ poll: async () => {
178
+ const response = await fetchWithLoginCancellation(DEVICE_TOKEN_URL, {
179
+ method: "POST",
180
+ headers: { "Content-Type": "application/json" },
181
+ body: JSON.stringify({
182
+ device_auth_id: device.deviceAuthId,
183
+ user_code: device.userCode,
184
+ }),
185
+ signal,
186
+ });
187
+ if (response.ok) {
188
+ const rawJson = await response.json();
189
+ const json = rawJson;
190
+ if (!json?.authorization_code || !json.code_verifier) {
191
+ return {
192
+ status: "failed",
193
+ message: `Invalid OpenAI Codex device auth token response: ${JSON.stringify(json)}`,
194
+ };
195
+ }
196
+ return {
197
+ status: "complete",
198
+ value: { authorizationCode: json.authorization_code, codeVerifier: json.code_verifier },
199
+ };
200
+ }
201
+ if (response.status === 403 || response.status === 404) {
202
+ return { status: "pending" };
203
+ }
204
+ const responseBody = await response.text().catch(() => "");
205
+ let errorCode;
206
+ try {
207
+ const json = JSON.parse(responseBody);
208
+ const error = json?.error;
209
+ errorCode = typeof error === "object" ? error?.code : error;
210
+ }
211
+ catch { }
212
+ if (errorCode === "deviceauth_authorization_pending") {
213
+ return { status: "pending" };
214
+ }
215
+ if (errorCode === "slow_down") {
216
+ return { status: "slow_down" };
217
+ }
218
+ return {
219
+ status: "failed",
220
+ message: `OpenAI Codex device auth failed with status ${response.status}${responseBody ? `: ${responseBody}` : ""}`,
221
+ };
222
+ },
223
+ });
146
224
  }
147
225
  async function createAuthorizationFlow(originator = "pi") {
148
226
  const { verifier, challenge } = await generatePKCE();
@@ -241,6 +319,35 @@ function getAccountId(accessToken) {
241
319
  const accountId = auth?.chatgpt_account_id;
242
320
  return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
243
321
  }
322
+ function credentialsFromToken(token) {
323
+ const accountId = getAccountId(token.access);
324
+ if (!accountId) {
325
+ throw new Error("Failed to extract accountId from token");
326
+ }
327
+ return {
328
+ access: token.access,
329
+ refresh: token.refresh,
330
+ expires: token.expires,
331
+ accountId,
332
+ };
333
+ }
334
+ async function exchangeAuthorizationCodeForCredentials(code, verifier, redirectUri, signal) {
335
+ return credentialsFromToken(await exchangeAuthorizationCode(code, verifier, redirectUri, signal));
336
+ }
337
+ /**
338
+ * Login with OpenAI Codex OAuth using the Codex device-code flow.
339
+ */
340
+ export async function loginOpenAICodexDeviceCode(options) {
341
+ const device = await startOpenAICodexDeviceAuth(options.signal);
342
+ options.onDeviceCode({
343
+ userCode: device.userCode,
344
+ verificationUri: DEVICE_VERIFICATION_URI,
345
+ intervalSeconds: device.intervalSeconds,
346
+ expiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,
347
+ });
348
+ const code = await pollOpenAICodexDeviceAuth(device, options.signal);
349
+ return exchangeAuthorizationCodeForCredentials(code.authorizationCode, code.codeVerifier, DEVICE_REDIRECT_URI, options.signal);
350
+ }
244
351
  /**
245
352
  * Login with OpenAI Codex OAuth
246
353
  *
@@ -325,20 +432,7 @@ export async function loginOpenAICodex(options) {
325
432
  if (!code) {
326
433
  throw new Error("Missing authorization code");
327
434
  }
328
- const tokenResult = await exchangeAuthorizationCode(code, verifier);
329
- if (tokenResult.type !== "success") {
330
- throw new Error(tokenResult.message);
331
- }
332
- const accountId = getAccountId(tokenResult.access);
333
- if (!accountId) {
334
- throw new Error("Failed to extract accountId from token");
335
- }
336
- return {
337
- access: tokenResult.access,
338
- refresh: tokenResult.refresh,
339
- expires: tokenResult.expires,
340
- accountId,
341
- };
435
+ return exchangeAuthorizationCodeForCredentials(code, verifier, REDIRECT_URI);
342
436
  }
343
437
  finally {
344
438
  server.close();
@@ -348,26 +442,32 @@ export async function loginOpenAICodex(options) {
348
442
  * Refresh OpenAI Codex OAuth token
349
443
  */
350
444
  export async function refreshOpenAICodexToken(refreshToken) {
351
- const result = await refreshAccessToken(refreshToken);
352
- if (result.type !== "success") {
353
- throw new Error(result.message);
354
- }
355
- const accountId = getAccountId(result.access);
356
- if (!accountId) {
357
- throw new Error("Failed to extract accountId from token");
358
- }
359
- return {
360
- access: result.access,
361
- refresh: result.refresh,
362
- expires: result.expires,
363
- accountId,
364
- };
445
+ return credentialsFromToken(await refreshAccessToken(refreshToken));
365
446
  }
366
447
  export const openaiCodexOAuthProvider = {
367
448
  id: "openai-codex",
368
449
  name: "ChatGPT Plus/Pro (Codex Subscription)",
369
450
  usesCallbackServer: true,
370
451
  async login(callbacks) {
452
+ const loginMethod = await callbacks.onSelect({
453
+ message: "Select OpenAI Codex login method:",
454
+ options: [
455
+ { id: OPENAI_CODEX_BROWSER_LOGIN_METHOD, label: "Browser login (default)" },
456
+ { id: OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD, label: "Device code login (headless)" },
457
+ ],
458
+ });
459
+ if (!loginMethod) {
460
+ throw new Error("Login cancelled");
461
+ }
462
+ if (loginMethod === OPENAI_CODEX_DEVICE_CODE_LOGIN_METHOD) {
463
+ return loginOpenAICodexDeviceCode({
464
+ onDeviceCode: callbacks.onDeviceCode,
465
+ signal: callbacks.signal,
466
+ });
467
+ }
468
+ if (loginMethod !== OPENAI_CODEX_BROWSER_LOGIN_METHOD) {
469
+ throw new Error(`Unknown OpenAI Codex login method: ${loginMethod}`);
470
+ }
371
471
  return loginOpenAICodex({
372
472
  onAuth: callbacks.onAuth,
373
473
  onPrompt: callbacks.onPrompt,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earendil-works/pi-ai",
3
- "version": "0.75.5",
3
+ "version": "0.78.0",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -92,7 +92,7 @@
92
92
  "license": "MIT",
93
93
  "repository": {
94
94
  "type": "git",
95
- "url": "git+https://github.com/earendil-works/pi-mono.git",
95
+ "url": "git+https://github.com/earendil-works/pi.git",
96
96
  "directory": "packages/ai"
97
97
  },
98
98
  "engines": {
@@ -50,12 +50,23 @@ export function parseArgs(args) {
50
50
  result.appendSystemPrompt = result.appendSystemPrompt ?? [];
51
51
  result.appendSystemPrompt.push(args[++i]);
52
52
  }
53
+ else if (arg === "--name" || arg === "-n") {
54
+ if (i + 1 < args.length) {
55
+ result.name = args[++i];
56
+ }
57
+ else {
58
+ result.diagnostics.push({ type: "error", message: "--name requires a value" });
59
+ }
60
+ }
53
61
  else if (arg === "--no-session") {
54
62
  result.noSession = true;
55
63
  }
56
64
  else if (arg === "--session" && i + 1 < args.length) {
57
65
  result.session = args[++i];
58
66
  }
67
+ else if (arg === "--session-id" && i + 1 < args.length) {
68
+ result.sessionId = args[++i];
69
+ }
59
70
  else if (arg === "--fork" && i + 1 < args.length) {
60
71
  result.fork = args[++i];
61
72
  }
@@ -77,6 +88,12 @@ export function parseArgs(args) {
77
88
  .map((s) => s.trim())
78
89
  .filter((name) => name.length > 0);
79
90
  }
91
+ else if ((arg === "--exclude-tools" || arg === "-xt") && i + 1 < args.length) {
92
+ result.excludeTools = args[++i]
93
+ .split(",")
94
+ .map((s) => s.trim())
95
+ .filter((name) => name.length > 0);
96
+ }
80
97
  else if (arg === "--thinking" && i + 1 < args.length) {
81
98
  const level = args[++i];
82
99
  if (isValidThinkingLevel(level)) {
@@ -210,15 +227,19 @@ ${chalk.bold("Options:")}
210
227
  --continue, -c Continue previous session
211
228
  --resume, -r Select a session to resume
212
229
  --session <path|id> Use specific session file or partial UUID
230
+ --session-id <id> Use exact project session ID, creating it if missing
213
231
  --fork <path|id> Fork specific session file or partial UUID into a new session
214
232
  --session-dir <dir> Directory for session storage and lookup
215
233
  --no-session Don't save session (ephemeral)
234
+ --name, -n <name> Set session display name
216
235
  --models <patterns> Comma-separated model patterns for Ctrl+P cycling
217
236
  Supports globs (anthropic/*, *sonnet*) and fuzzy matching
218
237
  --no-tools, -nt Disable all tools by default (built-in and extension)
219
238
  --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled
220
239
  --tools, -t <tools> Comma-separated allowlist of tool names to enable
221
240
  Applies to built-in, extension, and custom tools
241
+ --exclude-tools, -xt <tools> Comma-separated denylist of tool names to disable
242
+ Applies to built-in, extension, and custom tools
222
243
  --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh
223
244
  --extension, -e <path> Load an extension file (can be used multiple times)
224
245
  --no-extensions, -ne Disable extension discovery (explicit -e paths still work)
@@ -257,6 +278,9 @@ ${chalk.bold("Examples:")}
257
278
  # Continue previous session
258
279
  ${APP_NAME} --continue "What did we discuss?"
259
280
 
281
+ # Start a named session
282
+ ${APP_NAME} --name "Refactor auth module"
283
+
260
284
  # Use different model
261
285
  ${APP_NAME} --provider openai --model gpt-4o-mini "Help me refactor this code"
262
286
 
@@ -281,6 +305,9 @@ ${chalk.bold("Examples:")}
281
305
  # Read-only mode (no file modifications possible)
282
306
  ${APP_NAME} --tools read,grep,find,ls -p "Review the code in src/"
283
307
 
308
+ # Disable one tool while keeping the rest available
309
+ ${APP_NAME} --exclude-tools ask_question
310
+
284
311
  # Export a session file to HTML
285
312
  ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl
286
313
  ${APP_NAME} --export session.jsonl output.html
@@ -359,7 +359,15 @@ export function getInteractiveAssetsDir() {
359
359
  export function getBundledInteractiveAssetPath(name) {
360
360
  return join(getInteractiveAssetsDir(), name);
361
361
  }
362
- const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
362
+ let pkg = {};
363
+ try {
364
+ pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
365
+ }
366
+ catch (e) {
367
+ const err = e;
368
+ if (err.code !== "ENOENT")
369
+ throw e;
370
+ }
363
371
  const piConfigName = pkg.piConfig?.name;
364
372
  export const PACKAGE_NAME = pkg.name || "@earendil-works/pi-coding-agent";
365
373
  export const APP_NAME = piConfigName || "pi";
@@ -110,6 +110,7 @@ export async function createAgentSessionFromServices(options) {
110
110
  thinkingLevel: options.thinkingLevel,
111
111
  scopedModels: options.scopedModels,
112
112
  tools: options.tools,
113
+ excludeTools: options.excludeTools,
113
114
  noTools: options.noTools,
114
115
  customTools: options.customTools,
115
116
  sessionStartEvent: options.sessionStartEvent,
@@ -93,9 +93,11 @@ export class AgentSession {
93
93
  _extensionRunnerRef;
94
94
  _initialActiveToolNames;
95
95
  _allowedToolNames;
96
+ _excludedToolNames;
96
97
  _baseToolsOverride;
97
98
  _sessionStartEvent;
98
99
  _extensionUIContext;
100
+ _extensionMode = "print";
99
101
  _extensionCommandContextActions;
100
102
  _extensionAbortHandler;
101
103
  _extensionShutdownHandler;
@@ -123,6 +125,7 @@ export class AgentSession {
123
125
  this._extensionRunnerRef = config.extensionRunnerRef;
124
126
  this._initialActiveToolNames = config.initialActiveToolNames;
125
127
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
128
+ this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;
126
129
  this._baseToolsOverride = config.baseToolsOverride;
127
130
  this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
128
131
  // Always subscribe to agent events for internal handling
@@ -467,6 +470,16 @@ export class AgentSession {
467
470
  * Call this when completely done with the session.
468
471
  */
469
472
  dispose() {
473
+ try {
474
+ this.abortRetry();
475
+ this.abortCompaction();
476
+ this.abortBranchSummary();
477
+ this.abortBash();
478
+ this.agent.abort();
479
+ }
480
+ catch {
481
+ // Dispose must succeed even if an abort hook throws.
482
+ }
470
483
  this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
471
484
  this._disconnectFromAgent();
472
485
  this._eventListeners = [];
@@ -507,13 +520,14 @@ export class AgentSession {
507
520
  return this.agent.state.tools.map((t) => t.name);
508
521
  }
509
522
  /**
510
- * Get all configured tools with name, description, parameter schema, and source metadata.
523
+ * Get all configured tools with name, description, parameter schema, prompt guidelines, and source metadata.
511
524
  */
512
525
  getAllTools() {
513
526
  return Array.from(this._toolDefinitions.values()).map(({ definition, sourceInfo }) => ({
514
527
  name: definition.name,
515
528
  description: definition.description,
516
529
  parameters: definition.parameters,
530
+ promptGuidelines: definition.promptGuidelines,
517
531
  sourceInfo,
518
532
  }));
519
533
  }
@@ -668,7 +682,12 @@ export class AgentSession {
668
682
  });
669
683
  this._retryAttempt = 0;
670
684
  }
671
- return await this._checkCompaction(msg);
685
+ if (await this._checkCompaction(msg)) {
686
+ return true;
687
+ }
688
+ // The agent loop drains both queues before emitting agent_end. Any messages
689
+ // here were queued by agent_end extension handlers and need a continuation.
690
+ return this.agent.hasQueuedMessages();
672
691
  }
673
692
  /**
674
693
  * Send a prompt to the agent.
@@ -698,7 +717,7 @@ export class AgentSession {
698
717
  let currentText = text;
699
718
  let currentImages = options?.images;
700
719
  if (this._extensionRunner.hasHandlers("input")) {
701
- const inputResult = await this._extensionRunner.emitInput(currentText, currentImages, options?.source ?? "interactive");
720
+ const inputResult = await this._extensionRunner.emitInput(currentText, currentImages, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined);
702
721
  if (inputResult.action === "handled") {
703
722
  preflightResult?.(true);
704
723
  return;
@@ -1611,6 +1630,9 @@ export class AgentSession {
1611
1630
  if (bindings.uiContext !== undefined) {
1612
1631
  this._extensionUIContext = bindings.uiContext;
1613
1632
  }
1633
+ if (bindings.mode !== undefined) {
1634
+ this._extensionMode = bindings.mode;
1635
+ }
1614
1636
  if (bindings.commandContextActions !== undefined) {
1615
1637
  this._extensionCommandContextActions = bindings.commandContextActions;
1616
1638
  }
@@ -1668,7 +1690,7 @@ export class AgentSession {
1668
1690
  return `extension:${name}`;
1669
1691
  }
1670
1692
  _applyExtensionBindings(runner) {
1671
- runner.setUIContext(this._extensionUIContext);
1693
+ runner.setUIContext(this._extensionUIContext, this._extensionMode);
1672
1694
  runner.bindCommandContext(this._extensionCommandContextActions);
1673
1695
  this._extensionErrorUnsubscriber?.();
1674
1696
  this._extensionErrorUnsubscriber = this._extensionErrorListener
@@ -1781,6 +1803,7 @@ export class AgentSession {
1781
1803
  })();
1782
1804
  },
1783
1805
  getSystemPrompt: () => this.systemPrompt,
1806
+ getSystemPromptOptions: () => this._baseSystemPromptOptions,
1784
1807
  }, {
1785
1808
  registerProvider: (name, config) => {
1786
1809
  this._modelRegistry.registerProvider(name, config);
@@ -1796,7 +1819,8 @@ export class AgentSession {
1796
1819
  const previousRegistryNames = new Set(this._toolRegistry.keys());
1797
1820
  const previousActiveToolNames = this.getActiveToolNames();
1798
1821
  const allowedToolNames = this._allowedToolNames;
1799
- const isAllowedTool = (name) => !allowedToolNames || allowedToolNames.has(name);
1822
+ const excludedToolNames = this._excludedToolNames;
1823
+ const isAllowedTool = (name) => (!allowedToolNames || allowedToolNames.has(name)) && !excludedToolNames?.has(name);
1800
1824
  const registeredTools = this._extensionRunner.getAllRegisteredTools();
1801
1825
  const allCustomTools = [
1802
1826
  ...registeredTools,
@@ -92,6 +92,7 @@ export class ExtensionRunner {
92
92
  extensions;
93
93
  runtime;
94
94
  uiContext;
95
+ mode = "print";
95
96
  cwd;
96
97
  sessionManager;
97
98
  modelRegistry;
@@ -105,6 +106,7 @@ export class ExtensionRunner {
105
106
  getContextUsageFn = () => undefined;
106
107
  compactFn = () => { };
107
108
  getSystemPromptFn = () => "";
109
+ getSystemPromptOptionsFn = () => ({ cwd: this.cwd });
108
110
  newSessionHandler = async () => ({ cancelled: false });
109
111
  forkHandler = async () => ({ cancelled: false });
110
112
  navigateTreeHandler = async () => ({ cancelled: false });
@@ -148,6 +150,7 @@ export class ExtensionRunner {
148
150
  this.getContextUsageFn = contextActions.getContextUsage;
149
151
  this.compactFn = contextActions.compact;
150
152
  this.getSystemPromptFn = contextActions.getSystemPrompt;
153
+ this.getSystemPromptOptionsFn = contextActions.getSystemPromptOptions ?? (() => ({ cwd: this.cwd }));
151
154
  // Flush provider registrations queued during extension loading
152
155
  for (const { name, config, extensionPath } of this.runtime.pendingProviderRegistrations) {
153
156
  try {
@@ -202,8 +205,9 @@ export class ExtensionRunner {
202
205
  this.switchSessionHandler = async () => ({ cancelled: false });
203
206
  this.reloadHandler = async () => { };
204
207
  }
205
- setUIContext(uiContext) {
208
+ setUIContext(uiContext, mode = "print") {
206
209
  this.uiContext = uiContext ?? noOpUIContext;
210
+ this.mode = mode;
207
211
  }
208
212
  getUIContext() {
209
213
  return this.uiContext;
@@ -382,6 +386,10 @@ export class ExtensionRunner {
382
386
  runner.assertActive();
383
387
  return runner.uiContext;
384
388
  },
389
+ get mode() {
390
+ runner.assertActive();
391
+ return runner.mode;
392
+ },
385
393
  get hasUI() {
386
394
  runner.assertActive();
387
395
  return runner.hasUI();
@@ -441,6 +449,10 @@ export class ExtensionRunner {
441
449
  // createContext() stay lazy. A spread would eagerly read them once and freeze the
442
450
  // old values into the returned object, bypassing stale-instance checks.
443
451
  const context = Object.defineProperties({}, Object.getOwnPropertyDescriptors(this.createContext()));
452
+ context.getSystemPromptOptions = () => {
453
+ this.assertActive();
454
+ return this.getSystemPromptOptionsFn();
455
+ };
444
456
  context.waitForIdle = () => {
445
457
  this.assertActive();
446
458
  return this.waitForIdleFn();
@@ -790,14 +802,20 @@ export class ExtensionRunner {
790
802
  return { skillPaths, promptPaths, themePaths };
791
803
  }
792
804
  /** Emit input event. Transforms chain, "handled" short-circuits. */
793
- async emitInput(text, images, source) {
805
+ async emitInput(text, images, source, streamingBehavior) {
794
806
  const ctx = this.createContext();
795
807
  let currentText = text;
796
808
  let currentImages = images;
797
809
  for (const ext of this.extensions) {
798
810
  for (const handler of ext.handlers.get("input") ?? []) {
799
811
  try {
800
- const event = { type: "input", text: currentText, images: currentImages, source };
812
+ const event = {
813
+ type: "input",
814
+ text: currentText,
815
+ images: currentImages,
816
+ source,
817
+ streamingBehavior,
818
+ };
801
819
  const result = (await handler(event, ctx));
802
820
  if (result?.action === "handled")
803
821
  return result;