@markmo/europacore-client 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Moloney
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.
22
+
23
+ The fonts installed through @fontsource-variable/* are each under the SIL Open
24
+ Font License 1.1, which ships with them.
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # @markmo/europacore-client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A typed client for the europacore server API (`europacore/docs/server-api.md`): its types generated from the server's own OpenAPI document and event schema, the HTTP client, and the resilient SSE reader. No framework and no dependencies; it runs in browsers and in Node 22.
4
+
5
+ The session UI (`@markmo/europacore-session-ui`) and europacorecli use it, so a change to the server's wire shapes reaches both through one regeneration.
6
+
7
+ ## Use it
8
+
9
+ ```ts
10
+ import { EuropacoreClient, isEvent, pauseOf, type Session } from '@markmo/europacore-client';
11
+
12
+ const client = new EuropacoreClient({
13
+ baseUrl: 'https://agents.example.com/v1',
14
+ headers: async () => ({ Authorization: `Bearer ${await token()}` }),
15
+ });
16
+
17
+ const session: Session = await client.createSession({ agent: 'coder' });
18
+ await client.createRun(session.id, { input: { text: 'Fix the failing test' } });
19
+
20
+ const tail = client.subscribeSession(session.id, 0, {
21
+ onEnvelope: env => {
22
+ if (isEvent(env, 'tool-call-end')) console.log(env.data.display?.kind); // data typed from the schema
23
+ },
24
+ });
25
+
26
+ const question = pauseOf(session.pause, 'question'); // QuestionPause | null
27
+ const recent = await client.runsIndex({ status: 'failed', limit: 20 }); // GET /v1/runs
28
+ const reports = await client.evals(); // GET /v1/evals
29
+ ```
30
+
31
+ `new EuropacoreClient(() => transport)` reads the transport at each call, for an app whose base URL or headers change.
32
+
33
+ ## What is in it
34
+
35
+ - `src/generated/`: written by `scripts/generate.mjs`, never by hand.
36
+ - `api.ts`: openapi-typescript's `paths`, `operations` and `components` from `schema/server-openapi.json`. Every field of a response is there: the server writes each one whole, so a value it lacks is `null`.
37
+ - `events.ts`: json-schema-to-typescript's types for every definition in `schema/server-events.schema.json`, the pauses and tool displays as `kind` unions, plus `EventData` (each event type's `data`, from `x-events`) and `FeedData` (the attention feed's).
38
+ - `requestOptional.ts`: the fields a request may leave out of each schema it uses, for `RequestBody<'CreateRun'>`.
39
+ - `src/wire.ts` (and `src/wire/`): the types under the names the clients use (`Session`, `Run`, `Pause`, `AgentInfo`, `TranscriptItem`, ...). Most are generated types; where the schema leaves a field open (`Session.pause`, `ServerInfo.capabilities`) they are typed further, and the few shapes not in the schema yet (background and fleet tasks, memories, the transcript) are written out. `src/wire/conformance.ts` checks at compile time that the hand-written ones agree with the generated ones.
40
+ - `src/client.ts` and `src/resources.ts`: `EuropacoreClient`, one method per route. `src/http.ts`: one call with a deadline, retried when it is safe to send again.
41
+ - `src/stream.ts`, `src/sse.ts`, `src/attentionFeed.ts`: the session tail (`openSse`), the SSE parser, and `GET /v1/events`.
42
+
43
+ ## Regenerate the types
44
+
45
+ The schemas are vendored from a europacore commit, named in `schema/SOURCE`:
46
+
47
+ 1. Copy `docs/server-openapi.json` and `docs/server-events.schema.json` from that europacore commit into `schema/`.
48
+ 2. Put the commit, and the date, in `schema/SOURCE`.
49
+ 3. Run `npm run generate -w @markmo/europacore-client` (in `frontend/`).
50
+ 4. Run `npm run typecheck` and `npm test`: the compiler shows each place a changed shape reaches, here and in the session UI.
51
+
52
+ `npm test` runs `scripts/generate.mjs --check`, which fails when `src/generated/` is not what the vendored schemas give. The generators are pinned (`openapi-typescript`, `json-schema-to-typescript`), so the output changes only with the schemas or a deliberate upgrade.
53
+
54
+ The size gate (`frontend/scripts/check_sizes.mjs`) skips `src/generated/`: those files are as long as the schemas they come from, and their source of truth is the generator and its input, which the gate does measure.
55
+
56
+ ## Build and publish
57
+
58
+ `npm run build -w @markmo/europacore-client` writes `dist/`: one ES module (`index.js`) and its declarations. Inside this repository, the app, the session UI and its tests read the source instead, through the `europacore-source` export condition; the session UI's own declarations read `dist`, so `npm run build:packages` builds this package first.
59
+
60
+ Pushing a tag `europacore-client-v<version>` publishes it to npm (`.github/workflows/publish-europacore-client.yml`, trusted publishing); the tag must match `version`. A session UI release that needs a new client waits for the client's.
61
+
62
+ ## Use it before it is published
63
+
64
+ From another checkout, depend on the package by path and build it once:
65
+
66
+ ```sh
67
+ npm --prefix ../europacoreui/frontend ci
68
+ npm --prefix ../europacoreui/frontend run build -w @markmo/europacore-client
69
+ npm install ../europacoreui/frontend/packages/europacore-client
70
+ ```
71
+
72
+ Its relative imports carry `.js` and `dist/` is plain ESM with declarations, so it resolves under `moduleResolution: "nodenext"` as well as `"bundler"`.
@@ -0,0 +1,16 @@
1
+ import { type ConnectionState, type SseHandle } from './stream.js';
2
+ import type { Transport } from './transport.js';
3
+ import type { AttentionFrame } from './wire.js';
4
+ export interface AttentionFeedHandlers {
5
+ onFrame: (frame: AttentionFrame) => void;
6
+ onState?: (state: ConnectionState, detail?: string) => void;
7
+ /** Not retryable (a server or proxy without the route, say): the feed is closed. */
8
+ onFatal?: (error: Error) => void;
9
+ }
10
+ /** A frame's `{v, ts, type, data}` envelope as an AttentionFrame; null for a type this client doesn't know. */
11
+ export declare function attentionFrame(env: {
12
+ type?: unknown;
13
+ data?: unknown;
14
+ }): AttentionFrame | null;
15
+ /** Follow `{baseUrl}/events`, reconnecting (for a fresh snapshot) across drops, until closed. */
16
+ export declare function openAttentionFeed(baseUrl: string, transport: Omit<Transport, 'baseUrl'>, handlers: AttentionFeedHandlers): SseHandle;
@@ -0,0 +1,139 @@
1
+ import { type AttentionFeedHandlers } from './attentionFeed.js';
2
+ import { ResourceClient } from './resources.js';
3
+ import { type ConnectionState, type SseHandle } from './stream.js';
4
+ import type { AcceptResult, AddedDir, AgentInfo, BackgroundSubagent, BackgroundTask, ClaimsView, ContextUsage, CreateRun, CreateSession, Envelope, McpServer, ModelInfo, Page, RedoResult, RestoreResult, ResumeRequest, Run, RunInspection, RunSummary, ServerCommand, ServerInfo, Session, SessionPullRequest, SessionSchedule, SessionStatus, SessionTaskEntry, SnapshotInfo, TaskInfo, TranscriptItem, UndoRequest, UndoResult, WorkspaceDiff, WorkspaceInfo } from './wire.js';
5
+ export interface StreamHandlers {
6
+ onEnvelope: (env: Envelope) => void;
7
+ onState?: (state: ConnectionState, detail?: string) => void;
8
+ onFatal?: (error: Error) => void;
9
+ }
10
+ export declare class EuropacoreClient extends ResourceClient {
11
+ info(): Promise<ServerInfo>;
12
+ agents(): Promise<AgentInfo[]>;
13
+ agent(name: string): Promise<AgentInfo>;
14
+ models(): Promise<{
15
+ models: ModelInfo[];
16
+ default: string | null;
17
+ }>;
18
+ createSession(body: CreateSession): Promise<Session>;
19
+ listSessions(query?: {
20
+ agent?: string;
21
+ status?: SessionStatus;
22
+ cursor?: string;
23
+ limit?: number;
24
+ }): Promise<Page<Session>>;
25
+ getSession(id: string): Promise<Session>;
26
+ updateSession(id: string, patch: {
27
+ title?: string;
28
+ model?: string;
29
+ metadata?: Record<string, unknown>;
30
+ permission_mode?: string;
31
+ }): Promise<Session>;
32
+ deleteSession(id: string): Promise<void>;
33
+ /** The session's sub-agents delegated in the background. */
34
+ subagents(id: string): Promise<BackgroundSubagent[]>;
35
+ /**
36
+ * The session's background tasks (those running, and those finished since its last turn started), or a fleet
37
+ * session's run: its tasks, integrator and pull request.
38
+ */
39
+ tasks(id: string): Promise<SessionTaskEntry[]>;
40
+ /** Stop a background task; resolves to it as stopped. */
41
+ stopTask(id: string, taskId: string): Promise<BackgroundTask>;
42
+ /** All transcript items, oldest first. Pages run newest first; each page is oldest first. */
43
+ messages(id: string): Promise<TranscriptItem[]>;
44
+ /** A copy of the session: its last `keep_groups` turns, or the history before `before_message`. */
45
+ forkSession(id: string, body?: {
46
+ keep_groups?: number;
47
+ before_message?: string;
48
+ title?: string | null;
49
+ }): Promise<Session>;
50
+ /** What fills the session's context window, by category. */
51
+ context(id: string): Promise<ContextUsage>;
52
+ /** Summarise the session's older turns now. */
53
+ compact(id: string, instructions?: string): Promise<Record<string, unknown>>;
54
+ /** The slash commands the session's agent runs itself. */
55
+ commands(id: string): Promise<ServerCommand[]>;
56
+ /** The session's runs from its journal, oldest first. */
57
+ runs(id: string): Promise<RunSummary[]>;
58
+ /** One run step by step: tokens, cache and cost per call, tool calls with arguments and results, events. */
59
+ runSteps(id: string, runId: string): Promise<RunInspection>;
60
+ /** The session's agent's MCP servers and how each is doing. */
61
+ mcpServers(id: string): Promise<McpServer[]>;
62
+ /** The session's cron schedule; null when it has none. */
63
+ schedule(id: string): Promise<SessionSchedule | null>;
64
+ /** Set the session's schedule (replacing any); a 422 `invalid_schedule` says what is wrong with it. */
65
+ setSchedule(id: string, body: {
66
+ cron: string;
67
+ prompt: string;
68
+ timezone?: string;
69
+ }): Promise<SessionSchedule>;
70
+ /** Clear the session's schedule. */
71
+ clearSchedule(id: string): Promise<void>;
72
+ /** Connect an enabled MCP server again, after it failed or its process died. */
73
+ reconnectMcp(id: string, name: string): Promise<{
74
+ server: string;
75
+ tools: string[];
76
+ }>;
77
+ /** The session's file claims, the other sessions in its repository with theirs, and requests to or from it. */
78
+ claims(id: string): Promise<ClaimsView>;
79
+ /**
80
+ * Let go of one of your own sessions' claims (every one without `paths`), or with `to` hand them to another
81
+ * session. What follows arrives as `claim.resolved` and in the sessions' `metadata.claims`.
82
+ */
83
+ releaseClaims(id: string, body: {
84
+ paths?: string[];
85
+ to?: string;
86
+ note?: string;
87
+ }): Promise<ClaimsView>;
88
+ /** Ask, on behalf of your own session, for paths another session holds; the request arrives as `claim.requested`. */
89
+ requestClaims(id: string, body: {
90
+ paths: string[];
91
+ reason?: string;
92
+ }): Promise<ClaimsView>;
93
+ /** The session's workspace: branch and the files changed since `base` (HEAD by default). */
94
+ sessionWorkspace(sessionId: string, base?: string): Promise<WorkspaceInfo>;
95
+ /** A unified diff of the session's workspace; one file when `path` (repository-relative) is given. */
96
+ workspaceDiff(sessionId: string, options?: {
97
+ path?: string;
98
+ base?: string;
99
+ task?: string;
100
+ }): Promise<WorkspaceDiff>;
101
+ /** Task worktrees started from the session's branch (its delegated coders). */
102
+ sessionTasks(sessionId: string): Promise<TaskInfo[]>;
103
+ /** Fast-forward the session worktree's base branch to it; `message` commits leftovers first. */
104
+ acceptWorkspace(sessionId: string, message?: string): Promise<AcceptResult>;
105
+ /** Remove the session's worktree and branch; `force` discards unmerged work. */
106
+ discardWorkspace(sessionId: string, force?: boolean): Promise<void>;
107
+ /** The session's workspace snapshots, oldest first: one per turn, and one before each restore. */
108
+ listSnapshots(sessionId: string): Promise<SnapshotInfo[]>;
109
+ /** Put the workspace's files (those under `paths`, or all) back as snapshot `id` has them. */
110
+ restoreSnapshot(sessionId: string, id: number, paths?: string[]): Promise<RestoreResult>;
111
+ /** Undo the session's last turn (or `turns`, back to before `to`, or `all`): the conversation and its files. */
112
+ undo(sessionId: string, body?: UndoRequest): Promise<UndoResult>;
113
+ /** Put back what the newest undo since the session's last turn took away. */
114
+ redo(sessionId: string): Promise<RedoResult>;
115
+ /** Let the session's agent use `path` (under the user's workspace root) too; `writable` lets it write there. */
116
+ addDir(sessionId: string, path: string, writable?: boolean): Promise<AddedDir[]>;
117
+ /** Stop the session's agent using an added directory. */
118
+ removeDir(sessionId: string, path: string): Promise<AddedDir[]>;
119
+ /** The pull requests a session opened or registered. */
120
+ pullRequests(sessionId: string): Promise<SessionPullRequest[]>;
121
+ /** Register a pull request the session works on (one opened elsewhere), following its CI and reviews. */
122
+ addPullRequest(sessionId: string, ref: {
123
+ url?: string;
124
+ number?: number;
125
+ }): Promise<SessionPullRequest>;
126
+ /** Follow a session's pull request's CI and reviews, or stop. */
127
+ followPullRequest(sessionId: string, number: number, follow: boolean): Promise<SessionPullRequest>;
128
+ createRun(sessionId: string, body: CreateRun): Promise<Run>;
129
+ resume(sessionId: string, body: ResumeRequest): Promise<Run>;
130
+ getRun(runId: string): Promise<Run>;
131
+ cancelRun(runId: string, reason?: string): Promise<Run>;
132
+ steer(runId: string, text: string, commandId?: string): Promise<{
133
+ delivered: boolean;
134
+ }>;
135
+ /** Session tail: replays after `after` (omit for live-only), then follows every run. */
136
+ subscribeSession(sessionId: string, after: number | string | undefined, handlers: StreamHandlers): SseHandle;
137
+ /** The attention feed (`capabilities.attention_feed`): a snapshot of the caller's sessions, then each change. */
138
+ subscribeAttention(handlers: AttentionFeedHandlers): SseHandle;
139
+ }
@@ -0,0 +1,14 @@
1
+ import type { Problem } from './wire.js';
2
+ /** An HTTP failure from the server, carrying the RFC 9457 problem body when there is one. */
3
+ export declare class ApiError extends Error {
4
+ readonly status: number;
5
+ readonly code: string;
6
+ readonly problem: Problem;
7
+ constructor(problem: Problem);
8
+ }
9
+ /** The server didn't answer a request in time: hung, or too busy to. */
10
+ export declare class RequestTimeoutError extends Error {
11
+ constructor(method: string, path: string, timeoutMs: number);
12
+ }
13
+ export declare function problemFromResponse(res: Response): Promise<ApiError>;
14
+ export declare function describeError(error: unknown): string;