@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,280 @@
1
+ /**
2
+ * One-time migrations that run on startup.
3
+ */
4
+ import chalk from "chalk";
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
6
+ import { dirname, join } from "path";
7
+ import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.js";
8
+ import { migrateKeybindingsConfig } from "./core/keybindings.js";
9
+ const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
10
+ const EXTENSIONS_DOC_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/extensions.md";
11
+ /**
12
+ * Migrate legacy oauth.json and settings.json apiKeys to auth.json.
13
+ *
14
+ * @returns Array of provider names that were migrated
15
+ */
16
+ export function migrateAuthToAuthJson() {
17
+ const agentDir = getAgentDir();
18
+ const authPath = join(agentDir, "auth.json");
19
+ const oauthPath = join(agentDir, "oauth.json");
20
+ const settingsPath = join(agentDir, "settings.json");
21
+ // Skip if auth.json already exists
22
+ if (existsSync(authPath))
23
+ return [];
24
+ const migrated = {};
25
+ const providers = [];
26
+ // Migrate oauth.json
27
+ if (existsSync(oauthPath)) {
28
+ try {
29
+ const oauth = JSON.parse(readFileSync(oauthPath, "utf-8"));
30
+ for (const [provider, cred] of Object.entries(oauth)) {
31
+ migrated[provider] = { type: "oauth", ...cred };
32
+ providers.push(provider);
33
+ }
34
+ renameSync(oauthPath, `${oauthPath}.migrated`);
35
+ }
36
+ catch {
37
+ // Skip on error
38
+ }
39
+ }
40
+ // Migrate settings.json apiKeys
41
+ if (existsSync(settingsPath)) {
42
+ try {
43
+ const content = readFileSync(settingsPath, "utf-8");
44
+ const settings = JSON.parse(content);
45
+ if (settings.apiKeys && typeof settings.apiKeys === "object") {
46
+ for (const [provider, key] of Object.entries(settings.apiKeys)) {
47
+ if (!migrated[provider] && typeof key === "string") {
48
+ migrated[provider] = { type: "api_key", key };
49
+ providers.push(provider);
50
+ }
51
+ }
52
+ delete settings.apiKeys;
53
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
54
+ }
55
+ }
56
+ catch {
57
+ // Skip on error
58
+ }
59
+ }
60
+ if (Object.keys(migrated).length > 0) {
61
+ mkdirSync(dirname(authPath), { recursive: true });
62
+ writeFileSync(authPath, JSON.stringify(migrated, null, 2), { mode: 0o600 });
63
+ }
64
+ return providers;
65
+ }
66
+ /**
67
+ * Migrate sessions from ~/.pi/agent/*.jsonl to proper session directories.
68
+ *
69
+ * Bug in v0.30.0: Sessions were saved to ~/.pi/agent/ instead of
70
+ * ~/.pi/agent/sessions/<encoded-cwd>/. This migration moves them
71
+ * to the correct location based on the cwd in their session header.
72
+ *
73
+ * See: https://github.com/earendil-works/pi-mono/issues/320
74
+ */
75
+ export function migrateSessionsFromAgentRoot() {
76
+ const agentDir = getAgentDir();
77
+ // Find all .jsonl files directly in agentDir (not in subdirectories)
78
+ let files;
79
+ try {
80
+ files = readdirSync(agentDir)
81
+ .filter((f) => f.endsWith(".jsonl"))
82
+ .map((f) => join(agentDir, f));
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ if (files.length === 0)
88
+ return;
89
+ for (const file of files) {
90
+ try {
91
+ // Read first line to get session header
92
+ const content = readFileSync(file, "utf8");
93
+ const firstLine = content.split("\n")[0];
94
+ if (!firstLine?.trim())
95
+ continue;
96
+ const header = JSON.parse(firstLine);
97
+ if (header.type !== "session" || !header.cwd)
98
+ continue;
99
+ const cwd = header.cwd;
100
+ // Compute the correct session directory (same encoding as session-manager.ts)
101
+ const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
102
+ const correctDir = join(agentDir, "sessions", safePath);
103
+ // Create directory if needed
104
+ if (!existsSync(correctDir)) {
105
+ mkdirSync(correctDir, { recursive: true });
106
+ }
107
+ // Move the file
108
+ const fileName = file.split("/").pop() || file.split("\\").pop();
109
+ const newPath = join(correctDir, fileName);
110
+ if (existsSync(newPath))
111
+ continue; // Skip if target exists
112
+ renameSync(file, newPath);
113
+ }
114
+ catch {
115
+ // Skip files that can't be migrated
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * Migrate commands/ to prompts/ if needed.
121
+ * Works for both regular directories and symlinks.
122
+ */
123
+ function migrateCommandsToPrompts(baseDir, label) {
124
+ const commandsDir = join(baseDir, "commands");
125
+ const promptsDir = join(baseDir, "prompts");
126
+ if (existsSync(commandsDir) && !existsSync(promptsDir)) {
127
+ try {
128
+ renameSync(commandsDir, promptsDir);
129
+ console.log(chalk.green(`Migrated ${label} commands/ → prompts/`));
130
+ return true;
131
+ }
132
+ catch (err) {
133
+ console.log(chalk.yellow(`Warning: Could not migrate ${label} commands/ to prompts/: ${err instanceof Error ? err.message : err}`));
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+ function migrateKeybindingsConfigFile() {
139
+ const configPath = join(getAgentDir(), "keybindings.json");
140
+ if (!existsSync(configPath))
141
+ return;
142
+ try {
143
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
144
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
145
+ return;
146
+ }
147
+ const { config, migrated } = migrateKeybindingsConfig(parsed);
148
+ if (!migrated)
149
+ return;
150
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
151
+ }
152
+ catch {
153
+ // Ignore malformed files during migration
154
+ }
155
+ }
156
+ /**
157
+ * Move fd/rg binaries from tools/ to bin/ if they exist.
158
+ */
159
+ function migrateToolsToBin() {
160
+ const agentDir = getAgentDir();
161
+ const toolsDir = join(agentDir, "tools");
162
+ const binDir = getBinDir();
163
+ if (!existsSync(toolsDir))
164
+ return;
165
+ const binaries = ["fd", "rg", "fd.exe", "rg.exe"];
166
+ let movedAny = false;
167
+ for (const bin of binaries) {
168
+ const oldPath = join(toolsDir, bin);
169
+ const newPath = join(binDir, bin);
170
+ if (existsSync(oldPath)) {
171
+ if (!existsSync(binDir)) {
172
+ mkdirSync(binDir, { recursive: true });
173
+ }
174
+ if (!existsSync(newPath)) {
175
+ try {
176
+ renameSync(oldPath, newPath);
177
+ movedAny = true;
178
+ }
179
+ catch {
180
+ // Ignore errors
181
+ }
182
+ }
183
+ else {
184
+ // Target exists, just delete the old one
185
+ try {
186
+ rmSync?.(oldPath, { force: true });
187
+ }
188
+ catch {
189
+ // Ignore
190
+ }
191
+ }
192
+ }
193
+ }
194
+ if (movedAny) {
195
+ console.log(chalk.green(`Migrated managed binaries tools/ → bin/`));
196
+ }
197
+ }
198
+ /**
199
+ * Check for deprecated hooks/ and tools/ directories.
200
+ * Note: tools/ may contain fd/rg binaries extracted by pi, so only warn if it has other files.
201
+ */
202
+ function checkDeprecatedExtensionDirs(baseDir, label) {
203
+ const hooksDir = join(baseDir, "hooks");
204
+ const toolsDir = join(baseDir, "tools");
205
+ const warnings = [];
206
+ if (existsSync(hooksDir)) {
207
+ warnings.push(`${label} hooks/ directory found. Hooks have been renamed to extensions.`);
208
+ }
209
+ if (existsSync(toolsDir)) {
210
+ // Check if tools/ contains anything other than fd/rg (which are auto-extracted binaries)
211
+ try {
212
+ const entries = readdirSync(toolsDir);
213
+ const customTools = entries.filter((e) => {
214
+ const lower = e.toLowerCase();
215
+ return (lower !== "fd" && lower !== "rg" && lower !== "fd.exe" && lower !== "rg.exe" && !e.startsWith(".") // Ignore .DS_Store and other hidden files
216
+ );
217
+ });
218
+ if (customTools.length > 0) {
219
+ warnings.push(`${label} tools/ directory contains custom tools. Custom tools have been merged into extensions.`);
220
+ }
221
+ }
222
+ catch {
223
+ // Ignore read errors
224
+ }
225
+ }
226
+ return warnings;
227
+ }
228
+ /**
229
+ * Run extension system migrations (commands→prompts) and collect warnings about deprecated directories.
230
+ */
231
+ function migrateExtensionSystem(cwd) {
232
+ const agentDir = getAgentDir();
233
+ const projectDir = join(cwd, CONFIG_DIR_NAME);
234
+ // Migrate commands/ to prompts/
235
+ migrateCommandsToPrompts(agentDir, "Global");
236
+ migrateCommandsToPrompts(projectDir, "Project");
237
+ // Check for deprecated directories
238
+ const warnings = [
239
+ ...checkDeprecatedExtensionDirs(agentDir, "Global"),
240
+ ...checkDeprecatedExtensionDirs(projectDir, "Project"),
241
+ ];
242
+ return warnings;
243
+ }
244
+ /**
245
+ * Print deprecation warnings and wait for keypress.
246
+ */
247
+ export async function showDeprecationWarnings(warnings) {
248
+ if (warnings.length === 0)
249
+ return;
250
+ for (const warning of warnings) {
251
+ console.log(chalk.yellow(`Warning: ${warning}`));
252
+ }
253
+ console.log(chalk.yellow(`\nMove your extensions to the extensions/ directory.`));
254
+ console.log(chalk.yellow(`Migration guide: ${MIGRATION_GUIDE_URL}`));
255
+ console.log(chalk.yellow(`Documentation: ${EXTENSIONS_DOC_URL}`));
256
+ console.log(chalk.dim(`\nPress any key to continue...`));
257
+ await new Promise((resolve) => {
258
+ process.stdin.setRawMode?.(true);
259
+ process.stdin.resume();
260
+ process.stdin.once("data", () => {
261
+ process.stdin.setRawMode?.(false);
262
+ process.stdin.pause();
263
+ resolve();
264
+ });
265
+ });
266
+ console.log();
267
+ }
268
+ /**
269
+ * Run all migrations. Called once on startup.
270
+ *
271
+ * @returns Object with migration results and deprecation warnings
272
+ */
273
+ export function runMigrations(cwd) {
274
+ const migratedAuthProviders = migrateAuthToAuthJson();
275
+ migrateSessionsFromAgentRoot();
276
+ migrateToolsToBin();
277
+ migrateKeybindingsConfigFile();
278
+ const deprecationWarnings = migrateExtensionSystem(cwd);
279
+ return { migratedAuthProviders, deprecationWarnings };
280
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Run modes for the coding agent.
3
+ */
4
+ export { InteractiveMode } from "./interactive/interactive-mode.js";
5
+ export { runPrintMode } from "./print-mode.js";
6
+ export { RpcClient } from "./rpc/rpc-client.js";
7
+ export { runRpcMode } from "./rpc/rpc-mode.js";
@@ -0,0 +1,132 @@
1
+ import * as Diff from "diff";
2
+ import { theme } from "../theme/theme.js";
3
+ /**
4
+ * Parse diff line to extract prefix, line number, and content.
5
+ * Format: "+123 content" or "-123 content" or " 123 content" or " ..."
6
+ */
7
+ function parseDiffLine(line) {
8
+ const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
9
+ if (!match)
10
+ return null;
11
+ return { prefix: match[1], lineNum: match[2], content: match[3] };
12
+ }
13
+ /**
14
+ * Replace tabs with spaces for consistent rendering.
15
+ */
16
+ function replaceTabs(text) {
17
+ return text.replace(/\t/g, " ");
18
+ }
19
+ /**
20
+ * Compute word-level diff and render with inverse on changed parts.
21
+ * Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.
22
+ * Strips leading whitespace from inverse to avoid highlighting indentation.
23
+ */
24
+ function renderIntraLineDiff(oldContent, newContent) {
25
+ const wordDiff = Diff.diffWords(oldContent, newContent);
26
+ let removedLine = "";
27
+ let addedLine = "";
28
+ let isFirstRemoved = true;
29
+ let isFirstAdded = true;
30
+ for (const part of wordDiff) {
31
+ if (part.removed) {
32
+ let value = part.value;
33
+ // Strip leading whitespace from the first removed part
34
+ if (isFirstRemoved) {
35
+ const leadingWs = value.match(/^(\s*)/)?.[1] || "";
36
+ value = value.slice(leadingWs.length);
37
+ removedLine += leadingWs;
38
+ isFirstRemoved = false;
39
+ }
40
+ if (value) {
41
+ removedLine += theme.inverse(value);
42
+ }
43
+ }
44
+ else if (part.added) {
45
+ let value = part.value;
46
+ // Strip leading whitespace from the first added part
47
+ if (isFirstAdded) {
48
+ const leadingWs = value.match(/^(\s*)/)?.[1] || "";
49
+ value = value.slice(leadingWs.length);
50
+ addedLine += leadingWs;
51
+ isFirstAdded = false;
52
+ }
53
+ if (value) {
54
+ addedLine += theme.inverse(value);
55
+ }
56
+ }
57
+ else {
58
+ removedLine += part.value;
59
+ addedLine += part.value;
60
+ }
61
+ }
62
+ return { removedLine, addedLine };
63
+ }
64
+ /**
65
+ * Render a diff string with colored lines and intra-line change highlighting.
66
+ * - Context lines: dim/gray
67
+ * - Removed lines: red, with inverse on changed tokens
68
+ * - Added lines: green, with inverse on changed tokens
69
+ */
70
+ export function renderDiff(diffText, _options = {}) {
71
+ const lines = diffText.split("\n");
72
+ const result = [];
73
+ let i = 0;
74
+ while (i < lines.length) {
75
+ const line = lines[i];
76
+ const parsed = parseDiffLine(line);
77
+ if (!parsed) {
78
+ result.push(theme.fg("toolDiffContext", line));
79
+ i++;
80
+ continue;
81
+ }
82
+ if (parsed.prefix === "-") {
83
+ // Collect consecutive removed lines
84
+ const removedLines = [];
85
+ while (i < lines.length) {
86
+ const p = parseDiffLine(lines[i]);
87
+ if (!p || p.prefix !== "-")
88
+ break;
89
+ removedLines.push({ lineNum: p.lineNum, content: p.content });
90
+ i++;
91
+ }
92
+ // Collect consecutive added lines
93
+ const addedLines = [];
94
+ while (i < lines.length) {
95
+ const p = parseDiffLine(lines[i]);
96
+ if (!p || p.prefix !== "+")
97
+ break;
98
+ addedLines.push({ lineNum: p.lineNum, content: p.content });
99
+ i++;
100
+ }
101
+ // Only do intra-line diffing when there's exactly one removed and one added line
102
+ // (indicating a single line modification). Otherwise, show lines as-is.
103
+ if (removedLines.length === 1 && addedLines.length === 1) {
104
+ const removed = removedLines[0];
105
+ const added = addedLines[0];
106
+ const { removedLine, addedLine } = renderIntraLineDiff(replaceTabs(removed.content), replaceTabs(added.content));
107
+ result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`));
108
+ result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`));
109
+ }
110
+ else {
111
+ // Show all removed lines first, then all added lines
112
+ for (const removed of removedLines) {
113
+ result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
114
+ }
115
+ for (const added of addedLines) {
116
+ result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
117
+ }
118
+ }
119
+ }
120
+ else if (parsed.prefix === "+") {
121
+ // Standalone added line
122
+ result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
123
+ i++;
124
+ }
125
+ else {
126
+ // Context line
127
+ result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
128
+ i++;
129
+ }
130
+ }
131
+ return result.join("\n");
132
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Utilities for formatting keybinding hints in the UI.
3
+ */
4
+ import { getKeybindings } from "../../../../tui/index.js";
5
+ import { theme } from "../theme/theme.js";
6
+ function formatKeyPart(part, options) {
7
+ const displayPart = process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part;
8
+ return options.capitalize ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) : displayPart;
9
+ }
10
+ export function formatKeyText(key, options = {}) {
11
+ return key
12
+ .split("/")
13
+ .map((k) => k
14
+ .split("+")
15
+ .map((part) => formatKeyPart(part, options))
16
+ .join("+"))
17
+ .join("/");
18
+ }
19
+ function formatKeys(keys, options = {}) {
20
+ if (keys.length === 0)
21
+ return "";
22
+ return formatKeyText(keys.join("/"), options);
23
+ }
24
+ export function keyText(keybinding) {
25
+ return formatKeys(getKeybindings().getKeys(keybinding));
26
+ }
27
+ export function keyDisplayText(keybinding) {
28
+ return formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true });
29
+ }
30
+ export function keyHint(keybinding, description) {
31
+ return theme.fg("dim", keyText(keybinding)) + theme.fg("muted", ` ${description}`);
32
+ }
33
+ export function rawKeyHint(key, description) {
34
+ return theme.fg("dim", formatKeyText(key)) + theme.fg("muted", ` ${description}`);
35
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared utility for truncating text to visual lines (accounting for line wrapping).
3
+ * Used by both tool-execution.ts and bash-execution.ts for consistent behavior.
4
+ */
5
+ import { Text } from "../../../../tui/index.js";
6
+ /**
7
+ * Truncate text to a maximum number of visual lines (from the end).
8
+ * This accounts for line wrapping based on terminal width.
9
+ *
10
+ * @param text - The text content (may contain newlines)
11
+ * @param maxVisualLines - Maximum number of visual lines to show
12
+ * @param width - Terminal/render width
13
+ * @param paddingX - Horizontal padding for Text component (default 0).
14
+ * Use 0 when result will be placed in a Box (Box adds its own padding).
15
+ * Use 1 when result will be placed in a plain Container.
16
+ * @returns The truncated visual lines and count of skipped lines
17
+ */
18
+ export function truncateToVisualLines(text, maxVisualLines, width, paddingX = 0) {
19
+ if (!text) {
20
+ return { visualLines: [], skippedCount: 0 };
21
+ }
22
+ // Create a temporary Text component to render and get visual lines
23
+ const tempText = new Text(text, paddingX, 0);
24
+ const allVisualLines = tempText.render(width);
25
+ if (allVisualLines.length <= maxVisualLines) {
26
+ return { visualLines: allVisualLines, skippedCount: 0 };
27
+ }
28
+ // Take the last N visual lines
29
+ const truncatedLines = allVisualLines.slice(-maxVisualLines);
30
+ const skippedCount = allVisualLines.length - maxVisualLines;
31
+ return { visualLines: truncatedLines, skippedCount };
32
+ }
@@ -0,0 +1,3 @@
1
+ // Interactive mode removed (not used in SDK mode by spectral)
2
+ // Replaced with no-op exports to satisfy imports during build.
3
+ export const InteractiveMode = undefined;