@aexol/spectral 0.9.243 → 0.9.244

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.
@@ -2,7 +2,7 @@
2
2
  * Relay REST request handler (CLI side).
3
3
  *
4
4
  * Translates an inbound `RestRequestFrame` into a `dispatchRoute` call and
5
- * maps handler errors to status codes (400/404/500). Errors thrown by
5
+ * maps handler errors to status codes (400/404/409/500). Errors thrown by
6
6
  * handlers are returned in the body, never re-thrown — the backend has a
7
7
  * 30 s pending-request timeout and we want every request to terminate with
8
8
  * a wire response so the browser doesn't sit on a stale spinner.
@@ -22,6 +22,7 @@ import { type RestRequestDeps } from "./routing.js";
22
22
  * - 400: `BadRequestError` thrown by handler, OR malformed body for a
23
23
  * route that requires one, OR unknown route
24
24
  * - 404: `NotFoundError` thrown by handler
25
+ * - 409: `ConflictError` thrown by handler (e.g. mkdir on an existing path)
25
26
  * - 405: route matches but method doesn't (returned as 404 since we don't
26
27
  * distinguish — the matcher treats it as "no route", consistent
27
28
  * with the original Hono router which also 404'd)
@@ -1 +1 @@
1
- {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/relay/rest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/E;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,iBAAiB,CAAC,CAoE5B"}
1
+ {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/relay/rest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAO/B,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/E;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,iBAAiB,CAAC,CA+E5B"}
@@ -2,7 +2,7 @@
2
2
  * Relay REST request handler (CLI side).
3
3
  *
4
4
  * Translates an inbound `RestRequestFrame` into a `dispatchRoute` call and
5
- * maps handler errors to status codes (400/404/500). Errors thrown by
5
+ * maps handler errors to status codes (400/404/409/500). Errors thrown by
6
6
  * handlers are returned in the body, never re-thrown — the backend has a
7
7
  * 30 s pending-request timeout and we want every request to terminate with
8
8
  * a wire response so the browser doesn't sit on a stale spinner.
@@ -10,7 +10,7 @@
10
10
  * Extracted from `relay/dispatcher.ts` — the dispatcher re-exports
11
11
  * `handleRestRequest` so existing imports keep working.
12
12
  */
13
- import { BadRequestError, NotFoundError } from "../server/handlers/errors.js";
13
+ import { BadRequestError, ConflictError, NotFoundError, } from "../server/handlers/errors.js";
14
14
  import { dispatchRoute, matchRoute } from "./routing.js";
15
15
  /**
16
16
  * Dispatch a `rest_request` frame. Always resolves with a
@@ -22,6 +22,7 @@ import { dispatchRoute, matchRoute } from "./routing.js";
22
22
  * - 400: `BadRequestError` thrown by handler, OR malformed body for a
23
23
  * route that requires one, OR unknown route
24
24
  * - 404: `NotFoundError` thrown by handler
25
+ * - 409: `ConflictError` thrown by handler (e.g. mkdir on an existing path)
25
26
  * - 405: route matches but method doesn't (returned as 404 since we don't
26
27
  * distinguish — the matcher treats it as "no route", consistent
27
28
  * with the original Hono router which also 404'd)
@@ -71,6 +72,15 @@ export async function handleRestRequest(frame, deps) {
71
72
  body: { error: err.message, code: err.code },
72
73
  };
73
74
  }
75
+ if (err instanceof ConflictError) {
76
+ logger.info?.(`[rest] finish machineId=${frame.machineId} method=${method} path=${path} reqId=${reqId} status=409 durationMs=${Math.round(performance.now() - startedAt)}`);
77
+ return {
78
+ kind: "rest_response",
79
+ reqId,
80
+ status: 409,
81
+ body: { error: err.message, code: err.code },
82
+ };
83
+ }
74
84
  // Unexpected — log locally so operators can correlate, but only
75
85
  // return a sanitized message to the caller.
76
86
  logger.error?.(`[dispatcher] unexpected error in ${method} ${path}:`, err);
@@ -20,7 +20,7 @@ import { type WizardLoginResult } from "./wizard-login.js";
20
20
  * `id` is populated when the route had an `:id` placeholder.
21
21
  */
22
22
  interface RouteMatch {
23
- route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "unbind_studio" | "get_project_observations" | "list_project_sessions" | "cleanup_project_sessions" | "cleanup_all_sessions" | "create_session" | "get_session" | "get_session_active_agent" | "update_session_active_agent" | "get_session_messages" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "list_directory" | "get_home_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "reload_mcp_config" | "refresh_native_extensions" | "refresh_cloud_agents" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "get_prompt_mutation_settings" | "put_prompt_mutation_settings" | "wizard_login" | "list_agents" | "list_dev_processes" | "start_dev_process" | "kill_dev_process" | "restart_dev_process" | "scan_dev_servers" | "kill_dev_server";
23
+ route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "unbind_studio" | "get_project_observations" | "list_project_sessions" | "cleanup_project_sessions" | "cleanup_all_sessions" | "create_session" | "get_session" | "get_session_active_agent" | "update_session_active_agent" | "get_session_messages" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "list_directory" | "get_home_directory" | "create_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "reload_mcp_config" | "refresh_native_extensions" | "refresh_cloud_agents" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "get_prompt_mutation_settings" | "put_prompt_mutation_settings" | "wizard_login" | "list_agents" | "list_dev_processes" | "start_dev_process" | "kill_dev_process" | "restart_dev_process" | "scan_dev_servers" | "kill_dev_server";
24
24
  id?: string;
25
25
  /** Parsed query params, if the path carried a `?...` suffix. */
26
26
  query?: URLSearchParams;
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,7 @@ import { handleReloadMcpConfig } from "../server/handlers/mcp-reload.js";
16
16
  import { handlePathAutocomplete } from "../server/handlers/paths-autocomplete.js";
17
17
  import { handleGetHomeDirectory } from "../server/handlers/paths-home.js";
18
18
  import { handleListDirectory } from "../server/handlers/paths-list.js";
19
+ import { handleCreateDirectory } from "../server/handlers/paths-mkdir.js";
19
20
  import { handleSearchProjectFiles } from "../server/handlers/files-search.js";
20
21
  import { handleBindStudioProject, handleUnbindStudioProject, handleCleanupProjectSessions, handleCreateProject, handleDeleteProject, handleGetProjectObservations, handleListProjectsAsync, handleListSessionsByProjectAsync, handleUpdateProject, } from "../server/handlers/projects.js";
21
22
  import { handleCompactSession, handleCleanupAllSessions, handleCreateSession, handleDeleteSession, handleForkSession, handleRememberAndDeleteSession, handleGetSessionDetailAsync, handleGetSessionMessagesAsync, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, handleGetSessionActiveAgentAsync, handleUpdateSessionActiveAgentAsync, } from "../server/handlers/sessions.js";
@@ -110,6 +111,12 @@ export function matchRoute(method, path) {
110
111
  return { route: "get_home_directory" };
111
112
  return null;
112
113
  }
114
+ // /api/paths/mkdir
115
+ if (cleanPath === "/api/paths/mkdir") {
116
+ if (method === "POST")
117
+ return { route: "create_directory" };
118
+ return null;
119
+ }
113
120
  // /api/projects
114
121
  if (cleanPath === "/api/projects") {
115
122
  if (method === "GET")
@@ -553,6 +560,8 @@ export async function dispatchRoute(match, body, deps) {
553
560
  return await handleListDirectory(asObject(body));
554
561
  case "get_home_directory":
555
562
  return handleGetHomeDirectory();
563
+ case "create_directory":
564
+ return await handleCreateDirectory(asObject(body));
556
565
  case "search_project_files": {
557
566
  const searchQuery = match.query?.get("query") ?? "";
558
567
  return handleSearchProjectFiles(store, id, searchQuery);
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * Typed errors thrown by REST handlers.
3
3
  *
4
- * Each error carries a stable `code` that the relay envelope dispatcher
5
- * (Batch 3) maps to a wire-level status. Until then, the handlers throw
6
- * and the relay loop just acks — Batch 3 will catch and translate.
4
+ * Each error carries a stable `code` that the relay REST loop
5
+ * (`cli-node/src/relay/rest.ts`) catches and maps to a wire-level status
6
+ * (400/404/409/500) in the `rest_response` envelope.
7
7
  *
8
8
  * Why typed errors instead of returning `{ ok: false, code }` discriminated
9
9
  * unions? Two reasons:
10
10
  * - The handlers are also called directly from unit tests; `throw` lets
11
11
  * tests use `expect(...).rejects.toThrow(NotFoundError)` ergonomically.
12
- * - The Batch 3 dispatcher needs a single `try/catch` boundary; a sum
13
- * type per handler would force every handler to return its own variant.
12
+ * - The relay REST loop needs a single `try/catch` boundary; a sum type
13
+ * per handler would force every handler to return its own variant.
14
14
  */
15
- export type HandlerErrorCode = "BAD_REQUEST" | "NOT_FOUND";
15
+ export type HandlerErrorCode = "BAD_REQUEST" | "NOT_FOUND" | "CONFLICT";
16
16
  export declare class HandlerError extends Error {
17
17
  readonly code: HandlerErrorCode;
18
18
  constructor(code: HandlerErrorCode, message: string);
@@ -23,4 +23,7 @@ export declare class BadRequestError extends HandlerError {
23
23
  export declare class NotFoundError extends HandlerError {
24
24
  constructor(message: string);
25
25
  }
26
+ export declare class ConflictError extends HandlerError {
27
+ constructor(message: string);
28
+ }
26
29
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,WAAW,CAAC;AAE3D,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;gBACpB,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM;CAKpD;AAED,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,aAAc,SAAQ,YAAY;gBACjC,OAAO,EAAE,MAAM;CAI5B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAExE,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;gBACpB,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM;CAKpD;AAED,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,aAAc,SAAQ,YAAY;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,aAAc,SAAQ,YAAY;gBACjC,OAAO,EAAE,MAAM;CAI5B"}
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Typed errors thrown by REST handlers.
3
3
  *
4
- * Each error carries a stable `code` that the relay envelope dispatcher
5
- * (Batch 3) maps to a wire-level status. Until then, the handlers throw
6
- * and the relay loop just acks — Batch 3 will catch and translate.
4
+ * Each error carries a stable `code` that the relay REST loop
5
+ * (`cli-node/src/relay/rest.ts`) catches and maps to a wire-level status
6
+ * (400/404/409/500) in the `rest_response` envelope.
7
7
  *
8
8
  * Why typed errors instead of returning `{ ok: false, code }` discriminated
9
9
  * unions? Two reasons:
10
10
  * - The handlers are also called directly from unit tests; `throw` lets
11
11
  * tests use `expect(...).rejects.toThrow(NotFoundError)` ergonomically.
12
- * - The Batch 3 dispatcher needs a single `try/catch` boundary; a sum
13
- * type per handler would force every handler to return its own variant.
12
+ * - The relay REST loop needs a single `try/catch` boundary; a sum type
13
+ * per handler would force every handler to return its own variant.
14
14
  */
15
15
  export class HandlerError extends Error {
16
16
  code;
@@ -32,3 +32,9 @@ export class NotFoundError extends HandlerError {
32
32
  this.name = "NotFoundError";
33
33
  }
34
34
  }
35
+ export class ConflictError extends HandlerError {
36
+ constructor(message) {
37
+ super("CONFLICT", message);
38
+ this.name = "ConflictError";
39
+ }
40
+ }
@@ -16,8 +16,13 @@
16
16
  * starts-with match so the user can filter siblings while typing.
17
17
  *
18
18
  * Dotfiles are excluded — they are rarely intentional project roots.
19
- * Symlinks to directories ARE included (readdirSync with withFileTypes
20
- * returns true for both real dirs and symlink dirs).
19
+ * Symlinks to directories are NOT included: `readdir(..., { withFileTypes })`
20
+ * reports `Dirent.isDirectory() === false` for symlinks (even when the
21
+ * symlink target is a directory), and the filter in collectSuggestions keeps
22
+ * only real directories. Note that strategy 1's `stat(expanded)` does follow
23
+ * symlinks, so a typed prefix that is itself a symlink to a directory still
24
+ * drills into it — but its symlinked children are filtered out of the
25
+ * suggestions.
21
26
  *
22
27
  * Thread-safety: called from the relay-dispatcher hot path, so filesystem
23
28
  * enumeration uses `node:fs/promises` and yields to the event loop rather
@@ -1 +1 @@
1
- {"version":3,"file":"paths-autocomplete.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/paths-autocomplete.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAMH,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB;AAgCD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,sBAAsB,CAAC,CA+BjC"}
1
+ {"version":3,"file":"paths-autocomplete.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/paths-autocomplete.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAMH,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB;AAgCD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,sBAAsB,CAAC,CA+BjC"}
@@ -16,8 +16,13 @@
16
16
  * starts-with match so the user can filter siblings while typing.
17
17
  *
18
18
  * Dotfiles are excluded — they are rarely intentional project roots.
19
- * Symlinks to directories ARE included (readdirSync with withFileTypes
20
- * returns true for both real dirs and symlink dirs).
19
+ * Symlinks to directories are NOT included: `readdir(..., { withFileTypes })`
20
+ * reports `Dirent.isDirectory() === false` for symlinks (even when the
21
+ * symlink target is a directory), and the filter in collectSuggestions keeps
22
+ * only real directories. Note that strategy 1's `stat(expanded)` does follow
23
+ * symlinks, so a typed prefix that is itself a symlink to a directory still
24
+ * drills into it — but its symlinked children are filtered out of the
25
+ * suggestions.
21
26
  *
22
27
  * Thread-safety: called from the relay-dispatcher hot path, so filesystem
23
28
  * enumeration uses `node:fs/promises` and yields to the event loop rather
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Create a directory inside an existing parent directory.
3
+ *
4
+ * Used by the directory picker in the browser/desktop wrapper so users can
5
+ * scaffold a project folder without leaving the "Select local folder" dialog.
6
+ * Mirrors `paths-list.ts`: same path resolution (`join` over the trimmed
7
+ * parent), same error mapping, and no path whitelist — auth lives on the
8
+ * backend WS relay + team ACL, not on the CLI transport.
9
+ */
10
+ import type { DirectoryEntry } from "./paths-list.js";
11
+ export interface CreateDirectoryResult {
12
+ entry: DirectoryEntry;
13
+ }
14
+ /**
15
+ * Validate a new directory name against the cross-platform rules shared
16
+ * with the picker UI. Returns a human-readable error message, or `null`
17
+ * when the name is valid.
18
+ */
19
+ export declare function validateNewDirectoryName(name: string): string | null;
20
+ /**
21
+ * Create `parent/name` and return its entry. The parent must already exist
22
+ * and be a directory; the target must not exist (`EEXIST` surfaces as a
23
+ * distinguishable "already exists" conflict).
24
+ */
25
+ export declare function handleCreateDirectory(input: Record<string, unknown>): Promise<CreateDirectoryResult>;
26
+ //# sourceMappingURL=paths-mkdir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths-mkdir.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/paths-mkdir.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,cAAc,CAAC;CACvB;AAgCD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6BpE;AAQD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,qBAAqB,CAAC,CA0DhC"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Create a directory inside an existing parent directory.
3
+ *
4
+ * Used by the directory picker in the browser/desktop wrapper so users can
5
+ * scaffold a project folder without leaving the "Select local folder" dialog.
6
+ * Mirrors `paths-list.ts`: same path resolution (`join` over the trimmed
7
+ * parent), same error mapping, and no path whitelist — auth lives on the
8
+ * backend WS relay + team ACL, not on the CLI transport.
9
+ */
10
+ import { mkdir, stat } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { BadRequestError, ConflictError } from "./errors.js";
13
+ /**
14
+ * Windows-reserved device names (matched case-insensitively). Creating a
15
+ * file with one of these names fails on Windows, so we reject up front to
16
+ * keep the picker's error surfaced where the user can fix it.
17
+ */
18
+ const WINDOWS_RESERVED_NAMES = new Set([
19
+ "CON",
20
+ "PRN",
21
+ "AUX",
22
+ "NUL",
23
+ "COM1",
24
+ "COM2",
25
+ "COM3",
26
+ "COM4",
27
+ "COM5",
28
+ "COM6",
29
+ "COM7",
30
+ "COM8",
31
+ "COM9",
32
+ "LPT1",
33
+ "LPT2",
34
+ "LPT3",
35
+ "LPT4",
36
+ "LPT5",
37
+ "LPT6",
38
+ "LPT7",
39
+ "LPT8",
40
+ "LPT9",
41
+ ]);
42
+ /**
43
+ * Validate a new directory name against the cross-platform rules shared
44
+ * with the picker UI. Returns a human-readable error message, or `null`
45
+ * when the name is valid.
46
+ */
47
+ export function validateNewDirectoryName(name) {
48
+ if (name === "") {
49
+ return "name (string) is required";
50
+ }
51
+ if (name.includes("/") || name.includes("\\")) {
52
+ return "name must not contain path separators";
53
+ }
54
+ if (name === "." || name === "..") {
55
+ return `name must not be "${name}"`;
56
+ }
57
+ // Dotfiles are filtered from the picker's listings, so a folder whose
58
+ // name starts with "." would be created invisible — rejected to keep
59
+ // the contract explicit (mirrors the picker UI validator).
60
+ if (name.startsWith(".")) {
61
+ return "name must not start with a '.'";
62
+ }
63
+ if (/[<>:"|?*]/.test(name)) {
64
+ return "name must not contain any of the characters <>:\"|?*";
65
+ }
66
+ if (WINDOWS_RESERVED_NAMES.has(name.toUpperCase())) {
67
+ return `name is reserved on Windows: ${name}`;
68
+ }
69
+ if (/[. ]$/.test(name)) {
70
+ return "name must not end with a '.' or space";
71
+ }
72
+ if (Buffer.byteLength(name, "utf8") > 255) {
73
+ return "name must be at most 255 bytes";
74
+ }
75
+ return null;
76
+ }
77
+ function errCode(err) {
78
+ return err !== null && typeof err === "object" && "code" in err
79
+ ? err.code
80
+ : undefined;
81
+ }
82
+ /**
83
+ * Create `parent/name` and return its entry. The parent must already exist
84
+ * and be a directory; the target must not exist (`EEXIST` surfaces as a
85
+ * distinguishable "already exists" conflict).
86
+ */
87
+ export async function handleCreateDirectory(input) {
88
+ const rawParent = input.parent;
89
+ if (typeof rawParent !== "string" || rawParent.trim() === "") {
90
+ throw new BadRequestError("parent (string) is required");
91
+ }
92
+ const parent = rawParent.trim();
93
+ const rawName = input.name;
94
+ if (typeof rawName !== "string") {
95
+ throw new BadRequestError("name (string) is required");
96
+ }
97
+ const nameError = validateNewDirectoryName(rawName);
98
+ if (nameError !== null) {
99
+ throw new BadRequestError(nameError);
100
+ }
101
+ // The parent must exist and be a directory (stat).
102
+ let parentStats;
103
+ try {
104
+ parentStats = await stat(parent);
105
+ }
106
+ catch (err) {
107
+ const code = errCode(err);
108
+ if (code === "ENOENT") {
109
+ throw new BadRequestError(`Parent directory does not exist: ${parent}`);
110
+ }
111
+ if (code === "EACCES" || code === "EPERM") {
112
+ throw new BadRequestError(`Permission denied: ${parent}`);
113
+ }
114
+ throw new BadRequestError(`Could not access parent directory: ${err instanceof Error ? err.message : String(err)}`);
115
+ }
116
+ if (!parentStats.isDirectory()) {
117
+ throw new BadRequestError(`Parent is not a directory: ${parent}`);
118
+ }
119
+ // Path resolved exactly like paths-list: `join` over the trimmed parent.
120
+ const targetPath = join(parent, rawName);
121
+ try {
122
+ await mkdir(targetPath);
123
+ }
124
+ catch (err) {
125
+ const code = errCode(err);
126
+ if (code === "EEXIST") {
127
+ throw new ConflictError(`Directory already exists: ${targetPath}`);
128
+ }
129
+ if (code === "ENOENT") {
130
+ throw new BadRequestError(`Parent directory does not exist: ${parent}`);
131
+ }
132
+ if (code === "EACCES" || code === "EPERM") {
133
+ throw new BadRequestError(`Permission denied: ${targetPath}`);
134
+ }
135
+ throw new BadRequestError(`Could not create directory: ${err instanceof Error ? err.message : String(err)}`);
136
+ }
137
+ return { entry: { name: rawName, path: targetPath, isDirectory: true } };
138
+ }
@@ -16,6 +16,7 @@ export type CompactSessionMode = "dcp-lite";
16
16
  export interface CreateSessionInputRaw {
17
17
  projectId?: unknown;
18
18
  title?: unknown;
19
+ history?: unknown;
19
20
  }
20
21
  export interface UpdateSessionInputRaw {
21
22
  title?: unknown;
@@ -39,6 +40,24 @@ export interface GetSessionMessagesInputRaw {
39
40
  export interface CompactSessionInputRaw {
40
41
  mode?: unknown;
41
42
  }
43
+ export interface SessionHistoryMessage {
44
+ role: "user" | "assistant";
45
+ content: string;
46
+ }
47
+ /**
48
+ * Parse and validate the optional `history` body field.
49
+ *
50
+ * Contract:
51
+ * - omitted/null → empty array (behavior unchanged)
52
+ * - non-array → BadRequestError
53
+ * - non-object items or non-string role/content → BadRequestError with a
54
+ * per-field message (`history[i].role must be a string`)
55
+ * - only "user"/"assistant" roles are kept; others are dropped
56
+ * - image/attachment fields are stripped by construction — only role and
57
+ * content are carried over
58
+ * - clamped to 200 items / 20k chars per content / 150k chars total
59
+ */
60
+ export declare function parseSessionHistory(history: unknown): SessionHistoryMessage[];
42
61
  export declare function parseCompactSessionMode(body: unknown): CompactSessionMode;
43
62
  export declare function handleCreateSession(store: SessionStore, body: CreateSessionInputRaw): WireSessionSummary;
44
63
  export declare function handleGetSessionDetail(store: SessionStore, id: string): WireSessionDetail;
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/sessions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/sessions.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,65 @@
12
12
  import { projectHistoryForReplay } from "../../relay/history-projection.js";
13
13
  import { BadRequestError, NotFoundError } from "./errors.js";
14
14
  import { loadAgentSettings, loadAgentSettingsAsync } from "./agent-settings.js";
15
+ // ----------------------------------------------------------------------
16
+ // History seeding (POST /api/sessions `history` field)
17
+ //
18
+ // "Send as new subject": the caller may seed the new session with the past
19
+ // session's message history so the agent bridge loads it on first subscribe
20
+ // (SessionStreamManager.attachOnce reads the full store history to rehydrate
21
+ // the LLM bridge). Caps mirror the backend planSubjectSession resolver.
22
+ // ----------------------------------------------------------------------
23
+ const MAX_HISTORY_ITEMS = 200;
24
+ const MAX_HISTORY_CONTENT = 20_000;
25
+ const MAX_HISTORY_TOTAL = 150_000;
26
+ /**
27
+ * Parse and validate the optional `history` body field.
28
+ *
29
+ * Contract:
30
+ * - omitted/null → empty array (behavior unchanged)
31
+ * - non-array → BadRequestError
32
+ * - non-object items or non-string role/content → BadRequestError with a
33
+ * per-field message (`history[i].role must be a string`)
34
+ * - only "user"/"assistant" roles are kept; others are dropped
35
+ * - image/attachment fields are stripped by construction — only role and
36
+ * content are carried over
37
+ * - clamped to 200 items / 20k chars per content / 150k chars total
38
+ */
39
+ export function parseSessionHistory(history) {
40
+ if (history === undefined || history === null)
41
+ return [];
42
+ if (!Array.isArray(history)) {
43
+ throw new BadRequestError("history (array of {role, content}) is optional when provided");
44
+ }
45
+ const out = [];
46
+ let total = 0;
47
+ for (let i = 0; i < history.length; i++) {
48
+ const item = history[i];
49
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
50
+ throw new BadRequestError(`history[${i}] must be an object with role and content`);
51
+ }
52
+ const { role, content } = item;
53
+ if (typeof role !== "string") {
54
+ throw new BadRequestError(`history[${i}].role must be a string`);
55
+ }
56
+ if (typeof content !== "string") {
57
+ throw new BadRequestError(`history[${i}].content must be a string`);
58
+ }
59
+ if (role !== "user" && role !== "assistant")
60
+ continue;
61
+ if (out.length >= MAX_HISTORY_ITEMS)
62
+ break;
63
+ const remaining = MAX_HISTORY_TOTAL - total;
64
+ if (remaining <= 0)
65
+ break;
66
+ total += content.length > remaining ? remaining : content.length;
67
+ out.push({
68
+ role,
69
+ content: content.slice(0, Math.min(MAX_HISTORY_CONTENT, remaining)),
70
+ });
71
+ }
72
+ return out;
73
+ }
15
74
  function effectiveActiveAgent(stored, cwd, agentDir) {
16
75
  return stored ??
17
76
  loadAgentSettings(cwd ?? process.cwd(), agentDir).activePrimaryAgent;
@@ -42,7 +101,16 @@ export function handleCreateSession(store, body) {
42
101
  if (!project)
43
102
  throw new BadRequestError("Unknown projectId");
44
103
  const title = typeof body.title === "string" ? body.title : undefined;
45
- return store.createSession({ projectId: body.projectId, title });
104
+ // Validate the history BEFORE creating the session so an invalid payload
105
+ // never leaves an empty orphan row behind.
106
+ const history = parseSessionHistory(body.history);
107
+ const summary = store.createSession({ projectId: body.projectId, title });
108
+ // Append the seeded history in order — the bridge loads store messages at
109
+ // first subscribe, so these must be written at creation time.
110
+ for (const msg of history) {
111
+ store.appendMessage(summary.id, { role: msg.role, content: msg.content });
112
+ }
113
+ return history.length > 0 ? { ...summary, messageCount: history.length } : summary;
46
114
  }
47
115
  export function handleGetSessionDetail(store, id) {
48
116
  const meta = store.getSessionMeta(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.243",
3
+ "version": "0.9.244",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,