@celestea/studio 2.7.1
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/LICENSE +21 -0
- package/dist/app.d.ts +111 -0
- package/dist/app.js +219 -0
- package/dist/auth/api-token.d.ts +76 -0
- package/dist/auth/api-token.js +172 -0
- package/dist/auth/htpasswd.d.ts +22 -0
- package/dist/auth/htpasswd.js +61 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +14 -0
- package/dist/auth/page.d.ts +19 -0
- package/dist/auth/page.js +73 -0
- package/dist/auth/rate-limit.d.ts +27 -0
- package/dist/auth/rate-limit.js +47 -0
- package/dist/auth/token.d.ts +49 -0
- package/dist/auth/token.js +112 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +72 -0
- package/dist/deployment.d.ts +92 -0
- package/dist/deployment.js +154 -0
- package/dist/fake-runtime-adapter.d.ts +37 -0
- package/dist/fake-runtime-adapter.js +276 -0
- package/dist/handlers/auth.d.ts +24 -0
- package/dist/handlers/auth.js +155 -0
- package/dist/handlers/common.d.ts +76 -0
- package/dist/handlers/common.js +127 -0
- package/dist/handlers/config-shape.d.ts +76 -0
- package/dist/handlers/config-shape.js +227 -0
- package/dist/handlers/config.d.ts +26 -0
- package/dist/handlers/config.js +161 -0
- package/dist/handlers/context-shape.d.ts +33 -0
- package/dist/handlers/context-shape.js +59 -0
- package/dist/handlers/dialog.d.ts +22 -0
- package/dist/handlers/dialog.js +292 -0
- package/dist/handlers/exec.d.ts +22 -0
- package/dist/handlers/exec.js +131 -0
- package/dist/handlers/fs-read.d.ts +63 -0
- package/dist/handlers/fs-read.js +159 -0
- package/dist/handlers/fs.d.ts +57 -0
- package/dist/handlers/fs.js +153 -0
- package/dist/handlers/grants-shape.d.ts +45 -0
- package/dist/handlers/grants-shape.js +130 -0
- package/dist/handlers/grants.d.ts +21 -0
- package/dist/handlers/grants.js +267 -0
- package/dist/handlers/health.d.ts +37 -0
- package/dist/handlers/health.js +139 -0
- package/dist/handlers/index.d.ts +40 -0
- package/dist/handlers/index.js +85 -0
- package/dist/handlers/permissions.d.ts +9 -0
- package/dist/handlers/permissions.js +183 -0
- package/dist/handlers/plugins.d.ts +18 -0
- package/dist/handlers/plugins.js +23 -0
- package/dist/handlers/prompts.d.ts +12 -0
- package/dist/handlers/prompts.js +120 -0
- package/dist/handlers/providers.d.ts +12 -0
- package/dist/handlers/providers.js +178 -0
- package/dist/handlers/questions.d.ts +23 -0
- package/dist/handlers/questions.js +115 -0
- package/dist/handlers/session-model.d.ts +38 -0
- package/dist/handlers/session-model.js +105 -0
- package/dist/handlers/session-move.d.ts +22 -0
- package/dist/handlers/session-move.js +188 -0
- package/dist/handlers/session-tools.d.ts +19 -0
- package/dist/handlers/session-tools.js +75 -0
- package/dist/handlers/sessions.d.ts +32 -0
- package/dist/handlers/sessions.js +242 -0
- package/dist/handlers/usage.d.ts +26 -0
- package/dist/handlers/usage.js +77 -0
- package/dist/handlers/worker.d.ts +12 -0
- package/dist/handlers/worker.js +82 -0
- package/dist/handlers/workspaces.d.ts +12 -0
- package/dist/handlers/workspaces.js +88 -0
- package/dist/harness.test-util.d.ts +101 -0
- package/dist/harness.test-util.js +176 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +35 -0
- package/dist/plugins.d.ts +105 -0
- package/dist/plugins.js +97 -0
- package/dist/question-registry.d.ts +124 -0
- package/dist/question-registry.js +212 -0
- package/dist/question-rows.d.ts +32 -0
- package/dist/question-rows.js +43 -0
- package/dist/replay/compare.d.ts +47 -0
- package/dist/replay/compare.js +89 -0
- package/dist/replay/e2e-replay.d.ts +46 -0
- package/dist/replay/e2e-replay.js +91 -0
- package/dist/replay/expect-compact.d.ts +31 -0
- package/dist/replay/expect-compact.js +92 -0
- package/dist/replay/fixtures.d.ts +52 -0
- package/dist/replay/fixtures.js +53 -0
- package/dist/replay/host.d.ts +32 -0
- package/dist/replay/host.js +62 -0
- package/dist/replay/index.d.ts +16 -0
- package/dist/replay/index.js +16 -0
- package/dist/replay/probes.d.ts +55 -0
- package/dist/replay/probes.js +225 -0
- package/dist/replay/report.d.ts +13 -0
- package/dist/replay/report.js +103 -0
- package/dist/replay/session-e2e.d.ts +44 -0
- package/dist/replay/session-e2e.js +91 -0
- package/dist/routes.d.ts +47 -0
- package/dist/routes.js +59 -0
- package/dist/runtime/attachments-llm.d.ts +13 -0
- package/dist/runtime/attachments-llm.js +37 -0
- package/dist/runtime/boot-recovery.d.ts +32 -0
- package/dist/runtime/boot-recovery.js +71 -0
- package/dist/runtime/context-snapshot.d.ts +59 -0
- package/dist/runtime/context-snapshot.js +113 -0
- package/dist/runtime/engine-grants.d.ts +121 -0
- package/dist/runtime/engine-grants.js +338 -0
- package/dist/runtime/engine-permissions.d.ts +29 -0
- package/dist/runtime/engine-permissions.js +92 -0
- package/dist/runtime/engine-plugins.d.ts +189 -0
- package/dist/runtime/engine-plugins.js +292 -0
- package/dist/runtime/engine-profile.d.ts +28 -0
- package/dist/runtime/engine-profile.js +88 -0
- package/dist/runtime/engine-session.d.ts +9 -0
- package/dist/runtime/engine-session.js +9 -0
- package/dist/runtime/fallback-contract.d.ts +32 -0
- package/dist/runtime/fallback-contract.js +10 -0
- package/dist/runtime/fallback-host.d.ts +134 -0
- package/dist/runtime/fallback-host.js +299 -0
- package/dist/runtime/host-autowake.d.ts +79 -0
- package/dist/runtime/host-autowake.js +86 -0
- package/dist/runtime/image-downgrade.d.ts +49 -0
- package/dist/runtime/image-downgrade.js +90 -0
- package/dist/runtime/inbox-message.d.ts +11 -0
- package/dist/runtime/inbox-message.js +20 -0
- package/dist/runtime/index.d.ts +17 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/ledger-view.d.ts +40 -0
- package/dist/runtime/ledger-view.js +56 -0
- package/dist/runtime/llm-assembly.d.ts +66 -0
- package/dist/runtime/llm-assembly.js +114 -0
- package/dist/runtime/offline-llm.d.ts +71 -0
- package/dist/runtime/offline-llm.js +132 -0
- package/dist/runtime/provider-target.d.ts +10 -0
- package/dist/runtime/provider-target.js +10 -0
- package/dist/runtime/question-host.d.ts +33 -0
- package/dist/runtime/question-host.js +41 -0
- package/dist/runtime/question-view.d.ts +48 -0
- package/dist/runtime/question-view.js +76 -0
- package/dist/runtime/real-runtime-adapter.d.ts +95 -0
- package/dist/runtime/real-runtime-adapter.js +553 -0
- package/dist/runtime/recovery-audit.d.ts +81 -0
- package/dist/runtime/recovery-audit.js +95 -0
- package/dist/runtime/recovery-view.d.ts +36 -0
- package/dist/runtime/recovery-view.js +63 -0
- package/dist/runtime/session-compose.d.ts +235 -0
- package/dist/runtime/session-compose.js +350 -0
- package/dist/runtime/session-grants.d.ts +36 -0
- package/dist/runtime/session-grants.js +69 -0
- package/dist/runtime/session-lifecycle.d.ts +40 -0
- package/dist/runtime/session-lifecycle.js +86 -0
- package/dist/runtime/session-publisher.d.ts +40 -0
- package/dist/runtime/session-publisher.js +48 -0
- package/dist/runtime/session-release.d.ts +53 -0
- package/dist/runtime/session-release.js +80 -0
- package/dist/runtime/test-util.d.ts +78 -0
- package/dist/runtime/test-util.js +162 -0
- package/dist/runtime/watchdog-view.d.ts +33 -0
- package/dist/runtime/watchdog-view.js +45 -0
- package/dist/runtime/worker-bridge.d.ts +76 -0
- package/dist/runtime/worker-bridge.js +156 -0
- package/dist/runtime/worker-live.d.ts +22 -0
- package/dist/runtime/worker-live.js +25 -0
- package/dist/runtime/worker-recovery.d.ts +35 -0
- package/dist/runtime/worker-recovery.js +64 -0
- package/dist/runtime/worker-table.d.ts +58 -0
- package/dist/runtime/worker-table.js +70 -0
- package/dist/runtime-adapter.d.ts +421 -0
- package/dist/runtime-adapter.js +76 -0
- package/dist/serial-queue.d.ts +16 -0
- package/dist/serial-queue.js +22 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.js +119 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +31 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.js +186 -0
- package/dist/static.d.ts +20 -0
- package/dist/static.js +120 -0
- package/dist/store/builtin-sections.d.ts +52 -0
- package/dist/store/builtin-sections.js +118 -0
- package/dist/store/celestea-home.d.ts +12 -0
- package/dist/store/celestea-home.js +11 -0
- package/dist/store/fs-json.d.ts +51 -0
- package/dist/store/fs-json.js +119 -0
- package/dist/store/grants-audit.d.ts +84 -0
- package/dist/store/grants-audit.js +109 -0
- package/dist/store/grants-service.d.ts +44 -0
- package/dist/store/grants-service.js +41 -0
- package/dist/store/grants-tokens.d.ts +62 -0
- package/dist/store/grants-tokens.js +105 -0
- package/dist/store/grants.d.ts +113 -0
- package/dist/store/grants.js +238 -0
- package/dist/store/index.d.ts +36 -0
- package/dist/store/index.js +36 -0
- package/dist/store/mode.d.ts +35 -0
- package/dist/store/mode.js +41 -0
- package/dist/store/permissions.d.ts +61 -0
- package/dist/store/permissions.js +139 -0
- package/dist/store/prompts-compose.d.ts +56 -0
- package/dist/store/prompts-compose.js +62 -0
- package/dist/store/prompts-template.d.ts +26 -0
- package/dist/store/prompts-template.js +99 -0
- package/dist/store/prompts.d.ts +142 -0
- package/dist/store/prompts.js +273 -0
- package/dist/store/provider-probe.d.ts +81 -0
- package/dist/store/provider-probe.js +154 -0
- package/dist/store/providers.d.ts +97 -0
- package/dist/store/providers.js +219 -0
- package/dist/store/result.d.ts +29 -0
- package/dist/store/result.js +30 -0
- package/dist/store/session-id.d.ts +119 -0
- package/dist/store/session-id.js +220 -0
- package/dist/store/session-meta.d.ts +41 -0
- package/dist/store/session-meta.js +66 -0
- package/dist/store/session-ops.d.ts +109 -0
- package/dist/store/session-ops.js +264 -0
- package/dist/store/session-tools.d.ts +42 -0
- package/dist/store/session-tools.js +69 -0
- package/dist/store/sessions.d.ts +162 -0
- package/dist/store/sessions.js +307 -0
- package/dist/store/validate.d.ts +13 -0
- package/dist/store/validate.js +38 -0
- package/dist/store/workspaces.d.ts +67 -0
- package/dist/store/workspaces.js +228 -0
- package/dist/user-questions.d.ts +58 -0
- package/dist/user-questions.js +157 -0
- package/dist/version.d.ts +14 -0
- package/dist/version.js +52 -0
- package/package.json +38 -0
- package/webdist/assets/index-Bngs7gUm.js +98 -0
- package/webdist/assets/index-Bu6ci_rN.css +1 -0
- package/webdist/assets/katex-CCbS2qAY.js +257 -0
- package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
- package/webdist/build-meta.json +7 -0
- package/webdist/index.html +261 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session listing / creation / transcript reader (`src/workspaces.rs:1092-1221,1459-1501`).
|
|
3
|
+
*
|
|
4
|
+
* A "session" is a directory holding `cli-main.jsonl`. The id is
|
|
5
|
+
* `<workspace-basename>/<dir>`; the `worker:<sid>` space is served by the engine
|
|
6
|
+
* (RuntimeAdapter) instead.
|
|
7
|
+
*
|
|
8
|
+
* W880: NEW sessions land in `<CELESTEA_HOME>/workspaces/<ws>/sessions/<dir>`
|
|
9
|
+
* (outside the workspace). `resolve()`/`list()` still read the two legacy
|
|
10
|
+
* layouts (slice-A `<ws>/.celestea/sessions/`, then `<ws>/`) with the canonical
|
|
11
|
+
* container winning. W791: the ARCHIVED sessions (canonical
|
|
12
|
+
* `<home>/.../archive/<dir>`, legacy `<ws>/.celestea-archived/<dir>`, see
|
|
13
|
+
* `session-ops.ts`) are a second source of the same row shape — `listArchived()`
|
|
14
|
+
* — because `list()` skips dot-directories by design.
|
|
15
|
+
*
|
|
16
|
+
* The transcript endpoint is a pure projection of the append-only log
|
|
17
|
+
* (`projectMessages` from `@celestea/session`), stopping at the first
|
|
18
|
+
* unparsable line so a torn tail is dropped rather than reported.
|
|
19
|
+
*/
|
|
20
|
+
import type { StudioMessage } from "@celestea/core";
|
|
21
|
+
import type { SessionWorkspace } from "@celestea/runtime";
|
|
22
|
+
import { type StoreResult } from "./result.js";
|
|
23
|
+
import { type SessionMode } from "./mode.js";
|
|
24
|
+
import { type SessionMeta } from "./session-meta.js";
|
|
25
|
+
import { type WorkspacesStore } from "./workspaces.js";
|
|
26
|
+
export interface ResolvedSession {
|
|
27
|
+
workspace: string;
|
|
28
|
+
session: string;
|
|
29
|
+
/** Canonical `<workspace>/<session>`. */
|
|
30
|
+
id: string;
|
|
31
|
+
wsPath: string;
|
|
32
|
+
dir: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* W768 — THE single resolution of a session's workspace.
|
|
36
|
+
*
|
|
37
|
+
* Everything that needs to know "which workspace is this session in" goes
|
|
38
|
+
* through here: the system prompt's `{{workspace}}` / `{{workspace_dir}}`
|
|
39
|
+
* variables AND the per-session sandbox cwd / path-guard root. They used to be
|
|
40
|
+
* two independent derivations (a store lookup for the prompt, a process-wide env
|
|
41
|
+
* knob for the tools), which is exactly how the prompt ended up naming one
|
|
42
|
+
* workspace while `pwd` reported another.
|
|
43
|
+
*
|
|
44
|
+
* Pure projection of the store's own record: no second lookup, no re-parsing of
|
|
45
|
+
* the session id, nothing that could disagree with `resolve()`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function sessionWorkspaceOf(resolved: ResolvedSession | null): SessionWorkspace | null;
|
|
48
|
+
/**
|
|
49
|
+
* W779 T2 — the session's DISPLAY name.
|
|
50
|
+
*
|
|
51
|
+
* `session.json.title` when the session declared one (the original, un-sanitized
|
|
52
|
+
* title), else the directory name WITHOUT its `-<secs>.<nanos>[-N]` creation
|
|
53
|
+
* suffix. ONE rule, used by `list()` and by `branch()`'s default title, so the
|
|
54
|
+
* GUI never has to know how a session directory is named.
|
|
55
|
+
*/
|
|
56
|
+
export declare function displayTitle(meta: SessionMeta | null, dirName: string): string;
|
|
57
|
+
export interface SessionRow {
|
|
58
|
+
id: string;
|
|
59
|
+
workspace: string;
|
|
60
|
+
/**
|
|
61
|
+
* W779 T2: the display name — `session.json.title`, else the directory name
|
|
62
|
+
* with its creation suffix stripped (never `main-1789192174.492000000`).
|
|
63
|
+
*/
|
|
64
|
+
title: string;
|
|
65
|
+
model: string | null;
|
|
66
|
+
/**
|
|
67
|
+
* W729: the session's working mode. A session without `session.json.mode`
|
|
68
|
+
* (and every worker row) reads as `standard` — the P0 default (K8/M3).
|
|
69
|
+
*/
|
|
70
|
+
mode: SessionMode;
|
|
71
|
+
size: number;
|
|
72
|
+
modified: number;
|
|
73
|
+
active: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* W791: present (and `true`) ONLY on rows produced by `listArchived()` — the
|
|
76
|
+
* default listing never carries the key, so the frozen response body of
|
|
77
|
+
* `GET /api/sessions` is unchanged for every pre-W791 client.
|
|
78
|
+
*/
|
|
79
|
+
archived?: true;
|
|
80
|
+
/**
|
|
81
|
+
* W513 row kind: `session` = a filesystem session directory, `worker` = an
|
|
82
|
+
* engine-memory worker conversation (`workspace: "engine"`).
|
|
83
|
+
*/
|
|
84
|
+
kind?: "session" | "worker";
|
|
85
|
+
/** W513: this session has an in-flight turn (its OWN slot, not the process's). */
|
|
86
|
+
busy?: boolean;
|
|
87
|
+
}
|
|
88
|
+
export interface SessionCreateRequest {
|
|
89
|
+
workspace?: string;
|
|
90
|
+
title: string;
|
|
91
|
+
model?: string;
|
|
92
|
+
prompt?: string;
|
|
93
|
+
/** W729: optional at creation; absent = `standard` and NOT written (K8). */
|
|
94
|
+
mode?: string;
|
|
95
|
+
}
|
|
96
|
+
export declare class SessionsStore {
|
|
97
|
+
private readonly ws;
|
|
98
|
+
private readonly now;
|
|
99
|
+
constructor(ws: WorkspacesStore, now?: () => number);
|
|
100
|
+
/**
|
|
101
|
+
* Every filesystem session plus the engine rows handed in by the caller.
|
|
102
|
+
*
|
|
103
|
+
* W877 (slice A): sessions are scanned in BOTH layouts — the new
|
|
104
|
+
* `<ws>/.celestea/sessions/` root first, then the legacy workspace root. A
|
|
105
|
+
* directory name present in both is emitted ONCE and the new-layout row wins.
|
|
106
|
+
* Dot-directories stay invisible in either root (that is what keeps
|
|
107
|
+
* `.celestea`, `.celestea-archived` and `.celestea-trash` out of the
|
|
108
|
+
* default listing).
|
|
109
|
+
*/
|
|
110
|
+
list(extra?: readonly SessionRow[]): SessionRow[];
|
|
111
|
+
/**
|
|
112
|
+
* W791 (B1): every ARCHIVED session of every registered workspace.
|
|
113
|
+
*
|
|
114
|
+
* Archiving MOVES a session directory to `<ws>/.celestea-archived/<name>` and
|
|
115
|
+
* keeps its id (`<wsName>/<session>`) — it is the same session, parked in a
|
|
116
|
+
* hidden sibling. `list()` deliberately skips dot-directories, so before this
|
|
117
|
+
* method the ONLY endpoint that could name an archived session was
|
|
118
|
+
* `unarchive`; the GUI's archive panel (`?archived=1`) and the
|
|
119
|
+
* delete-an-archived-session path both need to enumerate them.
|
|
120
|
+
*
|
|
121
|
+
* The rows have EXACTLY the shape `list()` produces — same id/workspace/title/
|
|
122
|
+
* model/mode/size/modified derivation, same sort — plus `archived: true` and
|
|
123
|
+
* `active: false` (an archived session can never be the active one: archiving
|
|
124
|
+
* refuses the active session).
|
|
125
|
+
*/
|
|
126
|
+
listArchived(): SessionRow[];
|
|
127
|
+
/** One row of the frozen shape (shared by `list` and `listArchived`). */
|
|
128
|
+
private rowOf;
|
|
129
|
+
/**
|
|
130
|
+
* Registry lookup + sanitization. The physical dir is probed in BOTH layouts
|
|
131
|
+
* (W877 slice A): the new `<ws>/.celestea/sessions/<session>` wins when it
|
|
132
|
+
* exists, the legacy `<ws>/<session>` is the fallback, and when NEITHER exists
|
|
133
|
+
* the new layout is returned so every write path stays consistent with
|
|
134
|
+
* `create()`. No registry entry is created here.
|
|
135
|
+
*/
|
|
136
|
+
resolve(id: string): StoreResult<ResolvedSession>;
|
|
137
|
+
/** A real live session dir: a directory holding the session log. */
|
|
138
|
+
private isSessionDir;
|
|
139
|
+
/** Resolve + require the session directory to hold a log file. */
|
|
140
|
+
require(id: string): StoreResult<ResolvedSession>;
|
|
141
|
+
private pickWorkspace;
|
|
142
|
+
/** POST /api/sessions — create a session dir; NEVER activates it. */
|
|
143
|
+
create(req: SessionCreateRequest): StoreResult<string>;
|
|
144
|
+
/**
|
|
145
|
+
* `<base>`, then `<base>-1`, `<base>-2`, … until the name is free, returned
|
|
146
|
+
* inside `writeRoot`.
|
|
147
|
+
*
|
|
148
|
+
* `writeRoot` defaults to the canonical container (`create()`), while
|
|
149
|
+
* `rename`/`branch` pass the parent of the session's CURRENT dir so a session
|
|
150
|
+
* is renamed inside the layer it already lives in.
|
|
151
|
+
*
|
|
152
|
+
* Collision detection spans EVERY live-session layer: a name already taken by
|
|
153
|
+
* a legacy session must not be minted again in the canonical container (and
|
|
154
|
+
* vice versa), because `list()` and `resolve()` would then have two different
|
|
155
|
+
* physical dirs for one id.
|
|
156
|
+
*/
|
|
157
|
+
uniqueDir(wsPath: string, base: string, writeRoot?: string): string;
|
|
158
|
+
/** Transcript projection: torn tail dropped, no pairing logic. */
|
|
159
|
+
messages(resolved: ResolvedSession): StudioMessage[];
|
|
160
|
+
/** POST /api/clear — truncate the log (no backup, no 409 guard). */
|
|
161
|
+
truncate(resolved: ResolvedSession): StoreResult<void>;
|
|
162
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session listing / creation / transcript reader (`src/workspaces.rs:1092-1221,1459-1501`).
|
|
3
|
+
*
|
|
4
|
+
* A "session" is a directory holding `cli-main.jsonl`. The id is
|
|
5
|
+
* `<workspace-basename>/<dir>`; the `worker:<sid>` space is served by the engine
|
|
6
|
+
* (RuntimeAdapter) instead.
|
|
7
|
+
*
|
|
8
|
+
* W880: NEW sessions land in `<CELESTEA_HOME>/workspaces/<ws>/sessions/<dir>`
|
|
9
|
+
* (outside the workspace). `resolve()`/`list()` still read the two legacy
|
|
10
|
+
* layouts (slice-A `<ws>/.celestea/sessions/`, then `<ws>/`) with the canonical
|
|
11
|
+
* container winning. W791: the ARCHIVED sessions (canonical
|
|
12
|
+
* `<home>/.../archive/<dir>`, legacy `<ws>/.celestea-archived/<dir>`, see
|
|
13
|
+
* `session-ops.ts`) are a second source of the same row shape — `listArchived()`
|
|
14
|
+
* — because `list()` skips dot-directories by design.
|
|
15
|
+
*
|
|
16
|
+
* The transcript endpoint is a pure projection of the append-only log
|
|
17
|
+
* (`projectMessages` from `@celestea/session`), stopping at the first
|
|
18
|
+
* unparsable line so a torn tail is dropped rather than reported.
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { parseSessionJsonl, projectMessages } from "@celestea/session";
|
|
22
|
+
import { isDirectory, isFile, listEntries, statOf, writeFileRaw, removeDir, ensureDir } from "./fs-json.js";
|
|
23
|
+
import { badRequest, errText, fail, notFound, ok } from "./result.js";
|
|
24
|
+
import { DEFAULT_SESSION_MODE, parseMode, validateMode } from "./mode.js";
|
|
25
|
+
import { readSessionMeta, writeSessionMeta } from "./session-meta.js";
|
|
26
|
+
import { archiveRoots, baseName, liveDirCandidates, sessionDirName, sanitizeComponent, sessionRoots, sessionsRoot, stripCreationSuffix, workspaceBasename } from "./session-id.js";
|
|
27
|
+
import { validateModelName, validatePromptId } from "./validate.js";
|
|
28
|
+
import { SESSION_FILE } from "./workspaces.js";
|
|
29
|
+
/**
|
|
30
|
+
* W768 — THE single resolution of a session's workspace.
|
|
31
|
+
*
|
|
32
|
+
* Everything that needs to know "which workspace is this session in" goes
|
|
33
|
+
* through here: the system prompt's `{{workspace}}` / `{{workspace_dir}}`
|
|
34
|
+
* variables AND the per-session sandbox cwd / path-guard root. They used to be
|
|
35
|
+
* two independent derivations (a store lookup for the prompt, a process-wide env
|
|
36
|
+
* knob for the tools), which is exactly how the prompt ended up naming one
|
|
37
|
+
* workspace while `pwd` reported another.
|
|
38
|
+
*
|
|
39
|
+
* Pure projection of the store's own record: no second lookup, no re-parsing of
|
|
40
|
+
* the session id, nothing that could disagree with `resolve()`.
|
|
41
|
+
*/
|
|
42
|
+
export function sessionWorkspaceOf(resolved) {
|
|
43
|
+
if (resolved === null)
|
|
44
|
+
return null;
|
|
45
|
+
return { name: resolved.workspace, path: resolved.wsPath };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* W779 T2 — the session's DISPLAY name.
|
|
49
|
+
*
|
|
50
|
+
* `session.json.title` when the session declared one (the original, un-sanitized
|
|
51
|
+
* title), else the directory name WITHOUT its `-<secs>.<nanos>[-N]` creation
|
|
52
|
+
* suffix. ONE rule, used by `list()` and by `branch()`'s default title, so the
|
|
53
|
+
* GUI never has to know how a session directory is named.
|
|
54
|
+
*/
|
|
55
|
+
export function displayTitle(meta, dirName) {
|
|
56
|
+
const declared = meta?.title ?? "";
|
|
57
|
+
return declared === "" ? stripCreationSuffix(dirName) : declared;
|
|
58
|
+
}
|
|
59
|
+
export class SessionsStore {
|
|
60
|
+
ws;
|
|
61
|
+
now;
|
|
62
|
+
constructor(ws, now = Date.now) {
|
|
63
|
+
this.ws = ws;
|
|
64
|
+
this.now = now;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Every filesystem session plus the engine rows handed in by the caller.
|
|
68
|
+
*
|
|
69
|
+
* W877 (slice A): sessions are scanned in BOTH layouts — the new
|
|
70
|
+
* `<ws>/.celestea/sessions/` root first, then the legacy workspace root. A
|
|
71
|
+
* directory name present in both is emitted ONCE and the new-layout row wins.
|
|
72
|
+
* Dot-directories stay invisible in either root (that is what keeps
|
|
73
|
+
* `.celestea`, `.celestea-archived` and `.celestea-trash` out of the
|
|
74
|
+
* default listing).
|
|
75
|
+
*/
|
|
76
|
+
list(extra = []) {
|
|
77
|
+
const active = this.ws.activeSession();
|
|
78
|
+
const rows = [];
|
|
79
|
+
for (const w of this.ws.registry().workspaces) {
|
|
80
|
+
const name = workspaceBasename(w.path) ?? w.path;
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
// Canonical container FIRST, then the slice-A transitional root, then the
|
|
83
|
+
// oldest workspace root. Only a REAL session (a dir holding the log)
|
|
84
|
+
// shadows a lower layer: a stray empty dir must not hide a live session.
|
|
85
|
+
for (const root of sessionRoots(w.path)) {
|
|
86
|
+
for (const e of listEntries(root)) {
|
|
87
|
+
if (!e.isDir || e.name.startsWith(".") || seen.has(e.name))
|
|
88
|
+
continue;
|
|
89
|
+
const log = `${root}/${e.name}/${SESSION_FILE}`;
|
|
90
|
+
if (!isFile(log))
|
|
91
|
+
continue;
|
|
92
|
+
seen.add(e.name);
|
|
93
|
+
const row = this.rowOf(name, e.name, readSessionMeta(`${root}/${e.name}`), statOf(log));
|
|
94
|
+
rows.push({ ...row, active: active === row.id });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
rows.push(...extra);
|
|
99
|
+
rows.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
100
|
+
return rows;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* W791 (B1): every ARCHIVED session of every registered workspace.
|
|
104
|
+
*
|
|
105
|
+
* Archiving MOVES a session directory to `<ws>/.celestea-archived/<name>` and
|
|
106
|
+
* keeps its id (`<wsName>/<session>`) — it is the same session, parked in a
|
|
107
|
+
* hidden sibling. `list()` deliberately skips dot-directories, so before this
|
|
108
|
+
* method the ONLY endpoint that could name an archived session was
|
|
109
|
+
* `unarchive`; the GUI's archive panel (`?archived=1`) and the
|
|
110
|
+
* delete-an-archived-session path both need to enumerate them.
|
|
111
|
+
*
|
|
112
|
+
* The rows have EXACTLY the shape `list()` produces — same id/workspace/title/
|
|
113
|
+
* model/mode/size/modified derivation, same sort — plus `archived: true` and
|
|
114
|
+
* `active: false` (an archived session can never be the active one: archiving
|
|
115
|
+
* refuses the active session).
|
|
116
|
+
*/
|
|
117
|
+
listArchived() {
|
|
118
|
+
const rows = [];
|
|
119
|
+
for (const w of this.ws.registry().workspaces) {
|
|
120
|
+
const name = workspaceBasename(w.path) ?? w.path;
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
for (const root of archiveRoots(w.path)) {
|
|
123
|
+
for (const e of listEntries(root)) {
|
|
124
|
+
if (!e.isDir || e.name.startsWith(".") || seen.has(e.name))
|
|
125
|
+
continue;
|
|
126
|
+
const log = `${root}/${e.name}/${SESSION_FILE}`;
|
|
127
|
+
if (!isFile(log))
|
|
128
|
+
continue;
|
|
129
|
+
seen.add(e.name);
|
|
130
|
+
rows.push({ ...this.rowOf(name, e.name, readSessionMeta(`${root}/${e.name}`), statOf(log)), archived: true });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
rows.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
135
|
+
return rows;
|
|
136
|
+
}
|
|
137
|
+
/** One row of the frozen shape (shared by `list` and `listArchived`). */
|
|
138
|
+
rowOf(workspace, dirName, meta, st) {
|
|
139
|
+
return {
|
|
140
|
+
id: `${workspace}/${dirName}`,
|
|
141
|
+
workspace,
|
|
142
|
+
kind: "session",
|
|
143
|
+
title: displayTitle(meta, dirName),
|
|
144
|
+
model: meta?.model ?? null,
|
|
145
|
+
mode: meta?.mode ?? DEFAULT_SESSION_MODE,
|
|
146
|
+
size: st?.size ?? 0,
|
|
147
|
+
modified: st?.modified ?? 0,
|
|
148
|
+
active: false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Registry lookup + sanitization. The physical dir is probed in BOTH layouts
|
|
153
|
+
* (W877 slice A): the new `<ws>/.celestea/sessions/<session>` wins when it
|
|
154
|
+
* exists, the legacy `<ws>/<session>` is the fallback, and when NEITHER exists
|
|
155
|
+
* the new layout is returned so every write path stays consistent with
|
|
156
|
+
* `create()`. No registry entry is created here.
|
|
157
|
+
*/
|
|
158
|
+
resolve(id) {
|
|
159
|
+
const slash = id.trim().indexOf("/");
|
|
160
|
+
if (slash <= 0)
|
|
161
|
+
return badRequest(`invalid session id '${id}': expected '<workspace>/<session>'`);
|
|
162
|
+
const wsName = id.trim().slice(0, slash);
|
|
163
|
+
const wsPath = this.ws.workspacePath(wsName);
|
|
164
|
+
if (wsPath === undefined)
|
|
165
|
+
return notFound(`unknown workspace '${wsName}'`);
|
|
166
|
+
const session = sanitizeComponent(id.trim().slice(slash + 1));
|
|
167
|
+
if (session === "" || session === "." || session === ".." || session.startsWith(".")) {
|
|
168
|
+
return badRequest(`invalid session id '${id}'`);
|
|
169
|
+
}
|
|
170
|
+
// W880: the canonical candidate lives under CELESTEA_HOME, OUTSIDE the
|
|
171
|
+
// workspace, so the old `startsWith(wsPath/)` guard no longer applies. The
|
|
172
|
+
// session segment is already sanitized (no separators, never hidden) and
|
|
173
|
+
// every candidate is derived from the registered wsPath, so the result
|
|
174
|
+
// cannot escape either root.
|
|
175
|
+
const candidates = liveDirCandidates(wsPath, session);
|
|
176
|
+
const dir = candidates.find((c) => this.isSessionDir(c)) ?? candidates[0];
|
|
177
|
+
return ok({ workspace: wsName, session, id: `${wsName}/${session}`, wsPath, dir });
|
|
178
|
+
}
|
|
179
|
+
/** A real live session dir: a directory holding the session log. */
|
|
180
|
+
isSessionDir(dir) {
|
|
181
|
+
return isDirectory(dir) && isFile(`${dir}/${SESSION_FILE}`);
|
|
182
|
+
}
|
|
183
|
+
/** Resolve + require the session directory to hold a log file. */
|
|
184
|
+
require(id) {
|
|
185
|
+
const res = this.resolve(id);
|
|
186
|
+
if (!res.ok)
|
|
187
|
+
return res;
|
|
188
|
+
if (!isDirectory(res.value.dir) || !isFile(`${res.value.dir}/${SESSION_FILE}`)) {
|
|
189
|
+
return notFound(`unknown session '${id}'`);
|
|
190
|
+
}
|
|
191
|
+
return res;
|
|
192
|
+
}
|
|
193
|
+
pickWorkspace(requested) {
|
|
194
|
+
const asked = (requested ?? "").trim();
|
|
195
|
+
if (asked !== "")
|
|
196
|
+
return asked;
|
|
197
|
+
const active = this.ws.activeSession();
|
|
198
|
+
if (active !== null && active.includes("/"))
|
|
199
|
+
return active.slice(0, active.indexOf("/"));
|
|
200
|
+
const first = this.ws.registry().workspaces[0];
|
|
201
|
+
return first === undefined ? null : workspaceBasename(first.path);
|
|
202
|
+
}
|
|
203
|
+
/** POST /api/sessions — create a session dir; NEVER activates it. */
|
|
204
|
+
create(req) {
|
|
205
|
+
const wsName = this.pickWorkspace(req.workspace);
|
|
206
|
+
if (wsName === null)
|
|
207
|
+
return notFound(`unknown workspace '${(req.workspace ?? "").trim()}'`);
|
|
208
|
+
const wsPath = this.ws.workspacePath(wsName);
|
|
209
|
+
if (wsPath === undefined)
|
|
210
|
+
return notFound(`unknown workspace '${wsName}'`);
|
|
211
|
+
if (!isDirectory(wsPath))
|
|
212
|
+
return notFound(`workspace path '${wsPath}' is not accessible`);
|
|
213
|
+
if (req.title.trim() === "")
|
|
214
|
+
return badRequest("title must not be empty");
|
|
215
|
+
const base = sanitizeComponent(req.title);
|
|
216
|
+
if (base.startsWith(".") || base === "") {
|
|
217
|
+
return badRequest(`title '${req.title}' sanitizes to the hidden name '${base}'`);
|
|
218
|
+
}
|
|
219
|
+
const model = req.model ?? "";
|
|
220
|
+
if (model !== "") {
|
|
221
|
+
const bad = validateModelName(model);
|
|
222
|
+
if (bad !== null)
|
|
223
|
+
return badRequest(`invalid model: ${bad}`);
|
|
224
|
+
}
|
|
225
|
+
const prompt = req.prompt ?? "";
|
|
226
|
+
if (prompt !== "") {
|
|
227
|
+
const bad = validatePromptId(prompt);
|
|
228
|
+
if (bad !== null)
|
|
229
|
+
return badRequest(`invalid prompt: ${bad}`);
|
|
230
|
+
}
|
|
231
|
+
const mode = req.mode ?? "";
|
|
232
|
+
if (mode !== "") {
|
|
233
|
+
const bad = validateMode(mode);
|
|
234
|
+
if (bad !== null)
|
|
235
|
+
return badRequest(bad);
|
|
236
|
+
}
|
|
237
|
+
// W880: NEW sessions land in <CELESTEA_HOME>/workspaces/<ws>/sessions/
|
|
238
|
+
// (ensureDir below creates the container recursively); the id stays
|
|
239
|
+
// <wsName>/<dirName>. Nothing is created under the workspace root.
|
|
240
|
+
const dir = this.uniqueDir(wsPath, sessionDirName(req.title, this.now()));
|
|
241
|
+
try {
|
|
242
|
+
ensureDir(dir);
|
|
243
|
+
writeFileRaw(`${dir}/${SESSION_FILE}`, "");
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
return fail(500, `create failed: ${errText(e)}`);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
// W779 T2: the ORIGINAL title (trimmed, CJK/spaces preserved) is what the
|
|
250
|
+
// GUI shows; the directory name stays the sanitized+timestamped form.
|
|
251
|
+
writeSessionMeta(dir, {
|
|
252
|
+
title: req.title.trim(),
|
|
253
|
+
model,
|
|
254
|
+
prompt,
|
|
255
|
+
...(mode === "" ? {} : { mode: parseMode(mode) ?? DEFAULT_SESSION_MODE }),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
removeDir(dir);
|
|
260
|
+
return fail(500, `meta write failed: ${errText(e)}`);
|
|
261
|
+
}
|
|
262
|
+
// W885: the directory's own basename, per the platform (a win32 realpath
|
|
263
|
+
// returns backslashes, so `lastIndexOf("/")` produced the whole path —
|
|
264
|
+
// W883 E2).
|
|
265
|
+
return ok(`${wsName}/${baseName(dir)}`);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* `<base>`, then `<base>-1`, `<base>-2`, … until the name is free, returned
|
|
269
|
+
* inside `writeRoot`.
|
|
270
|
+
*
|
|
271
|
+
* `writeRoot` defaults to the canonical container (`create()`), while
|
|
272
|
+
* `rename`/`branch` pass the parent of the session's CURRENT dir so a session
|
|
273
|
+
* is renamed inside the layer it already lives in.
|
|
274
|
+
*
|
|
275
|
+
* Collision detection spans EVERY live-session layer: a name already taken by
|
|
276
|
+
* a legacy session must not be minted again in the canonical container (and
|
|
277
|
+
* vice versa), because `list()` and `resolve()` would then have two different
|
|
278
|
+
* physical dirs for one id.
|
|
279
|
+
*/
|
|
280
|
+
uniqueDir(wsPath, base, writeRoot = sessionsRoot(wsPath)) {
|
|
281
|
+
const roots = [writeRoot, ...sessionRoots(wsPath)];
|
|
282
|
+
let name = base;
|
|
283
|
+
let n = 1;
|
|
284
|
+
while (roots.some((root) => isFile(`${root}/${name}/${SESSION_FILE}`) || isDirectory(`${root}/${name}`))) {
|
|
285
|
+
name = `${base}-${n}`;
|
|
286
|
+
n += 1;
|
|
287
|
+
if (n > 1000)
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
return `${writeRoot}/${name}`;
|
|
291
|
+
}
|
|
292
|
+
/** Transcript projection: torn tail dropped, no pairing logic. */
|
|
293
|
+
messages(resolved) {
|
|
294
|
+
const text = readFileSync(`${resolved.dir}/${SESSION_FILE}`, "utf8");
|
|
295
|
+
return projectMessages(parseSessionJsonl(text).events);
|
|
296
|
+
}
|
|
297
|
+
/** POST /api/clear — truncate the log (no backup, no 409 guard). */
|
|
298
|
+
truncate(resolved) {
|
|
299
|
+
try {
|
|
300
|
+
writeFileRaw(`${resolved.dir}/${SESSION_FILE}`, "");
|
|
301
|
+
return ok(undefined);
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
return fail(500, errText(e));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side field validation shared by several endpoints.
|
|
3
|
+
*
|
|
4
|
+
* The host validates before it hands anything to the engine, and the
|
|
5
|
+
* error strings are contract text (`contracts/endpoints.json` errors[]), so
|
|
6
|
+
* they live here verbatim instead of being re-typed per handler.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MODEL_MAX_LEN = 128;
|
|
9
|
+
export declare function validateModelName(model: string): string | null;
|
|
10
|
+
export declare function validatePromptId(id: string): string | null;
|
|
11
|
+
export declare function isHttpUrl(url: string): boolean;
|
|
12
|
+
/** `parse_effort`: "" / "off" (case-insensitive) clears the effort. */
|
|
13
|
+
export declare function parseEffort(raw: string | null | undefined): string | null | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side field validation shared by several endpoints.
|
|
3
|
+
*
|
|
4
|
+
* The host validates before it hands anything to the engine, and the
|
|
5
|
+
* error strings are contract text (`contracts/endpoints.json` errors[]), so
|
|
6
|
+
* they live here verbatim instead of being re-typed per handler.
|
|
7
|
+
*/
|
|
8
|
+
/** Model ids are deliberately lax: `[A-Za-z0-9._-:/@]`, at most 128 chars. */
|
|
9
|
+
const MODEL_CHARS = /^[A-Za-z0-9._\-:/@]*$/;
|
|
10
|
+
export const MODEL_MAX_LEN = 128;
|
|
11
|
+
export function validateModelName(model) {
|
|
12
|
+
if (model.length > MODEL_MAX_LEN)
|
|
13
|
+
return `invalid model name: '${model}' exceeds ${MODEL_MAX_LEN} characters`;
|
|
14
|
+
for (const ch of model) {
|
|
15
|
+
if (!MODEL_CHARS.test(ch)) {
|
|
16
|
+
return `invalid model name '${model}': character ${JSON.stringify(ch)} is not allowed (only [A-Za-z0-9._-:/@]; no spaces, brackets or control characters)`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const PROMPT_ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
22
|
+
export function validatePromptId(id) {
|
|
23
|
+
return PROMPT_ID.test(id) ? null : "prompt id must be 1-128 chars of [A-Za-z0-9._-]";
|
|
24
|
+
}
|
|
25
|
+
export function isHttpUrl(url) {
|
|
26
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
27
|
+
}
|
|
28
|
+
/** `parse_effort`: "" / "off" (case-insensitive) clears the effort. */
|
|
29
|
+
export function parseEffort(raw) {
|
|
30
|
+
if (raw === null)
|
|
31
|
+
return null;
|
|
32
|
+
if (raw === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const v = raw.trim();
|
|
35
|
+
if (v === "" || v.toLowerCase() === "off")
|
|
36
|
+
return null;
|
|
37
|
+
return v;
|
|
38
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `workspaces.json` (v2) registry store — `contracts/data-files/workspaces.schema.json`,
|
|
3
|
+
* `src/workspaces.rs:122-140,794-970`.
|
|
4
|
+
*
|
|
5
|
+
* Frozen format facts honored here:
|
|
6
|
+
* - NO version field; the workspace KEY is the registered path's folder
|
|
7
|
+
* basename and is never stored;
|
|
8
|
+
* - v1 `{"name":…}` rows are tolerated and ignored; `active_session` is
|
|
9
|
+
* rewritten to the basename on load (idempotent);
|
|
10
|
+
* - unknown fields are tolerated, a malformed file is a HARD error (the
|
|
11
|
+
* unreadable registry is never overwritten with an empty one);
|
|
12
|
+
* - write = pretty JSON -> `<file>.json.tmp` -> rename (atomic, no fsync).
|
|
13
|
+
*/
|
|
14
|
+
import { type StoreResult } from "./result.js";
|
|
15
|
+
export declare const SESSION_FILE = "cli-main.jsonl";
|
|
16
|
+
export interface RegistryWorkspace {
|
|
17
|
+
path: string;
|
|
18
|
+
}
|
|
19
|
+
export interface RegistryData {
|
|
20
|
+
workspaces: RegistryWorkspace[];
|
|
21
|
+
active_session: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface WorkspaceRow {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
/** LIVE session dirs only (archived / trashed dirs are not counted). */
|
|
27
|
+
sessions: number;
|
|
28
|
+
}
|
|
29
|
+
export interface WorkspacesView {
|
|
30
|
+
workspaces: WorkspaceRow[];
|
|
31
|
+
active_session: string | null;
|
|
32
|
+
}
|
|
33
|
+
export declare class WorkspacesStore {
|
|
34
|
+
private readonly file;
|
|
35
|
+
private data;
|
|
36
|
+
constructor(file: string);
|
|
37
|
+
/** Snapshot of the registry (callers must not mutate it). */
|
|
38
|
+
registry(): RegistryData;
|
|
39
|
+
activeSession(): string | null;
|
|
40
|
+
workspacePath(name: string): string | undefined;
|
|
41
|
+
private persist;
|
|
42
|
+
/** Persist `active_session`; the caller owns the 500 wording. */
|
|
43
|
+
setActiveSession(id: string | null): StoreResult<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Count LIVE session dirs of a workspace across EVERY layer (W880): the
|
|
46
|
+
* canonical `<CELESTEA_HOME>/.../sessions`, the slice-A
|
|
47
|
+
* `<ws>/.celestea/sessions` and the oldest workspace root. A name present in
|
|
48
|
+
* more than one layer is counted ONCE (the canonical copy wins), matching
|
|
49
|
+
* `SessionsStore.list()`; missing a layer is how the GUI's workspace session
|
|
50
|
+
* count silently drops to 0.
|
|
51
|
+
*/
|
|
52
|
+
countSessions(path: string): number;
|
|
53
|
+
view(): WorkspacesView;
|
|
54
|
+
/** POST /api/workspaces — register only; the folder is never touched. */
|
|
55
|
+
register(rawPath: string): StoreResult<string>;
|
|
56
|
+
/** POST /api/workspaces/{name}/delete — deregister only. */
|
|
57
|
+
deregister(name: string): StoreResult<void>;
|
|
58
|
+
batchDelete(names: readonly string[]): {
|
|
59
|
+
deleted: number;
|
|
60
|
+
failed: Array<{
|
|
61
|
+
name: string;
|
|
62
|
+
error: string;
|
|
63
|
+
}>;
|
|
64
|
+
};
|
|
65
|
+
/** POST /api/workspaces/{name}/rename — really renames the FOLDER. */
|
|
66
|
+
renameWorkspace(name: string, newName: string): StoreResult<void>;
|
|
67
|
+
}
|