@fnndsc/calypso 0.2.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 FNNDSC / BCH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @fnndsc/calypso
2
+
3
+ **CALYPSO** **A**ccepts **L**anguage, **Y**ielding **P**ermitted **S**hell **O**perations — the intent layer and session daemon of the [mise](../../README.md) stack.
4
+
5
+ The name is a harbor reference. In the *Odyssey*, Calypso keeps the island where the voyager finds haven; the name is the Greek word for "to conceal," which the project keeps but turns around. **HARBOR** is that haven for the ChRIS operator, and CALYPSO is the keeper at its edge — the layer between you and the open water: the Collection+JSON sprawl, the complexity of a federated backend. What CALYPSO conceals is the friction, never the outcome. A harbor shelters without holding: the work is left as materialized, verifiable state, yours to leave and return to — CALYPSO the harbor you pass through, never the ground you stand on.
6
+
7
+ ## What this package is
8
+
9
+ CALYPSO hosts the [chell](../chell/README.md) engine behind a **session daemon** and serves it to surfaces over a WebSocket: the CLI today, a web console next, each rendering the same session. Because the engine is separable from its display (see [stage one](../../docs/calypso.adoc)), it can run where the data must stay — inside a spoke's trust boundary — while an operator drives it over a thin client.
10
+
11
+ This first slice is the **wire contract**: the typed protocol schemas and boundary validation every message crosses. The daemon, the session bus, and the remote client build on it.
12
+
13
+ ## The wire contract
14
+
15
+ The contract is defined as [zod](https://zod.dev) schemas — the single source of truth, from which the message types are inferred and against which every message is validated at the boundary.
16
+
17
+ - **Messages** (two direction-keyed unions):
18
+ - surface → daemon: `attach {protocolVersion, token, session?}`, `execute {id, line}`, `complete {id, prefix}`
19
+ - daemon → surface: `attached`, `result {id, envelopes}`, `complete {id, prefix, candidates}`, `output {id, channel, chunk}`, `session {surface, envelope}`, `error`
20
+ - **Envelope** — the `commandEnvelopeSchema` validates cumin's `CommandEnvelope` on the wire; a compile-time guard keeps the schema in step with cumin's type so the published contract can never silently drift from what the code produces.
21
+ - **Boundary validation** — structural violations are rejected; unknown *additive* fields are tolerated, so a daemon accepts extensions from a newer minor without understanding them. Parsing never throws.
22
+ - **Versioning** — the contract version (`CONTRACT_VERSION`) is carried in the attach handshake and refused on mismatch; within a major, changes are additive only.
23
+
24
+ ```ts
25
+ import { clientMessage_fromJson, attach_parse, type ClientMessage } from '@fnndsc/calypso';
26
+
27
+ const parsed = clientMessage_fromJson(rawSocketText);
28
+ if (!parsed.ok) socket.send({ type: 'error', reason: parsed.error });
29
+ ```
30
+
31
+ ## Status
32
+
33
+ Design-in-progress, not shipped. The full design — daemon, session bus, remote client, and the eventual natural-language intent layer that only ever *proposes* commands the deterministic shell validates and runs — is in [docs/calypso.adoc](../../docs/calypso.adoc) (the staged plan) and [docs/surfaces.adoc](../../docs/surfaces.adoc).
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @file The engine seam the daemon hosts.
3
+ *
4
+ * The daemon is deliberately engine-agnostic: it accepts anything shaped like
5
+ * a hosted engine rather than importing chell directly. This keeps calypso's
6
+ * dependencies to cumin and the wire libraries, and — because chell will
7
+ * import calypso's wire contract for its remote client — avoids a package
8
+ * cycle. chell's `ChellEngine` structurally satisfies this interface, so the
9
+ * launcher (in chell) creates a real engine and hands it to the daemon.
10
+ *
11
+ * @module
12
+ */
13
+ import type { CommandEnvelope } from '@fnndsc/cumin';
14
+ /**
15
+ * A completion answer: the candidates and the prefix they complete.
16
+ *
17
+ * @property candidates - The matching completion candidates.
18
+ * @property prefix - The input prefix the candidates complete.
19
+ */
20
+ export interface CompletionResult {
21
+ candidates: string[];
22
+ prefix: string;
23
+ }
24
+ /**
25
+ * The engine the daemon drives. Matches chell's `ChellEngine` structurally,
26
+ * so a real chell engine is assignable to it without calypso depending on
27
+ * chell.
28
+ */
29
+ export interface HostedEngine {
30
+ /**
31
+ * Executes one input line, yielding one envelope per executed command.
32
+ *
33
+ * @param line - The raw input line.
34
+ * @returns The envelopes of the executed commands, in order.
35
+ */
36
+ line_execute(line: string): Promise<CommandEnvelope[]>;
37
+ /**
38
+ * Computes completion candidates for a partial input line.
39
+ *
40
+ * @param linePrefix - The input line up to the cursor.
41
+ * @returns The matching candidates and the prefix they complete.
42
+ */
43
+ line_complete(linePrefix: string): Promise<CompletionResult>;
44
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/daemon/engine.ts"],"names":[],"mappings":""}
@@ -0,0 +1,217 @@
1
+ import type { HostedEngine } from './engine.js';
2
+ import type { ProgressEvent } from '../protocol/messages.js';
3
+ /** The result of a surface's local edit, returned by {@link CalypsoDaemon.edit_current}. */
4
+ export interface EditOutcome {
5
+ content: string;
6
+ changed: boolean;
7
+ }
8
+ /**
9
+ * Options for creating a daemon.
10
+ *
11
+ * @property engine - The engine to host.
12
+ * @property token - The attach token a surface must present.
13
+ * @property port - The port to bind; 0 (default) picks an ephemeral port.
14
+ * @property host - The interface to bind; loopback (`127.0.0.1`) by default.
15
+ * @property scrollbackSize - How many recent envelopes to retain for replay
16
+ * to an attaching surface; defaults to 200.
17
+ * @property promptProvider - Renders the current session's themed prompt
18
+ * string, which the daemon pushes to surfaces after each command and on
19
+ * attach. Omitted when a host does not push a prompt (e.g. tests).
20
+ */
21
+ export interface DaemonOptions {
22
+ engine: HostedEngine;
23
+ token: string;
24
+ port?: number;
25
+ host?: string;
26
+ scrollbackSize?: number;
27
+ promptProvider?: () => string | Promise<string>;
28
+ }
29
+ /**
30
+ * A WebSocket daemon hosting one engine for attached surfaces, with a session
31
+ * bus broadcasting activity across them.
32
+ */
33
+ export declare class CalypsoDaemon {
34
+ private readonly engine;
35
+ private readonly token;
36
+ private readonly port;
37
+ private readonly host;
38
+ private readonly scrollbackSize;
39
+ private readonly promptProvider;
40
+ /** The one session all surfaces share; returned in each attach ack. */
41
+ private readonly sessionId;
42
+ private readonly surfaces;
43
+ private readonly scrollback;
44
+ private wss;
45
+ /**
46
+ * Execution is serialized across the whole session (one foreground command
47
+ * at a time), so a prompt raised mid-command has one unambiguous surface to
48
+ * ask — the one running the current command.
49
+ */
50
+ private queue;
51
+ private currentOrigin;
52
+ private currentId;
53
+ private readonly pendingPrompts;
54
+ private promptSeq;
55
+ private readonly pendingPipes;
56
+ private pipeSeq;
57
+ private readonly pendingEdits;
58
+ private editSeq;
59
+ /**
60
+ * @param options - The engine to host, the attach token, the bind address,
61
+ * and the scrollback size.
62
+ */
63
+ constructor(options: DaemonOptions);
64
+ /**
65
+ * Starts listening.
66
+ *
67
+ * @returns The bound port (useful when an ephemeral port was requested).
68
+ */
69
+ start(): Promise<number>;
70
+ /**
71
+ * Stops listening and closes all connections.
72
+ *
73
+ * @returns A promise resolving when the server has closed.
74
+ */
75
+ stop(): Promise<void>;
76
+ /**
77
+ * Handles one surface connection: an attach handshake, then serialized
78
+ * command dispatch, with the surface removed from the bus on close.
79
+ *
80
+ * @param socket - The connected surface.
81
+ */
82
+ private connection_handle;
83
+ /**
84
+ * Validates an attach handshake: contract version, then constant-time
85
+ * token check. On success the surface is acknowledged, registered on the
86
+ * bus, and replayed the scrollback so it does not join blind; on failure it
87
+ * is told why and disconnected.
88
+ *
89
+ * @param socket - The connecting surface.
90
+ * @param raw - The first message received.
91
+ * @returns The registered surface, or null when the attach was refused.
92
+ */
93
+ private attach_handle;
94
+ /**
95
+ * Renders the current prompt (when the host supplies a provider) and pushes
96
+ * it to a surface, or to all surfaces when none is given. Called after each
97
+ * command — the context may have changed — and on attach.
98
+ *
99
+ * @param target - The surface to push to; omitted to push to all.
100
+ */
101
+ private promptline_push;
102
+ /**
103
+ * Replays the retained scrollback to a freshly attached surface as session
104
+ * events, so it arrives seeing recent activity rather than blank.
105
+ *
106
+ * @param socket - The surface to replay to.
107
+ */
108
+ private scrollback_replay;
109
+ /**
110
+ * Records an envelope in scrollback (trimming to the retention bound) and
111
+ * broadcasts it to every attached surface except the one that produced it,
112
+ * which already received it as the correlated `result`.
113
+ *
114
+ * @param origin - The surface that produced the envelope.
115
+ * @param envelope - The envelope to publish.
116
+ */
117
+ private bus_publish;
118
+ /**
119
+ * Runs one execute request: replies to the requester with the result, and
120
+ * publishes each envelope to the session bus.
121
+ *
122
+ * @param origin - The surface that submitted the request.
123
+ * @param message - The execute request.
124
+ */
125
+ private execute_run;
126
+ /**
127
+ * Raises a prompt on the surface running the current command, returning its
128
+ * answer. The host installs an engine-side input broker that calls this; the
129
+ * engine therefore prompts through the wire without knowing the transport.
130
+ *
131
+ * @param message - The prompt text to show.
132
+ * @param hidden - Whether to request no-echo entry (a password).
133
+ * @returns The surface's answer.
134
+ * @throws {Error} When no command is executing (nothing to prompt for) or the
135
+ * surface disconnects before answering.
136
+ */
137
+ prompt_current(message: string, hidden: boolean): Promise<string>;
138
+ /**
139
+ * Streams structured progress from the executing command to its origin
140
+ * surface. Progress is live-only; when no command is active or the surface is
141
+ * gone, the event is dropped.
142
+ *
143
+ * @param event - The structured progress facts.
144
+ * @returns True when the event was sent to a surface.
145
+ */
146
+ progress_current(event: ProgressEvent): boolean;
147
+ /**
148
+ * Streams an opaque live output chunk from the executing command to its
149
+ * origin surface. Output is live telemetry, distinct from final envelopes;
150
+ * when no command is active or the origin surface is gone, it is dropped.
151
+ *
152
+ * @param channel - The output channel that produced the chunk.
153
+ * @param chunk - The text chunk to forward.
154
+ * @returns True when the chunk was sent to a surface.
155
+ */
156
+ output_current(channel: 'data' | 'err' | 'status', chunk: string): boolean;
157
+ /**
158
+ * Resolves a pending prompt with the surface's answer.
159
+ *
160
+ * @param promptId - The prompt correlation id.
161
+ * @param answer - The answer the surface supplied.
162
+ */
163
+ private promptAnswer_settle;
164
+ /**
165
+ * Runs a pipeline segment on the surface running the current command,
166
+ * returning its output. The host installs a `pipeSegment` on its engine-side
167
+ * surface that calls this, so a pipeline's segments run on the client and
168
+ * never on the daemon host. Data crosses the wire base64-encoded.
169
+ *
170
+ * @param command - The segment command line.
171
+ * @param input - The bytes to feed the segment.
172
+ * @returns The segment's output bytes.
173
+ * @throws {Error} When no command is executing or the surface disconnects.
174
+ */
175
+ pipe_current(command: string, input: Buffer): Promise<Buffer>;
176
+ /**
177
+ * Resolves a pending pipe segment with the surface's output.
178
+ *
179
+ * @param pipeId - The pipe correlation id.
180
+ * @param output - The base64-encoded segment output.
181
+ */
182
+ private pipeResult_settle;
183
+ /**
184
+ * Opens content in the editor of the surface running the current command
185
+ * and returns the edited result. The host installs a `localEdit` on its
186
+ * engine-side surface that calls this, so `edit` opens the operator's own
187
+ * editor and never one on the daemon host.
188
+ *
189
+ * @param content - The content to edit.
190
+ * @param extension - An optional filename extension for syntax mode.
191
+ * @returns The edited content and whether it changed.
192
+ * @throws {Error} When no command is executing or the surface disconnects.
193
+ */
194
+ edit_current(content: string, extension: string | undefined): Promise<EditOutcome>;
195
+ /**
196
+ * Resolves a pending edit with the surface's result.
197
+ *
198
+ * @param editId - The edit correlation id.
199
+ * @param outcome - The edited content and changed flag.
200
+ */
201
+ private editResult_settle;
202
+ /**
203
+ * Runs one completion request and sends its reply, or an error. Completion
204
+ * is a read and is not broadcast.
205
+ *
206
+ * @param socket - The surface to reply to.
207
+ * @param message - The completion request.
208
+ */
209
+ private complete_run;
210
+ /**
211
+ * Sends a message to a surface if the socket is still open.
212
+ *
213
+ * @param socket - The destination surface.
214
+ * @param message - The message to send.
215
+ */
216
+ private send;
217
+ }