@celestea/core 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/README.md +95 -0
- package/contracts/data-files/checkpoint.schema.json +111 -0
- package/contracts/data-files/cli-main-jsonl-precompact.schema.json +27 -0
- package/contracts/data-files/cli-main-jsonl.schema.json +22 -0
- package/contracts/data-files/fallbacks.schema.json +71 -0
- package/contracts/data-files/index.json +124 -0
- package/contracts/data-files/pricing.schema.json +65 -0
- package/contracts/data-files/prompts.schema.json +130 -0
- package/contracts/data-files/providers.schema.json +177 -0
- package/contracts/data-files/registry-tsv.schema.json +74 -0
- package/contracts/data-files/session.schema.json +51 -0
- package/contracts/data-files/usage-ledger.schema.json +112 -0
- package/contracts/data-files/workspaces.schema.json +63 -0
- package/contracts/endpoints.json +4390 -0
- package/contracts/probe-evidence.json +219 -0
- package/contracts/route-table.snapshot.json +377 -0
- package/contracts/scope-hash-vectors.json +273 -0
- package/contracts/session-event.schema.json +441 -0
- package/contracts/sse-events.json +202 -0
- package/contracts/tools.json +730 -0
- package/dist/agent.d.ts +65 -0
- package/dist/agent.js +36 -0
- package/dist/celestea-home.d.ts +63 -0
- package/dist/celestea-home.js +96 -0
- package/dist/celestea-sources.d.ts +53 -0
- package/dist/celestea-sources.js +61 -0
- package/dist/context.d.ts +33 -0
- package/dist/context.js +55 -0
- package/dist/contracts/index.d.ts +234 -0
- package/dist/contracts/index.js +159 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +22 -0
- package/dist/event-bus.d.ts +60 -0
- package/dist/event-bus.js +100 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +66 -0
- package/dist/injection.d.ts +61 -0
- package/dist/injection.js +27 -0
- package/dist/json.d.ts +34 -0
- package/dist/json.js +127 -0
- package/dist/llm.d.ts +34 -0
- package/dist/llm.js +41 -0
- package/dist/memory.d.ts +72 -0
- package/dist/memory.js +123 -0
- package/dist/message.d.ts +189 -0
- package/dist/message.js +252 -0
- package/dist/plugin.d.ts +38 -0
- package/dist/plugin.js +49 -0
- package/dist/projection.d.ts +67 -0
- package/dist/projection.js +168 -0
- package/dist/question.d.ts +154 -0
- package/dist/question.js +82 -0
- package/dist/redact.d.ts +40 -0
- package/dist/redact.js +185 -0
- package/dist/repo.d.ts +14 -0
- package/dist/repo.js +87 -0
- package/dist/sandbox.d.ts +182 -0
- package/dist/sandbox.js +78 -0
- package/dist/session-event.d.ts +57 -0
- package/dist/session-event.js +425 -0
- package/dist/session-log.d.ts +71 -0
- package/dist/session-log.js +66 -0
- package/dist/skill-catalog.d.ts +29 -0
- package/dist/skill-catalog.js +52 -0
- package/dist/skills.d.ts +116 -0
- package/dist/skills.js +273 -0
- package/dist/sse-bus.d.ts +40 -0
- package/dist/sse-bus.js +105 -0
- package/dist/stream.d.ts +115 -0
- package/dist/stream.js +52 -0
- package/dist/tool-surface.d.ts +45 -0
- package/dist/tool-surface.js +98 -0
- package/dist/tool.d.ts +77 -0
- package/dist/tool.js +15 -0
- package/dist/turn-id.d.ts +37 -0
- package/dist/turn-id.js +76 -0
- package/dist/types.d.ts +396 -0
- package/dist/types.js +58 -0
- package/package.json +27 -0
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentLoop seam — port of `crates/core/src/agent.rs`.
|
|
3
|
+
*
|
|
4
|
+
* The loop drives one user turn against a Context: append to the session log,
|
|
5
|
+
* step the model, dispatch tool calls, write the final assistant message. The
|
|
6
|
+
* concrete loop lives in packages/agent-loop and is plugged in at compose time.
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from "./context.js";
|
|
9
|
+
import type { ImageRef } from "./message.js";
|
|
10
|
+
import type { ModelRequest } from "./stream.js";
|
|
11
|
+
export interface AgentConfig {
|
|
12
|
+
model: string;
|
|
13
|
+
system_prompt: string;
|
|
14
|
+
max_steps: number;
|
|
15
|
+
max_parallel_tool_calls: number;
|
|
16
|
+
/** Model context window in tokens; 0 disables context trimming. */
|
|
17
|
+
context_window_tokens: number;
|
|
18
|
+
/** Trim factor (0..=1) of the window that triggers old-message trimming. */
|
|
19
|
+
context_trim_threshold: number;
|
|
20
|
+
/** How many most-recent messages to keep when trimming (plus system). */
|
|
21
|
+
context_keep_recent: number;
|
|
22
|
+
}
|
|
23
|
+
/** The engine's `AgentConfig::default()` — values are contract (identity prompt included). */
|
|
24
|
+
export declare function defaultAgentConfig(): AgentConfig;
|
|
25
|
+
export declare class AgentError extends Error {
|
|
26
|
+
constructor(message: string);
|
|
27
|
+
}
|
|
28
|
+
export interface AgentLoop {
|
|
29
|
+
/**
|
|
30
|
+
* Drive one user turn; rejects with [AgentError] on a terminal failure.
|
|
31
|
+
*
|
|
32
|
+
* W804: `attachments` are content-addressed image references for THIS
|
|
33
|
+
* turn's user message. The loop writes them onto the `user_message` row; the
|
|
34
|
+
* bytes never enter the log. Omitted/empty => the pre-W804 row byte for byte.
|
|
35
|
+
*
|
|
36
|
+
* W855 (C8): `userInput === null` means "this turn has NO input of its own" —
|
|
37
|
+
* the content is whatever the turn-start drain already injected (a queued
|
|
38
|
+
* `next-turn` lane message or a mailbox receipt). The loop then writes NO
|
|
39
|
+
* `user_message` row for the input, so an autowake turn never fabricates an
|
|
40
|
+
* empty user bubble. `null` is not `""`: the host rejects empty text
|
|
41
|
+
* (`input must not be empty`) and `""` is reserved for image-only turns
|
|
42
|
+
* (W804, `""` + attachments).
|
|
43
|
+
*/
|
|
44
|
+
runTurn(ctx: Context, userInput: string | null, attachments?: readonly ImageRef[]): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Optional `AgentLoop` capability (W725): hand back the EXACT request the next
|
|
48
|
+
* step would build for `ctx` — system prompt, post-trim history and tool
|
|
49
|
+
* schemas, i.e. the very object the loop hands to `Llm.generate`.
|
|
50
|
+
*
|
|
51
|
+
* It exists so a read-only context viewer (the host's "what does the model see
|
|
52
|
+
* right now" pane) reads the engine's own assembly instead of re-deriving it:
|
|
53
|
+
* a second trim/derive implementation would drift from the loop.
|
|
54
|
+
*/
|
|
55
|
+
export interface AgentLoopContextSnapshot {
|
|
56
|
+
contextSnapshot(ctx: Context): ModelRequest;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `contextSnapshot()` of a loop, or null when it does not implement the
|
|
60
|
+
* capability (test doubles / scripted loops). Never throws: a missing
|
|
61
|
+
* capability is a "no snapshot available", not a broken turn.
|
|
62
|
+
*/
|
|
63
|
+
export declare function contextSnapshotOf(loop: unknown, ctx: Context): ModelRequest | null;
|
|
64
|
+
/** Well-known token for the agent loop service in a Context. */
|
|
65
|
+
export declare const AGENT_LOOP_SERVICE = "celestea.core.AgentLoop";
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentLoop seam — port of `crates/core/src/agent.rs`.
|
|
3
|
+
*
|
|
4
|
+
* The loop drives one user turn against a Context: append to the session log,
|
|
5
|
+
* step the model, dispatch tool calls, write the final assistant message. The
|
|
6
|
+
* concrete loop lives in packages/agent-loop and is plugged in at compose time.
|
|
7
|
+
*/
|
|
8
|
+
/** The engine's `AgentConfig::default()` — values are contract (identity prompt included). */
|
|
9
|
+
export function defaultAgentConfig() {
|
|
10
|
+
return {
|
|
11
|
+
model: "deepseek-chat",
|
|
12
|
+
system_prompt: "You are celestea, an AI agent. You are concise, accurate and direct.",
|
|
13
|
+
max_steps: 16,
|
|
14
|
+
max_parallel_tool_calls: 4,
|
|
15
|
+
context_window_tokens: 65_536,
|
|
16
|
+
context_trim_threshold: 0.8,
|
|
17
|
+
context_keep_recent: 10,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export class AgentError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "AgentError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `contextSnapshot()` of a loop, or null when it does not implement the
|
|
28
|
+
* capability (test doubles / scripted loops). Never throws: a missing
|
|
29
|
+
* capability is a "no snapshot available", not a broken turn.
|
|
30
|
+
*/
|
|
31
|
+
export function contextSnapshotOf(loop, ctx) {
|
|
32
|
+
const fn = loop?.contextSnapshot;
|
|
33
|
+
return typeof fn === "function" ? fn.call(loop, ctx) : null;
|
|
34
|
+
}
|
|
35
|
+
/** Well-known token for the agent loop service in a Context. */
|
|
36
|
+
export const AGENT_LOOP_SERVICE = "celestea.core.AgentLoop";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELESTEA_HOME — the cross-platform data root (W880).
|
|
3
|
+
*
|
|
4
|
+
* Every celestea artifact used to live inside the workspace: session dirs, the
|
|
5
|
+
* archive / trash siblings, the workspace prompt registry and run_code's
|
|
6
|
+
* transient programs. That pollutes the user's repository (a `git add -A` could
|
|
7
|
+
* commit private conversations) and ignores the OS conventions. W880 moves ALL of
|
|
8
|
+
* it into one data root, resolved with the order the established agent CLIs use
|
|
9
|
+
* (Claude Code `~/.claude`, Codex `~/.codex`, Gemini CLI `~/.gemini`):
|
|
10
|
+
*
|
|
11
|
+
* 1. `$CELESTEA_HOME` — explicit override; production systemd
|
|
12
|
+
* pins `/var/lib/celestea-agent` (the FHS
|
|
13
|
+
* `/var/lib/<service>` location);
|
|
14
|
+
* 2. `$XDG_DATA_HOME/celestea` (Linux) — XDG Base Directory, the ask behind
|
|
15
|
+
* anthropics/claude-code#1455;
|
|
16
|
+
* 3. `~/.celestea` — Linux/macOS default, the de-facto
|
|
17
|
+
* standard of the agent CLIs;
|
|
18
|
+
* 4. `%USERPROFILE%\.celestea` (Windows) — Windows default.
|
|
19
|
+
*
|
|
20
|
+
* This module is PURE: it reads the injected `env` / `platform` / `homedir`
|
|
21
|
+
* (defaulting to the process), never the filesystem, so the whole order is a
|
|
22
|
+
* unit-testable function. The layout under the root is
|
|
23
|
+
*
|
|
24
|
+
* <home>/workspaces/<workspace-folder>/{sessions,archive,trash,run-code}/
|
|
25
|
+
* <home>/workspaces/<workspace-folder>/prompts.json
|
|
26
|
+
*
|
|
27
|
+
* `<workspace-folder>` is the same key `workspaces.json` uses (the registered
|
|
28
|
+
* path's basename; see `apps/studio/src/store/workspaces.ts`).
|
|
29
|
+
*/
|
|
30
|
+
/** Env var: explicit data-root override (highest priority). */
|
|
31
|
+
export declare const CELESTEA_HOME_ENV = "CELESTEA_HOME";
|
|
32
|
+
/** Folder under `$XDG_DATA_HOME` on Linux. */
|
|
33
|
+
export declare const CELESTEA_DATA_DIR = "celestea";
|
|
34
|
+
/** Folder under the data root that holds every workspace's container. */
|
|
35
|
+
export declare const CELESTEA_WORKSPACES_DIR = "workspaces";
|
|
36
|
+
/** Live-session sub-container. */
|
|
37
|
+
export declare const CELESTEA_SESSIONS_DIR = "sessions";
|
|
38
|
+
/** Archived-session sub-container. */
|
|
39
|
+
export declare const CELESTEA_ARCHIVE_DIR = "archive";
|
|
40
|
+
/** Trashed-session sub-container. */
|
|
41
|
+
export declare const CELESTEA_TRASH_DIR = "trash";
|
|
42
|
+
/** run_code's transient program sub-container. */
|
|
43
|
+
export declare const CELESTEA_RUN_CODE_DIR = "run-code";
|
|
44
|
+
/** Workspace-scoped prompt registry file name. */
|
|
45
|
+
export declare const CELESTEA_PROMPTS_FILE = "prompts.json";
|
|
46
|
+
/** Everything `celesteaHome` may depend on, injectable for tests. */
|
|
47
|
+
export interface CelesteaHomeInput {
|
|
48
|
+
env?: Record<string, string | undefined>;
|
|
49
|
+
platform?: NodeJS.Platform | string;
|
|
50
|
+
homedir?: string;
|
|
51
|
+
}
|
|
52
|
+
/** The folder name that keys a workspace (same rule as `workspaces.json`). */
|
|
53
|
+
export declare function workspaceFolderName(wsPath: string, platform?: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the celestea data root. First hit wins:
|
|
56
|
+
* `$CELESTEA_HOME` -> `$XDG_DATA_HOME/celestea` (Linux) -> `~/.celestea`
|
|
57
|
+
* (Linux/macOS) -> `%USERPROFILE%\.celestea` (Windows).
|
|
58
|
+
*/
|
|
59
|
+
export declare function celesteaHome(input?: CelesteaHomeInput): string;
|
|
60
|
+
/** `<home>/workspaces/<workspace-folder>` — this workspace's container. */
|
|
61
|
+
export declare function workspaceHome(wsPath: string, input?: CelesteaHomeInput): string;
|
|
62
|
+
/** `<workspaceHome>/<sub>` — one of the fixed sub-containers. */
|
|
63
|
+
export declare function workspaceSubdir(wsPath: string, sub: string, input?: CelesteaHomeInput): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELESTEA_HOME — the cross-platform data root (W880).
|
|
3
|
+
*
|
|
4
|
+
* Every celestea artifact used to live inside the workspace: session dirs, the
|
|
5
|
+
* archive / trash siblings, the workspace prompt registry and run_code's
|
|
6
|
+
* transient programs. That pollutes the user's repository (a `git add -A` could
|
|
7
|
+
* commit private conversations) and ignores the OS conventions. W880 moves ALL of
|
|
8
|
+
* it into one data root, resolved with the order the established agent CLIs use
|
|
9
|
+
* (Claude Code `~/.claude`, Codex `~/.codex`, Gemini CLI `~/.gemini`):
|
|
10
|
+
*
|
|
11
|
+
* 1. `$CELESTEA_HOME` — explicit override; production systemd
|
|
12
|
+
* pins `/var/lib/celestea-agent` (the FHS
|
|
13
|
+
* `/var/lib/<service>` location);
|
|
14
|
+
* 2. `$XDG_DATA_HOME/celestea` (Linux) — XDG Base Directory, the ask behind
|
|
15
|
+
* anthropics/claude-code#1455;
|
|
16
|
+
* 3. `~/.celestea` — Linux/macOS default, the de-facto
|
|
17
|
+
* standard of the agent CLIs;
|
|
18
|
+
* 4. `%USERPROFILE%\.celestea` (Windows) — Windows default.
|
|
19
|
+
*
|
|
20
|
+
* This module is PURE: it reads the injected `env` / `platform` / `homedir`
|
|
21
|
+
* (defaulting to the process), never the filesystem, so the whole order is a
|
|
22
|
+
* unit-testable function. The layout under the root is
|
|
23
|
+
*
|
|
24
|
+
* <home>/workspaces/<workspace-folder>/{sessions,archive,trash,run-code}/
|
|
25
|
+
* <home>/workspaces/<workspace-folder>/prompts.json
|
|
26
|
+
*
|
|
27
|
+
* `<workspace-folder>` is the same key `workspaces.json` uses (the registered
|
|
28
|
+
* path's basename; see `apps/studio/src/store/workspaces.ts`).
|
|
29
|
+
*/
|
|
30
|
+
import { homedir as osHomedir } from "node:os";
|
|
31
|
+
import { posix, win32 } from "node:path";
|
|
32
|
+
/** Env var: explicit data-root override (highest priority). */
|
|
33
|
+
export const CELESTEA_HOME_ENV = "CELESTEA_HOME";
|
|
34
|
+
/** Folder under `$XDG_DATA_HOME` on Linux. */
|
|
35
|
+
export const CELESTEA_DATA_DIR = "celestea";
|
|
36
|
+
/** Folder under the data root that holds every workspace's container. */
|
|
37
|
+
export const CELESTEA_WORKSPACES_DIR = "workspaces";
|
|
38
|
+
/** Live-session sub-container. */
|
|
39
|
+
export const CELESTEA_SESSIONS_DIR = "sessions";
|
|
40
|
+
/** Archived-session sub-container. */
|
|
41
|
+
export const CELESTEA_ARCHIVE_DIR = "archive";
|
|
42
|
+
/** Trashed-session sub-container. */
|
|
43
|
+
export const CELESTEA_TRASH_DIR = "trash";
|
|
44
|
+
/** run_code's transient program sub-container. */
|
|
45
|
+
export const CELESTEA_RUN_CODE_DIR = "run-code";
|
|
46
|
+
/** Workspace-scoped prompt registry file name. */
|
|
47
|
+
export const CELESTEA_PROMPTS_FILE = "prompts.json";
|
|
48
|
+
/** The path implementation of a platform (deterministic across hosts in tests). */
|
|
49
|
+
function pathApi(platform) {
|
|
50
|
+
return platform === "win32" ? win32 : posix;
|
|
51
|
+
}
|
|
52
|
+
/** A non-blank, trimmed env value; `undefined` when unset or blank. */
|
|
53
|
+
function envValue(env, name) {
|
|
54
|
+
const raw = env[name];
|
|
55
|
+
if (typeof raw !== "string")
|
|
56
|
+
return undefined;
|
|
57
|
+
const trimmed = raw.trim();
|
|
58
|
+
return trimmed === "" ? undefined : trimmed;
|
|
59
|
+
}
|
|
60
|
+
/** The folder name that keys a workspace (same rule as `workspaces.json`). */
|
|
61
|
+
export function workspaceFolderName(wsPath, platform = process.platform) {
|
|
62
|
+
const base = pathApi(platform).basename(wsPath);
|
|
63
|
+
return base === "" || base === "/" ? wsPath : base;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the celestea data root. First hit wins:
|
|
67
|
+
* `$CELESTEA_HOME` -> `$XDG_DATA_HOME/celestea` (Linux) -> `~/.celestea`
|
|
68
|
+
* (Linux/macOS) -> `%USERPROFILE%\.celestea` (Windows).
|
|
69
|
+
*/
|
|
70
|
+
export function celesteaHome(input = {}) {
|
|
71
|
+
const env = input.env ?? process.env;
|
|
72
|
+
const platform = input.platform ?? process.platform;
|
|
73
|
+
const override = envValue(env, CELESTEA_HOME_ENV);
|
|
74
|
+
if (override !== undefined)
|
|
75
|
+
return override;
|
|
76
|
+
if (platform === "win32") {
|
|
77
|
+
const profile = envValue(env, "USERPROFILE");
|
|
78
|
+
return win32.join(profile ?? input.homedir ?? osHomedir(), ".celestea");
|
|
79
|
+
}
|
|
80
|
+
if (platform === "linux") {
|
|
81
|
+
const xdg = envValue(env, "XDG_DATA_HOME");
|
|
82
|
+
if (xdg !== undefined)
|
|
83
|
+
return posix.join(xdg, CELESTEA_DATA_DIR);
|
|
84
|
+
}
|
|
85
|
+
return posix.join(input.homedir ?? osHomedir(), ".celestea");
|
|
86
|
+
}
|
|
87
|
+
/** `<home>/workspaces/<workspace-folder>` — this workspace's container. */
|
|
88
|
+
export function workspaceHome(wsPath, input = {}) {
|
|
89
|
+
const platform = input.platform ?? process.platform;
|
|
90
|
+
return pathApi(platform).join(celesteaHome(input), CELESTEA_WORKSPACES_DIR, workspaceFolderName(wsPath, platform));
|
|
91
|
+
}
|
|
92
|
+
/** `<workspaceHome>/<sub>` — one of the fixed sub-containers. */
|
|
93
|
+
export function workspaceSubdir(wsPath, sub, input = {}) {
|
|
94
|
+
const platform = input.platform ?? process.platform;
|
|
95
|
+
return pathApi(platform).join(workspaceHome(wsPath, input), sub);
|
|
96
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W882 — the two-layer source model for Celestea artifacts.
|
|
3
|
+
*
|
|
4
|
+
* The user decision (W882, after the W879 industry survey) is B: ONE writable
|
|
5
|
+
* global container plus an OPTIONAL, hand-maintained, READ-ONLY project source.
|
|
6
|
+
* Claude Code ships the same two layers (project `.claude/` + user `~/.claude/`),
|
|
7
|
+
* Cursor does it with `.cursor/skills/` + `~/.cursor/skills/`. The rules are:
|
|
8
|
+
*
|
|
9
|
+
* 1. PROJECT layer = `<ws>/.celestea` — committed with the repo, shared
|
|
10
|
+
* by the team, maintained BY HAND. Celestea NEVER writes here.
|
|
11
|
+
* 2. GLOBAL layer = `<home>/workspaces/<ws>` — the ONE place Celestea writes
|
|
12
|
+
* (sessions / archive / trash / prompts / run-code, see `celestea-home.ts`).
|
|
13
|
+
* 3. `readLayers()` returns `[project, global]`: the array order IS the priority
|
|
14
|
+
* order, so the project layer WINS on a name collision.
|
|
15
|
+
*
|
|
16
|
+
* `<ws>/.celestea/sessions/` is the W877 transitional session layout. It stays
|
|
17
|
+
* readable (see `session-id.ts` / `sessions.ts`), but the write side is the
|
|
18
|
+
* global container only. These functions are PURE: they never touch the disk, so
|
|
19
|
+
* the whole layering is unit-testable without a filesystem.
|
|
20
|
+
*/
|
|
21
|
+
import { type CelesteaHomeInput } from "./celestea-home.js";
|
|
22
|
+
/** Folder that marks the project-level source root: `<ws>/.celestea` (read-only). */
|
|
23
|
+
export declare const PROJECT_SOURCE_DIR = ".celestea";
|
|
24
|
+
/** Sub-folder holding one directory per skill (both layers use the same shape). */
|
|
25
|
+
export declare const SKILLS_SUBDIR = "skills";
|
|
26
|
+
/** The single entry file of a skill directory. */
|
|
27
|
+
export declare const SKILL_FILE_NAME = "SKILL.md";
|
|
28
|
+
/** Which layer a source belongs to. `project` beats `global`. */
|
|
29
|
+
export type SourceName = "project" | "global";
|
|
30
|
+
/** One read layer: a priority-tagged root. */
|
|
31
|
+
export interface SourceLayer {
|
|
32
|
+
readonly source: SourceName;
|
|
33
|
+
readonly root: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `<ws>/.celestea` — the project-level source root.
|
|
37
|
+
*
|
|
38
|
+
* READ-ONLY by contract: no Celestea code path may create or write under it. The
|
|
39
|
+
* user maintains it by hand and commits it with the repository.
|
|
40
|
+
*/
|
|
41
|
+
export declare function projectSourceRoot(wsPath: string, platform?: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* `<home>/workspaces/<workspace-folder>` — the global source root. This is the
|
|
44
|
+
* SAME container `workspaceHome()` resolves; the alias exists so callers talk
|
|
45
|
+
* about "layers" without depending on the `CELESTEA_HOME` layout details.
|
|
46
|
+
*/
|
|
47
|
+
export declare function globalSourceRoot(wsPath: string, input?: CelesteaHomeInput): string;
|
|
48
|
+
/**
|
|
49
|
+
* The read layers of a workspace, HIGHEST PRIORITY FIRST: `[project, global]`.
|
|
50
|
+
* A consumer that walks the array and keeps the first hit therefore lets the
|
|
51
|
+
* project layer override the global one — reversing the array is a bug.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readLayers(wsPath: string, input?: CelesteaHomeInput): SourceLayer[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W882 — the two-layer source model for Celestea artifacts.
|
|
3
|
+
*
|
|
4
|
+
* The user decision (W882, after the W879 industry survey) is B: ONE writable
|
|
5
|
+
* global container plus an OPTIONAL, hand-maintained, READ-ONLY project source.
|
|
6
|
+
* Claude Code ships the same two layers (project `.claude/` + user `~/.claude/`),
|
|
7
|
+
* Cursor does it with `.cursor/skills/` + `~/.cursor/skills/`. The rules are:
|
|
8
|
+
*
|
|
9
|
+
* 1. PROJECT layer = `<ws>/.celestea` — committed with the repo, shared
|
|
10
|
+
* by the team, maintained BY HAND. Celestea NEVER writes here.
|
|
11
|
+
* 2. GLOBAL layer = `<home>/workspaces/<ws>` — the ONE place Celestea writes
|
|
12
|
+
* (sessions / archive / trash / prompts / run-code, see `celestea-home.ts`).
|
|
13
|
+
* 3. `readLayers()` returns `[project, global]`: the array order IS the priority
|
|
14
|
+
* order, so the project layer WINS on a name collision.
|
|
15
|
+
*
|
|
16
|
+
* `<ws>/.celestea/sessions/` is the W877 transitional session layout. It stays
|
|
17
|
+
* readable (see `session-id.ts` / `sessions.ts`), but the write side is the
|
|
18
|
+
* global container only. These functions are PURE: they never touch the disk, so
|
|
19
|
+
* the whole layering is unit-testable without a filesystem.
|
|
20
|
+
*/
|
|
21
|
+
import { posix, win32 } from "node:path";
|
|
22
|
+
import { workspaceHome } from "./celestea-home.js";
|
|
23
|
+
/** Folder that marks the project-level source root: `<ws>/.celestea` (read-only). */
|
|
24
|
+
export const PROJECT_SOURCE_DIR = ".celestea";
|
|
25
|
+
/** Sub-folder holding one directory per skill (both layers use the same shape). */
|
|
26
|
+
export const SKILLS_SUBDIR = "skills";
|
|
27
|
+
/** The single entry file of a skill directory. */
|
|
28
|
+
export const SKILL_FILE_NAME = "SKILL.md";
|
|
29
|
+
/** The path implementation of a platform (deterministic across hosts in tests). */
|
|
30
|
+
function pathApi(platform) {
|
|
31
|
+
return platform === "win32" ? win32 : posix;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `<ws>/.celestea` — the project-level source root.
|
|
35
|
+
*
|
|
36
|
+
* READ-ONLY by contract: no Celestea code path may create or write under it. The
|
|
37
|
+
* user maintains it by hand and commits it with the repository.
|
|
38
|
+
*/
|
|
39
|
+
export function projectSourceRoot(wsPath, platform = process.platform) {
|
|
40
|
+
return pathApi(platform).join(wsPath, PROJECT_SOURCE_DIR);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `<home>/workspaces/<workspace-folder>` — the global source root. This is the
|
|
44
|
+
* SAME container `workspaceHome()` resolves; the alias exists so callers talk
|
|
45
|
+
* about "layers" without depending on the `CELESTEA_HOME` layout details.
|
|
46
|
+
*/
|
|
47
|
+
export function globalSourceRoot(wsPath, input = {}) {
|
|
48
|
+
return workspaceHome(wsPath, input);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The read layers of a workspace, HIGHEST PRIORITY FIRST: `[project, global]`.
|
|
52
|
+
* A consumer that walks the array and keeps the first hit therefore lets the
|
|
53
|
+
* project layer override the global one — reversing the array is a bug.
|
|
54
|
+
*/
|
|
55
|
+
export function readLayers(wsPath, input = {}) {
|
|
56
|
+
const platform = input.platform ?? process.platform;
|
|
57
|
+
return [
|
|
58
|
+
{ source: "project", root: projectSourceRoot(wsPath, platform) },
|
|
59
|
+
{ source: "global", root: globalSourceRoot(wsPath, input) },
|
|
60
|
+
];
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context seam — port of `crates/core/src/context.rs`.
|
|
3
|
+
*
|
|
4
|
+
* A shared, token-keyed service container. Plugins provide services into it;
|
|
5
|
+
* consumers resolve services out of it. A parent chain lets an agent carry a
|
|
6
|
+
* scoped Context layered over the global one.
|
|
7
|
+
*
|
|
8
|
+
* TypeScript has no TypeId, so services are keyed by an explicit token: a
|
|
9
|
+
* well-known string (see `*_SERVICE` constants), a symbol, or a class
|
|
10
|
+
* constructor used by identity. Same semantics otherwise: a later `provide` of
|
|
11
|
+
* the same token replaces the earlier one, and `get` falls back to the parent
|
|
12
|
+
* scope.
|
|
13
|
+
*/
|
|
14
|
+
/** What can key a service slot. */
|
|
15
|
+
export type ServiceToken<T> = string | symbol | (abstract new (...args: never[]) => T);
|
|
16
|
+
export declare class Context {
|
|
17
|
+
private readonly services;
|
|
18
|
+
private readonly parent;
|
|
19
|
+
constructor(parent?: Context | null);
|
|
20
|
+
/** A fresh root context (`Context::new`). */
|
|
21
|
+
static root(): Context;
|
|
22
|
+
/** Register a service; a later registration of the same token replaces it. */
|
|
23
|
+
provide<T>(token: ServiceToken<T>, service: T): void;
|
|
24
|
+
/** Resolve a service by token, falling back to the parent scope. */
|
|
25
|
+
get<T>(token: ServiceToken<T>): T | undefined;
|
|
26
|
+
/** Resolve or throw: for call sites where a missing service is a bug. */
|
|
27
|
+
require<T>(token: ServiceToken<T>): T;
|
|
28
|
+
has<T>(token: ServiceToken<T>): boolean;
|
|
29
|
+
/** Create a child scope that falls back to this context (one per agent). */
|
|
30
|
+
scoped(): Context;
|
|
31
|
+
/** The tokens provided in THIS scope (parent services excluded). */
|
|
32
|
+
localTokens(): unknown[];
|
|
33
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context seam — port of `crates/core/src/context.rs`.
|
|
3
|
+
*
|
|
4
|
+
* A shared, token-keyed service container. Plugins provide services into it;
|
|
5
|
+
* consumers resolve services out of it. A parent chain lets an agent carry a
|
|
6
|
+
* scoped Context layered over the global one.
|
|
7
|
+
*
|
|
8
|
+
* TypeScript has no TypeId, so services are keyed by an explicit token: a
|
|
9
|
+
* well-known string (see `*_SERVICE` constants), a symbol, or a class
|
|
10
|
+
* constructor used by identity. Same semantics otherwise: a later `provide` of
|
|
11
|
+
* the same token replaces the earlier one, and `get` falls back to the parent
|
|
12
|
+
* scope.
|
|
13
|
+
*/
|
|
14
|
+
function tokenKey(token) {
|
|
15
|
+
return typeof token === "string" ? `service:${token}` : token;
|
|
16
|
+
}
|
|
17
|
+
export class Context {
|
|
18
|
+
services = new Map();
|
|
19
|
+
parent;
|
|
20
|
+
constructor(parent = null) {
|
|
21
|
+
this.parent = parent;
|
|
22
|
+
}
|
|
23
|
+
/** A fresh root context (`Context::new`). */
|
|
24
|
+
static root() {
|
|
25
|
+
return new Context(null);
|
|
26
|
+
}
|
|
27
|
+
/** Register a service; a later registration of the same token replaces it. */
|
|
28
|
+
provide(token, service) {
|
|
29
|
+
this.services.set(tokenKey(token), service);
|
|
30
|
+
}
|
|
31
|
+
/** Resolve a service by token, falling back to the parent scope. */
|
|
32
|
+
get(token) {
|
|
33
|
+
if (this.services.has(tokenKey(token)))
|
|
34
|
+
return this.services.get(tokenKey(token));
|
|
35
|
+
return this.parent?.get(token) ?? undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Resolve or throw: for call sites where a missing service is a bug. */
|
|
38
|
+
require(token) {
|
|
39
|
+
const svc = this.get(token);
|
|
40
|
+
if (svc === undefined)
|
|
41
|
+
throw new Error(`service not provided: ${String(token)}`);
|
|
42
|
+
return svc;
|
|
43
|
+
}
|
|
44
|
+
has(token) {
|
|
45
|
+
return this.get(token) !== undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Create a child scope that falls back to this context (one per agent). */
|
|
48
|
+
scoped() {
|
|
49
|
+
return new Context(this);
|
|
50
|
+
}
|
|
51
|
+
/** The tokens provided in THIS scope (parent services excluded). */
|
|
52
|
+
localTokens() {
|
|
53
|
+
return [...this.services.keys()];
|
|
54
|
+
}
|
|
55
|
+
}
|