@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.
- package/dist/generated/zeus/const.d.ts.map +1 -1
- package/dist/generated/zeus/const.js +14 -1
- package/dist/generated/zeus/index.d.ts +69 -0
- package/dist/generated/zeus/index.d.ts.map +1 -1
- package/dist/relay/rest.d.ts +2 -1
- package/dist/relay/rest.d.ts.map +1 -1
- package/dist/relay/rest.js +12 -2
- package/dist/relay/routing.d.ts +1 -1
- package/dist/relay/routing.d.ts.map +1 -1
- package/dist/relay/routing.js +9 -0
- package/dist/server/handlers/errors.d.ts +9 -6
- package/dist/server/handlers/errors.d.ts.map +1 -1
- package/dist/server/handlers/errors.js +11 -5
- package/dist/server/handlers/paths-autocomplete.d.ts +7 -2
- package/dist/server/handlers/paths-autocomplete.d.ts.map +1 -1
- package/dist/server/handlers/paths-autocomplete.js +7 -2
- package/dist/server/handlers/paths-mkdir.d.ts +26 -0
- package/dist/server/handlers/paths-mkdir.d.ts.map +1 -0
- package/dist/server/handlers/paths-mkdir.js +138 -0
- package/dist/server/handlers/sessions.d.ts +19 -0
- package/dist/server/handlers/sessions.d.ts.map +1 -1
- package/dist/server/handlers/sessions.js +69 -1
- package/package.json +1 -1
package/dist/relay/rest.d.ts
CHANGED
|
@@ -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)
|
package/dist/relay/rest.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/relay/rest.js
CHANGED
|
@@ -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);
|
package/dist/relay/routing.d.ts
CHANGED
|
@@ -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":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAKvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
|
package/dist/relay/routing.js
CHANGED
|
@@ -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
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
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
|
|
13
|
-
*
|
|
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;
|
|
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
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
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
|
|
13
|
-
*
|
|
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
|
|
20
|
-
*
|
|
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
|
|
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
|
|
20
|
-
*
|
|
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":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAK3H,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC;AAE5C,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;
|
|
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
|
-
|
|
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);
|