@aexol/spectral 0.7.1 → 0.7.5

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 (219) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/agent/agents.js +1 -1
  3. package/dist/agent/index.js +199 -184
  4. package/dist/commands/serve.js +0 -3
  5. package/dist/designer/data/systems/renault/DESIGN.md +1 -1
  6. package/dist/designer/philosophies.js +668 -0
  7. package/dist/mcp/sampling-handler.js +1 -1
  8. package/dist/memory/commands/status.js +1 -1
  9. package/dist/memory/compaction.js +2 -2
  10. package/dist/memory/config.js +1 -1
  11. package/dist/memory/debug-log.js +1 -1
  12. package/dist/memory/hooks/compaction-hook.js +29 -0
  13. package/dist/memory/index.js +2 -0
  14. package/dist/memory/observer.js +2 -2
  15. package/dist/memory/project-observations-store.js +14 -0
  16. package/dist/memory/tokens.js +1 -1
  17. package/dist/memory/tools/read-project-observations.js +82 -0
  18. package/dist/memory/tools/recall-observation.js +2 -2
  19. package/dist/pi/agent-core/agent-loop.js +501 -0
  20. package/dist/pi/agent-core/agent.js +401 -0
  21. package/dist/pi/agent-core/harness/agent-harness.js +899 -0
  22. package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
  23. package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
  24. package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
  25. package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
  26. package/dist/pi/agent-core/harness/messages.js +101 -0
  27. package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
  28. package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
  29. package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
  30. package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
  31. package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
  32. package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
  33. package/dist/pi/agent-core/harness/session/session.js +196 -0
  34. package/dist/pi/agent-core/harness/session/uuid.js +49 -0
  35. package/dist/pi/agent-core/harness/skills.js +310 -0
  36. package/dist/pi/agent-core/harness/system-prompt.js +29 -0
  37. package/dist/pi/agent-core/harness/types.js +93 -0
  38. package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
  39. package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
  40. package/dist/pi/agent-core/index.js +24 -0
  41. package/dist/pi/agent-core/node.js +2 -0
  42. package/dist/pi/agent-core/proxy.js +277 -0
  43. package/dist/pi/agent-core/types.js +1 -0
  44. package/dist/pi/ai/api-registry.js +43 -0
  45. package/dist/pi/ai/cli.js +120 -0
  46. package/dist/pi/ai/env-api-keys.js +169 -0
  47. package/dist/pi/ai/image-models.generated.js +441 -0
  48. package/dist/pi/ai/image-models.js +22 -0
  49. package/dist/pi/ai/images-api-registry.js +21 -0
  50. package/dist/pi/ai/images.js +13 -0
  51. package/dist/pi/ai/index.js +18 -0
  52. package/dist/pi/ai/models.generated.js +16220 -0
  53. package/dist/pi/ai/models.js +70 -0
  54. package/dist/pi/ai/oauth.js +1 -0
  55. package/dist/pi/ai/providers/anthropic.js +945 -0
  56. package/dist/pi/ai/providers/faux.js +367 -0
  57. package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
  58. package/dist/pi/ai/providers/openai-completions.js +945 -0
  59. package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
  60. package/dist/pi/ai/providers/register-builtins.js +97 -0
  61. package/dist/pi/ai/providers/simple-options.js +40 -0
  62. package/dist/pi/ai/providers/transform-messages.js +183 -0
  63. package/dist/pi/ai/session-resources.js +21 -0
  64. package/dist/pi/ai/stream.js +26 -0
  65. package/dist/pi/ai/types.js +1 -0
  66. package/dist/pi/ai/utils/diagnostics.js +24 -0
  67. package/dist/pi/ai/utils/event-stream.js +80 -0
  68. package/dist/pi/ai/utils/hash.js +13 -0
  69. package/dist/pi/ai/utils/headers.js +7 -0
  70. package/dist/pi/ai/utils/json-parse.js +112 -0
  71. package/dist/pi/ai/utils/node-http-proxy.js +96 -0
  72. package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
  73. package/dist/pi/ai/utils/oauth/device-code.js +54 -0
  74. package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
  75. package/dist/pi/ai/utils/oauth/index.js +121 -0
  76. package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
  77. package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
  78. package/dist/pi/ai/utils/oauth/pkce.js +30 -0
  79. package/dist/pi/ai/utils/oauth/types.js +1 -0
  80. package/dist/pi/ai/utils/overflow.js +150 -0
  81. package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
  82. package/dist/pi/ai/utils/typebox-helpers.js +20 -0
  83. package/dist/pi/ai/utils/validation.js +280 -0
  84. package/dist/pi/coding-agent/bun/cli.js +7 -0
  85. package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
  86. package/dist/pi/coding-agent/cli/args.js +340 -0
  87. package/dist/pi/coding-agent/cli/file-processor.js +82 -0
  88. package/dist/pi/coding-agent/cli/initial-message.js +21 -0
  89. package/dist/pi/coding-agent/cli.js +17 -0
  90. package/dist/pi/coding-agent/config.js +414 -0
  91. package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
  92. package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
  93. package/dist/pi/coding-agent/core/agent-session.js +2498 -0
  94. package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
  95. package/dist/pi/coding-agent/core/auth-storage.js +441 -0
  96. package/dist/pi/coding-agent/core/bash-executor.js +110 -0
  97. package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
  98. package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
  99. package/dist/pi/coding-agent/core/compaction/index.js +6 -0
  100. package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
  101. package/dist/pi/coding-agent/core/defaults.js +1 -0
  102. package/dist/pi/coding-agent/core/diagnostics.js +1 -0
  103. package/dist/pi/coding-agent/core/event-bus.js +24 -0
  104. package/dist/pi/coding-agent/core/exec.js +74 -0
  105. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
  106. package/dist/pi/coding-agent/core/export-html/index.js +225 -0
  107. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
  108. package/dist/pi/coding-agent/core/extensions/index.js +8 -0
  109. package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
  110. package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
  111. package/dist/pi/coding-agent/core/extensions/types.js +44 -0
  112. package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
  113. package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
  114. package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
  115. package/dist/pi/coding-agent/core/index.js +11 -0
  116. package/dist/pi/coding-agent/core/keybindings.js +294 -0
  117. package/dist/pi/coding-agent/core/messages.js +122 -0
  118. package/dist/pi/coding-agent/core/model-registry.js +728 -0
  119. package/dist/pi/coding-agent/core/model-resolver.js +494 -0
  120. package/dist/pi/coding-agent/core/output-guard.js +58 -0
  121. package/dist/pi/coding-agent/core/package-manager.js +2020 -0
  122. package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
  123. package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
  124. package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
  125. package/dist/pi/coding-agent/core/resource-loader.js +733 -0
  126. package/dist/pi/coding-agent/core/sdk.js +282 -0
  127. package/dist/pi/coding-agent/core/session-cwd.js +37 -0
  128. package/dist/pi/coding-agent/core/session-manager.js +1146 -0
  129. package/dist/pi/coding-agent/core/settings-manager.js +794 -0
  130. package/dist/pi/coding-agent/core/skills.js +386 -0
  131. package/dist/pi/coding-agent/core/slash-commands.js +24 -0
  132. package/dist/pi/coding-agent/core/source-info.js +18 -0
  133. package/dist/pi/coding-agent/core/system-prompt.js +122 -0
  134. package/dist/pi/coding-agent/core/telemetry.js +8 -0
  135. package/dist/pi/coding-agent/core/timings.js +30 -0
  136. package/dist/pi/coding-agent/core/tools/bash.js +341 -0
  137. package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
  138. package/dist/pi/coding-agent/core/tools/edit.js +324 -0
  139. package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
  140. package/dist/pi/coding-agent/core/tools/find.js +297 -0
  141. package/dist/pi/coding-agent/core/tools/grep.js +303 -0
  142. package/dist/pi/coding-agent/core/tools/index.js +111 -0
  143. package/dist/pi/coding-agent/core/tools/ls.js +168 -0
  144. package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
  145. package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
  146. package/dist/pi/coding-agent/core/tools/read.js +288 -0
  147. package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
  148. package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
  149. package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
  150. package/dist/pi/coding-agent/core/tools/write.js +212 -0
  151. package/dist/pi/coding-agent/index.js +41 -0
  152. package/dist/pi/coding-agent/main.js +5 -0
  153. package/dist/pi/coding-agent/migrations.js +280 -0
  154. package/dist/pi/coding-agent/modes/index.js +7 -0
  155. package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
  156. package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
  157. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
  158. package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
  159. package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
  160. package/dist/pi/coding-agent/modes/print-mode.js +130 -0
  161. package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
  162. package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
  163. package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
  164. package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
  165. package/dist/pi/coding-agent/utils/ansi.js +51 -0
  166. package/dist/pi/coding-agent/utils/changelog.js +86 -0
  167. package/dist/pi/coding-agent/utils/child-process.js +87 -0
  168. package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
  169. package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
  170. package/dist/pi/coding-agent/utils/clipboard.js +116 -0
  171. package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
  172. package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
  173. package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
  174. package/dist/pi/coding-agent/utils/git.js +162 -0
  175. package/dist/pi/coding-agent/utils/html.js +39 -0
  176. package/dist/pi/coding-agent/utils/image-convert.js +38 -0
  177. package/dist/pi/coding-agent/utils/image-resize.js +136 -0
  178. package/dist/pi/coding-agent/utils/mime.js +68 -0
  179. package/dist/pi/coding-agent/utils/paths.js +91 -0
  180. package/dist/pi/coding-agent/utils/photon.js +120 -0
  181. package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
  182. package/dist/pi/coding-agent/utils/shell.js +194 -0
  183. package/dist/pi/coding-agent/utils/sleep.js +16 -0
  184. package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
  185. package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
  186. package/dist/pi/coding-agent/utils/version-check.js +81 -0
  187. package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
  188. package/dist/pi/tui/autocomplete.js +631 -0
  189. package/dist/pi/tui/components/box.js +103 -0
  190. package/dist/pi/tui/components/cancellable-loader.js +34 -0
  191. package/dist/pi/tui/components/editor.js +1915 -0
  192. package/dist/pi/tui/components/image.js +88 -0
  193. package/dist/pi/tui/components/input.js +425 -0
  194. package/dist/pi/tui/components/loader.js +68 -0
  195. package/dist/pi/tui/components/markdown.js +633 -0
  196. package/dist/pi/tui/components/select-list.js +158 -0
  197. package/dist/pi/tui/components/settings-list.js +184 -0
  198. package/dist/pi/tui/components/spacer.js +22 -0
  199. package/dist/pi/tui/components/text.js +88 -0
  200. package/dist/pi/tui/components/truncated-text.js +50 -0
  201. package/dist/pi/tui/editor-component.js +1 -0
  202. package/dist/pi/tui/fuzzy.js +109 -0
  203. package/dist/pi/tui/index.js +31 -0
  204. package/dist/pi/tui/keybindings.js +173 -0
  205. package/dist/pi/tui/keys.js +1172 -0
  206. package/dist/pi/tui/kill-ring.js +43 -0
  207. package/dist/pi/tui/stdin-buffer.js +360 -0
  208. package/dist/pi/tui/terminal-image.js +335 -0
  209. package/dist/pi/tui/terminal.js +324 -0
  210. package/dist/pi/tui/tui.js +1076 -0
  211. package/dist/pi/tui/undo-stack.js +24 -0
  212. package/dist/pi/tui/utils.js +1016 -0
  213. package/dist/relay/dispatcher.js +30 -0
  214. package/dist/server/handlers/queue.js +52 -0
  215. package/dist/server/pi-bridge.js +9 -1
  216. package/dist/server/session-stream.js +76 -111
  217. package/dist/server/storage.js +154 -2
  218. package/dist/server/title-generator.js +14 -153
  219. package/package.json +24 -6
