@aexol/spectral 0.2.8 → 0.2.10

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.
@@ -15,7 +15,7 @@
15
15
  * keeps individual tool results manageable even across multiple MCP calls
16
16
  * in the same turn.
17
17
  */
18
- const MAX_MCP_TEXT_LENGTH = 20_000;
18
+ const MAX_MCP_TEXT_LENGTH = 100_000;
19
19
  function truncateTextBlock(block) {
20
20
  if (block.type !== "text")
21
21
  return block;
@@ -39,6 +39,7 @@
39
39
  * transparent to this layer — we just respond when we can.
40
40
  */
41
41
  import { BadRequestError, NotFoundError } from "../server/handlers/errors.js";
42
+ import { handlePathAutocomplete } from "../server/handlers/paths-autocomplete.js";
42
43
  import { handleCreateProject, handleDeleteProject, handleListProjects, handleListSessionsByProject, handleUpdateProject, } from "../server/handlers/projects.js";
43
44
  import { handleCreateSession, handleDeleteSession, handleGetSessionDetail, handleUpdateSession, } from "../server/handlers/sessions.js";
44
45
  import { shutdownState } from "../server/shutdown.js";
@@ -50,9 +51,16 @@ import { shutdownState } from "../server/shutdown.js";
50
51
  * also marginally slower and harder to read for ~9 routes.
51
52
  */
52
53
  export function matchRoute(method, path) {
53
- // Strip query string if any (we don't use any, but be defensive).
54
+ // Strip query string for path matching but keep it for the handler.
54
55
  const qIdx = path.indexOf("?");
55
56
  const cleanPath = qIdx === -1 ? path : path.slice(0, qIdx);
57
+ const query = qIdx >= 0 ? new URLSearchParams(path.slice(qIdx + 1)) : undefined;
58
+ // /api/paths/autocomplete
59
+ if (cleanPath === "/api/paths/autocomplete") {
60
+ if (method === "GET")
61
+ return { route: "list_path_autocomplete", query };
62
+ return null;
63
+ }
56
64
  // /api/projects
57
65
  if (cleanPath === "/api/projects") {
58
66
  if (method === "GET")
@@ -267,6 +275,10 @@ function dispatchRoute(match, body, deps) {
267
275
  }
268
276
  return { ok: true };
269
277
  }
278
+ case "list_path_autocomplete": {
279
+ const prefix = match.query?.get("prefix") ?? "";
280
+ return handlePathAutocomplete(prefix);
281
+ }
270
282
  }
271
283
  }
272
284
  /**
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Filesystem autocomplete for project path creation.
3
+ *
4
+ * When a user types a partial path in the New Project form, the browser
5
+ * sends `GET /api/paths/autocomplete?prefix=<partial>`. This handler
6
+ * expands the prefix (tilde → $HOME, relative → absolute), lists the
7
+ * containing directory, and returns up to 10 directory suggestions whose
8
+ * basenames start with the typed partial segment.
9
+ *
10
+ * Dotfiles are excluded — they are rarely intentional project roots.
11
+ * Symlinks to directories ARE included (readdirSync with withFileTypes
12
+ * returns true for both real dirs and symlink dirs).
13
+ *
14
+ * Thread-safety: synchronous, called from the relay-dispatcher hot path.
15
+ * fs ops block the event loop for microseconds on local SSDs; the form
16
+ * is typed by a single human, so this is perfectly fine.
17
+ */
18
+ import { readdirSync } from "node:fs";
19
+ import { join, sep } from "node:path";
20
+ import { expandPath } from "../paths.js";
21
+ /**
22
+ * List directories inside `expandedPrefix`'s parent that start with the
23
+ * trailing (post-separator) segment of the expanded prefix.
24
+ *
25
+ * Examples (assume $HOME=/Users/alice, dirs are `projects`, `proj-old`,
26
+ * `Documents`, `.config`):
27
+ * prefix="~/pro" → expanded="/Users/alice/pro", parent="/Users/alice/"
28
+ * → ["/Users/alice/projects", "/Users/alice/proj-old"]
29
+ * prefix="~/proj" → expanded="/Users/alice/proj"
30
+ * → ["/Users/alice/projects"]
31
+ * prefix="~" → expanded="/Users/alice", parent="/Users/alice/"
32
+ * → all top-level dirs in $HOME (except dotfiles)
33
+ */
34
+ export function handlePathAutocomplete(prefix) {
35
+ const expanded = expandPath(prefix);
36
+ // Split into parent directory and basename prefix.
37
+ // On POSIX, `expanded` is always absolute after expandPath, so we
38
+ // always hit the else branch below with a trailing sep.
39
+ const lastSep = expanded.lastIndexOf(sep);
40
+ let parentDir;
41
+ let basenamePrefix;
42
+ if (lastSep < 0) {
43
+ // Shouldn't happen after expandPath, but be defensive.
44
+ parentDir = expanded;
45
+ basenamePrefix = "";
46
+ }
47
+ else {
48
+ parentDir = expanded.slice(0, lastSep + 1);
49
+ basenamePrefix = expanded.slice(lastSep + 1);
50
+ }
51
+ let suggestions = [];
52
+ try {
53
+ const entries = readdirSync(parentDir, { withFileTypes: true });
54
+ suggestions = entries
55
+ .filter((e) => e.isDirectory() &&
56
+ e.name.startsWith(basenamePrefix) &&
57
+ !e.name.startsWith("."))
58
+ .map((e) => join(parentDir, e.name))
59
+ .slice(0, 10); // keep the list short for the UI
60
+ }
61
+ catch {
62
+ // Directory doesn't exist or is unreadable → return empty list.
63
+ // The caller (dispatcher) returns a 200 with an empty suggestions[]
64
+ // so the UI can show a "no matching directories" hint.
65
+ }
66
+ return { suggestions, expandedPrefix: expanded };
67
+ }
@@ -211,7 +211,7 @@ function parseWireToolEvents(eventsJsonl) {
211
211
  * or context-window overflow — both surface as "skipping empty intermediate
212
212
  * message" / hung sessions after reconnect.
213
213
  */
214
- const MAX_REHYDRATED_TEXT = 20_000;
214
+ const MAX_REHYDRATED_TEXT = 100_000;
215
215
  function sanitizeRehydratedBlock(block) {
216
216
  // Convert images to text placeholders — most LLM APIs reject
217
217
  // ImageContent inside tool-result messages (even multimodal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,