@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
@@ -6,7 +6,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
6
6
  import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
7
7
  import { withFileMutationQueue } from "./file-mutation-queue.js";
8
8
  import { resolveToCwd } from "./path-utils.js";
9
- import { invalidArgText, normalizeDisplayText, replaceTabs, shortenPath, str } from "./render-utils.js";
9
+ import { normalizeDisplayText, renderToolPath, replaceTabs, str } from "./render-utils.js";
10
10
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
11
11
  const writeSchema = Type.Object({
12
12
  path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
@@ -90,12 +90,11 @@ function trimTrailingEmptyLines(lines) {
90
90
  }
91
91
  return lines.slice(0, end);
92
92
  }
93
- function formatWriteCall(args, options, theme, cache) {
93
+ function formatWriteCall(args, options, theme, cache, cwd) {
94
94
  const rawPath = str(args?.file_path ?? args?.path);
95
95
  const fileContent = str(args?.content);
96
- const path = rawPath !== null ? shortenPath(rawPath) : null;
97
- const invalidArg = invalidArgText(theme);
98
- let text = `${theme.fg("toolTitle", theme.bold("write"))} ${path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...")}`;
96
+ const pathDisplay = renderToolPath(rawPath, theme, cwd);
97
+ let text = `${theme.fg("toolTitle", theme.bold("write"))} ${pathDisplay}`;
99
98
  if (fileContent === null) {
100
99
  text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
101
100
  }
@@ -176,7 +175,7 @@ export function createWriteToolDefinition(cwd, options) {
176
175
  else {
177
176
  component.cache = undefined;
178
177
  }
179
- component.setText(formatWriteCall(renderArgs, { expanded: context.expanded, isPartial: context.isPartial }, theme, component.cache));
178
+ component.setText(formatWriteCall(renderArgs, { expanded: context.expanded, isPartial: context.isPartial }, theme, component.cache, context.cwd));
180
179
  return component;
181
180
  },
182
181
  renderResult(result, _options, theme, context) {
@@ -1,4 +1,5 @@
1
1
  // Core session management
2
+ export { parseArgs } from "./cli/args.js";
2
3
  // Config paths
3
4
  export { getAgentDir, VERSION } from "./config.js";
4
5
  export { AgentSession, parseSkillBlock, } from "./core/agent-session.js";
@@ -36,6 +37,7 @@ export { getLanguageFromPath, getMarkdownTheme, getSelectListTheme, getSettingsL
36
37
  // Clipboard utilities
37
38
  export { copyToClipboard } from "./utils/clipboard.js";
38
39
  export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
40
+ export { convertToPng } from "./utils/image-convert.js";
39
41
  export { formatDimensionNote, resizeImage } from "./utils/image-resize.js";
40
42
  // Shell utilities
41
43
  export { getShellConfig } from "./utils/shell.js";
@@ -24,7 +24,7 @@ import { KeybindingsManager } from "./core/keybindings.js";
24
24
  import { resolveCliModel, resolveModelScope } from "./core/model-resolver.js";
25
25
  import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
26
26
  import { formatMissingSessionCwdPrompt, getMissingSessionCwdIssue, MissingSessionCwdError, } from "./core/session-cwd.js";
27
- import { SessionManager } from "./core/session-manager.js";
27
+ import { assertValidSessionId, SessionManager } from "./core/session-manager.js";
28
28
  import { SettingsManager } from "./core/settings-manager.js";
29
29
  import { printTimings, resetTimings, time } from "./core/timings.js";
30
30
  import { runMigrations, showDeprecationWarnings } from "./migrations.js";
@@ -104,6 +104,11 @@ async function prepareInitialMessage(parsed, autoResizeImages, stdinContent) {
104
104
  * Resolve a session argument to a file path.
105
105
  * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.
106
106
  */
107
+ async function findLocalSessionByExactId(sessionId, cwd, sessionDir) {
108
+ const localSessions = await SessionManager.list(cwd, sessionDir);
109
+ const localMatch = localSessions.find((s) => s.id === sessionId);
110
+ return localMatch ? { type: "local", path: localMatch.path } : undefined;
111
+ }
107
112
  async function resolveSessionPath(sessionArg, cwd, sessionDir) {
108
113
  // If it looks like a file path, resolve it before handing it to the session manager.
109
114
  if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
@@ -111,16 +116,15 @@ async function resolveSessionPath(sessionArg, cwd, sessionDir) {
111
116
  }
112
117
  // Try to match as session ID in current project first
113
118
  const localSessions = await SessionManager.list(cwd, sessionDir);
114
- const localMatches = localSessions.filter((s) => s.id.startsWith(sessionArg));
115
- if (localMatches.length >= 1) {
116
- return { type: "local", path: localMatches[0].path };
119
+ const localMatch = localSessions.find((s) => s.id === sessionArg) ?? localSessions.find((s) => s.id.startsWith(sessionArg));
120
+ if (localMatch) {
121
+ return { type: "local", path: localMatch.path };
117
122
  }
118
123
  // Try global search across all projects
119
- const allSessions = await SessionManager.listAll();
120
- const globalMatches = allSessions.filter((s) => s.id.startsWith(sessionArg));
121
- if (globalMatches.length >= 1) {
122
- const match = globalMatches[0];
123
- return { type: "global", path: match.path, cwd: match.cwd };
124
+ const allSessions = await SessionManager.listAll(sessionDir);
125
+ const globalMatch = allSessions.find((s) => s.id === sessionArg) ?? allSessions.find((s) => s.id.startsWith(sessionArg));
126
+ if (globalMatch) {
127
+ return { type: "global", path: globalMatch.path, cwd: globalMatch.cwd };
124
128
  }
125
129
  // Not found anywhere
126
130
  return { type: "not_found", arg: sessionArg };
@@ -152,9 +156,31 @@ function validateForkFlags(parsed) {
152
156
  process.exit(1);
153
157
  }
154
158
  }
155
- function forkSessionOrExit(sourcePath, cwd, sessionDir) {
159
+ function validateSessionIdFlags(parsed) {
160
+ if (parsed.sessionId === undefined)
161
+ return;
162
+ const conflictingFlags = [
163
+ parsed.session ? "--session" : undefined,
164
+ parsed.continue ? "--continue" : undefined,
165
+ parsed.resume ? "--resume" : undefined,
166
+ parsed.noSession ? "--no-session" : undefined,
167
+ ].filter((flag) => flag !== undefined);
168
+ if (conflictingFlags.length > 0) {
169
+ console.error(chalk.red(`Error: --session-id cannot be combined with ${conflictingFlags.join(", ")}`));
170
+ process.exit(1);
171
+ }
172
+ try {
173
+ assertValidSessionId(parsed.sessionId);
174
+ }
175
+ catch (error) {
176
+ const message = error instanceof Error ? error.message : String(error);
177
+ console.error(chalk.red(`Error: ${message}`));
178
+ process.exit(1);
179
+ }
180
+ }
181
+ function forkSessionOrExit(sourcePath, cwd, sessionDir, sessionId) {
156
182
  try {
157
- return SessionManager.forkFrom(sourcePath, cwd, sessionDir);
183
+ return SessionManager.forkFrom(sourcePath, cwd, sessionDir, { id: sessionId });
158
184
  }
159
185
  catch (error) {
160
186
  const message = error instanceof Error ? error.message : String(error);
@@ -163,16 +189,23 @@ function forkSessionOrExit(sourcePath, cwd, sessionDir) {
163
189
  }
164
190
  }
165
191
  async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
166
- if (parsed.noSession) {
167
- return SessionManager.inMemory();
192
+ if (parsed.noSession || parsed.help || parsed.listModels !== undefined) {
193
+ return SessionManager.inMemory(cwd);
168
194
  }
169
195
  if (parsed.fork) {
196
+ if (parsed.sessionId) {
197
+ const existingTarget = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
198
+ if (existingTarget) {
199
+ console.error(chalk.red(`Session already exists with id '${parsed.sessionId}'`));
200
+ process.exit(1);
201
+ }
202
+ }
170
203
  const resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);
171
204
  switch (resolved.type) {
172
205
  case "path":
173
206
  case "local":
174
207
  case "global":
175
- return forkSessionOrExit(resolved.path, cwd, sessionDir);
208
+ return forkSessionOrExit(resolved.path, cwd, sessionDir, parsed.sessionId);
176
209
  case "not_found":
177
210
  console.error(chalk.red(`No session found matching '${resolved.arg}'`));
178
211
  process.exit(1);
@@ -201,7 +234,7 @@ async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
201
234
  if (parsed.resume) {
202
235
  initTheme(settingsManager.getTheme(), true);
203
236
  try {
204
- const selectedPath = await selectSession((onProgress) => SessionManager.list(cwd, sessionDir, onProgress), SessionManager.listAll);
237
+ const selectedPath = await selectSession((onProgress) => SessionManager.list(cwd, sessionDir, onProgress), (onProgress) => SessionManager.listAll(sessionDir, onProgress));
205
238
  if (!selectedPath) {
206
239
  console.log(chalk.dim("No session selected"));
207
240
  process.exit(0);
@@ -215,7 +248,13 @@ async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
215
248
  if (parsed.continue) {
216
249
  return SessionManager.continueRecent(cwd, sessionDir);
217
250
  }
218
- return SessionManager.create(cwd, sessionDir);
251
+ if (parsed.sessionId) {
252
+ const existingSession = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
253
+ if (existingSession) {
254
+ return SessionManager.open(existingSession.path, sessionDir);
255
+ }
256
+ }
257
+ return SessionManager.create(cwd, sessionDir, { id: parsed.sessionId });
219
258
  }
220
259
  function buildSessionOptions(parsed, scopedModels, hasExistingSession, modelRegistry, settingsManager) {
221
260
  const options = {};
@@ -292,6 +331,9 @@ function buildSessionOptions(parsed, scopedModels, hasExistingSession, modelRegi
292
331
  if (parsed.tools) {
293
332
  options.tools = [...parsed.tools];
294
333
  }
334
+ if (parsed.excludeTools) {
335
+ options.excludeTools = [...parsed.excludeTools];
336
+ }
295
337
  return { options, cliThinkingFromModel, diagnostics };
296
338
  }
297
339
  function resolveCliPaths(cwd, paths) {
@@ -373,6 +415,7 @@ export async function main(args, options) {
373
415
  process.exit(1);
374
416
  }
375
417
  validateForkFlags(parsed);
418
+ validateSessionIdFlags(parsed);
376
419
  // Run migrations (pass cwd for project-local migrations)
377
420
  const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());
378
421
  time("runMigrations");
@@ -404,6 +447,14 @@ export async function main(args, options) {
404
447
  process.exit(1);
405
448
  }
406
449
  }
450
+ if (parsed.name !== undefined) {
451
+ const name = parsed.name.trim();
452
+ if (!name) {
453
+ console.error(chalk.red("Error: --name requires a non-empty value"));
454
+ process.exit(1);
455
+ }
456
+ sessionManager.appendSessionInfo(name);
457
+ }
407
458
  time("createSessionManager");
408
459
  const resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);
409
460
  const resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);
@@ -463,6 +514,7 @@ export async function main(args, options) {
463
514
  thinkingLevel: sessionOptions.thinkingLevel,
464
515
  scopedModels: sessionOptions.scopedModels,
465
516
  tools: sessionOptions.tools,
517
+ excludeTools: sessionOptions.excludeTools,
466
518
  noTools: sessionOptions.noTools,
467
519
  customTools: sessionOptions.customTools,
468
520
  });
@@ -482,6 +534,7 @@ export async function main(args, options) {
482
534
  agentDir,
483
535
  sessionManager,
484
536
  });
537
+ time("createAgentSessionRuntime");
485
538
  const { services, session, modelFallbackMessage } = runtime;
486
539
  const { settingsManager, modelRegistry, resourceLoader } = services;
487
540
  configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());
@@ -2,10 +2,12 @@
2
2
  * One-time migrations that run on startup.
3
3
  */
4
4
  import chalk from "chalk";
5
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
5
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
6
6
  import { dirname, join } from "path";
7
7
  import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.js";
8
8
  import { migrateKeybindingsConfig } from "./core/keybindings.js";
9
+ import { isLegacyEnvVarNameConfigValue } from "./core/resolve-config-value.js";
10
+ import { stripJsonComments } from "./utils/json.js";
9
11
  const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
10
12
  const EXTENSIONS_DOC_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/extensions.md";
11
13
  /**
@@ -63,6 +65,120 @@ export function migrateAuthToAuthJson() {
63
65
  }
64
66
  return providers;
65
67
  }
68
+ function migrateLegacyEnvVarString(value) {
69
+ return isLegacyEnvVarNameConfigValue(value) ? `$${value}` : undefined;
70
+ }
71
+ function migrateStringProperty(record, key, location, migrations) {
72
+ const value = record[key];
73
+ if (typeof value !== "string")
74
+ return false;
75
+ const migrated = migrateLegacyEnvVarString(value);
76
+ if (migrated === undefined)
77
+ return false;
78
+ record[key] = migrated;
79
+ migrations.push({ location, from: value, to: migrated });
80
+ return true;
81
+ }
82
+ function migrateHeadersConfig(headers, location, migrations) {
83
+ if (typeof headers !== "object" || headers === null || Array.isArray(headers))
84
+ return false;
85
+ const headerRecord = headers;
86
+ let migrated = false;
87
+ for (const [key, value] of Object.entries(headerRecord)) {
88
+ if (typeof value !== "string")
89
+ continue;
90
+ const migratedValue = migrateLegacyEnvVarString(value);
91
+ if (migratedValue === undefined)
92
+ continue;
93
+ headerRecord[key] = migratedValue;
94
+ migrations.push({ location: `${location}[${JSON.stringify(key)}]`, from: value, to: migratedValue });
95
+ migrated = true;
96
+ }
97
+ return migrated;
98
+ }
99
+ function migrateAuthJsonConfigValues(agentDir) {
100
+ const authPath = join(agentDir, "auth.json");
101
+ if (!existsSync(authPath))
102
+ return [];
103
+ try {
104
+ const parsed = JSON.parse(readFileSync(authPath, "utf-8"));
105
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
106
+ return [];
107
+ const authData = parsed;
108
+ const migrations = [];
109
+ for (const [provider, credential] of Object.entries(authData)) {
110
+ if (typeof credential !== "object" || credential === null || Array.isArray(credential))
111
+ continue;
112
+ const credentialRecord = credential;
113
+ if (credentialRecord.type !== "api_key")
114
+ continue;
115
+ migrateStringProperty(credentialRecord, "key", `auth.json[${JSON.stringify(provider)}].key`, migrations);
116
+ }
117
+ if (migrations.length === 0)
118
+ return [];
119
+ writeFileSync(authPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8");
120
+ chmodSync(authPath, 0o600);
121
+ return migrations;
122
+ }
123
+ catch {
124
+ return [];
125
+ }
126
+ }
127
+ function migrateModelsJsonConfigValues(agentDir) {
128
+ const modelsPath = join(agentDir, "models.json");
129
+ if (!existsSync(modelsPath))
130
+ return [];
131
+ const parsed = JSON.parse(stripJsonComments(readFileSync(modelsPath, "utf-8")));
132
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
133
+ return [];
134
+ const modelsData = parsed;
135
+ const providers = modelsData.providers;
136
+ if (typeof providers !== "object" || providers === null || Array.isArray(providers))
137
+ return [];
138
+ const migrations = [];
139
+ for (const [provider, providerConfig] of Object.entries(providers)) {
140
+ if (typeof providerConfig !== "object" || providerConfig === null || Array.isArray(providerConfig))
141
+ continue;
142
+ const providerRecord = providerConfig;
143
+ const providerLocation = `models.json.providers[${JSON.stringify(provider)}]`;
144
+ migrateStringProperty(providerRecord, "apiKey", `${providerLocation}.apiKey`, migrations);
145
+ migrateHeadersConfig(providerRecord.headers, `${providerLocation}.headers`, migrations);
146
+ if (Array.isArray(providerRecord.models)) {
147
+ for (let index = 0; index < providerRecord.models.length; index++) {
148
+ const modelConfig = providerRecord.models[index];
149
+ if (typeof modelConfig !== "object" || modelConfig === null || Array.isArray(modelConfig))
150
+ continue;
151
+ const modelRecord = modelConfig;
152
+ const modelKey = typeof modelRecord.id === "string" ? JSON.stringify(modelRecord.id) : String(index);
153
+ migrateHeadersConfig(modelRecord.headers, `${providerLocation}.models[${modelKey}].headers`, migrations);
154
+ }
155
+ }
156
+ const modelOverrides = providerRecord.modelOverrides;
157
+ if (typeof modelOverrides === "object" && modelOverrides !== null && !Array.isArray(modelOverrides)) {
158
+ for (const [modelId, modelOverride] of Object.entries(modelOverrides)) {
159
+ if (typeof modelOverride !== "object" || modelOverride === null || Array.isArray(modelOverride))
160
+ continue;
161
+ const modelOverrideRecord = modelOverride;
162
+ migrateHeadersConfig(modelOverrideRecord.headers, `${providerLocation}.modelOverrides[${JSON.stringify(modelId)}].headers`, migrations);
163
+ }
164
+ }
165
+ }
166
+ if (migrations.length === 0)
167
+ return [];
168
+ writeFileSync(modelsPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8");
169
+ return migrations;
170
+ }
171
+ function migrateExplicitEnvVarConfigValues() {
172
+ const agentDir = getAgentDir();
173
+ const migrations = [...migrateAuthJsonConfigValues(agentDir), ...migrateModelsJsonConfigValues(agentDir)];
174
+ if (migrations.length === 0)
175
+ return;
176
+ const details = migrations.map((migration) => ` - ${migration.location}: ${migration.from} -> ${migration.to}`);
177
+ console.log(chalk.yellow([
178
+ "Warning: Migrated API key/header environment references to explicit $ENV_VAR syntax. Plain strings will be treated as literals.",
179
+ ...details,
180
+ ].join("\n")));
181
+ }
66
182
  /**
67
183
  * Migrate sessions from ~/.pi/agent/*.jsonl to proper session directories.
68
184
  *
@@ -272,6 +388,7 @@ export async function showDeprecationWarnings(warnings) {
272
388
  */
273
389
  export function runMigrations(cwd) {
274
390
  const migratedAuthProviders = migrateAuthToAuthJson();
391
+ migrateExplicitEnvVarConfigValues();
275
392
  migrateSessionsFromAgentRoot();
276
393
  migrateToolsToBin();
277
394
  migrateKeybindingsConfigFile();
@@ -41,7 +41,7 @@ function getGroupLabel(metadata) {
41
41
  ? `User (${formatBaseDir(metadata.baseDir)})`
42
42
  : `Project (${formatBaseDir(metadata.baseDir)})`;
43
43
  }
44
- return metadata.scope === "user" ? "User (~/.pi/agent/)" : "Project (.pi/)";
44
+ return metadata.scope === "user" ? "User (~/.sling/agent/)" : "Project (.sling/)";
45
45
  }
46
46
  return metadata.scope === "user" ? "User settings" : "Project settings";
47
47
  }
@@ -68,7 +68,7 @@ export class LoginDialogComponent extends Container {
68
68
  /**
69
69
  * Called by onAuth callback - show URL and optional instructions
70
70
  */
71
- showAuth(url, instructions, options = {}) {
71
+ showAuth(url, instructions) {
72
72
  this.contentContainer.clear();
73
73
  this.contentContainer.addChild(new Spacer(1));
74
74
  const linkedUrl = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`;
@@ -80,9 +80,7 @@ export class LoginDialogComponent extends Container {
80
80
  this.contentContainer.addChild(new Spacer(1));
81
81
  this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
82
82
  }
83
- if (options.autoOpenBrowser ?? true) {
84
- this.openUrl(url);
85
- }
83
+ this.openUrl(url);
86
84
  this.tui.requestRender();
87
85
  }
88
86
  /**
@@ -8,6 +8,7 @@ import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { getProviders, } from "@earendil-works/pi-ai";
10
10
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
11
+ import chalk from "chalk";
11
12
  import { spawn, spawnSync } from "child_process";
12
13
  import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
13
14
  import { parseSkillBlock } from "../../core/agent-session.js";
@@ -91,6 +92,27 @@ function isAnthropicSubscriptionAuthKey(apiKey) {
91
92
  function isUnknownModel(model) {
92
93
  return !!model && model.provider === "unknown" && model.id === "unknown" && model.api === "unknown";
93
94
  }
95
+ function quoteIfNeeded(value) {
96
+ if (value.length > 0 && !/[^a-zA-Z0-9_\-./~:@]/.test(value)) {
97
+ return value;
98
+ }
99
+ return `'${value.replace(/'/g, `'\\''`)}'`;
100
+ }
101
+ export function formatResumeCommand(sessionManager) {
102
+ if (!process.stdout.isTTY)
103
+ return undefined;
104
+ if (!sessionManager.isPersisted())
105
+ return undefined;
106
+ const sessionFile = sessionManager.getSessionFile();
107
+ if (!sessionFile || !fs.existsSync(sessionFile))
108
+ return undefined;
109
+ const args = [APP_NAME];
110
+ if (!sessionManager.usesDefaultSessionDir()) {
111
+ args.push("--session-dir", quoteIfNeeded(sessionManager.getSessionDir()));
112
+ }
113
+ args.push("--session", sessionManager.getSessionId());
114
+ return args.join(" ");
115
+ }
94
116
  function hasDefaultModelProvider(providerId) {
95
117
  return providerId in defaultModelPerProvider;
96
118
  }
@@ -125,6 +147,7 @@ export class InteractiveMode {
125
147
  version;
126
148
  isInitialized = false;
127
149
  onInputCallback;
150
+ pendingUserInputs = [];
128
151
  loadingAnimation = undefined;
129
152
  workingMessage = undefined;
130
153
  workingVisible = true;
@@ -1106,6 +1129,7 @@ export class InteractiveMode {
1106
1129
  const uiContext = this.createExtensionUIContext();
1107
1130
  await this.session.bindExtensions({
1108
1131
  uiContext,
1132
+ mode: "tui",
1109
1133
  abortHandler: () => {
1110
1134
  this.restoreQueuedMessagesToEditor({ abort: true });
1111
1135
  },
@@ -1245,6 +1269,7 @@ export class InteractiveMode {
1245
1269
  // Create a context for shortcut handlers
1246
1270
  const createContext = () => ({
1247
1271
  ui: this.createExtensionUIContext(),
1272
+ mode: "tui",
1248
1273
  hasUI: true,
1249
1274
  cwd: this.sessionManager.getCwd(),
1250
1275
  sessionManager: this.sessionManager,
@@ -2130,6 +2155,9 @@ export class InteractiveMode {
2130
2155
  if (this.onInputCallback) {
2131
2156
  this.onInputCallback(text);
2132
2157
  }
2158
+ else {
2159
+ this.pendingUserInputs.push(text);
2160
+ }
2133
2161
  this.editor.addToHistory?.(text);
2134
2162
  };
2135
2163
  }
@@ -2608,6 +2636,10 @@ export class InteractiveMode {
2608
2636
  }
2609
2637
  }
2610
2638
  async getUserInput() {
2639
+ const queuedInput = this.pendingUserInputs.shift();
2640
+ if (queuedInput !== undefined) {
2641
+ return queuedInput;
2642
+ }
2611
2643
  return new Promise((resolve) => {
2612
2644
  this.onInputCallback = (text) => {
2613
2645
  this.onInputCallback = undefined;
@@ -2643,16 +2675,36 @@ export class InteractiveMode {
2643
2675
  * repaint the final frame while the process is exiting.
2644
2676
  */
2645
2677
  isShuttingDown = false;
2646
- async shutdown() {
2678
+ async shutdown(options) {
2647
2679
  if (this.isShuttingDown)
2648
2680
  return;
2649
2681
  this.isShuttingDown = true;
2650
2682
  this.unregisterSignalHandlers();
2683
+ if (options?.fromSignal) {
2684
+ // Signal-triggered shutdown (SIGTERM/SIGHUP). Emit extension cleanup
2685
+ // (session_shutdown) BEFORE touching the terminal. Extension teardown
2686
+ // such as removing sockets does not write to the tty, so it must not be
2687
+ // skipped if a later terminal-restore write fails on a dead or stalled
2688
+ // terminal. If the terminal is gone, the restore writes below emit EIO,
2689
+ // which the stdout/stderr error handler turns into emergencyTerminalExit;
2690
+ // the render loop is already idle, so this cannot hot-spin (see #4144).
2691
+ await this.runtimeHost.dispose();
2692
+ await this.ui.terminal.drainInput(1000);
2693
+ this.stop();
2694
+ process.exit(0);
2695
+ }
2696
+ // Interactive quit (Ctrl+D, Ctrl+C, /quit, extension shutdown()). Stop the
2697
+ // TUI before emitting shutdown events so extension UI cleanup cannot repaint
2698
+ // the final frame while the process is exiting.
2651
2699
  // Drain any in-flight Kitty key release events before stopping.
2652
2700
  // This prevents escape sequences from leaking to the parent shell over slow SSH.
2653
2701
  await this.ui.terminal.drainInput(1000);
2654
2702
  this.stop();
2655
2703
  await this.runtimeHost.dispose();
2704
+ const resumeCommand = formatResumeCommand(this.sessionManager);
2705
+ if (resumeCommand) {
2706
+ process.stdout.write(`${chalk.dim("To resume this session:")} ${resumeCommand}\n`);
2707
+ }
2656
2708
  process.exit(0);
2657
2709
  }
2658
2710
  emergencyTerminalExit() {
@@ -2711,11 +2763,12 @@ export class InteractiveMode {
2711
2763
  }
2712
2764
  for (const signal of signals) {
2713
2765
  const handler = () => {
2714
- if (signal === "SIGHUP") {
2715
- this.emergencyTerminalExit();
2716
- }
2766
+ // SIGHUP no longer hard-exits: graceful shutdown emits session_shutdown
2767
+ // first, then attempts terminal restore. A genuinely dead terminal
2768
+ // surfaces as an EIO on the restore writes, which the stdout/stderr
2769
+ // error handler converts into emergencyTerminalExit (see #4144, #5080).
2717
2770
  killTrackedDetachedChildren();
2718
- void this.shutdown();
2771
+ void this.shutdown({ fromSignal: true });
2719
2772
  };
2720
2773
  process.prependListener(signal, handler);
2721
2774
  this.signalCleanupHandlers.push(() => process.off(signal, handler));
@@ -3642,7 +3695,9 @@ export class InteractiveMode {
3642
3695
  }
3643
3696
  showSessionSelector() {
3644
3697
  this.showSelector((done) => {
3645
- const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), SessionManager.listAll, async (sessionPath) => {
3698
+ const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), (onProgress) => this.sessionManager.usesDefaultSessionDir()
3699
+ ? SessionManager.listAll(onProgress)
3700
+ : SessionManager.listAll(this.sessionManager.getSessionDir(), onProgress), async (sessionPath) => {
3646
3701
  done();
3647
3702
  await this.handleResumeSession(sessionPath);
3648
3703
  }, () => {
@@ -844,17 +844,27 @@ function buildCliHighlightTheme(t) {
844
844
  built_in: (s) => t.fg("syntaxType", s),
845
845
  literal: (s) => t.fg("syntaxNumber", s),
846
846
  number: (s) => t.fg("syntaxNumber", s),
847
+ regexp: (s) => t.fg("syntaxString", s),
847
848
  string: (s) => t.fg("syntaxString", s),
848
849
  comment: (s) => t.fg("syntaxComment", s),
850
+ doctag: (s) => t.fg("syntaxComment", s),
851
+ meta: (s) => t.fg("muted", s),
849
852
  function: (s) => t.fg("syntaxFunction", s),
850
853
  title: (s) => t.fg("syntaxFunction", s),
851
854
  class: (s) => t.fg("syntaxType", s),
852
855
  type: (s) => t.fg("syntaxType", s),
856
+ tag: (s) => t.fg("syntaxPunctuation", s),
857
+ name: (s) => t.fg("syntaxKeyword", s),
853
858
  attr: (s) => t.fg("syntaxVariable", s),
854
859
  variable: (s) => t.fg("syntaxVariable", s),
855
860
  params: (s) => t.fg("syntaxVariable", s),
856
861
  operator: (s) => t.fg("syntaxOperator", s),
857
862
  punctuation: (s) => t.fg("syntaxPunctuation", s),
863
+ emphasis: (s) => t.italic(s),
864
+ strong: (s) => t.bold(s),
865
+ link: (s) => t.underline(s),
866
+ addition: (s) => t.fg("toolDiffAdded", s),
867
+ deletion: (s) => t.fg("toolDiffRemoved", s),
858
868
  };
859
869
  }
860
870
  function getCliHighlightTheme(t) {
@@ -48,6 +48,7 @@ export async function runPrintMode(runtimeHost, options) {
48
48
  const rebindSession = async () => {
49
49
  session = runtimeHost.session;
50
50
  await session.bindExtensions({
51
+ mode: mode === "json" ? "json" : "print",
51
52
  commandContextActions: {
52
53
  waitForIdle: () => session.agent.waitForIdle(),
53
54
  newSession: async (newSessionOptions) => runtimeHost.newSession(newSessionOptions),
@@ -228,6 +228,7 @@ export async function runRpcMode(runtimeHost) {
228
228
  session = runtimeHost.session;
229
229
  await session.bindExtensions({
230
230
  uiContext: createExtensionUIContext(),
231
+ mode: "rpc",
231
232
  commandContextActions: {
232
233
  waitForIdle: () => session.agent.waitForIdle(),
233
234
  newSession: async (options) => runtimeHost.newSession(options),
@@ -427,7 +428,9 @@ export async function runRpcMode(runtimeHost) {
427
428
  // Bash
428
429
  // =================================================================
429
430
  case "bash": {
430
- const result = await session.executeBash(command.command);
431
+ const result = await session.executeBash(command.command, undefined, {
432
+ excludeFromContext: command.excludeFromContext,
433
+ });
431
434
  return success(id, "bash", result);
432
435
  }
433
436
  case "abort_bash": {
@@ -0,0 +1,13 @@
1
+ import chalk from "chalk";
2
+ const emittedDeprecationWarnings = new Set();
3
+ export function warnDeprecation(message) {
4
+ if (emittedDeprecationWarnings.has(message))
5
+ return;
6
+ emittedDeprecationWarnings.add(message);
7
+ console.warn(chalk.yellow(`Deprecation warning: ${message}`));
8
+ }
9
+ /** Clear deprecation warning state. Exported for tests. */
10
+ export function clearDeprecationWarningsForTests() {
11
+ emittedDeprecationWarnings.clear();
12
+ }
13
+ //# sourceMappingURL=deprecation.js.map
@@ -0,0 +1,7 @@
1
+ /** Strip `//` line comments and trailing commas from JSON, leaving string literals untouched. */
2
+ export function stripJsonComments(input) {
3
+ return input
4
+ .replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : ""))
5
+ .replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? (m[0] === '"' ? m : ""));
6
+ }
7
+ //# sourceMappingURL=json.js.map