@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,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<session-dir>/session.json` — the optional Studio session metadata
|
|
3
|
+
* (`contracts/data-files/session.schema.json`).
|
|
4
|
+
*
|
|
5
|
+
* The engine NEVER reads this file; only `POST /api/sessions` writes it (and
|
|
6
|
+
* only when `title` and/or `model` and/or `prompt` and/or `mode` is non-empty),
|
|
7
|
+
* `rename`/`branch` re-write it (see W779 below), and `activate`/`compact` honor
|
|
8
|
+
* its `model` override. Missing or corrupt files are tolerated (`None`), never
|
|
9
|
+
* repaired.
|
|
10
|
+
*
|
|
11
|
+
* W779 T2: `title` joins them — the ORIGINAL, un-sanitized session title (CJK,
|
|
12
|
+
* spaces and all), so the GUI can show `我的 会话` instead of the directory name
|
|
13
|
+
* `我的_会话-1700000000.0`. `POST /api/sessions` always has a title, so every
|
|
14
|
+
* session created from now on carries one; the field is optional on read, and a
|
|
15
|
+
* session without it falls back to the de-suffixed directory name.
|
|
16
|
+
*
|
|
17
|
+
* W729 (P0): `mode` joins `model`/`prompt` as a creation-time session property.
|
|
18
|
+
* K8: the default mode is *not* written — the KEY never appears for a session
|
|
19
|
+
* that did not ask for one. W779 T2 adds `title`, so the file itself now always
|
|
20
|
+
* exists; K8's guarantee is per-key (no `mode` key), not per-file.
|
|
21
|
+
*/
|
|
22
|
+
import { parseMode } from "./mode.js";
|
|
23
|
+
import { readJsonIfExists, writeTextPlain } from "./fs-json.js";
|
|
24
|
+
export const SESSION_META = "session.json";
|
|
25
|
+
/**
|
|
26
|
+
* Read the metadata; a corrupt file behaves exactly like a missing one, and a
|
|
27
|
+
* `mode` that is not a declared literal is dropped (never repaired on disk).
|
|
28
|
+
*/
|
|
29
|
+
export function readSessionMeta(dir) {
|
|
30
|
+
const out = readJsonIfExists(`${dir}/${SESSION_META}`);
|
|
31
|
+
if (!out.exists || out.error !== undefined)
|
|
32
|
+
return null;
|
|
33
|
+
if (typeof out.value !== "object" || out.value === null || Array.isArray(out.value))
|
|
34
|
+
return null;
|
|
35
|
+
const rec = out.value;
|
|
36
|
+
const meta = {};
|
|
37
|
+
if (typeof rec["title"] === "string")
|
|
38
|
+
meta.title = rec["title"];
|
|
39
|
+
if (typeof rec["model"] === "string")
|
|
40
|
+
meta.model = rec["model"];
|
|
41
|
+
if (typeof rec["prompt"] === "string")
|
|
42
|
+
meta.prompt = rec["prompt"];
|
|
43
|
+
const mode = parseMode(rec["mode"]);
|
|
44
|
+
if (mode !== null)
|
|
45
|
+
meta.mode = mode;
|
|
46
|
+
return meta;
|
|
47
|
+
}
|
|
48
|
+
/** Any of `title`/`model`/`prompt`/`mode` non-empty -> write; all empty -> no file. */
|
|
49
|
+
export function writeSessionMeta(dir, meta) {
|
|
50
|
+
const title = meta.title ?? "";
|
|
51
|
+
const model = meta.model ?? "";
|
|
52
|
+
const prompt = meta.prompt ?? "";
|
|
53
|
+
const mode = meta.mode ?? "";
|
|
54
|
+
if (title === "" && model === "" && prompt === "" && mode === "")
|
|
55
|
+
return;
|
|
56
|
+
const body = {};
|
|
57
|
+
if (title !== "")
|
|
58
|
+
body["title"] = title;
|
|
59
|
+
if (model !== "")
|
|
60
|
+
body["model"] = model;
|
|
61
|
+
if (prompt !== "")
|
|
62
|
+
body["prompt"] = prompt;
|
|
63
|
+
if (mode !== "")
|
|
64
|
+
body["mode"] = mode;
|
|
65
|
+
writeTextPlain(`${dir}/${SESSION_META}`, `${JSON.stringify(body, null, 2)}\n`);
|
|
66
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session directory moves: rename / branch / archive / trash
|
|
3
|
+
* (`src/workspaces.rs:1228-1311,1506-1650`).
|
|
4
|
+
*
|
|
5
|
+
* All four are plain filesystem moves: archiving is the canonical
|
|
6
|
+
* `<CELESTEA_HOME>/workspaces/<ws>/archive/<name>` (W880; id-preserving and
|
|
7
|
+
* reversible) while deleting moves to `<CELESTEA_HOME>/workspaces/<ws>/trash/<name>-<ts>`
|
|
8
|
+
* (NOT addressable by id afterwards). The legacy `<ws>/.celestea-archived` /
|
|
9
|
+
* `<ws>/.celestea-trash` siblings stay READABLE. The archive state needs no
|
|
10
|
+
* field in `workspaces.json` because the default scanner never lists it.
|
|
11
|
+
*
|
|
12
|
+
* W791: "invisible to the scanner" is about the DEFAULT listing only. An
|
|
13
|
+
* archived session keeps its id, so the three operations that address a session
|
|
14
|
+
* BY ID must agree on where it can be: `unarchive` always did, `trash` now looks
|
|
15
|
+
* in the archive too (B3), and the scanner answers the `?archived=1` listing
|
|
16
|
+
* (`SessionsStore.listArchived`, B1).
|
|
17
|
+
*
|
|
18
|
+
* W794 (裁决: "active 只是状态标记,不是保护理由"): the ACTIVE session is no
|
|
19
|
+
* longer refused by `archive` / `trash` — both move it like any other session.
|
|
20
|
+
* The active MARKER is a view preference, not a lock, so the operation owns the
|
|
21
|
+
* consequence: once the active session has moved away, `active_session` is set
|
|
22
|
+
* to `null` and persisted (it must never keep pointing at an id this call just
|
|
23
|
+
* removed). Cutting the session's in-flight model response and releasing its
|
|
24
|
+
* engine instance is the engine's half of the same removal and happens BEFORE
|
|
25
|
+
* these functions run (see `handlers/session-move.ts`).
|
|
26
|
+
*/
|
|
27
|
+
import { type StoreResult } from "./result.js";
|
|
28
|
+
import { type WorkspacesStore } from "./workspaces.js";
|
|
29
|
+
import { type SessionsStore } from "./sessions.js";
|
|
30
|
+
export interface BatchOutcome {
|
|
31
|
+
count: number;
|
|
32
|
+
failed: Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
error: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export declare class SessionOps {
|
|
38
|
+
private readonly ws;
|
|
39
|
+
private readonly sessions;
|
|
40
|
+
private readonly now;
|
|
41
|
+
constructor(ws: WorkspacesStore, sessions: SessionsStore, now?: () => number);
|
|
42
|
+
/** POST /api/sessions/{id}/rename — same name is a no-op success. */
|
|
43
|
+
rename(id: string, newTitle: string): StoreResult<string>;
|
|
44
|
+
/**
|
|
45
|
+
* W779 T2: `session.json.title` <- [title], keeping every other key. A session
|
|
46
|
+
* that never had a meta file gets a `{title}` one (the title belongs to the
|
|
47
|
+
* session, not to the model/prompt/mode trio).
|
|
48
|
+
*/
|
|
49
|
+
private writeTitle;
|
|
50
|
+
/** POST /api/sessions/{id}/branch — copy the log into a sibling dir. */
|
|
51
|
+
branch(id: string, title?: string): StoreResult<string>;
|
|
52
|
+
private move;
|
|
53
|
+
/**
|
|
54
|
+
* POST /api/sessions/{id}/archive — id-preserving move into the archive.
|
|
55
|
+
*
|
|
56
|
+
* W794: the active session is archived like any other; the active MARKER is
|
|
57
|
+
* cleared afterwards (the archived row is not part of the default listing, so a
|
|
58
|
+
* marker still pointing at it would contradict `GET /api/sessions`).
|
|
59
|
+
*/
|
|
60
|
+
archive(id: string): StoreResult<void>;
|
|
61
|
+
unarchive(id: string): StoreResult<void>;
|
|
62
|
+
/** POST /api/sessions/batch-archive — per-id, always 200. */
|
|
63
|
+
batchArchive(ids: readonly string[]): {
|
|
64
|
+
archived: number;
|
|
65
|
+
failed: Array<{
|
|
66
|
+
id: string;
|
|
67
|
+
error: string;
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
70
|
+
/** POST /api/sessions/batch-delete — per-id move to `.celestea-trash`. */
|
|
71
|
+
batchDelete(ids: readonly string[]): {
|
|
72
|
+
deleted: number;
|
|
73
|
+
failed: Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
error: string;
|
|
76
|
+
}>;
|
|
77
|
+
};
|
|
78
|
+
private batch;
|
|
79
|
+
/**
|
|
80
|
+
* The session's directory wherever it currently IS: the live location first,
|
|
81
|
+
* else the archive (W791 B3).
|
|
82
|
+
*
|
|
83
|
+
* Deleting an ARCHIVED session used to answer `unknown session '<id>'`: the
|
|
84
|
+
* operation only ever looked at `<ws>/<session>`, which is exactly where an
|
|
85
|
+
* archived session is NOT. The id is unchanged by archiving, so the archive is
|
|
86
|
+
* the only other place it can be — and both a live and an archived copy is
|
|
87
|
+
* impossible (`unarchive` refuses to overwrite a live session).
|
|
88
|
+
*/
|
|
89
|
+
private locate;
|
|
90
|
+
/**
|
|
91
|
+
* One session into `<ws>/.celestea-trash/<session>-<ts>`.
|
|
92
|
+
*
|
|
93
|
+
* W794: no active-session guard. Deleting the focused session is a normal
|
|
94
|
+
* operation — the marker is cleared instead, in the same atomic write the
|
|
95
|
+
* registry uses for every other `active_session` change.
|
|
96
|
+
*/
|
|
97
|
+
private trash;
|
|
98
|
+
/**
|
|
99
|
+
* W794: `active_session` must not survive the session it names.
|
|
100
|
+
*
|
|
101
|
+
* Called only AFTER the directory has actually moved (a failure to move leaves
|
|
102
|
+
* the registry untouched — the session is still there and still active). The
|
|
103
|
+
* value becomes `null`: no other session is silently promoted, so the UI shows
|
|
104
|
+
* what is true (nothing is focused) instead of a session the user never chose.
|
|
105
|
+
* A failed persist is a 500 — the caller's `failed[]` / error then says the
|
|
106
|
+
* registry could not be updated, rather than pretending the id is gone.
|
|
107
|
+
*/
|
|
108
|
+
private clearActiveIf;
|
|
109
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session directory moves: rename / branch / archive / trash
|
|
3
|
+
* (`src/workspaces.rs:1228-1311,1506-1650`).
|
|
4
|
+
*
|
|
5
|
+
* All four are plain filesystem moves: archiving is the canonical
|
|
6
|
+
* `<CELESTEA_HOME>/workspaces/<ws>/archive/<name>` (W880; id-preserving and
|
|
7
|
+
* reversible) while deleting moves to `<CELESTEA_HOME>/workspaces/<ws>/trash/<name>-<ts>`
|
|
8
|
+
* (NOT addressable by id afterwards). The legacy `<ws>/.celestea-archived` /
|
|
9
|
+
* `<ws>/.celestea-trash` siblings stay READABLE. The archive state needs no
|
|
10
|
+
* field in `workspaces.json` because the default scanner never lists it.
|
|
11
|
+
*
|
|
12
|
+
* W791: "invisible to the scanner" is about the DEFAULT listing only. An
|
|
13
|
+
* archived session keeps its id, so the three operations that address a session
|
|
14
|
+
* BY ID must agree on where it can be: `unarchive` always did, `trash` now looks
|
|
15
|
+
* in the archive too (B3), and the scanner answers the `?archived=1` listing
|
|
16
|
+
* (`SessionsStore.listArchived`, B1).
|
|
17
|
+
*
|
|
18
|
+
* W794 (裁决: "active 只是状态标记,不是保护理由"): the ACTIVE session is no
|
|
19
|
+
* longer refused by `archive` / `trash` — both move it like any other session.
|
|
20
|
+
* The active MARKER is a view preference, not a lock, so the operation owns the
|
|
21
|
+
* consequence: once the active session has moved away, `active_session` is set
|
|
22
|
+
* to `null` and persisted (it must never keep pointing at an id this call just
|
|
23
|
+
* removed). Cutting the session's in-flight model response and releasing its
|
|
24
|
+
* engine instance is the engine's half of the same removal and happens BEFORE
|
|
25
|
+
* these functions run (see `handlers/session-move.ts`).
|
|
26
|
+
*/
|
|
27
|
+
import { copyFileSync, renameSync, mkdirSync, existsSync } from "node:fs";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { isDirectory, isFile, removeDir } from "./fs-json.js";
|
|
30
|
+
import { badRequest, conflict, errText, fail, notFound, ok } from "./result.js";
|
|
31
|
+
import { archiveDirCandidates, archiveRoots, baseName, parentDir, sanitizeComponent, timestampSuffix, trashRoots } from "./session-id.js";
|
|
32
|
+
import { readSessionMeta, writeSessionMeta } from "./session-meta.js";
|
|
33
|
+
import { SESSION_FILE } from "./workspaces.js";
|
|
34
|
+
import { displayTitle } from "./sessions.js";
|
|
35
|
+
/** W885: platform-aware parent (the pre-W885 hardcoded `/` broke on win32). */
|
|
36
|
+
function parentOf(path) {
|
|
37
|
+
return parentDir(path);
|
|
38
|
+
}
|
|
39
|
+
/** W885: platform-aware basename (`dir.slice(lastIndexOf("/") + 1)` before). */
|
|
40
|
+
function baseOf(path) {
|
|
41
|
+
return baseName(path);
|
|
42
|
+
}
|
|
43
|
+
export class SessionOps {
|
|
44
|
+
ws;
|
|
45
|
+
sessions;
|
|
46
|
+
now;
|
|
47
|
+
constructor(ws, sessions, now = Date.now) {
|
|
48
|
+
this.ws = ws;
|
|
49
|
+
this.sessions = sessions;
|
|
50
|
+
this.now = now;
|
|
51
|
+
}
|
|
52
|
+
/** POST /api/sessions/{id}/rename — same name is a no-op success. */
|
|
53
|
+
rename(id, newTitle) {
|
|
54
|
+
const found = this.sessions.require(id);
|
|
55
|
+
if (!found.ok)
|
|
56
|
+
return found;
|
|
57
|
+
const res = found.value;
|
|
58
|
+
const title = newTitle.trim();
|
|
59
|
+
const base = sanitizeComponent(title);
|
|
60
|
+
if (base === "" || base === "." || base === "..")
|
|
61
|
+
return badRequest("new title must not be empty");
|
|
62
|
+
if (base.startsWith("."))
|
|
63
|
+
return badRequest(`title '${title}' sanitizes to the hidden name '${base}'`);
|
|
64
|
+
if (base === res.session)
|
|
65
|
+
return ok(res.id);
|
|
66
|
+
const newDir = this.sessions.uniqueDir(res.wsPath, base, parentOf(res.dir));
|
|
67
|
+
try {
|
|
68
|
+
renameSync(res.dir, newDir);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
return fail(500, `move failed: ${errText(e)}`);
|
|
72
|
+
}
|
|
73
|
+
// W779 T2: the directory name is the sanitized title, so the DISPLAY name
|
|
74
|
+
// lives in session.json — an update that fails reports 500 rather than
|
|
75
|
+
// leaving the session labelled with its old title.
|
|
76
|
+
const titled = this.writeTitle(newDir, title);
|
|
77
|
+
if (!titled.ok) {
|
|
78
|
+
// N1 (W815): a failed meta write must not leave the directory moved — the
|
|
79
|
+
// caller (and `active_session`) still addresses the OLD id, so undo it.
|
|
80
|
+
try {
|
|
81
|
+
renameSync(newDir, res.dir);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return fail(500, `${titled.error}; rollback failed: ${errText(e)}`);
|
|
85
|
+
}
|
|
86
|
+
return titled;
|
|
87
|
+
}
|
|
88
|
+
return ok(`${res.workspace}/${baseOf(newDir)}`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* W779 T2: `session.json.title` <- [title], keeping every other key. A session
|
|
92
|
+
* that never had a meta file gets a `{title}` one (the title belongs to the
|
|
93
|
+
* session, not to the model/prompt/mode trio).
|
|
94
|
+
*/
|
|
95
|
+
writeTitle(dir, title) {
|
|
96
|
+
try {
|
|
97
|
+
writeSessionMeta(dir, { ...(readSessionMeta(dir) ?? {}), title });
|
|
98
|
+
return ok(undefined);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return fail(500, `meta write failed: ${errText(e)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** POST /api/sessions/{id}/branch — copy the log into a sibling dir. */
|
|
105
|
+
branch(id, title) {
|
|
106
|
+
const found = this.sessions.require(id);
|
|
107
|
+
if (!found.ok)
|
|
108
|
+
return found;
|
|
109
|
+
const res = found.value;
|
|
110
|
+
const parent = readSessionMeta(res.dir);
|
|
111
|
+
const asked = (title ?? "").trim();
|
|
112
|
+
// W779 T2: the default name is derived from the PARENT'S DISPLAY NAME, not
|
|
113
|
+
// from its directory (`alpha-1700000000.0-分支` was the old leak).
|
|
114
|
+
const display = asked === "" ? `${displayTitle(parent, res.session)}-分支` : asked;
|
|
115
|
+
const base = sanitizeComponent(display);
|
|
116
|
+
if (base === "" || base === "." || base === "..")
|
|
117
|
+
return badRequest("title must not be empty");
|
|
118
|
+
if (base.startsWith("."))
|
|
119
|
+
return badRequest(`title sanitizes to the hidden name '${base}'`);
|
|
120
|
+
const newDir = this.sessions.uniqueDir(res.wsPath, `${base}-${timestampSuffix(this.now())}`, parentOf(res.dir));
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync(newDir, { recursive: false });
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
return fail(500, `create failed: ${errText(e)}`);
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
copyFileSync(`${res.dir}/${SESSION_FILE}`, `${newDir}/${SESSION_FILE}`);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
removeDir(newDir);
|
|
132
|
+
return fail(500, `copy failed: ${errText(e)}`);
|
|
133
|
+
}
|
|
134
|
+
// The branch INHERITS model/prompt/mode and carries its OWN title (the asked
|
|
135
|
+
// one, or `<parent display>-分支`); the log copy above never touches meta.
|
|
136
|
+
try {
|
|
137
|
+
writeSessionMeta(newDir, { ...(parent ?? {}), title: display });
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
removeDir(newDir);
|
|
141
|
+
return fail(500, `meta write failed: ${errText(e)}`);
|
|
142
|
+
}
|
|
143
|
+
return ok(`${res.workspace}/${baseOf(newDir)}`);
|
|
144
|
+
}
|
|
145
|
+
move(res, from, to) {
|
|
146
|
+
try {
|
|
147
|
+
mkdirSync(parentOf(to), { recursive: true });
|
|
148
|
+
renameSync(from, to);
|
|
149
|
+
return ok(undefined);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return fail(500, `move failed: ${errText(e)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* POST /api/sessions/{id}/archive — id-preserving move into the archive.
|
|
157
|
+
*
|
|
158
|
+
* W794: the active session is archived like any other; the active MARKER is
|
|
159
|
+
* cleared afterwards (the archived row is not part of the default listing, so a
|
|
160
|
+
* marker still pointing at it would contradict `GET /api/sessions`).
|
|
161
|
+
*/
|
|
162
|
+
archive(id) {
|
|
163
|
+
const resolved = this.sessions.resolve(id);
|
|
164
|
+
if (!resolved.ok)
|
|
165
|
+
return resolved;
|
|
166
|
+
const res = resolved.value;
|
|
167
|
+
if (!isDirectory(res.dir) || !isFile(`${res.dir}/${SESSION_FILE}`))
|
|
168
|
+
return notFound(`unknown session '${id}'`);
|
|
169
|
+
const dst = join(archiveRoots(res.wsPath)[0] ?? res.wsPath, res.session);
|
|
170
|
+
if (archiveDirCandidates(res.wsPath, res.session).some((c) => existsSync(c))) {
|
|
171
|
+
return conflict(`session '${id}' is already archived`);
|
|
172
|
+
}
|
|
173
|
+
const moved = this.move(res, res.dir, dst);
|
|
174
|
+
if (!moved.ok)
|
|
175
|
+
return moved;
|
|
176
|
+
return this.clearActiveIf(id);
|
|
177
|
+
}
|
|
178
|
+
unarchive(id) {
|
|
179
|
+
const resolved = this.sessions.resolve(id);
|
|
180
|
+
if (!resolved.ok)
|
|
181
|
+
return resolved;
|
|
182
|
+
const res = resolved.value;
|
|
183
|
+
const src = archiveDirCandidates(res.wsPath, res.session).find((c) => isDirectory(c));
|
|
184
|
+
if (src === undefined)
|
|
185
|
+
return notFound(`session '${id}' is not archived`);
|
|
186
|
+
if (existsSync(res.dir))
|
|
187
|
+
return conflict(`a live session already exists at '${id}'`);
|
|
188
|
+
return this.move(res, src, res.dir);
|
|
189
|
+
}
|
|
190
|
+
/** POST /api/sessions/batch-archive — per-id, always 200. */
|
|
191
|
+
batchArchive(ids) {
|
|
192
|
+
const out = this.batch(ids, (id) => this.archive(id));
|
|
193
|
+
return { archived: out.count, failed: out.failed };
|
|
194
|
+
}
|
|
195
|
+
/** POST /api/sessions/batch-delete — per-id move to `.celestea-trash`. */
|
|
196
|
+
batchDelete(ids) {
|
|
197
|
+
const out = this.batch(ids, (id) => this.trash(id));
|
|
198
|
+
return { deleted: out.count, failed: out.failed };
|
|
199
|
+
}
|
|
200
|
+
batch(ids, op) {
|
|
201
|
+
let count = 0;
|
|
202
|
+
const failed = [];
|
|
203
|
+
for (const id of ids) {
|
|
204
|
+
const res = op(id);
|
|
205
|
+
if (res.ok)
|
|
206
|
+
count += 1;
|
|
207
|
+
else
|
|
208
|
+
failed.push({ id, error: res.error });
|
|
209
|
+
}
|
|
210
|
+
return { count, failed };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The session's directory wherever it currently IS: the live location first,
|
|
214
|
+
* else the archive (W791 B3).
|
|
215
|
+
*
|
|
216
|
+
* Deleting an ARCHIVED session used to answer `unknown session '<id>'`: the
|
|
217
|
+
* operation only ever looked at `<ws>/<session>`, which is exactly where an
|
|
218
|
+
* archived session is NOT. The id is unchanged by archiving, so the archive is
|
|
219
|
+
* the only other place it can be — and both a live and an archived copy is
|
|
220
|
+
* impossible (`unarchive` refuses to overwrite a live session).
|
|
221
|
+
*/
|
|
222
|
+
locate(res) {
|
|
223
|
+
if (isDirectory(res.dir) && isFile(`${res.dir}/${SESSION_FILE}`))
|
|
224
|
+
return res.dir;
|
|
225
|
+
const archived = archiveDirCandidates(res.wsPath, res.session).find((c) => isDirectory(c) && isFile(`${c}/${SESSION_FILE}`));
|
|
226
|
+
return archived ?? null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* One session into `<ws>/.celestea-trash/<session>-<ts>`.
|
|
230
|
+
*
|
|
231
|
+
* W794: no active-session guard. Deleting the focused session is a normal
|
|
232
|
+
* operation — the marker is cleared instead, in the same atomic write the
|
|
233
|
+
* registry uses for every other `active_session` change.
|
|
234
|
+
*/
|
|
235
|
+
trash(id) {
|
|
236
|
+
const resolved = this.sessions.resolve(id);
|
|
237
|
+
if (!resolved.ok)
|
|
238
|
+
return resolved;
|
|
239
|
+
const res = resolved.value;
|
|
240
|
+
const from = this.locate(res);
|
|
241
|
+
if (from === null)
|
|
242
|
+
return notFound(`unknown session '${id}'`);
|
|
243
|
+
const dst = join(trashRoots(res.wsPath)[0] ?? res.wsPath, `${res.session}-${timestampSuffix(this.now())}`);
|
|
244
|
+
const moved = this.move(res, from, dst);
|
|
245
|
+
if (!moved.ok)
|
|
246
|
+
return moved;
|
|
247
|
+
return this.clearActiveIf(id);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* W794: `active_session` must not survive the session it names.
|
|
251
|
+
*
|
|
252
|
+
* Called only AFTER the directory has actually moved (a failure to move leaves
|
|
253
|
+
* the registry untouched — the session is still there and still active). The
|
|
254
|
+
* value becomes `null`: no other session is silently promoted, so the UI shows
|
|
255
|
+
* what is true (nothing is focused) instead of a session the user never chose.
|
|
256
|
+
* A failed persist is a 500 — the caller's `failed[]` / error then says the
|
|
257
|
+
* registry could not be updated, rather than pretending the id is gone.
|
|
258
|
+
*/
|
|
259
|
+
clearActiveIf(id) {
|
|
260
|
+
if (this.ws.activeSession() !== id.trim())
|
|
261
|
+
return ok(undefined);
|
|
262
|
+
return this.ws.setActiveSession(null);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W860: session-level TOOL SWITCHES — the pure SUBTRACTIVE layer beside the W9
|
|
3
|
+
* permission baseline.
|
|
4
|
+
*
|
|
5
|
+
* A session may DISABLE tools by name (`<session dir>/tools.json`). The list is
|
|
6
|
+
* a DENY set ONLY: `engine-grants.ts` unions it with the preset's `toolDeny`
|
|
7
|
+
* (preset first, session second) into the session's effective deny, so the engine
|
|
8
|
+
* can never turn a blocked name back on — including the names the `execution`
|
|
9
|
+
* mode folded away. Nothing here ever ADDS a tool (grants' `tool_extra` is the
|
|
10
|
+
* only additive path, and it is unaffected by this file).
|
|
11
|
+
*
|
|
12
|
+
* Same discipline as permission.json: whitelist + validate every field, atomic
|
|
13
|
+
* 0600 write, and a corrupt / foreign file means "run as if nothing were
|
|
14
|
+
* disabled" — never "repair it". That is deliberately fail-closed for the DENY
|
|
15
|
+
* side: an unreadable list must never silently WIDEN the tool face.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SESSION_TOOLS_FILE = "tools.json";
|
|
18
|
+
export interface SessionToolsRead {
|
|
19
|
+
/** The normalized disabled list; empty whenever the file is absent or void. */
|
|
20
|
+
disabled: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Why the file was ignored. At most one entry, and it always says the session
|
|
23
|
+
* runs with nothing disabled (the same "degrade + say so" contract as
|
|
24
|
+
* `readSessionPermission`).
|
|
25
|
+
*/
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read + validate; NEVER throws. A missing file is `{disabled: []}` with no
|
|
30
|
+
* warning (the pre-W860 bytes); a broken one is the empty deny set plus ONE
|
|
31
|
+
* warning that states the session runs with no tools disabled.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readSessionTools(dir: string, expectedSession: string): SessionToolsRead;
|
|
34
|
+
/**
|
|
35
|
+
* The ONE normalizer: trim, drop blanks, dedupe, keep first-occurrence order.
|
|
36
|
+
* The PUT endpoint validates the raw array first (any non-string or blank is a
|
|
37
|
+
* 422); a hand-written file goes through the same function on read, where a
|
|
38
|
+
* blank entry is merely dropped.
|
|
39
|
+
*/
|
|
40
|
+
export declare function normalizeDisabledTools(disabled: readonly string[]): string[];
|
|
41
|
+
/** Normalize + atomic 0600 write; returns the list that was persisted. */
|
|
42
|
+
export declare function writeSessionTools(dir: string, session: string, disabled: readonly string[], now: number): string[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W860: session-level TOOL SWITCHES — the pure SUBTRACTIVE layer beside the W9
|
|
3
|
+
* permission baseline.
|
|
4
|
+
*
|
|
5
|
+
* A session may DISABLE tools by name (`<session dir>/tools.json`). The list is
|
|
6
|
+
* a DENY set ONLY: `engine-grants.ts` unions it with the preset's `toolDeny`
|
|
7
|
+
* (preset first, session second) into the session's effective deny, so the engine
|
|
8
|
+
* can never turn a blocked name back on — including the names the `execution`
|
|
9
|
+
* mode folded away. Nothing here ever ADDS a tool (grants' `tool_extra` is the
|
|
10
|
+
* only additive path, and it is unaffected by this file).
|
|
11
|
+
*
|
|
12
|
+
* Same discipline as permission.json: whitelist + validate every field, atomic
|
|
13
|
+
* 0600 write, and a corrupt / foreign file means "run as if nothing were
|
|
14
|
+
* disabled" — never "repair it". That is deliberately fail-closed for the DENY
|
|
15
|
+
* side: an unreadable list must never silently WIDEN the tool face.
|
|
16
|
+
*/
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { readJsonIfExists, writeJsonAtomic } from "./fs-json.js";
|
|
19
|
+
export const SESSION_TOOLS_FILE = "tools.json";
|
|
20
|
+
/**
|
|
21
|
+
* Read + validate; NEVER throws. A missing file is `{disabled: []}` with no
|
|
22
|
+
* warning (the pre-W860 bytes); a broken one is the empty deny set plus ONE
|
|
23
|
+
* warning that states the session runs with no tools disabled.
|
|
24
|
+
*/
|
|
25
|
+
export function readSessionTools(dir, expectedSession) {
|
|
26
|
+
const out = readJsonIfExists(join(dir, SESSION_TOOLS_FILE));
|
|
27
|
+
if (!out.exists)
|
|
28
|
+
return { disabled: [], warnings: [] };
|
|
29
|
+
const voided = (reason) => ({
|
|
30
|
+
disabled: [],
|
|
31
|
+
warnings: ["tools_unreadable: " + reason + " — the session runs with no tools disabled"],
|
|
32
|
+
});
|
|
33
|
+
if (out.error !== undefined)
|
|
34
|
+
return voided("unparsable tools.json: " + out.error);
|
|
35
|
+
const value = out.value;
|
|
36
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
37
|
+
return voided("tools.json is not an object");
|
|
38
|
+
const rec = value;
|
|
39
|
+
if (rec["version"] !== 1)
|
|
40
|
+
return voided("unknown tools.json version " + JSON.stringify(rec["version"]));
|
|
41
|
+
if (rec["session"] !== expectedSession)
|
|
42
|
+
return voided("tools.json belongs to " + JSON.stringify(rec["session"]));
|
|
43
|
+
const raw = rec["disabled"];
|
|
44
|
+
if (!Array.isArray(raw) || raw.some((name) => typeof name !== "string"))
|
|
45
|
+
return voided("tools.json has no \`disabled\` array of strings");
|
|
46
|
+
return { disabled: normalizeDisabledTools(raw), warnings: [] };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The ONE normalizer: trim, drop blanks, dedupe, keep first-occurrence order.
|
|
50
|
+
* The PUT endpoint validates the raw array first (any non-string or blank is a
|
|
51
|
+
* 422); a hand-written file goes through the same function on read, where a
|
|
52
|
+
* blank entry is merely dropped.
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeDisabledTools(disabled) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const value of disabled) {
|
|
57
|
+
const name = value.trim();
|
|
58
|
+
if (name === "" || out.includes(name))
|
|
59
|
+
continue;
|
|
60
|
+
out.push(name);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** Normalize + atomic 0600 write; returns the list that was persisted. */
|
|
65
|
+
export function writeSessionTools(dir, session, disabled, now) {
|
|
66
|
+
const normalized = normalizeDisabledTools(disabled);
|
|
67
|
+
writeJsonAtomic(join(dir, SESSION_TOOLS_FILE), { version: 1, session, disabled: normalized, updated_at: now }, { mode: 0o600 });
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|