@aexol/spectral 0.8.0 → 0.8.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.
- package/dist/extensions/kanban-bridge.js +668 -0
- package/dist/extensions/spectral-vision-fallback.js +3 -2
- package/dist/mcp/init.js +1 -9
- package/dist/memory/index.js +2 -0
- package/dist/memory/tools/write-project-observation.js +60 -0
- package/dist/relay/auto-research.js +34 -0
- package/dist/sdk/ai/env-api-keys.js +9 -49
- package/dist/sdk/ai/utils/oauth/anthropic.js +1 -1
- package/dist/sdk/ai/utils/oauth/openai-codex.js +1 -1
- package/dist/sdk/coding-agent/config.js +2 -69
- package/dist/sdk/coding-agent/core/extensions/loader.js +2 -35
- package/dist/sdk/coding-agent/core/extensions/runner.js +1 -2
- package/dist/sdk/coding-agent/core/model-resolver-utils.js +8 -0
- package/dist/sdk/coding-agent/core/model-resolver.js +1 -1
- package/dist/sdk/coding-agent/core/resource-loader.js +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.js +1 -170
- package/dist/sdk/coding-agent/core/system-prompt.js +3 -1
- package/dist/sdk/coding-agent/core/theme.js +202 -0
- package/dist/sdk/coding-agent/core/tools/bash.js +17 -18
- package/dist/sdk/coding-agent/core/tools/edit.js +7 -8
- package/dist/sdk/coding-agent/core/tools/find.js +9 -13
- package/dist/sdk/coding-agent/core/tools/grep.js +10 -14
- package/dist/sdk/coding-agent/core/tools/ls.js +9 -10
- package/dist/sdk/coding-agent/core/tools/read.js +15 -25
- package/dist/sdk/coding-agent/{modes/interactive/components/diff.js → core/tools/render-diff.js} +18 -31
- package/dist/sdk/coding-agent/core/tools/write.js +10 -11
- package/dist/sdk/coding-agent/index.js +7 -5
- package/dist/sdk/coding-agent/modes/index.js +0 -1
- package/dist/sdk/coding-agent/modes/rpc/rpc-mode.js +2 -2
- package/dist/sdk/coding-agent/utils/photon.js +2 -10
- package/dist/sdk/coding-agent/utils/pi-user-agent.js +1 -2
- package/dist/server/agent-bridge.js +2 -1
- package/package.json +1 -1
- package/dist/sdk/coding-agent/bun/cli.js +0 -7
- package/dist/sdk/coding-agent/bun/restore-sandbox-env.js +0 -31
- package/dist/sdk/coding-agent/cli/args.js +0 -340
- package/dist/sdk/coding-agent/cli/file-processor.js +0 -82
- package/dist/sdk/coding-agent/cli/initial-message.js +0 -21
- package/dist/sdk/coding-agent/core/footer-data-provider.js +0 -309
- package/dist/sdk/coding-agent/modes/interactive/components/keybinding-hints.js +0 -35
- package/dist/sdk/coding-agent/modes/interactive/components/visual-truncate.js +0 -26
- package/dist/sdk/coding-agent/modes/interactive/interactive-mode.js +0 -3
- package/dist/sdk/coding-agent/modes/interactive/theme/theme.js +0 -1022
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import { execFile, spawnSync } from "child_process";
|
|
2
|
-
import { existsSync, readFileSync, statSync, unwatchFile, watchFile } from "fs";
|
|
3
|
-
import { dirname, join, resolve } from "path";
|
|
4
|
-
import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.js";
|
|
5
|
-
/**
|
|
6
|
-
* Find git metadata paths by walking up from cwd.
|
|
7
|
-
* Handles both regular git repos (.git is a directory) and worktrees (.git is a file).
|
|
8
|
-
*/
|
|
9
|
-
function findGitPaths(cwd) {
|
|
10
|
-
let dir = cwd;
|
|
11
|
-
while (true) {
|
|
12
|
-
const gitPath = join(dir, ".git");
|
|
13
|
-
if (existsSync(gitPath)) {
|
|
14
|
-
try {
|
|
15
|
-
const stat = statSync(gitPath);
|
|
16
|
-
if (stat.isFile()) {
|
|
17
|
-
const content = readFileSync(gitPath, "utf8").trim();
|
|
18
|
-
if (content.startsWith("gitdir: ")) {
|
|
19
|
-
const gitDir = resolve(dir, content.slice(8).trim());
|
|
20
|
-
const headPath = join(gitDir, "HEAD");
|
|
21
|
-
if (!existsSync(headPath))
|
|
22
|
-
return null;
|
|
23
|
-
const commonDirPath = join(gitDir, "commondir");
|
|
24
|
-
const commonGitDir = existsSync(commonDirPath)
|
|
25
|
-
? resolve(gitDir, readFileSync(commonDirPath, "utf8").trim())
|
|
26
|
-
: gitDir;
|
|
27
|
-
return { repoDir: dir, commonGitDir, headPath };
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
else if (stat.isDirectory()) {
|
|
31
|
-
const headPath = join(gitPath, "HEAD");
|
|
32
|
-
if (!existsSync(headPath))
|
|
33
|
-
return null;
|
|
34
|
-
return { repoDir: dir, commonGitDir: gitPath, headPath };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const parent = dirname(dir);
|
|
42
|
-
if (parent === dir)
|
|
43
|
-
return null;
|
|
44
|
-
dir = parent;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/** Ask git for the current branch. Returns null on detached HEAD or if git is unavailable. */
|
|
48
|
-
function resolveBranchWithGitSync(repoDir) {
|
|
49
|
-
const result = spawnSync("git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
50
|
-
cwd: repoDir,
|
|
51
|
-
encoding: "utf8",
|
|
52
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
53
|
-
});
|
|
54
|
-
const branch = result.status === 0 ? result.stdout.trim() : "";
|
|
55
|
-
return branch || null;
|
|
56
|
-
}
|
|
57
|
-
/** Ask git for the current branch asynchronously. Returns null on detached HEAD or if git is unavailable. */
|
|
58
|
-
function resolveBranchWithGitAsync(repoDir) {
|
|
59
|
-
return new Promise((resolvePromise) => {
|
|
60
|
-
execFile("git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
61
|
-
cwd: repoDir,
|
|
62
|
-
encoding: "utf8",
|
|
63
|
-
}, (error, stdout) => {
|
|
64
|
-
if (error) {
|
|
65
|
-
resolvePromise(null);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const branch = stdout.trim();
|
|
69
|
-
resolvePromise(branch || null);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Provides git branch and extension statuses - data not otherwise accessible to extensions.
|
|
75
|
-
* Token stats, model info available via ctx.sessionManager and ctx.model.
|
|
76
|
-
*/
|
|
77
|
-
export class FooterDataProvider {
|
|
78
|
-
cwd;
|
|
79
|
-
static WATCH_DEBOUNCE_MS = 500;
|
|
80
|
-
extensionStatuses = new Map();
|
|
81
|
-
cachedBranch = undefined;
|
|
82
|
-
gitPaths = undefined;
|
|
83
|
-
headWatcher = null;
|
|
84
|
-
reftableWatcher = null;
|
|
85
|
-
reftableTablesListWatcher = null;
|
|
86
|
-
reftableTablesListPath = null;
|
|
87
|
-
branchChangeCallbacks = new Set();
|
|
88
|
-
availableProviderCount = 0;
|
|
89
|
-
refreshTimer = null;
|
|
90
|
-
gitWatcherRetryTimer = null;
|
|
91
|
-
refreshInFlight = false;
|
|
92
|
-
refreshPending = false;
|
|
93
|
-
disposed = false;
|
|
94
|
-
constructor(cwd) {
|
|
95
|
-
this.cwd = cwd;
|
|
96
|
-
this.gitPaths = findGitPaths(cwd);
|
|
97
|
-
this.setupGitWatcher();
|
|
98
|
-
}
|
|
99
|
-
/** Current git branch, null if not in repo, "detached" if detached HEAD */
|
|
100
|
-
getGitBranch() {
|
|
101
|
-
if (this.cachedBranch === undefined) {
|
|
102
|
-
this.cachedBranch = this.resolveGitBranchSync();
|
|
103
|
-
}
|
|
104
|
-
return this.cachedBranch;
|
|
105
|
-
}
|
|
106
|
-
/** Extension status texts set via ctx.ui.setStatus() */
|
|
107
|
-
getExtensionStatuses() {
|
|
108
|
-
return this.extensionStatuses;
|
|
109
|
-
}
|
|
110
|
-
/** Subscribe to git branch changes. Returns unsubscribe function. */
|
|
111
|
-
onBranchChange(callback) {
|
|
112
|
-
this.branchChangeCallbacks.add(callback);
|
|
113
|
-
return () => this.branchChangeCallbacks.delete(callback);
|
|
114
|
-
}
|
|
115
|
-
/** Internal: set extension status */
|
|
116
|
-
setExtensionStatus(key, text) {
|
|
117
|
-
if (text === undefined) {
|
|
118
|
-
this.extensionStatuses.delete(key);
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
this.extensionStatuses.set(key, text);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/** Internal: clear extension statuses */
|
|
125
|
-
clearExtensionStatuses() {
|
|
126
|
-
this.extensionStatuses.clear();
|
|
127
|
-
}
|
|
128
|
-
/** Number of unique providers with available models (for footer display) */
|
|
129
|
-
getAvailableProviderCount() {
|
|
130
|
-
return this.availableProviderCount;
|
|
131
|
-
}
|
|
132
|
-
/** Internal: update available provider count */
|
|
133
|
-
setAvailableProviderCount(count) {
|
|
134
|
-
this.availableProviderCount = count;
|
|
135
|
-
}
|
|
136
|
-
setCwd(cwd) {
|
|
137
|
-
if (this.cwd === cwd) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
this.cwd = cwd;
|
|
141
|
-
if (this.refreshTimer) {
|
|
142
|
-
clearTimeout(this.refreshTimer);
|
|
143
|
-
this.refreshTimer = null;
|
|
144
|
-
}
|
|
145
|
-
this.clearGitWatchers();
|
|
146
|
-
this.cachedBranch = undefined;
|
|
147
|
-
this.gitPaths = findGitPaths(cwd);
|
|
148
|
-
this.setupGitWatcher();
|
|
149
|
-
this.notifyBranchChange();
|
|
150
|
-
}
|
|
151
|
-
/** Internal: cleanup */
|
|
152
|
-
dispose() {
|
|
153
|
-
this.disposed = true;
|
|
154
|
-
if (this.refreshTimer) {
|
|
155
|
-
clearTimeout(this.refreshTimer);
|
|
156
|
-
this.refreshTimer = null;
|
|
157
|
-
}
|
|
158
|
-
this.clearGitWatchers();
|
|
159
|
-
this.branchChangeCallbacks.clear();
|
|
160
|
-
}
|
|
161
|
-
notifyBranchChange() {
|
|
162
|
-
for (const cb of this.branchChangeCallbacks)
|
|
163
|
-
cb();
|
|
164
|
-
}
|
|
165
|
-
scheduleRefresh() {
|
|
166
|
-
if (this.disposed || this.refreshTimer)
|
|
167
|
-
return;
|
|
168
|
-
if (this.refreshInFlight) {
|
|
169
|
-
this.refreshPending = true;
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
this.refreshTimer = setTimeout(() => {
|
|
173
|
-
this.refreshTimer = null;
|
|
174
|
-
void this.refreshGitBranchAsync();
|
|
175
|
-
}, FooterDataProvider.WATCH_DEBOUNCE_MS);
|
|
176
|
-
}
|
|
177
|
-
async refreshGitBranchAsync() {
|
|
178
|
-
if (this.disposed)
|
|
179
|
-
return;
|
|
180
|
-
if (this.refreshInFlight) {
|
|
181
|
-
this.refreshPending = true;
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
this.refreshInFlight = true;
|
|
185
|
-
try {
|
|
186
|
-
const nextBranch = await this.resolveGitBranchAsync();
|
|
187
|
-
if (this.disposed)
|
|
188
|
-
return;
|
|
189
|
-
if (this.cachedBranch !== undefined && this.cachedBranch !== nextBranch) {
|
|
190
|
-
this.cachedBranch = nextBranch;
|
|
191
|
-
this.notifyBranchChange();
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
this.cachedBranch = nextBranch;
|
|
195
|
-
}
|
|
196
|
-
finally {
|
|
197
|
-
this.refreshInFlight = false;
|
|
198
|
-
if (this.refreshPending && !this.disposed) {
|
|
199
|
-
this.refreshPending = false;
|
|
200
|
-
this.scheduleRefresh();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
resolveGitBranchSync() {
|
|
205
|
-
try {
|
|
206
|
-
if (!this.gitPaths)
|
|
207
|
-
return null;
|
|
208
|
-
const content = readFileSync(this.gitPaths.headPath, "utf8").trim();
|
|
209
|
-
if (content.startsWith("ref: refs/heads/")) {
|
|
210
|
-
const branch = content.slice(16);
|
|
211
|
-
return branch === ".invalid" ? (resolveBranchWithGitSync(this.gitPaths.repoDir) ?? "detached") : branch;
|
|
212
|
-
}
|
|
213
|
-
return "detached";
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
async resolveGitBranchAsync() {
|
|
220
|
-
try {
|
|
221
|
-
if (!this.gitPaths)
|
|
222
|
-
return null;
|
|
223
|
-
const content = readFileSync(this.gitPaths.headPath, "utf8").trim();
|
|
224
|
-
if (content.startsWith("ref: refs/heads/")) {
|
|
225
|
-
const branch = content.slice(16);
|
|
226
|
-
return branch === ".invalid"
|
|
227
|
-
? ((await resolveBranchWithGitAsync(this.gitPaths.repoDir)) ?? "detached")
|
|
228
|
-
: branch;
|
|
229
|
-
}
|
|
230
|
-
return "detached";
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
clearGitWatchers() {
|
|
237
|
-
closeWatcher(this.headWatcher);
|
|
238
|
-
this.headWatcher = null;
|
|
239
|
-
closeWatcher(this.reftableWatcher);
|
|
240
|
-
this.reftableWatcher = null;
|
|
241
|
-
closeWatcher(this.reftableTablesListWatcher);
|
|
242
|
-
this.reftableTablesListWatcher = null;
|
|
243
|
-
if (this.reftableTablesListPath) {
|
|
244
|
-
unwatchFile(this.reftableTablesListPath);
|
|
245
|
-
this.reftableTablesListPath = null;
|
|
246
|
-
}
|
|
247
|
-
if (this.gitWatcherRetryTimer) {
|
|
248
|
-
clearTimeout(this.gitWatcherRetryTimer);
|
|
249
|
-
this.gitWatcherRetryTimer = null;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
scheduleGitWatcherRetry() {
|
|
253
|
-
if (this.disposed || this.gitWatcherRetryTimer) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
this.gitWatcherRetryTimer = setTimeout(() => {
|
|
257
|
-
this.gitWatcherRetryTimer = null;
|
|
258
|
-
this.setupGitWatcher();
|
|
259
|
-
}, FS_WATCH_RETRY_DELAY_MS);
|
|
260
|
-
}
|
|
261
|
-
handleGitWatcherError() {
|
|
262
|
-
this.clearGitWatchers();
|
|
263
|
-
this.scheduleGitWatcherRetry();
|
|
264
|
-
}
|
|
265
|
-
setupGitWatcher() {
|
|
266
|
-
this.clearGitWatchers();
|
|
267
|
-
if (!this.gitPaths)
|
|
268
|
-
return;
|
|
269
|
-
// Watch the directory containing HEAD, not HEAD itself.
|
|
270
|
-
// Git uses atomic writes (write temp, rename over HEAD), which changes the inode.
|
|
271
|
-
// fs.watch on a file stops working after the inode changes.
|
|
272
|
-
this.headWatcher = watchWithErrorHandler(dirname(this.gitPaths.headPath), (_eventType, filename) => {
|
|
273
|
-
if (!filename || filename === "HEAD") {
|
|
274
|
-
this.scheduleRefresh();
|
|
275
|
-
}
|
|
276
|
-
}, () => this.handleGitWatcherError());
|
|
277
|
-
if (!this.headWatcher) {
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
// In reftable repos, branch switches update files in the reftable directory
|
|
281
|
-
// instead of HEAD. Watch it separately so the footer picks up those changes.
|
|
282
|
-
const reftableDir = join(this.gitPaths.commonGitDir, "reftable");
|
|
283
|
-
if (existsSync(reftableDir)) {
|
|
284
|
-
this.reftableWatcher = watchWithErrorHandler(reftableDir, () => {
|
|
285
|
-
this.scheduleRefresh();
|
|
286
|
-
}, () => this.handleGitWatcherError());
|
|
287
|
-
if (!this.reftableWatcher) {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const tablesListPath = join(reftableDir, "tables.list");
|
|
291
|
-
if (existsSync(tablesListPath)) {
|
|
292
|
-
this.reftableTablesListPath = tablesListPath;
|
|
293
|
-
this.reftableTablesListWatcher = watchWithErrorHandler(tablesListPath, () => {
|
|
294
|
-
this.scheduleRefresh();
|
|
295
|
-
}, () => this.handleGitWatcherError());
|
|
296
|
-
if (!this.reftableTablesListWatcher) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
watchFile(tablesListPath, { interval: 250 }, (current, previous) => {
|
|
300
|
-
if (current.mtimeMs !== previous.mtimeMs ||
|
|
301
|
-
current.ctimeMs !== previous.ctimeMs ||
|
|
302
|
-
current.size !== previous.size) {
|
|
303
|
-
this.scheduleRefresh();
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utilities for formatting keybinding hints in the UI.
|
|
3
|
-
*/
|
|
4
|
-
import { getKeybindings } from "../../../core/keybindings.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
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utility for truncating text to visual lines.
|
|
3
|
-
* Simplified for headless mode — returns lines directly without Text component.
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Truncate text to a maximum number of visual lines (from the end).
|
|
7
|
-
*
|
|
8
|
-
* @param text - The text content (may contain newlines)
|
|
9
|
-
* @param maxVisualLines - Maximum number of visual lines to show
|
|
10
|
-
* @param _width - Ignored in headless mode
|
|
11
|
-
* @param _paddingX - Ignored in headless mode
|
|
12
|
-
* @returns The truncated visual lines and count of skipped lines
|
|
13
|
-
*/
|
|
14
|
-
export function truncateToVisualLines(text, maxVisualLines, _width, _paddingX = 0) {
|
|
15
|
-
if (!text) {
|
|
16
|
-
return { visualLines: [], skippedCount: 0 };
|
|
17
|
-
}
|
|
18
|
-
const allVisualLines = text.split("\n");
|
|
19
|
-
if (allVisualLines.length <= maxVisualLines) {
|
|
20
|
-
return { visualLines: allVisualLines, skippedCount: 0 };
|
|
21
|
-
}
|
|
22
|
-
// Take the last N visual lines
|
|
23
|
-
const truncatedLines = allVisualLines.slice(-maxVisualLines);
|
|
24
|
-
const skippedCount = allVisualLines.length - maxVisualLines;
|
|
25
|
-
return { visualLines: truncatedLines, skippedCount };
|
|
26
|
-
}
|