@@ -0,0 +1,20 @@
1
+ import { join } from "node:path";
2
+ import { getDocsPath } from "../config.js";
3
+ const UNKNOWN_PROVIDER = "unknown";
4
+ export function getProviderLoginHelp() {
5
+ return [
6
+ "Use /login to log into a provider via OAuth or API key. See:",
7
+ ` ${join(getDocsPath(), "providers.md")}`,
8
+ ` ${join(getDocsPath(), "models.md")}`,
9
+ ].join("\n");
10
+ }
11
+ export function formatNoModelsAvailableMessage() {
12
+ return `No models available. ${getProviderLoginHelp()}`;
13
+ }
14
+ export function formatNoModelSelectedMessage() {
15
+ return `No model selected.\n\n${getProviderLoginHelp()}\n\nThen use /model to select a model.`;
16
+ }
17
+ export function formatNoApiKeyFoundMessage(provider) {
18
+ const providerDisplay = provider === UNKNOWN_PROVIDER ? "the selected model" : provider;
19
+ return `No API key found for ${providerDisplay}.\n\n${getProviderLoginHelp()}`;
20
+ }
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Credential storage for API keys and OAuth tokens.
3
+ * Handles loading, saving, and refreshing credentials from auth.json.
4
+ *
5
+ * Uses file locking to prevent race conditions when multiple pi instances
6
+ * try to refresh tokens simultaneously.
7
+ */
8
+ import { findEnvKeys, getEnvApiKey, } from "../../ai/index.js";
9
+ import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "../../ai/oauth.js";
10
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
11
+ import { dirname, join } from "path";
12
+ import lockfile from "proper-lockfile";
13
+ import { getAgentDir } from "../config.js";
14
+ import { normalizePath } from "../utils/paths.js";
15
+ import { resolveConfigValue } from "./resolve-config-value.js";
16
+ export class FileAuthStorageBackend {
17
+ authPath;
18
+ constructor(authPath = join(getAgentDir(), "auth.json")) {
19
+ this.authPath = normalizePath(authPath);
20
+ }
21
+ ensureParentDir() {
22
+ const dir = dirname(this.authPath);
23
+ if (!existsSync(dir)) {
24
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
25
+ }
26
+ }
27
+ ensureFileExists() {
28
+ if (!existsSync(this.authPath)) {
29
+ writeFileSync(this.authPath, "{}", "utf-8");
30
+ chmodSync(this.authPath, 0o600);
31
+ }
32
+ }
33
+ acquireLockSyncWithRetry(path) {
34
+ const maxAttempts = 10;
35
+ const delayMs = 20;
36
+ let lastError;
37
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
38
+ try {
39
+ return lockfile.lockSync(path, { realpath: false });
40
+ }
41
+ catch (error) {
42
+ const code = typeof error === "object" && error !== null && "code" in error
43
+ ? String(error.code)
44
+ : undefined;
45
+ if (code !== "ELOCKED" || attempt === maxAttempts) {
46
+ throw error;
47
+ }
48
+ lastError = error;
49
+ const start = Date.now();
50
+ while (Date.now() - start < delayMs) {
51
+ // Sleep synchronously to avoid changing callers to async.
52
+ }
53
+ }
54
+ }
55
+ throw lastError ?? new Error("Failed to acquire auth storage lock");
56
+ }
57
+ withLock(fn) {
58
+ this.ensureParentDir();
59
+ this.ensureFileExists();
60
+ let release;
61
+ try {
62
+ release = this.acquireLockSyncWithRetry(this.authPath);
63
+ const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined;
64
+ const { result, next } = fn(current);
65
+ if (next !== undefined) {
66
+ writeFileSync(this.authPath, next, "utf-8");
67
+ chmodSync(this.authPath, 0o600);
68
+ }
69
+ return result;
70
+ }
71
+ finally {
72
+ if (release) {
73
+ release();
74
+ }
75
+ }
76
+ }
77
+ async withLockAsync(fn) {
78
+ this.ensureParentDir();
79
+ this.ensureFileExists();
80
+ let release;
81
+ let lockCompromised = false;
82
+ let lockCompromisedError;
83
+ const throwIfCompromised = () => {
84
+ if (lockCompromised) {
85
+ throw lockCompromisedError ?? new Error("Auth storage lock was compromised");
86
+ }
87
+ };
88
+ try {
89
+ release = await lockfile.lock(this.authPath, {
90
+ retries: {
91
+ retries: 10,
92
+ factor: 2,
93
+ minTimeout: 100,
94
+ maxTimeout: 10000,
95
+ randomize: true,
96
+ },
97
+ stale: 30000,
98
+ onCompromised: (err) => {
99
+ lockCompromised = true;
100
+ lockCompromisedError = err;
101
+ },
102
+ });
103
+ throwIfCompromised();
104
+ const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined;
105
+ const { result, next } = await fn(current);
106
+ throwIfCompromised();
107
+ if (next !== undefined) {
108
+ writeFileSync(this.authPath, next, "utf-8");
109
+ chmodSync(this.authPath, 0o600);
110
+ }
111
+ throwIfCompromised();
112
+ return result;
113
+ }
114
+ finally {
115
+ if (release) {
116
+ try {
117
+ await release();
118
+ }
119
+ catch {
120
+ // Ignore unlock errors when lock is compromised.
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ export class InMemoryAuthStorageBackend {
127
+ value;
128
+ withLock(fn) {
129
+ const { result, next } = fn(this.value);
130
+ if (next !== undefined) {
131
+ this.value = next;
132
+ }
133
+ return result;
134
+ }
135
+ async withLockAsync(fn) {
136
+ const { result, next } = await fn(this.value);
137
+ if (next !== undefined) {
138
+ this.value = next;
139
+ }
140
+ return result;
141
+ }
142
+ }
143
+ /**
144
+ * Credential storage backed by a JSON file.
145
+ */
146
+ export class AuthStorage {
147
+ data = {};
148
+ runtimeOverrides = new Map();
149
+ fallbackResolver;
150
+ loadError = null;
151
+ errors = [];
152
+ storage;
153
+ constructor(storage) {
154
+ this.storage = storage;
155
+ this.reload();
156
+ }
157
+ static create(authPath) {
158
+ return new AuthStorage(new FileAuthStorageBackend(authPath ?? join(getAgentDir(), "auth.json")));
159
+ }
160
+ static fromStorage(storage) {
161
+ return new AuthStorage(storage);
162
+ }
163
+ static inMemory(data = {}) {
164
+ const storage = new InMemoryAuthStorageBackend();
165
+ storage.withLock(() => ({ result: undefined, next: JSON.stringify(data, null, 2) }));
166
+ return AuthStorage.fromStorage(storage);
167
+ }
168
+ /**
169
+ * Set a runtime API key override (not persisted to disk).
170
+ * Used for CLI --api-key flag.
171
+ */
172
+ setRuntimeApiKey(provider, apiKey) {
173
+ this.runtimeOverrides.set(provider, apiKey);
174
+ }
175
+ /**
176
+ * Remove a runtime API key override.
177
+ */
178
+ removeRuntimeApiKey(provider) {
179
+ this.runtimeOverrides.delete(provider);
180
+ }
181
+ /**
182
+ * Set a fallback resolver for API keys not found in auth.json or env vars.
183
+ * Used for custom provider keys from models.json.
184
+ */
185
+ setFallbackResolver(resolver) {
186
+ this.fallbackResolver = resolver;
187
+ }
188
+ recordError(error) {
189
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
190
+ this.errors.push(normalizedError);
191
+ }
192
+ parseStorageData(content) {
193
+ if (!content) {
194
+ return {};
195
+ }
196
+ return JSON.parse(content);
197
+ }
198
+ /**
199
+ * Reload credentials from storage.
200
+ */
201
+ reload() {
202
+ let content;
203
+ try {
204
+ this.storage.withLock((current) => {
205
+ content = current;
206
+ return { result: undefined };
207
+ });
208
+ this.data = this.parseStorageData(content);
209
+ this.loadError = null;
210
+ }
211
+ catch (error) {
212
+ this.loadError = error;
213
+ this.recordError(error);
214
+ }
215
+ }
216
+ persistProviderChange(provider, credential) {
217
+ if (this.loadError) {
218
+ return;
219
+ }
220
+ try {
221
+ this.storage.withLock((current) => {
222
+ const currentData = this.parseStorageData(current);
223
+ const merged = { ...currentData };
224
+ if (credential) {
225
+ merged[provider] = credential;
226
+ }
227
+ else {
228
+ delete merged[provider];
229
+ }
230
+ return { result: undefined, next: JSON.stringify(merged, null, 2) };
231
+ });
232
+ }
233
+ catch (error) {
234
+ this.recordError(error);
235
+ }
236
+ }
237
+ /**
238
+ * Get credential for a provider.
239
+ */
240
+ get(provider) {
241
+ return this.data[provider] ?? undefined;
242
+ }
243
+ /**
244
+ * Set credential for a provider.
245
+ */
246
+ set(provider, credential) {
247
+ this.data[provider] = credential;
248
+ this.persistProviderChange(provider, credential);
249
+ }
250
+ /**
251
+ * Remove credential for a provider.
252
+ */
253
+ remove(provider) {
254
+ delete this.data[provider];
255
+ this.persistProviderChange(provider, undefined);
256
+ }
257
+ /**
258
+ * List all providers with credentials.
259
+ */
260
+ list() {
261
+ return Object.keys(this.data);
262
+ }
263
+ /**
264
+ * Check if credentials exist for a provider in auth.json.
265
+ */
266
+ has(provider) {
267
+ return provider in this.data;
268
+ }
269
+ /**
270
+ * Check if any form of auth is configured for a provider.
271
+ * Unlike getApiKey(), this doesn't refresh OAuth tokens.
272
+ */
273
+ hasAuth(provider) {
274
+ if (this.runtimeOverrides.has(provider))
275
+ return true;
276
+ if (this.data[provider])
277
+ return true;
278
+ if (getEnvApiKey(provider))
279
+ return true;
280
+ if (this.fallbackResolver?.(provider))
281
+ return true;
282
+ return false;
283
+ }
284
+ /**
285
+ * Return auth status without exposing credential values or refreshing tokens.
286
+ */
287
+ getAuthStatus(provider) {
288
+ if (this.data[provider]) {
289
+ return { configured: true, source: "stored" };
290
+ }
291
+ if (this.runtimeOverrides.has(provider)) {
292
+ return { configured: false, source: "runtime", label: "--api-key" };
293
+ }
294
+ const envKeys = findEnvKeys(provider);
295
+ if (envKeys?.[0]) {
296
+ return { configured: false, source: "environment", label: envKeys[0] };
297
+ }
298
+ if (this.fallbackResolver?.(provider)) {
299
+ return { configured: false, source: "fallback", label: "custom provider config" };
300
+ }
301
+ return { configured: false };
302
+ }
303
+ /**
304
+ * Get all credentials (for passing to getOAuthApiKey).
305
+ */
306
+ getAll() {
307
+ return { ...this.data };
308
+ }
309
+ drainErrors() {
310
+ const drained = [...this.errors];
311
+ this.errors = [];
312
+ return drained;
313
+ }
314
+ /**
315
+ * Login to an OAuth provider.
316
+ */
317
+ async login(providerId, callbacks) {
318
+ const provider = getOAuthProvider(providerId);
319
+ if (!provider) {
320
+ throw new Error(`Unknown OAuth provider: ${providerId}`);
321
+ }
322
+ const credentials = await provider.login(callbacks);
323
+ this.set(providerId, { type: "oauth", ...credentials });
324
+ }
325
+ /**
326
+ * Logout from a provider.
327
+ */
328
+ logout(provider) {
329
+ this.remove(provider);
330
+ }
331
+ /**
332
+ * Refresh OAuth token with backend locking to prevent race conditions.
333
+ * Multiple pi instances may try to refresh simultaneously when tokens expire.
334
+ */
335
+ async refreshOAuthTokenWithLock(providerId) {
336
+ const provider = getOAuthProvider(providerId);
337
+ if (!provider) {
338
+ return null;
339
+ }
340
+ const result = await this.storage.withLockAsync(async (current) => {
341
+ const currentData = this.parseStorageData(current);
342
+ this.data = currentData;
343
+ this.loadError = null;
344
+ const cred = currentData[providerId];
345
+ if (cred?.type !== "oauth") {
346
+ return { result: null };
347
+ }
348
+ if (Date.now() < cred.expires) {
349
+ return { result: { apiKey: provider.getApiKey(cred), newCredentials: cred } };
350
+ }
351
+ const oauthCreds = {};
352
+ for (const [key, value] of Object.entries(currentData)) {
353
+ if (value.type === "oauth") {
354
+ oauthCreds[key] = value;
355
+ }
356
+ }
357
+ const refreshed = await getOAuthApiKey(providerId, oauthCreds);
358
+ if (!refreshed) {
359
+ return { result: null };
360
+ }
361
+ const merged = {
362
+ ...currentData,
363
+ [providerId]: { type: "oauth", ...refreshed.newCredentials },
364
+ };
365
+ this.data = merged;
366
+ this.loadError = null;
367
+ return { result: refreshed, next: JSON.stringify(merged, null, 2) };
368
+ });
369
+ return result;
370
+ }
371
+ /**
372
+ * Get API key for a provider.
373
+ * Priority:
374
+ * 1. Runtime override (CLI --api-key)
375
+ * 2. API key from auth.json
376
+ * 3. OAuth token from auth.json (auto-refreshed with locking)
377
+ * 4. Environment variable
378
+ * 5. Fallback resolver (models.json custom providers)
379
+ */
380
+ async getApiKey(providerId, options) {
381
+ // Runtime override takes highest priority
382
+ const runtimeKey = this.runtimeOverrides.get(providerId);
383
+ if (runtimeKey) {
384
+ return runtimeKey;
385
+ }
386
+ const cred = this.data[providerId];
387
+ if (cred?.type === "api_key") {
388
+ return resolveConfigValue(cred.key);
389
+ }
390
+ if (cred?.type === "oauth") {
391
+ const provider = getOAuthProvider(providerId);
392
+ if (!provider) {
393
+ // Unknown OAuth provider, can't get API key
394
+ return undefined;
395
+ }
396
+ // Check if token needs refresh
397
+ const needsRefresh = Date.now() >= cred.expires;
398
+ if (needsRefresh) {
399
+ // Use locked refresh to prevent race conditions
400
+ try {
401
+ const result = await this.refreshOAuthTokenWithLock(providerId);
402
+ if (result) {
403
+ return result.apiKey;
404
+ }
405
+ }
406
+ catch (error) {
407
+ this.recordError(error);
408
+ // Refresh failed - re-read file to check if another instance succeeded
409
+ this.reload();
410
+ const updatedCred = this.data[providerId];
411
+ if (updatedCred?.type === "oauth" && Date.now() < updatedCred.expires) {
412
+ // Another instance refreshed successfully, use those credentials
413
+ return provider.getApiKey(updatedCred);
414
+ }
415
+ // Refresh truly failed - return undefined so model discovery skips this provider
416
+ // User can /login to re-authenticate (credentials preserved for retry)
417
+ return undefined;
418
+ }
419
+ }
420
+ else {
421
+ // Token not expired, use current access token
422
+ return provider.getApiKey(cred);
423
+ }
424
+ }
425
+ // Fall back to environment variable
426
+ const envKey = getEnvApiKey(providerId);
427
+ if (envKey)
428
+ return envKey;
429
+ // Fall back to custom resolver (e.g., models.json custom providers)
430
+ if (options?.includeFallback !== false) {
431
+ return this.fallbackResolver?.(providerId) ?? undefined;
432
+ }
433
+ return undefined;
434
+ }
435
+ /**
436
+ * Get all registered OAuth providers
437
+ */
438
+ getOAuthProviders() {
439
+ return getOAuthProviders();
440
+ }
441
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Bash command execution with streaming support and cancellation.
3
+ *
4
+ * This module provides a unified bash execution implementation used by:
5
+ * - AgentSession.executeBash() for interactive and RPC modes
6
+ * - Direct calls from modes that need bash execution
7
+ */
8
+ import { randomBytes } from "node:crypto";
9
+ import { createWriteStream } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { stripAnsi } from "../utils/ansi.js";
13
+ import { sanitizeBinaryOutput } from "../utils/shell.js";
14
+ import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
15
+ // ============================================================================
16
+ // Implementation
17
+ // ============================================================================
18
+ /**
19
+ * Execute a bash command using custom BashOperations.
20
+ * Used for remote execution (SSH, containers, etc.).
21
+ */
22
+ export async function executeBashWithOperations(command, cwd, operations, options) {
23
+ const outputChunks = [];
24
+ let outputBytes = 0;
25
+ const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
26
+ let tempFilePath;
27
+ let tempFileStream;
28
+ let totalBytes = 0;
29
+ const ensureTempFile = () => {
30
+ if (tempFilePath) {
31
+ return;
32
+ }
33
+ const id = randomBytes(8).toString("hex");
34
+ tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
35
+ tempFileStream = createWriteStream(tempFilePath);
36
+ for (const chunk of outputChunks) {
37
+ tempFileStream.write(chunk);
38
+ }
39
+ };
40
+ const decoder = new TextDecoder();
41
+ const onData = (data) => {
42
+ totalBytes += data.length;
43
+ // Sanitize: strip ANSI, replace binary garbage, normalize newlines
44
+ const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\r/g, "");
45
+ // Start writing to temp file if exceeds threshold
46
+ if (totalBytes > DEFAULT_MAX_BYTES) {
47
+ ensureTempFile();
48
+ }
49
+ if (tempFileStream) {
50
+ tempFileStream.write(text);
51
+ }
52
+ // Keep rolling buffer
53
+ outputChunks.push(text);
54
+ outputBytes += text.length;
55
+ while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
56
+ const removed = outputChunks.shift();
57
+ outputBytes -= removed.length;
58
+ }
59
+ // Stream to callback
60
+ if (options?.onChunk) {
61
+ options.onChunk(text);
62
+ }
63
+ };
64
+ try {
65
+ const result = await operations.exec(command, cwd, {
66
+ onData,
67
+ signal: options?.signal,
68
+ });
69
+ const fullOutput = outputChunks.join("");
70
+ const truncationResult = truncateTail(fullOutput);
71
+ if (truncationResult.truncated) {
72
+ ensureTempFile();
73
+ }
74
+ if (tempFileStream) {
75
+ tempFileStream.end();
76
+ }
77
+ const cancelled = options?.signal?.aborted ?? false;
78
+ return {
79
+ output: truncationResult.truncated ? truncationResult.content : fullOutput,
80
+ exitCode: cancelled ? undefined : (result.exitCode ?? undefined),
81
+ cancelled,
82
+ truncated: truncationResult.truncated,
83
+ fullOutputPath: tempFilePath,
84
+ };
85
+ }
86
+ catch (err) {
87
+ // Check if it was an abort
88
+ if (options?.signal?.aborted) {
89
+ const fullOutput = outputChunks.join("");
90
+ const truncationResult = truncateTail(fullOutput);
91
+ if (truncationResult.truncated) {
92
+ ensureTempFile();
93
+ }
94
+ if (tempFileStream) {
95
+ tempFileStream.end();
96
+ }
97
+ return {
98
+ output: truncationResult.truncated ? truncationResult.content : fullOutput,
99
+ exitCode: undefined,
100
+ cancelled: true,
101
+ truncated: truncationResult.truncated,
102
+ fullOutputPath: tempFilePath,
103
+ };
104
+ }
105
+ if (tempFileStream) {
106
+ tempFileStream.end();
107
+ }
108
+ throw err;
109
+ }
110
+ }